Skip to content

Commit a8d73c3

Browse files
authored
Merge pull request #3 from adewale/claude/tuftean-marginalia-viz-TB0fw
Hero collapse, section blocks, editor polish, rubric scores
2 parents ff84e86 + 4144ef2 commit a8d73c3

13 files changed

Lines changed: 325 additions & 28 deletions
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { EditorState } from 'https://esm.sh/@codemirror/state@6.5.2';
2-
import { EditorView } from 'https://esm.sh/@codemirror/view@6.41.1?deps=@codemirror/state@6.5.2';
2+
import { EditorView, lineNumbers } from 'https://esm.sh/@codemirror/view@6.41.1?deps=@codemirror/state@6.5.2';
33
import { defaultHighlightStyle, syntaxHighlighting } from 'https://esm.sh/@codemirror/language@6.12.3?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1';
44
import { python } from 'https://esm.sh/@codemirror/lang-python@6.2.1?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1,@codemirror/language@6.12.3';
55

@@ -15,6 +15,7 @@ if (textarea && form) {
1515
extensions: [
1616
python(),
1717
syntaxHighlighting(defaultHighlightStyle),
18+
lineNumbers(),
1819
EditorView.lineWrapping,
1920
EditorView.updateListener.of((update) => {
2021
if (update.docChanged) textarea.value = update.state.doc.toString();

public/editor.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { EditorState } from 'https://esm.sh/@codemirror/state@6.5.2';
2-
import { EditorView } from 'https://esm.sh/@codemirror/view@6.41.1?deps=@codemirror/state@6.5.2';
2+
import { EditorView, lineNumbers } from 'https://esm.sh/@codemirror/view@6.41.1?deps=@codemirror/state@6.5.2';
33
import { defaultHighlightStyle, syntaxHighlighting } from 'https://esm.sh/@codemirror/language@6.12.3?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1';
44
import { python } from 'https://esm.sh/@codemirror/lang-python@6.2.1?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1,@codemirror/language@6.12.3';
55

@@ -15,6 +15,7 @@ if (textarea && form) {
1515
extensions: [
1616
python(),
1717
syntaxHighlighting(defaultHighlightStyle),
18+
lineNumbers(),
1819
EditorView.lineWrapping,
1920
EditorView.updateListener.of((update) => {
2021
if (update.docChanged) textarea.value = update.state.doc.toString();
Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,47 @@
2020
.cm-scroller { font-family: inherit; line-height: 1.5; }
2121
.cm-content { padding: 0; }
2222
.cm-line { padding: 0; }
23-
.cm-gutters { display: none; }
23+
.cm-gutters { background: transparent; border-right: 1px solid var(--hairline-soft); color: var(--muted); font-variant-numeric: tabular-nums; }
24+
.cm-lineNumbers .cm-gutterElement { padding: 0 var(--space-2) 0 0; min-width: 2ch; text-align: right; font-size: .85em; }
2425
.cm-activeLine, .cm-activeLineGutter { background: transparent; }
2526
.cm-selectionBackground, .cm-focused .cm-selectionBackground { background: rgba(255, 72, 1, 0.18) !important; }
2627
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-variant-numeric: tabular-nums; }
2728
.syntax-inline { padding: .08rem .25rem; border-radius: .25rem; background: var(--accent-soft); color: var(--text); font-size: .94em; }
2829
.brand { font-weight: 800; }
2930
.nav-links { display: flex; gap: .35rem; }
3031
.nav-links a { padding: 0 .9rem; color: var(--muted); }
31-
.hero { overflow: hidden; border: 1px solid var(--hairline); border-radius: 1rem; padding: clamp(1.5rem, 5vw, 4rem); margin-bottom: 1.25rem; background: linear-gradient(135deg, var(--surface), var(--surface-3)); box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); }
32-
.hero p { max-width: 66ch; color: var(--muted); font-size: 1.08rem; }
32+
.hero { overflow: hidden; border: 1px solid var(--hairline); border-radius: 1rem; padding: clamp(1.25rem, 3.5vw, 2.5rem); margin-bottom: 1.25rem; background: linear-gradient(135deg, var(--surface), var(--surface-3)); box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transform-origin: top left; }
33+
.hero h1 { font-size: clamp(2rem, 4vw, 3rem); margin-bottom: var(--space-3); transform-origin: top left; }
34+
.hero p { max-width: 60ch; color: var(--muted); font-size: 1rem; }
35+
@supports (animation-timeline: scroll()) {
36+
@media (prefers-reduced-motion: no-preference) {
37+
.hero { animation: hero-fade linear forwards; animation-timeline: scroll(root); animation-range: 0 280px; }
38+
.hero h1 { animation: hero-h1-morph linear forwards; animation-timeline: scroll(root); animation-range: 0 240px; }
39+
.hero p { animation: hero-p-fade linear forwards; animation-timeline: scroll(root); animation-range: 0 140px; }
40+
body:has(.hero) { padding-top: var(--space-2); }
41+
body:has(.hero) header { opacity: 0; background: rgba(245, 241, 235, 0); box-shadow: none; margin-bottom: var(--space-2); animation: header-emerge linear forwards; animation-timeline: scroll(root); animation-range: 40px 240px; }
42+
body:has(.hero) header .brand { filter: blur(4px); transform: scale(0.88); animation: brand-focus linear forwards; animation-timeline: scroll(root); animation-range: 80px 240px; }
43+
}
44+
}
45+
@keyframes hero-fade {
46+
to { opacity: 0; transform: scale(0.92) translateY(-8px); }
47+
}
48+
@keyframes hero-h1-morph {
49+
to { transform: scale(0.32) translate(-32%, -50%); opacity: 0; }
50+
}
51+
@keyframes hero-p-fade {
52+
to { opacity: 0; transform: translateY(-4px); }
53+
}
54+
@keyframes brand-focus {
55+
to { filter: blur(0); transform: scale(1); }
56+
}
57+
@keyframes header-emerge {
58+
to { opacity: 1; background: rgba(245, 241, 235, 0.95); box-shadow: 0 1px 8px rgba(82, 16, 0, 0.06); }
59+
}
3360
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--space-3); }
61+
.home-section { margin-top: var(--space-6); }
62+
.home-section:first-of-type { margin-top: 0; }
63+
.home-section .eyebrow { margin: 0 0 var(--space-2); }
3464
.card { display: block; min-height: 10rem; border: 1px solid var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface-2); color: inherit; text-decoration: none; box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transition-property: transform, background-color, border-color; transition-duration: 200ms; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); }
3565
.card:hover { transform: translateY(-2px); background: var(--surface-3); border-color: var(--accent); }
3666
.card h2 { text-decoration: underline; text-decoration-color: var(--hairline); text-underline-offset: .18em; }
@@ -81,9 +111,12 @@
81111
.playground { margin-top: var(--space-6); padding-top: var(--space-4); border-top: 1px solid var(--hairline); }
82112
.playground > h2 { font-size: clamp(1.5rem, 2.5vw, 2rem); letter-spacing: -0.03em; margin-bottom: var(--space-3); }
83113
.runner-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(18rem, .75fr); gap: var(--space-4); align-items: stretch; }
114+
@media (max-width: 980px) { .runner-grid { grid-template-columns: 1fr; } }
84115
.runner-panel { min-height: 18rem; display: flex; flex-direction: column; border: 1px dashed var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface); }
85-
.runner-panel h2 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; }
116+
.runner-panel h3 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; }
86117
.runner-panel pre { flex: 1; min-height: 0; overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; }
118+
.runner-editor { border-style: solid; background: var(--surface-2); cursor: text; box-shadow: inset 0 1px 2px rgba(82, 16, 0, 0.04); transition: box-shadow 160ms cubic-bezier(0.2, 0, 0, 1); }
119+
.runner-editor:focus-within { box-shadow: inset 0 1px 2px rgba(82, 16, 0, 0.04), 0 0 0 3px rgba(255, 72, 1, 0.12); }
87120
.execution-time { min-height: 1.5rem; margin: var(--space-2) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--hairline-soft); color: var(--muted); font-size: .88rem; font-variant-numeric: tabular-nums; }
88121
.playground-toolbar { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin: .8rem 0 1rem; }
89122
.tool-button { min-height: 40px; border: 1px solid var(--hairline); border-radius: 9999px; padding: .62rem .9rem; background: var(--surface-2); color: var(--text); cursor: pointer; transition-property: transform, background-color, border-style; transition-duration: 150ms; }

