jig-ui 0.1.0 → 0.2.1

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 CHANGED
@@ -37,6 +37,17 @@ global one.
37
37
  Update later with `npx jig-ui@latest update` — files you have edited are left
38
38
  alone.
39
39
 
40
+ Install writes the rules and the design tokens into `.jig/`. Import the tokens
41
+ your surface needs — one brand file, one mode file:
42
+
43
+ ```css
44
+ @import ".jig/tokens/brand.default.css";
45
+ @import ".jig/tokens/mode.product.css";
46
+ ```
47
+
48
+ Copy `brand.default.css` to `brand.<yourproject>.css` and edit that; `jig update`
49
+ will not overwrite a file you have changed.
50
+
40
51
  Token setup is currently manual: copy `jig.config.example.json` to
41
52
  `jig.config.json` and import the token files described in
42
53
  `rules/02-tokens.md`. A scaffolding command for this is planned but not yet
@@ -46,14 +57,14 @@ implemented.
46
57
 
47
58
  | File | Contents | Load |
48
59
  | --- | --- | --- |
49
- | `rules/00-anti-patterns.md` | 48 universal rules with corrections | **Always** |
60
+ | `rules/00-anti-patterns.md` | 87 universal rules with corrections | **Always** |
50
61
  | `rules/01-modes.md` | `editorial` / `product` / `operator` profiles | **Always** |
51
62
  | `rules/02-tokens.md` | Token contract, naming, consumption | On setup, or when adding a token |
52
63
  | `rules/03-patterns.md` | Component anatomy and behaviour | When building a covered pattern |
53
64
  | `rules/04-principles.md` | Five frames + seven tiebreakers | Novel decisions, or rule conflicts |
54
65
  | `rules/05-copy.md` | Interface text rules | Writing any user-facing string |
55
- | `tokens/brand.*.css` | Identity. One per project. | Imported by the app |
56
- | `tokens/mode.*.css` | Density, scale, rhythm, motion | One per surface |
66
+ | `.jig/tokens/brand.*.css` | Identity. One per project. | Imported by the app |
67
+ | `.jig/tokens/mode.*.css` | Density, scale, rhythm, motion | One per surface |
57
68
 
58
69
  `00` and `01` are the always-loaded core and are sized to stay cheap in context. `03` is the largest file and should be loaded per-pattern rather than wholesale.
59
70
 
@@ -64,7 +75,7 @@ Drop this in the project root so mode selection does not require asking on every
64
75
  ```jsonc
65
76
  // jig.config.json
66
77
  {
67
- "brand": "tokens/brand.acme.css",
78
+ "brand": ".jig/tokens/brand.acme.css",
68
79
  "surfaces": [
69
80
  { "match": "/", "mode": "editorial" },
70
81
  { "match": "/app/**", "mode": "product" },
@@ -78,8 +89,8 @@ Without this file, follow the selection procedure in `rules/01-modes.md`: infer,
78
89
  ## Consuming tokens
79
90
 
80
91
  ```css
81
- @import "tokens/brand.acme.css"; /* one per project */
82
- @import "tokens/mode.product.css"; /* one per surface */
92
+ @import ".jig/tokens/brand.acme.css"; /* one per project */
93
+ @import ".jig/tokens/mode.product.css"; /* one per surface */
83
94
  ```
84
95
 
85
96
  Then `var(--color-text-strong)`, `var(--spacing-card)`, `var(--text-body)` in any framework. For Tailwind v4, wrap both imports in `@theme` to generate utilities. See `rules/02-tokens.md`.
@@ -108,8 +119,12 @@ Re-run after any significant edit to `00` or `03`.
108
119
 
109
120
  Written from general UI and accessibility practice, plus the constraints specific to agent-generated output — which is where most of the structure comes from: the anti-patterns-first ordering, the mode split, the brand × mode token architecture, and the decidability test applied to every rule.
110
121
 
111
- **Not yet reconciled against *Practical UI*.** The numeric defaults throughout are placeholders chosen for internal consistency, not values derived from any source. Adham Dannaway's book (2nd ed. 2024) is the intended source for most of them. See `RECONCILE.md` for the open list; where the book states a position, overwrite the default here.
112
-
113
- principles.design informed the rules-versus-principles split, and the standard `rules/04-principles.md` is held to.
122
+ **The numeric defaults are being reconciled.** Type scale, spacing steps, control sizes
123
+ and motion durations started as internally consistent guesses and are being checked, row by
124
+ row, against an external reference on interface design. `RECONCILE.md` tracks the status of
125
+ each: adopted, deliberately kept different, or still open. The accessibility floors are
126
+ outside that process — contrast ratios and target sizes come from WCAG 2.1 AA and are not
127
+ adjustable.
114
128
 
115
- Nothing here reproduces either source. Read the book — it teaches the reasoning that this system only records the output of.
129
+ principles.design informed the rules-versus-principles split, and the standard
130
+ `rules/04-principles.md` is held to.
package/dist/index.js CHANGED
@@ -250,11 +250,16 @@ function renderCommandTable(metadata) {
250
250
  }
251
251
 
252
252
  // src/install/vendor.ts
253
- function vendorHeader(file, version2) {
253
+ var DELIMITERS = {
254
+ html: ["<!--", "-->"],
255
+ css: ["/*", "*/"]
256
+ };
257
+ function vendorHeader(file, version2, style = "html") {
258
+ const [open, close] = DELIMITERS[style];
254
259
  return [
255
- `<!-- ${file} \u2014 vendored from Jig v${version2}.`,
260
+ `${open} ${file} \u2014 vendored from Jig v${version2}.`,
256
261
  " Licensed Apache-2.0. See .jig/LICENSE and .jig/NOTICE.",
257
- " Edit freely: `jig update` will not overwrite a file you have changed. -->",
262
+ ` Edit freely: \`jig update\` will not overwrite a file you have changed. ${close}`,
258
263
  "",
