Skip to content

Commit 276b462

Browse files
committed
Refresh documentation homepage with recorded demos and coloured cards
1 parent 235e7e7 commit 276b462

13 files changed

Lines changed: 734 additions & 41 deletions

File tree

‎docs/_ext/demo.py‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from docutils import nodes
1818
from docutils.parsers.rst import Directive
1919
from sphinx.application import Sphinx
20-
from sphinx.errors import NoUri
20+
from sphinx.errors import ExtensionError, NoUri
2121
from sphinx.util.osutil import relative_uri
2222

2323
if typing.TYPE_CHECKING:
@@ -155,8 +155,21 @@ def copy_example_sources(app: Sphinx, exception: Exception | None) -> None:
155155
)
156156

157157

158+
def validate_showcase_assets(_app: Sphinx) -> None:
159+
"""Check recordings embedded as raw HTML on the documentation homepage."""
160+
name: str
161+
for name in ('readme/colors', 'readme/multibar', 'readme/hero'):
162+
svg_path: pathlib.Path = DEMOS_BY_NAME[name].svg_path
163+
if not svg_path.is_file():
164+
raise ExtensionError(
165+
f'showcase animation not rendered: {name} '
166+
f'(run: python scripts/render_demos.py --only {name})'
167+
)
168+
169+
158170
def setup(app: Sphinx) -> dict[str, typing.Any]:
159171
app.add_directive('demo', DemoDirective)
172+
app.connect('builder-inited', validate_showcase_assets)
160173
app.connect('build-finished', copy_example_sources)
161174
app.add_css_file('vendor/xterm.css')
162175
app.add_css_file('livecode/livecode.css')

