@duro-app/cli 5.15.0 → 5.17.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.
Files changed (77) hide show
  1. package/README.md +32 -11
  2. package/dist/bin.js +13 -5
  3. package/dist/bin.js.map +1 -1
  4. package/dist/commands/doctor.d.ts +7 -1
  5. package/dist/commands/doctor.d.ts.map +1 -1
  6. package/dist/commands/doctor.js +25 -12
  7. package/dist/commands/doctor.js.map +1 -1
  8. package/dist/commands/hook.d.ts.map +1 -1
  9. package/dist/commands/hook.js +11 -2
  10. package/dist/commands/hook.js.map +1 -1
  11. package/dist/commands/list.d.ts.map +1 -1
  12. package/dist/commands/list.js +23 -2
  13. package/dist/commands/list.js.map +1 -1
  14. package/dist/commands/lookup.d.ts.map +1 -1
  15. package/dist/commands/lookup.js +15 -1
  16. package/dist/commands/lookup.js.map +1 -1
  17. package/dist/commands/mcp.d.ts +57 -0
  18. package/dist/commands/mcp.d.ts.map +1 -1
  19. package/dist/commands/mcp.js +87 -37
  20. package/dist/commands/mcp.js.map +1 -1
  21. package/dist/commands/mockup.d.ts +33 -5
  22. package/dist/commands/mockup.d.ts.map +1 -1
  23. package/dist/commands/mockup.js +167 -13
  24. package/dist/commands/mockup.js.map +1 -1
  25. package/dist/commands/skill.d.ts +6 -0
  26. package/dist/commands/skill.d.ts.map +1 -1
  27. package/dist/commands/skill.js +168 -24
  28. package/dist/commands/skill.js.map +1 -1
  29. package/dist/format.d.ts.map +1 -1
  30. package/dist/format.js +7 -0
  31. package/dist/format.js.map +1 -1
  32. package/dist/hook-script.d.ts +2 -2
  33. package/dist/hook-script.d.ts.map +1 -1
  34. package/dist/hook-script.js +1 -1
  35. package/dist/manifest.d.ts +2 -0
  36. package/dist/manifest.d.ts.map +1 -1
  37. package/dist/manifest.js +28 -10
  38. package/dist/manifest.js.map +1 -1
  39. package/dist/registry-types.d.ts +5 -0
  40. package/dist/registry-types.d.ts.map +1 -1
  41. package/dist/registry.d.ts +20 -0
  42. package/dist/registry.d.ts.map +1 -1
  43. package/dist/registry.js +38 -0
  44. package/dist/registry.js.map +1 -1
  45. package/dist/skill-template.d.ts +1 -1
  46. package/dist/skill-template.d.ts.map +1 -1
  47. package/dist/skill-template.js +20 -9
  48. package/dist/skill-template.js.map +1 -1
  49. package/package.json +6 -5
  50. package/registry.json +35 -0
  51. package/{LICENSE → skills/colors/LICENSE} +1 -1
  52. package/skills/colors/NOTICE.md +36 -0
  53. package/skills/colors/SKILL.md +99 -0
  54. package/skills/colors/color-formats.md +90 -0
  55. package/skills/colors/color-usage.md +109 -0
  56. package/skills/colors/contrast.md +70 -0
  57. package/skills/colors/palette-generation.md +108 -0
  58. package/skills/colors/palette-structure.md +72 -0
  59. package/skills/colors/token-naming.md +99 -0
  60. package/skills/typography/LICENSE +21 -0
  61. package/skills/typography/NOTICE.md +36 -0
  62. package/skills/typography/SKILL.md +169 -0
  63. package/skills/typography/choosing-fonts.md +53 -0
  64. package/skills/typography/css-cheat-sheet.md +67 -0
  65. package/skills/typography/details-and-accessibility.md +84 -0
  66. package/skills/typography/spacing-and-sizing.md +87 -0
  67. package/skills/typography/variable-fonts-and-opentype.md +100 -0
  68. package/skills/typography/wrapping-and-punctuation.md +23 -0
  69. package/skills/ui/LICENSE +21 -0
  70. package/skills/ui/NOTICE.md +36 -0
  71. package/skills/ui/SKILL.md +139 -0
  72. package/skills/ui/animations.md +170 -0
  73. package/skills/ui/enter-exit.md +157 -0
  74. package/skills/ui/icon-transitions.md +72 -0
  75. package/skills/ui/icons.md +105 -0
  76. package/skills/ui/performance.md +63 -0
  77. package/skills/ui/surfaces.md +166 -0
