Skip to content

style: visual redesign — theme tokenization, WCAG fixes, PlotOfTheDay redesign#5235

Merged
MarkusNeusinger merged 6 commits intomainfrom
ui/visual-redesign
Apr 10, 2026
Merged

style: visual redesign — theme tokenization, WCAG fixes, PlotOfTheDay redesign#5235
MarkusNeusinger merged 6 commits intomainfrom
ui/visual-redesign

Conversation

@MarkusNeusinger
Copy link
Copy Markdown
Owner

Summary

  • Theme tokenization: Replace 80+ hardcoded hex colors and font strings across 30 files with centralized theme tokens (app/src/theme/index.ts)
  • WCAG AA compliance: Fix 15+ contrast violations where #9ca3af (2.9:1 ratio) was used for text — all text now meets 4.5:1 minimum
  • PlotOfTheDay redesign: Terminal-style frame with $ python prompt, split layout (image left, info right), fade-in animation, GitHub link to source file, real library/Python versions from API
  • Responsive breadcrumb: pyplots.aipp, matplotlibmpl on mobile via new shortLabel prop
  • New theme tokens: fontSize.micro/xxs, colors.primaryDark/accentBg/codeBlock/highlight/tooltipLight, shared style constants (headingStyle, subheadingStyle, textStyle, codeBlockStyle, tableStyle)
  • Code quality: Fix 3 pre-existing lint errors (setState in useEffect), fix SpecPage useCallback warning, remove unused ImageLightbox, centralize LIB_ABBREV
  • Style guide: Complete documentation with new sections (data-dense typography, page headings, code blocks, tables, chart colors, gray usage rule)

Test plan

  • Visual check all pages: Home, Spec (overview + detail), Catalog, Legal, MCP, Stats, Debug, NotFound, Interactive
  • Verify PlotOfTheDay fade-in animation and terminal frame on desktop + mobile
  • Verify breadcrumb abbreviations on mobile (< 600px)
  • Verify WCAG contrast with browser DevTools (all text ≥ 4.5:1)
  • Confirm 0 lint errors (cd app && yarn lint)
  • Confirm build passes (cd app && yarn build)

🤖 Generated with Claude Code

MarkusNeusinger and others added 4 commits April 9, 2026 00:01
…aner navigation

- Theme tokens: font scale +15%, semantic color tokens for WCAG AA contrast
- MUI theme: primary color aligned to Python blue, global tooltip style
- Navigation: remove "catalog" from breadcrumb, add catalog link in rightAction + footer
- Lightbox: new ImageLightbox component replaces image-click-to-go-back with zoom
- SpecPage: keyboard arrow keys for library switching, "< all implementations" link
- SpecOverview: responsive grid fix (1/2/3 cols instead of fixed 3)
- RelatedSpecs: single-row auto-fit grid, abbreviated library names, tooltips
- Tags: larger chips (24px), tag count tooltips from globalCounts API
- Cards: focus-visible only, ">>> copied" overlay, blue hover on action buttons
- Toolbar: larger icons (1.4rem), better contrast
- Layout: ultrawide support (max-width 2200px, xl padding)
- All components migrated from hardcoded values to theme tokens
- New style guide: docs/reference/style-guide.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- increase icon sizes for better visibility
- adjust related specs query limit to 24
- enhance tab navigation in RelatedSpecs component
- remove unnecessary elements from UI for cleaner design
…n PlotOfTheDay

