@sondalab/ui-kit 0.2.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.
@@ -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.
@@ -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
+ }
@@ -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).
@@ -0,0 +1,168 @@
1
+ # Sondalab Design Language — Family-Resemblance Spec
2
+
3
+ > **What this file is:** the family-resemblance rules that sit *above* the token contract — the shared grammar of layout, graphic motifs, type treatment, and editorial voice that makes every Sondalab surface (spexr, introspect, daos, future products) read as one package. Where `design-system-spec.md` is the machine contract (the canonical `--sl-*`/terminal values and their generators), this file is the human contract (how to *use* those values so products look related without looking identical).
4
+ > **Audience:** engineers and designers building or migrating any Sondalab UI surface; reviewers checking cross-product coherence.
5
+ > **Owner:** marcellobarile.
6
+ > **Companion files:** `design-system-spec.md` (token contract — the values this language spends), `design-system-plan.md` (token pipeline build sequence), the identity board artifact `https://claude.ai/code/artifact/abbebb7f-5778-4af6-a9b4-2b3f169ae20f` (visual thesis). The per-product application of this language is tracked in a separate implementation plan (writing-plans), not here.
7
+
8
+ ## Legend (status vocabulary)
9
+
10
+ - **Shipped / Delivered** — merged to the surface's production codebase, available to users.
11
+ - **PoC implemented (not delivered)** — built as a prototype, not merged/released.
12
+ - **Placeholder (not delivered)** — stub or scaffold only.
13
+
14
+ At authoring time (2026-08-22): spexr consumes the token architecture (delivered); introspect has adopted the shared type (delivered, type-only); daos landing and the sondalab-ui showcase are **not delivered** (this language spec is design only until the pass-1 plan lands).
15
+
16
+ ## Why this exists
17
+
18
+ Colour and type harmonisation alone (phase 2 tokens) does not make products feel like one suite. introspect after its type swap still "looks the same" because the values converged but the *grammar* did not: the same accent and font can carry two unrelated layouts. This document codifies the grammar — the reusable editorial decisions — so a new product (or a reviewer) can achieve family resemblance without re-deriving it.
19
+
20
+ The reference implementation is **daos landing** (`daos/docs/daos-landing.html`): it already expresses the intended editorial modernism (hairline structure, mono structural labels, oversized display type, inversion bands, real content over marketing copy). This spec extracts daos's vocabulary and generalises it; daos is the *source*, not the first target.
21
+
22
+ ## Decision 1 — Theme selector policy (two tiers)
23
+
24
+ Products reach Sondalab colour through one of two blessed patterns. This was previously decided twice, inconsistently (spexr uses `data-sl-theme`; introspect and daos use `[data-theme]`); it is now a policy keyed on substrate, not an ad-hoc per-product call.
25
+
26
+ - **Tier A — full theme adoption.** Set `data-sl-theme="light|dark|high-contrast"` on the root and consume the generated theme layer (`themes/*.css`) wholesale. Best for greenfield surfaces and chrome-heavy hosts that need every neutral. **Adopter:** spexr (its Theia chrome needs the full neutral set; see `spexr-theme-contribution.ts`).
27
+ - **Tier B — shared-root adoption.** Vendor only the shared `:root` layer of the token CSS (`--sl-*` type/space/radius/motion/etc., theme-independent). The product keeps its own `[data-theme]` colour layer, but **every colour value in that layer must resolve to a Sondalab value** (a product token equals an `--sl-*` value or an accent-registry value; no free-floating hex). Best for surfaces with an existing editorial palette. **Adopters:** introspect, daos.
28
+
29
+ **Why not one tier:** the generated theme layer is scoped `[data-sl-theme]`; a product on `[data-theme]` can never match that selector, so forcing Tier A on introspect/daos would mean rewriting their working colour layers for no visual gain. Tier B keeps their selector and pins their *values*.
30
+
31
+ **Compliance test (both tiers):** open the surface, sample any surface/text/accent colour, and confirm it equals a value in `sondalab.tokens.json` or the accent registry below. Tier is an implementation detail; value-equality is the invariant.
32
+
33
+ **Migration note (the loser):** Tier B is the default for products with a pre-existing palette. A Tier-B product should migrate to Tier A only if it later drops its own colour layer entirely (unlikely for introspect/daos). No forced migration.
34
+
35
+ ## Decision 2 — Product accent registry
36
+
37
+ Shared grammar, per-product accent. The brand default is amber (with a teal *depth* companion); each product may override the accent hue only. Everything else (neutrals, type, spacing, motion) is shared.
38
+
39
+ | Product | Accent (light / dark) | Source | Note |
40
+ |---|---|---|---|
41
+ | **Sondalab default / daos** | `#B56A0C` / `#F5A83C` (depth `#14606C` / `#35A0B4`) | `sondalab.tokens.json` `accent.*.default` | daos re-baselines from its near-amber `#9a6a15`/`#d29a44` to the exact token values. |
42
+ | **spexr** | `#5b6cff` / `#8b96ff` | `spexr-overrides.css` | Delivered. |
43
+ | **introspect** | `#2ee6c0` (single) | introspect `--cy` | ⚠ Differs from the Sondalab teal depth `#35A0B4`. **Resolved:** introspect's brighter mint-cyan is registered as its product accent (kept); it is not required to match `depth`. `depth` is the *default* product's teal, not a constraint on cyan-accented products. |
44
+
45
+ Machine values of this table: `packages/ui-kit/agent/accent-registry.json` (consumed by the compliance audit). The table above stays authoritative for the reasoning; `scripts/accent-registry.test.mjs` asserts product-set parity between the two.
46
+
47
+ Rule: a product picks **one** accent hue and derives hover/active/subtle from it; it never introduces a second chromatic accent. Semantic status colours (ok/warn/err) are separate from the accent and shared — but the `--sl-status-*` tokens are tuned for **on-canvas** use. On an inverted surface (a block whose background is the strongest neutral, e.g. an ink-on-paper output panel) they lose contrast; there, derive an on-ink status pair locally rather than using the canvas tokens (daos `.artifact .ok`/`.no` are a correct instance of this — kept as light-on-ink greens/reds, not swapped to the darker canvas tokens).
48
+
49
+ ## The family-resemblance layer
50
+
51
+ Each rule below carries a **compliance level per substrate** (see the matrix). Layout rules are expressed as **invariants** (a spacing scale, a rule weight, a measure, a type-scale relationship) — never as fixed templates — so a data-grid app with hard pixel columns can satisfy them as readily as a marketing page.
52
+
53
+ ### L1 — Type treatment
54
+
55
+ - **Three roles, fixed pairing.** Display/serif = `--sl-font-display` (Fraunces); body/UI sans = `--sl-font-sans` (Instrument Sans); mono = `--sl-font-mono` (Martian Mono). A surface may substitute the *mono* face for a narrower one where fixed pixel grids demand it (introspect keeps IBM Plex Mono; Martian is materially wider and overflows `ch`-clamped columns) — this is an `adapt`, not a violation, provided the substitute stays monospace and the sans/display roles are unchanged.
56
+ - **Structural labels are mono, uppercase, wide-tracked.** Eyebrows, section indices, meta, wordmark, badges, tags: `--sl-font-mono`, `text-transform: uppercase`, `letter-spacing` in the `0.14em–0.22em` band, size `--sl-text-xs`. This mono-label motif is the single strongest family signal — a surface reads as Sondalab primarily through it.
57
+ - **Headings.** `font-weight: 600`, `text-wrap: balance`, `letter-spacing: -0.02em`, `line-height: --sl-line-tight`. Sizes step off the shared scale (`--sl-text-xl`/`--sl-text-2xl`) for in-flow headings.
58
+ - **Oversized display is a structural element, used sparingly.** Hero/anchor headings scale beyond the fixed token scale via a fluid clamp (pattern: `clamp(2.7rem, 1.6rem + 5vw, 5.4rem)`), not a token — the size is fluid, so it is a documented pattern, not a `--sl-text-*` addition. At most one oversized anchor per screen.
59
+ - **Body measure.** Running text is capped at ~`63ch` (convention var `--sl-measure: 63ch`, defined locally — not a token, it is layout-level). Never let prose run full-bleed.
60
+
61
+ ### L2 — Structure & spacing
62
+
63
+ - **Spacing rhythm off the scale.** All gaps/padding step off `--sl-space-*` (0.25rem base). No arbitrary spacing values in new work.
64
+ - **Hairline structure.** Structural separation is a single `1px` rule in the theme's line colour — section tops, header/footer borders, card edges, gutter rulers. Weight is always `1px`; no heavy borders, no drop-shadow-as-separator (shadows are reserved for genuinely floating surfaces via `--sl-shadow-*`).
65
+ - **Hairline grid technique.** Card grids are formed by a `1px` gap over a line-colour background (`display: grid; gap: 1px; background: <line>`), cells painting their own surface — producing crisp shared rules between cells instead of doubled borders. This is a signature; prefer it over bordered-card grids (which the house style otherwise avoids).
66
+ - **Gutter & ruler.** Content sits in a max-width wrap (`~78rem`) with a fluid gutter (`clamp(1.25rem, 5vw, 6rem)`, convention var `--sl-gutter`); on wide viewports a `1px` vertical ruler at the gutter edge is an optional editorial accent.
67
+ - **Sections lead with an index.** A section opens with a mono `sec-idx` label (numbered where the content is genuinely a sequence — see the structure-is-information rule) beside its heading, separated from the previous section by a `1px` top rule.
68
+
69
+ ### L3 — Signature components
70
+
71
+ These recurring parts carry the family look. A surface need not use all, but any it does use should match:
72
+
73
+ - **Wordmark (official logo)** — the brand is typographic, no pictorial mark. Primary: `sondalab.` set in `--sl-font-display` (Fraunces) weight 500, `letter-spacing: -0.05em`, with the trailing period in the accent (`.dot { color: accent }`). Compact lockup (headers, tight spots): `sonda·lab` in `--sl-font-mono`, uppercase, tracked `0.2em`, with the middle dot in the accent. Source of truth: the identity board artifact `https://claude.ai/code/artifact/abbebb7f-5778-4af6-a9b4-2b3f169ae20f`; reference implementation: `sondalab-ui/showcase/index.html` (`.brand-word` hero + `.wordmark` header).
74
+ - **Eyebrow** — mono uppercase micro-label above a heading (`--sl-text-xs`, tracked `0.22em`, muted).
75
+ - **Tag / tier marker** — inline mono uppercase pill: `1px` border in a soft accent tint, small dot or square glyph before the label, accent-coloured text. Used for status/classification.
76
+ - **Inversion band** — a full-width block flipped to `text-on-*` on the strongest neutral (ink background, paper text), used to punctuate rhythm and mark the thesis or primary call-to-action. The recurring inversion is a core motif; at least one per long page is idiomatic.
77
+ - **Sticky translucent header** — `position: sticky`, background `color-mix(paper 86%, transparent)` + `backdrop-filter: blur()`, `1px` bottom rule; brand wordmark (mono, tracked) left, mono meta/controls right.
78
+ - **Real-content blocks** — where a surface shows output (JSON, terminal, code, data rows), show *actual* content in mono on the ink surface, not illustrative filler. This is both an editorial-voice rule and a visual one.
79
+
80
+ **Canonical recipe — eyebrow + tag.** These two are the strongest family signal, so their values are fixed here (a convention, like `--sl-measure`; not a token, because products bind them to their own local colour vars). Every implementation cites this recipe rather than re-deriving it; a product substitutes its own token names for the roles in brackets.
81
+
82
+ ```css
83
+ /* eyebrow — mono uppercase micro-label */
84
+ .eyebrow {
85
+ font-family: var(--sl-font-mono); /* or the product's mono face */
86
+ font-size: var(--sl-text-xs); /* ~9.5–11px in px-scaled UIs */
87
+ letter-spacing: 0.2em; /* band 0.18–0.22em */
88
+ text-transform: uppercase;
89
+ color: [text-muted];
90
+ }
91
+ /* tag / tier marker — accent-tinted pill with a leading dot */
92
+ .tag {
93
+ display: inline-flex; align-items: center; gap: 6px;
94
+ font-family: var(--sl-font-mono);
95
+ font-size: var(--sl-text-xs); font-weight: 500;
96
+ letter-spacing: 0.16em; text-transform: uppercase;
97
+ color: [accent];
98
+ border: 1px solid [accent-subtle];
99
+ background: [accent-subtle];
100
+ padding: 3px 7px; line-height: 1;
101
+ }
102
+ .tag::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: [accent]; flex: none; }
103
+ ```
104
+
105
+ Current citations: introspect `.sl-eyebrow`/`.sl-tag` in `introspect/web/src/theme.css`; spexr `.spexr-agent-chat__label`, `.spexr-spec-resources__caption`, and `.spexr-df-card__status` in `packages/theia-extensions/src/browser/style/spexr.css` — these adopt the **eyebrow** half of the recipe (mono uppercase tracked, via spexr's `--sl-font-code`). spexr has **not** adopted the tag/pill half; its existing status pills `.spexr-df-pill--attn/--error` are untouched. In spexr the eyebrow face is the design mono `--sl-font-code`, deliberately distinct from code-content runs (`.spexr-df-card__branch`/`__trail`, which mirror the editor font `--theia-code-font-family`): labels are design-layer, code content mirrors the editor.
106
+
107
+ ### L4 — Motion
108
+
109
+ - **One easing, three speeds.** All transitions use `--sl-motion-ease` with `--sl-motion-fast|mid|slow`. Entrance reveals use a small rise (`translateY(~6px)` + opacity) on `--sl-motion-mid|slow`.
110
+ - **Stagger, don't fade-everything.** Where multiple related elements enter, stagger them (nth-child delays) rather than fading the whole screen at once.
111
+ - **Respect reduced motion.** The token layer already zeroes durations under `prefers-reduced-motion`; never re-introduce motion that bypasses the tokens.
112
+ - **Interaction, not opacity.** Hover/focus states change colour/border/background meaningfully; `hover:opacity` alone is not a state.
113
+
114
+ ### L5 — Editorial voice (copy)
115
+
116
+ - **Precise and technical, never marketing.** Name things by what they are and do. Prefer a real measured figure or executed output to an adjective.
117
+ - **Active voice; controls state their effect.** A button says what happens; its confirmation says it happened.
118
+ - **Structure encodes truth.** Numbered markers, eyebrows, dividers appear only where they reflect something real (an actual sequence, a real classification) — not as decoration.
119
+ - **English for product copy and all identifiers**, matching the suite.
120
+
121
+ ## Per-substrate compliance matrix
122
+
123
+ Substrates: **IDE** (fixed-chrome host, e.g. spexr in Theia), **App** (data-grid web app, e.g. introspect), **Page** (marketing/landing, e.g. daos), **Term** (terminal/CLI, e.g. smartctx). Levels: **R** required · **A** adapt (honour the intent within substrate limits) · **N/A** not applicable.
124
+
125
+ | Rule | IDE | App | Page | Term |
126
+ |---|---|---|---|---|
127
+ | Theme selector (Tier A/B) | R (A) | R (B) | R (B) | N/A |
128
+ | Accent registry (one hue) | R | R | R | R (roles) |
129
+ | L1 type roles (display/sans/mono) | R | A (mono sub allowed) | R | A (mono only) |
130
+ | L1 mono uppercase structural labels | R | R | R | A (dim/caps where ANSI allows) |
131
+ | L1 heading treatment | A (host headings limited) | R | R | N/A |
132
+ | L1 oversized display anchor | N/A | A (where a hero exists) | R | N/A |
133
+ | L1 body measure ~63ch | A | R | R | R (wrap width) |
134
+ | L2 spacing off `--sl-space-*` | R | R | R | A (char cells) |
135
+ | L2 hairline `1px` structure | A (host chrome fixed) | R | R | A (box-drawing rules) |
136
+ | L2 hairline-grid technique | N/A | R | R | N/A |
137
+ | L2 gutter/ruler | N/A | A | R | N/A |
138
+ | L2 section index | N/A | A | R | N/A |
139
+ | L3 eyebrow / tag / tier | A | R | R | A |
140
+ | L3 inversion band | N/A | A | R | A (inverse video attribute) |
141
+ | L3 sticky translucent header | N/A | R | R | N/A |
142
+ | L3 real-content blocks | R | R | R | R |
143
+ | L4 motion (ease/speeds/stagger) | A | R | R | N/A |
144
+ | L4 reduced-motion respect | R | R | R | N/A |
145
+ | L5 editorial voice | R | R | R | R |
146
+
147
+ ## Reconciling shared grammar with "one unexpected layout decision per screen"
148
+
149
+ The house aesthetic (personal `CLAUDE.md`, UI Aesthetics) requires ≥1 unexpected, intentional layout decision per screen. That does **not** conflict with this shared grammar: the grammar fixes the *materials* (type roles, spacing scale, rule weight, motifs, motion, voice), while the surprise is spent on *composition* (an asymmetric split, an oversized anchor, a full-bleed inversion, an unexpected adjacency). Family resemblance comes from shared materials; distinctiveness comes from per-screen composition. A screen that satisfies the grammar but reads as a generic centred card grid has failed the house rule; a screen that invents a new type scale or a second accent to be "surprising" has failed the grammar. Spend boldness on layout, keep the materials shared.
150
+
151
+ ## New-token policy
152
+
153
+ Express the language in **existing** `--sl-*` tokens. A new token forces regenerating `packages/`, satisfying `check-drift.yml`, and bumping **both** `packages/ui-kit/package.json` and `packages/palette-py/pyproject.toml` in the same PR (see `AGENTS.md`). Therefore:
154
+
155
+ - **Kept as conventions, not tokens** (layout-level, fluid, or product-local): `--sl-measure` (~63ch), `--sl-gutter` (fluid clamp), the oversized-display clamp pattern. Products define these locally with the canonical values above.
156
+ - **A new token is justified only** for a genuine shared, fixed, theme-independent value that recurs across ≥2 products and cannot be a convention — and then it goes through the full regen + dual-bump flow.
157
+
158
+ ## Pass-1 application (scope & order)
159
+
160
+ Full task breakdown lives in the implementation plan (writing-plans). Order (daos is the source, so it is extracted-from first and re-based later, once the language is written):
161
+
162
+ 1. **Extract from daos** → this spec. *(Done at authoring.)*
163
+ 2. **Write this spec** (`design-language.md`). *(This file.)*
164
+ 3. **sondalab-ui showcase** — a page that renders the tokens + L3 signature components against this spec. Proves the language at zero user-facing risk. Status target: **PoC implemented (not delivered)**.
165
+ 4. **Re-baseline daos landing** — swap its local palette vars onto Sondalab values (Tier B), align accent to the registry, mono-label/measure already conformant. Low risk; aesthetics already correct.
166
+ 5. **introspect** — apply beyond the type swap: mono structural labels, hairline structure, section indices, signature header, editorial voice. Most work, most regression surface; done last.
167
+
168
+ spexr is out of pass 1 (token + chrome already delivered); its remaining margin is L3 signature components inside SPEXR panels, tracked separately.
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/env node
2
+ // sl-audit — deterministic Sondalab design-system compliance audit.
3
+ // Checks only what is decidable from CSS text (colour value-equality, tier
4
+ // consistency). Judgement-level rules (missing labels, editorial voice,
5
+ // layout composition) are out of scope by design — they belong to the
6
+ // sl-ds-auditor subagent. See design-system-agentic-spec.md.
7
+ //
8
+ // Ships inside @sondalab/ui-kit as the `sl-audit` bin. It resolves the colour
9
+ // allowlist from the package's own token CSS (tokens.css + themes/*.css), so it
10
+ // works identically in-repo and when installed in a consumer's node_modules.
11
+ // Surface globs resolve against the caller's cwd.
12
+
13
+ import { readFileSync, globSync } from "node:fs";
14
+ import { fileURLToPath } from "node:url";
15
+ import { dirname, resolve, relative } from "node:path";
16
+
17
+ const HERE = dirname(fileURLToPath(import.meta.url)); // packages/ui-kit/agent
18
+ const PKG_DIR = resolve(HERE, ".."); // packages/ui-kit — holds tokens.css + themes/
19
+ const TOKEN_CSS = ["tokens.css", "themes/light.css", "themes/dark.css", "themes/high-contrast.css"];
20
+
21
+ // --- colour normalization -------------------------------------------------
22
+
23
+ /** Canonicalize a CSS colour literal to lowercase `#rrggbb` or `#rrggbbaa`.
24
+ * Returns null for anything that is not a hex/rgb literal (var(), keywords). */
25
+ export function normalizeColor(raw) {
26
+ if (typeof raw !== "string") return null;
27
+ const s = raw.trim().toLowerCase();
28
+
29
+ const hex = /^#([0-9a-f]{3,8})$/.exec(s);
30
+ if (hex) {
31
+ const h = hex[1];
32
+ if (h.length === 3) return "#" + [...h].map((c) => c + c).join("");
33
+ if (h.length === 4) return "#" + [...h].map((c) => c + c).join("");
34
+ if (h.length === 6) return "#" + h;
35
+ if (h.length === 8) return "#" + h;
36
+ return null; // 5 or 7 digits: not a valid colour
37
+ }
38
+
39
+ const rgb = /^rgba?\(([^)]*)\)$/.exec(s);
40
+ if (rgb) {
41
+ const body = rgb[1].trim();
42
+ let rgbPart = body;
43
+ let alpha = null;
44
+ if (body.includes("/")) {
45
+ const [lhs, rhs] = body.split("/");
46
+ rgbPart = lhs.trim();
47
+ alpha = rhs.trim();
48
+ }
49
+ const parts = rgbPart.split(/[\s,]+/).filter(Boolean);
50
+ if (parts.length < 3) return null;
51
+ if (alpha === null && parts.length >= 4) alpha = parts[3];
52
+ const [r, g, b] = parts.slice(0, 3).map((n) => channelToHex(n));
53
+ if (r === null || g === null || b === null) return null;
54
+ let out = "#" + r + g + b;
55
+ if (alpha !== null) {
56
+ const a = alphaToHex(alpha);
57
+ if (a !== null && a !== "ff") out += a;
58
+ }
59
+ return out;
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ function channelToHex(n) {
66
+ let v;
67
+ if (n.endsWith("%")) v = Math.round((parseFloat(n) / 100) * 255);
68
+ else v = Math.round(parseFloat(n));
69
+ if (!Number.isFinite(v)) return null;
70
+ v = Math.max(0, Math.min(255, v));
71
+ return v.toString(16).padStart(2, "0");
72
+ }
73
+
74
+ function alphaToHex(a) {
75
+ const v = parseFloat(a);
76
+ if (!Number.isFinite(v)) return null;
77
+ return Math.round(Math.max(0, Math.min(1, v)) * 255)
78
+ .toString(16)
79
+ .padStart(2, "0");
80
+ }
81
+
82
+ /** The opaque base (`#rrggbb`) of a normalized colour, dropping any alpha. */
83
+ export function colorBase(normalized) {
84
+ return normalized.slice(0, 7);
85
+ }
86
+
87
+ // --- extraction -----------------------------------------------------------
88
+
89
+ const COLOR_RE = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})\b|rgba?\([^)]*\)/g;
90
+
91
+ /** Every hex/rgb literal in a CSS string, with 1-based line numbers.
92
+ * var() references are intentionally ignored — they resolve to tokens. */
93
+ export function extractColors(css) {
94
+ const out = [];
95
+ for (const m of css.matchAll(COLOR_RE)) {
96
+ const line = css.slice(0, m.index).split("\n").length;
97
+ out.push({ value: m[0], line });
98
+ }
99
+ return out;
100
+ }
101
+
102
+ function collectStrings(node, acc) {
103
+ if (typeof node === "string") acc.push(node);
104
+ else if (Array.isArray(node)) for (const v of node) collectStrings(v, acc);
105
+ else if (node && typeof node === "object")
106
+ for (const v of Object.values(node)) collectStrings(v, acc);
107
+ return acc;
108
+ }
109
+
110
+ /** Set of opaque colour bases (`#rrggbb`) appearing anywhere in a tokens.json
111
+ * object. Kept for unit tests and callers that hold the token object. */
112
+ export function buildAllowlistBases(tokens) {
113
+ const bases = new Set();
114
+ for (const s of collectStrings(tokens, [])) {
115
+ for (const { value } of extractColors(s)) {
116
+ const norm = normalizeColor(value);
117
+ if (norm) bases.add(colorBase(norm));
118
+ }
119
+ }
120
+ return bases;
121
+ }
122
+
123
+ /** Set of opaque colour bases extracted from token CSS text (tokens.css +
124
+ * themes/*.css). This is the shipped, drift-proof allowlist source — the CSS is
125
+ * generated from sondalab.tokens.json and always ships with the package. */
126
+ export function buildAllowlistBasesFromCss(cssTexts) {
127
+ const bases = new Set();
128
+ for (const css of cssTexts) {
129
+ for (const { value } of extractColors(css)) {
130
+ const norm = normalizeColor(value);
131
+ if (norm) bases.add(colorBase(norm));
132
+ }
133
+ }
134
+ return bases;
135
+ }
136
+
137
+ function accentColorStrings(accentEntry) {
138
+ return collectStrings(accentEntry, []).filter((s) => normalizeColor(s));
139
+ }
140
+
141
+ // --- tier detection -------------------------------------------------------
142
+
143
+ /** 'A' (data-sl-theme), 'B' (data-theme), or null if neither appears. */
144
+ export function detectTier(css) {
145
+ if (/\[data-sl-theme/.test(css)) return "A";
146
+ if (/\[data-theme/.test(css)) return "B";
147
+ return null;
148
+ }
149
+
150
+ // --- selector context -----------------------------------------------------
151
+
152
+ /** For each 1-based line, a context string = the enclosing selector plus the
153
+ * current declaration (property + value accumulated across lines, reset at
154
+ * `;`/`{`/`}`). The declaration part carries the property name onto every line
155
+ * of a multi-line value, so blessed suppression keyed on a custom-property name
156
+ * (e.g. `--bg-grad`) reaches its continuation lines. Not a full CSS parser. */
157
+ function contextByLine(css) {
158
+ const lines = css.split("\n");
159
+ const map = new Array(lines.length + 1).fill("");
160
+ let selector = "";
161
+ let selPending = "";
162
+ let decl = "";
163
+ lines.forEach((line, i) => {
164
+ // snapshot the state entering this line, then include the raw line so a
165
+ // selector or property declared on this same line is matchable too.
166
+ map[i + 1] = selector + " " + decl + " " + line;
167
+ for (const ch of line) {
168
+ if (ch === "{") {
169
+ selector = selPending.trim();
170
+ selPending = "";
171
+ decl = "";
172
+ } else if (ch === "}") {
173
+ selector = "";
174
+ selPending = "";
175
+ decl = "";
176
+ } else if (ch === ";") {
177
+ decl = "";
178
+ selPending += ch;
179
+ } else {
180
+ selPending += ch;
181
+ decl += ch;
182
+ }
183
+ }
184
+ });
185
+ return map;
186
+ }
187
+
188
+ /** Whether a blessed selector appears in a context string, delimited so a
189
+ * short name does not match a longer one (`--rule` must not match `--rule-2`
190
+ * or `--ruler`). Empty selectors never match. */
191
+ export function matchesSelector(context, selector) {
192
+ const sel = selector.trim();
193
+ if (!sel) return false;
194
+ const esc = sel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
195
+ return new RegExp(`(?<![\\w-])${esc}(?![\\w-])`).test(context);
196
+ }
197
+
198
+ // --- audit ----------------------------------------------------------------
199
+
200
+ function levelFor(matrix, rule, substrate) {
201
+ if (!matrix || !substrate) return "R";
202
+ const row = matrix.rules?.[rule];
203
+ return row?.[substrate] ?? "R";
204
+ }
205
+
206
+ /** Audit one CSS string. Pure — no I/O. Provide either `allowlistBases` (a Set
207
+ * of `#rrggbb` bases, the shipped path) or `tokens` (a tokens.json object, the
208
+ * test path); `allowlistBases` wins when both are given. */
209
+ export function auditCss({
210
+ css,
211
+ tokens,
212
+ allowlistBases,
213
+ accentEntry = {},
214
+ blessed = { deviations: [] },
215
+ declaredTier = null,
216
+ substrate = null,
217
+ matrix = null,
218
+ file = "",
219
+ }) {
220
+ const findings = [];
221
+ const bases = allowlistBases ? new Set(allowlistBases) : buildAllowlistBases(tokens);
222
+ for (const s of accentColorStrings(accentEntry)) bases.add(colorBase(normalizeColor(s)));
223
+
224
+ const colorDeviations = (blessed.deviations || []).filter((d) => d.kind === "color");
225
+ const blessedColorSelectors = colorDeviations
226
+ .flatMap((d) => d.selectors || [])
227
+ .map((s) => s.trim())
228
+ .filter(Boolean); // an empty selector would match every context — drop it
229
+ // value-based blessing — the honest mechanism for demo data / literals that
230
+ // do not live under a distinctive selector (e.g. a JS swatch object).
231
+ const blessedValueBases = new Set(
232
+ colorDeviations
233
+ .flatMap((d) => d.values || [])
234
+ .map((v) => normalizeColor(v))
235
+ .filter(Boolean)
236
+ .map(colorBase),
237
+ );
238
+
239
+ // check 2 — tier consistency
240
+ const detected = detectTier(css);
241
+ if (declaredTier && detected && detected !== declaredTier) {
242
+ const level = levelFor(matrix, "theme-selector", substrate);
243
+ if (level !== "N/A")
244
+ findings.push({
245
+ rule: "theme-selector",
246
+ level,
247
+ file,
248
+ line: css.slice(0, css.indexOf(detected === "A" ? "[data-sl-theme" : "[data-theme")).split("\n").length,
249
+ message: `selector implies Tier ${detected} but product declared Tier ${declaredTier}`,
250
+ });
251
+ }
252
+
253
+ // check 1 — colour value-equality
254
+ const ctxMap = contextByLine(css);
255
+ for (const { value, line } of extractColors(css)) {
256
+ const norm = normalizeColor(value);
257
+ if (!norm) continue;
258
+ if (bases.has(colorBase(norm))) continue;
259
+ if (blessedValueBases.has(colorBase(norm))) continue;
260
+ const context = ctxMap[line] || "";
261
+ const blessedHit = blessedColorSelectors.some((sel) => matchesSelector(context, sel));
262
+ if (blessedHit) continue;
263
+ const level = levelFor(matrix, "accent-one-hue", substrate);
264
+ if (level === "N/A") continue;
265
+ findings.push({
266
+ rule: "accent-one-hue",
267
+ level,
268
+ file,
269
+ line,
270
+ message: `colour ${value} does not resolve to a token, product accent, or blessed value`,
271
+ });
272
+ }
273
+
274
+ return {
275
+ file,
276
+ substrate,
277
+ tier: { declared: declaredTier, detected },
278
+ findings,
279
+ };
280
+ }
281
+
282
+ // --- registry / config resolution ----------------------------------------
283
+
284
+ export function resolveAccentEntry(registry, product) {
285
+ const seen = new Set();
286
+ let name = product;
287
+ while (name) {
288
+ if (seen.has(name)) throw new Error(`accent-registry: circular inherits at "${name}"`);
289
+ seen.add(name);
290
+ const entry = registry.products?.[name];
291
+ if (!entry) return {};
292
+ if (entry.inherits) {
293
+ name = entry.inherits;
294
+ continue;
295
+ }
296
+ return entry;
297
+ }
298
+ return {};
299
+ }
300
+
301
+ function loadJson(path) {
302
+ return JSON.parse(readFileSync(path, "utf8"));
303
+ }
304
+
305
+ function loadTokenCss() {
306
+ const texts = [];
307
+ for (const rel of TOKEN_CSS) {
308
+ try {
309
+ texts.push(readFileSync(resolve(PKG_DIR, rel), "utf8"));
310
+ } catch {
311
+ /* a theme file may be absent in a trimmed install — skip it */
312
+ }
313
+ }
314
+ return texts;
315
+ }
316
+
317
+ // --- CLI ------------------------------------------------------------------
318
+
319
+ function parseArgs(argv) {
320
+ const args = {};
321
+ for (let i = 0; i < argv.length; i++) {
322
+ const a = argv[i];
323
+ if (a.startsWith("--")) args[a.slice(2)] = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : true;
324
+ }
325
+ return args;
326
+ }
327
+
328
+ function group(findings, level) {
329
+ return findings.filter((f) => f.level === level);
330
+ }
331
+
332
+ function main() {
333
+ const args = parseArgs(process.argv.slice(2));
334
+ if (!args.surface || !args.product) {
335
+ console.error("usage: sl-audit --surface <glob> --product <name> [--substrate IDE|App|Page|Term] [--fail-on R]");
336
+ process.exit(2);
337
+ }
338
+
339
+ const allowlistBases = buildAllowlistBasesFromCss(loadTokenCss());
340
+ const registry = loadJson(resolve(HERE, "accent-registry.json"));
341
+ const matrix = loadJson(resolve(HERE, "substrate-matrix.json"));
342
+ let blessed = { deviations: [] };
343
+ try {
344
+ blessed = loadJson(resolve(HERE, `blessed/${args.product}.json`));
345
+ } catch {
346
+ /* no blessed file yet — treat as none */
347
+ }
348
+
349
+ const substrate = args.substrate || matrix.products?.[args.product] || blessed.substrate || null;
350
+ const declaredTier = args.tier || matrix.tiers?.[args.product] || blessed.tier || null;
351
+ if (!declaredTier && substrate && substrate !== "Term")
352
+ console.warn(`warning: no tier declared for "${args.product}" (add it to substrate-matrix.json "tiers") — tier-consistency check disabled`);
353
+ const accentEntry = resolveAccentEntry(registry, args.product);
354
+
355
+ const cwd = process.cwd();
356
+ const files = globSync(args.surface, { cwd }).map((f) => resolve(cwd, f));
357
+ const all = [];
358
+ for (const file of files) {
359
+ const css = readFileSync(file, "utf8");
360
+ const res = auditCss({
361
+ css,
362
+ allowlistBases,
363
+ accentEntry,
364
+ blessed,
365
+ declaredTier,
366
+ substrate,
367
+ matrix,
368
+ file: relative(cwd, file),
369
+ });
370
+ all.push(...res.findings);
371
+ }
372
+
373
+ const R = group(all, "R");
374
+ const A = group(all, "A");
375
+ const naCount = matrix.rules
376
+ ? Object.values(matrix.rules).filter((r) => r[substrate] === "N/A").length
377
+ : 0;
378
+
379
+ console.log(`\n${args.product} (${substrate}, Tier ${declaredTier ?? "?"}) — sl-audit`);
380
+ console.log(` ${files.length} file(s) scanned\n`);
381
+ printBucket("R-violation", R);
382
+ printBucket("A-deviation", A);
383
+ console.log(`N/A: ${naCount} rules skipped for substrate ${substrate}\n`);
384
+
385
+ if (args["fail-on"] === "R" && R.length > 0) process.exit(1);
386
+ }
387
+
388
+ function printBucket(label, findings) {
389
+ if (findings.length === 0) {
390
+ console.log(`${label} (0)\n`);
391
+ return;
392
+ }
393
+ console.log(`${label} (${findings.length})`);
394
+ for (const f of findings) console.log(` ${f.file}:${f.line} ${f.message}`);
395
+ console.log("");
396
+ }
397
+
398
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
399
+ main();
400
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "$note": "Machine source of the per-substrate compliance matrix in design-language.md. level ∈ R (required) | A (adapt) | N/A (not applicable). Adaptation nuance (the parentheticals in the doc) stays in design-language.md; this file carries only the level per (rule, substrate) so the audit can decide what is blocking.",
3
+ "products": {
4
+ "spexr": "IDE",
5
+ "introspect": "App",
6
+ "daos": "Page",
7
+ "default": "Page",
8
+ "smartctx": "Term"
9
+ },
10
+ "tiers": {
11
+ "$note": "Theme-selector tier per product (design-language.md §Decision 1). Authoritative machine source for the audit's tier-consistency check. Term products have no tier (no theme selector).",
12
+ "spexr": "A",
13
+ "introspect": "B",
14
+ "daos": "B",
15
+ "default": "A"
16
+ },
17
+ "rules": {
18
+ "theme-selector": { "IDE": "R", "App": "R", "Page": "R", "Term": "N/A" },
19
+ "accent-one-hue": { "IDE": "R", "App": "R", "Page": "R", "Term": "R" },
20
+ "l1-type-roles": { "IDE": "R", "App": "A", "Page": "R", "Term": "A" },
21
+ "l1-mono-labels": { "IDE": "R", "App": "R", "Page": "R", "Term": "A" },
22
+ "l1-heading": { "IDE": "A", "App": "R", "Page": "R", "Term": "N/A" },
23
+ "l1-oversized-anchor":{ "IDE": "N/A", "App": "A", "Page": "R", "Term": "N/A" },
24
+ "l1-body-measure": { "IDE": "A", "App": "R", "Page": "R", "Term": "R" },
25
+ "l2-spacing-scale": { "IDE": "R", "App": "R", "Page": "R", "Term": "A" },
26
+ "l2-hairline-1px": { "IDE": "A", "App": "R", "Page": "R", "Term": "A" },
27
+ "l2-hairline-grid": { "IDE": "N/A", "App": "R", "Page": "R", "Term": "N/A" },
28
+ "l2-gutter-ruler": { "IDE": "N/A", "App": "A", "Page": "R", "Term": "N/A" },
29
+ "l2-section-index": { "IDE": "N/A", "App": "A", "Page": "R", "Term": "N/A" },
30
+ "l3-eyebrow-tag": { "IDE": "A", "App": "R", "Page": "R", "Term": "A" },
31
+ "l3-inversion-band": { "IDE": "N/A", "App": "A", "Page": "R", "Term": "A" },
32
+ "l3-sticky-header": { "IDE": "N/A", "App": "R", "Page": "R", "Term": "N/A" },
33
+ "l3-real-content": { "IDE": "R", "App": "R", "Page": "R", "Term": "R" },
34
+ "l4-motion": { "IDE": "A", "App": "R", "Page": "R", "Term": "N/A" },
35
+ "l4-reduced-motion": { "IDE": "R", "App": "R", "Page": "R", "Term": "N/A" },
36
+ "l5-voice": { "IDE": "R", "App": "R", "Page": "R", "Term": "R" }
37
+ }
38
+ }
@@ -0,0 +1,35 @@
1
+ # Workflow — Adopt a single Sondalab component
2
+
3
+ > **What this file is:** the ordered procedure for pulling **one** Sondalab component or signature pattern into an existing app **without** a full migration — the lightest of the three workflows. Companion to `migrate.md` (whole-surface compliance) and `author.md` (greenfield).
4
+ > **Reads:** `catalog.md` + `catalog.json` (the component), `../design-language.md` (grammar), `substrate-matrix.json` (substrate applicability), `sl-audit` (verdict, scoped to the touched file).
5
+ > **Invariant:** adopt touches only what it adds. The rest of the app is out of scope — do not audit or "fix" surrounding code. The change is small and reversible; the gate is showing the diff, not a full migration plan.
6
+
7
+ ## When to use this (vs migrate)
8
+
9
+ Use adopt when the ask is "add a Sondalab eyebrow/tag/inversion band here", "give this header the house look", "use the DS button on this one screen" — a taste of the family look, not a wholesale adoption. If the ask is "bring this app onto the DS", that is `migrate.md`.
10
+
11
+ ## Steps
12
+
13
+ ### 0 — Identify the component and the host
14
+ Pick the target from `catalog.md` (a primitive like `sl-tag`, or a signature pattern like the inversion band). Note the host app's **substrate** and **tier** — an adopted component must honour the host's tier: on a Tier-B app you bind the component's `--slc-*` role-vars to the app's local colour layer; you do not switch the app to `data-sl-theme`.
15
+
16
+ ### 1 — Bind, don't fork
17
+ Copy the component's markup from `catalog.md`. Resolve its `--slc-*`/`--sl-*` role-vars to the host's existing values — every one must resolve to a Sondalab token or the host's registered accent (Tier-B rule). If the host lacks a role-var the component needs, define it once, mapped to a Sondalab value. Never bind the component to a raw literal.
18
+
19
+ ### 2 — Insert
20
+ Place the component in the host markup. Keep the change minimal and localised — one component, the files it touches, nothing else. Match the host's existing structure and naming.
21
+
22
+ ### 3 — Audit the touched file only
23
+ ```sh
24
+ npx sl-audit --surface "<the one file you edited>" --product <host> --substrate <host substrate>
25
+ ```
26
+ The **added** component must contribute zero R-violations. Pre-existing findings elsewhere in the file are **not** yours to fix in an adopt — note them (they are migrate's job) but leave them. Populate `blessed/<host>.json` only for a deviation your added component legitimately needs.
27
+
28
+ ### 4 — Judgement check (the one component)
29
+ Against `design-language.md`, scoped to just what you added: does the eyebrow/tag read as mono-uppercase-tracked? Does an adopted inversion band avoid an alpha-on-canvas hairline inside it? Is the copy in the component precise/active (L5)? See `HANDOFF.md` for judgement vs mechanical.
30
+
31
+ ### 5 — Show the diff
32
+ Present the diff (it is small). No full migration plan — the change is localised and reversible. Render the touched screen across the host's themes to confirm the component sits right.
33
+
34
+ ## Output
35
+ One Sondalab component living correctly in the host app, bound to the host's colour layer, auditing clean for what it added — the rest of the app untouched.
@@ -0,0 +1,33 @@
1
+ # Workflow — Author a new Sondalab surface
2
+
3
+ > **What this file is:** the ordered procedure an agent follows to author a *new* interface or element that is Sondalab-compliant from the first line, using the component catalog. Companion to `migrate.md` (which brings an *existing* surface into compliance).
4
+ > **Reads:** `catalog.md` + `catalog.json` (components), `../design-language.md` (grammar), the token CSS (`../../tokens.css` + `../../themes/*.css`, the values), `substrate-matrix.json` (what's Required for the substrate), `sl-audit` (verdict).
5
+ > **Invariant:** authoring is greenfield, so the audit should pass on the *first* run — compliance is built in, not retrofitted. If your first audit is dirty, you bound a colour to a literal instead of a role-var.
6
+
7
+ ## Steps
8
+
9
+ ### 0 — Classify substrate
10
+ Decide the substrate (**IDE** / **App** / **Page** / **Term**) and the product (for its accent). The substrate's Required column in `substrate-matrix.json` is your checklist of what this surface must have.
11
+
12
+ ### 1 — Choose components from the catalog
13
+ For each part of the screen, pick the matching entry from `catalog.md`. Prefer the ready primitives (`sl-*` classes) over hand-rolled markup; reach for the signature patterns (sticky header, inversion band, hairline grid, section index, oversized anchor) to carry the family look. `catalog.json` is the machine index if you need to list what exists.
14
+
15
+ ### 2 — Compose (spend the surprise here)
16
+ Assemble the components into a layout. Family resemblance comes from the shared materials (the catalog); distinctiveness comes from composition — every screen needs **≥1 unexpected, intentional layout decision** (an asymmetric split, an oversized anchor, a full-bleed inversion). A generic centred card grid fails the house rule; a second accent or a new type scale fails the grammar. Spend boldness on layout, keep materials shared.
17
+
18
+ ### 3 — Bind role-vars per tier
19
+ - **Tier A** (greenfield is the common case): consume `@sondalab/ui-kit` `components.css` + a theme (`themes/*.css`) under `data-sl-theme`. Components resolve their `--slc-*` role-vars automatically.
20
+ - **Tier B** (you have an existing colour layer): map each `--slc-*` role-var to a value that resolves to a Sondalab token or your registered accent. Never bind a component to a literal.
21
+
22
+ ### 4 — Audit + judgement pass
23
+ Run the audit (must be clean on greenfield):
24
+ ```sh
25
+ npx sl-audit --surface "<glob>" --product <name>
26
+ ```
27
+ Then the judgement pass the script can't do (delegate to `sl-ds-auditor` or do it yourself against `design-language.md`, scoped to the substrate's Required rules): are the structural mono labels present (eyebrow, section index)? Is there real content, not filler? Is the editorial voice precise/active (L5)? See `HANDOFF.md` for what is judgement vs mechanical.
28
+
29
+ ### 5 — Visual check
30
+ Render across the applicable themes (light / dark / high-contrast); verify reduced-motion and that active/pressed states show the accent, not a neutral.
31
+
32
+ ## Output
33
+ A new surface that passes `sl-audit` clean, satisfies its substrate's Required rules, and reads as Sondalab through the shared components — with its one intentional composition surprise.
@@ -0,0 +1,45 @@
1
+ # Workflow — Migrate a surface onto the Sondalab DS
2
+
3
+ > **What this file is:** the ordered, harness-agnostic procedure an agent follows to migrate an existing app's UI onto the Sondalab design system. It is the portable core; the Claude Code `sl-migrate` skill loads and executes it. Any other harness can follow it directly.
4
+ > **Reads:** `../design-language.md` (grammar), the token CSS (`../../tokens.css` + `../../themes/*.css`, the values), the agent core (`accent-registry.json`, `substrate-matrix.json`, `blessed/<product>.json`), `sl-audit` (verdict engine).
5
+ > **Invariant:** migrating a live surface is semi-irreversible. Work on a branch; do not edit before the human approves the mapping plan (Step 3 gate).
6
+
7
+ ## Division of labour (do not blur)
8
+
9
+ - **`sl-audit.mjs`** — the mechanical verdict: colour value-equality and tier consistency, decidable from CSS text. Trust it for those; never re-derive them by eye.
10
+ - **You (the agent)** — the judgement: is a structural label *missing*, is the editorial voice off, is an inversion band warranted, does the composition earn its "one unexpected decision". The audit cannot see these; you read `design-language.md` and decide.
11
+ - **The human** — approves the blessed list (Step 2) and the mapping plan (Step 3). Applies nothing without that yes.
12
+
13
+ ## Steps
14
+
15
+ ### 0 — Classify substrate
16
+ Determine the product name and its substrate: **IDE** (fixed-chrome host), **App** (data-grid web app), **Page** (marketing/landing), **Term** (terminal). Read it from `substrate-matrix.json` `products` if listed, else infer. The substrate selects which rules are Required vs Adapt vs N/A — skip this and you will, e.g., propose a sticky translucent header for a Theia panel the matrix marks N/A.
17
+
18
+ ### 1 — Baseline audit
19
+ Create a branch. Run the audit, no edits:
20
+ ```sh
21
+ npx sl-audit --surface "<glob>" --product <name> --substrate <IDE|App|Page|Term>
22
+ ```
23
+ Record the grouped baseline (R-violation / A-deviation / N/A counts). This is the before-picture the final report is measured against.
24
+
25
+ ### 2 — Separate blessed deviations from defects · GATE
26
+ `design-language.md` authorizes colours that fail literal value-equality: accent-derived hover/active/subtle tints, locally-derived on-ink status pairs, alpha-on-canvas hairlines. For each finding decide:
27
+ - **Blessed** → add an entry to `blessed/<product>.json` with a `why` that cites the authorizing `design-language.md` rule. It silences the finding *and* records the intentional deviation.
28
+ - **Defect** → keep it on the list to fix in Step 3.
29
+
30
+ An accent-derivation swamp (many tints off the product's single accent) is the common case for Tier-B products; bless the derivations as a group rather than mapping each to a token. **Present the blessed list and wait for the human's confirmation before continuing.**
31
+
32
+ ### 3 — Propose the mapping plan · GATE
33
+ For each remaining R-violation, map the free-floating value to an `--sl-*` role or the product's accent-registry value. Add your judgement-level findings (missing mono labels, hairline structure, section index, editorial voice) scoped to the substrate's Required rules. Present the plan **in chat**: the value→token mapping plus the concrete edits. **Stop. Apply nothing until the human says yes.**
34
+
35
+ ### 4 — Apply
36
+ After approval, edit per the product's tier (A: consume the theme layer; B: keep the local colour layer but pin every value to a Sondalab value). Small, rollback-friendly chunks.
37
+
38
+ ### 5 — Re-audit and diff
39
+ Re-run the audit. R-violations must reach 0 or be blessed. Produce a before/after report (baseline counts vs current).
40
+
41
+ ### 6 — Visual check
42
+ Render the surface across the applicable themes (light / dark / high-contrast). Verify reduced-motion. Check the dark-invert hairline regression explicitly: alpha-on-canvas rules (`--rule` style) can go invisible inside an inverted block — confirm they remain visible.
43
+
44
+ ## Output
45
+ A compliant surface + the before/after compliance report + a populated `blessed/<product>.json` + the CSS diff, all on the branch for review.
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@sondalab/ui-kit",
3
- "version": "0.2.0",
4
- "description": "Sondalab design tokens as CSS custom properties: shared grammar + per-product accent, with light / dark / high-contrast themes.",
3
+ "version": "0.3.0",
4
+ "description": "Sondalab design tokens as CSS custom properties + the agentic core: component catalog, compliance audit (sl-audit), and author/migrate/adopt workflows.",
5
5
  "license": "MIT",
6
6
  "author": "marcellobarile",
7
7
  "publishConfig": { "access": "public" },
8
8
  "type": "module",
9
+ "bin": {
10
+ "sl-audit": "./agent/sl-audit.mjs"
11
+ },
9
12
  "exports": {
10
13
  "./tokens.css": "./tokens.css",
11
14
  "./components.css": "./components.css",
@@ -15,7 +18,12 @@
15
18
  "./assets/wordmark.svg": "./assets/sondalab-wordmark.svg",
16
19
  "./assets/lockup.svg": "./assets/sondalab-lockup.svg",
17
20
  "./assets/favicon.svg": "./assets/favicon.svg",
18
- "./assets/app-icon.svg": "./assets/app-icon.svg"
21
+ "./assets/app-icon.svg": "./assets/app-icon.svg",
22
+ "./agent/sl-audit.mjs": "./agent/sl-audit.mjs",
23
+ "./agent/catalog.json": "./agent/catalog.json",
24
+ "./agent/accent-registry.json": "./agent/accent-registry.json",
25
+ "./agent/substrate-matrix.json": "./agent/substrate-matrix.json",
26
+ "./agent/*": "./agent/*"
19
27
  },
20
28
  "files": [
21
29
  "tokens.css",
@@ -28,6 +36,7 @@
28
36
  "assets/favicon.svg",
29
37
  "assets/app-icon.svg",
30
38
  "assets/README.md",
31
- "README.md"
39
+ "README.md",
40
+ "agent/"
32
41
  ]
33
42
  }