trykeyline 0.0.0-stage → 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/.claude-plugin/marketplace.json +18 -0
- package/LICENSE +37 -0
- package/README.md +117 -2
- package/bin/keyline.mjs +280 -0
- package/package.json +38 -4
- package/plugins/keyline/.claude-plugin/plugin.json +14 -0
- package/plugins/keyline/skills/keyline/SKILL.md +57 -0
- package/plugins/keyline/skills/keyline/commands/audit.md +25 -0
- package/plugins/keyline/skills/keyline/commands/color.md +7 -0
- package/plugins/keyline/skills/keyline/commands/critique.md +43 -0
- package/plugins/keyline/skills/keyline/commands/depth.md +7 -0
- package/plugins/keyline/skills/keyline/commands/init.md +15 -0
- package/plugins/keyline/skills/keyline/commands/layout.md +7 -0
- package/plugins/keyline/skills/keyline/commands/motion.md +7 -0
- package/plugins/keyline/skills/keyline/commands/patterns.md +7 -0
- package/plugins/keyline/skills/keyline/commands/polish.md +19 -0
- package/plugins/keyline/skills/keyline/commands/refine.md +29 -0
- package/plugins/keyline/skills/keyline/commands/sync.md +17 -0
- package/plugins/keyline/skills/keyline/commands/type.md +7 -0
- package/plugins/keyline/skills/keyline/domains/color/deriving-a-ramp.md +144 -0
- package/plugins/keyline/skills/keyline/domains/color/guide.md +243 -0
- package/plugins/keyline/skills/keyline/domains/color/status-and-exceptions.md +174 -0
- package/plugins/keyline/skills/keyline/domains/color/theming-mechanics.md +121 -0
- package/plugins/keyline/skills/keyline/domains/depth/elevation-recipes.md +191 -0
- package/plugins/keyline/skills/keyline/domains/depth/fallbacks-and-performance.md +132 -0
- package/plugins/keyline/skills/keyline/domains/depth/guide.md +255 -0
- package/plugins/keyline/skills/keyline/domains/depth/translucent-materials.md +159 -0
- package/plugins/keyline/skills/keyline/domains/layout/forks-gating-and-disclosures.md +201 -0
- package/plugins/keyline/skills/keyline/domains/layout/guide.md +284 -0
- package/plugins/keyline/skills/keyline/domains/layout/measure-and-rhythm.md +193 -0
- package/plugins/keyline/skills/keyline/domains/layout/radius-lines-and-layers.md +122 -0
- package/plugins/keyline/skills/keyline/domains/motion/guards.md +162 -0
- package/plugins/keyline/skills/keyline/domains/motion/guide.md +234 -0
- package/plugins/keyline/skills/keyline/domains/motion/ladder-and-curves.md +139 -0
- package/plugins/keyline/skills/keyline/domains/motion/patterns.md +196 -0
- package/plugins/keyline/skills/keyline/domains/motion/performance.md +121 -0
- package/plugins/keyline/skills/keyline/domains/patterns/actions-and-forms.md +204 -0
- package/plugins/keyline/skills/keyline/domains/patterns/agents-and-live-states.md +111 -0
- package/plugins/keyline/skills/keyline/domains/patterns/collections-and-dashboards.md +131 -0
- package/plugins/keyline/skills/keyline/domains/patterns/copy.md +123 -0
- package/plugins/keyline/skills/keyline/domains/patterns/guide.md +216 -0
- package/plugins/keyline/skills/keyline/domains/patterns/iteration.md +78 -0
- package/plugins/keyline/skills/keyline/domains/type/display-tier.md +108 -0
- package/plugins/keyline/skills/keyline/domains/type/guide.md +251 -0
- package/plugins/keyline/skills/keyline/domains/type/ladders.md +169 -0
- package/plugins/keyline/skills/keyline/domains/type/numerals-and-loading.md +107 -0
- package/plugins/keyline/skills/keyline/domains/type/prose-and-emphasis.md +125 -0
- package/plugins/keyline/skills/keyline/domains/type/type-audit.md +90 -0
- package/plugins/keyline/skills/keyline/glass/color/assets/color-tokens.css +319 -0
- package/plugins/keyline/skills/keyline/glass/color/assets/theme.ts +119 -0
- package/plugins/keyline/skills/keyline/glass/color/guide.md +222 -0
- package/plugins/keyline/skills/keyline/glass/color/intent-ramp.md +136 -0
- package/plugins/keyline/skills/keyline/glass/color/theming.md +112 -0
- package/plugins/keyline/skills/keyline/glass/depth/assets/GlassPanel.tsx +171 -0
- package/plugins/keyline/skills/keyline/glass/depth/assets/glass.css +322 -0
- package/plugins/keyline/skills/keyline/glass/depth/blur-and-tint.md +50 -0
- package/plugins/keyline/skills/keyline/glass/depth/depth-cues.md +87 -0
- package/plugins/keyline/skills/keyline/glass/depth/guide.md +216 -0
- package/plugins/keyline/skills/keyline/glass/depth/performance-and-fallbacks.md +37 -0
- package/plugins/keyline/skills/keyline/glass/layout/adaptive-presentations.md +231 -0
- package/plugins/keyline/skills/keyline/glass/layout/assets/SectionNav.tsx +244 -0
- package/plugins/keyline/skills/keyline/glass/layout/assets/layout-tokens.css +199 -0
- package/plugins/keyline/skills/keyline/glass/layout/guide.md +258 -0
- package/plugins/keyline/skills/keyline/glass/layout/measure-and-rhythm.md +134 -0
- package/plugins/keyline/skills/keyline/glass/layout/radius-and-elevation.md +102 -0
- package/plugins/keyline/skills/keyline/glass/motion/assets/DropMenu.tsx +106 -0
- package/plugins/keyline/skills/keyline/glass/motion/assets/motion-presets.ts +210 -0
- package/plugins/keyline/skills/keyline/glass/motion/assets/motion-tokens.css +176 -0
- package/plugins/keyline/skills/keyline/glass/motion/assets/useReveal.ts +65 -0
- package/plugins/keyline/skills/keyline/glass/motion/assets/useScrollTick.ts +183 -0
- package/plugins/keyline/skills/keyline/glass/motion/easing-and-duration.md +108 -0
- package/plugins/keyline/skills/keyline/glass/motion/guide.md +286 -0
- package/plugins/keyline/skills/keyline/glass/motion/reduced-motion.md +81 -0
- package/plugins/keyline/skills/keyline/glass/motion/scroll-performance.md +58 -0
- package/plugins/keyline/skills/keyline/glass/motion/shared-element.md +161 -0
- package/plugins/keyline/skills/keyline/glass/motion/sound.md +192 -0
- package/plugins/keyline/skills/keyline/glass/motion/springs.md +51 -0
- package/plugins/keyline/skills/keyline/glass/type/assets/prose.tsx +268 -0
- package/plugins/keyline/skills/keyline/glass/type/assets/type-tokens.css +414 -0
- package/plugins/keyline/skills/keyline/glass/type/guide.md +222 -0
- package/plugins/keyline/skills/keyline/glass/type/long-form-prose.md +151 -0
- package/plugins/keyline/skills/keyline/glass/type/scale.md +69 -0
- package/plugins/keyline/skills/keyline/glass/type/weight-and-tracking.md +61 -0
- package/plugins/keyline/skills/keyline/scripts/check/css.mjs +112 -0
- package/plugins/keyline/skills/keyline/scripts/check/design.mjs +67 -0
- package/plugins/keyline/skills/keyline/scripts/check/files.mjs +44 -0
- package/plugins/keyline/skills/keyline/scripts/check/markup.mjs +149 -0
- package/plugins/keyline/skills/keyline/scripts/check/rules.mjs +584 -0
- package/plugins/keyline/skills/keyline/scripts/check.mjs +294 -0
- package/plugins/keyline/skills/keyline/scripts/contrast.mjs +153 -0
- package/plugins/keyline/skills/keyline/scripts/scan.mjs +340 -0
- package/plugins/keyline/skills/keyline/system/design-md.md +136 -0
- package/plugins/keyline/skills/keyline/system/detection.md +53 -0
- package/plugins/keyline/skills/keyline/system/guide.md +101 -0
- package/plugins/keyline/skills/keyline/system/presets.md +58 -0
- package/plugins/keyline/skills/keyline/system/roles.md +150 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# `/keyline critique`
|
|
2
|
+
|
|
3
|
+
A designer's read of one screen, flow or component: what a reader notices first, what's working, and the few changes that would matter most. Judgement, ranked — not a checklist. For every law checked with evidence, use `/keyline audit`.
|
|
4
|
+
|
|
5
|
+
**Read:** `DESIGN.md`. Then the law headings (the `### N.` lines) of all six `domains/<domain>/guide.md` — enough to know which laws exist and cite them by number. Read a law's full text only for the two or three domains where the screen's biggest problems sit, after step 3 has found them. A critique ranks a few changes; it doesn't need every law in full, which is what `/keyline audit` is for.
|
|
6
|
+
|
|
7
|
+
**Steps:**
|
|
8
|
+
|
|
9
|
+
1. **Look at it.** Render it if you can — the running app, a preview, a screenshot through whatever browser tool you have — at a desktop and a phone width, in each theme. Read the code too, for what rendering hides (states, focus, reduced motion). If nothing can be rendered, say the critique is from code alone. With a headless Chrome and no other tool, `--screenshot --window-size=1440,900` shoots a page; dark mode needs the page's own theme switch (its `data-theme` or class) set in the URL or DOM, because Chrome's `--blink-settings` flag only gives light reliably.
|
|
10
|
+
2. **Name what the screen is for and who reads it**, in one sentence. Everything below is measured against it.
|
|
11
|
+
3. **Read it as its reader would, in order:** what the eye lands on first, second, third — and whether that order matches what matters. Then how it groups, what competes, what's missing, what's noise.
|
|
12
|
+
4. **Tie each problem to a law**, by domain and number (`layout law 3`), so the fix is a rule rather than a taste. Where a named UX law explains *why* the reader feels it, name that too (`layout law 13, Fitts's law`) — it tells the owner the problem is about people, not preference. The names are reasons, not a checklist: cite one only where it fits the finding.
|
|
13
|
+
|
|
14
|
+
| UX law | The reader… | Where Keyline holds it |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Aesthetic-usability | forgives a polished screen, and overlooks its problems in testing | Here: judge the laws, not the polish — a beautiful screen still gets every check |
|
|
17
|
+
| Hick's law | takes longer to choose as choices grow | patterns law 8 |
|
|
18
|
+
| Fitts's law | hits big, near targets faster | layout law 13 |
|
|
19
|
+
| Jakob's law | expects this product to work like the others they use | patterns law 14 |
|
|
20
|
+
| Miller's law | holds only a few chunks at once | patterns law 8 |
|
|
21
|
+
| Doherty threshold | keeps pace with responses under ≈ 400ms | patterns law 15; motion law 3 |
|
|
22
|
+
| Proximity | groups what sits close | layout law 2 |
|
|
23
|
+
| Similarity | groups what looks alike | patterns law 3; layout law 3 (one style per device) |
|
|
24
|
+
| Common region | groups what shares a boundary | layout law 3 — the quietest boundary that holds, a card last |
|
|
25
|
+
| Visual hierarchy | reads in the order size, weight, colour and position set | type laws 3–4; layout law 2; step 3 above |
|
|
26
|
+
| Progressive disclosure | copes when detail arrives on request | patterns law 8; layout law 11 |
|
|
27
|
+
| Recognition over recall | finds seeing easier than remembering | patterns law 16; patterns law 7 (shortcuts shown where the action lives) |
|
|
28
|
+
| Von Restorff | remembers the one thing that differs | colour law 7; motion law 9; one primary button (patterns law 13) |
|
|
29
|
+
| Peak-end rule | remembers the best or worst moment and the end | patterns law 17 |
|
|
30
|
+
| Goal-gradient | speeds up near the goal | patterns law 17 |
|
|
31
|
+
| Serial position | remembers the first and last items | patterns law 3; primary destinations first in the shell (layout dials) |
|
|
32
|
+
| Tesler's law | shouldn't carry complexity the product can | patterns law 16 |
|
|
33
|
+
| Postel's law | types things in many forms | patterns law 16 |
|
|
34
|
+
| Occam's razor | is served by the fewest parts that do the job | motion law 1; patterns law 12; `commands/refine.md` |
|
|
35
|
+
| Feedback loops | needs to see every action land | patterns law 15 |
|
|
36
|
+
|
|
37
|
+
**Report:**
|
|
38
|
+
|
|
39
|
+
- **The read** — two or three sentences: what the screen gets right, and the one thing that most undercuts it.
|
|
40
|
+
- **Changes, most important first** — at most five. Each: what's wrong, as the reader feels it; the law; the fix in a sentence, in the project's names; and the command that makes it (`/keyline layout`).
|
|
41
|
+
- **Keep** — what's working and shouldn't be touched by the fixes.
|
|
42
|
+
|
|
43
|
+
Don't pad to five. Don't list what's fine as a problem to fix. **Changes no code.**
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# `/keyline depth`
|
|
2
|
+
|
|
3
|
+
**Guide:** `domains/depth/guide.md` · **`DESIGN.md` section:** `## Surfaces` · **Procedure:** `commands/refine.md`
|
|
4
|
+
|
|
5
|
+
Makes every surface say where it sits — flat, raised, floating or modal — with each level one bundle of surface, edge, shadow and layer. Shadows from one light, dark themes that lift by lightening, scrims, gradient strokes, and translucency only where it earns its place, legible from its tint alone, with every fallback.
|
|
6
|
+
|
|
7
|
+
Typical finds: a card that should be a section; four shadow recipes for two levels; dark-mode cards that vanish because the shadow was the only cue; blur stacked on blur; text over glass that fails contrast against its worst backdrop; no `@supports` or reduced-transparency fallback. Deeper references: `elevation-recipes.md`, `translucent-materials.md`, `fallbacks-and-performance.md`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# `/keyline init`
|
|
2
|
+
|
|
3
|
+
Fit Keyline to this project: find out once what it calls things, who it's for and where it already breaks the laws, and write that down in `DESIGN.md` at the project root.
|
|
4
|
+
|
|
5
|
+
**Read:** `system/guide.md`, then `system/roles.md`, `system/presets.md` and `system/design-md.md`. `system/detection.md` when the scan leaves you unsure where a stack keeps its tokens.
|
|
6
|
+
|
|
7
|
+
**Steps:** follow `system/guide.md` from step 1 to step 5 — scan, map by usage, measure contrast, choose the preset from the reader, ask at most four questions, write the file. Before writing `## Open issues`, read the law headings (the `### N.` lines) of each domain guide you cite, so every citation names the right law.
|
|
8
|
+
|
|
9
|
+
- **`DESIGN.md` already exists:** this is `/keyline sync`, not a rewrite. Say so and run that instead.
|
|
10
|
+
- **A new project with no interface yet:** there is nothing to scan. Ask what the product is and who reads it, propose a preset, and write `## Profile` with the preset's defaults from each domain guide's `## Dials`, marked as defaults. Generate tokens only if asked (`system/guide.md` §6).
|
|
11
|
+
- **Asked for one domain only** (by another command finding `DESIGN.md` missing): write `## Profile` and that domain's section, and leave the rest for a full run.
|
|
12
|
+
|
|
13
|
+
**Report**, briefly: the preset and the evidence for it, how many roles mapped cleanly, which names you proposed, and the three open issues that matter most. Then the next step — usually `/keyline critique` on the main screen, or `/keyline polish` if the user wants changes.
|
|
14
|
+
|
|
15
|
+
**Changes no code.** Generating tokens is a separate request.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# `/keyline layout`
|
|
2
|
+
|
|
3
|
+
**Guide:** `domains/layout/guide.md` · **`DESIGN.md` section:** `## Layout` · **Procedure:** `commands/refine.md` · **Alias:** `spacing`
|
|
4
|
+
|
|
5
|
+
Lets the layout carry the hierarchy: one reading measure every template shares, spacing that groups (a heading closer to what it introduces than to what came before), radii that nest, line weights by job, one z-index ladder, breakpoints named for what changes, a column grid where unlike blocks sit side by side, targets big enough to hit, and boxes only where something is genuinely a separate object.
|
|
6
|
+
|
|
7
|
+
Typical finds: equal margins above and below a heading; every section in a card, cards in cards, a bento grid of unlike things; a radius inside a radius that doesn't subtract the padding; `z-index: 9999`; a hover-only control with no touch path; a 16px icon button with no padding to hit; content that unmounts when collapsed. Deeper references: `measure-and-rhythm.md`, `radius-lines-and-layers.md`, `forks-gating-and-disclosures.md`.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# `/keyline motion`
|
|
2
|
+
|
|
3
|
+
**Guide:** `domains/motion/guide.md` · **`DESIGN.md` section:** `## Motion` · **Procedure:** `commands/refine.md` · **Alias:** `animate`
|
|
4
|
+
|
|
5
|
+
Makes every animation explain a change: durations scaled to how much of the screen moves, one standard curve, a colour curve that never overshoots, springs named by character, closing faster than opening, shared elements that travel, reduced motion as a real alternative, content never hidden behind an animation that might not run, and scroll work that stays off the main thread.
|
|
6
|
+
|
|
7
|
+
Typical finds: `transition: all`; a dozen durations where three would do; a menu that closes as slowly as it opens; reveal-on-scroll that leaves content invisible without JavaScript; hover effects that fire on touch; animating `top` and `height` instead of transforms. Check every change with reduced motion on. Deeper references: `ladder-and-curves.md`, `patterns.md`, `guards.md`, `performance.md`.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# `/keyline patterns`
|
|
2
|
+
|
|
3
|
+
**Guide:** `domains/patterns/guide.md` · **`DESIGN.md` section:** `## Patterns` · **Procedure:** `commands/refine.md` · **Alias:** `copy`
|
|
4
|
+
|
|
5
|
+
How the product behaves and what it says: short, sentence-case labels and verb buttons; a row's second line naming type and origin; one treatment for destructive actions, with confirmation scaled to the loss; an AI agent's work shown step by step with approvals inline; keyboard-first dense tools; optimistic changes that roll back; dashboards, forms and sign-in that ask for no more than they need; actions that answer at once and say when they're slow; inputs that forgive format; flows that show how near the end is and end on a designed screen; on landing pages and articles, elements that do what their form promises. A pattern kept in one place and broken in another is the finding.
|
|
6
|
+
|
|
7
|
+
Typical finds: "Click here to submit your form"; Title Case generated from keys; "OK" on a delete dialog; three different confirm treatments; an agent that goes silent while it works; a dashboard of stat tiles that answer nothing; a Save button that sits silent for two seconds; a phone field that rejects spaces; a wizard that ends on "Success". Feedback rounds follow the guide's `## Iterating on feedback`. Deeper references: `copy.md`, `actions-and-forms.md`, `agents-and-live-states.md`, `collections-and-dashboards.md`, `iteration.md`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# `/keyline polish`
|
|
2
|
+
|
|
3
|
+
All six refining commands on one target, in order, each as its own reviewable pass. The finishing pass before something ships, and the default when the user says "make this better".
|
|
4
|
+
|
|
5
|
+
**Order:** `type` → `color` → `layout` → `depth` → `motion` → `patterns`. Each builds on the one before: type settles the roles colour gives tiers to; spacing groups type that has stopped moving; surfaces sit on finished spacing and colour; motion animates surfaces that exist; words are checked last, in the product as it now stands. If a `patterns` change alters a label's length enough to move the layout, recheck that spot.
|
|
6
|
+
|
|
7
|
+
**Steps:**
|
|
8
|
+
|
|
9
|
+
1. Read `DESIGN.md`. If it's missing, run `/keyline init` first and say so — polish without a profile is guessing.
|
|
10
|
+
2. Agree the target: one screen, flow or component. "The whole app" is several polishes; offer to start with the screen readers see most.
|
|
11
|
+
3. Run `commands/refine.md` once per domain, in order, reading each domain's guide when its turn comes. Keep each pass to its domain.
|
|
12
|
+
4. **Settle.** Later passes create and change elements the earlier ones never saw — the depth pass turns a callout into a note, and nothing has given the note its type role. Re-run each earlier domain's `## Checklist` on the elements later passes created or changed, and fix within that domain.
|
|
13
|
+
5. Render the result beside the starting point.
|
|
14
|
+
|
|
15
|
+
Polish improves the screen it's given; it doesn't move navigation or add features (`commands/refine.md`, the two limits).
|
|
16
|
+
|
|
17
|
+
**Report**, per domain in order: what changed and why, in a line or two each — a domain with nothing to fix says so — then what the settling pass changed. Then what was measured, what went into `DESIGN.md`, and anything left for later with its command.
|
|
18
|
+
|
|
19
|
+
If the user wants to review as you go, stop after each domain and show it before starting the next.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# How a refining command works
|
|
2
|
+
|
|
3
|
+
`type`, `color`, `layout`, `depth`, `motion` and `patterns` share one procedure. Each command's own file names its guide and what it covers; this is the rest.
|
|
4
|
+
|
|
5
|
+
1. **Read the domain's `guide.md` in full**, and the references its laws point to for the parts the target touches. Under preset `glass`, read `glass/<domain>/guide.md` too, and take values and components from its `assets/`.
|
|
6
|
+
2. **Read the domain's section of `DESIGN.md`** and every `## Decisions` entry it covers. A decision beats a dial; never "fix" a decided value back.
|
|
7
|
+
3. **Find the problems first.** Run `node <keyline>/scripts/check.mjs <target> --root <project root> --rule <domain>` for what can be checked mechanically, then the guide's `## Auditing existing code` on the target only, and list what breaks a law before changing anything. With no target, take the screen the conversation is about; if there isn't one, ask which, or offer the whole project as a longer pass.
|
|
8
|
+
4. **Fix in the project's names**, at the smallest scope that removes the cause — a token over twenty call sites, a shared component over its instances. Add a role only where the guide's `## Building it` says one is missing, in the project's naming style, marked `proposed` in `DESIGN.md`. Where the project has no tokens for the domain at all — no type tokens, no motion tokens — propose a full set in the project's style, each marked `proposed`, and fix against it.
|
|
9
|
+
5. **Stay in the domain.** Something another domain owns — a colour noticed while fixing type — is reported with its owning command, not fixed. The exception: a fix that belongs to an earlier domain in polish order but only became visible in this pass — the note this pass made from a callout now needs its type role — is made here, and reported as that domain's.
|
|
10
|
+
6. **Sweep** every route the changed rule reaches and report what moved (`domains/patterns/iteration.md`).
|
|
11
|
+
7. **Look at it** rendered, before and after, in each theme and at a narrow width; for motion, also with reduced motion on.
|
|
12
|
+
8. **Write back** to `DESIGN.md`: the domain's section if a role or value changed, `## Decisions` for anything moved off the preset (with the user's go-ahead) and for any change a law forced that a later session might undo (marked *law*), and strike fixed `## Open issues`.
|
|
13
|
+
9. **Run the guide's `## Checklist`** on what you changed, and the detector again on the target: nothing you touched should still be flagged.
|
|
14
|
+
|
|
15
|
+
**Three limits hold in every pass:**
|
|
16
|
+
|
|
17
|
+
- **The brief wins over the preset.** An element the product or its brief requires stays, even where a dial or a law would cut it — a dashboard's activity feed, a top bar the product has. The pass changes how it's built, not whether it exists.
|
|
18
|
+
- **Improving is not restructuring.** Improving an existing screen doesn't move its navigation or add features — a collapsible icon rail, a theme toggle, a new view. Those go under `## Open issues` unless the user asks for them.
|
|
19
|
+
- **Improving keeps what the design already chose.** Its brand colour and gradients stay wherever they do a job, and its corners stay at least as round as they were. A dial describes a new system, so an existing design is never greyed or sharpened to fit one. Only a law removes something, and the report names it. When a pass would make the screen plainer than it was, check whether a law actually asked for that.
|
|
20
|
+
|
|
21
|
+
**Where a preset's default and the existing design disagree**, this is the order, and the one place it is decided:
|
|
22
|
+
|
|
23
|
+
- **Dividers.** The design's section device stays: panels stay panels, made quieter (layout law 3). The preset's "full-bleed hairline, no tiles" describes a new system. One thing changes either way: a row of stat tiles folds into one strip of figures parted by hairlines, because that is a law about figures, not a divider dial — unless keeping the tiles is recorded under `## Decisions`.
|
|
24
|
+
- **Corners.** The nesting law (layout law 5) never sharpens a corner. Where inner = outer − inset would come out sharper than the inner corner is now, raise the outer radius or trim the inset until they share a centre; if neither fits, the corners aren't concentric and the inner keeps its radius.
|
|
25
|
+
- **Line weight.** The design's line weights stay, like its corners. Product's 0.5px hairline is for a new system, or for an existing line that reads heavy and whose colour is strong enough to survive halving; a border already near the surface colour keeps its 1px.
|
|
26
|
+
|
|
27
|
+
Record each of these calls under `## Decisions`, so the next run makes the same one.
|
|
28
|
+
|
|
29
|
+
**Report:** what was wrong, as a reader would feel it; what changed, by token or component, with the law; what was measured (sizes, ratios, durations); and anything noticed outside the domain, with its command.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# `/keyline sync`
|
|
2
|
+
|
|
3
|
+
Bring `DESIGN.md` back in line with code that has moved since it was written. An audit of the file, not a rewrite of it.
|
|
4
|
+
|
|
5
|
+
**Read:** `system/guide.md` §0, and `system/design-md.md` for the format.
|
|
6
|
+
|
|
7
|
+
**Steps:**
|
|
8
|
+
|
|
9
|
+
1. Rescan with `node <keyline>/scripts/scan.mjs . --ignore <prose and example dirs>`, using the same ignores as last time if `DESIGN.md` records them.
|
|
10
|
+
2. Compare against every table in `DESIGN.md`: values that changed, tokens that appeared or disappeared, roles now done by a different token, a theme or area that's new.
|
|
11
|
+
3. Re-measure every text tier with `<keyline>/scripts/contrast.mjs --ladder`, per theme, against its worst surface.
|
|
12
|
+
4. Walk `## Open issues`: strike what's fixed (with the evidence that it is), keep what isn't, add what's new.
|
|
13
|
+
5. Propose the edits as a short diff and apply them. Every name the user chose and every `## Decisions` entry survives; a decision the code now contradicts is reported, never silently dropped.
|
|
14
|
+
|
|
15
|
+
**Report:** what drifted, what's newly fixed, what's newly broken, and anything in `## Decisions` the code no longer follows.
|
|
16
|
+
|
|
17
|
+
**Changes no code** — only `DESIGN.md`.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# `/keyline type`
|
|
2
|
+
|
|
3
|
+
**Guide:** `domains/type/guide.md` · **`DESIGN.md` section:** `## Type` · **Procedure:** `commands/refine.md`
|
|
4
|
+
|
|
5
|
+
Makes the hierarchy readable at a glance: every size belongs to a role, nothing outranks the heading it sits under, weights each have one job, leading and tracking are corrected for size, emphasis uses axes the system already has, numbers that align are tabular, and fonts load without moving the text.
|
|
6
|
+
|
|
7
|
+
Typical finds: a template opening a step above the rest; a lead larger than its heading; five weights where two would do; uppercase eyebrows over every section; body copy tracked; prices in proportional figures; imported Markdown on the browser's own ladder. Deeper references: `ladders.md`, `display-tier.md`, `prose-and-emphasis.md`, `numerals-and-loading.md`, `type-audit.md` (the sweep after a type change).
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Deriving a ramp
|
|
2
|
+
|
|
3
|
+
From a preset and a brand hue (or none) to every role, per theme, every ratio measured. Dials change the inputs, not the method.
|
|
4
|
+
|
|
5
|
+
## Work in OKLCH
|
|
6
|
+
|
|
7
|
+
Pick values in `oklch(L C h)`, not hex or HSL: lightness is perceptual, so greys 0.02 apart look equally different anywhere on the ramp, and hue holds as lightness changes — a teal stays teal as it darkens, where HSL drifts. Browsers and `contrast.mjs` take `oklch()`. Where the stack needs hex (some design tools, email, older build tooling), keep the OKLCH source in a comment beside it, so a value can move without re-deriving the rest.
|
|
8
|
+
|
|
9
|
+
## The three inputs
|
|
10
|
+
|
|
11
|
+
1. **The neutral hue and chroma.** No hue and chroma 0 for pure greys; otherwise the brand hue at the preset's neutral chroma.
|
|
12
|
+
2. **The worst surface for each tier.** List the surfaces each text tier really lands on — tertiary text in a table meets hover and selected rows; an article caption, the page and a subtle band — and solve against the worst.
|
|
13
|
+
3. **The text targets.** One ratio per tier, from the table, adjusted so adjacent tiers keep the preset's minimum step.
|
|
14
|
+
|
|
15
|
+
| Preset | Tiers, top to bottom (target ratio on the worst surface) | Minimum step |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `editorial` | 15 · 8 · 6.5 · 5.5 · 4.6 | 1.15× |
|
|
18
|
+
| `product` | 15 · 7.5 · 4.8 | 1.4× |
|
|
19
|
+
| `expressive` | 15 · 8 · 6 · 4.6 | 1.25× |
|
|
20
|
+
|
|
21
|
+
Drop unused middle tiers; never the floor.
|
|
22
|
+
|
|
23
|
+
## Solving a tier's lightness
|
|
24
|
+
|
|
25
|
+
For a surface of relative luminance `Yₛ` and a target ratio `R`, the text's luminance `Y` is:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
text darker than the surface (light theme): Y = (Yₛ + 0.05) / R − 0.05
|
|
29
|
+
text lighter than the surface (dark theme): Y = R × (Yₛ + 0.05) − 0.05
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For a near-neutral colour, OKLCH lightness is very nearly the cube root of luminance, so `L ≈ ∛Y` and `Yₛ ≈ Lₛ³` — a one-line starting value; verify it, nudging by 0.005 until the check passes:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
node <keyline>/scripts/contrast.mjs --ladder "oklch(0.955 0.008 195)" \
|
|
36
|
+
"oklch(0.22 0.008 195)" "oklch(0.41 0.008 195)" "oklch(0.52 0.008 195)"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Worked: a tertiary target of 4.8 on a light hover row at `L 0.955` (`Yₛ ≈ 0.871`) gives `Y = 0.921 / 4.8 − 0.05 = 0.142`, so `L ≈ 0.52`. On a dark hover row at `L 0.24` (`Yₛ ≈ 0.014`), `Y = 4.8 × 0.064 − 0.05 = 0.256`, so `L ≈ 0.635`.
|
|
40
|
+
|
|
41
|
+
For accents and status foregrounds `∛Y` is only a first guess: at high chroma luminance drifts from lightness, most in blues and violets (`oklch(0.55 0.25 290)` matches a grey near L 0.52). Let the script decide.
|
|
42
|
+
|
|
43
|
+
## Spacing surfaces and lines
|
|
44
|
+
|
|
45
|
+
Space surfaces by contrast against the page, not equal lightness steps, so the ratios read as the same steps in both themes.
|
|
46
|
+
|
|
47
|
+
| Role | Ratio against `surface.page` | Light L (from 1.0) | Dark L (from 0.17) |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| `surface.raised` | 1.03–1.08 | 0.985 | 0.21 |
|
|
50
|
+
| `surface.subtle` | 1.05–1.08 | 0.975 | 0.20 |
|
|
51
|
+
| `surface.hover` | 1.12–1.16 | 0.955 | 0.24 |
|
|
52
|
+
| `surface.selected` | 1.12–1.22, plus accent chroma | 0.95 | 0.25 |
|
|
53
|
+
| `surface.pressed` | 1.2–1.27 | 0.935 | 0.27 |
|
|
54
|
+
| `border.default` | 1.25–1.45 | 0.90 | 0.30 |
|
|
55
|
+
| `border.hover` · `mark` | 1.8–2.1 | 0.80 | 0.40 |
|
|
56
|
+
| `border.control` (an input's edge) | ≥ 3 on every surface an input sits on | 0.62 | 0.52 |
|
|
57
|
+
|
|
58
|
+
Steps land about 0.02 apart in L in light, 0.03 in dark. Light `border.default` may sit lighter than `surface.pressed` if borders should recede further (law 1). Light `surface.raised` may be the page colour with a border, or white on a grey page — either way round, so long as every level steps the same way; dark must be lighter than the page.
|
|
59
|
+
|
|
60
|
+
## Deriving the accent from a brand hue
|
|
61
|
+
|
|
62
|
+
The brand hue is given; derive the rest per theme. A logo's colour is where the hue comes from, not the accent: tuned for a mark seen once, at full strength it reads too bright across a working screen. Keep its hue and tone chroma and lightness down until white text clears 4.5:1.
|
|
63
|
+
|
|
64
|
+
1. **`accent` in light.** The highest in-gamut chroma at a lightness where `text.on-accent` clears 4.5:1 — with white text, usually L 0.45–0.55. If the script reports clipping, lower the chroma, not the lightness.
|
|
65
|
+
2. **`accent` in dark.** Either one value, L roughly 0.72–0.78 with chroma trimmed a little, with `text.on-accent` flipped to the dark `surface.page`; or, where the brand keeps white text on its fill, two: the fill near its light-theme lightness, and a lighter `accent.ink` at 4.5:1 on the dark page for links, icons and brand text, since no single value clears both. Measure each as a new colour.
|
|
66
|
+
3. **Its hover.** 0.05 in L away from the page — darker in light, lighter in dark: more contrast in both.
|
|
67
|
+
4. **`surface.selected`.** For a selected row in a dense table, the accent hue at the `surface.selected` lightness, chroma about 0.03. For the current nav item or tab under product and expressive, the stronger tint colour law 7 asks for: the accent at about 25% over its surface, carrying `accent.ink`. Re-measure the *lowest* text tier on each: this fails most often, because a tint costs a little luminance.
|
|
68
|
+
5. **`focus`.** The accent, if it clears 3:1 on every surface including `surface.selected`, and on the page: `outline-offset` leaves a gap of page colour, so a ring round an accent button is seen against the page.
|
|
69
|
+
|
|
70
|
+
**When the brand colour can't be text.** Yellows, limes and light cyans have no lightness at usable chroma where white text clears 4.5:1, and fail as text on white. Keep them for fills and marks with dark `text.on-accent`, and derive a darker token for anything text-sized — `text.link`, if links are coloured. Name both for their jobs; neither is "brand-yellow".
|
|
71
|
+
|
|
72
|
+
## Mapping an existing numbered ramp
|
|
73
|
+
|
|
74
|
+
A project with `gray-50…950` or `blue-100…900` keeps its names; each step gets the role most of its usage does (border, caption, hover). Three findings are common:
|
|
75
|
+
|
|
76
|
+
- **One step doing two jobs**, usually a border grey also used for meta text; record it, since it can rarely pass both 3:1 and 4.5:1.
|
|
77
|
+
- **One job at several steps**, usually borders at three adjacent greys; the majority step becomes the role, the rest are strays.
|
|
78
|
+
- **No step that works as a text tier** between secondary and the floor; the fix is a new token picked by the method above, not a borrowed step.
|
|
79
|
+
|
|
80
|
+
## The translucent set
|
|
81
|
+
|
|
82
|
+
Starting values; measure each over the lightest and darkest surface it can sit on (`contrast.mjs` composites the alpha first):
|
|
83
|
+
|
|
84
|
+
| Role | Light | Dark |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| a layered hover wash | `oklch(0 0 0 / 0.04)` | `oklch(1 0 0 / 0.05)` |
|
|
87
|
+
| `border.layered` | `oklch(0 0 0 / 0.08)` | `oklch(1 0 0 / 0.1)` |
|
|
88
|
+
| `border.layered`, hovered | `oklch(0 0 0 / 0.14)` | `oklch(1 0 0 / 0.16)` |
|
|
89
|
+
|
|
90
|
+
Tint these toward the neutral hue if the solids are tinted; a pure-black hairline beside tinted greys reads as dirt.
|
|
91
|
+
|
|
92
|
+
## Worked example: Harbour
|
|
93
|
+
|
|
94
|
+
A fictional clinic-scheduling product: `product` preset, teal brand hue at OKLCH h 195, neutral chroma 0.008. Tertiary text sits in table rows, so the worst surface is `surface.hover`; every ratio is measured with `contrast.mjs`. Core blocks, then the full set:
|
|
95
|
+
|
|
96
|
+
```css
|
|
97
|
+
:root {
|
|
98
|
+
color-scheme: light;
|
|
99
|
+
--surface-page: oklch(1 0 0);
|
|
100
|
+
--surface-hover: oklch(0.955 0.008 195);
|
|
101
|
+
--border-default: oklch(0.9 0.008 195);
|
|
102
|
+
--text-primary: oklch(0.22 0.008 195);
|
|
103
|
+
--text-secondary: oklch(0.41 0.008 195);
|
|
104
|
+
--text-tertiary: oklch(0.52 0.008 195);
|
|
105
|
+
--accent: oklch(0.5 0.08 195); /* white text 5.77:1 */
|
|
106
|
+
}
|
|
107
|
+
:root[data-theme="dark"] {
|
|
108
|
+
color-scheme: dark;
|
|
109
|
+
--surface-page: oklch(0.17 0.008 195);
|
|
110
|
+
--surface-hover: oklch(0.24 0.008 195);
|
|
111
|
+
--border-default: oklch(0.3 0.008 195);
|
|
112
|
+
--text-primary: oklch(0.92 0.008 195);
|
|
113
|
+
--text-secondary: oklch(0.73 0.008 195);
|
|
114
|
+
--text-tertiary: oklch(0.64 0.008 195);
|
|
115
|
+
--accent: oklch(0.75 0.1 195); /* re-picked; dark page text on it 8.94:1 */
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
| Role | Light | Dark | Measured |
|
|
120
|
+
|---|---|---|---|
|
|
121
|
+
| `surface.page` | `oklch(1 0 0)` `#ffffff` | `oklch(0.17 0.008 195)` `#0b1110` | — |
|
|
122
|
+
| `surface.subtle` | `oklch(0.975 0.008 195)` | `oklch(0.2 0.008 195)` | — |
|
|
123
|
+
| `surface.raised` | `oklch(0.985 0.008 195)` | `oklch(0.21 0.008 195)` | — |
|
|
124
|
+
| `surface.hover` | `oklch(0.955 0.008 195)` | `oklch(0.24 0.008 195)` | — |
|
|
125
|
+
| `surface.selected` | `oklch(0.95 0.03 195)` | `oklch(0.25 0.03 195)` | — |
|
|
126
|
+
| `surface.pressed` | `oklch(0.935 0.008 195)` | `oklch(0.27 0.008 195)` | — |
|
|
127
|
+
| `border.default` | `oklch(0.9 0.008 195)` | `oklch(0.3 0.008 195)` | decorative |
|
|
128
|
+
| `border.control` | `oklch(0.62 0.008 195)` | `oklch(0.52 0.008 195)` | 3.38 / 3.22 on the worst surface an input sits on |
|
|
129
|
+
| `mark` | `oklch(0.8 0.008 195)` | `oklch(0.4 0.008 195)` | decorative |
|
|
130
|
+
| `text.primary` | `oklch(0.22 0.008 195)` | `oklch(0.92 0.008 195)` | 15.19:1 / 12.99:1 on hover |
|
|
131
|
+
| `text.secondary` | `oklch(0.41 0.008 195)` | `oklch(0.73 0.008 195)` | 7.74:1 / 6.88:1 on hover |
|
|
132
|
+
| `text.tertiary` | `oklch(0.52 0.008 195)` | `oklch(0.64 0.008 195)` | 4.83:1 / 4.90:1 on hover |
|
|
133
|
+
| `accent` · `focus` | `oklch(0.5 0.08 195)` | `oklch(0.75 0.1 195)` | focus 5.07:1 (light) on hover; 5.04 / 7.42 on selected |
|
|
134
|
+
| `accent`, hovered | `oklch(0.45 0.075 195)` | `oklch(0.8 0.1 195)` | — |
|
|
135
|
+
| `text.on-accent` | `oklch(1 0 0)` | `oklch(0.17 0.008 195)` | 5.77 / 8.94 |
|
|
136
|
+
| `surface.inverse` | = `text.primary` | = `text.primary` | — |
|
|
137
|
+
| `text.inverse` | = `surface.page` | = `surface.page` | 17.27 / 15.11 |
|
|
138
|
+
|
|
139
|
+
What the measurements found:
|
|
140
|
+
|
|
141
|
+
- **The first dark `surface.selected` failed:** at `oklch(0.27 0.035 195)` tertiary text on a selected row was 4.44:1; `oklch(0.25 0.03 195)` gives 4.73:1. The page check would never have shown it.
|
|
142
|
+
- **`surface.pressed` fails tertiary text in dark** (4.48:1). A press is momentary, so Harbour excludes it from the tertiary check and records that under `## Decisions`, so the next audit doesn't rediscover it as a bug.
|
|
143
|
+
|
|
144
|
+
The ladder steps, 15.19 → 7.74 → 4.83 in light (2.0×, 1.6×) and 12.99 → 6.88 → 4.90 in dark (1.9×, 1.4×), both clear the `product` minimum of 1.4×.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Colour and Theming
|
|
2
|
+
|
|
3
|
+
A near-monochrome reading site, a dense product with a full status palette and a brand page spending colour broadly differ in how much colour they spend, not in how it is organised: **a colour token encodes a job, not a lightness.** `border.default` is not "a bit darker than the hover surface", it is *the colour an edge is*. Indexed by intent, picking a colour means naming a role, a reviewer can catch a mistake in the diff, and a second theme is a re-tint of the same names rather than a second palette.
|
|
4
|
+
|
|
5
|
+
## Before you start
|
|
6
|
+
|
|
7
|
+
Read `DESIGN.md` at the project root: `## Profile` for the preset and any areas on another one, `## Colour` for the project's name for each role and the decisions already made. Reason in roles; write code in the project's names.
|
|
8
|
+
|
|
9
|
+
- **No `DESIGN.md`:** run `/keyline init` for this domain only, do the task, then recommend the full setup.
|
|
10
|
+
- **A role you need has no row:** reuse a value already doing that job; otherwise add one in the project's naming style and record it in `DESIGN.md`.
|
|
11
|
+
- **The task would break a law or move a dial off its preset:** say so and ask; if the user confirms, record it with its reason under `## Decisions`.
|
|
12
|
+
- **Preset `glass`:** the values are in `DESIGN.md`; `glass/color/guide.md` has the exact recipes behind them.
|
|
13
|
+
|
|
14
|
+
## Laws
|
|
15
|
+
|
|
16
|
+
### 1. A token is named for its job, not its lightness
|
|
17
|
+
|
|
18
|
+
**Every colour in the interface is a token, and every token answers one question — "what colour is a border?", "what colour is a pressed surface?" — never a position on a lightness scale.**
|
|
19
|
+
|
|
20
|
+
Indexed by lightness (`gray-100` lightest, `gray-900` darkest), every pick is a judgement — "a border, so… 300? 400?" — and six months later there are four border greys. Indexed by intent, the question has one answer, and a border at the pressed-surface step is visibly wrong in review. Brand hues too: `indigo-50` to `indigo-900` must still say which step is `accent`, `accent` hovered and `surface.selected`; `bg-indigo-600` here and `bg-indigo-700` there on one button means the ramp does no work. Map steps to roles; components reference the roles.
|
|
21
|
+
|
|
22
|
+
- **Intent can beat monotonicity.** A border sits *beside* content and must stay quieter than a pressed surface, which sits *under* it, so in light the border step may legitimately be lighter — in the Glass preset, `gray-400` (border) is lighter than `gray-300` (pressed).
|
|
23
|
+
- **"I need something between two steps" is a misdiagnosis.** The element has a different job from the one assumed; if no role fits, it's a new role, named and recorded — not an inline hex.
|
|
24
|
+
|
|
25
|
+
The allowed literals:
|
|
26
|
+
|
|
27
|
+
- **Illustration.** Colours inside an image, SVG artwork, or an HTML or SVG product mock or diagram marked `role="img"`. Text in it that a reader is meant to read still clears 4.5:1 against what is behind it.
|
|
28
|
+
- **Third-party marks.** Logos and provider marks — Google's, Slack's, a calendar provider's dot — keep their own colours. They are marks, not interface hues, and count against neither the one accent nor the exceptions (law 7).
|
|
29
|
+
- **Multi-layer values** built once as a shared constant (law 10).
|
|
30
|
+
|
|
31
|
+
### 2. Text tiers are their own tokens, never border steps
|
|
32
|
+
|
|
33
|
+
**A text colour is picked for text contrast, never a mid-ramp step borrowed from the border and fill range.**
|
|
34
|
+
|
|
35
|
+
Text needs 4.5:1 against its surface; a border or icon a user must see to operate a control, 3:1; a decorative edge, only visibility. One value rarely serves both, least in a *light* theme, where mid steps are light tints on white. The Glass preset shipped this for a whole revision — timestamps at **1.66:1** on the border-hover step, icons at **2.38:1** on border-active, its most-used text colour at **4.12:1** on fill-hover, beside a note claiming "about 4.6:1" — until three new tokens, picked for contrast, went between its two text steps.
|
|
36
|
+
|
|
37
|
+
Dark mode hides this — the same indices sit far lighter on near-black, so every mid step passes, and a system reviewed in dark ships a light theme whose body copy fails. So `text.primary`, `text.secondary`, `text.tertiary` and any tiers between are separate tokens. Icons take their label's tier or one below — an icon has more visual mass than text at the same value — and both advance together on hover. Decorative marks with no text (bullets, dots, inactive rail markers) are `mark`, stay on the ramp and aren't held to text contrast; kept faint, a list doesn't read as a column of dark dots.
|
|
38
|
+
|
|
39
|
+
### 3. The ladder clears the floor and strictly descends, measured where it's worst
|
|
40
|
+
|
|
41
|
+
**Every text tier clears 4.5:1 against the worst surface it actually lands on, in the worst theme, and each tier is measurably below the one above it.**
|
|
42
|
+
|
|
43
|
+
- **Computed, not estimated.** Use `<keyline>/scripts/contrast.mjs` (`--ladder <surface> <tier> <tier> …`, top tier first), which composites translucent colours first — the number the reader sees. Where the surface itself is translucent — a wash over a surface, a glass header over an image — `--over <backdrop>` composites it over the backdrop before the text is measured: `node <keyline>/scripts/contrast.mjs --over "#0b1220" --ladder "rgba(250,250,250,0.82)" "#16161a" "#4b4b52"`.
|
|
44
|
+
- **The worst surface, not the page.** Tertiary text may pass on `surface.page` and fail on `surface.hover`, `surface.subtle` or `surface.selected`; measure on the surface leaving least contrast — the darkest in light, the lightest in dark.
|
|
45
|
+
- **Strictly descending, with a step you can see.** Above 4.5:1 the tiers crowd into a narrow band, and it's easy to pass WCAG and lose the hierarchy — tiers at 5.1 and 4.9 are one tier with two names. `--ladder` flags a tier not below the one above; the minimum *size* of the step is a dial.
|
|
46
|
+
|
|
47
|
+
### 4. Anything on an unknown backdrop takes a translucent colour
|
|
48
|
+
|
|
49
|
+
**A border, divider, hairline or hover wash that can sit over anything but a known opaque surface uses a translucent value, and that translucent set inverts between themes.**
|
|
50
|
+
|
|
51
|
+
A solid border on a translucent panel ignores what's behind it, and over a photograph or a scrolled page reads as a frame pasted on top; a translucent one — "a little more contrast than whatever is here" — stays right over anything. That is black at low alpha in light and *white* in dark, because a dark-mode edge is lighter than its surface and a black overlay would disappear. Built once, with the same role names in both themes, it needs no per-component theme branching.
|
|
52
|
+
|
|
53
|
+
`border.layered` is the canonical role; project roles under it are fine. Translucent *material* tints are colours and live here (as runtime values, law 10); blur, edge, shadow and fallback surface belong to the depth guide.
|
|
54
|
+
|
|
55
|
+
### 5. Focus is findable on every surface
|
|
56
|
+
|
|
57
|
+
**The focus colour clears 3:1 against every surface it can land on, is drawn with a gap of page colour between it and the element, and nothing it sits next to shares it.**
|
|
58
|
+
|
|
59
|
+
```css
|
|
60
|
+
:focus-visible {
|
|
61
|
+
outline: 2px solid var(--focus);
|
|
62
|
+
outline-offset: 2px;
|
|
63
|
+
}
|
|
64
|
+
a:focus-visible { outline-offset: 3px; } /* ragged inline edges need more room */
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`:focus-visible`, never `:focus`, so a click leaves no ring — except a skip link, which must appear on keyboard focus regardless of heuristics. The offset makes the ring an object, not a border; `outline`, not `box-shadow`, because forced-colours mode removes shadows and keeps outlines. `outline: none` without a replacement is a bug under every preset.
|
|
68
|
+
|
|
69
|
+
Focus may share a token with `accent` but may not sit beside it: an accent ring round an accent button on an accent-tinted row is invisible exactly where it's needed. If the accent is everywhere, the ring gets a different colour or a double ring (page-colour inner, focus-colour outer). Status colours keep out of the focus hue for the same reason.
|
|
70
|
+
|
|
71
|
+
A dark band on a light-only page — a testimonial strip, a closing call to action — is a local theme: `focus` is overridden inside it, like its surfaces and text, and measured against the band.
|
|
72
|
+
|
|
73
|
+
### 6. Colour never carries a signal alone
|
|
74
|
+
|
|
75
|
+
**Every state or status expressed in colour is also expressed in something that survives without colour — an icon, a word, a weight, a rule, a marker, a position.**
|
|
76
|
+
|
|
77
|
+
About one man in twelve has some colour-vision deficiency, and every reader loses hue in sunlight, greyscale screenshots, cheap projectors; a red border as the only sign of an error tells them nothing. Status is an icon plus a sentence, with colour reinforcing both: **shape carries the state, colour confirms it.** A status glyph whose shape moves with the work, a ring that fills until it is a check, reads in greyscale; so do priority bars. The case people miss is state *within* a neutral system: once every tier clears 4.5:1 only two or three greys are readable, too close to mean "selected" or "current", so a nav item marked active only by being one tier darker needs a rule, a marker, a fill or a weight as well.
|
|
78
|
+
|
|
79
|
+
A status palette is a set of **pairs**, each a foreground at 4.5:1 on the page and its own tint plus a tint that reads as a surface, not a highlighter, re-picked per theme.
|
|
80
|
+
|
|
81
|
+
→ Status pairs with a worked set, the glyph shapes, and the exception test in full: `domains/color/status-and-exceptions.md`
|
|
82
|
+
|
|
83
|
+
### 7. Colour is spent structurally
|
|
84
|
+
|
|
85
|
+
**Every hue has one job, written down. Each chromatic spend beyond the accent and the status palette is named for what it's for, with a reason that can't spread to another element or another job, and contained so that only the mark takes the colour.**
|
|
86
|
+
|
|
87
|
+
Colour is attention, spent because of what the element *is*, never as decoration. Content looks like everything else the reader came for; only the interface speaking about it — an offer, a warning, a way in — may announce itself. The Glass preset's one exception, a "short version" panel, is the site's only offer, so the reason cannot spread; "it's important", "it's new" or "it's the brand colour" can, and becomes a palette within two components, because every author thinks their element qualifies. That test is for colour being *added*. Colour the design already spends on a job is not on trial (below).
|
|
88
|
+
|
|
89
|
+
**Colour the design already has stays.** When improving an existing interface, its brand colour stays wherever it does a job: the selected nav item, the primary data series, a progress or usage bar, a brand mark, the gradient every avatar shares. Each of those is the owner's decision and part of how the product is recognised. Turning it grey is a regression that no law asks for. Colour comes out only when it breaks a law, and the change cites that law: text under its contrast floor, a second hue competing with the primary action, colour carrying a state on its own, or body text dyed for emphasis. Even then, the first fix is the shade (re-pick it until it passes) rather than taking the colour away. Muting the brand for calm is a dial for a new system, not a fix for an existing one.
|
|
90
|
+
|
|
91
|
+
**"Too grey" is answered with jobs, not decoration.** Near-monochrome can go too far: a records product built to it came back as "all black and white" from its owner and "wants more colour" from its client within days. What it lacked was hue that answers a question at a glance, one family per question — *what* a thing is (its type), *how* it's doing (status), *where* it lives (its area), *who* (a person). A hue never crosses into another job: a type hue never signals state, and the danger red never marks something that is merely live.
|
|
92
|
+
|
|
93
|
+
Only the edge, the icon or the glyph takes the colour, never the text inside: a coloured paragraph shares the loudness until it means nothing. Record each as `exception.<what-it's-for>` in `DESIGN.md`, a family as `exception.<question>.<member>`, with its reason: how many a system holds is a dial; a written, non-transferable job is not. First ask whether weight, position and space can do the job. Most can.
|
|
94
|
+
|
|
95
|
+
**One hue acts, and colour points at what still needs a person.** The accent's jobs — primary action, focus, "your turn" — share one hue, and it marks one action per view. A second highlight hue beside it (a teal next to a blue) splits one meaning in two, and readers start asking which colour means what; whatever wanted it goes to a status pair, a neutral marker or weight. A hue family is not a highlight: it sits on marks and asks nothing of the reader. A place in the data with one member, today's column on a calendar, may be a written exception.
|
|
96
|
+
|
|
97
|
+
**Where the reader *is* takes the accent as a tint, not a fill.** Under product and expressive, the current nav item sits on `surface.selected`: the accent at about 25% over the surface it sits on, with the icon and label in `accent.ink`. Measure that label at 4.5:1 on the tint in each theme, and deepen the ink, not the tint, until it passes. The tint marks a place and the solid fill marks the action, so the selected item and the primary button never read as two calls to act. A page tab marks its current tab with layout law 3's underline instead, in `text.primary`. **This is the one definition of current**: the type guide adds weight to it, the motion guide moves it, neither re-picks it. Editorial and glass keep the current item neutral (a marker, a rule or weight), because a reading page has little chrome and the accent there belongs to focus.
|
|
98
|
+
|
|
99
|
+
**A chart's primary series takes the accent at full strength:** it is what the screen is about, not a second highlight, so no washed-out opacity or grey bar. Comparison and previous-period series are neutral. Three or more series use the chart palette in `status-and-exceptions.md`. Rising and falling figures on a dashboard may take the success and danger foregrounds, with an arrow or sign beside the figure so the colour isn't the only carrier. Code takes syntax highlighting under every preset: a token palette is a reading aid, not decoration, and it counts against no exception limit.
|
|
100
|
+
|
|
101
|
+
**Success has one job; whether finishing is that job is a dial.** Ask whether completion is the news. In a sequence the reader passes through — setup steps, passed checks, a wizard — it isn't: under editorial and product, done is an ink check on a neutral circle, because a column of green ticks outshouts the one item still pending, and success is kept for an outcome the reader must notice, approved or released. On a board or list people open to see what got done, it is: done takes `status.success` on a filled check whose shape already says done, while what's late takes warning or danger so the eye still finds what needs a person. Expressive may colour completion per brand, sequences included, counted among its exceptions.
|
|
102
|
+
|
|
103
|
+
→ Hue families, figures, done states, charts, live and AI marks, destructive rows: `domains/color/status-and-exceptions.md`
|
|
104
|
+
|
|
105
|
+
### 8. A theme is a re-pick, not a reuse
|
|
106
|
+
|
|
107
|
+
**Every coloured value — accent, focus, status pair, exception, each hue in a family, every stop of every gradient — is chosen afresh for each theme and measured there.**
|
|
108
|
+
|
|
109
|
+
- **Accents.** A hue at 4.5:1 on white has far less on near-black; reused, it dims the ring and links. Re-pick its lightness (usually trimming chroma) and re-check `text.on-accent`, which often flips from light to dark. A dark fill that keeps white text is too dark to be text on the dark page; no one value is both, so dark adds a lighter `accent.ink` for links, icons and brand text.
|
|
110
|
+
- **Small marks, against every ground.** Area colours shared across themes fell to 1.8:1 on a light frame. A mark the reader needs clears 3:1 on every ground it sits on — panel, frame, hover row — in each theme.
|
|
111
|
+
- **Gradients move every stop.** The darkest stop can sit a few points above a dark page and vanish down half its length; lightening only that stop compresses the ramp until it isn't one gradient. Move every stop by roughly the same amount.
|
|
112
|
+
- **Near-black, not black.** Dark `surface.page` sits a little above `#000`: pure black gives shadows nothing to sit on and glares against light text. Light `text.primary` is near-black too: pure black on white halates at small sizes.
|
|
113
|
+
- **"Raised" is ordered, and in dark it is lighter** (depth law 5). Dark fills are tinted, not darkened, so they don't punch a hole in the page.
|
|
114
|
+
- **Not a mechanical inversion,** which puts every gap in the wrong place. Space each theme's surfaces by contrast against *its own* page, so hover and pressed read as the same steps in both, with a large jump between the last edge step and the lowest text tier.
|
|
115
|
+
|
|
116
|
+
### 9. One mechanism sets the theme, before first paint
|
|
117
|
+
|
|
118
|
+
**When the user can choose a theme, an attribute or class on the root element is the mechanism — not the `prefers-color-scheme` media query — and it is set by a blocking inline script before the stylesheet applies.**
|
|
119
|
+
|
|
120
|
+
A toggle and a media query are two sources of truth, and once a user has chosen, the OS shouldn't win; the media query may seed the first visit, not be the switch. (Without a toggle it is the only source, and fine.)
|
|
121
|
+
|
|
122
|
+
The script is inline in `<head>`, blocking, before the stylesheet, and in `try/catch` because storage throws in some privacy modes. Skip any of the four and dark-mode users get a white flash on every load, the most noticeable bug a themed site can have. Under SSR, render the attribute from a cookie. Declare `color-scheme` per theme, or scrollbars, form controls and autofill stay light on a black page.
|
|
123
|
+
|
|
124
|
+
### 10. Values that change by context are runtime; the vocabulary is build-time
|
|
125
|
+
|
|
126
|
+
**A value that has to change with theme, user preference or container is a runtime custom property on the root; the fixed vocabulary may be build-time tokens. Anything that isn't a single colour — a gradient, a shadow stack — is runtime regardless.**
|
|
127
|
+
|
|
128
|
+
A build-time token (a Sass variable, an inlined JS constant, a static theme object) can't be re-declared by a media query, so `prefers-reduced-transparency`, `prefers-contrast: more` and the toggle can't reach it; and no token system has a slot for `linear-gradient()` or a four-layer `box-shadow`. A preference block re-declaring theme values at the same specificity goes **after** every theme block and names every theme selector.
|
|
129
|
+
|
|
130
|
+
→ The head script, persistence, two or three states, the crossfade, theme islands, runtime vs build-time per stack, preference overrides: `domains/color/theming-mechanics.md`
|
|
131
|
+
|
|
132
|
+
## Dials
|
|
133
|
+
|
|
134
|
+
| Dial | editorial | product | expressive |
|
|
135
|
+
|---|---|---|---|
|
|
136
|
+
| Palette | Near-monochrome neutral | Near-monochrome neutral plus a status palette | Brand palette used broadly, each hue placed by rule |
|
|
137
|
+
| Neutral chroma (OKLCH C) | 0–0.01 | 0–0.015, toward brand hue | 0–0.03, toward brand hue |
|
|
138
|
+
| What the accent does | Focus, and nothing else | Primary action, focus, selection, the primary data series | Brand surfaces, action, focus, selection |
|
|
139
|
+
| Brand colour already in the design | Kept where it has a job | Kept where it has a job | Kept where it has a job |
|
|
140
|
+
| Chromatic exceptions beyond accent and status | ≤ 1 | None by default; a hue family per question the reader can't answer at a glance (what, where, who), plus one-member exceptions — each listed | ≤ 4, each named |
|
|
141
|
+
| Hues that call for action | 1, the accent | 1, the accent | Brand hues, each placed by rule |
|
|
142
|
+
| Completed states | Uncoloured | `status.success` where completion is the news; uncoloured in a sequence passed through | Uncoloured, or per brand, sequences included, counted among the exceptions |
|
|
143
|
+
| Current location | Uncoloured: a marker, rule or weight | `surface.selected` (accent ≈ 25%) with icon and label in `accent.ink` | As product, or per brand |
|
|
144
|
+
| Link colour | Text colour, underlined | `accent` | `accent` or a brand hue, underlined in prose |
|
|
145
|
+
| Text tiers, `text.primary` to `text.tertiary` | 3–5 | 3 | 3–4 |
|
|
146
|
+
| Minimum step between adjacent tiers (ratio of ratios) | 1.15× | 1.4× | 1.25× |
|
|
147
|
+
| `text.tertiary` target on its worst surface | ≥ 4.6:1 | ≥ 4.8:1 | ≥ 4.6:1 |
|
|
148
|
+
| Status palette | Only if something reports state | `danger`, `warning`, `success`, `info` — required | As needed |
|
|
149
|
+
| Theme control | Light and dark, following the device; a toggle optional, two-state if added | Light and dark, following the device; a toggle optional, light, dark and system if added | One theme allowed; if two, following the device, a toggle optional, two-state if added |
|
|
150
|
+
| Dark `surface.page` lightness (OKLCH L) | 0.14–0.18 | 0.17–0.21 | 0.12–0.18 |
|
|
151
|
+
|
|
152
|
+
**Palette and neutral chroma.** Near-monochrome makes the accent and the one exception loud by default, but every state must then be carried by something other than colour (law 6). Tinted greys tie to the brand, but past about 0.015 chroma they read as colour, and a tinted `text.tertiary` beside a status foreground can look like a status.
|
|
153
|
+
|
|
154
|
+
**Exceptions and done.** An editorial page's content is prose, so its colour can be the one offer, and nothing on it is a task to finish. A product holds many kinds of record in many places by many people and is scanned, not read; in grey alone every row must be read to be sorted, which is usually what "too grey" means, and its boards exist to show what got done. So it starts from the status palette alone and adds a family, or colours done, when a question can't be answered at a glance, never because a screen looks plain.
|
|
155
|
+
|
|
156
|
+
**What the accent does.** Restricted to focus, colour *is* where the keyboard is. On primary actions it shows a working user the next step — worth it in a product, at the price of law 5's care for the ring beside accent buttons.
|
|
157
|
+
|
|
158
|
+
**Links.** Text-coloured links keep a reading page calm and leave colour for focus, but must be underlined because colour isn't carrying them. Accent links scan faster in a product, where links are navigation, not citation.
|
|
159
|
+
|
|
160
|
+
**Tiers, step and tertiary target.** More tiers give prose finer emphasis, but each must clear the floor, so every extra one shrinks the steps; a light theme rarely fits more than five. A product has fewer, more distinct tiers, told apart at a glance across a dense table, and a wider margin above 4.5:1 to absorb the surfaces the floor wasn't measured on (hover, selected, zebra rows).
|
|
161
|
+
|
|
162
|
+
**Theme control.** Following the device needs no control and no head script (law 9). A toggle, where one is wanted: two states are predictable; "system" changes the page when the OS crosses sunset, surprising someone who chose from the page's menu. Three follow the OS for those who want it, and cost a live listener.
|
|
163
|
+
|
|
164
|
+
**Dark page lightness.** Lower is more dramatic but leaves less room below the raised steps; a product's dark theme stacks panels, so its page sits higher.
|
|
165
|
+
|
|
166
|
+
## Building it for a project
|
|
167
|
+
|
|
168
|
+
1. **List the jobs.** From `roles.md`, keep the colour roles this project has and strike the rest — no dark theme, no dark column; no status, no status rows. Add project roles under their nearest parent.
|
|
169
|
+
2. **Choose the neutral:** a hue (the brand's, or none) and a chroma from the dial, in OKLCH.
|
|
170
|
+
3. **Fix the ends, per theme:** `surface.page` and `text.primary` near-white and near-black in light, the dial's dark lightness and a light grey in dark.
|
|
171
|
+
4. **Place surfaces and lines** (`surface.raised`, `surface.hover`, `surface.pressed`, `surface.subtle`, `border.default`, `border.hover`) as small steps down from the page in light (or `surface.raised` a step up, white on a grey page), up in dark, spaced by contrast against it; what must be seen (an input's edge, an icon, the focus ring) clears 3:1.
|
|
172
|
+
5. **Solve the text tiers from contrast targets** (tertiary at the dial's target, the rest at least the minimum step apart) on each tier's worst surface; verify per theme with `contrast.mjs --ladder`.
|
|
173
|
+
6. **Derive the accent from the brand hue**, per theme: `text.on-accent` at 4.5:1 and `focus` at 3:1 on every surface; then its hover and `surface.selected` tint (about 25% of the accent over the surface; translucent, so it re-tints with the theme), with `accent.ink` at 4.5:1 on that tint, re-measuring the lowest text tier on it too.
|
|
174
|
+
7. **Status pairs**, if the preset needs them; **exceptions and hue families**, each passing law 7, every mark at 3:1 on its grounds.
|
|
175
|
+
8. **The translucent set**: black at low alpha in light, white in dark, for `border.layered` and any layered washes.
|
|
176
|
+
9. **Write it** as custom properties, one block per theme, with the framework's tokens pointing at them (law 10), and record the ratios in `DESIGN.md`.
|
|
177
|
+
|
|
178
|
+
→ The contrast formula, surface spacing, the brand ramp, per-preset targets, and *Harbour*, a worked `product` example with a teal hue, measured on its worst surfaces: `domains/color/deriving-a-ramp.md`
|
|
179
|
+
|
|
180
|
+
## Auditing existing code
|
|
181
|
+
|
|
182
|
+
Collect evidence, then measure. The greps assume CSS, Tailwind or CSS-in-JS; adapt them.
|
|
183
|
+
|
|
184
|
+
| Look for | Grep for | Law |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| Colour literals outside token files and artwork | `#[0-9a-fA-F]{3,8}\b`, `rgba?\(`, `hsla?\(`, `oklch\(` | 1 |
|
|
187
|
+
| The same job at several values | every value used for borders; for captions and meta | 1 |
|
|
188
|
+
| Mid-ramp steps used as text | `text-(gray\|neutral\|slate\|zinc)-(300\|400\|500)`, or the project's equivalent | 2 |
|
|
189
|
+
| Icons on a text-primary tier by default | icons with the top text colour at rest | 2 |
|
|
190
|
+
| Solid borders on translucent surfaces | `backdrop-filter` elements, then their border values | 4 |
|
|
191
|
+
| Lost focus | `outline:\s*(none\|0)`, `:focus\b` without `-visible`, `ring-0` | 5 |
|
|
192
|
+
| Colour-only status | error or success styling with no icon or message; nav active by colour alone | 6 |
|
|
193
|
+
| Uncounted colour | each hue outside accent, focus and status, and its job; third-party marks and `role="img"` illustration aside | 7 |
|
|
194
|
+
| Greyed brand (when comparing to the original) | a brand colour or gradient the original spent on selection, a data series, a meter, a mark or avatars, now grey or muted with no law cited | 7 |
|
|
195
|
+
| A second action hue; done coloured against the dial | two hues on buttons, links or "needs you" markers; success colour on passed steps, checks or the current nav item | 7 |
|
|
196
|
+
| A hue doing two jobs | a type or area hue on state or text; danger red on a live or recording state; status colour on a plain count or a zero | 7 |
|
|
197
|
+
| Reused accents and half-moved gradients | some gradient stops re-declared per theme, not all; single-value accents | 8 |
|
|
198
|
+
| Faint marks | type, area and person hues shared across themes; each measured at `--min 3` on panel, frame and hover | 8 |
|
|
199
|
+
| Pure black | `#000\b`, `#000000`, `black` on surfaces | 8 |
|
|
200
|
+
| Two theme mechanisms | `prefers-color-scheme` in CSS alongside a toggle; missing `color-scheme` | 9 |
|
|
201
|
+
| Flash of wrong theme | theme set in a component effect, not an inline head script | 9 |
|
|
202
|
+
| Compiled context values | build-time tints or gradients a preference query must replace | 10 |
|
|
203
|
+
|
|
204
|
+
Then measure, in every theme: each text tier on its worst surface (`contrast.mjs --ladder`), the focus colour at `--min 3` on every surface it can land on, each status foreground on the page and its tint.
|
|
205
|
+
|
|
206
|
+
Report each finding as one line in `DESIGN.md` `## Open issues`, with evidence — a ratio, a count, a file: "`text.tertiary` (`--ink-3`) is 4.21:1 on `surface.hover` in light — fails 4.5:1. Used in 22 files." Don't fix what wasn't asked for.
|
|
207
|
+
|
|
208
|
+
## Writing back to DESIGN.md
|
|
209
|
+
|
|
210
|
+
- **The ratios.** Every text tier and the focus colour, per theme, with the surface measured on; re-measure when a value in `## Colour` changes.
|
|
211
|
+
- **New roles.** Any you added — a missing `surface.selected`, a tier between secondary and tertiary — in the project's naming style, marked `proposed` until accepted.
|
|
212
|
+
- **Exceptions.** Each `exception.<name>` under **Exceptions**: what it's for, and why that reason can't spread; a family with its question, its members and the marks it may take.
|
|
213
|
+
- **Moved dials.** Under `## Decisions`, dated, with the reason: "Links in `accent`, not text colour: the docs area is navigation-heavy and readers scan for links."
|
|
214
|
+
- **Open issues.** As in the audit; remove the line once fixed.
|
|
215
|
+
|
|
216
|
+
## Checklist
|
|
217
|
+
|
|
218
|
+
- [ ] Every colour is a job-named token; literals only in artwork and shared multi-layer constants.
|
|
219
|
+
- [ ] Components use roles (`accent`, its hover, `surface.selected`), not brand-hue steps.
|
|
220
|
+
- [ ] `text.primary` through `text.tertiary` are their own tokens, never border or fill steps.
|
|
221
|
+
- [ ] Every tier clears 4.5:1 on its worst surface, per theme, computed with `contrast.mjs`.
|
|
222
|
+
- [ ] The text ladder strictly descends in every theme.
|
|
223
|
+
- [ ] Icons take their label's tier or one below; `mark` is only for decoration without text.
|
|
224
|
+
- [ ] Control edges and icons that must be seen clear 3:1.
|
|
225
|
+
- [ ] Anything layered is translucent (`border.layered` and kin), inverting per theme.
|
|
226
|
+
- [ ] `focus` clears 3:1 on every surface it can land on, uses `:focus-visible` with an offset, never sits beside its own colour.
|
|
227
|
+
- [ ] No state or status is signalled by colour alone.
|
|
228
|
+
- [ ] Each `status.*` is a fg/bg pair per theme, fg at 4.5:1 on the page and its tint.
|
|
229
|
+
- [ ] Each `exception.<name>` has a written reason no other element shares; only its mark is coloured.
|
|
230
|
+
- [ ] Each hue family answers one question (what, how, where, who) and never another's.
|
|
231
|
+
- [ ] One hue calls for action; the reader's current location follows the dial (the accent tint under product and expressive, uncoloured under editorial); done is coloured only where the dial says completion is the news.
|
|
232
|
+
- [ ] Accents, status pairs, exceptions and family hues are re-picked per theme; every gradient stop moves; marks clear 3:1 on every ground.
|
|
233
|
+
- [ ] Dark `surface.page` and light `text.primary` are near-black, not `#000`.
|
|
234
|
+
- [ ] With a toggle, the theme is a root attribute or class set by a blocking inline script before the stylesheet.
|
|
235
|
+
- [ ] `color-scheme` is declared in every theme.
|
|
236
|
+
- [ ] Context-dependent and multi-layer values are runtime custom properties; preference overrides follow every theme block.
|
|
237
|
+
- [ ] Every brand colour and gradient the original spent on a job is still there, or the law that removed it is named.
|
|
238
|
+
- [ ] Product and expressive: the current nav item is on the accent tint, with its label at 4.5:1 in `accent.ink`; the primary chart series is the accent at full strength.
|
|
239
|
+
- [ ] Dial check: palette, accent's jobs and link colour match the preset.
|
|
240
|
+
- [ ] Dial check: exception count and done colour match the preset.
|
|
241
|
+
- [ ] Dial check: tier count, minimum step and tertiary target match the preset.
|
|
242
|
+
- [ ] Dial check: status palette present if the preset requires it.
|
|
243
|
+
- [ ] Dial check: theme control and dark page lightness match, or `## Decisions` says why not.
|