fix(a11y): address R9 code-review findings (glass shadow, focus dedup)
- #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).
This commit is contained in:
@@ -98,7 +98,7 @@ export default function AddToPlaylist({
|
||||
<ListPlus className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<PopoverSurface popover={pop} className="glass-menu w-60 rounded-xl p-2 shadow-2xl">
|
||||
<PopoverSurface popover={pop} className="glass-menu w-60 rounded-xl p-2">
|
||||
{/* 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. */}
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function BackToTop({ dep, threshold = 600 }: { dep?: unknown; thr
|
||||
return createPortal(
|
||||
<button
|
||||
onClick={toTop}
|
||||
className={`fixed bottom-5 right-5 z-chrome inline-flex items-center justify-center w-16 h-16 rounded-full glass-card glass-hover text-fg shadow-lg hover:text-accent transition-all duration-200 ${
|
||||
className={`fixed bottom-5 right-5 z-chrome inline-flex items-center justify-center w-16 h-16 rounded-full glass-card glass-hover text-fg hover:text-accent transition-all duration-200 ${
|
||||
show ? "opacity-100 translate-y-0" : "opacity-0 translate-y-3 pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function ChatDock({ meId }: { meId: number }) {
|
||||
function ChatWindow({ chat, meId }: { chat: DockChat; meId: number }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="pointer-events-auto relative w-80 max-w-[calc(100vw-2rem)] glass rounded-2xl shadow-2xl flex flex-col overflow-hidden">
|
||||
<div className="pointer-events-auto relative w-80 max-w-[calc(100vw-2rem)] glass rounded-2xl flex flex-col overflow-hidden">
|
||||
{/* One-shot attention flash on a new message; keyed by the counter so it replays each time. */}
|
||||
{chat.flash > 0 && (
|
||||
<div
|
||||
|
||||
@@ -649,7 +649,7 @@ function QuotaUserPicker({ onPick }: { onPick: (u: { id: number; email: string }
|
||||
className={inputCls}
|
||||
/>
|
||||
{open && filtered.length > 0 && (
|
||||
<div className="absolute z-base mt-1 w-full rounded-lg glass-menu shadow-xl overflow-hidden">
|
||||
<div className="absolute z-base mt-1 w-full rounded-lg glass-menu overflow-hidden">
|
||||
{filtered.map((u) => (
|
||||
<button
|
||||
key={u.id}
|
||||
|
||||
@@ -284,7 +284,7 @@ export default function GlassTuner() {
|
||||
{open && (
|
||||
<div
|
||||
style={{ pointerEvents: "auto" }}
|
||||
className="glass-menu rounded-l-2xl rounded-r-none w-[300px] max-h-[82vh] flex flex-col overflow-hidden shadow-2xl"
|
||||
className="glass-menu rounded-l-2xl rounded-r-none w-[300px] max-h-[82vh] flex flex-col overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/60">
|
||||
<span className="text-sm font-semibold text-fg">◐ Glass Tuner</span>
|
||||
|
||||
@@ -66,9 +66,7 @@ export default function Modal({
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div
|
||||
className={`glass relative w-full ${maxWidth} max-h-full overflow-y-auto rounded-2xl shadow-2xl`}
|
||||
>
|
||||
<div className={`glass relative w-full ${maxWidth} max-h-full overflow-y-auto rounded-2xl`}>
|
||||
<div className="flex items-start justify-between gap-3 p-5 pb-3">
|
||||
<h2 className="text-lg font-semibold leading-snug">{title}</h2>
|
||||
<button
|
||||
|
||||
@@ -241,7 +241,7 @@ export default function NavSidebar({
|
||||
{!slim && (
|
||||
<button
|
||||
onClick={() => setPage("feed")}
|
||||
className="text-lg font-bold tracking-tight select-none cursor-pointer rounded-md -mx-1 px-1 hover:bg-card hover:opacity-90 transition outline-hidden focus-visible:outline-2 focus-visible:outline-accent/60"
|
||||
className="text-lg font-bold tracking-tight select-none cursor-pointer rounded-md -mx-1 px-1 hover:bg-card hover:opacity-90 transition"
|
||||
title={t("header.feed")}
|
||||
>
|
||||
Sift<span className="text-accent">lode</span>
|
||||
@@ -251,7 +251,7 @@ export default function NavSidebar({
|
||||
onClick={onToggleCollapse}
|
||||
title={pinned ? t("nav.unpin") : t("nav.pin")}
|
||||
aria-label={pinned ? t("nav.unpin") : t("nav.pin")}
|
||||
className="p-1 rounded-md text-muted hover:text-fg hover:bg-card transition outline-hidden focus-visible:outline-2 focus-visible:outline-accent/60"
|
||||
className="p-1 rounded-md text-muted hover:text-fg hover:bg-card transition"
|
||||
>
|
||||
{pinned ? <PinOff className="w-4 h-4" /> : <Pin className="w-4 h-4" />}
|
||||
</button>
|
||||
@@ -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"
|
||||
>
|
||||
<Info className="w-5 h-5" />
|
||||
</button>
|
||||
@@ -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"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
@@ -916,7 +916,7 @@ export default function PlayerModal({
|
||||
<div
|
||||
ref={cardRef}
|
||||
tabIndex={-1}
|
||||
className="glass-card relative flex-1 min-w-0 max-h-full overflow-y-auto rounded-2xl shadow-2xl outline-hidden"
|
||||
className="glass-card relative flex-1 min-w-0 max-h-full overflow-y-auto rounded-2xl outline-hidden"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -288,7 +288,7 @@ function ViewRow({
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
title={t("views.reorder")}
|
||||
className="p-1 text-muted/60 hover:text-muted cursor-grab touch-none shrink-0 rounded-sm outline-hidden focus-visible:outline-2 focus-visible:outline-accent/60"
|
||||
className="p-1 text-muted/60 hover:text-muted cursor-grab touch-none shrink-0"
|
||||
>
|
||||
<GripVertical className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -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({
|
||||
<button
|
||||
onClick={onToggleDefault}
|
||||
title={view.is_default ? t("views.isDefault") : t("views.makeDefault")}
|
||||
className={`p-1 rounded outline-hidden transition focus-visible:outline-2 focus-visible:outline-accent/60 ${
|
||||
className={`p-1 rounded transition ${
|
||||
view.is_default
|
||||
? "text-accent"
|
||||
: "text-muted opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 hover:text-accent"
|
||||
@@ -317,21 +317,21 @@ function ViewRow({
|
||||
<button
|
||||
onClick={onShare}
|
||||
title={t("views.share")}
|
||||
className="p-1 rounded-sm text-muted opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 hover:text-fg transition outline-hidden focus-visible:outline-2 focus-visible:outline-accent/60"
|
||||
className="p-1 rounded-sm text-muted opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 hover:text-fg transition"
|
||||
>
|
||||
<Share2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onStartRename}
|
||||
title={t("views.rename")}
|
||||
className="p-1 rounded-sm text-muted opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 hover:text-fg transition outline-hidden focus-visible:outline-2 focus-visible:outline-accent/60"
|
||||
className="p-1 rounded-sm text-muted opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 hover:text-fg transition"
|
||||
>
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
title={t("views.delete")}
|
||||
className="p-1 rounded-sm text-muted opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 hover:text-red-400 transition outline-hidden focus-visible:outline-2 focus-visible:outline-accent/60"
|
||||
className="p-1 rounded-sm text-muted opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 hover:text-red-400 transition"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function SearchBar({
|
||||
if (onGo && !goDisabled) onGo();
|
||||
};
|
||||
return (
|
||||
<div className="pointer-events-auto flex-1 min-w-0 max-w-xl h-11 pl-3 pr-2 rounded-2xl glass-menu flex items-center gap-2 focus-within:outline-2 focus-within:outline-accent/60">
|
||||
<div className="pointer-events-auto flex-1 min-w-0 max-w-xl h-11 pl-3 pr-2 rounded-2xl glass-menu flex items-center gap-2 focus-within:outline-2 focus-within:outline-accent/60 focus-within:outline-offset-2">
|
||||
<Search className="w-4 h-4 shrink-0 text-muted" />
|
||||
<input
|
||||
value={value}
|
||||
|
||||
@@ -61,7 +61,7 @@ function UserShare({ job }: { job: DownloadJob }) {
|
||||
className={inputCls}
|
||||
/>
|
||||
{open && filtered.length > 0 && (
|
||||
<div className="absolute z-base mt-1 w-full rounded-lg glass-menu shadow-xl overflow-hidden">
|
||||
<div className="absolute z-base mt-1 w-full rounded-lg glass-menu overflow-hidden">
|
||||
{filtered.map((u) => (
|
||||
<button
|
||||
key={u.id}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function DraftSaveBar({
|
||||
const saving = state === "saving";
|
||||
const floating = variant === "floating";
|
||||
const wrap = floating
|
||||
? "fixed bottom-4 left-1/2 -translate-x-1/2 z-rail glass rounded-2xl shadow-lg flex items-center justify-between gap-4 px-4 py-3 w-[min(48rem,calc(100%-2rem))]"
|
||||
? "fixed bottom-4 left-1/2 -translate-x-1/2 z-rail glass rounded-2xl flex items-center justify-between gap-4 px-4 py-3 w-[min(48rem,calc(100%-2rem))]"
|
||||
: "flex items-center justify-between gap-3 border-t border-border/60 px-4 py-3 bg-card/40";
|
||||
// The floating variant is fixed to the viewport but renders from a page inside the page
|
||||
// scroller, so it goes to <body> — otherwise the scroller's stacking context (its edge-fade
|
||||
|
||||
Reference in New Issue
Block a user