feat(a11y): R9 focus foundation — glass shadow fix, focus-visible ring, highlight/selected split
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 <NavItem>, 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).
This commit is contained in:
+82
-57
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user