fix(tradein/v2): a11y P3a — нативные контролы + модальный диалог + aria-live (#2062)
CRITICAL-доступность (часть 1): клавиатура/скринридер. Без визуальных изменений (chrome-reset кнопок вынесен в CSS-классы, :hover и font-size сохранены). - Клик-only <div> → нативные <button>: nav-табы (+aria-current), user-меню (aria-haspopup/expanded + Esc-close), «Выйти», «Подробнее →» (ResultPanel, +aria-label по разделу), строки 03 СВОДКИ (ObjectSummary, +aria-label). - SectionOverlay → настоящая модалка: role=dialog, aria-modal, aria-labelledby, Esc-close, фокус на диалог при открытии, focus-trap (Tab/Shift+Tab), возврат фокуса на триггер при закрытии; «← К ОЦЕНКЕ» → <button>. - page.tsx: aria-live="polite" role=status — «Идёт расчёт… / Оценка готова / Недостаточно данных / Ошибка»; фон (content-wrap) inert когда открыт оверлей или drawer (overlay/drawer остаются интерактивны). Остаётся P3b: ParamsPanel-форма (Dd→listbox+клавиши, БАЛКОН→radiogroup, input <label>/aria) — заход «с клавиатуры можно завершить оценку».
This commit is contained in:
parent
0641183ecd
commit
7066117cb8
5 changed files with 205 additions and 26 deletions
|
|
@ -617,6 +617,19 @@ export default function TradeInV2Page() {
|
||||||
rightContent = <SummaryPlaceholder />;
|
rightContent = <SummaryPlaceholder />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Polite SR announcement reflecting the estimate lifecycle. Same precedence as
|
||||||
|
// middleContent: loading → error → insufficient → ready → silent.
|
||||||
|
let statusMessage = "";
|
||||||
|
if (loading) {
|
||||||
|
statusMessage = "Идёт расчёт оценки…";
|
||||||
|
} else if (restoreError) {
|
||||||
|
statusMessage = "Ошибка загрузки отчёта";
|
||||||
|
} else if (estimate && insufficient) {
|
||||||
|
statusMessage = "Недостаточно данных для оценки";
|
||||||
|
} else if (estimate && !insufficient) {
|
||||||
|
statusMessage = "Оценка готова";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: 1536 * artboardScale, height: 1024 * artboardScale }}>
|
<div style={{ width: 1536 * artboardScale, height: 1024 * artboardScale }}>
|
||||||
<div
|
<div
|
||||||
|
|
@ -634,6 +647,25 @@ export default function TradeInV2Page() {
|
||||||
>
|
>
|
||||||
<style>{HELPER_STYLES}</style>
|
<style>{HELPER_STYLES}</style>
|
||||||
|
|
||||||
|
{/* Polite, visually-hidden status announcer for SR users. */}
|
||||||
|
<div
|
||||||
|
aria-live="polite"
|
||||||
|
role="status"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
padding: 0,
|
||||||
|
margin: -1,
|
||||||
|
overflow: "hidden",
|
||||||
|
clip: "rect(0 0 0 0)",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
border: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{statusMessage}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* OUTER HUD FRAME */}
|
{/* OUTER HUD FRAME */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -661,6 +693,7 @@ export default function TradeInV2Page() {
|
||||||
|
|
||||||
{/* CONTENT WRAP */}
|
{/* CONTENT WRAP */}
|
||||||
<div
|
<div
|
||||||
|
inert={nav !== 0 || drawerOpen ? true : undefined}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
inset: 14,
|
inset: 14,
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export function ObjectSummary({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<style>{`
|
<style>{`
|
||||||
.os-row { transition: background .12s; }
|
.os-row { background: none; border: none; transition: background .12s; }
|
||||||
.os-row:hover { background: rgba(46,139,255,.07); }
|
.os-row:hover { background: rgba(46,139,255,.07); }
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
|
|
@ -159,21 +159,26 @@ export function ObjectSummary({
|
||||||
{/* Per-section totals (clickable) */}
|
{/* Per-section totals (clickable) */}
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
|
||||||
{data.summary.rows.map((row, i) => (
|
{data.summary.rows.map((row, i) => (
|
||||||
<div
|
<button
|
||||||
key={row.label}
|
key={row.label}
|
||||||
|
type="button"
|
||||||
className="os-row"
|
className="os-row"
|
||||||
onClick={() => onNavigate(row.nav)}
|
onClick={() => onNavigate(row.nav)}
|
||||||
|
aria-label={`${row.label} — подробнее`}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: "9px 6px",
|
padding: "9px 6px",
|
||||||
|
cursor: "pointer",
|
||||||
|
borderRadius: 5,
|
||||||
|
width: "100%",
|
||||||
|
fontFamily: "inherit",
|
||||||
|
textAlign: "left",
|
||||||
borderBottom:
|
borderBottom:
|
||||||
i < data.summary.rows.length - 1
|
i < data.summary.rows.length - 1
|
||||||
? `1px dotted ${tokens.lineGradient}`
|
? `1px dotted ${tokens.lineGradient}`
|
||||||
: undefined,
|
: undefined,
|
||||||
cursor: "pointer",
|
|
||||||
borderRadius: 5,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|
@ -211,7 +216,7 @@ export function ObjectSummary({
|
||||||
</span>
|
</span>
|
||||||
<span style={{ color: tokens.accent, fontSize: 11 }}>›</span>
|
<span style={{ color: tokens.accent, fontSize: 11 }}>›</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -273,9 +273,11 @@ export default function ResultPanel({
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
className="rp-more"
|
className="rp-more"
|
||||||
onClick={() => onNavigate(card.nav)}
|
onClick={() => onNavigate(card.nav)}
|
||||||
|
aria-label={`Подробнее — ${card.title.join(" ")}`}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|
@ -285,18 +287,24 @@ export default function ResultPanel({
|
||||||
letterSpacing: 0.3,
|
letterSpacing: 0.3,
|
||||||
color: accent,
|
color: accent,
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
fontFamily: "inherit",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Подробнее <span>→</span>
|
Подробнее <span>→</span>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
className="rp-more"
|
className="rp-more"
|
||||||
onClick={() => onNavigate(card.nav)}
|
onClick={() => onNavigate(card.nav)}
|
||||||
|
aria-label={`Подробнее — ${card.title.join(" ")}`}
|
||||||
style={{
|
style={{
|
||||||
marginTop: 16,
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
|
|
@ -305,10 +313,17 @@ export default function ResultPanel({
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
letterSpacing: 0.3,
|
letterSpacing: 0.3,
|
||||||
color: accent,
|
color: accent,
|
||||||
|
width: "100%",
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
marginTop: 16,
|
||||||
|
fontFamily: "inherit",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Подробнее <span>→</span>
|
Подробнее <span>→</span>
|
||||||
</div>
|
</button>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
|
@ -635,9 +650,11 @@ export default function ResultPanel({
|
||||||
объект
|
объект
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
className="rp-more"
|
className="rp-more"
|
||||||
onClick={() => onNavigate(3)}
|
onClick={() => onNavigate(3)}
|
||||||
|
aria-label="Подробнее — цена и срок продажи"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|
@ -646,11 +663,16 @@ export default function ResultPanel({
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
letterSpacing: 0.3,
|
letterSpacing: 0.3,
|
||||||
color: accent,
|
color: accent,
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
|
fontFamily: "inherit",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Подробнее <span>→</span>
|
Подробнее <span>→</span>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* right range — deals */}
|
{/* right range — deals */}
|
||||||
|
|
@ -756,9 +778,11 @@ export default function ResultPanel({
|
||||||
>
|
>
|
||||||
ИСТОЧНИКИ ДАННЫХ
|
ИСТОЧНИКИ ДАННЫХ
|
||||||
</span>
|
</span>
|
||||||
<span
|
<button
|
||||||
|
type="button"
|
||||||
className="rp-more"
|
className="rp-more"
|
||||||
onClick={() => onNavigate(2)}
|
onClick={() => onNavigate(2)}
|
||||||
|
aria-label="Подробнее — источники данных"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|
@ -767,10 +791,15 @@ export default function ResultPanel({
|
||||||
fontSize: 9.5,
|
fontSize: 9.5,
|
||||||
letterSpacing: 0.3,
|
letterSpacing: 0.3,
|
||||||
color: accent,
|
color: accent,
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
fontFamily: "inherit",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Подробнее <span>→</span>
|
Подробнее <span>→</span>
|
||||||
</span>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
// with HUD corner brackets, a header (section number + title + "← К ОЦЕНКЕ"
|
// with HUD corner brackets, a header (section number + title + "← К ОЦЕНКЕ"
|
||||||
// button) and a scrollable body that swaps the active view.
|
// button) and a scrollable body that swaps the active view.
|
||||||
|
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
import { tokens } from "./tokens";
|
import { tokens } from "./tokens";
|
||||||
import { overlayNums, overlayTitles } from "./fixtures";
|
import { overlayNums, overlayTitles } from "./fixtures";
|
||||||
import HistoryView from "./HistoryView";
|
import HistoryView from "./HistoryView";
|
||||||
|
|
@ -37,8 +39,78 @@ export default function SectionOverlay({
|
||||||
analytics,
|
analytics,
|
||||||
cache,
|
cache,
|
||||||
}: SectionOverlayProps) {
|
}: SectionOverlayProps) {
|
||||||
|
// Modal dialog plumbing. `dialogRef` is the dialog root, `lastFocused` keeps
|
||||||
|
// the element that had focus before the overlay opened so we can restore it on
|
||||||
|
// close, and `onCloseRef` holds the latest onClose so the mount-only effect
|
||||||
|
// (deps []) never re-runs / re-traps focus on parent re-renders even though
|
||||||
|
// page.tsx passes a fresh arrow each render.
|
||||||
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
|
const lastFocused = useRef<Element | null>(null);
|
||||||
|
const onCloseRef = useRef(onClose);
|
||||||
|
onCloseRef.current = onClose;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const dialog = dialogRef.current;
|
||||||
|
// Remember the trigger, then move focus into the dialog.
|
||||||
|
lastFocused.current = document.activeElement;
|
||||||
|
dialog?.focus();
|
||||||
|
|
||||||
|
function getFocusable(): HTMLElement[] {
|
||||||
|
if (!dialog) return [];
|
||||||
|
const nodes = dialog.querySelectorAll<HTMLElement>(
|
||||||
|
'button, [href], input, select, [tabindex]:not([tabindex="-1"])',
|
||||||
|
);
|
||||||
|
return Array.from(nodes).filter((el) => !el.hasAttribute("disabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
onCloseRef.current();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key !== "Tab" || !dialog) return;
|
||||||
|
|
||||||
|
const focusable = getFocusable();
|
||||||
|
if (focusable.length === 0) {
|
||||||
|
// Nothing tabbable inside — keep focus pinned on the dialog itself.
|
||||||
|
e.preventDefault();
|
||||||
|
dialog.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = focusable[0];
|
||||||
|
const last = focusable[focusable.length - 1];
|
||||||
|
const activeEl = document.activeElement;
|
||||||
|
const inside = dialog.contains(activeEl);
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
if (activeEl === first || activeEl === dialog || !inside) {
|
||||||
|
e.preventDefault();
|
||||||
|
last.focus();
|
||||||
|
}
|
||||||
|
} else if (activeEl === last || !inside) {
|
||||||
|
e.preventDefault();
|
||||||
|
first.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("keydown", onKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", onKeyDown);
|
||||||
|
// Return focus to whatever opened the overlay.
|
||||||
|
(lastFocused.current as HTMLElement | null)?.focus?.();
|
||||||
|
};
|
||||||
|
// Mount-only: refs (dialogRef/onCloseRef) are stable, so no extra deps.
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={dialogRef}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="v2-overlay-title"
|
||||||
|
tabIndex={-1}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: 28,
|
left: 28,
|
||||||
|
|
@ -59,7 +131,7 @@ export default function SectionOverlay({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<style>{`
|
<style>{`
|
||||||
.tiv2-so-back { transition: all .15s; }
|
.tiv2-so-back { color: ${tokens.ink}; transition: all .15s; }
|
||||||
.tiv2-so-back:hover { border-color: ${tokens.accent}; color: ${tokens.accent}; }
|
.tiv2-so-back:hover { border-color: ${tokens.accent}; color: ${tokens.accent}; }
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
|
|
@ -111,6 +183,7 @@ export default function SectionOverlay({
|
||||||
{overlayNums[active]}
|
{overlayNums[active]}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
id="v2-overlay-title"
|
||||||
style={{
|
style={{
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
|
|
@ -121,7 +194,8 @@ export default function SectionOverlay({
|
||||||
{overlayTitles[active]}
|
{overlayTitles[active]}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="tiv2-so-back"
|
className="tiv2-so-back"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -133,6 +207,9 @@ export default function SectionOverlay({
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
padding: "6px 12px",
|
padding: "6px 12px",
|
||||||
background: tokens.surface.w60,
|
background: tokens.surface.w60,
|
||||||
|
// reset UA button defaults while keeping the original box look;
|
||||||
|
// base colour lives in .tiv2-so-back so :hover can win
|
||||||
|
fontFamily: "inherit",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: 13 }}>←</span>
|
<span style={{ fontSize: 13 }}>←</span>
|
||||||
|
|
@ -141,7 +218,7 @@ export default function SectionOverlay({
|
||||||
>
|
>
|
||||||
К ОЦЕНКЕ
|
К ОЦЕНКЕ
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* overlay body */}
|
{/* overlay body */}
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,10 @@ export default function TopNav({
|
||||||
.tnav-tab { color: ${tokens.muted}; font-weight: 400; cursor: pointer; transition: color .2s; }
|
.tnav-tab { color: ${tokens.muted}; font-weight: 400; cursor: pointer; transition: color .2s; }
|
||||||
.tnav-tab:hover { color: ${tokens.ink}; }
|
.tnav-tab:hover { color: ${tokens.ink}; }
|
||||||
.tnav-tab--active { color: ${tokens.ink}; font-weight: 600; }
|
.tnav-tab--active { color: ${tokens.ink}; font-weight: 600; }
|
||||||
|
.tnav-userbtn { background: none; border: none; }
|
||||||
.tnav-userbtn:hover { background: rgba(46,139,255,.07); }
|
.tnav-userbtn:hover { background: rgba(46,139,255,.07); }
|
||||||
.tnav-menuitem:hover { background: rgba(46,139,255,.09); }
|
.tnav-menuitem:hover { background: rgba(46,139,255,.09); }
|
||||||
|
.tnav-logout { background: none; border: none; }
|
||||||
.tnav-logout:hover { background: rgba(205,104,104,.1); }
|
.tnav-logout:hover { background: rgba(205,104,104,.1); }
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
|
|
@ -184,11 +186,27 @@ export default function TopNav({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{navLabels.map((label, i) => (
|
{navLabels.map((label, i) => (
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
key={label}
|
key={label}
|
||||||
onClick={() => onNavigate(i)}
|
onClick={() => onNavigate(i)}
|
||||||
|
aria-current={active === i ? "page" : undefined}
|
||||||
className={`tnav-tab${active === i ? " tnav-tab--active" : ""}`}
|
className={`tnav-tab${active === i ? " tnav-tab--active" : ""}`}
|
||||||
style={{ position: "relative", whiteSpace: "nowrap" }}
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
// NB: family/size/letterSpacing inherit for chrome reset, but do
|
||||||
|
// NOT set `font`/`color` shorthand — the .tnav-tab(--active)/:hover
|
||||||
|
// CSS must keep driving tab colour + weight.
|
||||||
|
fontFamily: "inherit",
|
||||||
|
fontSize: "inherit",
|
||||||
|
letterSpacing: "inherit",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
<div
|
<div
|
||||||
|
|
@ -220,15 +238,23 @@ export default function TopNav({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User menu */}
|
{/* User menu */}
|
||||||
<div style={{ position: "relative" }}>
|
<div
|
||||||
<div
|
style={{ position: "relative" }}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Escape") setUserOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setUserOpen((o) => !o)}
|
onClick={() => setUserOpen((o) => !o)}
|
||||||
className="tnav-userbtn"
|
className="tnav-userbtn"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={userOpen}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|
@ -237,6 +263,8 @@ export default function TopNav({
|
||||||
padding: "5px 8px 5px 6px",
|
padding: "5px 8px 5px 6px",
|
||||||
borderRadius: "9px",
|
borderRadius: "9px",
|
||||||
transition: "background .15s",
|
transition: "background .15s",
|
||||||
|
fontFamily: "inherit",
|
||||||
|
textAlign: "left",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -281,7 +309,7 @@ export default function TopNav({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span style={{ color: tokens.muted2, fontSize: "9px" }}>▾</span>
|
<span style={{ color: tokens.muted2, fontSize: "9px" }}>▾</span>
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
{userOpen && (
|
{userOpen && (
|
||||||
<div
|
<div
|
||||||
|
|
@ -425,13 +453,20 @@ export default function TopNav({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUserOpen(false);
|
setUserOpen(false);
|
||||||
onLogout?.();
|
onLogout?.();
|
||||||
}}
|
}}
|
||||||
className="tnav-logout"
|
className="tnav-logout"
|
||||||
style={{ ...menuItemStyle, color: tokens.danger }}
|
style={{
|
||||||
|
...menuItemStyle,
|
||||||
|
color: tokens.danger,
|
||||||
|
width: "100%",
|
||||||
|
fontFamily: "inherit",
|
||||||
|
textAlign: "left",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none">
|
<svg width="15" height="15" viewBox="0 0 15 15" fill="none">
|
||||||
<path
|
<path
|
||||||
|
|
@ -441,7 +476,7 @@ export default function TopNav({
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Выйти
|
Выйти
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue