Skip to content

Commit 6cc9b85

Browse files
apiadclaude
andcommitted
feat(examples): canonical 01_static — design-tokens reference page
Pure markup + CSS, no server. Exercises: - Document + Element + HTML builder with a chunky body tree - StyleSheet with normalize.css preamble + ~30 class selectors - Style fluent API: .font/.color/.padding/.background/.border/.rounded/.flexbox - A wide slice of Colors.* named registry (22 colors via ElementSet.spawn) - Unit types: px / rem / em / pc (%) - Document/StyleSheet.render(path) → 01_static.html + .css on disk Thin smoke test verifies the module renders to disk and the output contains expected markers (DOCTYPE, title, swatch markup with inline rgba background, .swatch-chip + .tokens-heading selectors in CSS). Three framework rough edges surfaced and filed at issues/7: - selector parser has no descendant combinator (forces flat class names) - ElementSet.spawn(iterable, tag) param naming is misleading - Element.div().span() chaining stops at the parent Generated examples/*.html and examples/*.css added to .gitignore so running the example doesn't dirty the working tree. 38 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a1006e3 commit 6cc9b85

4 files changed

Lines changed: 354 additions & 0 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,7 @@ dmypy.json
128128
# Pyre type checker
129129
.pyre/
130130
.issues-sync-state
131+
132+
# Generated outputs from canonical examples (run-locally artifacts)
133+
examples/*.html
134+
examples/*.css

examples/01_static.py

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
"""Tier 1 canonical example — a design-tokens reference page.
2+
3+
Pure markup + CSS, no server. Demonstrates `Document`, `StyleSheet`, the
4+
`Style` fluent builder, the `Colors` named registry, `ElementSet.spawn` for
5+
generated grids, and the `Unit` types (px, rem, em, %).
6+
7+
Run:
8+
9+
python examples/01_static.py
10+
11+
Then open `01_static.html` in a browser. The script writes that file and
12+
`01_static.css` alongside itself.
13+
14+
Note: violetear's CSS selector parser only supports compound selectors
15+
(tag + classes + id + pseudo) — no descendant combinators. So every
16+
nested element gets its own flat class name (`.swatch-chip` instead of
17+
`.swatch .chip`). This is a real framework limitation worth tracking.
18+
"""
19+
20+
from pathlib import Path
21+
22+
from violetear import Document, HTML, Style, StyleSheet
23+
from violetear.color import Colors, hex
24+
from violetear.units import em, pc, px, rem
25+
26+
27+
# ---------------------------------------------------------------------------
28+
# Token tables
29+
# ---------------------------------------------------------------------------
30+
31+
PALETTE = [
32+
Colors.Coral,
33+
Colors.Tomato,
34+
Colors.Gold,
35+
Colors.Khaki,
36+
Colors.OliveDrab,
37+
Colors.ForestGreen,
38+
Colors.SeaGreen,
39+
Colors.Teal,
40+
Colors.SteelBlue,
41+
Colors.RoyalBlue,
42+
Colors.MidnightBlue,
43+
Colors.Indigo,
44+
Colors.BlueViolet,
45+
Colors.MediumOrchid,
46+
Colors.Crimson,
47+
Colors.DeepPink,
48+
Colors.Chocolate,
49+
Colors.SaddleBrown,
50+
Colors.Lavender,
51+
Colors.Silver,
52+
Colors.DimGray,
53+
Colors.Black,
54+
]
55+
56+
# Typography demo: rendered as spans with size-variant classes so we don't
57+
# rely on a descendant combinator to style "sample h1" etc.
58+
TYPE_SCALE = [
59+
("type-h1", "Aa — Heading 1", "2.5rem · 700"),
60+
("type-h2", "Aa — Heading 2", "2.0rem · 700"),
61+
("type-h3", "Aa — Heading 3", "1.5rem · 600"),
62+
("type-h4", "Aa — Heading 4", "1.25rem · 600"),
63+
("type-h5", "Aa — Heading 5", "1.1rem · 600"),
64+
("type-h6", "Aa — Heading 6", "1.0rem · 600"),
65+
(
66+
"type-body",
67+
"Body — the quick brown fox jumps over the lazy dog.",
68+
"1.0rem · 400",
69+
),
70+
("type-small", "Small — annotations and metadata.", "0.875rem · 400"),
71+
("type-caption", "Caption — image labels and footnotes.", "0.75rem · 400"),
72+
]
73+
74+
SPACING_STEPS = [4, 8, 16, 24, 32, 48, 64]
75+
76+
UNITS_DEMO = [
77+
("32px (absolute)", px(32)),
78+
("2rem (root-relative)", rem(2.0)),
79+
("2em (parent-relative)", em(2.0)),
80+
("25% (container-relative)", pc(0.25)),
81+
]
82+
83+
84+
# ---------------------------------------------------------------------------
85+
# Stylesheet — flat class selectors only
86+
# ---------------------------------------------------------------------------
87+
88+
sheet = StyleSheet(normalize=True)
89+
90+
sheet.select("body").font(
91+
size=rem(1.0), family="system-ui, -apple-system, 'Segoe UI', sans-serif"
92+
).color(Colors.DarkSlateGray).background(Colors.WhiteSmoke).padding(rem(2.5))
93+
94+
sheet.select(".page").rules(max_width="960px").margin("auto").background(
95+
Colors.White
96+
).padding(rem(2.5)).rounded(px(8)).border(px(1), Colors.Gainsboro)
97+
98+
sheet.select(".page-title").font(size=rem(2.5), weight=700).color(Colors.Indigo).margin(
99+
bottom=rem(0.25)
100+
)
101+
102+
sheet.select(".subtitle").color(Colors.SlateGray).font(size=rem(1.0)).margin(
103+
bottom=rem(2.0)
104+
)
105+
106+
sheet.select(".tokens-section").margin(top=rem(2.0))
107+
108+
sheet.select(".tokens-heading").font(size=rem(1.5), weight=600).color(
109+
Colors.DarkSlateGray
110+
).rules(border_bottom=f"2px solid {Colors.Lavender}").padding(bottom=rem(0.25)).margin(
111+
bottom=rem(1.0)
112+
)
113+
114+
# Palette grid
115+
sheet.select(".palette").rules(
116+
display="grid",
117+
grid_template_columns="repeat(auto-fill, minmax(120px, 1fr))",
118+
gap="16px",
119+
)
120+
sheet.select(".swatch").flexbox(direction="column", gap=px(6))
121+
sheet.select(".swatch-chip").rules(
122+
height="72px", border="1px solid rgba(0,0,0,0.08)"
123+
).rounded(px(6))
124+
sheet.select(".swatch-name").font(size=rem(0.875), weight=600).color(
125+
Colors.DarkSlateGray
126+
)
127+
sheet.select(".swatch-hex").font(
128+
size=rem(0.75), family="ui-monospace, monospace"
129+
).color(Colors.SlateGray)
130+
131+
# Typography rows — each row is sample text + spec
132+
sheet.select(".type-row").flexbox(
133+
direction="row", gap=px(24), align="baseline"
134+
).padding(top=rem(0.5), bottom=rem(0.5)).rules(
135+
border_bottom=f"1px solid {Colors.Gainsboro}"
136+
)
137+
sheet.select(".type-sample").rules(flex_grow=1)
138+
sheet.select(".type-rule").font(size=rem(0.8), family="ui-monospace, monospace").color(
139+
Colors.SlateGray
140+
)
141+
sheet.select(".type-h1").font(size=rem(2.5), weight=700).color(Colors.Indigo)
142+
sheet.select(".type-h2").font(size=rem(2.0), weight=700).color(Colors.Indigo)
143+
sheet.select(".type-h3").font(size=rem(1.5), weight=600).color(Colors.DarkSlateGray)
144+
sheet.select(".type-h4").font(size=rem(1.25), weight=600).color(Colors.DarkSlateGray)
145+
sheet.select(".type-h5").font(size=rem(1.1), weight=600).color(Colors.DarkSlateGray)
146+
sheet.select(".type-h6").font(size=rem(1.0), weight=600).color(Colors.DarkSlateGray)
147+
sheet.select(".type-body").font(size=rem(1.0), weight=400).color(Colors.DarkSlateGray)
148+
sheet.select(".type-small").font(size=rem(0.875), weight=400).color(Colors.SlateGray)
149+
sheet.select(".type-caption").font(size=rem(0.75), weight=400).color(Colors.SlateGray)
150+
151+
# Spacing
152+
sheet.select(".spacing").flexbox(direction="column", gap=px(8))
153+
sheet.select(".spacing-row").flexbox(direction="row", gap=px(12), align="center")
154+
sheet.select(".spacing-bar").rules(height="16px").background(Colors.Coral).rounded(
155+
px(2)
156+
)
157+
sheet.select(".spacing-label").font(
158+
size=rem(0.8), family="ui-monospace, monospace"
159+
).color(Colors.SlateGray).rules(min_width="60px")
160+
161+
# Units
162+
sheet.select(".units").flexbox(direction="column", gap=px(8))
163+
sheet.select(".units-row").flexbox(direction="row", gap=px(12), align="center")
164+
sheet.select(".units-bar").rules(height="16px").background(Colors.SteelBlue).rounded(
165+
px(2)
166+
)
167+
sheet.select(".units-label").font(
168+
size=rem(0.8), family="ui-monospace, monospace"
169+
).color(Colors.SlateGray).rules(min_width="160px")
170+
171+
172+
# ---------------------------------------------------------------------------
173+
# Document tree
174+
# ---------------------------------------------------------------------------
175+
176+
doc = Document(title="violetear · Design Tokens")
177+
doc.style(href="01_static.css")
178+
179+
180+
def _populate_swatch(color, el):
181+
"""Fill one swatch cell. When spawn() iterates an iterable, the first
182+
arg is the item itself (here a Color), not an integer index."""
183+
el.classes("swatch").extend(
184+
HTML.div(classes="swatch-chip").style(Style().background(color)),
185+
HTML.div(text=color.name or "—", classes="swatch-name"),
186+
HTML.div(text=hex(color), classes="swatch-hex"),
187+
)
188+
189+
190+
with doc.body as body:
191+
with body.div(classes="page") as page:
192+
page.h1(text="Design Tokens").classes("page-title")
193+
page.p(
194+
text="A reference page for the violetear named-color palette, type scale, spacing, and units."
195+
).classes("subtitle")
196+
197+
# Palette — generated grid via ElementSet.spawn.
198+
with page.div(classes="tokens-section") as palette_sec:
199+
palette_sec.h2(text="Palette").classes("tokens-heading")
200+
palette_grid = palette_sec.div(classes="palette")
201+
palette_grid.spawn(PALETTE, "div").each(_populate_swatch)
202+
203+
# Typography — each row uses a size-variant class on the sample span.
204+
with page.div(classes="tokens-section") as type_sec:
205+
type_sec.h2(text="Typography").classes("tokens-heading")
206+
for variant, sample_text, rule in TYPE_SCALE:
207+
with type_sec.div(classes="type-row") as row:
208+
row.div(classes="type-sample").add(
209+
HTML.span(text=sample_text).classes(variant)
210+
)
211+
row.div(text=rule, classes="type-rule")
212+
213+
# Spacing
214+
with page.div(classes="tokens-section") as space_sec:
215+
space_sec.h2(text="Spacing").classes("tokens-heading")
216+
with space_sec.div(classes="spacing") as box:
217+
for step in SPACING_STEPS:
218+
with box.div(classes="spacing-row") as row:
219+
row.div(text=f"{step}px", classes="spacing-label")
220+
row.div(classes="spacing-bar").style(Style().width(px(step)))
221+
222+
# Units
223+
with page.div(classes="tokens-section") as units_sec:
224+
units_sec.h2(text="Units").classes("tokens-heading")
225+
with units_sec.div(classes="units") as box:
226+
for label, unit in UNITS_DEMO:
227+
with box.div(classes="units-row") as row:
228+
row.div(text=label, classes="units-label")
229+
row.div(classes="units-bar").style(Style().width(unit))
230+
231+
232+
# ---------------------------------------------------------------------------
233+
# Entry point
234+
# ---------------------------------------------------------------------------
235+
236+
237+
def write_to(out_dir: Path) -> tuple[Path, Path]:
238+
"""Render the document and stylesheet to disk. Returns (html_path, css_path)."""
239+
html_path = out_dir / "01_static.html"
240+
css_path = out_dir / "01_static.css"
241+
doc.render(html_path)
242+
sheet.render(css_path)
243+
return html_path, css_path
244+
245+
246+
if __name__ == "__main__":
247+
html_path, css_path = write_to(Path(__file__).parent)
248+
print(f"Wrote {html_path}")
249+
print(f"Wrote {css_path}")
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
number: 7
3+
title: "Framework rough edges surfaced during canonical-examples build"
4+
state: open
5+
labels:
6+
---
7+
8+
# Framework rough edges surfaced during canonical-examples build
9+
10+
Running log of small framework limitations and ergonomic friction discovered while building the canonical examples (see [[6-canonical-examples-design]]). Per that spec's policy, the build continues; gaps land here as a backlog rather than blocking forward progress.
11+
12+
## 7.1 — Selector parser has no descendant combinator
13+
14+
**Tier(s):** 01
15+
16+
**Symptom.** `sheet.select("section.tokens h2")` raises `ValueError: Invalid CSS selector`. Only compound selectors (tag + classes + id + pseudo) are accepted by `Selector.parse` in `violetear/selector.py`. Workaround in the example: use only flat class selectors (`.tokens-heading` instead of `section.tokens h2`).
17+
18+
**Where to fix.** `violetear/selector.py` — the `SELECTOR` regex matches one compound; would need a top-level grammar that handles whitespace-separated descendants (and ideally `>` child, `+` adjacent, `~` general sibling). Probably a few dozen lines.
19+
20+
**Impact.** Cascading CSS authoring is awkward — every nested element needs a unique class name. For tiny examples this is fine; for a real app it'd push you toward Atomic CSS or component-scoped class explosion.
21+
22+
## 7.2 — `ElementSet.spawn(iterable, tag)` argument naming is misleading
23+
24+
**Tier(s):** 01
25+
26+
**Symptom.** Looking at the signature `spawn(count, tag)` plus the markup.py implementation, when you pass an iterable instead of an int, each *item* becomes the "index" passed to `.each(fn)`. The first dispatched subagent wrote `def _populate_swatch(i, el): color = PALETTE[i]` — natural assumption from the param name, but actually `i` is the Color itself.
27+
28+
**Where to fix.** Two options:
29+
- Rename `count` parameter to `count_or_items` and document the dual behavior, OR
30+
- Add a separate `spawn_from(iterable, tag)` method that's explicit about the item-iteration variant.
31+
32+
**Impact.** Confusing for new users. The fluent `.each(lambda item, el: ...)` form is clean once you know the rule.
33+
34+
## 7.3 — `Element.div().span(...)` chaining stops at the parent
35+
36+
**Tier(s):** 01
37+
38+
**Symptom.** `row.div(classes="type-sample").span(text=...)` raises `AttributeError: 'Element' object has no attribute 'span'`. Tag-method chaining (`div`, `span`, `h1`, etc.) lives on `ElementBuilder`, not on `Element`. To add a child element you either need a `with element as builder:` scope, or `element.add(HTML.span(...))`.
39+
40+
**Where to fix.** This may be intentional API design — Element + ElementBuilder is a deliberate split. But fluent users expect chained children to "just work". Could be addressed by:
41+
- Adding the tag methods to `Element` directly (most natural), OR
42+
- Returning `ElementBuilder(new_element)` from `tag()` methods instead of the raw Element, so chaining keeps working.
43+
44+
**Impact.** Trips up new users; forces a context-manager-or-`HTML.*` pattern that's slightly more verbose than necessary.
45+
46+
---
47+
48+
Add entries here as more examples land.

tests/test_examples_canonical.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
Thin smoke tests for the canonical examples in `examples/0N_*.py`.
3+
4+
Each test verifies that the example module loads, builds, and produces
5+
output with the expected shape. The goal is to catch regressions when
6+
the framework changes — not to validate the examples' behavior in depth.
7+
8+
See `issues/6-canonical-examples-design.md` for the design.
9+
"""
10+
11+
import importlib.util
12+
from pathlib import Path
13+
14+
EXAMPLES_DIR = Path(__file__).resolve().parent.parent / "examples"
15+
16+
17+
def _load(filename: str):
18+
"""Import an example module by filename without it polluting sys.modules."""
19+
spec = importlib.util.spec_from_file_location(
20+
filename.removesuffix(".py"), EXAMPLES_DIR / filename
21+
)
22+
module = importlib.util.module_from_spec(spec)
23+
spec.loader.exec_module(module)
24+
return module
25+
26+
27+
# -- 01_static -----------------------------------------------------------------
28+
29+
30+
def test_01_static_renders_html_and_css(tmp_path):
31+
"""Tier 1: pure-markup example writes a valid HTML + CSS pair to disk."""
32+
mod = _load("01_static.py")
33+
34+
html_path, css_path = mod.write_to(tmp_path)
35+
36+
assert html_path.exists()
37+
assert css_path.exists()
38+
39+
html = html_path.read_text()
40+
css = css_path.read_text()
41+
42+
# HTML shape
43+
assert html.lstrip().startswith("<!DOCTYPE html>")
44+
assert "<title>violetear · Design Tokens</title>" in html
45+
assert 'class="page-title"' in html
46+
assert 'class="swatch"' in html
47+
# Inline swatch chip style proves a Color rendered into the doc
48+
assert "background-color: rgba(" in html
49+
50+
# CSS shape: normalize preamble + at least one of our class selectors
51+
assert "modern-normalize" in css
52+
assert ".swatch-chip" in css
53+
assert ".tokens-heading" in css

0 commit comments

Comments
 (0)