From 51f9c8d0ed5aa0722d5928dab36401cfa340d49e Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 8 Aug 2026 04:27:51 +0200 Subject: [PATCH 1/4] =?UTF-8?q?feat(a11y):=20R9=20focus=20foundation=20?= =?UTF-8?q?=E2=80=94=20glass=20shadow=20fix,=20focus-visible=20ring,=20hig?= =?UTF-8?q?hlight/selected=20split?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit S1 — recipe foundation: - Move .glass/.glass-card/.glass-menu into @layer components so a JSX shadow-* utility overrides the glass box-shadow again; the ~9 dead shadow-* on glass surfaces (modals, menus, save-bar, chat) now paint their intended drop-shadow. Perf-mode and :hover overrides stay unlayered so they still win. - Add a .selectable recipe: the hover tint menu/list rows already use, PLUS the app-wide focus-visible ring that was missing on non-input controls (the biggest a11y gap). - Split the popover roving highlight from the selected state (AnchoredPopover): a selected/checked PopoverItem now gets a ring ONLY, so its resting paint (bg-accent fill / text-accent / checkbox) survives being highlighted instead of downgrading to a 35% tint or being recoloured. Fixes the 4 review findings deferred from R10-S0 generically; drops the leftover per-consumer data-active ring in tableHeader. S2 — worst offenders: - Account-menu trigger + switch/add/sign-out rows adopt .selectable (visible keyboard focus). - Saved-views rows: group-focus-within reveals the hover-only action buttons for keyboard users; aria-current on the active row; focus-visible ring on every button. - The 3 invisible-focus inputs get an accent focus ring: SearchBar + ShareDialog via focus-within on the glass pill/row, AboutCell via focus-visible on the trigger. Deferred to a later R9 sprint: the full unification of all ~14 selection idioms, a shared , and .is-current/.chip recipes (no consumer yet in this pass). Verified: tsc/eslint/prettier/knip/vitest (79) green; browser computed-style checks confirm the glass shadow now wins, the focus outlines render (2px accent), and the split renders (selected ring-only, unselected fill). --- frontend/src/components/AboutCell.tsx | 2 +- frontend/src/components/AnchoredPopover.tsx | 14 +- frontend/src/components/NavSidebar.tsx | 8 +- frontend/src/components/SavedViewsWidget.tsx | 15 +- frontend/src/components/SearchBar.tsx | 2 +- frontend/src/components/ShareDialog.tsx | 2 +- frontend/src/components/tableHeader.tsx | 4 +- frontend/src/index.css | 139 +++++++++++-------- 8 files changed, 108 insertions(+), 78 deletions(-) diff --git a/frontend/src/components/AboutCell.tsx b/frontend/src/components/AboutCell.tsx index e2af1a75..9ddb995f 100644 --- a/frontend/src/components/AboutCell.tsx +++ b/frontend/src/components/AboutCell.tsx @@ -45,7 +45,7 @@ export default function AboutCell({ text, channelId }: { text: string | null; ch ref={pop.refs.setReference} {...pop.getReferenceProps()} tabIndex={0} - className="block max-w-88 truncate text-muted cursor-help outline-hidden" + className="block max-w-88 truncate text-muted cursor-help rounded-sm outline-hidden focus-visible:outline-2 focus-visible:outline-accent/60 focus-visible:outline-offset-2" > {preview} diff --git a/frontend/src/components/AnchoredPopover.tsx b/frontend/src/components/AnchoredPopover.tsx index f807375d..f4c173fe 100644 --- a/frontend/src/components/AnchoredPopover.tsx +++ b/frontend/src/components/AnchoredPopover.tsx @@ -250,11 +250,15 @@ type ItemRole = "menuitem" | "menuitemradio" | "menuitemcheckbox" | "option"; * focus-visible heuristic, so a `:focus-visible` highlight can stay invisible until the first arrow * key. (Since the Tailwind v4 migration `bg-accent/NN` works — v4 does the opacity modifier via * color-mix — so the recipe no longer needs a hand-written color-mix arbitrary value.) */ -// Applied to EVERY PopoverItem so the roving-focus row is clearly visible across all schemes: an -// accent fill plus a 1px inset accent ring for a defined edge. (The previous per-consumer 24% fill -// alone read as barely-there — a real UAT finding, 2026-08-08.) -const ACTIVE_HIGHLIGHT = +// The roving-focus highlight, SPLIT by selection state (R9). A NON-selected row gets the familiar +// accent fill + inset ring. A SELECTED/checked row gets ring-ONLY, so the consumer's own resting +// selected paint (a bg-accent fill, text-accent, a checkbox) SURVIVES being highlighted instead of +// being downgraded to a 35% tint or having its text recoloured (the R10-S0 review findings). The two +// can't be merged onto one recipe: the fill and the selected paint both drive background/color on the +// same element, so the highlight must yield to the selected state rather than overwrite it. +const HIGHLIGHT_UNSELECTED = "data-active:bg-accent/35 data-active:text-fg data-active:ring-1 data-active:ring-inset data-active:ring-accent/60"; +const HIGHLIGHT_SELECTED = "data-active:ring-2 data-active:ring-inset data-active:ring-accent/70"; export function PopoverItem({ children, @@ -323,7 +327,7 @@ export function PopoverItem({ tabIndex={focusActive ? 0 : -1} data-active={visualActive ? "true" : undefined} disabled={disabled} - className={`${ACTIVE_HIGHLIGHT}${className ? ` ${className}` : ""}`} + className={`${checked || selected ? HIGHLIGHT_SELECTED : HIGHLIGHT_UNSELECTED}${className ? ` ${className}` : ""}`} > {children} diff --git a/frontend/src/components/NavSidebar.tsx b/frontend/src/components/NavSidebar.tsx index d9cac4c0..15c7d638 100644 --- a/frontend/src/components/NavSidebar.tsx +++ b/frontend/src/components/NavSidebar.tsx @@ -322,7 +322,7 @@ export default function NavSidebar({ ref={acctPop.refs.setReference} {...acctPop.getReferenceProps()} title={slim ? `${name} · ${roleLabel}` : undefined} - className={`${rowBase} ${slim ? "justify-center px-0" : ""} text-muted hover:text-fg hover:bg-card`} + className={`${rowBase} selectable ${slim ? "justify-center px-0" : ""} text-muted hover:text-fg hover:bg-card`} > switchTo(a.id)} title={t("header.account.switchTo", { name: a.display_name ?? a.email })} - className="w-full flex items-center gap-2.5 px-2 py-1.5 rounded-lg hover:bg-card transition" + className="selectable w-full flex items-center gap-2.5 px-2 py-1.5 rounded-lg hover:bg-card transition" > {t("header.account.addAccount")} diff --git a/frontend/src/components/SearchBar.tsx b/frontend/src/components/SearchBar.tsx index 7b2983bd..2694d3bb 100644 --- a/frontend/src/components/SearchBar.tsx +++ b/frontend/src/components/SearchBar.tsx @@ -31,7 +31,7 @@ export default function SearchBar({ if (onGo && !goDisabled) onGo(); }; return ( -
+
void } return (
-
+
({ checked={!filterOn} onSelect={() => onApply(col.key, "")} label={t("datatable.all")} - className={`text-left text-xs px-2 py-1.5 rounded-md outline-hidden transition data-active:ring-1 data-active:ring-accent ${ + className={`text-left text-xs px-2 py-1.5 rounded-md outline-hidden transition ${ !filterOn ? "bg-accent text-accent-fg" : "text-muted hover:bg-card" }`} > @@ -171,7 +171,7 @@ function FilterFunnel({ checked={value === o.value} onSelect={() => onApply(col.key, o.value)} label={o.label} - className={`text-left text-xs px-2 py-1.5 rounded-md outline-hidden transition data-active:ring-1 data-active:ring-accent ${ + className={`text-left text-xs px-2 py-1.5 rounded-md outline-hidden transition ${ value === o.value ? "bg-accent text-accent-fg" : "text-muted hover:bg-card" }`} > diff --git a/frontend/src/index.css b/frontend/src/index.css index 300cf9bf..f44109df 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -265,69 +265,94 @@ body { } /* ===== Liquid-glass surface system (theme-aware, GPU-light) ===== */ -.glass { - /* Frosted overlay surface: translucent enough that the blurred backdrop shows +/* In @layer components so a JSX shadow-* utility (which lives in @layer utilities, always above + components) can override a glass surface's default box-shadow again. Before v4/R9 these rules were + unlayered and always won, so ~9 `shadow-*` on glass surfaces were silent no-ops. The perf-mode and + :hover overrides below stay UNLAYERED on purpose (they must beat the utility). */ +@layer components { + .glass { + /* Frosted overlay surface: translucent enough that the blurred backdrop shows through (the glassy look), opaque enough that the background isn't distracting. The strong blur turns whatever is behind into soft colour, not a sharp image. Layer 1 (top) = the optional scrim; layer 2 = the translucent surface tint. */ - background: - linear-gradient( - color-mix(in srgb, var(--bg) var(--glass-scrim), transparent), - color-mix(in srgb, var(--bg) var(--glass-scrim), transparent) - ), - color-mix(in srgb, var(--surface) var(--glass-surface-alpha), transparent); - backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); - -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); - border: 1px solid color-mix(in srgb, var(--border) var(--glass-border-alpha), transparent); - border-top-color: color-mix( - in srgb, - #fff var(--glass-edge), - color-mix(in srgb, var(--border) var(--glass-border-alpha), transparent) - ); - box-shadow: - inset 0 1px 0 color-mix(in srgb, #fff var(--glass-inset), transparent), - 0 18px 44px -16px rgba(0, 0, 0, 0.6); -} -.glass-card { - /* No backdrop-filter here: cards render in bulk (feed grid) over a solid background + background: + linear-gradient( + color-mix(in srgb, var(--bg) var(--glass-scrim), transparent), + color-mix(in srgb, var(--bg) var(--glass-scrim), transparent) + ), + color-mix(in srgb, var(--surface) var(--glass-surface-alpha), transparent); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 1px solid color-mix(in srgb, var(--border) var(--glass-border-alpha), transparent); + border-top-color: color-mix( + in srgb, + #fff var(--glass-edge), + color-mix(in srgb, var(--border) var(--glass-border-alpha), transparent) + ); + box-shadow: + inset 0 1px 0 color-mix(in srgb, #fff var(--glass-inset), transparent), + 0 18px 44px -16px rgba(0, 0, 0, 0.6); + } + .glass-card { + /* No backdrop-filter here: cards render in bulk (feed grid) over a solid background where blur adds almost nothing visually but is GPU-expensive and triggers reflow. Translucency + border + depth keep the look; blur is reserved for .glass overlays. */ - background: - linear-gradient( - color-mix(in srgb, var(--bg) var(--glass-scrim), transparent), - color-mix(in srgb, var(--bg) var(--glass-scrim), transparent) - ), - color-mix(in srgb, var(--card) var(--glass-card-alpha), transparent); - border: 1px solid color-mix(in srgb, var(--border) 65%, transparent); - border-top-color: color-mix( - in srgb, - #fff var(--glass-edge), - color-mix(in srgb, var(--border) 65%, transparent) - ); - box-shadow: - inset 0 1px 0 color-mix(in srgb, #fff calc(var(--glass-inset) / 2), transparent), - 0 8px 22px -14px rgba(0, 0, 0, 0.45); -} -.glass-menu { - /* Floating menus/popovers hover over undimmed content (no backdrop scrim like dialogs), + background: + linear-gradient( + color-mix(in srgb, var(--bg) var(--glass-scrim), transparent), + color-mix(in srgb, var(--bg) var(--glass-scrim), transparent) + ), + color-mix(in srgb, var(--card) var(--glass-card-alpha), transparent); + border: 1px solid color-mix(in srgb, var(--border) 65%, transparent); + border-top-color: color-mix( + in srgb, + #fff var(--glass-edge), + color-mix(in srgb, var(--border) 65%, transparent) + ); + box-shadow: + inset 0 1px 0 color-mix(in srgb, #fff calc(var(--glass-inset) / 2), transparent), + 0 8px 22px -14px rgba(0, 0, 0, 0.45); + } + .glass-menu { + /* Floating menus/popovers hover over undimmed content (no backdrop scrim like dialogs), so they must be near-opaque to stay readable — only a hint of translucency + the blur. */ - background: - linear-gradient( - color-mix(in srgb, var(--bg) var(--glass-scrim), transparent), - color-mix(in srgb, var(--bg) var(--glass-scrim), transparent) - ), - color-mix(in srgb, var(--surface) var(--glass-menu-alpha), transparent); - backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); - -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); - border: 1px solid color-mix(in srgb, var(--border) 80%, transparent); - border-top-color: color-mix( - in srgb, - #fff var(--glass-edge), - color-mix(in srgb, var(--border) 80%, transparent) - ); - box-shadow: - inset 0 1px 0 color-mix(in srgb, #fff calc(var(--glass-inset) - 2%), transparent), - 0 18px 44px -16px rgba(0, 0, 0, 0.6); + background: + linear-gradient( + color-mix(in srgb, var(--bg) var(--glass-scrim), transparent), + color-mix(in srgb, var(--bg) var(--glass-scrim), transparent) + ), + color-mix(in srgb, var(--surface) var(--glass-menu-alpha), transparent); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 1px solid color-mix(in srgb, var(--border) 80%, transparent); + border-top-color: color-mix( + in srgb, + #fff var(--glass-edge), + color-mix(in srgb, var(--border) 80%, transparent) + ); + box-shadow: + inset 0 1px 0 color-mix(in srgb, #fff calc(var(--glass-inset) - 2%), transparent), + 0 18px 44px -16px rgba(0, 0, 0, 0.6); + } + + /* R9: standard interactive-row recipe. The hover tint menu/list rows already use, PLUS the + app-wide focus-visible ring that was missing on non-input controls (buttons, rows, menu items) — + the biggest single a11y win. In @layer components so a consumer's own utilities still win; the + accent ring renders correctly under v4 (color-mix), matching the input ring in ui/form.tsx. */ + .selectable { + transition: + background-color 0.15s ease, + color 0.15s ease; + } + .selectable:hover { + background-color: var(--card); + } + .selectable:focus-visible { + /* outline (not a box-shadow ring) so it never fights a glass surface's box-shadow and never gets + clipped by an overflow-hidden ancestor; inset offset keeps it inside tightly-packed rows. */ + outline: 2px solid color-mix(in srgb, var(--accent) 60%, transparent); + outline-offset: -2px; + } } .glass-hover:hover { border-color: color-mix(in srgb, var(--accent) 55%, transparent); From c11e2822e9904c0af3b4f0fe3c10f4d96b694b25 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 8 Aug 2026 04:51:02 +0200 Subject: [PATCH 2/4] =?UTF-8?q?feat(a11y):=20R9=20focus=20foundation=20?= =?UTF-8?q?=E2=80=94=20app-wide=20focus-visible=20fallback=20+=20nav=20rai?= =?UTF-8?q?l=20focus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Global :focus-visible fallback (@layer base): one accent outline for keyboard users on every button/link/[role=button]/[role=tab]/summary that doesn't define its own. Non-input controls had no visible keyboard focus app-wide; this closes the void with a single rule. More specific focus styles (.selectable, an input's focus:ring, a menu item's data-active highlight) override it. - PopoverItem now carries outline-hidden so menu items keep their data-active highlight without also drawing the global outline (no double ring). - Nav rail rows adopt .selectable (via rowBase); rail chrome buttons (logo/pin/about/sign-out) get a focus-visible ring; the settings row already uses rowBase. Deduped the account trigger's selectable. Deferred to R10: the chip/pill selected-PAINT unification (a .chip/.pill/.is-current repaint of the ~14 selection idioms) — the R10 token redefinition repaints those surfaces anyway, at less risk than a hand migration now. R9's job (the focus/a11y foundation) is complete. Verified: tsc/eslint/prettier/knip/vitest (79) green; browser computed-style confirms the global ring renders (2px accent, offset 2px) on a plain button, .selectable overrides it with an inset ring, and an outline-hidden menu button draws no global outline. --- frontend/src/components/AnchoredPopover.tsx | 2 +- frontend/src/components/NavSidebar.tsx | 13 +++++++------ frontend/src/index.css | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/AnchoredPopover.tsx b/frontend/src/components/AnchoredPopover.tsx index f4c173fe..bdce815e 100644 --- a/frontend/src/components/AnchoredPopover.tsx +++ b/frontend/src/components/AnchoredPopover.tsx @@ -327,7 +327,7 @@ export function PopoverItem({ tabIndex={focusActive ? 0 : -1} data-active={visualActive ? "true" : undefined} disabled={disabled} - className={`${checked || selected ? HIGHLIGHT_SELECTED : HIGHLIGHT_UNSELECTED}${className ? ` ${className}` : ""}`} + className={`outline-hidden ${checked || selected ? HIGHLIGHT_SELECTED : HIGHLIGHT_UNSELECTED}${className ? ` ${className}` : ""}`} > {children} diff --git a/frontend/src/components/NavSidebar.tsx b/frontend/src/components/NavSidebar.tsx index 15c7d638..1f496586 100644 --- a/frontend/src/components/NavSidebar.tsx +++ b/frontend/src/components/NavSidebar.tsx @@ -168,7 +168,8 @@ export default function NavSidebar({ .filter((p) => sys.has(p)) .map((p) => ({ page: p, ...META(p) })); - const rowBase = "w-full flex items-center gap-3 rounded-lg px-2.5 py-2 text-sm transition"; + const rowBase = + "selectable w-full flex items-center gap-3 rounded-lg px-2.5 py-2 text-sm transition"; const name = me.display_name ?? me.email.split("@")[0]; // Role is surfaced as a plain title tooltip on the account row (no visual badge). Demo isn't a // real role (it's a flag on a `user` row), so surface it first. @@ -240,7 +241,7 @@ export default function NavSidebar({ {!slim && ( @@ -289,7 +290,7 @@ export default function NavSidebar({ onClick={onOpenAbout} title={t("header.account.about")} aria-label={t("header.account.about")} - className="p-2 rounded-lg text-muted hover:text-fg hover:bg-card transition" + className="p-2 rounded-lg text-muted hover:text-fg hover:bg-card transition outline-hidden focus-visible:outline-2 focus-visible:outline-accent/60" > @@ -297,7 +298,7 @@ export default function NavSidebar({ onClick={logout} title={t("header.account.signOut")} aria-label={t("header.account.signOut")} - className="p-2 rounded-lg text-muted hover:text-fg hover:bg-card transition" + className="p-2 rounded-lg text-muted hover:text-fg hover:bg-card transition outline-hidden focus-visible:outline-2 focus-visible:outline-accent/60" > @@ -322,7 +323,7 @@ export default function NavSidebar({ ref={acctPop.refs.setReference} {...acctPop.getReferenceProps()} title={slim ? `${name} · ${roleLabel}` : undefined} - className={`${rowBase} selectable ${slim ? "justify-center px-0" : ""} text-muted hover:text-fg hover:bg-card`} + className={`${rowBase} ${slim ? "justify-center px-0" : ""} text-muted hover:text-fg hover:bg-card`} > Date: Sat, 8 Aug 2026 09:28:23 +0200 Subject: [PATCH 3/4] fix(a11y): address R9 code-review findings (glass shadow, focus dedup) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #1: remove the 9 dead `shadow-*` from glass surfaces instead of letting them override the tuned glass box-shadow. The @layer components move made a JSX shadow-* win AND atomically drop the glass inset top-edge highlight; deleting the (always-dead) utilities keeps the intended glass depth incl. the inset sheen. Verified in-browser: a .glass box-shadow again includes its inset segment. - #3: drop the per-element focus-visible outlines from the rail chrome buttons and SavedViews action buttons — the global button:focus-visible fallback already draws the identical 2px accent ring, so they were redundant. Verified: the pin button rings from the global rule alone. - #5: unify the focus-within outline offset (offset-2) on the SearchBar and ShareDialog pills. - #2 (double outline on the pill when an inner button is focused): NOT fixed — the clean fix (has-[input:focus] so only the input rings the pill) does not generate under Tailwind v4 (measured: no rule emitted). Kept the working focus-within, which is a11y-correct (every element still shows a focus indicator); the overlapping ring is an accepted cosmetic edge-case. - #4 (weak ring on solid-fill selected items): deferred to R10 with the selected-paint tokenisation. Gate green (tsc/eslint/prettier/knip/vitest 79). --- frontend/src/components/AddToPlaylist.tsx | 2 +- frontend/src/components/BackToTop.tsx | 2 +- frontend/src/components/ChatDock.tsx | 2 +- frontend/src/components/DownloadCenter.tsx | 2 +- frontend/src/components/GlassTuner.tsx | 2 +- frontend/src/components/Modal.tsx | 4 +--- frontend/src/components/NavSidebar.tsx | 8 ++++---- frontend/src/components/PlayerModal.tsx | 2 +- frontend/src/components/SavedViewsWidget.tsx | 12 ++++++------ frontend/src/components/SearchBar.tsx | 2 +- frontend/src/components/ShareDialog.tsx | 2 +- frontend/src/components/ui/DraftSaveBar.tsx | 2 +- frontend/src/index.css | 9 +++++---- 13 files changed, 25 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/AddToPlaylist.tsx b/frontend/src/components/AddToPlaylist.tsx index 7e3a548d..0cfc74c2 100644 --- a/frontend/src/components/AddToPlaylist.tsx +++ b/frontend/src/components/AddToPlaylist.tsx @@ -98,7 +98,7 @@ export default function AddToPlaylist({ - + {/* A React portal still propagates events up the REACT tree, so a click inside this panel would otherwise reach the feed card that hosts the trigger and start playback. */}
e.stopPropagation()}> diff --git a/frontend/src/components/BackToTop.tsx b/frontend/src/components/BackToTop.tsx index 8a888244..ec9ff758 100644 --- a/frontend/src/components/BackToTop.tsx +++ b/frontend/src/components/BackToTop.tsx @@ -51,7 +51,7 @@ export default function BackToTop({ dep, threshold = 600 }: { dep?: unknown; thr return createPortal( @@ -290,7 +290,7 @@ export default function NavSidebar({ onClick={onOpenAbout} title={t("header.account.about")} aria-label={t("header.account.about")} - className="p-2 rounded-lg text-muted hover:text-fg hover:bg-card transition outline-hidden focus-visible:outline-2 focus-visible:outline-accent/60" + className="p-2 rounded-lg text-muted hover:text-fg hover:bg-card transition" > @@ -298,7 +298,7 @@ export default function NavSidebar({ onClick={logout} title={t("header.account.signOut")} aria-label={t("header.account.signOut")} - className="p-2 rounded-lg text-muted hover:text-fg hover:bg-card transition outline-hidden focus-visible:outline-2 focus-visible:outline-accent/60" + className="p-2 rounded-lg text-muted hover:text-fg hover:bg-card transition" > diff --git a/frontend/src/components/PlayerModal.tsx b/frontend/src/components/PlayerModal.tsx index 6187ec42..f429e7d3 100644 --- a/frontend/src/components/PlayerModal.tsx +++ b/frontend/src/components/PlayerModal.tsx @@ -916,7 +916,7 @@ export default function PlayerModal({
e.stopPropagation()} >
@@ -296,7 +296,7 @@ function ViewRow({ onClick={onApply} title={t("views.apply")} aria-current={active ? "true" : undefined} - className={`flex-1 min-w-0 text-left text-xs py-1 truncate rounded-sm outline-hidden focus-visible:outline-2 focus-visible:outline-accent/60 ${ + className={`flex-1 min-w-0 text-left text-xs py-1 truncate ${ active ? "text-accent font-medium" : "text-fg" }`} > @@ -306,7 +306,7 @@ function ViewRow({ diff --git a/frontend/src/components/SearchBar.tsx b/frontend/src/components/SearchBar.tsx index 2694d3bb..aa4f1af7 100644 --- a/frontend/src/components/SearchBar.tsx +++ b/frontend/src/components/SearchBar.tsx @@ -31,7 +31,7 @@ export default function SearchBar({ if (onGo && !goDisabled) onGo(); }; return ( -
+
{open && filtered.length > 0 && ( -
+
{filtered.map((u) => (