@@ -0,0 +1,109 @@
1
+ # Color usage
2
+
3
+ Deploying color once the system exists: meaning, emphasis, gradients and appearance variants. For picking values see [palette-generation.md](palette-generation.md), for naming them [token-naming.md](token-naming.md), for checking pairs [contrast.md](contrast.md).
4
+
5
+ ## One color, one meaning
6
+
7
+ Users read a near-miss in hue as a slightly different shade, not as a different color.
8
+
9
+ ```css
10
+ /* Bad: the accent means both "link" and "decorative heading" */
11
+ a { color: #3b82f6; }
12
+ .section-title { color: #4f8ef7; }
13
+
14
+ /* Good: interactive elements own the accent; headings stay neutral */
15
+ a { color: var(--color-accent-text); }
16
+ .section-title { color: var(--color-text-primary); }
17
+ ```
18
+
19
+ The rule runs both ways. A color must not be *absent* where its meaning occurs. If the accent means interactive, an interactive element rendered neutral is just as misleading.
20
+
21
+ ## Use tokens in their role
22
+
23
+ A value that happens to work in two roles stops working in both at the next theme change.
24
+
25
+ ```css
26
+ /* Bad: separator token repurposed as a text color because it looked right */
27
+ .caption { color: var(--color-border); }
28
+
29
+ /* Bad: text token repurposed as a background */
30
+ .tag { background: var(--color-text-secondary); }
31
+ ```
32
+
33
+ The role inventory in [token-naming.md](token-naming.md) is the list of roles a system needs.
34
+
35
+ ## One colored action per view
36
+
37
+ ```html
38
+ <!-- Good: one filled primary action, neutral secondaries -->
39
+ <button class="bg-accent-solid text-text-on-accent">Save</button>
40
+ <button class="text-text-secondary">Cancel</button>
41
+
42
+ <!-- Bad: every action colored, so nothing is primary -->
43
+ <button class="bg-accent-solid text-text-on-accent">Save</button>
44
+ <button class="bg-accent-solid text-text-on-accent">Duplicate</button>
45
+ <button class="bg-accent-solid text-text-on-accent">Export</button>
46
+ ```
47
+
48
+ Selected states may use the accent on the glyph and label. An active tab or a checked segment is state, not emphasis.
49
+
50
+ ## Gradients
51
+
52
+ **The interpolation space is a look, not a correctness setting.** Three are worth knowing, and the difference between them is most visible in the middle of the gradient:
53
+
54
+ ```css
55
+ /* sRGB: the default and the classic. Midpoint darkens and mutes. */
56
+ background: linear-gradient(#3b82f6, #ec4899);
57
+
58
+ /* oklab: even brightness across the transition. The best default. */
59
+ background: linear-gradient(in oklab, #3b82f6, #ec4899);
60
+
61
+ /* oklch: travels around the hue wheel, staying vivid throughout. */
62
+ background: linear-gradient(in oklch, #3b82f6, #ec4899);
63
+ ```
64
+
65
+ `oklab` and sRGB are **rectangular**, interpolating in a straight line through the color space. `oklch` is **polar**, interpolating the hue angle, so it arcs around the wheel through every hue between the stops. That is why it stays saturated and why it can produce hues nobody asked for. A blue-to-pink gradient routes through purple, which is either the look or a surprise.
66
+
67
+ **The gray dead zone is a rectangular-space problem.** Two hues on opposite sides of the wheel sit either side of the neutral axis. A straight line between them passes near gray, and the middle goes lifeless. Either switch to a polar space, which routes around the axis, or add a third stop between the two and keep the space you have.
68
+
69
+ With a polar space you also control which way it goes around:
70
+
71
+ ```css
72
+ /* The short way round, usually what you want */
73
+ background: linear-gradient(in oklch shorter hue, #3b82f6, #ec4899);
74
+
75
+ /* The long way, sweeps most of the spectrum */
76
+ background: linear-gradient(in oklch longer hue, #3b82f6, #ec4899);
77
+ ```
78
+
79
+ **Banding shows up on large areas.** A gradient spanning a hero with little contrast between its stops steps visibly on 8-bit displays. Widen the contrast, shrink the area or overlay a subtle noise texture.
80
+
81
+ **Keep text off gradients where you can.** Contrast varies continuously across one, so a single measurement does not describe it. Where text must sit on a gradient, measure the worst region rather than the average, or put a scrim behind it.
82
+
83
+ ## Color across cultures
84
+
85
+ Where a color carries meaning in finance, status or alerts, verify the meaning holds in every locale you ship to. Chinese financial UIs show gains in red and losses in green, the reverse of English ones. Where the product ships to such markets, make gain and loss per-locale tokens rather than hardcoded values.
86
+
87
+ ## Light, dark and increased contrast
88
+
89
+ When the product ships a dark appearance, every custom color needs a dark variant, derived per [palette-generation.md](palette-generation.md). Users who enable increased contrast expect visibly stronger differentiation in each appearance:
90
+
91
+ ```css
92
+ :root {
93
+ --color-accent-solid: var(--blue-600);
94
+ }
95
+
96
+ @media (prefers-contrast: more) {
97
+ :root { --color-accent-solid: var(--blue-900); }
98
+ }
99
+
100
+ @media (prefers-color-scheme: dark) {
101
+ :root { --color-accent-solid: var(--blue-400); }
102
+ }
103
+
104
+ @media (prefers-color-scheme: dark) and (prefers-contrast: more) {
105
+ :root { --color-accent-solid: var(--blue-200); }
106
+ }
107
+ ```
108
+
109
+ A single `prefers-contrast` block shared by both appearances darkens the color on a dark background too, lowering contrast there. The increased-contrast variant widens the gap to its background by at least `0.15` of OKLCH `L` over the default. Remeasure it, aiming for Lc 90 on body text. Widening the gap without remeasuring is not fixing it.
@@ -0,0 +1,70 @@
1
+ # Contrast
2
+
3
+ Contrast is measured between a **foreground color**, meaning text, an icon or a UI element, and the **background color** it actually renders against, usually the nearest ancestor that paints one. Identify that background first. Measuring against the page background when the element sits on a card gives the wrong answer.
4
+
5
+ `accessibility` decides which requirement applies to a pair. The thresholds below are what a measured pair is reported against. Where neither that skill nor the project names a standard, report the WCAG 2 result and add the APCA value.
6
+
7
+ ## WCAG 2 thresholds
8
+
9
+ WCAG 2 is the standard formal conformance claims are made against.
10
+
11
+ | Content type | AA | AAA |
12
+ | --- | --- | --- |
13
+ | Text | 4.5:1 | 7:1 |
14
+ | Large text, at least `24px` or `18.67px` bold | 3:1 | 4.5:1 |
15
+ | UI components and graphical objects | 3:1 | n/a |
16
+
17
+ Disabled or inactive controls and logos are exempt.
18
+
19
+ ## APCA thresholds
20
+
21
+ Lc, Lightness Contrast, is APCA's measure of perceived contrast. These levels simplify APCA's full font-size and weight lookup table:
22
+
23
+ | Content type | Minimum |
24
+ | --- | --- |
25
+ | Body text, columns or blocks of text | Lc 75, preferred Lc 90 |
26
+ | Non-body text such as labels and headlines | Lc 60 |
27
+ | Large text, at least `36px` regular or `24px` bold | Lc 45 |
28
+ | Fine-detail or outline icons | Lc 45 |
29
+ | Solid icons and UI components | Lc 30 |
30
+ | Non-text that only has to be discernible, such as dividers | Lc 15 |
31
+
32
+ Disabled and placeholder text are not covered by a requirement. Aim for Lc 30 so they stay legible.
33
+
34
+ Lc is signed. Positive means dark text on a light background, negative means light text on a dark background. Compare the absolute value against the threshold.
35
+
36
+ ## Fix a failing pair only on request
37
+
38
+ **Change lightness first.** It is the channel contrast responds to. Hue and saturation move the measured value far less.
39
+
40
+ Move the foreground away from the background in perceived lightness, holding hue and saturation, then remeasure. Keeping hue fixed is what stops a contrast fix becoming a palette change.
41
+
42
+ ```css
43
+ /* Failing: text too close to its background in lightness (Lc ≈ 50) */
44
+ color: #7d93b0;
45
+ background: #eef2f7;
46
+
47
+ /* Fixed: darker text, same hue (Lc ≈ 88) */
48
+ color: #2b3a4f;
49
+ background: #eef2f7;
50
+ ```
51
+
52
+ Two constraints on the fix:
53
+
54
+ - **Mid-lightness backgrounds cap what is achievable.** On a background near OKLCH `L` `0.75`, even pure black text reaches only about Lc 60. Body text needs a background near one extreme, so a mid-range background is the thing that has to change.
55
+ - **Pushing lightness can push the color out of gamut.** Reduce saturation as needed to keep it renderable. See [color-formats.md](color-formats.md).
56
+
57
+ Always remeasure after changing a value. Do not assume a fix landed.
58
+
59
+ ## Pick text polarity from the background
60
+
61
+ Compare black and white using the project's required contrast metric. APCA and WCAG 2 can favor different text colors on the same background. A higher score does not by itself mean the pair passes.
62
+
63
+ WCAG 2's neutral crossover is near 56% OKLCH lightness. At `oklch(65% 0 0)`, black measures about `6.49:1` and white `3.23:1`. Only black passes AA for normal text. For colored backgrounds or other foregrounds, measure the actual pair rather than applying a crossover.
64
+
65
+ ## What to check
66
+
67
+ - **Every pair, in every appearance.** APCA scores mirrored pairs differently, so a pair passing in light mode can fail in dark.
68
+ - **Translucent surfaces.** A color on a `backdrop-filter` header or an overlay shifts with whatever scrolls behind it. Test against the lightest and darkest content it can sit over, or make the surface opaque enough that the shift cannot break the pair.
69
+ - **Computed colors.** `color-mix()`, relative color syntax and opacity modifiers resolve at render time; measure the rendered result, not the declaration.
70
+ - **Text over images.** There is no single background color. Measure the worst region, or guarantee one with a scrim.
@@ -0,0 +1,108 @@
1
+ # Palette generation
2
+
3
+ Producing the values once the structure is decided. For which ramps to build and what each step is for, see [palette-structure.md](palette-structure.md).
4
+
5
+ ## Start from the brand color
6
+
7
+ A brand color arrives as one value, usually a hex. Two decisions come before any ramp exists:
8
+
9
+ **Which step does it occupy?** A brand color meant for buttons and links belongs on the solid-fill step, `600` in Tailwind and `9` in Radix. Then the primary button renders the actual brand color, not an approximation.
10
+
11
+ **Is it pinned or snapped?** Pin a contractually fixed brand color. It stays exact, the ramp builds outward from it and that one step spaces slightly unevenly. Otherwise snap it onto the ramp so the spacing stays regular, which almost always looks better.
12
+
13
+ A brand color that fails contrast behind white text is still the brand color, just not the solid-fill step. Put it where it lands and use a darker step for interactive fills. Never quietly darken the brand. In the example below, `#3b82f6` lands on `500` and fills use `600`, because white on `#3b82f6` measures 3.68:1.
14
+
15
+ ## What a correct ramp looks like
16
+
17
+ Properties of the finished ramp, checkable against any output in any notation:
18
+
19
+ - **Lightness changes steadily, measured in OKLCH `L`.** HSL lightness is not perceptual, and evenly spaced HSL values bunch at one end.
20
+ - **Hue is constant end to end.** Every step is recognisably the same color. A wandering hue reads as two colors blended and will not sit correctly against a neutral built on a different hue.
21
+ - **Vividness peaks in the middle and falls off at both ends.** The lightest and darkest steps are nearly neutral; the middle carries the color. Holding full vividness into the extremes gives a `50` that glows and a `950` like ink spilled on the brand.
22
+ - **Steps are finer at the light end.** Light backgrounds need finer distinctions than dark ones. Keep `50`–`200` about `0.04`–`0.05` apart in OKLCH `L` and mid-ramp steps about `0.07`–`0.10`. Even spacing across the whole range leaves too few pale steps, so `100` is already too dark for a subtle surface.
23
+ - **No two adjacent steps are indistinguishable.** Adjacent steps less than `0.03` apart in OKLCH `L` mean the ramp has more steps than decisions. Drop one.
24
+ - **Both ends stop short of pure black and white.** A ramp that reaches them loses its identity exactly where the page background lives.
25
+
26
+ ## Use a color library
27
+
28
+ Never compute these by hand or by eye. `culori`, `colorjs.io` and `chroma.js` all convert between notations, measure perceived lightness and interpolate perceptually. Read the brand color in whatever format it arrives, do the math in a perceptual space and emit the project's notation:
29
+
30
+ ```js
31
+ import { formatHex, interpolate, samples } from 'culori'
32
+
33
+ // Perceptual interpolation, hex in and hex out.
34
+ const ramp = interpolate(['#eff6ff', '#3b82f6', '#172554'], 'oklab')
35
+ const steps = samples(11).map((t) => formatHex(ramp(t)))
36
+ ```
37
+
38
+ This is a starting point. Linear sampling spaces lightness evenly within each segment, so check the output against the properties above and tighten `50`–`200` by hand.
39
+
40
+ The output format is the project's choice. For a ramp the interpolation space is not, because sRGB interpolation produces muddy mid-steps. Decorative gradients are the opposite case, where the space is a deliberate look ([color-usage.md](color-usage.md)).
41
+
42
+ For comparison, Tailwind's blue ramp has the finer light end the generated one lacks:
43
+
44
+ ```css
45
+ :root {
46
+ --blue-50: #eff6ff;
47
+ --blue-100: #dbeafe;
48
+ --blue-200: #bfdbfe;
49
+ --blue-300: #93c5fd;
50
+ --blue-400: #60a5fa;
51
+ --blue-500: #3b82f6;
52
+ --blue-600: #2563eb;
53
+ --blue-700: #1d4ed8;
54
+ --blue-800: #1e40af;
55
+ --blue-900: #1e3a8a;
56
+ --blue-950: #172554;
57
+ }
58
+ ```
59
+
60
+ ## Several hues at once
61
+
62
+ With an accent plus status ramps, the ramps must agree step for step. `red-500` and `blue-500` should read as equally bright and vivid, or a red button looks heavier than a blue one at the same step.
63
+
64
+ - **Match perceived lightness exactly.** Same step, same brightness, across every hue.
65
+ - **Match vividness relatively, not absolutely.** Hues do not share a maximum vividness. A saturated yellow and a saturated blue are not equally far from gray, and no format makes them so. Set each ramp to the same *proportion* of what its own hue reaches.
66
+
67
+ Yellows and cyans are the usual casualties, peaking much lower than reds and blues. Copy the numbers across and the warning color looks weak beside the danger one.
68
+
69
+ ## Dark mode
70
+
71
+ A dark palette is not the light one reversed. Reversal is the starting point, not the output.
72
+
73
+ Swap the semantic roles first, then tune the values:
74
+
75
+ ```css
76
+ :root {
77
+ --color-bg-page: var(--neutral-50);
78
+ --color-text-primary: var(--neutral-950);
79
+ }
80
+
81
+ .dark {
82
+ --color-bg-page: var(--neutral-950);
83
+ --color-text-primary: var(--neutral-50);
84
+ }
85
+ ```
86
+
87
+ Three things almost always need hand-tuning after the swap:
88
+
89
+ - **Vividness comes down.** A color that reads as confident on white reads as neon on near-black. Dark appearances need the accent a step or two less vivid.
90
+ - **The dark end needs more separation.** Steps distinguishable as pale backgrounds collapse into each other as dark surfaces.
91
+ - **Contrast does not survive the mirror.** APCA scores mirrored pairs differently, so recheck every pair in both appearances ([contrast.md](contrast.md)).
92
+
93
+ ### Choosing the switching mechanism
94
+
95
+ Use one mechanism throughout:
96
+
97
+ - **`prefers-color-scheme` alone** is correct with no theme toggle. Nothing to persist, nothing to hydrate.
98
+ - **A `.dark` class** is required as soon as users can override the system setting. The media query then sets only the initial value.
99
+ - **`light-dark()`** collapses both values into one declaration. It needs `color-scheme: light dark` and reads that property rather than a class, so a class-based toggle must set `color-scheme` too.
100
+
101
+ ```css
102
+ :root {
103
+ color-scheme: light dark;
104
+ --color-bg: light-dark(#ffffff, #172554);
105
+ }
106
+ ```
107
+
108
+ A media query setting some tokens and a class setting others gives a half-themed interface the moment a user overrides their system preference.
@@ -0,0 +1,72 @@
1
+ # Palette structure
2
+
3
+ What a color system is made of, before any values exist. For computing the values see [palette-generation.md](palette-generation.md); for what to call them see [token-naming.md](token-naming.md).
4
+
5
+ ## What a system needs
6
+
7
+ Most products need three kinds of ramp and nothing else:
8
+
9
+ | Ramp | How many | Notes |
10
+ | --- | --- | --- |
11
+ | Neutral | 1 | Carries 80–90% of the interface: backgrounds, borders, body text |
12
+ | Accent | 1 | The brand hue. Interactive and selected states |
13
+ | Status | 0–4 | `danger`, `warning`, `success`, `info`. Add one only when the product shows that state |
14
+
15
+ ## Every step has a job
16
+
17
+ Each step maps to a role. Generate the steps the roles below call for and skip the rest.
18
+
19
+ | Role | Tailwind | Radix |
20
+ | --- | --- | --- |
21
+ | Page background | `50` | `1` |
22
+ | Subtle background | `50` | `2` |
23
+ | Component background | `100` | `3` |
24
+ | Component hover | `200` | `4` |
25
+ | Component active / selected | `200` | `5` |
26
+ | Subtle border, separator | `200` | `6` |
27
+ | Border, focus ring | `300` | `7` |
28
+ | Hovered border | `400` | `8` |
29
+ | Solid fill | `600` | `9` |
30
+ | Solid fill hover | `700` | `10` |
31
+ | Low-contrast text | `700` | `11` |
32
+ | High-contrast text | `900` | `12` |
33
+
34
+ The two conventions differ in kind, not only in numbering:
35
+
36
+ - **Radix defines its 12 steps by role.** Step 9 is "the solid fill" in every ramp and appearance. The dark scale is a separate ramp reusing the same numbers, so `--accent-9` is the fill in both and component CSS never changes.
37
+ - **Tailwind defines its 11 steps by lightness.** `50` is light, `950` is dark. The mapping above therefore holds in light mode and inverts in dark, with the page background at `950` and high-contrast text at `50`. Components either swap step numbers per appearance or read a semantic token that swaps once.
38
+
39
+ Match whichever the project uses. For a new system prefer Radix's model, because a role-defined step survives a theme change that a lightness-defined step does not. On Tailwind, keep `50`–`950` and put the role mapping in the semantic tier.
40
+
41
+ Solid fill sits on `600` because white text on most Tailwind `500` hues fails 4.5:1. Where the table repeats a step, those roles render identically. A design that needs, say, component hover and subtle border to differ needs a 12-step ramp.
42
+
43
+ ## Neutrals
44
+
45
+ A pure gray ramp is a perfectly good default. It sits under any accent hue without competing and never needs revisiting when the brand color changes.
46
+
47
+ Tinting the neutral toward the accent hue is a stylistic option, not a correction. OKLCH chroma `0.005`–`0.02` at the accent's hue puts the grays in the accent's family. Enough to measure, not enough to name.
48
+
49
+ Warm neutrals, hue toward orange, read approachable and editorial; cool ones, toward blue, read technical and precise. Whichever you pick, including none, hold it across the whole ramp. A warm gray border on a cool gray background is visible even when neither color is nameable alone.
50
+
51
+ Neutrals carry the most roles, so they need the most steps. Never generate fewer neutral steps than accent steps.
52
+
53
+ ## Status colors
54
+
55
+ Convention constrains status hues before taste does. Red reads as danger, amber as warning, green as success. See the cultural exceptions in [color-usage.md](color-usage.md).
56
+
57
+ Two rules govern them:
58
+
59
+ - **Keep every status hue at least `15°` of OKLCH hue from the accent.** If the brand is red, danger cannot also be red. Where convention leaves nowhere to move, give the destructive action a distinct treatment, such as an outlined button with an icon, or it reads as the primary action.
60
+ - **Status ramps need fewer steps than the accent.** Most render four roles: a background, a border, a solid fill and text. Generate the full ramp only where the product styles status components across the whole range.
61
+
62
+ ## Auditing an existing palette
63
+
64
+ Before restructuring a system, inventory it. Most codebases hold several times more colors than the design has decisions.
65
+
66
+ 1. **Collect every literal.** Grep for hex, `rgb(`, `hsl(`, `oklch(` and the project's utility-class prefixes. Include SVG `fill` and `stroke`, chart configs and email templates.
67
+ 2. **Sort by perceived lightness within each hue family.** Duplicates surface immediately as near-identical neighbors.
68
+ 3. **Collapse near-duplicates.** Two colors within `0.02` of each other in OKLab ΔE are one color that drifted. Keep the one used most and retire the others. Never average them.
69
+ 4. **Assign each survivor a role** from the table above. A color matching no role is a missing token or a mistake. Decide which, and say so in the finding.
70
+ 5. **Count what is left.** More ramps than **What a system needs** lists means the palette outgrew its structure, not that the product needs more color.
71
+
72
+ Report the inventory before changing anything. Consolidating a palette changes rendered output on screens nobody asked you to touch, so it stays a proposal until the user accepts it.
@@ -0,0 +1,99 @@
1
+ # Token naming
2
+
3
+ Naming is what makes a palette usable by anyone who did not build it. For which ramps exist and what each step does, see [palette-structure.md](palette-structure.md).
4
+
5
+ ## Two tiers
6
+
7
+ **Primitives** name a value. They are the ramp, named by hue and step, as in `--blue-600` or `--neutral-200`. A primitive describes what the color *is*, so it never changes meaning between themes and is never applied directly in a component.
8
+
9
+ **Semantics** name a job. They point at a primitive and take the name of the role they fill, as in `--color-text-secondary` or `--color-border-subtle`. Components only ever reference this tier.
10
+
11
+ ```css
12
+ :root {
13
+ /* Tier 1: primitives, named by hue and step. Never used directly. */
14
+ --blue-600: #2563eb;
15
+ --neutral-200: #e5e7eb;
16
+ --neutral-700: #374151;
17
+
18
+ /* Tier 2: semantics, named by role. This is what components use. */
19
+ --color-accent-solid: var(--blue-600);
20
+ --color-border: var(--neutral-200);
21
+ --color-text-secondary: var(--neutral-700);
22
+ }
23
+ ```
24
+
25
+ The tiering is what makes theming possible. Dark mode, a white-label theme and an increased-contrast variant all repoint the semantic tier, leaving the primitives and every component untouched. A codebase applying `--blue-600` directly in components has no theming seam. Adding one later means auditing every usage to work out which meant "the accent" and which just wanted blue.
26
+
27
+ Add a third, component-level tier (`--color-button-danger-bg`) only where a component genuinely and intentionally diverges from the system. One component token is a documented exception; twenty mean the semantic tier is missing roles.
28
+
29
+ ## The role inventory
30
+
31
+ A system is complete when every role below has a token. Build against this list rather than adding tokens as components demand them, or the palette ends up shaped like whichever screen came first.
32
+
33
+ | Group | Roles |
34
+ | --- | --- |
35
+ | Surfaces | page background, surface, raised (menus, popovers), sunken (inputs, wells), overlay scrim |
36
+ | Text | primary, secondary, disabled, inverse, on-accent |
37
+ | Borders | subtle, default, strong, focus ring, separator |
38
+ | Accent | subtle background, border, solid, solid hover, text |
39
+ | Status | per status shipped: subtle background, border, solid, text |
40
+
41
+ Separator and border are separate roles even when they share a value today. A separator divides content; a border encloses a control. They diverge the first time someone restyles inputs, and a system that conflated them gets untangled at that moment.
42
+
43
+ ## Naming grammar
44
+
45
+ Use one shape and never deviate. Neutral roles lead with the property, as `--color-{bg|text|border}-{variant}-{state}`. Accent and status roles lead with the role, as `--color-{accent|danger|…}-{subtle|border|solid|text}-{state}`.
46
+
47
+ ```css
48
+ --color-bg-surface
49
+ --color-text-secondary
50
+ --color-border-strong
51
+ --color-accent-solid-hover
52
+ --color-danger-subtle
53
+ --color-accent-text
54
+ ```
55
+
56
+ Pick one word per concept and use only that word. Consistency matters more than the vocabulary. A reader who has seen `--color-text-primary` must be able to guess `--color-text-disabled` without looking:
57
+
58
+ | Concept | Pick one | Never mix in |
59
+ | --- | --- | --- |
60
+ | Foreground | `text` | `fg`, `foreground`, `content`, `ink` |
61
+ | Background | `bg` | `background`, `surface` as a synonym, `fill` |
62
+ | Edge | `border` | `stroke`, `outline`, `line` |
63
+ | Brand color | `accent` | `primary`, `brand`, `theme` used interchangeably |
64
+
65
+ Reserve `primary` for exactly one meaning. `--color-text-primary` for body text beside `--color-primary` for the brand is the most common naming collision there is, and it makes every `primary` token ambiguous until you open its definition. Use `accent` for the brand and let `primary` mean "the most prominent of its group".
66
+
67
+ ## Anti-patterns
68
+
69
+ | Name | Problem | Instead |
70
+ | --- | --- | --- |
71
+ | `--color-blue-button` | Appearance at the semantic tier; lies the moment the brand changes | `--color-accent-solid` |
72
+ | `--color-sidebar-gray` | Named for where it was used first; the second usage makes it nonsense | `--color-bg-surface` |
73
+ | `--color-light-gray` | Lies in dark mode, where it is the dark one | `--neutral-200` as a primitive |
74
+ | `--color-text-2` | Numbered semantics carry no meaning; nobody can guess what `3` would be | `--color-text-secondary` |
75
+ | `--color-gray-hover` | Mixes a hue with a state and belongs to no tier | `--color-bg-surface-hover` |
76
+ | `--blue-600` used in a component | Skips the semantic tier and removes the theming seam | Point a semantic token at it |
77
+
78
+ ## In Tailwind projects
79
+
80
+ Tailwind v4 generates utilities from `@theme`, so names declared there become the API. Declare primitives and semantics in the same block; the `--color-*` namespace is what produces `bg-*`, `text-*` and `border-*`:
81
+
82
+ ```css
83
+ @theme {
84
+ /* Primitives */
85
+ --color-blue-50: #eff6ff;
86
+ --color-blue-600: #2563eb;
87
+ --color-blue-900: #1e3a8a;
88
+
89
+ /* Semantics: what templates should use */
90
+ --color-accent-solid: var(--color-blue-600);
91
+ --color-text-secondary: var(--color-neutral-700);
92
+ }
93
+ ```
94
+
95
+ That yields `bg-accent-solid` and `text-text-secondary` alongside `bg-blue-600`. Tailwind prefixes every `--color-*` name with the utility, so a `text-` token doubles. Accept the doubling, or follow the convention the project already uses. Both tiers are reachable, so the discipline is a convention rather than a constraint. Templates use the semantic utilities, and a raw `bg-blue-600` in a component is the thing to flag.
96
+
97
+ Repoint semantics for dark mode in a `.dark` block outside `@theme`. Leave the block without `inline`, so utilities read the semantic variable at render time.
98
+
99
+ Opacity modifiers work on either tier, as in `bg-accent-solid/50`. But a color carrying alpha cannot be contrast-checked against a static background, because what it renders depends on what sits behind it. Use solid tokens for anything with text on it.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jakub Krehel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,36 @@
1
+ # Notice
2
+
3
+ This skill is a fork of `better-typography/` from the repository `jakubkrehel/skills`, at commit `4c66b1fa86e9ab70187b43d7da9b6fdb29f2ef63`, fetched 2026-10-16.
4
+
5
+ Local edits:
6
+
7
+ - Renamed by `scripts/fork-interface-skills.sh` at dotfiles commit `5fbdc4b` (`better-typography` to `typography`).
8
+ - Dropped `agents/openai.yaml`.
9
+ - Added the precedence block after the introduction of `SKILL.md`.
10
+ - Reconciled the clause in `ui/SKILL.md` that limited project tokens, so a project token always wins.
11
+
12
+ Updating: diff upstream from the pinned commit, have a frontier model port the changes over the local edits, then bump the pin.
13
+
14
+ The upstream license (MIT) applies; the text is in `LICENSE`:
15
+
16
+ > MIT License
17
+ >
18
+ > Copyright (c) 2026 Jakub Krehel
19
+ >
20
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
21
+ > of this software and associated documentation files (the "Software"), to deal
22
+ > in the Software without restriction, including without limitation the rights
23
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
24
+ > copies of the Software, and to permit persons to whom the Software is
25
+ > furnished to do so, subject to the following conditions:
26
+ >
27
+ > The above copyright notice and this permission notice shall be included in all
28
+ > copies or substantial portions of the Software.
29
+ >
30
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
31
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
32
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
33
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
34
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
35
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
36
+ > SOFTWARE.