From c16b90ffe9ba44a9030f62126cae577908484349 Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Fri, 10 Jul 2026 00:01:33 +0200 Subject: [PATCH 1/3] fix: dark-mode contrast for stock MUI components + self-explanatory spec tabs MUI's palette must hold raw light-mode hexes (decomposeColor rejects CSS vars), so stock components rendered light-theme colors on dark: unselected tab labels at ~2.1:1, invisible dividers, wrong skeletons and alerts. MuiTab, MuiDivider, MuiSkeleton and MuiAlert are now wired to the CSS-var system. The spec-detail tabs also stop hiding the page's main content: index 0 (Code in detail mode, Spec on hub pages) starts open, the selected tab shows a collapse caret so the toggle behavior is discoverable, the quality tab reads "Quality 91" with an explanatory aria-label instead of a bare number, and tabs/panels got standard id/aria-controls wiring. Audit 2026-07-08 High#4 + High#5 + Low#1. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 17 +++++ app/src/sections/spec-detail/RelatedSpecs.tsx | 2 +- .../spec-detail/SpecTabs/SpecTabs.test.tsx | 58 +++++++++++++---- .../sections/spec-detail/SpecTabs/index.tsx | 63 ++++++++++++++++--- app/src/sections/spec-detail/SpecTabs/md.tsx | 11 +++- app/src/theme/components.ts | 34 ++++++++++ 6 files changed, 160 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4855e02df9..9adfcdfe9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,23 @@ aggregate instead: an italic *Catalog* line at the end of the version section an ## [Unreleased] +### Changed + +- **Spec-detail tabs are self-explanatory now** — the Code tab (Spec tab on hub pages) starts + open instead of everything collapsed, the selected tab shows a small caret signaling the + click-to-collapse toggle, the quality tab reads "Quality 91" (with an explanatory + `aria-label`) instead of a bare number, and tabs↔panels got standard `id`/`aria-controls` + wiring (audit 2026-07-08 High#5 + Low#1). + +### Fixed + +- **Dark-mode contrast for stock MUI components** — the MUI palette is locked to light-mode + hexes (it can't hold CSS variables), so unselected tab labels, dividers, skeletons, and + alerts rendered light-theme colors on dark backgrounds (~2.1:1 label contrast). MuiTab, + MuiDivider, MuiSkeleton, and MuiAlert are now wired to the CSS-var system + (`--ink-soft`/`--rule`/`--bg-elevated`), and the two `borderColor: 'divider'` usages in + SpecTabs/RelatedSpecs use `var(--rule)` (audit 2026-07-08 High#4). + ### Added - **`llms.txt` for AI agents** — `/llms.txt` previously fell through to the SPA shell (flagged diff --git a/app/src/sections/spec-detail/RelatedSpecs.tsx b/app/src/sections/spec-detail/RelatedSpecs.tsx index 27b7b89c82..a2e5010343 100644 --- a/app/src/sections/spec-detail/RelatedSpecs.tsx +++ b/app/src/sections/spec-detail/RelatedSpecs.tsx @@ -111,7 +111,7 @@ export function RelatedSpecs({ specId, mode = 'spec', library, onHoverTags }: Re '@keyframes relatedFadeIn': { from: { opacity: 0 }, to: { opacity: 1 } }, }} > - + setExpanded(e => !e)} diff --git a/app/src/sections/spec-detail/SpecTabs/SpecTabs.test.tsx b/app/src/sections/spec-detail/SpecTabs/SpecTabs.test.tsx index 20e3d6af9f..0cfe490e60 100644 --- a/app/src/sections/spec-detail/SpecTabs/SpecTabs.test.tsx +++ b/app/src/sections/spec-detail/SpecTabs/SpecTabs.test.tsx @@ -271,10 +271,7 @@ describe('SpecTabs', () => { const onTrackEvent = vi.fn(); render(); - // Open Code tab - await user.click(screen.getByRole('tab', { name: /code/i })); - - // Click copy button + // Code tab starts open — the copy button is immediately available const copyButton = screen.getByRole('button', { name: /copy code/i }); await user.click(copyButton); @@ -337,11 +334,14 @@ describe('SpecTabs', () => { }); // ------------------------------------------------------- - // 12. Quality tab label shows numeric score when present + // 12. Quality tab label shows labeled score when present // ------------------------------------------------------- - it('shows numeric score in the Quality tab label', () => { + it('labels the Quality tab with the score and an explanatory aria-label', () => { render(); - expect(screen.getByRole('tab', { name: /93/i })).toBeInTheDocument(); + const qualityTab = screen.getByRole('tab', { name: /quality: ai review score 93 of 100/i }); + expect(qualityTab).toBeInTheDocument(); + // Visible label is "Quality 93" (short form "93" on xs) + expect(qualityTab).toHaveTextContent('Quality 93'); }); it('shows "Quality" in the tab label when score is null', () => { @@ -384,20 +384,54 @@ describe('SpecTabs', () => { }); // ------------------------------------------------------- - // 15. Code tab shows code via CodeHighlighter + // 15. Code tab starts open in detail mode (default-open) // ------------------------------------------------------- - it('renders CodeHighlighter with code when Code tab is open', async () => { - const user = userEvent.setup(); + it('renders CodeHighlighter without any click — Code tab starts open', async () => { render(); - await user.click(screen.getByRole('tab', { name: /code/i })); - await waitFor(() => { expect(screen.getByTestId('code-highlighter')).toBeInTheDocument(); }); expect(screen.getByTestId('code-highlighter')).toHaveTextContent( 'import matplotlib print("hello")' ); + expect(screen.getByRole('tab', { name: /code/i })).toHaveAttribute('aria-selected', 'true'); + }); + + it('starts with the Spec tab open in overviewMode', () => { + render(); + expect(screen.getByRole('tab', { name: /spec/i })).toHaveAttribute('aria-selected', 'true'); + // Spec content is visible without a click + expect(screen.getByText('A scatter plot showing data points')).toBeVisible(); + }); + + it('collapses the default-open Code tab on click and tracks the close', async () => { + const user = userEvent.setup(); + const onTrackEvent = vi.fn(); + render(); + + await user.click(screen.getByRole('tab', { name: /code/i })); + + expect(onTrackEvent).toHaveBeenCalledWith('tab_toggle', { + action: 'close', + tab: 'code', + library: 'matplotlib', + }); + }); + + // ------------------------------------------------------- + // 15b. Tabs a11y wiring: tab ↔ tabpanel id pairing + // ------------------------------------------------------- + it('wires each tab to its panel via id/aria-controls', () => { + render(); + + const codeTab = screen.getByRole('tab', { name: /code/i }); + const panelId = codeTab.getAttribute('aria-controls'); + expect(panelId).toBeTruthy(); + const panel = document.getElementById(panelId as string); + expect(panel).not.toBeNull(); + expect(panel).toHaveAttribute('role', 'tabpanel'); + expect(panel).toHaveAttribute('aria-labelledby', codeTab.id); }); // ------------------------------------------------------- diff --git a/app/src/sections/spec-detail/SpecTabs/index.tsx b/app/src/sections/spec-detail/SpecTabs/index.tsx index 68d6adb64a..d721a05de1 100644 --- a/app/src/sections/spec-detail/SpecTabs/index.tsx +++ b/app/src/sections/spec-detail/SpecTabs/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useId, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -86,8 +86,9 @@ export function SpecTabs({ overviewMode = false, highlightedTags = [], }: SpecTabsProps) { - // In overview mode, start with Spec tab open; in detail mode, all collapsed - const [tabIndex, setTabIndex] = useState(null); + // Index 0 starts open — the Code tab in detail mode, the Spec tab in + // overview mode. An all-collapsed start hides the page's main content. + const [tabIndex, setTabIndex] = useState(0); const [expandedCategories, setExpandedCategories] = useState>({}); const [tagCounts, setTagCounts] = useState> | null>( getCachedTagCounts() @@ -166,9 +167,26 @@ export function SpecTabs({ // In overview mode, use different tab indexing (only Spec tab at index 0) const specTabIndex = overviewMode ? 0 : 1; + // Standard tabs a11y wiring; useId keeps ids unique if two SpecTabs render + // on one page. + const uid = useId(); + const tabA11yProps = (index: number) => ({ + id: `${uid}-tab-${index}`, + 'aria-controls': `${uid}-tabpanel-${index}`, + }); + + // The selected tab collapses on a second click — show a caret as the + // affordance, otherwise the toggle behavior is undiscoverable. + const collapseCaret = (index: number) => + tabIndex === index ? ( + + ▴ + + ) : null; + return ( - + {!overviewMode && ( tabIndex === 0 && handleTabChange(e, 0)} icon={} iconPosition="start" - label="Code" + label={<>Code{collapseCaret(0)}} /> )} tabIndex === specTabIndex && handleTabChange(e, specTabIndex)} icon={} iconPosition="start" @@ -214,11 +234,13 @@ export function SpecTabs({ Spec + {collapseCaret(specTabIndex)} } /> {!overviewMode && ( tabIndex === 2 && handleTabChange(e, 2)} icon={} iconPosition="start" @@ -230,13 +252,20 @@ export function SpecTabs({ Impl + {collapseCaret(2)} } /> )} {!overviewMode && ( tabIndex === 3 && handleTabChange(e, 3)} + aria-label={ + qualityScore !== null + ? `Quality: AI review score ${Math.round(qualityScore)} of 100` + : 'Quality' + } icon={ } iconPosition="start" - label={qualityScore !== null ? `${Math.round(qualityScore)}` : 'Quality'} + label={ + qualityScore !== null ? ( + <> + + {`Quality ${Math.round(qualityScore)}`} + + + {`${Math.round(qualityScore)}`} + + {collapseCaret(3)} + + ) : ( + <>Quality{collapseCaret(3)} + ) + } /> )} @@ -254,7 +297,7 @@ export function SpecTabs({ {/* Code Tab - only in detail mode */} {!overviewMode && ( - + + + + (id / aria-controls pair) + idPrefix?: string; } -export function TabPanel({ children, value, index }: TabPanelProps) { +export function TabPanel({ children, value, index, idPrefix }: TabPanelProps) { const isOpen = value === index; return ( - + {children} diff --git a/app/src/theme/components.ts b/app/src/theme/components.ts index 7bf7650a48..d6365e4f8a 100644 --- a/app/src/theme/components.ts +++ b/app/src/theme/components.ts @@ -12,6 +12,40 @@ export const components: ThemeOptions['components'] = { }, }, }, + // Stock MUI components fall back to the light-mode palette hexes (the MUI + // palette can't hold var(...) tokens — see palette.ts). Wire the handful of + // stock components we render to the CSS-var system so they adapt to + // [data-theme='dark'] like everything else. + MuiTab: { + styleOverrides: { + root: { + color: 'var(--ink-soft)', + }, + }, + }, + MuiDivider: { + styleOverrides: { + root: { + borderColor: 'var(--rule)', + }, + }, + }, + MuiSkeleton: { + styleOverrides: { + root: { + backgroundColor: 'var(--rule)', + }, + }, + }, + MuiAlert: { + styleOverrides: { + root: { + backgroundColor: 'var(--bg-elevated)', + color: 'var(--ink)', + border: '1px solid var(--rule)', + }, + }, + }, MuiTooltip: { defaultProps: { enterDelay: 200, From 4b257f12527f2cc5de6a01d45a4600462d11ed89 Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Fri, 10 Jul 2026 00:02:17 +0200 Subject: [PATCH 2/3] docs: append PR ref #9622 to changelog entries Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9adfcdfe9c..91fb89accb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ aggregate instead: an italic *Catalog* line at the end of the version section an open instead of everything collapsed, the selected tab shows a small caret signaling the click-to-collapse toggle, the quality tab reads "Quality 91" (with an explanatory `aria-label`) instead of a bare number, and tabs↔panels got standard `id`/`aria-controls` - wiring (audit 2026-07-08 High#5 + Low#1). + wiring (audit 2026-07-08 High#5 + Low#1) (#9622). ### Fixed @@ -31,7 +31,7 @@ aggregate instead: an italic *Catalog* line at the end of the version section an alerts rendered light-theme colors on dark backgrounds (~2.1:1 label contrast). MuiTab, MuiDivider, MuiSkeleton, and MuiAlert are now wired to the CSS-var system (`--ink-soft`/`--rule`/`--bg-elevated`), and the two `borderColor: 'divider'` usages in - SpecTabs/RelatedSpecs use `var(--rule)` (audit 2026-07-08 High#4). + SpecTabs/RelatedSpecs use `var(--rule)` (audit 2026-07-08 High#4) (#9622). ### Added From 5bc480408bdbcfcf9dc2a87a777cdb7a29ddfe41 Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Fri, 10 Jul 2026 00:09:51 +0200 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20address=20Copilot=20review=20?= =?UTF-8?q?=E2=80=94=20divider=20::before/::after=20colors,=20aria-hidden?= =?UTF-8?q?=20panels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- app/src/sections/spec-detail/SpecTabs/SpecTabs.test.tsx | 6 ++++++ app/src/sections/spec-detail/SpecTabs/md.tsx | 1 + app/src/theme/components.ts | 6 ++++++ 3 files changed, 13 insertions(+) diff --git a/app/src/sections/spec-detail/SpecTabs/SpecTabs.test.tsx b/app/src/sections/spec-detail/SpecTabs/SpecTabs.test.tsx index 0cfe490e60..111fda9f74 100644 --- a/app/src/sections/spec-detail/SpecTabs/SpecTabs.test.tsx +++ b/app/src/sections/spec-detail/SpecTabs/SpecTabs.test.tsx @@ -432,6 +432,12 @@ describe('SpecTabs', () => { expect(panel).not.toBeNull(); expect(panel).toHaveAttribute('role', 'tabpanel'); expect(panel).toHaveAttribute('aria-labelledby', codeTab.id); + + // The open panel is exposed; collapsed panels are aria-hidden + expect(panel).toHaveAttribute('aria-hidden', 'false'); + const specTab = screen.getByRole('tab', { name: /spec/i }); + const specPanel = document.getElementById(specTab.getAttribute('aria-controls') as string); + expect(specPanel).toHaveAttribute('aria-hidden', 'true'); }); // ------------------------------------------------------- diff --git a/app/src/sections/spec-detail/SpecTabs/md.tsx b/app/src/sections/spec-detail/SpecTabs/md.tsx index 3dfe61f92d..331595f591 100644 --- a/app/src/sections/spec-detail/SpecTabs/md.tsx +++ b/app/src/sections/spec-detail/SpecTabs/md.tsx @@ -21,6 +21,7 @@ export function TabPanel({ children, value, index, idPrefix }: TabPanelProps) { role="tabpanel" id={idPrefix ? `${idPrefix}-tabpanel-${index}` : undefined} aria-labelledby={idPrefix ? `${idPrefix}-tab-${index}` : undefined} + aria-hidden={!isOpen} sx={{ pt: 2 }} > {children} diff --git a/app/src/theme/components.ts b/app/src/theme/components.ts index d6365e4f8a..a834f1547e 100644 --- a/app/src/theme/components.ts +++ b/app/src/theme/components.ts @@ -27,6 +27,12 @@ export const components: ThemeOptions['components'] = { styleOverrides: { root: { borderColor: 'var(--rule)', + // Dividers with children render their lines via ::before/::after, + // which carry their own borderTop — the root borderColor alone + // doesn't reach them. + '&::before, &::after': { + borderColor: 'var(--rule)', + }, }, }, },