Skip to content

Commit 8a0efe7

Browse files
committed
Redesign docs site and add theme support
1 parent db112e7 commit 8a0efe7

11 files changed

Lines changed: 2514 additions & 873 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
- `visit_index_expr`: now rejects non-integer index expressions.
2222

2323
### Changed
24+
- **Documentation Site Redesign**
25+
- Reworked the React/Vite docs frontend under `site/` into a cleaner editorial layout with a warm monochrome token system, flatter panels, and a top-led navigation shell.
26+
- Rebuilt the home page around an image-led docs landing composition with a framed code hero, quick-start strip, feature bento, pipeline overview, and structured footer.
27+
- Normalized shared docs primitives (`PageHeader`, `SectionPanel`, `MetricTile`, code/table/callout styling) so content pages inherit the same minimalist visual system.
28+
- Added light, dark, and system theme support with a header theme toggle and detection for extension-driven dark mode so site styling does not stack awkwardly with tools like Dark Reader.
29+
2430
- **Semantic Coverage Expansion**
2531
- Unskipped 9 previously skipped semantic tests across `test/test_semantic_generics.py` and `test/test_semantic_control_flow.py`.
2632
- Converted deferred coverage into active failing tests to track implementation work directly in CI.

site/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ Design tokens and shared interface styles live in:
4242

4343
Primary shared UI primitives live in `site/src/components/`.
4444

45+
The current site direction is:
46+
47+
- warm monochrome, paper-toned surfaces
48+
- editorial serif display type with a restrained sans body
49+
- 1px border discipline and flat panels
50+
- top navigation plus drawer-based full route navigation
51+
- image-led home page with framed media and minimal motion via reveal-on-scroll
52+
- built-in `light` / `dark` / `system` theme modes with persistence
53+
- dark-extension detection to avoid double-dark styling when browser extensions inject their own theme layer
54+
4555
## Deployment
4656

4757
GitHub Pages deploy runs from `.github/workflows/deploy-docs.yml` when docs-related files change on `main`/`master`.

