Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,20 @@ aggregate instead: an italic *Catalog* line at the end of the version section an
window-level Space/Enter/Backspace handler now bails out when the keystroke targets an
interactive element (button, link, focused card/chip/toggle), so keyboard-activating a card
no longer double-fires with a random-plot jump (audit 2026-07-08 Quick Win 1) (#9620).
- **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) (#9622).

### 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) (#9622).
- **Bot-served pages now carry the site's actual content** — the crawler-facing HTML
(`/seo-proxy/*`, what Googlebot & social bots see) grows from a title+description shell to a
real document: spec hubs list and link every implementation, implementation pages embed the
Expand Down
2 changes: 1 addition & 1 deletion app/src/sections/spec-detail/RelatedSpecs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function RelatedSpecs({ specId, mode = 'spec', library, onHoverTags }: Re
'@keyframes relatedFadeIn': { from: { opacity: 0 }, to: { opacity: 1 } },
}}
>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Box sx={{ borderBottom: 1, borderColor: 'var(--rule)' }}>
<Tabs
value={expanded ? 0 : false}
onChange={() => setExpanded(e => !e)}
Expand Down
64 changes: 52 additions & 12 deletions app/src/sections/spec-detail/SpecTabs/SpecTabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,7 @@ describe('SpecTabs', () => {
const onTrackEvent = vi.fn();
render(<SpecTabs {...baseProps} onTrackEvent={onTrackEvent} />);

// 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);

Expand Down Expand Up @@ -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(<SpecTabs {...baseProps} qualityScore={93} />);
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', () => {
Expand Down Expand Up @@ -384,20 +384,60 @@ 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(<SpecTabs {...baseProps} />);

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(<SpecTabs {...baseProps} overviewMode />);
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(<SpecTabs {...baseProps} onTrackEvent={onTrackEvent} />);

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(<SpecTabs {...baseProps} />);

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);

// 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');
});

// -------------------------------------------------------
Expand Down
63 changes: 53 additions & 10 deletions app/src/sections/spec-detail/SpecTabs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useId, useState } from 'react';

import { useNavigate } from 'react-router-dom';

Expand Down Expand Up @@ -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<number | null>(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<number | null>(0);
const [expandedCategories, setExpandedCategories] = useState<Record<string, boolean>>({});
const [tagCounts, setTagCounts] = useState<Record<string, Record<string, number>> | null>(
getCachedTagCounts()
Expand Down Expand Up @@ -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 ? (
<Box component="span" aria-hidden sx={{ ml: 0.5, fontSize: '0.6rem', opacity: 0.6 }}>
</Box>
) : null;

return (
<Box sx={{ mt: 3, maxWidth: { xs: '100%', md: 1200, lg: 1400, xl: 1600 }, mx: 'auto' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Box sx={{ borderBottom: 1, borderColor: 'var(--rule)' }}>
<Tabs
value={tabIndex !== null ? tabIndex : false}
onChange={handleTabChange}
Expand Down Expand Up @@ -196,13 +214,15 @@ export function SpecTabs({
>
{!overviewMode && (
<Tab
{...tabA11yProps(0)}
onClick={e => tabIndex === 0 && handleTabChange(e, 0)}
icon={<CodeIcon sx={{ fontSize: '1.1rem' }} />}
iconPosition="start"
label="Code"
label={<>Code{collapseCaret(0)}</>}
/>
)}
<Tab
{...tabA11yProps(specTabIndex)}
onClick={e => tabIndex === specTabIndex && handleTabChange(e, specTabIndex)}
icon={<DescriptionIcon sx={{ fontSize: '1.1rem' }} />}
iconPosition="start"
Expand All @@ -214,11 +234,13 @@ export function SpecTabs({
<Box component="span" sx={{ display: { xs: 'inline', sm: 'none' } }}>
Spec
</Box>
{collapseCaret(specTabIndex)}
</>
}
/>
{!overviewMode && (
<Tab
{...tabA11yProps(2)}
onClick={e => tabIndex === 2 && handleTabChange(e, 2)}
icon={<ImageIcon sx={{ fontSize: '1.1rem' }} />}
iconPosition="start"
Expand All @@ -230,13 +252,20 @@ export function SpecTabs({
<Box component="span" sx={{ display: { xs: 'inline', sm: 'none' } }}>
Impl
</Box>
{collapseCaret(2)}
</>
}
/>
)}
{!overviewMode && (
<Tab
{...tabA11yProps(3)}
onClick={e => tabIndex === 3 && handleTabChange(e, 3)}
aria-label={
qualityScore !== null
? `Quality: AI review score ${Math.round(qualityScore)} of 100`
: 'Quality'
}
icon={
<StarIcon
sx={{
Expand All @@ -246,15 +275,29 @@ export function SpecTabs({
/>
}
iconPosition="start"
label={qualityScore !== null ? `${Math.round(qualityScore)}` : 'Quality'}
label={
qualityScore !== null ? (
<>
<Box component="span" sx={{ display: { xs: 'none', sm: 'inline' } }}>
{`Quality ${Math.round(qualityScore)}`}
</Box>
<Box component="span" sx={{ display: { xs: 'inline', sm: 'none' } }}>
{`${Math.round(qualityScore)}`}
</Box>
{collapseCaret(3)}
</>
) : (
<>Quality{collapseCaret(3)}</>
)
}
/>
)}
</Tabs>
</Box>

{/* Code Tab - only in detail mode */}
{!overviewMode && (
<TabPanel value={tabIndex} index={0}>
<TabPanel value={tabIndex} index={0} idPrefix={uid}>
<CodeTab
code={code}
specId={specId}
Expand All @@ -266,7 +309,7 @@ export function SpecTabs({
)}

{/* Specification Tab */}
<TabPanel value={tabIndex} index={specTabIndex}>
<TabPanel value={tabIndex} index={specTabIndex} idPrefix={uid}>
<SpecTab
title={title}
description={description}
Expand All @@ -278,7 +321,7 @@ export function SpecTabs({

{/* Implementation Tab - only in detail mode */}
{!overviewMode && (
<TabPanel value={tabIndex} index={2}>
<TabPanel value={tabIndex} index={2} idPrefix={uid}>
<ImplTab
imageDescription={imageDescription}
strengths={strengths}
Expand All @@ -294,7 +337,7 @@ export function SpecTabs({

{/* Quality Tab - only in detail mode */}
{!overviewMode && (
<TabPanel value={tabIndex} index={3}>
<TabPanel value={tabIndex} index={3} idPrefix={uid}>
<QualityTab
qualityScore={qualityScore}
criteriaChecklist={criteriaChecklist}
Expand Down
12 changes: 10 additions & 2 deletions app/src/sections/spec-detail/SpecTabs/md.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,21 @@ interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number | null;
// Shared id prefix wiring the panel to its <Tab> (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 (
<Collapse in={isOpen}>
<Box role="tabpanel" sx={{ pt: 2 }}>
<Box
Comment on lines 19 to +20
role="tabpanel"
id={idPrefix ? `${idPrefix}-tabpanel-${index}` : undefined}
aria-labelledby={idPrefix ? `${idPrefix}-tab-${index}` : undefined}
aria-hidden={!isOpen}
sx={{ pt: 2 }}
>
Comment thread
MarkusNeusinger marked this conversation as resolved.
{children}
</Box>
</Collapse>
Expand Down
40 changes: 40 additions & 0 deletions app/src/theme/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,46 @@ 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)',
// 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)',
},
},
Comment thread
MarkusNeusinger marked this conversation as resolved.
},
},
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,
Expand Down
Loading