‎docs/_static/brand.css‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* Shared typography and controls for the homepage and Furo documentation. */
2+
body {
3+
--home-accent: #6550b5;
4+
--home-card-transfer: #e1f0ee;
5+
--home-card-jobs: #eee5f7;
6+
--home-card-logs: #f8edcf;
7+
--home-surface: #efece5;
8+
}
9+
body[data-theme="dark"] {
10+
--home-accent: #b9a5f0;
11+
--home-card-transfer: #203a37;
12+
--home-card-jobs: #352c44;
13+
--home-card-logs: #403922;
14+
--home-surface: #28272b;
15+
}
16+
@media (prefers-color-scheme: dark) {
17+
body:not([data-theme="light"]) {
18+
--home-accent: #b9a5f0;
19+
--home-card-transfer: #203a37;
20+
--home-card-jobs: #352c44;
21+
--home-card-logs: #403922;
22+
--home-surface: #28272b;
23+
}
24+
}
25+
a { text-underline-offset: 0.18em; }
26+
a:focus-visible, button:focus-visible, input:focus-visible,
27+
textarea:focus-visible, [tabindex]:focus-visible {
28+
outline: 3px solid #b87917;
29+
outline-offset: 4px;
30+
}
31+
.sidebar-brand-text { font-weight: 750; letter-spacing: -0.035em; }
32+
.demo-button { background: #6550b5; color: #fff; }
33+
.visually-hidden {
34+
position: absolute;
35+
width: 1px;
36+
height: 1px;
37+
padding: 0;
38+
overflow: hidden;
39+
clip-path: inset(50%);
40+
white-space: nowrap;
41+
}

‎docs/_static/demos/tutorial-step1.svg‎

Lines changed: 1 addition & 1 deletion
Loading

‎docs/_static/home.css‎

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
.home-page { max-width: 1120px; margin: auto; }
2+
.home-page * { box-sizing: border-box; }
3+
.home-page #progressbar2 > h1 { display: none; }
4+
.home-page h1, .home-page h2, .home-page h3 { color: var(--color-foreground-primary); }
5+
.home-page h2 { margin: 0 0 22px; font-size: 28px; letter-spacing: -0.035em; }
6+
.home-page h3 { margin: 0 0 10px; font-size: 19px; }
7+
.home-page a { color: var(--home-accent); }
8+
.home-header {
9+
min-height: 88px;
10+
padding: 20px 40px;
11+
display: flex;
12+
gap: 26px;
13+
align-items: center;
14+
border-bottom: 1px solid var(--color-background-border);
15+
}
16+
.home-brand { font-size: 24px; font-weight: 800; letter-spacing: -0.045em; text-decoration: none; }
17+
.home-nav { display: flex; gap: 20px; font-size: 13px; }
18+
.home-nav a { color: var(--color-foreground-primary); text-decoration: none; }
19+
.home-nav a:hover, .home-footer a:hover { text-decoration: underline; }
20+
.home-tools { display: flex; align-items: center; gap: 15px; margin-left: auto; }
21+
.home-search input { width: 145px; font: inherit; font-size: 13px; padding: 9px 11px; border: 1px solid var(--color-background-border); border-radius: 6px; background: var(--color-background-primary); color: var(--color-foreground-primary); }
22+
.home-version { font-size: 12px; white-space: nowrap; }
23+
.home-tools .theme-toggle { padding: 4px; height: 32px; width: 32px; color: var(--color-foreground-primary); cursor: pointer; }
24+
.home-tools .theme-toggle svg { width: 24px; height: 24px; }
25+
.no-js .home-tools .theme-toggle { display: none; }
26+
.home-hero { padding: 60px 40px 24px; }
27+
.home-hero-top { display: grid; grid-template-columns: 1.35fr 1fr; gap: 52px; align-items: center; }
28+
.home-eyebrow { color: var(--home-accent); font: 12px var(--font-stack--monospace); text-transform: uppercase; letter-spacing: 0.12em; margin: 0; }
29+
.home-page #home-title { font-size: clamp(46px, 5.7vw, 64px); line-height: 1.07; font-weight: 750; letter-spacing: -0.045em; margin: 18px 0 22px; }
30+
#home-title span { color: var(--home-accent); }
31+
.home-description { margin: 0; font-size: 17px; line-height: 1.7; max-width: 610px; }
32+
.home-install { padding: 25px; background: var(--home-surface); border-radius: 14px; }
33+
.home-install-label { margin: 0 0 16px; font-size: 14px; font-weight: 650; }
34+
.home-install pre { margin: 0; padding: 16px; border: 1px solid var(--color-background-border); border-radius: 6px; background: var(--color-background-primary); overflow-x: auto; }
35+
.home-install code { font-size: 14px; background: none; padding: 0; white-space: pre; }
36+
.home-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; }
37+
.home-button { display: inline-block; padding: 11px 14px; font-size: 13px; font-weight: 650; border: 1px solid transparent; border-radius: 6px; text-decoration: none; }
38+
.home-page .home-button.primary { background: #6550b5; color: #fff; }
39+
.home-page .home-button.secondary { border-color: var(--color-background-border); color: var(--home-accent); }
40+
.home-button:hover { filter: brightness(0.93); }
41+
.home-install-note { margin: 16px 0 0; font-size: 12px; line-height: 1.7; color: var(--color-foreground-secondary); }
42+
.home-showcase { margin-top: 35px; border: 1px solid #38414e; border-radius: 10px; overflow: hidden; background: #101418; scroll-margin-top: 24px; }
43+
.home-terminal-title { padding: 13px 18px; color: #b9c5d2; font: 12px var(--font-stack--monospace); border-bottom: 1px solid #323b46; }
44+
.home-recording-scroll { overflow-x: auto; background: #101418; }
45+
#showcase-recording { display: block; width: 100%; min-width: 900px; aspect-ratio: 1080 / 168; border: 0; }
46+
#showcase-recording p { color: #d6e2ef; padding: 22px; }
47+
.home-recording-controls { display: flex; justify-content: space-between; gap: 12px; align-items: center; padding: 10px 14px; background: #18202a; }
48+
.home-recording-controls[hidden] { display: none; }
49+
.home-recording-tabs { display: flex; flex-wrap: wrap; gap: 5px; }
50+
.home-recording-controls button { font: 12px var(--font-stack); border: 1px solid transparent; border-radius: 5px; padding: 9px 11px; color: #c5ced9; background: transparent; cursor: pointer; }
51+
.home-recording-controls button[aria-selected="true"] { background: #6550b5; color: #fff; }
52+
.home-recording-controls button:hover { border-color: #8090a2; }
53+
.home-recording-controls button:disabled { color: #94a0b0; cursor: default; }
54+
#showcase-pause { white-space: nowrap; }
55+
.home-caption { margin: 10px 0 0; color: var(--color-foreground-secondary); font-size: 12px; line-height: 1.7; }
56+
.home-motion-note { display: none; }
57+
@media (prefers-reduced-motion: reduce) {
58+
.home-motion-note { display: inline; }
59+
}
60+
.home-use-cases { padding: 24px 40px 0; }
61+
.home-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
62+
.home-card { padding: 24px; border-radius: 8px; border: 1px solid #80808012; }
63+
.home-card.transfer { background: var(--home-card-transfer); }
64+
.home-card.jobs { background: var(--home-card-jobs); }
65+
.home-card.logs { background: var(--home-card-logs); }
66+
.home-card p { margin: 0 0 18px; font-size: 14px; line-height: 1.7; }
67+
.home-page .home-card a { color: inherit; }
68+
.home-card > a { font-size: 12px; font-weight: 650; text-decoration: none; }
69+
.home-card > a:hover { text-decoration: underline; }
70+
.home-learn { display: grid; grid-template-columns: 0.9fr 1.25fr; align-items: start; gap: 50px; padding: 62px 40px 0; }
71+
.home-learn > div { min-width: 0; }
72+
.home-learn-intro p { font-size: 15px; line-height: 1.8; color: var(--color-foreground-secondary); }
73+
.home-quickstart h3 { font-size: 15px; font-weight: 650; margin: 0 0 6px; }
74+
.home-code-note { font-size: 12px; line-height: 1.7; margin: 0 0 12px; color: var(--color-foreground-secondary); }
75+
.home-quickstart .demo { margin: 0; background: var(--home-surface); }
76+
.home-quickstart .demo-animation { display: none; }
77+
.home-quickstart .demo-source { max-height: 330px; overflow: auto; }
78+
.home-quickstart .demo-source pre { font-size: 12px; }
79+
.home-quickstart .demo:has(.demo-editor) .demo-source { display: none; }
80+
.home-quickstart .demo-editor { font-size: 12px; line-height: 1.6; max-height: 400px; }
81+
.home-quickstart .demo-terminal { display: none; }
82+
.home-quickstart .demo-run[data-started] .demo-terminal { display: block; }
83+
.home-quickstart .demo-controls { border-top: 0; padding: 11px 14px; }
84+
.home-quickstart .demo-button { padding: 6px 16px; font: 13px var(--font-stack); }
85+
.home-quickstart .demo-run:not(:has(.demo-editor)) { padding: 12px; font-size: 13px; }
86+
.home-guide-links { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin: 48px 40px 42px; padding-top: 27px; border-top: 1px solid var(--color-background-border); }
87+
.home-guide-links a { text-decoration: none; }
88+
.home-guide-links a:hover strong { text-decoration: underline; }
89+
.home-guide-links strong { display: block; font-size: 14px; margin-bottom: 8px; }
90+
.home-guide-links span { display: block; font-size: 12px; color: var(--color-foreground-secondary); }
91+
.home-footer { border-top: 1px solid var(--color-background-border); padding: 25px 40px 35px; display: flex; justify-content: space-between; gap: 20px; font-size: 12px; color: var(--color-foreground-secondary); }
92+
.home-footer nav { display: flex; flex-wrap: wrap; gap: 18px; }
93+
.home-footer a { color: inherit; text-decoration: none; }
94+
@media (max-width: 1000px) {
95+
.home-header { flex-wrap: wrap; gap: 18px; }
96+
.home-tools { margin-left: auto; }
97+
.home-nav { order: 3; width: 100%; }
98+
.home-hero-top { gap: 30px; }
99+
.home-actions { flex-direction: column; align-items: flex-start; }
100+
}
101+
@media (max-width: 768px) {
102+
.home-header { padding: 20px 22px; gap: 16px; }
103+
.home-brand { font-size: 22px; }
104+
.home-tools { gap: 12px; }
105+
.home-version { font-size: 11px; }
106+
.home-search input { width: 125px; }
107+
.home-hero { padding: 35px 22px 22px; }
108+
.home-hero-top, .home-learn { grid-template-columns: 1fr; gap: 28px; }
109+
.home-actions { flex-direction: row; }
110+
.home-cards { grid-template-columns: 1fr; }
111+
.home-use-cases { padding: 16px 22px 0; }
112+
.home-card { padding: 24px; }
113+
.home-learn { padding: 40px 22px 0; }
114+
.home-recording-controls { flex-wrap: wrap; gap: 4px; }
115+
.home-recording-tabs { width: 100%; }
116+
.home-recording-controls button { padding: 9px; }
117+
.home-guide-links { grid-template-columns: repeat(2, 1fr); margin: 38px 22px 30px; gap: 24px; }
118+
.home-footer { padding: 24px 22px 70px; flex-direction: column; }
119+
}
120+
@media (max-width: 480px) {
121+
.home-tools { width: 100%; margin: 0; }
122+
.home-search { flex: 1; }
123+
.home-search input { width: 100%; }
124+
.home-nav { order: 0; gap: 16px; }
125+
.home-page #home-title { font-size: 48px; }
126+
.home-install { padding: 20px; }
127+
}

‎docs/_static/livecode/livecode.js‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,10 @@ function createPanel(container, source) {
9595
const editor = document.createElement('textarea');
9696
editor.className = 'demo-editor';
9797
editor.value = source;
98-
editor.rows = source.split('\n').length;
98+
editor.rows = source.trimEnd().split('\n').length;
99+
editor.wrap = 'off';
99100
editor.spellcheck = false;
101+
editor.setAttribute('aria-label', 'Python example source');
100102

101103
const button = document.createElement('button');
102104
button.className = 'demo-button';
@@ -105,6 +107,7 @@ function createPanel(container, source) {
105107

106108
const status = document.createElement('span');
107109
status.className = 'demo-status';
110+
status.setAttribute('role', 'status');
108111

109112
const screen = document.createElement('div');
110113
screen.className = 'demo-terminal';
@@ -138,6 +141,7 @@ function createPanel(container, source) {
138141
};
139142

140143
button.addEventListener('click', async () => {
144+
container.dataset.started = 'true';
141145
activePanel = panel;
142146
panel.setStatus('loading');
143147
terminal.reset();
@@ -192,7 +196,10 @@ document.addEventListener('DOMContentLoaded', () => {
192196
continue;
193197
}
194198
fetch(container.dataset.source)
195-
.then((response) => response.text())
199+
.then((response) => {
200+
if (!response.ok) throw new Error('Example source unavailable.');
201+
return response.text();
202+
})
196203
.then((source) => createPanel(container, source))
197204
.catch(() => {
198205
container.textContent = 'Live console unavailable.';

‎docs/_static/showcase.js‎

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/* Recorded examples use the SVG animation clock, independent of Python. */
2+
/** @returns {void} */
3+
(() => {
4+
/** @type {HTMLElement | null} */
5+
const recording = document.getElementById('showcase-recording');
6+
/** @type {Element | null} */
7+
const controls = document.querySelector('.home-recording-controls');
8+
/** @type {HTMLElement | null} */
9+
const panel = document.getElementById('showcase-panel');
10+
/** @type {HTMLElement | null} */
11+
const title = document.getElementById('showcase-title');
12+
/** @type {HTMLElement | null} */
13+
const guide = document.getElementById('showcase-guide');
14+
/** @type {HTMLElement | null} */
15+
const pause = document.getElementById('showcase-pause');
16+
if (!(recording instanceof HTMLObjectElement)
17+
|| !(controls instanceof HTMLElement)
18+
|| !(guide instanceof HTMLAnchorElement)
19+
|| !(pause instanceof HTMLButtonElement) || !panel || !title) return;
20+
21+
/** @type {HTMLButtonElement[]} */
22+
const tabs = Array.from(controls.querySelectorAll('button[role="tab"]'))
23+
.filter(/** @param {Element} tab */ (tab) => tab instanceof HTMLButtonElement);
24+
if (!tabs.length) return;
25+
/** @type {MediaQueryList} */
26+
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
27+
/** @type {boolean} */
28+
let paused = false;
29+
30+
/** @returns {SVGSVGElement | null} */
31+
function svgRoot() {
32+
try {
33+
/** @type {SVGSVGElement | null} */
34+
const root = recording.contentDocument?.querySelector('svg') || null;
35+
return typeof root?.pauseAnimations === 'function' ? root : null;
36+
} catch {
37+
return null;
38+
}
39+
}
40+
41+
/** @returns {void} */
42+
function updatePlayback() {
43+
/** @type {SVGSVGElement | null} */
44+
const root = svgRoot();
45+
pause.disabled = !root || reducedMotion.matches;
46+
pause.textContent = reducedMotion.matches
47+
? 'Reduced motion'
48+
: paused ? 'Resume recording' : 'Pause recording';
49+
if (!root) return;
50+
/** @type {DOMRect} */
51+
const bounds = root.viewBox.baseVal;
52+
recording.style.aspectRatio = `${bounds.width} / ${bounds.height}`;
53+
if (paused || reducedMotion.matches) root.pauseAnimations();
54+
else root.unpauseAnimations();
55+
}
56+
57+
/** @param {HTMLButtonElement} tab @returns {void} */
58+
function selectTab(tab) {
59+
/** @type {string | undefined} */
60+
const source = tab.dataset.recording;
61+
/** @type {string | undefined} */
62+
const heading = tab.dataset.title;
63+
/** @type {string | undefined} */
64+
const href = tab.dataset.guide;
65+
if (!source || !heading || !href) return;
66+
for (/** @type {HTMLButtonElement} */ const item of tabs) {
67+
/** @type {boolean} */
68+
const selected = item === tab;
69+
item.setAttribute('aria-selected', String(selected));
70+
item.tabIndex = selected ? 0 : -1;
71+
}
72+
panel.setAttribute('aria-labelledby', tab.id);
73+
title.textContent = heading;
74+
guide.href = href;
75+
recording.setAttribute('aria-label', `Recorded output: ${heading}`);
76+
/** @type {HTMLAnchorElement | null} */
77+
const fallback = recording.querySelector('a');
78+
if (fallback) fallback.href = href;
79+
if (recording.getAttribute('data') !== source) {
80+
pause.disabled = true;
81+
recording.data = source;
82+
}
83+
}
84+
85+
/** @param {KeyboardEvent} event @param {number} index @returns {void} */
86+
function navigateTabs(event, index) {
87+
/** @type {Record<string, number>} */
88+
const destinations = {
89+
ArrowRight: (index + 1) % tabs.length,
90+
ArrowLeft: (index + tabs.length - 1) % tabs.length,
91+
Home: 0,
92+
End: tabs.length - 1,
93+
};
94+
if (!(event.key in destinations)) return;
95+
event.preventDefault();
96+
/** @type {HTMLButtonElement} */
97+
const tab = tabs[destinations[event.key]];
98+
tab.focus();
99+
selectTab(tab);
100+
}
101+
102+
/** @param {HTMLButtonElement} tab @param {number} index @returns {void} */
103+
function attachTab(tab, index) {
104+
tab.addEventListener('click', /** @returns {void} */ () => selectTab(tab));
105+
tab.addEventListener('keydown',
106+
/** @param {KeyboardEvent} event @returns {void} */
107+
(event) => navigateTabs(event, index));
108+
}
109+
110+
tabs.forEach(attachTab);
111+
pause.addEventListener('click', /** @returns {void} */ () => {
112+
paused = !paused;
113+
updatePlayback();
114+
});
115+
recording.addEventListener('load', updatePlayback);
116+
recording.addEventListener('error', /** @returns {void} */ () => {
117+
pause.disabled = true;
118+
});
119+
reducedMotion.addEventListener('change', updatePlayback);
120+
panel.setAttribute('role', 'tabpanel');
121+
panel.setAttribute('aria-labelledby', tabs[0].id);
122+
controls.hidden = false;
123+
updatePlayback();
124+
})();

0 commit comments

Comments
 (0)