@sondalab/ui-kit 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -2
- package/agent/HANDOFF.md +52 -0
- package/agent/README.md +119 -0
- package/agent/accent-registry.json +13 -0
- package/agent/blessed/daos.json +24 -0
- package/agent/blessed/default.json +26 -0
- package/agent/blessed/introspect.json +15 -0
- package/agent/blessed/spexr.json +15 -0
- package/agent/catalog.json +24 -0
- package/agent/catalog.md +194 -0
- package/agent/design-language.md +168 -0
- package/agent/sl-audit.mjs +400 -0
- package/agent/substrate-matrix.json +38 -0
- package/agent/workflows/adopt.md +35 -0
- package/agent/workflows/author.md +33 -0
- package/agent/workflows/migrate.md +45 -0
- package/assets/README.md +53 -0
- package/assets/app-icon.svg +4 -0
- package/assets/favicon.svg +3 -0
- package/assets/sondalab-lockup.svg +13 -0
- package/assets/sondalab-wordmark.svg +13 -0
- package/components.css +394 -0
- package/package.json +24 -4
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# @sondalab/ui-kit
|
|
2
2
|
|
|
3
|
-
Sondalab design tokens as CSS custom properties (`--sl-*`)
|
|
3
|
+
Sondalab design tokens as CSS custom properties (`--sl-*`) plus a CSS-only component layer (`.sl-*`). No build step, no runtime JS — import the stylesheets and theme via a `data-sl-theme` attribute.
|
|
4
4
|
|
|
5
5
|
```css
|
|
6
6
|
@import "@sondalab/ui-kit/tokens.css"; /* shared: type, spacing, radii, motion, shadow, z */
|
|
7
|
+
@import "@sondalab/ui-kit/components.css"; /* .sl-* components (reads role-vars, see below) */
|
|
7
8
|
@import "@sondalab/ui-kit/themes/light.css"; /* [data-sl-theme="light"] surfaces, text, borders, status, accent */
|
|
8
9
|
@import "@sondalab/ui-kit/themes/dark.css";
|
|
9
10
|
@import "@sondalab/ui-kit/themes/high-contrast.css";
|
|
@@ -22,4 +23,44 @@ Sondalab design tokens as CSS custom properties (`--sl-*`). CSS-only: no compone
|
|
|
22
23
|
|
|
23
24
|
**Fonts** (load separately, e.g. Google Fonts): Fraunces (display), Instrument Sans (body), Martian Mono (mono).
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
## Components (`components.css`)
|
|
27
|
+
|
|
28
|
+
Primitives: `.sl-eyebrow`, `.sl-tag` (`--plain`), `.sl-btn` (`--primary` / `--ghost` / `--sm`), `.sl-card` (`__title` / `__body`), `.sl-badge` (`--success` / `--warning` / `--danger` / `--info`), `.sl-field` (`__label` / `__input`), `.sl-kbd`, `.sl-code`, `.sl-link`, `.sl-rule` (`--labeled`).
|
|
29
|
+
|
|
30
|
+
Complex (composed shells — no behaviour JS; the consumer wires interaction): `.sl-callout` (`--success`/`--warning`/`--danger`/`--info`), `.sl-table` (`--numeric`; wrap in an `overflow-x:auto` container), `.sl-dialog` (visual shell for the native `<dialog>` — drive with `.showModal()`/`.close()`), `.sl-tabs` (`__list`/`__tab` — supply your own ARIA + panel switching), `.sl-tooltip` (CSS-only via `data-sl-tooltip`).
|
|
31
|
+
|
|
32
|
+
### Colour contract — role-vars (`--slc-*`)
|
|
33
|
+
|
|
34
|
+
Components never read theme tokens directly (those resolve only under `[data-sl-theme]`). They read role-vars that default through a fallback chain to the `--sl-*` token, then to a brand literal. **Tier A** products (full `data-sl-theme`, e.g. spexr) need zero wiring — the role-vars pick up the live theme automatically. **Tier B** products (value-pinned, e.g. daos, introspect) map the role-vars **once** to their own colour layer. That block is the *entire* integration surface:
|
|
35
|
+
|
|
36
|
+
```css
|
|
37
|
+
:root {
|
|
38
|
+
--slc-accent: var(--your-accent);
|
|
39
|
+
--slc-accent-hover: var(--your-accent-hover);
|
|
40
|
+
--slc-accent-active: var(--your-accent-active);
|
|
41
|
+
--slc-accent-subtle: var(--your-accent-tint); /* ~12% accent over surface */
|
|
42
|
+
--slc-on-accent: var(--your-on-accent); /* text on a filled accent */
|
|
43
|
+
--slc-surface: var(--your-surface);
|
|
44
|
+
--slc-raised: var(--your-raised);
|
|
45
|
+
--slc-overlay: var(--your-overlay); /* dialog backdrop */
|
|
46
|
+
--slc-text: var(--your-text);
|
|
47
|
+
--slc-text-secondary: var(--your-text-2);
|
|
48
|
+
--slc-text-muted: var(--your-text-muted);
|
|
49
|
+
--slc-border: var(--your-border);
|
|
50
|
+
--slc-border-subtle: var(--your-border-subtle);
|
|
51
|
+
--slc-border-strong: var(--your-border-strong);
|
|
52
|
+
--slc-focus: var(--your-accent);
|
|
53
|
+
--slc-success: var(--your-success); --slc-warning: var(--your-warning);
|
|
54
|
+
--slc-danger: var(--your-danger); --slc-info: var(--your-info);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Inversion bands.** `--slc-border` defaults to an alpha-over-canvas value; inside a band whose background is the strongest neutral (`.invert`), that alpha sits on the wrong ground and the hairline disappears. Re-derive it at the band from the band's foreground — no new token:
|
|
59
|
+
|
|
60
|
+
```css
|
|
61
|
+
.invert { --slc-border: color-mix(in srgb, currentColor 22%, transparent); }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The full reference implementation — every component in all three themes — is `showcase/index.html` (§04).
|
|
65
|
+
|
|
66
|
+
Generated tokens (`tokens.css`, `themes/*.css`) come from the canonical `sondalab.tokens.json` — do not hand-edit them. `components.css` is hand-authored.
|
package/agent/HANDOFF.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Agent hand-off — Sondalab DS agentic layer
|
|
2
|
+
|
|
3
|
+
> **What this file is:** operational guidance for an AI agent working with the Sondalab DS agentic layer. It records what the audit does **not** mechanically check and how to handle those gaps, so you don't either (a) trust the script for things it doesn't cover or (b) mechanically "fix" intentional deviations. Read it before running a migrate/author/adopt job.
|
|
4
|
+
> **Audience:** agents (and their humans) running `sl-audit.mjs` or the migrate workflow.
|
|
5
|
+
> **Companion:** `README.md` (usage), `design-language.md` (the grammar, shipped alongside). The contract with its human-facing "Open decisions" is `docs/design-system-agentic-spec.md` in the source repo (https://github.com/sondalab-ai/sondalab-ui).
|
|
6
|
+
|
|
7
|
+
## What the script decides — and what it does not
|
|
8
|
+
|
|
9
|
+
`sl-audit.mjs` (Slice 1) mechanically checks exactly two things:
|
|
10
|
+
|
|
11
|
+
1. **Colour value-equality** — every hex/rgb literal resolves to a token, the product's accent, or a blessed value.
|
|
12
|
+
2. **Tier consistency** — the theme selector (`[data-sl-theme]` vs `[data-theme]`) matches the product's tier in `substrate-matrix.json`.
|
|
13
|
+
|
|
14
|
+
**Everything else is your judgement.** The script is silent on it — silence is not a pass. In particular, the checks below were specified but deliberately **not** built, because the design language itself uses "violating" values in blessed places, so a mechanical flag would fire on the DS's own reference. Do not treat their absence as "these are fine"; treat them as *yours to judge*.
|
|
15
|
+
|
|
16
|
+
## Deferred checks — judge these yourself, do not mechanically flag or fix
|
|
17
|
+
|
|
18
|
+
### Spacing off the `--sl-space-*` scale *(check 3 — deferred)*
|
|
19
|
+
The scale is `{0, .25, .5, .75, 1, 1.5, 2, 3}rem`. But off-scale literals are legitimate in places:
|
|
20
|
+
- The **canonical tag recipe** in `design-language.md` L3 is `padding: 3px 7px` — off-scale on purpose.
|
|
21
|
+
- Hairline nudges (`margin-bottom: -1px` to overlap a 1px rule), optical adjustments, and `ch`/`%`/`vw`/`calc()` values.
|
|
22
|
+
|
|
23
|
+
**How to judge:** new *layout* spacing (section gaps, stack rhythm, card padding) should step off `--sl-space-*`. Recipe-level and optical one-offs are fine. When in doubt, match what the nearest `design-language.md` recipe does; don't invent a new spacing literal for structural rhythm.
|
|
24
|
+
|
|
25
|
+
### Border width > 1px *(check 4 — deferred)*
|
|
26
|
+
The hairline rule (L2) is about *structural separators* being 1px. But 2px borders are intentional as **emphasis accents**, not separators — confirmed in the reference: `components.css:208` (a keycap's 2px bottom edge) and `components.css:360` (a 2px active-tab underline). Focus rings also legitimately use the 2px `--sl-focus-ring-width` token (usually on `outline`, which is not a border).
|
|
27
|
+
|
|
28
|
+
**How to judge:** a 1px rule that *separates* content (section top, card edge, header/footer, gutter ruler) must be 1px. A 2px edge that *emphasizes* (active-tab underline, keycap, measure-tick) is a deliberate accent — leave it. Never bump a separator to 2px; never flatten an emphasis accent to 1px.
|
|
29
|
+
|
|
30
|
+
**Active/pressed control states resolve to the product accent** (amber for the `default` product; the product's registered hue otherwise) — an active tab, a pressed button, a selected control shows the accent, never a neutral. So a 2px active-state edge is coloured with the accent (or its `active` step), and a colour-value-equality finding there should map to the accent, not be flattened away.
|
|
31
|
+
|
|
32
|
+
### Eyebrow / tag recipe conformance *(check 6 — deferred)*
|
|
33
|
+
For elements already named eyebrow/tag, the recipe (mono + uppercase + tracking `0.14–0.22em`) is fixed in `design-language.md` L3. The script does not verify it.
|
|
34
|
+
|
|
35
|
+
**How to judge:** when you author or touch an eyebrow/tag/section-index, cite the canonical recipe in `design-language.md` and match it — don't re-derive tracking/size. When migrating, check existing eyebrow/tag elements against the recipe by eye.
|
|
36
|
+
|
|
37
|
+
### Font-family *(check 5 — dropped, not deferred)*
|
|
38
|
+
Evaluated and **dropped**: the only literal faces across the calibrated surfaces are introspect's IBM Plex Mono (a blessed mono substitute) and spexr's editor font (blessed). A font check would yield nothing. Just keep using `var(--sl-font-display|sans|mono)`; the only allowed substitution is a *narrower mono* where fixed pixel grids demand it (`design-language.md` L1), recorded as a blessed `font` deviation.
|
|
39
|
+
|
|
40
|
+
## Structural / judgement rules the script never sees
|
|
41
|
+
|
|
42
|
+
These are always yours (the `sl-ds-auditor` subagent covers them by reading `design-language.md`), scoped to the substrate's Required column in `substrate-matrix.json`:
|
|
43
|
+
|
|
44
|
+
- **Missing** structural labels (eyebrow, section index, tag) where the substrate requires them.
|
|
45
|
+
- Hairline structure and the hairline-grid technique, gutter/ruler, section-index-leads-section.
|
|
46
|
+
- Signature components present and matching: inversion band, sticky translucent header, real-content blocks.
|
|
47
|
+
- **L5 editorial voice** — precise/technical, active, structure-encodes-truth, English identifiers.
|
|
48
|
+
- The house rule: ≥1 unexpected, intentional *composition* decision per screen (spend boldness on layout, keep materials shared).
|
|
49
|
+
|
|
50
|
+
## If you think a deferred check should become mechanical
|
|
51
|
+
|
|
52
|
+
That is a human decision (see the spec's "Open decisions"), because it requires committing to a blessing policy for the legitimate off-scale/2px cases above. Don't add the check to `sl-audit.mjs` on your own initiative — raise it, with the specific reference example it would flag, and let the human decide.
|
package/agent/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Sondalab DS — Agentic Core
|
|
2
|
+
|
|
3
|
+
> **What this file is:** the usage guide for the Sondalab design-system *agentic layer* — how an AI agent (in any harness, in this repo or a consumer repo) loads this core and runs the author / migrate / adopt workflows against the shared design system.
|
|
4
|
+
> **Audience:** anyone pointing an agent at a Sondalab surface — the DS maintainer, and engineers on consumer apps (spexr, introspect, daos, and future products) who want their UI to read as Sondalab without re-deriving the grammar.
|
|
5
|
+
> **Status:** shipped in `@sondalab/ui-kit` (v0.3.0+). The migrate / author / adopt workflows, the component catalog, and the `sl-audit` bin all ship in the package; a consumer resolves them from `node_modules/@sondalab/ui-kit/agent/`.
|
|
6
|
+
> **Companion files:** `./design-language.md` (the grammar this tooling enforces — never restated here; shipped alongside). The tooling contract and the token spec live in the source repo: `docs/design-system-agentic-spec.md` and `docs/design-system-spec.md` at https://github.com/sondalab-ai/sondalab-ui.
|
|
7
|
+
|
|
8
|
+
## What this core is
|
|
9
|
+
|
|
10
|
+
The design system has always had the *rules* (`design-language.md`) and the *values* (`sondalab.tokens.json`). This core adds the missing third thing: a way for an agent to **act** on them repeatably. It is deliberately thin — it holds only what the prose docs are not:
|
|
11
|
+
|
|
12
|
+
| File | What it is |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `accent-registry.json` | Machine mirror of the per-product accent table (`design-language.md` §Decision 2). |
|
|
15
|
+
| `substrate-matrix.json` | The per-substrate compliance matrix (IDE/App/Page/Term → R/A/N-A) as a lookup. |
|
|
16
|
+
| `blessed/<product>.json` | Per-product allowlist of authorized deviations, one justification each. |
|
|
17
|
+
| `workflows/migrate.md` | The ordered migrate procedure with human gates. |
|
|
18
|
+
| `workflows/author.md` | The ordered author procedure (greenfield), built on the catalog. |
|
|
19
|
+
| `workflows/adopt.md` | The ordered adopt procedure (one component into an existing app). |
|
|
20
|
+
| `catalog.md` + `catalog.json` | Copy-paste component catalog (primitives + signature patterns) with markup, role-vars, and substrate; its machine index. Snippets are audited clean. |
|
|
21
|
+
| `HANDOFF.md` | What the audit does **not** check mechanically, and how to judge those cases (deferred spacing/border/recipe checks). Read before a job. |
|
|
22
|
+
|
|
23
|
+
Everything else — the actual rules, why they exist, the compliance philosophy — stays in `design-language.md`. This core **references** it; it never forks it.
|
|
24
|
+
|
|
25
|
+
The verdict engine is `sl-audit` (`agent/sl-audit.mjs`, exposed as the `sl-audit` bin). It reads this core plus the shipped token CSS (`../tokens.css` + `../themes/*.css`) and reports what is decidable from CSS text. What requires human/agent judgement (missing structural labels, editorial voice, layout composition) is out of the script's scope by design.
|
|
26
|
+
|
|
27
|
+
**What the script checks today (Slice 1):** colour value-equality and theme-selector tier consistency — only. Spacing-off-scale, border width, font-family, and eyebrow/tag recipe conformance are *specified* (see the agentic spec) but **not yet implemented in the script**; until they land they are judgement-level and belong to the `sl-ds-auditor` subagent, not the audit script. A colour finding can be silenced two ways in `blessed/<product>.json`: `selectors` (a distinctive selector/property the colour sits under) or `values` (an exact colour value — for demo data with no distinctive selector).
|
|
28
|
+
|
|
29
|
+
## The three workflows
|
|
30
|
+
|
|
31
|
+
| Workflow | Job | For whom | Status |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| **Migrate** | Move an existing app's UI onto the DS: audit → map values to tokens → pick tier → apply L1–L5. | An app with its own palette adopting the DS wholesale (introspect, daos). | Slice 1 |
|
|
34
|
+
| **Author** | Scaffold a new surface/element DS-compliant from the first line. | Greenfield screens or components. | Slice 2 — catalog + `workflows/author.md` + `sl-author` skill landed |
|
|
35
|
+
| **Adopt** | Pull one signature component (eyebrow, tag, inversion band) into an app without a full migration. | Consumers wanting a taste of the family look. | Slice 3 — `workflows/adopt.md` + `sl-adopt` skill landed |
|
|
36
|
+
|
|
37
|
+
All three share the same core and the same read-only auditor. Migrate is the deepest; author and adopt are subsets of its machinery.
|
|
38
|
+
|
|
39
|
+
## How an agent loads the core
|
|
40
|
+
|
|
41
|
+
**In Claude Code (this repo).** The wrappers do it for you:
|
|
42
|
+
|
|
43
|
+
- Say *"migrate `<app>` to the Sondalab DS"* or run `/sl-migrate` → the `sl-migrate` skill drives `workflows/migrate.md` and holds the gates.
|
|
44
|
+
- Say *"build a new `<screen>` in the Sondalab DS"* or run `/sl-author` → the `sl-author` skill drives `workflows/author.md` and builds from `catalog.md`.
|
|
45
|
+
- Say *"add a Sondalab `<component>` to this app"* or run `/sl-adopt` → the `sl-adopt` skill drives `workflows/adopt.md` (one component, scoped to the touched file).
|
|
46
|
+
- The `sl-ds-auditor` subagent runs the audit and reads `design-language.md` for the judgement-level findings, returning them grouped by severity with `file:line`.
|
|
47
|
+
|
|
48
|
+
**In any other harness / a consumer repo.** `npm i @sondalab/ui-kit`, then point the agent at the installed core and give it the relevant workflow as its instructions:
|
|
49
|
+
|
|
50
|
+
1. `node_modules/@sondalab/ui-kit/agent/` (this core — registry, matrix, blessed, catalog, workflows),
|
|
51
|
+
2. `node_modules/@sondalab/ui-kit/agent/design-language.md` (the grammar),
|
|
52
|
+
3. `node_modules/@sondalab/ui-kit/tokens.css` + `themes/*.css` (the values the audit checks against).
|
|
53
|
+
|
|
54
|
+
Then run the audit via the shipped bin:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
npx sl-audit --surface "web/src/**/*.css" --product introspect --substrate App
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Surface globs resolve against your current working directory; the audit resolves the token allowlist from the installed package.
|
|
61
|
+
|
|
62
|
+
## Reading an audit report
|
|
63
|
+
|
|
64
|
+
Findings are bucketed by enforceability — a flat list would destroy the tier nuance the DS is built on:
|
|
65
|
+
|
|
66
|
+
- **R-violation (blocking)** — a required rule is broken. Must reach zero (or be blessed) before a migration is done.
|
|
67
|
+
- **A-deviation (needs justification)** — an adapt-level rule diverges. Either bless it (record why in `blessed/<product>.json`) or fix it.
|
|
68
|
+
- **N/A** — not applicable to this substrate. Never printed.
|
|
69
|
+
|
|
70
|
+
Example (illustrative):
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
introspect (App, Tier B) — sl-audit
|
|
74
|
+
|
|
75
|
+
R-violation (2)
|
|
76
|
+
web/src/theme.css:44 colour #3aa0ff does not resolve to a token, accent, or blessed value
|
|
77
|
+
web/src/theme.css:12 selector [data-sl-theme] but product declared Tier B ([data-theme])
|
|
78
|
+
|
|
79
|
+
A-deviation (1)
|
|
80
|
+
web/src/theme.css:88 #1e2a24 on-ink status green — bless or fix (design-language.md §Decision 2)
|
|
81
|
+
|
|
82
|
+
N/A: 6 rules skipped for substrate App
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Use cases
|
|
86
|
+
|
|
87
|
+
### 1 — Maintainer migrates a live app (Migrate, Slice 1)
|
|
88
|
+
|
|
89
|
+
> *"Migrate daos to the Sondalab DS."*
|
|
90
|
+
|
|
91
|
+
The agent classifies daos as **Page / Tier B**, runs a **baseline audit**, and separates real defects from blessed deviations. daos's `--rule`/`--rule-2` (alpha-on-canvas hairlines) and its `.artifact .ok`/`.no` (on-ink status pairs) are *authorized* by `design-language.md` — they go into `blessed/daos.json`, not the defect list. The remaining R-violations become a **token-mapping plan**, presented in chat. **Nothing is edited until you say yes.** After approval the agent applies the mapping in small chunks, re-audits (R-violations → 0), and does a visual pass across light/dark/high-contrast — including the dark-invert hairline regression that alpha-on-canvas rules are prone to.
|
|
92
|
+
|
|
93
|
+
### 2 — Consumer app adopts one component (Adopt, Slice 3)
|
|
94
|
+
|
|
95
|
+
> *"Add a Sondalab eyebrow + tag to our settings header, we're not migrating everything yet."*
|
|
96
|
+
|
|
97
|
+
The agent pulls the canonical eyebrow/tag recipe from `design-language.md` L3, binds it to the consumer's local colour vars (Tier B — values must still resolve to Sondalab), and runs the audit scoped to the touched file only. No full migration; the rest of the app is untouched.
|
|
98
|
+
|
|
99
|
+
### 3 — Greenfield element, compliant from the start (Author, Slice 2)
|
|
100
|
+
|
|
101
|
+
> *"Scaffold a new empty-state panel for introspect."*
|
|
102
|
+
|
|
103
|
+
The agent starts from the substrate (App), applies the required rules for that column of the matrix (mono structural labels, hairline structure, spacing off `--sl-space-*`, one accent hue), and the audit passes on first run because compliance was built in, not retrofitted.
|
|
104
|
+
|
|
105
|
+
### 4 — CI guard (any slice)
|
|
106
|
+
|
|
107
|
+
The audit is deterministic, so it runs headless:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
npx sl-audit --surface "web/src/**/*.css" --product introspect --substrate App --fail-on R
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A non-zero exit on any unblessed R-violation keeps a merged surface from drifting off the DS — the prose "compliance test" in `design-language.md`, now enforceable.
|
|
114
|
+
|
|
115
|
+
## Extending the core
|
|
116
|
+
|
|
117
|
+
- **New product** → add its accent to `accent-registry.json` (and its row to the `design-language.md` table; the parity test enforces both exist), then create `blessed/<product>.json`.
|
|
118
|
+
- **New blessed deviation** → add an entry with a `why` that cites the `design-language.md` rule authorizing it. An unjustified entry is a defect in disguise.
|
|
119
|
+
- **New rule** → it belongs in `design-language.md` first (prose), then in `substrate-matrix.json` (its per-substrate levels), then optionally in the script if it is CSS-text-decidable. Rules that need intent stay with the subagent.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$note": "Machine source of the per-product accent table in design-language.md §Decision 2. The prose table stays authoritative for reasoning; this file is the values. scripts/accent-registry.test.mjs asserts product-set parity between the two.",
|
|
3
|
+
"products": {
|
|
4
|
+
"default": {
|
|
5
|
+
"light": "#B56A0C",
|
|
6
|
+
"dark": "#F5A83C",
|
|
7
|
+
"depth": { "light": "#14606C", "dark": "#35A0B4" }
|
|
8
|
+
},
|
|
9
|
+
"daos": { "inherits": "default" },
|
|
10
|
+
"spexr": { "light": "#5b6cff", "dark": "#8b96ff" },
|
|
11
|
+
"introspect": { "single": "#2ee6c0" }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$note": "Authorized deviations for daos. Each entry cites the design-language.md rule that blesses it. An entry without a justifying rule is a defect in disguise. Populated further during migrate calibration.",
|
|
3
|
+
"substrate": "Page",
|
|
4
|
+
"deviations": [
|
|
5
|
+
{
|
|
6
|
+
"kind": "color",
|
|
7
|
+
"selectors": [
|
|
8
|
+
"--rule",
|
|
9
|
+
"--rule-2"
|
|
10
|
+
],
|
|
11
|
+
"value": "color-mix alpha-on-canvas hairline",
|
|
12
|
+
"why": "design-language.md L2 hairline structure: hairlines need alpha over canvas. Dark-invert visibility already handled in daos .invert CSS."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"kind": "color",
|
|
16
|
+
"selectors": [
|
|
17
|
+
".artifact .ok",
|
|
18
|
+
".artifact .no"
|
|
19
|
+
],
|
|
20
|
+
"value": "light-on-ink status greens/reds",
|
|
21
|
+
"why": "design-language.md §Decision 2: on an inverted surface, derive an on-ink status pair locally rather than using the canvas --sl-status-* tokens. Cited there as a correct instance."
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$note": "Authorized deviations for the reference implementation (showcase, product=default). Established during Slice 1 calibration: the audit correctly flagged these; review confirmed they are intentional, so they are recorded here rather than 'fixed'.",
|
|
3
|
+
"substrate": "Page",
|
|
4
|
+
"deviations": [
|
|
5
|
+
{
|
|
6
|
+
"kind": "color",
|
|
7
|
+
"selectors": [
|
|
8
|
+
"--bg-grad"
|
|
9
|
+
],
|
|
10
|
+
"value": "near-canvas gradient-field stops (identity board atmospheric field)",
|
|
11
|
+
"why": "Page-local convention (showcase :root comment 'per spec: not tokens'), keyed per theme, fading to transparent. The identity-board field is the visual thesis; its stops are derived-from-canvas atmosphere, not a palette."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"kind": "color",
|
|
15
|
+
"values": [
|
|
16
|
+
"#8b96ff",
|
|
17
|
+
"#a3acff",
|
|
18
|
+
"rgba(139,150,255,0.12)",
|
|
19
|
+
"#2ee6c0",
|
|
20
|
+
"#5ad1ff",
|
|
21
|
+
"rgba(46,230,192,0.12)"
|
|
22
|
+
],
|
|
23
|
+
"why": "The showcase demonstrates the per-product accent registry (design-language.md §Decision 2): spexr's indigo and introspect's cyan (plus derived hover/subtle) appear as demo swatch content, not as a second accent adopted by the showcase. Blessed by exact value, not selector — the swatches live in a JS object with no distinctive selector."
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$note": "Authorized deviations for introspect. Populated during migrate calibration; colour deviations added as the baseline audit surfaces and blesses them.",
|
|
3
|
+
"substrate": "App",
|
|
4
|
+
"deviations": [
|
|
5
|
+
{
|
|
6
|
+
"kind": "font",
|
|
7
|
+
"selectors": [
|
|
8
|
+
"--sl-font-mono",
|
|
9
|
+
"mono"
|
|
10
|
+
],
|
|
11
|
+
"value": "IBM Plex Mono",
|
|
12
|
+
"why": "design-language.md L1: a surface may substitute the mono face for a narrower one where fixed pixel grids demand it. Martian Mono overflows ch-clamped columns. sans/display roles unchanged."
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$note": "Authorized deviations for spexr (Tier A, delivered). Presumed compliant; populated only if calibration surfaces a blessed case.",
|
|
3
|
+
"substrate": "IDE",
|
|
4
|
+
"deviations": [
|
|
5
|
+
{
|
|
6
|
+
"kind": "font",
|
|
7
|
+
"selectors": [
|
|
8
|
+
".spexr-df-card__branch",
|
|
9
|
+
".spexr-df-card__trail"
|
|
10
|
+
],
|
|
11
|
+
"value": "--theia-code-font-family (editor mono)",
|
|
12
|
+
"why": "design-language.md L3: code-content runs mirror the editor font; design-layer labels use --sl-font-code. Deliberate split, not a violation."
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$note": "Machine index of catalog.md. Each entry maps a component id to its classes, the --slc-* role-vars it consumes, and its kind. scripts/catalog-audit.test.mjs asserts every id has a section in catalog.md and that every snippet audits clean.",
|
|
3
|
+
"components": [
|
|
4
|
+
{ "id": "eyebrow", "kind": "primitive", "classes": ["sl-eyebrow"], "roleVars": ["--slc-text-muted"] },
|
|
5
|
+
{ "id": "tag", "kind": "primitive", "classes": ["sl-tag", "sl-tag--plain"], "roleVars": ["--slc-accent", "--slc-accent-subtle", "--slc-text-muted", "--slc-border"] },
|
|
6
|
+
{ "id": "button", "kind": "primitive", "classes": ["sl-btn", "sl-btn--primary", "sl-btn--ghost", "sl-btn--sm"], "roleVars": ["--slc-accent", "--slc-accent-hover", "--slc-accent-active", "--slc-on-accent", "--slc-border", "--slc-text"] },
|
|
7
|
+
{ "id": "link", "kind": "primitive", "classes": ["sl-link"], "roleVars": ["--slc-accent"] },
|
|
8
|
+
{ "id": "badge", "kind": "primitive", "classes": ["sl-badge", "sl-badge--success", "sl-badge--warning", "sl-badge--danger", "sl-badge--info"], "roleVars": ["--slc-success", "--slc-warning", "--slc-danger", "--slc-info"] },
|
|
9
|
+
{ "id": "card", "kind": "primitive", "classes": ["sl-card", "sl-card__title", "sl-card__body"], "roleVars": ["--slc-surface", "--slc-border", "--slc-text", "--slc-text-secondary"] },
|
|
10
|
+
{ "id": "field", "kind": "primitive", "classes": ["sl-field", "sl-field__label", "sl-field__input"], "roleVars": ["--slc-surface", "--slc-border", "--slc-border-strong", "--slc-text", "--slc-focus"] },
|
|
11
|
+
{ "id": "callout", "kind": "primitive", "classes": ["sl-callout", "sl-callout--success", "sl-callout--warning", "sl-callout--danger", "sl-callout--info", "sl-callout__title", "sl-callout__body"], "roleVars": ["--slc-surface", "--slc-border", "--slc-info", "--slc-success", "--slc-warning", "--slc-danger"] },
|
|
12
|
+
{ "id": "rule", "kind": "primitive", "classes": ["sl-rule", "sl-rule--labeled"], "roleVars": ["--slc-border"] },
|
|
13
|
+
{ "id": "table", "kind": "primitive", "classes": ["sl-table", "sl-table--numeric"], "roleVars": ["--slc-border", "--slc-text", "--slc-text-secondary", "--slc-surface"] },
|
|
14
|
+
{ "id": "tabs", "kind": "primitive", "classes": ["sl-tabs", "sl-tabs__list", "sl-tabs__tab"], "roleVars": ["--slc-accent", "--slc-text", "--slc-text-muted", "--slc-border"] },
|
|
15
|
+
{ "id": "kbd", "kind": "primitive", "classes": ["sl-kbd", "sl-code"], "roleVars": ["--slc-surface", "--slc-border", "--slc-text"] },
|
|
16
|
+
{ "id": "dialog", "kind": "primitive", "classes": ["sl-dialog", "sl-dialog__title", "sl-dialog__body", "sl-dialog__actions"], "roleVars": ["--slc-raised", "--slc-border", "--slc-overlay", "--slc-text", "--slc-text-secondary"] },
|
|
17
|
+
|
|
18
|
+
{ "id": "sticky-header", "kind": "signature", "classes": [], "roleVars": ["--sl-bg-canvas", "--sl-border-subtle", "--sl-z-overlay"] },
|
|
19
|
+
{ "id": "inversion-band", "kind": "signature", "classes": [], "roleVars": ["--sl-text-primary", "--sl-bg-canvas"] },
|
|
20
|
+
{ "id": "hairline-grid", "kind": "signature", "classes": [], "roleVars": ["--sl-border-subtle", "--sl-bg-surface", "--sl-space-5"] },
|
|
21
|
+
{ "id": "section-index", "kind": "signature", "classes": [], "roleVars": ["--sl-border-subtle", "--sl-font-mono", "--sl-text-xs", "--sl-text-muted", "--sl-space-8"] },
|
|
22
|
+
{ "id": "oversized-anchor","kind": "signature", "classes": [], "roleVars": ["--sl-font-display", "--sl-line-tight"] }
|
|
23
|
+
]
|
|
24
|
+
}
|
package/agent/catalog.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Component catalog — Sondalab DS (agent-facing)
|
|
2
|
+
|
|
3
|
+
> **What this file is:** a copy-paste catalog of Sondalab DS primitives and signature patterns, for an agent authoring a new interface. Each entry gives the canonical markup, the role-vars it consumes, its substrate applicability, and a do/don't. Snippets use only `var(--slc-*/--sl-*)` and `color-mix()` over those — never a colour literal — so every snippet here passes `sl-audit` by construction (`scripts/catalog-audit.test.mjs` enforces it).
|
|
4
|
+
> **Audience:** agents building or extending a Sondalab surface (author workflow); humans reviewing component usage.
|
|
5
|
+
> **How to use:** pick the component, copy the markup, bind it to your product's theme (Tier A: consume `@sondalab/ui-kit` `components.css` + a theme; Tier B: map the `--slc-*` role-vars to your local colour layer). Then run `sl-audit`. The machine index is `catalog.json`.
|
|
6
|
+
> **Companion:** `README.md` (usage), `HANDOFF.md` (what the audit does not check), `./design-language.md` (the grammar these components express), `workflows/author.md` (the procedure that consumes this catalog).
|
|
7
|
+
|
|
8
|
+
The `--slc-*` role-vars (defined at the top of `components.css`) are the seam: components read them, and they resolve to `--sl-*` tokens (Tier A) or to your product's colour layer (Tier B). Never hardcode a colour into a component — bind the role-var.
|
|
9
|
+
|
|
10
|
+
## Primitives (ready classes in `components.css`)
|
|
11
|
+
|
|
12
|
+
### Eyebrow — `sl-eyebrow`
|
|
13
|
+
Mono uppercase micro-label above a heading. The single strongest family signal. Substrate: **R** on App/Page/IDE.
|
|
14
|
+
```html
|
|
15
|
+
<p class="sl-eyebrow">System status</p>
|
|
16
|
+
```
|
|
17
|
+
Consumes: `--slc-text-muted`, `--sl-font-mono`. Don't: sentence-case it or use a sans face.
|
|
18
|
+
|
|
19
|
+
### Tag / tier marker — `sl-tag` (`--plain`)
|
|
20
|
+
Accent-tinted pill with a leading dot; classification/status. Substrate: **R** App/Page.
|
|
21
|
+
```html
|
|
22
|
+
<span class="sl-tag">Delivered</span>
|
|
23
|
+
<span class="sl-tag sl-tag--plain">Draft</span>
|
|
24
|
+
```
|
|
25
|
+
Consumes: `--slc-accent`, `--slc-accent-subtle` (`--plain`: `--slc-text-muted`, `--slc-border`). Don't: introduce a second accent hue for tags.
|
|
26
|
+
|
|
27
|
+
### Button — `sl-btn` (`--primary` · `--ghost` · `--sm`)
|
|
28
|
+
Filled-accent primary or bordered ghost. Active/hover track the accent hue. Substrate: **R** App/IDE, **A** Page.
|
|
29
|
+
```html
|
|
30
|
+
<button class="sl-btn sl-btn--primary">Save changes</button>
|
|
31
|
+
<button class="sl-btn sl-btn--ghost">Cancel</button>
|
|
32
|
+
<button class="sl-btn sl-btn--primary sl-btn--sm">Add</button>
|
|
33
|
+
```
|
|
34
|
+
Consumes: `--slc-accent`, `--slc-accent-hover`, `--slc-accent-active`, `--slc-on-accent`, `--slc-border`, `--slc-text`. Do: state the effect in the label (L5 voice). Don't: use a default-blue CTA or `hover:opacity` as the only state.
|
|
35
|
+
|
|
36
|
+
### Link — `sl-link`
|
|
37
|
+
Underlined, accent on hover. Substrate: **R** App/Page.
|
|
38
|
+
```html
|
|
39
|
+
<a class="sl-link" href="/specs">read the spec</a>
|
|
40
|
+
```
|
|
41
|
+
Consumes: `--slc-accent`. Don't: remove the underline (leave the affordance).
|
|
42
|
+
|
|
43
|
+
### Badge — `sl-badge` (`--success` · `--warning` · `--danger` · `--info`)
|
|
44
|
+
Semantic status pill. Semantic colour is **independent of the accent hue** and shared. Substrate: **R** App/IDE.
|
|
45
|
+
```html
|
|
46
|
+
<span class="sl-badge sl-badge--success">Passing</span>
|
|
47
|
+
<span class="sl-badge sl-badge--danger">Failed</span>
|
|
48
|
+
```
|
|
49
|
+
Consumes: `--slc-success`, `--slc-warning`, `--slc-danger`, `--slc-info`. Don't: recolour a badge to the accent — status ≠ accent.
|
|
50
|
+
|
|
51
|
+
### Card — `sl-card` (`__title` · `__body`)
|
|
52
|
+
Surface panel. Substrate: **R** App.
|
|
53
|
+
```html
|
|
54
|
+
<article class="sl-card">
|
|
55
|
+
<h3 class="sl-card__title">Migration report</h3>
|
|
56
|
+
<div class="sl-card__body">2 R-violations resolved, 1 blessed.</div>
|
|
57
|
+
</article>
|
|
58
|
+
```
|
|
59
|
+
Consumes: `--slc-surface`, `--slc-border`, `--slc-text`, `--slc-text-secondary`. Don't: make a `rounded-lg` card grid the primary layout — prefer the hairline grid (below).
|
|
60
|
+
|
|
61
|
+
### Field — `sl-field` (`__label` · `__input`)
|
|
62
|
+
Label + input/textarea/select. Substrate: **R** App.
|
|
63
|
+
```html
|
|
64
|
+
<label class="sl-field">
|
|
65
|
+
<span class="sl-field__label">Product</span>
|
|
66
|
+
<input class="sl-field__input" type="text" placeholder="introspect">
|
|
67
|
+
</label>
|
|
68
|
+
```
|
|
69
|
+
Consumes: `--slc-surface`, `--slc-border`, `--slc-border-strong`, `--slc-text`, `--slc-focus`. Do: keep the shared focus ring.
|
|
70
|
+
|
|
71
|
+
### Callout — `sl-callout` (`--success` · `--warning` · `--danger` · `--info`)
|
|
72
|
+
Banner / admonition. The accent is a short "measure tick" on the top edge (not a left stripe). Substrate: **A** App/Page.
|
|
73
|
+
```html
|
|
74
|
+
<div class="sl-callout sl-callout--info">
|
|
75
|
+
<p class="sl-callout__title">Tier B</p>
|
|
76
|
+
<div class="sl-callout__body">Every colour must resolve to a Sondalab value.</div>
|
|
77
|
+
</div>
|
|
78
|
+
```
|
|
79
|
+
Consumes: `--slc-surface`, `--slc-border`, `--slc-info`/`--slc-success`/etc. Don't: reinstate a full left border stripe.
|
|
80
|
+
|
|
81
|
+
### Rule — `sl-rule` (`--labeled`)
|
|
82
|
+
Hairline divider; `--labeled` centres a mono caption over the line. Substrate: **R** App/Page.
|
|
83
|
+
```html
|
|
84
|
+
<hr class="sl-rule">
|
|
85
|
+
<div class="sl-rule sl-rule--labeled">since 0.2.0</div>
|
|
86
|
+
```
|
|
87
|
+
Consumes: `--slc-border`. Weight is always 1px — never thicken a separator.
|
|
88
|
+
|
|
89
|
+
### Table — `sl-table` (`--numeric`)
|
|
90
|
+
Data table shell. Wrap in an `overflow-x:auto` container so wide tables never scroll the page. Substrate: **R** App.
|
|
91
|
+
```html
|
|
92
|
+
<div style="overflow-x:auto">
|
|
93
|
+
<table class="sl-table sl-table--numeric">
|
|
94
|
+
<thead><tr><th>Product</th><th>R-violations</th></tr></thead>
|
|
95
|
+
<tbody><tr><td>daos</td><td>2</td></tr></tbody>
|
|
96
|
+
</table>
|
|
97
|
+
</div>
|
|
98
|
+
```
|
|
99
|
+
Consumes: `--slc-border`, `--slc-text`, `--slc-text-secondary`, `--slc-surface`. Do: `--numeric` for tabular figures.
|
|
100
|
+
|
|
101
|
+
### Tabs — `sl-tabs` (`__list` · `__tab`)
|
|
102
|
+
Visual-only tabs (consumer wires ARIA + panel switching). Active tab uses a 2px accent underline — an emphasis accent, not a separator. Substrate: **R** App.
|
|
103
|
+
```html
|
|
104
|
+
<div class="sl-tabs">
|
|
105
|
+
<div class="sl-tabs__list" role="tablist">
|
|
106
|
+
<button class="sl-tabs__tab" aria-selected="true">Tokens</button>
|
|
107
|
+
<button class="sl-tabs__tab">Components</button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
```
|
|
111
|
+
Consumes: `--slc-accent`, `--slc-text`, `--slc-text-muted`, `--slc-border`. Active state is the accent (never a neutral).
|
|
112
|
+
|
|
113
|
+
### Kbd / inline code — `sl-kbd` · `sl-code`
|
|
114
|
+
Keyboard key and inline monospace code. Substrate: **A**.
|
|
115
|
+
```html
|
|
116
|
+
Press <kbd class="sl-kbd">⌘K</kbd>, then run <code class="sl-code">sl-audit</code>.
|
|
117
|
+
```
|
|
118
|
+
Consumes: `--slc-surface`, `--slc-border`, `--slc-text`, `--sl-font-mono`.
|
|
119
|
+
|
|
120
|
+
### Dialog — `sl-dialog` (`__title` · `__body` · `__actions`)
|
|
121
|
+
Visual shell for the native `<dialog>` (consumer wires open/close). Substrate: **R** App.
|
|
122
|
+
```html
|
|
123
|
+
<dialog class="sl-dialog">
|
|
124
|
+
<h2 class="sl-dialog__title">Discard changes?</h2>
|
|
125
|
+
<div class="sl-dialog__body">This cannot be undone.</div>
|
|
126
|
+
<div class="sl-dialog__actions">
|
|
127
|
+
<button class="sl-btn sl-btn--ghost">Keep</button>
|
|
128
|
+
<button class="sl-btn sl-btn--primary">Discard</button>
|
|
129
|
+
</div>
|
|
130
|
+
</dialog>
|
|
131
|
+
```
|
|
132
|
+
Consumes: `--slc-raised`, `--slc-border`, `--slc-overlay`, `--slc-text`, `--slc-text-secondary`.
|
|
133
|
+
|
|
134
|
+
## Signature patterns (page-level composition — copy the CSS)
|
|
135
|
+
|
|
136
|
+
These are not single classes; they are the composition motifs from `design-language.md` L2/L3. Copy the CSS into your surface (they read `--sl-*` tokens directly, theme-independent).
|
|
137
|
+
|
|
138
|
+
### Sticky translucent header
|
|
139
|
+
Substrate: **R** App/Page. The house header: sticky, translucent, 1px bottom rule; mono wordmark left, mono meta right.
|
|
140
|
+
```css
|
|
141
|
+
.site-header {
|
|
142
|
+
position: sticky; top: 0; z-index: var(--sl-z-overlay);
|
|
143
|
+
background: color-mix(in srgb, var(--sl-bg-canvas) 84%, transparent);
|
|
144
|
+
backdrop-filter: saturate(140%) blur(10px);
|
|
145
|
+
border-bottom: 1px solid var(--sl-border-subtle);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
Don't: use an opaque header or a drop-shadow instead of the 1px rule.
|
|
149
|
+
|
|
150
|
+
### Inversion band
|
|
151
|
+
Substrate: **R** Page, **A** App/IDE/Term. A full-width block flipped to paper-on-ink; punctuates rhythm, marks the thesis or primary CTA. At least one per long page.
|
|
152
|
+
```css
|
|
153
|
+
.invert {
|
|
154
|
+
background: var(--sl-text-primary);
|
|
155
|
+
color: var(--sl-bg-canvas);
|
|
156
|
+
}
|
|
157
|
+
.invert .sl-eyebrow { color: color-mix(in srgb, var(--sl-bg-canvas) 70%, transparent); }
|
|
158
|
+
```
|
|
159
|
+
Don't: put an alpha-on-canvas hairline inside it (it vanishes) — derive an on-ink rule locally.
|
|
160
|
+
|
|
161
|
+
### Hairline grid
|
|
162
|
+
Substrate: **R** App/Page. Cards formed by a 1px gap over a line-colour background; cells paint their own surface → crisp shared rules, no doubled borders. Prefer over bordered-card grids.
|
|
163
|
+
```css
|
|
164
|
+
.hgrid {
|
|
165
|
+
display: grid; gap: 1px;
|
|
166
|
+
background: var(--sl-border-subtle);
|
|
167
|
+
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
|
168
|
+
}
|
|
169
|
+
.hgrid > * { background: var(--sl-bg-surface); padding: var(--sl-space-5); }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Section with mono index
|
|
173
|
+
Substrate: **R** Page. A section opens with a mono index label beside its heading, separated from the previous section by a 1px top rule. Number it only where the content is a real sequence.
|
|
174
|
+
```css
|
|
175
|
+
.section { border-top: 1px solid var(--sl-border-subtle); padding-block: var(--sl-space-8); }
|
|
176
|
+
.section__idx { font-family: var(--sl-font-mono); font-size: var(--sl-text-xs); letter-spacing: 0.2em; text-transform: uppercase; color: var(--sl-text-muted); }
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Oversized display anchor
|
|
180
|
+
Substrate: **R** Page (**A** App where a hero exists). One oversized display heading per screen, sized by a fluid clamp (a documented pattern, not a token).
|
|
181
|
+
```css
|
|
182
|
+
.anchor {
|
|
183
|
+
font-family: var(--sl-font-display); font-weight: 600;
|
|
184
|
+
letter-spacing: -0.02em; line-height: var(--sl-line-tight);
|
|
185
|
+
font-size: clamp(2.7rem, 1.6rem + 5vw, 5.4rem);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
Don't: use more than one oversized anchor per screen.
|
|
189
|
+
|
|
190
|
+
## Rules of use (all components)
|
|
191
|
+
|
|
192
|
+
- **Bind role-vars, never literals.** A component's colour comes from a `--slc-*`/`--sl-*` var. A raw hex fails `sl-audit`.
|
|
193
|
+
- **One accent hue.** The accent varies per product (see `accent-registry.json`); status colours (success/warning/danger/info) are shared and independent of it.
|
|
194
|
+
- **Spend surprise on composition, not materials.** Family resemblance comes from these shared components; distinctiveness comes from how you compose them (≥1 unexpected layout decision per screen).
|