259
264
  ""
260
265
  ].join("\n");
@@ -361,6 +366,15 @@ function install(opts) {
361
366
  const body = readFileSync2(join3(rulesDir, file), "utf8");
362
367
  planned.push({ key: relKey(".jig", file), content: vendorHeader(file, opts.version) + body, checkable: true });
363
368
  }
369
+ const tokensDir = join3(opts.packageRoot, "tokens");
370
+ for (const file of readdirSync(tokensDir).filter((f) => f.endsWith(".css")).sort()) {
371
+ const body = readFileSync2(join3(tokensDir, file), "utf8");
372
+ planned.push({
373
+ key: relKey(".jig", "tokens", file),
374
+ content: vendorHeader(file, opts.version, "css") + body,
375
+ checkable: true
376
+ });
377
+ }
364
378
  planned.push({
365
379
  key: relKey(".jig", "rules.index.json"),
366
380
  content: readFileSync2(join3(opts.packageRoot, "rules.index.json"), "utf8"),
@@ -452,6 +466,15 @@ function update(opts) {
452
466
  }
453
467
  write(key, vendorHeader(file, opts.version) + readFileSync3(join4(rulesDir, file), "utf8"));
454
468
  }
469
+ const tokensDir = join4(opts.packageRoot, "tokens");
470
+ for (const file of readdirSync2(tokensDir).filter((f) => f.endsWith(".css")).sort()) {
471
+ const key = relKey(".jig", "tokens", file);
472
+ if (isModified(installRoot, key, existing)) {
473
+ skipped.push(key);
474
+ continue;
475
+ }
476
+ write(key, vendorHeader(file, opts.version, "css") + readFileSync3(join4(tokensDir, file), "utf8"));
477
+ }
455
478
  const indexKey = relKey(".jig", "rules.index.json");
456
479
  if (isModified(installRoot, indexKey, existing)) {
457
480
  skipped.push(indexKey);
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "jig-ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "A design system for coding agents. 104 numbered UI rules, brand x mode design tokens, and an installer for Claude Code, Codex, Cursor and opencode.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "bin": {
8
- "jig": "./dist/index.js"
8
+ "jig": "dist/index.js"
9
9
  },
10
10
  "files": [
11
11
  "dist",
@@ -67,12 +67,13 @@ Where several already apply — a table's rows are aligned, alike, and close —
67
67
  ✅ Let the content pick the layout. Three items of unequal weight are a list, not a grid.
68
68
 
69
69
  ### A-07 Oversized radius everywhere
70
- ❌ A single large radius (16px+) applied to cards, buttons, inputs and badges alike
71
- ✅ `--radius-control` for controls, `--radius-surface` for containers. Both derive from `--radius-sm`, which is a brand decision. Small elements take small radii.
70
+ ❌ One radius applied to cards, buttons, inputs and badges alike, regardless of element size
71
+ ✅ `--radius-control` for controls, `--radius-surface` for containers. `--radius-control` selects `sm` in every mode; `--radius-surface` selects `md` in `editorial` and `product`, `sm` in `operator` — the selection is per-mode, not a fixed derivation. Small elements take small radii.
72
+ In `operator`, `--radius-surface` also selects `sm`, so cards, buttons and inputs converge on one 8px radius. That is not this rule recurring: it is a per-mode selection made deliberately for density. The distinction is whether the value was chosen or defaulted to.
72
73
 
73
74
  ### A-08 Shadow as the only depth cue
74
75
  ❌ A drop shadow on every card, or several shadow sizes with no rule governing which means what
75
- ✅ `--shadow-surface` is `none` in all three modes. Depth comes from `--color-stroke-weak` or a surface-colour step. `--shadow-raised` exists for overlays only — dialogs, popovers, dropdowns.
76
+ ✅ `--shadow-surface` is `none` in all three modes. Depth comes from `--color-stroke-weak` or a surface-colour step. `--shadow-raised` exists for exactly two cases: overlays — dialogs, popovers, dropdowns — and sticky navigation in `editorial`, which must read as above the content scrolling beneath it.
76
77
 
77
78
  ### A-09 Marketing voice in an application
78
79
  ❌ "Supercharge your workflow" on an internal dashboard
@@ -191,7 +192,7 @@ Two legitimate departures, and they are not the same:
191
192
  ### C-68 Non-interactive elements styled like interactive ones
192
193
  ❌ A "Verified" badge with the brand fill and the shape of the primary button; decorative icons carrying the same border and colour as a secondary button
193
194
  ✅ Things that look alike are expected to behave alike. If an element does nothing when clicked, it must not carry the visual signature of something that does — brand fill, button shape, or control border.
194
- Differentiate deliberately: change the shape (a badge is more rounded than a button), the tone (`success` for a verified state rather than `brand`), and the emphasis (a `fill` background rather than a solid one, so the real primary action stays the most prominent thing on screen).
195
+ Differentiate deliberately: change the shape (a badge, pill, chip or avatar takes `--radius-full`, more rounded than a button's `--radius-control`), the tone (`success` for a verified state rather than `brand`), and the emphasis (a `fill` background rather than a solid one, so the real primary action stays the most prominent thing on screen).
195
196
  The converse also holds: two elements that do the same job should look the same.
196
197
 
197
198
  ---
package/rules/01-modes.md CHANGED
@@ -62,23 +62,25 @@ At a seam between modes:
62
62
  **Reader:** first-time, on mobile data, scanning before committing attention.
63
63
  **Tiebreaker:** legibility over density. When in doubt, larger and further apart.
64
64
 
65
- | Property | Value |
65
+ | Property | This mode selects |
66
66
  | --- | --- |
67
- | Body size | 18px (16px minimum on dense secondary text) |
68
- | Type ratio | 1.250 (major third) |
69
- | Heading ramp | 24 / 32 / 48 / 64 |
70
- | Measure | 68ch |
71
- | Base unit | 4px |
72
- | Section rhythm | 96px desktop · 56px mobile |
73
- | Card padding | 32px |
74
- | Control height | 48px |
75
- | Radius | brand default (typically 8px) |
76
- | Elevation | none, except sticky navigation |
77
- | Motion | 200–300ms; entrance animation permitted **once**, in the first viewport only |
78
- | Colour usage | Neutral-dominant. Accent for links and primary CTA only. |
67
+ | UI text | `--text-body` |
68
+ | Long-form text | `--text-prose` · `--leading-prose` |
69
+ | Heading ramp | `--text-h3` → `--text-h2` → `--text-h1` |
70
+ | Type ratio | 1.250 Major Third — the largest of the three |
71
+ | Measure | `--measure-prose` |
72
+ | Section rhythm | `--spacing-section` |
73
+ | Card padding | `--spacing-card` |
74
+ | Control height | `--size-control` — the tallest of the three |
75
+ | Radius | `--radius-control` (sm) · `--radius-surface` (md) |
76
+ | Elevation | `--shadow-none`; `--shadow-raised` for sticky nav only |
77
+ | Motion | `--duration-base`; entrance animation **once**, first viewport only |
78
+ | Colour usage | Neutral-dominant. `--color-brand` for links and primary CTA only. |
79
79
  | Imagery | Central. Real photography or commissioned illustration. |
80
80
  | Keyboard | Standard tab order; no shortcuts expected |
81
81
 
82
+ Resolved values: `02-tokens.md`.
83
+
82
84
  **Mode-specific rules**
83
85
  - One hero maximum, at the top. A second full-viewport section is a second hero.
84
86
  - Every page states its subject above the fold in text, not only in an image.
@@ -94,24 +96,26 @@ At a seam between modes:
94
96
  **Reader:** returning, task-focused, moderate familiarity, mixed device.
95
97
  **Tiebreaker:** predictability over novelty. A boring pattern the user already knows beats a better one they must learn.
96
98
 
97
- | Property | Value |
99
+ | Property | This mode selects |
98
100
  | --- | --- |
99
- | Body size | 16px |
100
- | Type ratio | 1.200 (minor third) |
101
- | Heading ramp | 20 / 24 / 32 |
102
- | Measure | 60ch |
103
- | Base unit | 4px |
104
- | Section rhythm | 48px desktop · 32px mobile |
105
- | Card padding | 24px |
106
- | Control height | 40px |
107
- | Table row height | 48px |
108
- | Radius | brand default, one step tighter than `editorial` |
109
- | Elevation | overlays only (modal, popover, dropdown) |
110
- | Motion | 150ms; state change only, no entrance animation |
111
- | Colour usage | Accent for primary action. Full semantic set. Status as subtle fill plus text. |
101
+ | UI text | `--text-body` |
102
+ | Long-form text | `--text-prose` · `--leading-prose` |
103
+ | Heading ramp | `--text-h3` → `--text-h2` → `--text-h1` |
104
+ | Type ratio | 1.200 Minor Third — the mid value of the three |
105
+ | Measure | `--measure-prose` |
106
+ | Section rhythm | `--spacing-section` |
107
+ | Card padding | `--spacing-card` |
108
+ | Control height | `--size-control` — the mid value of the three |
109
+ | Table row height | `--size-row` |
110
+ | Radius | `--radius-control` (sm) · `--radius-surface` (md) |
111
+ | Elevation | `--shadow-none`; `--shadow-raised`/`--shadow-overlay` for overlays only (modal, popover, dropdown) |
112
+ | Motion | `--duration-base`; state change only, no entrance animation |
113
+ | Colour usage | `--color-brand` for primary action. Full semantic set. Status as subtle fill plus text. |
112
114
  | Imagery | Sparse. Illustration permitted in empty states only. |
113
115
  | Keyboard | Shortcuts for frequent actions; documented in-app |
114
116
 
117
+ Resolved values: `02-tokens.md`.
118
+
115
119
  **Mode-specific rules**
116
120
  - One primary action per view. Everything else is secondary or tertiary.
117
121
  - Destructive actions are never adjacent to their most common neighbour, and always confirm.
@@ -127,25 +131,27 @@ At a seam between modes:
127
131
  **Reader:** expert, in the tool for hours, keyboard-driven, high repetition.
128
132
  **Tiebreaker:** speed of repeated use over clarity of first use. Discoverability is worth sacrificing for throughput here, and nowhere else.
129
133
 
130
- | Property | Value |
134
+ | Property | This mode selects |
131
135
  | --- | --- |
132
- | Body size | 14px |
133
- | Type ratio | 1.150 |
134
- | Heading ramp | 16 / 18 / 22 |
135
- | Measure | 72ch (prose only; data columns are not prose) |
136
- | Base unit | 4px |
137
- | Section rhythm | 24px |
138
- | Card padding | 12px |
139
- | Control height | 32px (28px in compact rows) |
140
- | Table row height | 36px |
141
- | Radius | 4px maximum |
142
- | Elevation | overlays only, minimal |
143
- | Motion | 100ms; **no entrance animation of any kind** |
136
+ | UI text | `--text-body` |
137
+ | Long-form text | `--text-prose` · `--leading-prose` |
138
+ | Heading ramp | `--text-h3` → `--text-h2` → `--text-h1` |
139
+ | Type ratio | 1.125 Major Second — the smallest of the three |
140
+ | Measure | `--measure-prose` (prose only; data columns are not prose) |
141
+ | Section rhythm | `--spacing-section` |
142
+ | Card padding | `--spacing-card` |
143
+ | Control height | `--size-control` — the shortest of the three (`--size-control-sm` in compact rows) |
144
+ | Table row height | `--size-row` (`--size-row-compact` in compact rows) |
145
+ | Radius | `--radius-control` (sm) · `--radius-surface` (sm) — both sm in this mode |
146
+ | Elevation | `--shadow-none`; overlays only, minimal |
147
+ | Motion | `--duration-base`; **no entrance animation of any kind** |
144
148
  | Colour usage | Neutral-dominant. Colour is *exclusively* semantic. No decorative accent. |
145
149
  | Imagery | None. Icons only. |
146
150
  | Keyboard | Full keyboard operation mandatory. Shortcut reference required. |
147
151
  | Numerals | Tabular figures mandatory on all numeric columns |
148
152
 
153
+ Resolved values: `02-tokens.md`.
154
+
149
155
  **Mode-specific rules**
150
156
  - Density is the feature. More rows visible beats more comfortable rows.
151
157
  - Every table supports: sort, filter, column visibility, and a stable row identity across refreshes.
@@ -163,11 +169,12 @@ Useful when a decision straddles two modes.
163
169
 
164
170
  | | `editorial` | `product` | `operator` |
165
171
  | --- | --- | --- | --- |
166
- | Body | 18px | 16px | 14px |
167
- | Control height | 48px | 40px | 32px |
168
- | Section rhythm | 96px | 48px | 24px |
169
- | Card padding | 32px | 24px | 12px |
170
- | Motion | 200–300ms | 150ms | 100ms |
172
+ | Type ratio | 1.250 | 1.200 | 1.125 |
173
+ | Body | `--text-body` | same as editorial | smaller |
174
+ | Control height | tallest | mid | shortest |
175
+ | Section rhythm | `--spacing-xxl` | `--spacing-xl` | `--spacing-m` |
176
+ | Card padding | `--spacing-m` | `--spacing-m` | `--spacing-s` |
177
+ | Motion | slowest | mid | fastest |
171
178
  | Entrance animation | once, first viewport | none | none |
172
179
  | Decorative colour | accent only | primary action | none |
173
180
  | Imagery | central | empty states | none |
@@ -182,7 +189,7 @@ Attempting to vary these by mode is a category error:
182
189
  - **Accessibility floors.** Contrast, focus indication, target size, semantic markup. Identical in all three. `operator` being dense does not license a 24px tap target or a 3:1 body contrast.
183
190
  - **Brand identity.** Palette, typeface, logo, voice.
184
191
  - **State completeness.** Every mode renders loading, empty, error and disabled.
185
- - **The anti-pattern file.** All 48 rules apply everywhere.
192
+ - **The anti-pattern file.** All 87 rules in it apply everywhere.
186
193
 
187
194
  ---
188
195
 
@@ -192,17 +199,17 @@ Per project, one file supplying:
192
199
 
193
200
  - **Palette** — neutral ramp (12 steps, warm/cool/true declared), one accent ramp, semantic set (danger, warning, success, info) tuned to the accent's temperature.
194
201
  - **Typeface** — display and text families, and whether they differ. Numeric font-feature settings.
195
- - **Radius personality** — the base radius that mode scales from. This carries more brand character than colour does.
202
+ - **Radius personality** — the brand-scale radius options (`sm`, `md`, `lg`, `full`) that each mode selects from, not a fixed derivation. This carries more brand character than colour does.
196
203
  - **Elevation personality** — border-led or shadow-led. Pick one; do not mix within a project.
197
204
  - **Voice** — sentence case or title case, contraction policy, error-message tone.
198
205
 
199
- Default when no brand is supplied: warm neutral ramp anchored on `#fafaf7`, no accent, 6px base radius, border-led elevation. Greyscale output plus a stated question beats an invented purple (`A-01`).
206
+ Default when no brand is supplied: warm neutral ramp anchored on `#fafaf7`, no accent, 8px base radius (`--radius-sm`), border-led elevation. Greyscale output plus a stated question beats an invented purple (`A-01`).
200
207
 
201
208
  ---
202
209
 
203
210
  ## Notes for the author (not for the agent)
204
211
 
205
- **Decided, not derived.** These numbers are internally consistent and defensible, but several are judgement calls that should be tuned once you have run real work through them: the 18px editorial body, the 36px operator row, the three section-rhythm values, and the motion durations. Change them in this file, never at the call site.
212
+ **Decided, not derived.** These numbers are internally consistent and defensible, but several are judgement calls that should be tuned once you have run real work through them: the operator row height, the three section-rhythm values, and the motion durations. Change them in this file, never at the call site.
206
213
 
207
214
  **Where your taste is recorded here:**
208
215
  - The zero-JS default in `editorial` — a stronger position than most systems take, and consistent with your writing on JS-dependent forms.
@@ -19,9 +19,13 @@ tokens/
19
19
 
20
20
  A surface loads **exactly one brand file and exactly one mode file**.
21
21
 
22
+ **Tokens live at `.jig/tokens/`.** That is the only location, in every scope and
23
+ every project — `jig install` puts them there, `jig update` refreshes them there,
24
+ and nothing relocates them. Import from that path and it stays correct.
25
+
22
26
  ```css
23
- @import "tokens/brand.acme.css";
24
- @import "tokens/mode.operator.css";
27
+ @import ".jig/tokens/brand.acme.css";
28
+ @import ".jig/tokens/mode.operator.css";
25
29
  ```
26
30
 
27
31
  Three separate mode files rather than one file with variants. The trade: a surface cannot switch modes at runtime, and shared values are duplicated across three files. In exchange each surface ships only the tokens it uses, the files are independently readable, and there is no cascade to reason about. For a system where mode is a routing decision rather than a user preference, that is the right trade.
@@ -50,7 +54,7 @@ Limited options, chosen once. The point is not the specific values — it is tha
50
54
  | --- | --- | --- | --- | --- | --- |
51
55
  | 8 | 16 | 24 | 32 | 48 | 80 |
52
56
 
53
- Modes **select** from these; they never define their own values. `--spacing-card` is `L` in `editorial`, `M` in `product`, `S` in `operator` — same option set, different selection. This is why there are no arbitrary numbers left in the mode files.
57
+ Modes **select** from these; they never define their own values. `--spacing-card` is `M` in `editorial`, `M` in `product`, `S` in `operator` — same option set, different selection. This is why there are no arbitrary numbers left in the mode files.
54
58
 
55
59
  **Type — the scale ratio varies by mode**, because scale size should track interface complexity. A large ratio gives dramatic steps that suit content-led pages; a small ratio gives fine gradations that suit dense tools needing many levels in little space.
56
60
 
@@ -60,6 +64,8 @@ Modes **select** from these; they never define their own values. `--spacing-card
60
64
  | `product` | 1.200 Minor Third | | 14 | 16 | 18 | 20 | 24 | 32 |
61
65
  | `operator` | 1.125 Major Second | | 12 | 14 | 16 | 16 | 18 | 22 |
62
66
 
67
+ `editorial` omits the rung the ratio would put between H2 and H1 (a step near 40); the ratio names the ladder, not every adjacent step — its H2→H1 jump (32→48) is 1.5, not 1.25.
68
+
63
69
  **`--text-body` and `--text-prose` are different roles, not two sizes of the same thing.** Body is UI text — labels, controls, table cells, short strings read in glances. Prose is sustained reading, and never drops below 18px on a page anyone is expected to actually read (`B-75`).
64
70
 
65
71
  Line heights are unitless and floor at **1.5** for body and prose, easing down as size rises. Raise it further when lines are long, when the typeface is heavy or dark, or when it simply looks large for its nominal size. Between 1.5 and 2 is the comfortable band for prose.
@@ -72,7 +78,9 @@ Line heights are unitless and floor at **1.5** for body and prose, easing down a
72
78
 
73
79
  **Typeface.** One sans serif by default: most legible small, neutral across brands, least likely to be the wrong choice. When picking one — prefer a popular face with many weights, a tall x-height and generous default spacing, with OpenType features and the language coverage the product needs. When in doubt, the platform system font is tried, tested and free to load. A second face is permitted for headings only (`B-76`).
74
80
 
75
- **Radius — three options**, by element size: 8px small (buttons, inputs, badges), 16px medium (cards, panels), 32px large (hero surfaces).
81
+ **Radius — four options**, by element size: 8px small (buttons, inputs), 16px medium (cards, panels), 32px large (hero media and full-bleed surfaces), and a full/pill radius for pills, badges, avatars and chips (`--radius-full`).
82
+
83
+ `--radius-control` selects `sm` in all three modes. `--radius-surface` selects `md` in `editorial` and `product`, but `sm` in `operator` — the selection is per-mode, not a fixed derivation. `--radius-lg` and `--radius-full` are brand-scale options; no mode currently selects either.
76
84
 
77
85
  **Shadow — two options** with stated meanings: `raised` sits above the page, `overlay` floats over it. `A-08` still prefers a stroke; these exist for when depth is the point.
78
86
 
@@ -186,8 +194,8 @@ In dark, elevated surfaces get **lighter**, not shadowed. Border-led elevation s
186
194
 
187
195
  **Plain CSS, any framework**
188
196
  ```css
189
- @import "tokens/brand.default.css";
190
- @import "tokens/mode.product.css";
197
+ @import ".jig/tokens/brand.default.css";
198
+ @import ".jig/tokens/mode.product.css";
191
199
 
192
200
  .card {
193
201
  background: var(--color-surface);
@@ -201,8 +209,8 @@ In dark, elevated surfaces get **lighter**, not shadowed. Border-led elevation s
201
209
  ```css
202
210
  @import "tailwindcss";
203
211
  @theme {
204
- @import "tokens/brand.default.css";
205
- @import "tokens/mode.product.css";
212
+ @import ".jig/tokens/brand.default.css";
213
+ @import ".jig/tokens/mode.product.css";
206
214
  }
207
215
  ```
208
216
  Yields `bg-surface`, `rounded-surface`, `p-card`, `text-body` as utilities.
@@ -38,7 +38,7 @@ Read this before building any pattern that reports something to the user. The mo
38
38
 
39
39
  | Weight | Treatment | Use |
40
40
  | --- | --- | --- |
41
- | **Primary** | Solid `--color-brand` fill, `--color-brand-on` text, `--radius-control` | The one action the view exists for |
41
+ | **Primary** | Solid `--color-brand` fill, `--color-on-brand` text, `--radius-control` | The one action the view exists for |
42
42
  | **Secondary** | Transparent fill, `--color-brand` border **and** text, same radius and height | The alternative, or several actions of equal weight |
43
43
  | **Tertiary** | Transparent, no border, `--color-brand` **underlined** text | Least important actions, repeated actions, destructive actions |
44
44
 
@@ -53,7 +53,7 @@ Four numbers, and most button designs in the wild fail at least one:
53
53
  | Button **shape** — fill or border — against its background | **3:1** |
54
54
  | Button **text** against the button | **4.5:1** |
55
55
  | Two buttons sharing a style, distinguished only by contrast | **3:1 between them** |
56
- | Hit area | **48×48px** |
56
+ | Hit area | `--size-touch-target` — **48×48px** floor |
57
57
 
58
58
  A secondary button's fill or border is **not decorative**. It is the only thing identifying the element as a button, so it carries the 3:1 non-text requirement (`02-tokens.md`). Strip it and you have coloured text.
59
59
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  Two kinds of principle, doing opposite work.
7
7
 
8
- **Part 1 — Frames (five).** Generative. Use these to find the rule that does not exist yet. `00`–`03` cover known failures; these are the method for recognising a new one. Adapted from the foundations in *Practical UI* (Adham Dannaway).
8
+ **Part 1 — Frames (five).** Generative. Use these to find the rule that does not exist yet. `00`–`03` cover known failures; these are the method for recognising a new one.
9
9
 
10
10
  **Part 2 — Tiebreakers.** Adjudicative. Use only when two existing rules point in different directions.
11
11
 
@@ -21,7 +21,10 @@
21
21
  --brand-h: 264;
22
22
  --brand-s: 0%;
23
23
  --brand-l: 15%;
24
- --brand-base: hsl(var(--brand-h) var(--brand-s) var(--brand-l));
24
+
25
+ /* The solid brand colour. Interactive elements reference this (I-56).
26
+ The variations below are alpha steps off the same hue. */
27
+ --color-brand: hsl(var(--brand-h) var(--brand-s) var(--brand-l));
25
28
 
26
29
  /* ================= ELEVATION BACKGROUNDS (solid) =================
27
30
  Light comes from above: higher elevation is lighter.
@@ -50,32 +53,42 @@
50
53
 
51
54
  /* ================= SYSTEM COLOURS =================
52
55
  Three, with familiar traffic-light meanings. Four variations each.
53
- Never the only signal — pair with an icon and text (C-20). */
54
- --error-base: hsl(0 71% 44%);
55
- --warning-base: hsl(42 82% 36%);
56
- --success-base: hsl(162 95% 26%);
57
- /* info is NOT in the book's set of three. Kept as an optional fourth. */
58
- --info-base: hsl(220 70% 42%);
59
-
60
- --color-text-error: hsl(0 71% 44% / 100%);
61
- --color-stroke-error-strong: hsl(0 71% 44% / 80%);
62
- --color-stroke-error-weak: hsl(0 71% 44% / 20%);
63
- --color-fill-error: hsl(0 71% 44% / 5%);
64
-
65
- --color-text-warning: hsl(42 82% 36% / 100%);
66
- --color-stroke-warning-strong: hsl(42 82% 36% / 80%);
67
- --color-stroke-warning-weak: hsl(42 82% 36% / 20%);
68
- --color-fill-warning: hsl(42 82% 36% / 5%);
69
-
70
- --color-text-success: hsl(162 95% 26% / 100%);
71
- --color-stroke-success-strong: hsl(162 95% 26% / 80%);
72
- --color-stroke-success-weak: hsl(162 95% 26% / 20%);
73
- --color-fill-success: hsl(162 95% 26% / 5%);
74
-
75
- --color-text-info: hsl(220 70% 42% / 100%);
76
- --color-stroke-info-strong: hsl(220 70% 42% / 80%);
77
- --color-stroke-info-weak: hsl(220 70% 42% / 20%);
78
- --color-fill-info: hsl(220 70% 42% / 5%);
56
+ Never the only signal — pair with an icon and text (C-20).
57
+
58
+ Requirements when you replace one:
59
+ - text (100%) >= 4.5:1 against --color-bg-base AND --color-bg-raised
60
+ - stroke-strong (80%) >= 3:1 against the same two, composited
61
+ - stroke-weak and fill are decorative and have no floor
62
+ These are not style preferences. A system colour that fails them is
63
+ unreadable to the people the colour exists to warn.
64
+ Each colour is one h/s/l triple (plus a fill alpha, which differs between
65
+ light and dark) referenced by all four variations below, so changing the
66
+ colour means changing it in one place instead of 4–5. */
67
+ --error-h: 0; --error-s: 71%; --error-l: 44%; --error-fill-a: 5%;
68
+ --warning-h: 42; --warning-s: 82%; --warning-l: 29%; --warning-fill-a: 5%;
69
+ --success-h: 162; --success-s: 95%; --success-l: 23%; --success-fill-a: 5%;
70
+ /* info is NOT in the reference's set of three. Kept as an optional fourth. */
71
+ --info-h: 220; --info-s: 70%; --info-l: 42%; --info-fill-a: 5%;
72
+
73
+ --color-text-error: hsl(var(--error-h) var(--error-s) var(--error-l) / 100%);
74
+ --color-stroke-error-strong: hsl(var(--error-h) var(--error-s) var(--error-l) / 80%);
75
+ --color-stroke-error-weak: hsl(var(--error-h) var(--error-s) var(--error-l) / 20%);
76
+ --color-fill-error: hsl(var(--error-h) var(--error-s) var(--error-l) / var(--error-fill-a));
77
+
78
+ --color-text-warning: hsl(var(--warning-h) var(--warning-s) var(--warning-l) / 100%);
79
+ --color-stroke-warning-strong: hsl(var(--warning-h) var(--warning-s) var(--warning-l) / 80%);
80
+ --color-stroke-warning-weak: hsl(var(--warning-h) var(--warning-s) var(--warning-l) / 20%);
81
+ --color-fill-warning: hsl(var(--warning-h) var(--warning-s) var(--warning-l) / var(--warning-fill-a));
82
+
83
+ --color-text-success: hsl(var(--success-h) var(--success-s) var(--success-l) / 100%);
84
+ --color-stroke-success-strong: hsl(var(--success-h) var(--success-s) var(--success-l) / 80%);
85
+ --color-stroke-success-weak: hsl(var(--success-h) var(--success-s) var(--success-l) / 20%);
86
+ --color-fill-success: hsl(var(--success-h) var(--success-s) var(--success-l) / var(--success-fill-a));
87
+
88
+ --color-text-info: hsl(var(--info-h) var(--info-s) var(--info-l) / 100%);
89
+ --color-stroke-info-strong: hsl(var(--info-h) var(--info-s) var(--info-l) / 80%);
90
+ --color-stroke-info-weak: hsl(var(--info-h) var(--info-s) var(--info-l) / 20%);
91
+ --color-fill-info: hsl(var(--info-h) var(--info-s) var(--info-l) / var(--info-fill-a));
79
92
 
80
93
  /* ================= STATE LAYERS =================
81
94
  Transparent overlays, so hover and press need no new colours and work on
@@ -92,8 +105,8 @@
92
105
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", monospace;
93
106
  --font-numeric-features: "tnum" 1, "lnum" 1;
94
107
 
95
- /* ================= RADIUS — three, by element size ================= */
96
- --radius-sm: 8px; /* buttons, inputs, badges, checkboxes */
108
+ /* ================= RADIUS — four, by element size ================= */
109
+ --radius-sm: 8px; /* buttons, inputs, checkboxes */
97
110
  --radius-md: 16px; /* cards, panels */
98
111
  --radius-lg: 32px; /* large surfaces, hero media */
99
112
  --radius-full: 9999px;
@@ -125,26 +138,13 @@
125
138
  --brand-l: 88%; /* lighten and desaturate the brand for dark surfaces */
126
139
  --color-on-brand: hsl(var(--brand-h) 6% 10%);
127
140
 
128
- /* System colours lighten and desaturate in dark mode. */
129
- --color-text-error: hsl(0 60% 72% / 100%);
130
- --color-stroke-error-strong: hsl(0 60% 72% / 80%);
131
- --color-stroke-error-weak: hsl(0 60% 72% / 20%);
132
- --color-fill-error: hsl(0 60% 72% / 8%);
133
-
134
- --color-text-warning: hsl(42 70% 70% / 100%);
135
- --color-stroke-warning-strong: hsl(42 70% 70% / 80%);
136
- --color-stroke-warning-weak: hsl(42 70% 70% / 20%);
137
- --color-fill-warning: hsl(42 70% 70% / 8%);
138
-
139
- --color-text-success: hsl(162 45% 66% / 100%);
140
- --color-stroke-success-strong: hsl(162 45% 66% / 80%);
141
- --color-stroke-success-weak: hsl(162 45% 66% / 20%);
142
- --color-fill-success: hsl(162 45% 66% / 8%);
143
-
144
- --color-text-info: hsl(220 55% 74% / 100%);
145
- --color-stroke-info-strong: hsl(220 55% 74% / 80%);
146
- --color-stroke-info-weak: hsl(220 55% 74% / 20%);
147
- --color-fill-info: hsl(220 55% 74% / 8%);
141
+ /* System colours lighten and desaturate in dark mode. Only the h/s/l and
142
+ fill-alpha vars need overriding here — the four hsl() declarations
143
+ above pick the new values up automatically. */
144
+ --error-h: 0; --error-s: 60%; --error-l: 72%; --error-fill-a: 8%;
145
+ --warning-h: 42; --warning-s: 70%; --warning-l: 70%; --warning-fill-a: 8%;
146
+ --success-h: 162; --success-s: 45%; --success-l: 66%; --success-fill-a: 8%;
147
+ --info-h: 220; --info-s: 55%; --info-l: 74%; --info-fill-a: 8%;
148
148
 
149
149
  --shadow-raised: none;
150
150
  --shadow-overlay: 0 8px 24px -8px rgb(0 0 0 / 60%);