- Replace 80+ hardcoded hex colors with theme tokens across all components and pages
- Fix 15+ WCAG AA contrast violations (#9ca3af used as text color)
- Add new theme tokens: fontSize.micro/xxs, colors.primaryDark/accentBg/codeBlock/highlight/tooltipLight
- Add shared style constants: headingStyle, subheadingStyle, textStyle, codeBlockStyle, tableStyle
- Centralize LIB_ABBREV map in constants, add responsive shortLabel to Breadcrumb (pp/mpl/sns on mobile)
- Redesign PlotOfTheDay: terminal-style frame, split layout, fade-in animation, GitHub link, real versions
- Fix 3 pre-existing lint errors (setState in useEffect → render-time ref pattern)
- Fix SpecPage useCallback missing dependency warning
- Remove unused ImageLightbox component
- Improve RelatedSpecs grid (2→3→4→6 columns), spacing, "tags in common" label
- Update style guide documentation with all new tokens and sections
- Update Serena memories with style guide reference

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 10, 2026 22:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a frontend visual redesign by centralizing theme values into reusable tokens, addressing WCAG contrast issues, and updating several UI components (notably PlotOfTheDay and breadcrumb behavior). It also extends the API payload for PlotOfTheDay and increases the maximum “related” results returned.

Changes:

  • Introduce/expand theme tokens and shared sx style constants; replace hardcoded colors/fonts across many pages/components.
  • Redesign PlotOfTheDay UI and enrich it with GitHub source links + library/Python version metadata from the API.
  • Add responsive breadcrumb short labels and refresh several pages (Catalog/Spec/Stats/Legal/MCP/etc.) to match the new style guide.

Reviewed changes

Copilot reviewed 34 out of 35 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
docs/reference/style-guide.md New/expanded frontend style guide documentation
app/src/theme/index.ts Adds new tokens + shared style constants
app/src/pages/StatsPage.tsx Refactors styling to tokens + breadcrumb shortLabel
app/src/pages/SpecPage.tsx Download flow update, keyboard nav, breadcrumb/action redesign
app/src/pages/NotFoundPage.tsx Tokenizes fonts/colors
app/src/pages/McpPage.tsx Reuses shared theme constants (heading/text/table/code)
app/src/pages/LegalPage.tsx Reuses shared theme constants + breadcrumb shortLabel
app/src/pages/InteractivePage.tsx Tokenizes background/typography + breadcrumb shortLabel
app/src/pages/HomePage.tsx Tokenizes scroll-to-top button colors
app/src/pages/DebugPage.tsx Tokenizes UI + breadcrumb shortLabel
app/src/pages/CatalogPage.tsx Tokenizes UI + breadcrumb shortLabel
app/src/main.tsx Applies theme tokens to MUI theme + tooltip overrides
app/src/constants/index.ts Adds shared LIB_ABBREV mapping
app/src/components/ToolbarActions.tsx Tokenizes toolbar icon styling + focus-visible outline
app/src/components/SpecTabs.tsx Tokenizes styles + adds global tag-count tooltips
app/src/components/SpecOverview.tsx Adds download confirmation + tokenizes styles
app/src/components/SpecDetailView.tsx Adds zoom/pan behavior + tokenizes styles
app/src/components/RelatedSpecs.tsx Expands related grid + adds expand/collapse UI
app/src/components/PlotOfTheDay.tsx Full redesign + GitHub link + version display
app/src/components/LoaderSpinner.tsx Tokenizes loader colors
app/src/components/LibraryPills.tsx Tokenizes pills styling
app/src/components/Layout.tsx Tokenizes app background + container sizing tweaks
app/src/components/ImagesGrid.tsx Tokenizes empty-state alert styling
app/src/components/ImageCard.tsx Tokenizes styles + focus-visible + copy overlay tweaks
app/src/components/Header.tsx Tokenizes brand colors/typography
app/src/components/Footer.tsx Tokenizes footer styling + adds catalog link
app/src/components/FilterBar.tsx Tokenizes filter UI styles
app/src/components/CodeHighlighter.tsx Tokenizes code font family
app/src/components/Breadcrumb.tsx Adds shortLabel support + tokenizes styles
api/routers/insights.py PlotOfTheDay response adds version fields; related limit max raised
agentic/commands/prime.md Updates Serena onboarding/tooling guidance
.serena/memories/style_guide.md Adds style guide memory for Serena workflows
.serena/memories/code_style.md Updates frontend styling + state reset guidance
.gitignore Ignores /screenshots/
.claude/settings.json Updates allowed tool patterns
Comments suppressed due to low confidence (1)

app/src/components/LibraryPills.tsx:19

  • LIBRARY_ABBREV is duplicated here while the PR introduces a shared LIB_ABBREV map in app/src/constants. Keeping two sources of truth risks them drifting (new libraries/renames). Import and reuse the shared constant instead of redefining it locally.
// Library abbreviations (same as filter display)
const LIBRARY_ABBREV: Record<string, string> = {
  matplotlib: 'mpl',
  seaborn: 'sns',
  plotly: 'ply',
  bokeh: 'bok',
  altair: 'alt',
  plotnine: 'p9',
  pygal: 'pyg',
  highcharts: 'hc',
  letsplot: 'lp',
};

Comment on lines +61 to +66
// Reset zoom when library changes (no effect needed)
if (prevLibRef.current !== selectedLibrary) {
prevLibRef.current = selectedLibrary;
setZoomed(false);
setOrigin({ x: 50, y: 50 });
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling setZoomed/setOrigin during render (inside the if (prevLibRef.current !== selectedLibrary) block) triggers state updates while rendering, which can cause render loops / React warnings (especially in StrictMode) and makes the component harder to reason about. Move this reset into a useEffect watching selectedLibrary, or derive zoomed/origin from selectedLibrary via state keyed by library.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +48
// Reset expanded when specId changes (no effect needed)
if (prevSpecIdRef.current !== specId) {
prevSpecIdRef.current = specId;
setExpanded(false);
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setExpanded(false) is executed during render when specId changes. Updating state while rendering can trigger React warnings and unpredictable render behavior. Reset expanded in a useEffect on specId (or keep expanded in a key-scoped state) instead of calling the setter during render.

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +83
const handleZoomToggle = useCallback(
(e: React.MouseEvent) => {
if (!containerRef.current) return;
if (!zoomed) {
const rect = containerRef.current.getBoundingClientRect();
setOrigin({
x: ((e.clientX - rect.left) / rect.width) * 100,
y: ((e.clientY - rect.top) / rect.height) * 100,
});
}
setAnimating(true);
setZoomed((z) => !z);
setTimeout(() => setAnimating(false), 300);
},
[zoomed],
);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The zoom toggle uses setTimeout(() => setAnimating(false), 300) without cleanup. If the user navigates away quickly, this can call setState on an unmounted component (React warning) and can stack multiple timers on rapid toggles. Store the timeout id in a ref and clear it in a cleanup effect, or use requestAnimationFrame/CSS transition end callbacks.

Copilot uses AI. Check for mistakes.
Comment on lines 117 to 131
<Box
onClick={onImageClick}
ref={containerRef}
onClick={handleZoomToggle}
onMouseMove={handleMouseMove}
onTouchMove={handleTouchMove}
sx={{
position: 'relative',
borderRadius: 2,
overflow: 'hidden',
bgcolor: '#fff',
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
aspectRatio: '16/9',
cursor: 'pointer',
cursor: zoomed ? 'zoom-out' : 'zoom-in',
touchAction: zoomed ? 'none' : 'auto',
'&:hover .impl-counter': {
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main image container is clickable (zoom in/out) but isn't keyboard-accessible (no tabIndex, no role="button", no key handlers) and doesn't expose an accessible name. This makes the new zoom interaction inaccessible to keyboard/screen-reader users. Consider adding a dedicated zoom IconButton (preferred) or making the container focusable with proper role, aria-label, and Enter/Space handling.

Copilot uses AI. Check for mistakes.
Comment thread app/src/components/SpecTabs.tsx Outdated
Comment on lines +194 to +202
fetch(`${API_URL}/plots/filter?limit=1`)
.then(r => r.ok ? r.json() : null)
.then(data => {
if (data?.globalCounts) {
cachedTagCounts = data.globalCounts;
setTagCounts(data.globalCounts);
}
})
.catch(() => {});
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global tag counts fetch runs in an effect but doesn't guard against setting state after unmount (no AbortController / cancelled flag). If the user navigates away before the request resolves, setTagCounts can fire on an unmounted component. Add a cancellation flag or AbortController and check it before calling setTagCounts.

Suggested change
fetch(`${API_URL}/plots/filter?limit=1`)
.then(r => r.ok ? r.json() : null)
.then(data => {
if (data?.globalCounts) {
cachedTagCounts = data.globalCounts;
setTagCounts(data.globalCounts);
}
})
.catch(() => {});
const controller = new AbortController();
fetch(`${API_URL}/plots/filter?limit=1`, { signal: controller.signal })
.then(r => r.ok ? r.json() : null)
.then(data => {
if (!controller.signal.aborted && data?.globalCounts) {
cachedTagCounts = data.globalCounts;
setTagCounts(data.globalCounts);
}
})
.catch((error) => {
if (error instanceof DOMException && error.name === 'AbortError') {
return;
}
});
return () => {
controller.abort();
};

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +105
<IconButton
onClick={handleDismiss}
size="small"
sx={{
color: colors.gray[400], p: 0.25,
'&:hover': { color: colors.gray[600] },
}}
>
<CloseIcon sx={{ fontSize: fontSize.sm }} />
</IconButton>
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dismiss (close) IconButton is missing an aria-label, so screen readers won't announce its purpose. Add an aria-label like "Dismiss plot of the day" (or equivalent).

Copilot uses AI. Check for mistakes.
Comment thread .serena/memories/style_guide.md Outdated
Comment on lines +40 to +41
## Highlight Colors (not tokenized)
`#dbeafe`/`#1e40af` (highlighted tag chips) and `#90caf9` (tooltip text on dark bg) are intentionally kept as direct values.
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This note says highlight colors are "not tokenized" and intentionally kept as direct values, but the theme now exposes colors.highlight and colors.tooltipLight tokens. Update this guidance to match the actual theme API so contributors don't reintroduce hardcoded values.

Suggested change
## Highlight Colors (not tokenized)
`#dbeafe`/`#1e40af` (highlighted tag chips) and `#90caf9` (tooltip text on dark bg) are intentionally kept as direct values.
## Highlight Colors
Use theme tokens for highlight treatments: `colors.highlight` for highlighted tag chips and `colors.tooltipLight` for tooltip text on dark backgrounds. Do not reintroduce hardcoded highlight hex values.

Copilot uses AI. Check for mistakes.
- Add library_version/python_version to mock_impl in test_potd_with_db
- Update useCodeFetch tests to match /specs/{id}/{lib}/code endpoint
- Fix Footer test: stats link fires internal_link, not external_link

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 10, 2026

…keyboard support

- SpecDetailView: cleanup setTimeout on unmount via ref, add keyboard
  accessibility to zoom container (role, tabIndex, Enter/Space, focus-visible)
- SpecTabs: add AbortController to tag counts fetch for unmount cleanup
- PlotOfTheDay: add aria-label to dismiss button
- Update Serena style_guide memory to reflect tokenized highlight colors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 10, 2026 22:22
@MarkusNeusinger MarkusNeusinger merged commit dc04df2 into main Apr 10, 2026
10 of 11 checks passed
@MarkusNeusinger MarkusNeusinger deleted the ui/visual-redesign branch April 10, 2026 22:24
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 38 changed files in this pull request and generated 3 comments.

Comment on lines +235 to +244
{currentImpl && (
<Tooltip title="Copy Code" disableFocusListener>
<IconButton
onClick={() => onCopyCode(currentImpl)}
onClick={(e: React.MouseEvent) => { (e.currentTarget as HTMLElement).blur(); onCopyCode(currentImpl); }}
aria-label="Copy code"
sx={{
bgcolor: 'rgba(255,255,255,0.9)',
'&:hover': { bgcolor: '#fff' },
'&:hover': { bgcolor: '#fff', color: colors.primary },
}}
size="small"
size="medium"
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The click handlers on the action IconButtons call blur() unconditionally. This removes focus even for keyboard activation (Enter/Space), which can make keyboard navigation confusing and reduces visible focus indication. Prefer relying on :focus-visible styling (already used elsewhere) or only blurring on pointer/mouse interactions.

Copilot uses AI. Check for mistakes.
Comment on lines +223 to 239
<Tooltip title="Copy Code" disableFocusListener>
<IconButton
onClick={(e: React.MouseEvent) => { (e.currentTarget as HTMLElement).blur(); onCopyCode(impl); }}
aria-label="Copy code"
sx={{
bgcolor: 'rgba(255,255,255,0.9)',
'&:hover': { bgcolor: '#fff', color: colors.primary },
}}
size="medium"
>
<ContentCopyIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Download PNG" disableFocusListener>
<IconButton
onClick={() => onDownload(impl)}
onClick={(e: React.MouseEvent) => { (e.currentTarget as HTMLElement).blur(); onDownload(impl); }}
aria-label="Download PNG"
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The action button click handlers call blur() before invoking onCopyCode/onDownload. This also fires for keyboard activation, causing focus to be lost unexpectedly. Consider removing the manual blur and instead using :focus-visible styling to avoid mouse focus rings while keeping keyboard focus behavior accessible.

Copilot uses AI. Check for mistakes.
}}>
"{data.image_description.trim()}"
<Typography sx={{ fontFamily: mono, fontSize: fontSize.xxs, color: semanticColors.mutedText, whiteSpace: 'nowrap' }}>
{data.library_name}{data.library_version && data.library_version !== 'unknown' ? ` ${data.library_version}` : ''} · Python {data.python_version || '3.13'}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UI falls back to hardcoding Python 3.13 when python_version is missing. That can display an incorrect version (and conflicts with the goal of showing real versions from the API). Consider showing an explicit "unknown"/omitting the version when null, similar to the library_version !== 'unknown' handling.

Suggested change
{data.library_name}{data.library_version && data.library_version !== 'unknown' ? ` ${data.library_version}` : ''} · Python {data.python_version || '3.13'}
{data.library_name}
{data.library_version && data.library_version !== 'unknown' ? ` ${data.library_version}` : ''}
{data.python_version && data.python_version !== 'unknown' ? ` · Python ${data.python_version}` : ''}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants