Skip to content

Commit 0408b98

Browse files
sgammonclaude
andcommitted
fix(a11y): color-contrast + dialog name; address Copilot review
Accessibility (Storybook a11y / Chromatic): - Accent text used --primary (#d4007f), which fails WCAG AA on the dark surfaces (3.87:1). Switch text usages to --primary-emphasis (the design system's on-surface accent — magenta-400 on dark), leaving bg/border accents alone. Fixes SectionTabs, ApiMethod/ParamRow, SupportMatrix, MobileNav, TableOfContents, Sidebar, StatStrip, AiActions. - --subtle-foreground was neutral-500 (#737373) in dark, failing AA for the small mono micro-labels; step it up to neutral-400 (light unchanged). - MobileNav's Sheet drawer (role=dialog) had no accessible name -> add aria-label. Copilot review: - ai-actions: swallow clipboard writeText rejection (matches CopyButton). - stat-strip: emit <dt> before <dd> for valid <dl> markup; order-* preserves the value-on-top layout. Story/test class assertions updated to the -emphasis token to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9cdd6cc commit 0408b98

12 files changed

Lines changed: 39 additions & 28 deletions

packages/tokens/src/tokens.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@
142142

143143
--muted: var(--eld-neutral-800);
144144
--muted-foreground: var(--eld-neutral-400);
145-
--subtle-foreground: var(--eld-neutral-500);
145+
/* neutral-500 (#737373) fails WCAG AA for small text on the dark surfaces;
146+
step up to neutral-400 so micro-labels clear 4.5:1 (light stays 500). */
147+
--subtle-foreground: var(--eld-neutral-400);
146148

147149
--accent: var(--eld-neutral-700);
148150
--accent-foreground: var(--eld-neutral-50);

packages/ui/src/components/ai-actions.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,17 @@ export function AiActions({
4141
{...props}
4242
>
4343
<div className="mb-0.5 flex items-center gap-1.5 font-mono text-[11px] font-semibold tracking-wider text-subtle-foreground uppercase">
44-
<Sparkles aria-hidden className="h-[13px] w-[13px] text-[var(--primary)]" />
44+
<Sparkles aria-hidden className="h-[13px] w-[13px] text-[var(--primary-emphasis)]" />
4545
{title}
4646
</div>
4747

4848
{markdown ? (
4949
<button
5050
type="button"
5151
onClick={() => {
52-
void navigator.clipboard?.writeText(markdown);
52+
// Swallow clipboard rejections (no permission / headless) so a failed
53+
// copy doesn't surface as an unhandled promise rejection.
54+
void navigator.clipboard?.writeText(markdown)?.catch(() => {});
5355
setCopied(true);
5456
setTimeout(() => setCopied(false), 1500);
5557
}}

packages/ui/src/components/api-method.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ export interface ParamRowProps {
1818
export function ParamRow({ name, type, description, className }: ParamRowProps) {
1919
return (
2020
<div className={cn("flex items-baseline gap-3", className)}>
21-
<code className="w-24 shrink-0 font-mono text-[12.5px] text-[var(--primary)]">{name}</code>
21+
<code className="w-24 shrink-0 font-mono text-[12.5px] text-[var(--primary-emphasis)]">{name}</code>
2222
<span className="shrink-0 font-mono text-xs text-[var(--eld-syntax-type)]">{type}</span>
2323
{description ? (
24-
<span className="text-[13.5px] text-muted-foreground [&_code]:font-mono [&_code]:text-[var(--primary)]">
24+
<span className="text-[13.5px] text-muted-foreground [&_code]:font-mono [&_code]:text-[var(--primary-emphasis)]">
2525
{description}
2626
</span>
2727
) : null}
@@ -79,14 +79,14 @@ export function ApiMethod({
7979
<a
8080
href={`#${anchorId}`}
8181
aria-label={`Link to ${typeof signature === "string" ? signature : "this method"}`}
82-
className="text-[var(--primary)] opacity-55 transition-opacity hover:opacity-100"
82+
className="text-[var(--primary-emphasis)] opacity-55 transition-opacity hover:opacity-100"
8383
>
8484
<Hash aria-hidden className="h-3.5 w-3.5" />
8585
</a>
8686
) : null}
8787
</div>
8888
{description ? (
89-
<p className="mb-4 max-w-[640px] text-[14.5px] leading-relaxed text-muted-foreground [&_code]:font-mono [&_code]:text-[var(--primary)]">
89+
<p className="mb-4 max-w-[640px] text-[14.5px] leading-relaxed text-muted-foreground [&_code]:font-mono [&_code]:text-[var(--primary-emphasis)]">
9090
{description}
9191
</p>
9292
) : null}

packages/ui/src/components/mobile-nav.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function MobileNav({
7878
size="icon"
7979
aria-label="Open navigation"
8080
className={cn(
81-
drawerOpen && "border-[var(--primary)] bg-[var(--primary-soft)] text-[var(--primary)]",
81+
drawerOpen && "border-[var(--primary)] bg-[var(--primary-soft)] text-[var(--primary-emphasis)]",
8282
)}
8383
/>
8484
}
@@ -99,20 +99,25 @@ export function MobileNav({
9999
onClick={onSearch}
100100
className="flex h-11 w-full items-center gap-2.5 rounded-xl border border-[var(--border-strong)] bg-[var(--muted)] px-3.5 text-[var(--subtle-foreground)]"
101101
>
102-
<Search aria-hidden className="h-4 w-4 text-[var(--primary)]" />
102+
<Search aria-hidden className="h-4 w-4 text-[var(--primary-emphasis)]" />
103103
<span className="flex-1 text-left text-sm">{searchPlaceholder}</span>
104-
<Sparkle aria-hidden className="h-[15px] w-[15px] text-[var(--primary)]" />
104+
<Sparkle aria-hidden className="h-[15px] w-[15px] text-[var(--primary-emphasis)]" />
105105
</button>
106106
</div>
107107
</div>
108108

109-
<SheetContent side="left" showCloseButton={false} className="flex w-72 flex-col gap-5 p-4">
109+
<SheetContent
110+
side="left"
111+
showCloseButton={false}
112+
aria-label={section?.title ? `${section.title} navigation` : "Navigation menu"}
113+
className="flex w-72 flex-col gap-5 p-4"
114+
>
110115
{section ? (
111116
<div className="flex items-center gap-2.5">
112117
{section.icon ? (
113118
<span
114119
aria-hidden
115-
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-[var(--primary-soft)] text-[var(--primary)]"
120+
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-[var(--primary-soft)] text-[var(--primary-emphasis)]"
116121
>
117122
{section.icon}
118123
</span>
@@ -136,7 +141,7 @@ export function MobileNav({
136141
const linkClassName = cn(
137142
"rounded-lg px-2.5 py-2 text-sm",
138143
item.active
139-
? "font-semibold text-[var(--primary)] bg-[var(--primary-soft)]"
144+
? "font-semibold text-[var(--primary-emphasis)] bg-[var(--primary-soft)]"
140145
: "text-muted-foreground",
141146
);
142147
return item.href ? (

packages/ui/src/components/section-tabs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function SectionTabs({ items, version, onSelect, className, ...props }: S
5858
}
5959
className={cn(
6060
"relative px-3 py-3.5 text-[13.5px] text-muted-foreground transition-colors",
61-
item.active && "font-semibold text-[var(--primary)]",
61+
item.active && "font-semibold text-[var(--primary-emphasis)]",
6262
)}
6363
>
6464
{item.label}

packages/ui/src/components/sidebar.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function SectionSwitcher({
5151
>
5252
<span className="flex min-w-0 items-center gap-2.5">
5353
{active?.icon ? (
54-
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-[var(--primary-soft)] text-[var(--primary)]">
54+
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-[var(--primary-soft)] text-[var(--primary-emphasis)]">
5555
{active.icon}
5656
</span>
5757
) : null}
@@ -78,7 +78,7 @@ export function SectionSwitcher({
7878
className="items-center gap-2.5 py-1.5"
7979
>
8080
{section.icon ? (
81-
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-[var(--primary-soft)] text-[var(--primary)]">
81+
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-[var(--primary-soft)] text-[var(--primary-emphasis)]">
8282
{section.icon}
8383
</span>
8484
) : null}
@@ -89,7 +89,7 @@ export function SectionSwitcher({
8989
) : null}
9090
</span>
9191
{section.id === activeId ? (
92-
<Check aria-hidden className="ml-auto h-4 w-4 shrink-0 text-[var(--primary)]" />
92+
<Check aria-hidden className="ml-auto h-4 w-4 shrink-0 text-[var(--primary-emphasis)]" />
9393
) : null}
9494
</DropdownMenuItem>
9595
))}
@@ -134,7 +134,7 @@ export function SidebarItem({ label, href, active, external, comingSoon, onSelec
134134
className={cn(
135135
"flex items-center justify-between gap-2 rounded-lg px-2.5 py-1.5 text-[13.5px] transition-colors",
136136
active
137-
? "bg-[var(--primary-soft)] font-semibold text-[var(--primary)]"
137+
? "bg-[var(--primary-soft)] font-semibold text-[var(--primary-emphasis)]"
138138
: "text-muted-foreground hover:bg-[var(--hover)] hover:text-foreground",
139139
)}
140140
>

packages/ui/src/components/stat-strip.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ describe("StatStrip", () => {
2424

2525
it("emphasizes only the flagged stat's value", () => {
2626
render(<StatStrip stats={stats} />);
27-
expect(screen.getByText("94%")).toHaveClass("text-[var(--primary)]");
28-
expect(screen.getByText("24")).not.toHaveClass("text-[var(--primary)]");
29-
expect(screen.getByText("89%")).not.toHaveClass("text-[var(--primary)]");
27+
expect(screen.getByText("94%")).toHaveClass("text-[var(--primary-emphasis)]");
28+
expect(screen.getByText("24")).not.toHaveClass("text-[var(--primary-emphasis)]");
29+
expect(screen.getByText("89%")).not.toHaveClass("text-[var(--primary-emphasis)]");
3030
});
3131

3232
it("exposes an accessible label on the region", () => {

packages/ui/src/components/stat-strip.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,17 @@ export function StatStrip({
4242
key={i}
4343
className={cn("flex flex-col gap-1.5 p-5", !lastInRow && !last && "border-r border-border")}
4444
>
45+
{/* Semantic order is <dt> (term/label) then <dd> (value); `order-*`
46+
keeps the value visually on top per the mockup. */}
47+
<dt className="order-2 text-xs text-muted-foreground">{stat.label}</dt>
4548
<dd
4649
className={cn(
47-
"m-0 font-display text-[28px] font-bold leading-none",
48-
stat.emphasis ? "text-[var(--primary)]" : "text-foreground",
50+
"order-1 m-0 font-display text-[28px] font-bold leading-none",
51+
stat.emphasis ? "text-[var(--primary-emphasis)]" : "text-foreground",
4952
)}
5053
>
5154
{stat.value}
5255
</dd>
53-
<dt className="text-xs text-muted-foreground">{stat.label}</dt>
5456
</div>
5557
);
5658
})}

packages/ui/src/components/support-matrix.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function SupportMatrix({
7373
const StatusIcon = status.icon;
7474
return (
7575
<tr key={i} className="border-t border-border">
76-
<td className="px-4 py-2.5 font-mono text-[var(--primary)]">{row.method}</td>
76+
<td className="px-4 py-2.5 font-mono text-[var(--primary-emphasis)]">{row.method}</td>
7777
<td className="px-4 py-2.5 text-center">
7878
<StatusIcon
7979
role="img"

packages/ui/src/components/table-of-contents.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe("TableOfContents", () => {
2121
render(<TableOfContents items={items} activeId="installation" />);
2222
const active = screen.getByRole("link", { name: "Installation" });
2323
expect(active).toHaveAttribute("aria-current", "location");
24-
expect(active).toHaveClass("text-[var(--primary)]");
24+
expect(active).toHaveClass("text-[var(--primary-emphasis)]");
2525

2626
const inactive = screen.getByRole("link", { name: "Overview" });
2727
expect(inactive).not.toHaveAttribute("aria-current");

0 commit comments

Comments
 (0)