public/site.css

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,47 @@
2020
.cm-scroller { font-family: inherit; line-height: 1.5; }
2121
.cm-content { padding: 0; }
2222
.cm-line { padding: 0; }
23-
.cm-gutters { display: none; }
23+
.cm-gutters { background: transparent; border-right: 1px solid var(--hairline-soft); color: var(--muted); font-variant-numeric: tabular-nums; }
24+
.cm-lineNumbers .cm-gutterElement { padding: 0 var(--space-2) 0 0; min-width: 2ch; text-align: right; font-size: .85em; }
2425
.cm-activeLine, .cm-activeLineGutter { background: transparent; }
2526
.cm-selectionBackground, .cm-focused .cm-selectionBackground { background: rgba(255, 72, 1, 0.18) !important; }
2627
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-variant-numeric: tabular-nums; }
2728
.syntax-inline { padding: .08rem .25rem; border-radius: .25rem; background: var(--accent-soft); color: var(--text); font-size: .94em; }
2829
.brand { font-weight: 800; }
2930
.nav-links { display: flex; gap: .35rem; }
3031
.nav-links a { padding: 0 .9rem; color: var(--muted); }
31-
.hero { overflow: hidden; border: 1px solid var(--hairline); border-radius: 1rem; padding: clamp(1.5rem, 5vw, 4rem); margin-bottom: 1.25rem; background: linear-gradient(135deg, var(--surface), var(--surface-3)); box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); }
32-
.hero p { max-width: 66ch; color: var(--muted); font-size: 1.08rem; }
32+
.hero { overflow: hidden; border: 1px solid var(--hairline); border-radius: 1rem; padding: clamp(1.25rem, 3.5vw, 2.5rem); margin-bottom: 1.25rem; background: linear-gradient(135deg, var(--surface), var(--surface-3)); box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transform-origin: top left; }
33+
.hero h1 { font-size: clamp(2rem, 4vw, 3rem); margin-bottom: var(--space-3); transform-origin: top left; }
34+
.hero p { max-width: 60ch; color: var(--muted); font-size: 1rem; }
35+
@supports (animation-timeline: scroll()) {
36+
@media (prefers-reduced-motion: no-preference) {
37+
.hero { animation: hero-fade linear forwards; animation-timeline: scroll(root); animation-range: 0 280px; }
38+
.hero h1 { animation: hero-h1-morph linear forwards; animation-timeline: scroll(root); animation-range: 0 240px; }
39+
.hero p { animation: hero-p-fade linear forwards; animation-timeline: scroll(root); animation-range: 0 140px; }
40+
body:has(.hero) { padding-top: var(--space-2); }
41+
body:has(.hero) header { opacity: 0; background: rgba(245, 241, 235, 0); box-shadow: none; margin-bottom: var(--space-2); animation: header-emerge linear forwards; animation-timeline: scroll(root); animation-range: 40px 240px; }
42+
body:has(.hero) header .brand { filter: blur(4px); transform: scale(0.88); animation: brand-focus linear forwards; animation-timeline: scroll(root); animation-range: 80px 240px; }
43+
}
44+
}
45+
@keyframes hero-fade {
46+
to { opacity: 0; transform: scale(0.92) translateY(-8px); }
47+
}
48+
@keyframes hero-h1-morph {
49+
to { transform: scale(0.32) translate(-32%, -50%); opacity: 0; }
50+
}
51+
@keyframes hero-p-fade {
52+
to { opacity: 0; transform: translateY(-4px); }
53+
}
54+
@keyframes brand-focus {
55+
to { filter: blur(0); transform: scale(1); }
56+
}
57+
@keyframes header-emerge {
58+
to { opacity: 1; background: rgba(245, 241, 235, 0.95); box-shadow: 0 1px 8px rgba(82, 16, 0, 0.06); }
59+
}
3360
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--space-3); }
61+
.home-section { margin-top: var(--space-6); }
62+
.home-section:first-of-type { margin-top: 0; }
63+
.home-section .eyebrow { margin: 0 0 var(--space-2); }
3464
.card { display: block; min-height: 10rem; border: 1px solid var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface-2); color: inherit; text-decoration: none; box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transition-property: transform, background-color, border-color; transition-duration: 200ms; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); }
3565
.card:hover { transform: translateY(-2px); background: var(--surface-3); border-color: var(--accent); }
3666
.card h2 { text-decoration: underline; text-decoration-color: var(--hairline); text-underline-offset: .18em; }
@@ -81,9 +111,12 @@
81111
.playground { margin-top: var(--space-6); padding-top: var(--space-4); border-top: 1px solid var(--hairline); }
82112
.playground > h2 { font-size: clamp(1.5rem, 2.5vw, 2rem); letter-spacing: -0.03em; margin-bottom: var(--space-3); }
83113
.runner-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(18rem, .75fr); gap: var(--space-4); align-items: stretch; }
114+
@media (max-width: 980px) { .runner-grid { grid-template-columns: 1fr; } }
84115
.runner-panel { min-height: 18rem; display: flex; flex-direction: column; border: 1px dashed var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface); }
85-
.runner-panel h2 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; }
116+
.runner-panel h3 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; }
86117
.runner-panel pre { flex: 1; min-height: 0; overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; }
118+
.runner-editor { border-style: solid; background: var(--surface-2); cursor: text; box-shadow: inset 0 1px 2px rgba(82, 16, 0, 0.04); transition: box-shadow 160ms cubic-bezier(0.2, 0, 0, 1); }
119+
.runner-editor:focus-within { box-shadow: inset 0 1px 2px rgba(82, 16, 0, 0.04), 0 0 0 3px rgba(255, 72, 1, 0.12); }
87120
.execution-time { min-height: 1.5rem; margin: var(--space-2) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--hairline-soft); color: var(--muted); font-size: .88rem; font-variant-numeric: tabular-nums; }
88121
.playground-toolbar { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin: .8rem 0 1rem; }
89122
.tool-button { min-height: 40px; border: 1px solid var(--hairline); border-radius: 9999px; padding: .62rem .9rem; background: var(--surface-2); color: var(--text); cursor: pointer; transition-property: transform, background-color, border-style; transition-duration: 150ms; }

