jig-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +7 -0
- package/README.md +115 -0
- package/dist/index.js +544 -0
- package/package.json +61 -0
- package/rules/00-anti-patterns.md +501 -0
- package/rules/01-modes.md +213 -0
- package/rules/02-tokens.md +210 -0
- package/rules/03-patterns.md +459 -0
- package/rules/04-principles.md +154 -0
- package/rules/05-copy.md +153 -0
- package/rules.index.json +637 -0
- package/templates/SKILL.md.tmpl +53 -0
- package/templates/command-metadata.json +27 -0
- package/tokens/brand.default.css +152 -0
- package/tokens/mode.editorial.css +72 -0
- package/tokens/mode.operator.css +74 -0
- package/tokens/mode.product.css +69 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# 01 · Modes
|
|
2
|
+
|
|
3
|
+
**Status:** draft v0.1
|
|
4
|
+
**Depends on:** `00-anti-patterns.md` (universal, applies in every mode)
|
|
5
|
+
**Feeds:** `02-tokens.md`
|
|
6
|
+
|
|
7
|
+
## The two switches
|
|
8
|
+
|
|
9
|
+
This system has two orthogonal axes. Keep them separate.
|
|
10
|
+
|
|
11
|
+
| | **Mode** | **Brand** |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| Answers | How is this surface used? | Whose product is this? |
|
|
14
|
+
| Values | `editorial` · `product` · `operator` | Per-client identity |
|
|
15
|
+
| Varies | Between surfaces *within* one project | Between projects, constant within one |
|
|
16
|
+
| Controls | Density, rhythm, type scale, motion budget, colour *usage* | Palette, typeface, radius personality, elevation personality |
|
|
17
|
+
| Defined in | This file | `03-brand.md` (per project) |
|
|
18
|
+
|
|
19
|
+
A token is resolved as **brand × mode**. Brand says the accent is `oklch(0.55 0.13 25)`; mode says whether it appears on large surfaces or only on the primary action.
|
|
20
|
+
|
|
21
|
+
Collapsing these into one switch produces `theme-marketing-dark-compact` and a system nobody uses. Do not do it.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Choosing a mode
|
|
26
|
+
|
|
27
|
+
1. If the project config declares a mode for this route or surface, use it.
|
|
28
|
+
2. If not, infer from the signals below and **state the inference in one line** before building.
|
|
29
|
+
3. If the signals conflict, ask. Do not guess — the density decision is expensive to reverse.
|
|
30
|
+
|
|
31
|
+
| Signal | → mode |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| Visitor arrives from search or a link, may never return | `editorial` |
|
|
34
|
+
| User has an account and a task | `product` |
|
|
35
|
+
| User is doing this job all day, knows the system, uses the keyboard | `operator` |
|
|
36
|
+
| Content is primarily prose or media | `editorial` |
|
|
37
|
+
| Content is primarily records, rows, filters | `operator` |
|
|
38
|
+
| Success is measured in comprehension or trust | `editorial` |
|
|
39
|
+
| Success is measured in task completion | `product` |
|
|
40
|
+
| Success is measured in throughput or error rate | `operator` |
|
|
41
|
+
|
|
42
|
+
### The one-sentence distinction
|
|
43
|
+
|
|
44
|
+
**`editorial` optimises for first use. `operator` optimises for the thousandth. `product` sits between and must serve both.**
|
|
45
|
+
|
|
46
|
+
When a mode decision is genuinely ambiguous, resolve it with that sentence rather than by taste.
|
|
47
|
+
|
|
48
|
+
### Mixed projects
|
|
49
|
+
|
|
50
|
+
Mode is a property of a **surface**, not a project. A single site routinely spans all three: marketing pages (`editorial`), an authenticated app (`product`), an admin area (`operator`).
|
|
51
|
+
|
|
52
|
+
At a seam between modes:
|
|
53
|
+
- Brand stays constant. Same palette, typeface, logo, radius personality.
|
|
54
|
+
- Density, type scale and motion switch cleanly at the route boundary.
|
|
55
|
+
- Never blend two modes inside one view. A dense table inside a spacious marketing page is a mode error; give the table its own surface or redesign it as editorial content.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## M-01 · `editorial`
|
|
60
|
+
|
|
61
|
+
**For:** marketing sites, blogs, documentation, brochureware, landing pages.
|
|
62
|
+
**Reader:** first-time, on mobile data, scanning before committing attention.
|
|
63
|
+
**Tiebreaker:** legibility over density. When in doubt, larger and further apart.
|
|
64
|
+
|
|
65
|
+
| Property | Value |
|
|
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. |
|
|
79
|
+
| Imagery | Central. Real photography or commissioned illustration. |
|
|
80
|
+
| Keyboard | Standard tab order; no shortcuts expected |
|
|
81
|
+
|
|
82
|
+
**Mode-specific rules**
|
|
83
|
+
- One hero maximum, at the top. A second full-viewport section is a second hero.
|
|
84
|
+
- Every page states its subject above the fold in text, not only in an image.
|
|
85
|
+
- Prose blocks are measure-capped even when the container is wide.
|
|
86
|
+
- No horizontal scrolling regions on mobile. Reflow instead.
|
|
87
|
+
- Total JS budget for a content page: **0 KB** unless a specific feature requires it. Interactivity is opt-in per component and must be justified in a comment.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## M-02 · `product`
|
|
92
|
+
|
|
93
|
+
**For:** authenticated application UI, customer-facing dashboards, settings, onboarding.
|
|
94
|
+
**Reader:** returning, task-focused, moderate familiarity, mixed device.
|
|
95
|
+
**Tiebreaker:** predictability over novelty. A boring pattern the user already knows beats a better one they must learn.
|
|
96
|
+
|
|
97
|
+
| Property | Value |
|
|
98
|
+
| --- | --- |
|
|
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. |
|
|
112
|
+
| Imagery | Sparse. Illustration permitted in empty states only. |
|
|
113
|
+
| Keyboard | Shortcuts for frequent actions; documented in-app |
|
|
114
|
+
|
|
115
|
+
**Mode-specific rules**
|
|
116
|
+
- One primary action per view. Everything else is secondary or tertiary.
|
|
117
|
+
- Destructive actions are never adjacent to their most common neighbour, and always confirm.
|
|
118
|
+
- Every list view handles four states explicitly: loading, empty-never, empty-filtered, error.
|
|
119
|
+
- Forms save on explicit action, not on blur, unless the surface is a settings panel — and then say so.
|
|
120
|
+
- Navigation position is fixed across the app. Never move the primary nav between sections.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## M-03 · `operator`
|
|
125
|
+
|
|
126
|
+
**For:** internal tools, admin areas, back-office, data entry, monitoring.
|
|
127
|
+
**Reader:** expert, in the tool for hours, keyboard-driven, high repetition.
|
|
128
|
+
**Tiebreaker:** speed of repeated use over clarity of first use. Discoverability is worth sacrificing for throughput here, and nowhere else.
|
|
129
|
+
|
|
130
|
+
| Property | Value |
|
|
131
|
+
| --- | --- |
|
|
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** |
|
|
144
|
+
| Colour usage | Neutral-dominant. Colour is *exclusively* semantic. No decorative accent. |
|
|
145
|
+
| Imagery | None. Icons only. |
|
|
146
|
+
| Keyboard | Full keyboard operation mandatory. Shortcut reference required. |
|
|
147
|
+
| Numerals | Tabular figures mandatory on all numeric columns |
|
|
148
|
+
|
|
149
|
+
**Mode-specific rules**
|
|
150
|
+
- Density is the feature. More rows visible beats more comfortable rows.
|
|
151
|
+
- Every table supports: sort, filter, column visibility, and a stable row identity across refreshes.
|
|
152
|
+
- Bulk actions wherever a single action exists on more than ~20 rows.
|
|
153
|
+
- Destructive actions require typed confirmation, not a checkbox.
|
|
154
|
+
- Timestamps are absolute and precise, with relative time secondary. "3 hours ago" is unacceptable alone in an audit context.
|
|
155
|
+
- Never hide information behind hover in a tool used all day. Truncation must be resolvable by click or expand, not hover.
|
|
156
|
+
- Latency budget: any action a user repeats hourly responds in under 200ms or shows progress.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Comparison
|
|
161
|
+
|
|
162
|
+
Useful when a decision straddles two modes.
|
|
163
|
+
|
|
164
|
+
| | `editorial` | `product` | `operator` |
|
|
165
|
+
| --- | --- | --- | --- |
|
|
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 |
|
|
171
|
+
| Entrance animation | once, first viewport | none | none |
|
|
172
|
+
| Decorative colour | accent only | primary action | none |
|
|
173
|
+
| Imagery | central | empty states | none |
|
|
174
|
+
| Optimises for | first use | both | thousandth use |
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## What mode does *not* control
|
|
179
|
+
|
|
180
|
+
Attempting to vary these by mode is a category error:
|
|
181
|
+
|
|
182
|
+
- **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
|
+
- **Brand identity.** Palette, typeface, logo, voice.
|
|
184
|
+
- **State completeness.** Every mode renders loading, empty, error and disabled.
|
|
185
|
+
- **The anti-pattern file.** All 48 rules apply everywhere.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## `03-brand.md` — stub
|
|
190
|
+
|
|
191
|
+
Per project, one file supplying:
|
|
192
|
+
|
|
193
|
+
- **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
|
+
- **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.
|
|
196
|
+
- **Elevation personality** — border-led or shadow-led. Pick one; do not mix within a project.
|
|
197
|
+
- **Voice** — sentence case or title case, contraction policy, error-message tone.
|
|
198
|
+
|
|
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`).
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Notes for the author (not for the agent)
|
|
204
|
+
|
|
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.
|
|
206
|
+
|
|
207
|
+
**Where your taste is recorded here:**
|
|
208
|
+
- The zero-JS default in `editorial` — a stronger position than most systems take, and consistent with your writing on JS-dependent forms.
|
|
209
|
+
- Absolute-first timestamps in `operator` — that is the procurement instinct: the record is evidence before it is a convenience.
|
|
210
|
+
- Typed confirmation for destructive operator actions, and no hover-hidden information in all-day tools.
|
|
211
|
+
- Border-led elevation as the unbranded default.
|
|
212
|
+
|
|
213
|
+
**Open question worth resolving before tokens.** `product` is currently defined as the midpoint of the other two, which is how it earns its place, but it is also the mode that most often needs to lean. A customer dashboard leans editorial; a billing admin screen leans operator. Consider whether `product` needs a documented `dense` variant, or whether such surfaces should simply be declared `operator`. My inclination is the latter — three modes you apply confidently beat five you deliberate over — but it is your call, and it affects how many token sets `02` has to emit.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# 02 · Tokens
|
|
2
|
+
|
|
3
|
+
**Status:** draft v0.1
|
|
4
|
+
**Depends on:** `01-modes.md`
|
|
5
|
+
**Canonical format:** CSS custom properties
|
|
6
|
+
**Consumed by:** any framework that renders to the web
|
|
7
|
+
|
|
8
|
+
## Architecture
|
|
9
|
+
|
|
10
|
+
Tokens resolve as **brand × mode**. Two layers, loaded in order.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
tokens/
|
|
14
|
+
brand.default.css ← one per project. Identity. Mode-independent.
|
|
15
|
+
mode.editorial.css ← density, rhythm, scale, motion
|
|
16
|
+
mode.product.css
|
|
17
|
+
mode.operator.css
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
A surface loads **exactly one brand file and exactly one mode file**.
|
|
21
|
+
|
|
22
|
+
```css
|
|
23
|
+
@import "tokens/brand.acme.css";
|
|
24
|
+
@import "tokens/mode.operator.css";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
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.
|
|
28
|
+
|
|
29
|
+
## Why CSS custom properties
|
|
30
|
+
|
|
31
|
+
They are the only token format every web framework consumes natively with no build step.
|
|
32
|
+
|
|
33
|
+
| Consumer | Usage |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| Plain CSS / any framework | `color: var(--color-text-strong)` |
|
|
36
|
+
| Tailwind v4 | Wrap in `@theme { }` — generates utilities automatically |
|
|
37
|
+
| CSS-in-JS (styled-components, emotion) | `color: var(--color-text-strong)` inside template literals |
|
|
38
|
+
| Vue / Svelte / Angular | Identical to plain CSS, scoped or global |
|
|
39
|
+
| React inline styles | `style={{ color: 'var(--color-text-strong)' }}` |
|
|
40
|
+
|
|
41
|
+
**Boundary:** this does not cover React Native or native platforms, which cannot read CSS. If a non-web target enters scope, author in DTCG JSON and generate these files with Style Dictionary or Terrazzo. The naming contract below is DTCG-compatible, so that migration is mechanical. Do not build the pipeline before you need it.
|
|
42
|
+
|
|
43
|
+
## Predefined option sets
|
|
44
|
+
|
|
45
|
+
Limited options, chosen once. The point is not the specific values — it is that there are few of them, so a decision is a selection rather than an invention.
|
|
46
|
+
|
|
47
|
+
**Spacing — six options, 8pt base.** Identical in every mode.
|
|
48
|
+
|
|
49
|
+
| XS | S | M | L | XL | XXL |
|
|
50
|
+
| --- | --- | --- | --- | --- | --- |
|
|
51
|
+
| 8 | 16 | 24 | 32 | 48 | 80 |
|
|
52
|
+
|
|
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.
|
|
54
|
+
|
|
55
|
+
**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
|
+
|
|
57
|
+
| Mode | Ratio | | Caption | Body (UI) | Prose | H3 | H2 | H1 |
|
|
58
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
59
|
+
| `editorial` | 1.250 Major Third | | 14 | 16 | 18 | 24 | 32 | 48 |
|
|
60
|
+
| `product` | 1.200 Minor Third | | 14 | 16 | 18 | 20 | 24 | 32 |
|
|
61
|
+
| `operator` | 1.125 Major Second | | 12 | 14 | 16 | 16 | 18 | 22 |
|
|
62
|
+
|
|
63
|
+
**`--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
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
**Measure: 40–80 characters.** Below 40 the eye returns too often; above 80 it loses the line. `--measure-prose` sits mid-range in every mode.
|
|
68
|
+
|
|
69
|
+
**Weights: two.** Regular (400) and bold (600). See `B-77`.
|
|
70
|
+
|
|
71
|
+
**Letter spacing** tightens as size grows — most text typefaces are spaced for small sizes and look loose when scaled up. `--tracking-h1` is the most negative; body is 0.
|
|
72
|
+
|
|
73
|
+
**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
|
+
|
|
75
|
+
**Radius — three options**, by element size: 8px small (buttons, inputs, badges), 16px medium (cards, panels), 32px large (hero surfaces).
|
|
76
|
+
|
|
77
|
+
**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
|
+
|
|
79
|
+
## Colour naming
|
|
80
|
+
|
|
81
|
+
Two layers, and only one of them is used in component code.
|
|
82
|
+
|
|
83
|
+
**Primitive** — named by appearance, numbered 0–1000 by contrast. `grey.light.700`, `green.dark.1000`. These exist to be referenced by semantics. **Never use a primitive directly in a component.**
|
|
84
|
+
|
|
85
|
+
**Semantic** — named by use, in the order `element.tone.emphasis.state`:
|
|
86
|
+
|
|
87
|
+
| element | tone | emphasis | state |
|
|
88
|
+
| --- | --- | --- | --- |
|
|
89
|
+
| text | neutral | strong | hover |
|
|
90
|
+
| stroke | brand | weak | press |
|
|
91
|
+
| icon | error | | focus |
|
|
92
|
+
| fill | warning | | disabled |
|
|
93
|
+
| background | success | | |
|
|
94
|
+
|
|
95
|
+
Words that are the default are omitted, which is why `--color-text-strong` needs no tone and `--color-fill` needs neither. Examples: `--color-text-error`, `--color-stroke-strong`, `--color-fill-success`, `--color-stroke-brand-weak`.
|
|
96
|
+
|
|
97
|
+
The payoff is mode switching: one semantic name maps to a different primitive in light and dark, so component code never mentions a mode.
|
|
98
|
+
|
|
99
|
+
Resist per-component tokens (`--button-bg`). They multiply fast and rarely earn it.
|
|
100
|
+
|
|
101
|
+
## Naming contract
|
|
102
|
+
|
|
103
|
+
Names align to Tailwind v4's theme namespaces. This is free for other frameworks — they are ordinary custom properties — and means the same file can be wrapped in `@theme` to generate utilities without any framework taking a dependency on Tailwind.
|
|
104
|
+
|
|
105
|
+
| Namespace | Holds | Layer |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| `--color-*` | All colour | brand |
|
|
108
|
+
| `--font-*` | Font families | brand |
|
|
109
|
+
| `--text-*` | Font sizes | mode |
|
|
110
|
+
| `--leading-*` | Line heights | mode |
|
|
111
|
+
| `--tracking-*` | Letter spacing | mode |
|
|
112
|
+
| `--spacing-*` | Spacing values | mode |
|
|
113
|
+
| `--radius-*` | Corner radii | brand scale, mode selection |
|
|
114
|
+
| `--shadow-*` | Elevation | brand |
|
|
115
|
+
| `--duration-*`, `--ease-*` | Motion | mode |
|
|
116
|
+
| `--size-*` | Control and row heights | mode |
|
|
117
|
+
| `--measure-*` | Line length caps | mode |
|
|
118
|
+
|
|
119
|
+
**Rules**
|
|
120
|
+
1. Semantic names only at the point of use. `--color-text-strong`, not `--color-neutral-900`, in component code. Primitives exist to build semantics, not to be consumed directly.
|
|
121
|
+
2. No component-scoped tokens. `--button-bg` belongs in the component, referencing `--color-brand`.
|
|
122
|
+
3. A value that cannot be expressed as a token is a missing token, not an exception (`H-47`).
|
|
123
|
+
|
|
124
|
+
## Colour architecture
|
|
125
|
+
|
|
126
|
+
**Foregrounds are transparent. Backgrounds are solid.**
|
|
127
|
+
|
|
128
|
+
Foreground colours (text, icons, strokes, fills) are opacities of black in light mode and white in dark. Background colours are three solid elevation levels.
|
|
129
|
+
|
|
130
|
+
This is not a stylistic choice. A solid foreground looks correct on one background and wrong on the next — a grey tag reads as prominent on white and recedes into a grey panel. Dark mode has three background levels, so a solid fill is wrong on at least two of them. A transparent foreground mixes with whatever is beneath it and keeps a consistent prominence everywhere.
|
|
131
|
+
|
|
132
|
+
It also removes tokens rather than adding them: hover and press become transparent layers reused across every component and both modes.
|
|
133
|
+
|
|
134
|
+
**Three elevation levels**, consistent across modes: `base` (page), `raised` (cards, panels), `overlay` (dialogs, dropdowns).
|
|
135
|
+
|
|
136
|
+
- **Light mode:** shadows carry elevation, plus lighter-on-darker — a white card on an off-white page reads as raised without a shadow at all.
|
|
137
|
+
- **Dark mode:** shadows are nearly invisible. **Depth comes from the background colour**, so `--shadow-raised` resolves to `none` and the raised background does the work.
|
|
138
|
+
|
|
139
|
+
| | Light | Dark |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| `text-strong` | black 90% | white 100% |
|
|
142
|
+
| `text-weak` | black 60% | white 78% |
|
|
143
|
+
| `stroke-strong` | black 45% | white 60% |
|
|
144
|
+
| `stroke-weak` | black 10% | white 12% |
|
|
145
|
+
| `fill` | black 4% | white 6% |
|
|
146
|
+
|
|
147
|
+
Brand and each system colour take the same four variations: **100%** text, **80%** stroke-strong, **20%** stroke-weak, **5%** fill.
|
|
148
|
+
|
|
149
|
+
**Neutral or monochromatic.** The default is neutral (pure black/white opacities), which works with any brand colour. For a monochromatic palette, tint the dark-mode backgrounds with the brand hue and, in light mode, replace the black opacities with a heavily saturated brand hue at low lightness. Change `--brand-h` and `--brand-s`; nothing else moves.
|
|
150
|
+
|
|
151
|
+
## Contrast contract
|
|
152
|
+
|
|
153
|
+
**Floor: WCAG 2.1 AA.** Two thresholds, and the boundary between them is a common mistake.
|
|
154
|
+
|
|
155
|
+
| | Threshold |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| Small text — 18px or less | **4.5:1** |
|
|
158
|
+
| Large text — 24px+ regular, or 18px+ bold | **3:1** |
|
|
159
|
+
| UI elements — input, checkbox and radio borders; meaningful icons | **3:1** |
|
|
160
|
+
| Decorative elements that convey no meaning | none |
|
|
161
|
+
|
|
162
|
+
**Test against `fill`, not against the background.** Text and controls can sit on a `fill` surface (a tag, a badge, a highlighted row), which is the lowest-contrast case. A colour that passes on the page background can fail inside a tag. In dark mode, test against `bg-overlay` — the brightest level, and therefore the worst case for a light-on-dark foreground.
|
|
163
|
+
|
|
164
|
+
**The brand colour needs 4.5:1** against both `bg-raised` and `fill`, because it is used for link and button text.
|
|
165
|
+
|
|
166
|
+
**When the brand colour cannot reach 4.5:1** — a yellow or very light brand, or a dark brand on a dark surface:
|
|
167
|
+
1. Darken or lighten it slightly, if brand recognition survives.
|
|
168
|
+
2. Use `--color-text-strong` for interactive elements instead, and keep the brand colour decorative.
|
|
169
|
+
3. Add a border to buttons so they clear 3:1.
|
|
170
|
+
|
|
171
|
+
### APCA
|
|
172
|
+
|
|
173
|
+
WCAG 2's algorithm has known failures — it will pass black text on orange and fail white text on the same orange, when the white is plainly more readable, and it works poorly on dark interfaces. APCA (WCAG 3 draft) measures perceptually and scores by size and weight rather than a flat ratio.
|
|
174
|
+
|
|
175
|
+
Guidance: **for commercial work, comply with WCAG 2.1 AA**, because that is what is legally referenced. Check APCA as well, particularly on dark surfaces. Aim to pass both.
|
|
176
|
+
|
|
177
|
+
APCA reference values: **90** preferred for body text · **75** minimum body at 18px+ · **60** other text · **45** large text and UI elements · **30** absolute floor for placeholder and disabled text · **15** non-text.
|
|
178
|
+
|
|
179
|
+
## Dark mode
|
|
180
|
+
|
|
181
|
+
Not an inversion (`C-21`). Each brand file supplies a dark block under `@media (prefers-color-scheme: dark)` and `[data-theme="dark"]`, remapping semantics only. Mode files are theme-independent — density does not change with colour scheme.
|
|
182
|
+
|
|
183
|
+
In dark, elevated surfaces get **lighter**, not shadowed. Border-led elevation survives the switch; shadow-led does not, which is one reason border-led is the unbranded default.
|
|
184
|
+
|
|
185
|
+
## Consuming
|
|
186
|
+
|
|
187
|
+
**Plain CSS, any framework**
|
|
188
|
+
```css
|
|
189
|
+
@import "tokens/brand.default.css";
|
|
190
|
+
@import "tokens/mode.product.css";
|
|
191
|
+
|
|
192
|
+
.card {
|
|
193
|
+
background: var(--color-surface);
|
|
194
|
+
border: 1px solid var(--color-stroke-weak);
|
|
195
|
+
border-radius: var(--radius-surface);
|
|
196
|
+
padding: var(--spacing-card);
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Tailwind v4** — wrap the same files, nothing else changes
|
|
201
|
+
```css
|
|
202
|
+
@import "tailwindcss";
|
|
203
|
+
@theme {
|
|
204
|
+
@import "tokens/brand.default.css";
|
|
205
|
+
@import "tokens/mode.product.css";
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
Yields `bg-surface`, `rounded-surface`, `p-card`, `text-body` as utilities.
|
|
209
|
+
|
|
210
|
+
**Multiple modes in one app** — scope by route, not by class. Each surface loads its own mode file at the layout or entry level. Do not attempt to nest two modes in one document (`01-modes.md`, seam rules).
|