site/src/components/CodeBlock.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@ export default function CodeBlock({ code, lang, title }: CodeBlockProps) {
1010
const html = useHighlight(code, lang)
1111

1212
return (
13-
<figure className="code-shell">
13+
<figure className="code-shell" data-reveal>
1414
{(title || lang) && (
1515
<figcaption className="code-head">
16-
<span>{title || lang}</span>
17-
{lang && <span className="doc-chip">{lang}</span>}
16+
<span className="code-head-dots" aria-hidden="true">
17+
<span />
18+
<span />
19+
<span />
20+
</span>
21+
<span className="code-head-title">{title || lang}</span>
22+
<span className="code-head-meta">{lang ? 'A7' : ''}</span>
1823
</figcaption>
1924
)}
2025
{html ? (

site/src/components/Layout.tsx

Lines changed: 143 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
11
import { useEffect, useState } from 'react'
2-
import { Outlet } from 'react-router-dom'
2+
import { NavLink, Outlet, useLocation } from 'react-router-dom'
33
import Sidebar from './Sidebar'
4+
import { useTheme } from '../hooks/useTheme'
5+
6+
type PrimaryNavItem =
7+
| { kind: 'route'; to: string; label: string; end?: boolean }
8+
| { kind: 'link'; href: string; label: string }
9+
10+
const PRIMARY_NAV: PrimaryNavItem[] = [
11+
{ kind: 'route', to: '/', label: 'Docs', end: true },
12+
{ kind: 'route', to: '/start', label: 'Guide' },
13+
{ kind: 'route', to: '/language', label: 'Reference' },
14+
{ kind: 'route', to: '/examples', label: 'Examples' },
15+
{ kind: 'route', to: '/testing', label: 'Playground' },
16+
{ kind: 'route', to: '/changelog', label: 'Blog' },
17+
{ kind: 'link', href: 'https://github.com/Airbus5717/a7-py', label: 'GitHub ↗' },
18+
]
419

520
export default function Layout() {
621
const [sidebarOpen, setSidebarOpen] = useState(false)
22+
const { pathname } = useLocation()
23+
const { preference, resolvedTheme, darkExtensionActive, cycleTheme } = useTheme()
724

825
useEffect(() => {
926
if (!sidebarOpen) {
@@ -26,6 +43,34 @@ export default function Layout() {
2643
}
2744
}, [sidebarOpen])
2845

46+
useEffect(() => {
47+
document.documentElement.classList.add('reveal-ready')
48+
49+
const elements = Array.from(document.querySelectorAll<HTMLElement>('[data-reveal]'))
50+
const observer = new IntersectionObserver(
51+
(entries) => {
52+
entries.forEach((entry) => {
53+
if (entry.isIntersecting) {
54+
entry.target.classList.add('is-visible')
55+
observer.unobserve(entry.target)
56+
}
57+
})
58+
},
59+
{
60+
threshold: 0.14,
61+
rootMargin: '0px 0px -8% 0px',
62+
},
63+
)
64+
65+
elements.forEach((element, index) => {
66+
element.classList.remove('is-visible')
67+
element.style.setProperty('--reveal-delay', `${(index % 6) * 80}ms`)
68+
observer.observe(element)
69+
})
70+
71+
return () => observer.disconnect()
72+
}, [pathname])
73+
2974
return (
3075
<div className="app-shell">
3176
<a className="skip-link" href="#main-content">
@@ -34,11 +79,97 @@ export default function Layout() {
3479

3580
{sidebarOpen && <div className="mobile-overlay" onClick={() => setSidebarOpen(false)} />}
3681

37-
<aside className={`app-sidebar${sidebarOpen ? ' open' : ''}`}>
82+
<aside className={`app-sidebar${sidebarOpen ? ' open' : ''}`} aria-hidden={!sidebarOpen}>
3883
<Sidebar onClose={() => setSidebarOpen(false)} />
3984
</aside>
4085

4186
<div className="app-main">
87+
<header className="site-header">
88+
<div className="site-header-inner">
89+
<NavLink to="/" className="site-brand">
90+
<span className="site-brand-mark">A7</span>
91+
</NavLink>
92+
93+
<nav className="site-nav" aria-label="Primary">
94+
{PRIMARY_NAV.map((item) => (
95+
item.kind === 'route' ? (
96+
<NavLink
97+
key={item.to}
98+
to={item.to}
99+
end={item.end}
100+
className={({ isActive }) => `site-nav-link${isActive ? ' active' : ''}`}
101+
>
102+
{item.label}
103+
</NavLink>
104+
) : (
105+
<a
106+
key={item.href}
107+
className="site-nav-link"
108+
href={item.href}
109+
target="_blank"
110+
rel="noopener noreferrer"
111+
>
112+
{item.label}
113+
</a>
114+
)
115+
))}
116+
</nav>
117+
118+
<div className="site-header-tools">
119+
<button type="button" className="site-search" aria-label="Search documentation">
120+
<span className="site-search-label">
121+
<svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
122+
<circle cx="11" cy="11" r="6.5" />
123+
<path strokeLinecap="round" d="M16 16l4 4" />
124+
</svg>
125+
Search docs
126+
</span>
127+
<span className="site-search-kbd">/</span>
128+
</button>
129+
130+
<button
131+
type="button"
132+
onClick={cycleTheme}
133+
className="theme-toggle"
134+
aria-label={`Theme: ${preference}. Click to cycle theme mode.`}
135+
title={darkExtensionActive ? 'Dark extension detected. Site theme will avoid double-dark styling.' : undefined}
136+
>
137+
<span className="theme-toggle-icon" aria-hidden="true">
138+
{resolvedTheme === 'dark' ? (
139+
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
140+
<path strokeLinecap="round" strokeLinejoin="round" d="M21 12.8A9 9 0 1111.2 3a7 7 0 009.8 9.8z" />
141+
</svg>
142+
) : preference === 'system' ? (
143+
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
144+
<rect x="3.5" y="4.5" width="17" height="12" rx="2" />
145+
<path strokeLinecap="round" d="M8 19.5h8M12 16.5v3" />
146+
</svg>
147+
) : (
148+
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
149+
<circle cx="12" cy="12" r="4.5" />
150+
<path strokeLinecap="round" d="M12 2.5v2.5M12 19v2.5M21.5 12H19M5 12H2.5M18.7 5.3l-1.8 1.8M7.1 16.9l-1.8 1.8M18.7 18.7l-1.8-1.8M7.1 7.1L5.3 5.3" />
151+
</svg>
152+
)}
153+
</span>
154+
<span className="theme-toggle-label">{preference}</span>
155+
{darkExtensionActive ? <span className="theme-toggle-badge">ext</span> : null}
156+
</button>
157+
158+
<button
159+
type="button"
160+
onClick={() => setSidebarOpen(true)}
161+
className="site-menu-button"
162+
aria-label="Open site navigation"
163+
>
164+
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
165+
<path strokeLinecap="round" strokeLinejoin="round" d="M4 7h16M4 12h16M4 17h16" />
166+
</svg>
167+
<span className="site-menu-button-text">Menu</span>
168+
</button>
169+
</div>
170+
</div>
171+
</header>
172+
42173
<header className="mobile-header">
43174
<div className="mobile-header-row">
44175
<button
@@ -60,14 +191,16 @@ export default function Layout() {
60191
<Outlet />
61192
</main>
62193

63-
<footer className="app-footer">
64-
<div className="app-footer-inner">
65-
A7 Programming Language ·{' '}
66-
<a href="https://github.com/Airbus5717/a7-py" target="_blank" rel="noopener noreferrer">
67-
GitHub
68-
</a>
69-
</div>
70-
</footer>
194+
{pathname !== '/' ? (
195+
<footer className="app-footer">
196+
<div className="app-footer-inner">
197+
<span>A7 Programming Language</span>
198+
<a href="https://github.com/Airbus5717/a7-py" target="_blank" rel="noopener noreferrer">
199+
GitHub
200+
</a>
201+
</div>
202+
</footer>
203+
) : null}
71204
</div>
72205
</div>
73206
)

site/src/components/MetricTile.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ interface MetricTileProps {
66

77
export default function MetricTile({ label, value, note }: MetricTileProps) {
88
return (
9-
<div className="metric-tile">
9+
<div className="metric-tile" data-reveal>
1010
<p className="metric-label">{label}</p>
1111
<p className="metric-value">{value}</p>
1212
{note ? <p className="metric-note">{note}</p> : null}

site/src/components/PageHeader.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ interface PageHeaderProps {
99

1010
export default function PageHeader({ title, summary, eyebrow, actions }: PageHeaderProps) {
1111
return (
12-
<header className="page-header">
13-
{eyebrow && <span className="page-header-eyebrow">{eyebrow}</span>}
14-
<h1 className="page-header-title">{title}</h1>
15-
<p className="page-header-summary">{summary}</p>
16-
{actions ? <div style={{ marginTop: 'var(--space-3)' }}>{actions}</div> : null}
12+
<header className="page-header" data-reveal>
13+
<div className="page-header-copy">
14+
{eyebrow && <span className="page-header-eyebrow">{eyebrow}</span>}
15+
<h1 className="page-header-title">{title}</h1>
16+
<p className="page-header-summary">{summary}</p>
17+
</div>
18+
{actions ? <div className="page-header-actions">{actions}</div> : null}
1719
</header>
1820
)
1921
}

site/src/components/SectionPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ interface SectionPanelProps {
99

1010
export default function SectionPanel({ title, subtitle, children, className = '' }: SectionPanelProps) {
1111
return (
12-
<section className={`section-panel ${className}`.trim()}>
12+
<section className={`section-panel ${className}`.trim()} data-reveal>
1313
{title && <h2 className="section-title">{title}</h2>}
1414
{subtitle && <p className="section-subtitle">{subtitle}</p>}
1515
{children}

site/src/hooks/useTheme.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { useEffect, useMemo, useState } from 'react'
2+
3+
export type ThemePreference = 'system' | 'light' | 'dark'
4+
type ResolvedTheme = 'light' | 'dark' | 'extension-dark'
5+
6+
const STORAGE_KEY = 'a7-docs-theme'
7+
8+
function readStoredPreference(): ThemePreference {
9+
if (typeof window === 'undefined') {
10+
return 'system'
11+
}
12+
13+
const stored = window.localStorage.getItem(STORAGE_KEY)
14+
return stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system'
15+
}
16+
17+
function hasDarkExtensionActive() {
18+
const html = document.documentElement
19+
const body = document.body
20+
21+
if (
22+
html.hasAttribute('data-darkreader-mode')
23+
|| html.hasAttribute('data-darkreader-scheme')
24+
|| html.classList.contains('darkreader')
25+
|| body?.classList.contains('darkreader')
26+
) {
27+
return true
28+
}
29+
30+
return Boolean(
31+
document.querySelector(
32+
'style.darkreader, link.darkreader, meta[name="darkreader"], style[data-darkreader], style[id*="dark-reader"]',
33+
),
34+
)
35+
}
36+
37+
export function useTheme() {
38+
const [preference, setPreference] = useState<ThemePreference>(() => readStoredPreference())
39+
const [prefersDark, setPrefersDark] = useState(false)
40+
const [darkExtensionActive, setDarkExtensionActive] = useState(false)
41+
42+
useEffect(() => {
43+
const media = window.matchMedia('(prefers-color-scheme: dark)')
44+
const updatePreference = () => setPrefersDark(media.matches)
45+
46+
updatePreference()
47+
media.addEventListener('change', updatePreference)
48+
49+
return () => media.removeEventListener('change', updatePreference)
50+
}, [])
51+
52+
useEffect(() => {
53+
const updateExtensionState = () => setDarkExtensionActive(hasDarkExtensionActive())
54+
55+
updateExtensionState()
56+
57+
const observer = new MutationObserver(updateExtensionState)
58+
observer.observe(document.documentElement, { attributes: true, childList: true, subtree: true })
59+
60+
return () => observer.disconnect()
61+
}, [])
62+
63+
const resolvedTheme = useMemo<ResolvedTheme>(() => {
64+
if (darkExtensionActive) {
65+
return 'extension-dark'
66+
}
67+
68+
if (preference === 'dark') {
69+
return 'dark'
70+
}
71+
72+
if (preference === 'light') {
73+
return 'light'
74+
}
75+
76+
return prefersDark ? 'dark' : 'light'
77+
}, [darkExtensionActive, preference, prefersDark])
78+
79+
useEffect(() => {
80+
window.localStorage.setItem(STORAGE_KEY, preference)
81+
}, [preference])
82+
83+
useEffect(() => {
84+
const root = document.documentElement
85+
root.dataset.themePreference = preference
86+
root.dataset.theme = resolvedTheme === 'dark' ? 'dark' : 'light'
87+
root.dataset.darkExtensionActive = darkExtensionActive ? 'true' : 'false'
88+
root.style.colorScheme = resolvedTheme === 'dark' ? 'dark' : 'light'
89+
}, [darkExtensionActive, preference, resolvedTheme])
90+
91+
const cycleTheme = () => {
92+
setPreference((current) => {
93+
if (current === 'system') {
94+
return 'light'
95+
}
96+
97+
if (current === 'light') {
98+
return 'dark'
99+
}
100+
101+
return 'system'
102+
})
103+
}
104+
105+
return {
106+
preference,
107+
resolvedTheme,
108+
darkExtensionActive,
109+
setPreference,
110+
cycleTheme,
111+
}
112+
}

0 commit comments

Comments
 (0)