scripts/build_prototypes.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def render_article(example: dict, *, banners: dict[str, str] | None = None) -> s
102102
output = html.escape(example.get("expected_output", ""))
103103
return f"""
104104
<article class="example-shell">
105-
<div class="example-top"><a class="text-link" href="/"> All examples</a><a class="text-link" href="{html.escape(example['doc_url'])}">Python docs reference</a></div>
105+
<div class="example-top"><a class="text-link" href="/"> All examples</a><a class="text-link" href="{html.escape(example['doc_url'])}">Python docs reference</a></div>
106106
<section class="example-intro">
107107
<p class="eyebrow">{html.escape(example['section'])}</p>
108108
<h1>{html.escape(example['title'])}</h1>
@@ -115,10 +115,10 @@ def render_article(example: dict, *, banners: dict[str, str] | None = None) -> s
115115
<h2>Run the complete example</h2>
116116
<div class="runner-grid">
117117
<div class="runner-panel runner-editor">
118-
<h2>Example code</h2>
118+
<h3>Example code</h3>
119119
<pre><code class="language-python">{code}</code></pre>
120120
</div>
121-
<section class="runner-panel output-panel"><h2>Expected output</h2><pre><code>{output}</code></pre></section>
121+
<section class="runner-panel output-panel"><h3>Expected output</h3><pre><code>{output}</code></pre></section>
122122
</div>
123123
</section>
124124
</article>

src/app.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -499,22 +499,38 @@ def _layout(title: str, content: str, description: str | None = None, path: str
499499

500500

501501
def render_home() -> str:
502-
cards = []
502+
# Group examples by section in the order each section first appears
503+
# in the manifest. Each section gets its own .home-section wrapper
504+
# holding an eyebrow (tight, ~12px above its cards) and the
505+
# section's grid; sections are spaced ~48px apart for clear
506+
# separation. The shared outer .grid is gone — using one grid
507+
# per section gives explicit control over the eyebrow's vertical
508+
# relationship to its own cards vs the previous section.
509+
by_section: dict[str, list[dict]] = {}
503510
for example in list_examples():
504-
cards.append(
511+
by_section.setdefault(example["section"], []).append(example)
512+
sections_html = []
513+
for section, examples in by_section.items():
514+
card_markup = "".join(
505515
_replace(
506-
'<a class="card" href="/examples/__SLUG__"><p class="eyebrow">__SECTION__</p><h2>__TITLE__</h2><p class="meta">__SUMMARY__</p></a>',
516+
'<a class="card" href="/examples/__SLUG__"><h2>__TITLE__</h2><p class="meta">__SUMMARY__</p></a>',
507517
{
508-
"SECTION": html.escape(example["section"]),
509518
"SLUG": html.escape(example["slug"]),
510519
"TITLE": html.escape(example["title"]),
511520
"SUMMARY": html.escape(example["summary"]),
512521
},
513522
)
523+
for example in examples
524+
)
525+
sections_html.append(
526+
f'<section class="home-section">'
527+
f'<p class="eyebrow">{html.escape(section)}</p>'
528+
f'<div class="grid">{card_markup}</div>'
529+
f'</section>'
514530
)
515531
content = _replace(
516532
_template("home.html"),
517-
{"PYTHON_VERSION": html.escape(PYTHON_VERSION), "CARDS": "".join(cards)},
533+
{"PYTHON_VERSION": html.escape(PYTHON_VERSION), "CARDS": "".join(sections_html)},
518534
)
519535
return _layout(
520536
"Python By Example",
@@ -542,7 +558,7 @@ def render_journeys_index():
542558
<section class="hero">
543559
<p class="eyebrow">Journeys</p>
544560
<h1>Python learning journeys</h1>
545-
<p>These paths compose individual examples into larger mental maps. They are inspired by the way Apprenticeship Patterns treats small patterns as material for longer learning journeys.</p>
561+
<p>These paths compose individual examples into larger mental maps. They are inspired by the way <a class="text-link" href="https://www.oreilly.com/library/view/apprenticeship-patterns/9780596806842/">Apprenticeship Patterns</a> treats small patterns as material for longer learning journeys.</p>
546562
</section>
547563
<section class="grid journey-grid">{"".join(cards)}</section>
548564
'''
@@ -576,7 +592,7 @@ def render_journey_page(journey):
576592
sections.append(f'<section class="journey-section"><h2>{html.escape(section["title"])}</h2><p class="meta">{html.escape(section["summary"])}</p>{figure_html}<ul class="journey-list">{"".join(rows)}</ul></section>')
577593
content = f'''
578594
<article class="example-shell journey-page">
579-
<div class="example-top"><a class="text-link" href="/"> All examples</a><a class="text-link" href="{html.escape(REFERENCE_URL)}">Python docs reference</a></div>
595+
<div class="example-top"><a class="text-link" href="/"> All examples</a><a class="text-link" href="{html.escape(REFERENCE_URL)}">Python docs reference</a></div>
580596
<section class="example-intro">
581597
<p class="eyebrow">Journey</p>
582598
<h1>{html.escape(journey["title"])}</h1>

src/asset_manifest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Generated by scripts/fingerprint_assets.py. Do not edit by hand.
2-
ASSET_PATHS = {'SITE_CSS': '/site.1452cc5609f2.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'}
3-
HTML_CACHE_VERSION = '2ef350ca9050'
2+
ASSET_PATHS = {'SITE_CSS': '/site.57a55415849b.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.a4a7766e1b9b.js'}
3+
HTML_CACHE_VERSION = '324f7ab4825b'

0 commit comments

Comments
 (0)