okchroma 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,134 +1,94 @@
1
1
  # OKChroma
2
2
 
3
- **A color-system engine.** Give it one (or two) brand color(s) and it generates a complete,
4
- accessible, theme-ready color system around it.
5
-
6
- Each color family is a light and dark ramp built from pre-reserved roles — 3 papers,
7
- 4 highlighters, 1 crayon, 1 pencil, and 2 pens — plus the stamp fill with its hover and
8
- pressed states and its on-text color, for each brand hex inputted. Alongside the brand color(s), it generates a neutral and the critical / warning / positive / info signal scales. The inputted hex color(s) is/are preserved in identity swatches for logos or other brand identity moments.
9
-
10
- The point is **white-label predictability**: every ramps's stops land at the same
11
- perceived lightness and play the same role, so you map your design tokens to step
12
- **numbers** once and they hold for any color scale. Contrast is built into the math, not
13
- bolted on after: the system solves against **WCAG** ratios (every text color guaranteed
14
- to pass), with **APCA** as an added legibility floor on CTA text (Lc 65 clearance on top
15
- of the 4.5:1 requirement).
16
-
17
- Output comes in two interchangeable forms carrying the same values:
18
-
19
- - **CSS custom properties** — per-brand CSS generated live (`brandCss`) + the static
20
- `dist/signals.css`, via the semantic layer `tokens/semantic.css`.
21
- - **Figma variables** — written into a Figma file by the bundled plugin.
22
-
23
- The live demo and the Figma plugin are previews/front-ends; the engine and its output are
24
- the product.
25
-
26
- > The "reserved role per step" model is a conceptual nod to
3
+ **A color-system engine.** Give it one brand hex, or two, and it resolves a complete
4
+ light and dark token system whose contrast requirements are solved during generation,
5
+ then emits it as CSS custom properties or Figma variables.
6
+
7
+ Every family (neutral, brand, brand-alt, critical, warning, positive, info) carries the
8
+ same scale: 3 papers, 4 highlighters, 1 crayon, 1 pencil, 2 pens, plus the stamp (the
9
+ solid fill with its hover and pressed states, its edge, and its text). The neutral adds
10
+ the two poles. Light and dark resolve together and ship on the same names, so a token is
11
+ mapped once and holds for any brand.
12
+
13
+ Contrast is built into the math, not checked afterwards: crayon reads on every paper at
14
+ 3:1 (the non-text bar), pencil at 4.5:1, pen at 4.5:1 on every paper and highlighter in
15
+ both directions; the stamp's text passes 4.5:1 on its fill. APCA is used once, as a booster
16
+ that nudges the stamp fill until its text reads at Lc 65. Each claim, its scope, and the audit that
17
+ proves it: [Guarantees](https://egerrity.github.io/okchroma/#/docs/guarantees).
18
+
19
+ > The reserved-role-per-stop model is a conceptual nod to
27
20
  > [Radix Colors](https://www.radix-ui.com/colors/docs/palette-composition/understanding-the-scale).
28
- > It is **not** a dependency and does not affect the math — all color computation is original.
29
-
30
- ## Quick start
31
-
32
- ```bash
33
- npm install
34
- npm run demo:build # generate token CSS + bundle the demo
35
- npx serve . # then open http://localhost:3000/demo/index.html
36
- ```
37
-
38
- Live editing: `npm run dev` (watch mode). Build the Figma plugin: `npm run plugin:build`,
39
- then import `plugin/manifest.json` in Figma.
21
+ > It is not a dependency and does not touch the math; the color computation is original.
40
22
 
41
- ## Use as an npm package
23
+ ## Install
42
24
 
43
25
  ```bash
44
26
  npm install okchroma
45
27
  ```
46
28
 
47
- ```ts
48
- import { resolveTheme, brandCss, signalsCss } from 'okchroma'
29
+ ESM and CommonJS builds with TypeScript declarations; no runtime dependencies (the one
30
+ perceptual-distance library the engine uses is bundled in).
49
31
 
50
- const t = resolveTheme({ primaryHex: '#E93D82', name: 'Acme' })
51
- const css = brandCss('acme', 'Acme', t.themed) + '\n' + signalsCss()
32
+ ```ts
33
+ import { resolveTheme, brandCss, signalsCss, neutralTintHue } from 'okchroma'
34
+
35
+ const theme = resolveTheme({ primaryHex: '#E93D82', name: 'acme', deriveSecondary: true })
36
+ const neutralH = neutralTintHue(theme.themed.scale.brandH)
37
+ const css = brandCss('acme', 'Acme', theme.themed, theme.secondary?.scale ?? null,
38
+ '', 'default', undefined, theme.secondary?.style, false, null, true, neutralH)
39
+ + '\n' + signalsCss()
40
+ // put css in a stylesheet; set data-brand="acme" on the themed root,
41
+ // and data-theme="dark" on it for dark mode
52
42
  ```
53
43
 
54
- `resolveTheme` solves the full system for a seed (primary, optional secondary, neutral
55
- source, signal policy). `brandCss` emits the solved light and dark values as CSS custom
56
- properties scoped to `[data-brand="acme"]`, with dark values under
57
- `[data-brand="acme"][data-theme="dark"]`; the per-brand neutral is included in the same
58
- block. Variables are named `--brand-<token>`, `--neutral-<token>`, and
59
- `--<signal>-<token>`. Inject the string into a `<style>` tag at runtime or write it to a
60
- file at build time; `signalsCss()` is brand-independent and emitted once.
61
-
62
- The same resolved theme feeds the other emitters: `themeToFigma` returns the Figma
63
- variable payload, and `emitDtcgRamp` returns DTCG JSON carrying the live requirement in
64
- `$extensions`. ESM and CJS builds ship with TypeScript declarations. The package bundles
65
- its one runtime dependency (helmlab), so installing it adds nothing transitive.
66
-
67
- ## How it works (30 seconds)
68
-
69
- Every token is a **requirement the engine solves, not a frozen value**. A pure-data
70
- declaration (`src/engine/requirements/spec.ts`) states each stop's producer (perceptual
71
- placement, warm drift, chroma ladder) and its requirements (contrast floors, seam
72
- separations, on-text rules); a resolver executes it per seed — **produce → require →
73
- refine**. `resolveBrand(hex)` runs that engine, applies **policy** (status-color
74
- collisions, signal shifts), and an **emitter** (`cssRender` / `figmaRender`) maps the
75
- resolved stops onto named tokens and picks light vs dark. The declaration also
76
- round-trips to DTCG tokens (`$value` fallback + the live requirement in `$extensions`).
77
-
78
- - **Engine:** `src/engine/*` — pure TypeScript with one runtime dependency (helmlab,
79
- the P2 adjacency metric), bundled into the npm package at build time.
80
- - **Entry points:** `resolveBrand` (`src/engine/resolve.ts`) and `generateScale`
81
- (`src/engine/colorEngine.ts`, an adapter over the resolver — same signature as always).
82
- - **Build:** `src/build.ts` writes the one static output, `dist/signals.css`. There is no
83
- brand roster: per-brand CSS is generated live by whichever caller resolves a hex (the
84
- demo's hex input, a plugin's form field).
85
-
86
- ## The Figma plugin
87
-
88
- The bundled plugin runs the same engine inside Figma and writes the resolved system into
89
- your file as variables: **theme** + **mode** collections whose semantic tokens alias onto
90
- shared primitives — per-brand ramps under `brand/<name>`, with neutrals and signals
91
- deduplicated across brands, light + dark values on every token.
92
-
93
- **Install** — from source:
44
+ The Figma tree comes from `themeToFigma`. Signatures, the full input, and an end-to-end example:
45
+ [Install and API](https://egerrity.github.io/okchroma/#/docs/install).
46
+
47
+ ## Run from source
94
48
 
95
49
  ```bash
96
- npm install && npm run plugin:build
50
+ npm install
51
+ npm run demo:build # writes dist/signals.css and bundles the demo
52
+ npx serve . # open http://localhost:3000/demo/index.html
53
+ npm run dev # watch mode
97
54
  ```
98
55
 
99
- then Figma → Plugins → Development → **Import plugin from manifest** →
100
- `plugin/manifest.json`.
101
-
102
- > The hosted download for **this** plugin is **withdrawn** as of 2026-07-29. The scale
103
- > changed shape in that round — `highlight-9` and `on-highlight` were removed and the pen
104
- > stops renumbered — and this plugin does not yet carry the rename table that migrates an
105
- > existing Figma file across it, so updating an installed copy would orphan its variable
106
- > bindings. Building from source is unaffected: a fresh file gets the new shape correctly.
107
- >
108
- > The **extended collections** plugin (Figma Enterprise) does carry the migration and is
109
- > published as normal — **[install it here](https://egerrity.github.io/okchroma/install.html)**.
110
-
111
- **Use** — name the brand, pick a primary, and Apply. Optional before applying:
112
-
113
- - **Secondary color** — "+ Add secondary" starts on *From primary* (a pastel derived from
114
- your primary); type any hex for a custom secondary in **Tint / Pastel / Outline / Exact**
115
- style.
116
- - **Neutral color** — *Default* (a touch of primary hue), *Medium* (slightly more tint), *Intense*, or *True grey*.
117
- - **Contrast standard** — **APCA** (default) or **WCAG**. One profile per collection pair:
118
- applying the other profile to an existing file forks a clearly-labeled second pair
119
- (`theme-wcag`/`mode-wcag`) instead of ever mixing values.
120
-
121
- Re-applying the same brand name updates it in place (after a confirm); a new name adds a
122
- brand to the same collections — two applies with two brand colors is a multi-brand system
123
- on shared foundations.
56
+ `npm run typecheck` runs the compiler; the audit gates (`npm run req:audit`,
57
+ `npm run audit:guarantee`, and the rest of `package.json`) each sweep agnostic seeds and
58
+ fail on the worst case. What each proves:
59
+ [How it is verified](https://egerrity.github.io/okchroma/#/docs/guarantees/how-it-is-verified).
124
60
 
125
61
  ## Documentation
126
62
 
127
- Full system documentation — overview, architecture (data flow + design decisions),
128
- dependencies, setup — is in **[docs/architecture.md](docs/architecture.md)**.
129
- The scale model and its per-stop targets are in **[docs/scale.md](docs/scale.md)**.
130
- The requirement-token schema — the DTCG token format, field by field, with real
131
- emitted examples — is in **[docs/schema.md](docs/schema.md)**.
63
+ The docs site is the source: [egerrity.github.io/okchroma/#/docs](https://egerrity.github.io/okchroma/#/docs).
64
+
65
+ - [Overview](https://egerrity.github.io/okchroma/#/docs/overview): what goes in, what comes out, the token roster.
66
+ - [Output contract](https://egerrity.github.io/okchroma/#/docs/output): the naming grammar, families and prefixes, modes and selectors, a live CSS block and Figma tree.
67
+ - [Guarantees](https://egerrity.github.io/okchroma/#/docs/guarantees): every claim stated exactly, and how it is verified.
68
+ - [How the theme is generated](https://egerrity.github.io/okchroma/#/docs/generation): the pipeline in execution order, with the constants and the code that runs each step.
69
+ - [Signals and companions](https://egerrity.github.io/okchroma/#/docs/signals) and the [Reference](https://egerrity.github.io/okchroma/#/docs/reference): glossary, constants, option types.
70
+
71
+ In the repo: [docs/architecture.md](docs/architecture.md) (the maintainer's map: modules,
72
+ pipeline stages, data structures, the extended plugin's zones),
73
+ [docs/scale.md](docs/scale.md) (the scale and its declared targets),
74
+ [docs/schema.md](docs/schema.md) (the format of the experimental DTCG export),
75
+ [docs/agents.md](docs/agents.md) (how a coding agent consuming the tokens should read
76
+ them), and [CHANGELOG.md](CHANGELOG.md).
77
+
78
+ ## The Figma plugins
79
+
80
+ - **OKChroma Extended** (`plugin-ext/`) is the shipped Figma front-end. It requires the
81
+ Figma desktop app and a Figma Enterprise plan: it writes extended variable collections,
82
+ one base collection with light and dark modes plus one extension per brand that
83
+ overrides only what differs. Download and install steps:
84
+ [install page](https://egerrity.github.io/okchroma/install.html). Build from source with
85
+ `npm run plugin-ext:build`; see [plugin-ext/README.md](plugin-ext/README.md).
86
+ - **OKChroma** (`plugin/`, the community plugin) is withdrawn from download until it
87
+ carries the rename table that migrates an existing file across the scale change of July
88
+ 2026. It still builds from source with `npm run plugin:build` and imports from
89
+ `plugin/manifest.json`; a fresh file gets the current shape.
90
+
91
+ The demo and the plugins are front-ends. The product is the engine and what it emits.
132
92
 
133
93
  ## License
134
94
 
@@ -919,9 +919,9 @@ var P_LIFT = { hue: "warm-torsion", L: "perceptual-lift", chroma: "ladder" };
919
919
  var P_FIXED = { hue: "warm-torsion", L: "fixed", chroma: "ladder" };
920
920
  var P_TEXT = { hue: "warm-torsion", L: "perceptual", chroma: "brand" };
921
921
  var S8 = { metric: "wcag", against: "paper-5", target: STOP_8_NONTEXT_CONTRAST, level: "AA" };
922
- var T9 = { metric: "wcag", against: "paper-3", target: PENCIL_9_CONTRAST, level: "AA" };
922
+ var T9 = { metric: "wcag", against: "paper-5", target: PENCIL_9_CONTRAST, level: "AA" };
923
923
  var T10 = { metric: "wcag", against: "highlighter-20", target: PEN_10_CONTRAST, level: "AA" };
924
- var T11 = { metric: "wcag", against: "paper-3", target: PEN_11_CONTRAST_FLOOR, level: "AAA" };
924
+ var T11 = { metric: "wcag", against: "paper-5", target: PEN_11_CONTRAST_FLOOR, level: "AAA" };
925
925
  var ONS = { onFill: { metric: "apca-pole", enforce: true, ratioFloor: 4.5, coEnforceLc: 65 } };
926
926
  var LIGHT = {
927
927
  stops: [
@@ -947,9 +947,9 @@ var LIGHT = {
947
947
  { stop: 11, rootL: ROOT_L_LIGHT[11], group: groupOf(11), produce: PL_TEXT, chromaMult: SCALE_C_LIGHT[11].textMult, textMaxC: SCALE_C_LIGHT[11].textMaxC, chromaFloor: SCALE_C_LIGHT[11].chromaFloor, require: T11 }
948
948
  ],
949
949
  roles: [
950
- { role: "cta", produce: { hue: "constant", L: "anchor", chroma: "brand" }, floorL: 0, chromaMult: 1 },
951
- { role: "cta-hover", produce: { hue: "constant", L: "hover", chroma: "brand" }, floorL: 0, chromaMult: 1 },
952
- { role: "cta-pressed", produce: { hue: "constant", L: "pressed", chroma: "brand" }, floorL: 0, chromaMult: 1 }
950
+ { role: STAMP_FILL, produce: { hue: "constant", L: "anchor", chroma: "brand" }, floorL: 0, chromaMult: 1 },
951
+ { role: STAMP_FILL_HOVER, produce: { hue: "constant", L: "hover", chroma: "brand" }, floorL: 0, chromaMult: 1 },
952
+ { role: STAMP_FILL_PRESSED, produce: { hue: "constant", L: "pressed", chroma: "brand" }, floorL: 0, chromaMult: 1 }
953
953
  ],
954
954
  ons: ONS
955
955
  };
@@ -979,9 +979,9 @@ var DARK = {
979
979
  { stop: 11, rootL: ROOT_L_DARK[11], group: groupOf(11), produce: P_TEXT, chromaMult: SCALE_C_DARK[11].textMult, textMaxC: SCALE_C_DARK[11].textMaxC, chromaFloor: SCALE_C_DARK[11].chromaFloor, require: T11 }
980
980
  ],
981
981
  roles: [
982
- { role: "cta", produce: { hue: "constant", L: "anchor", chroma: "brand" }, floorL: DARK_CTA_MIN_L, chromaMult: 1 },
983
- { role: "cta-hover", produce: { hue: "constant", L: "hover", chroma: "brand" }, floorL: DARK_CTA_MIN_L, chromaMult: 1 },
984
- { role: "cta-pressed", produce: { hue: "constant", L: "pressed", chroma: "brand" }, floorL: DARK_CTA_MIN_L, chromaMult: 1 }
982
+ { role: STAMP_FILL, produce: { hue: "constant", L: "anchor", chroma: "brand" }, floorL: DARK_CTA_MIN_L, chromaMult: 1 },
983
+ { role: STAMP_FILL_HOVER, produce: { hue: "constant", L: "hover", chroma: "brand" }, floorL: DARK_CTA_MIN_L, chromaMult: 1 },
984
+ { role: STAMP_FILL_PRESSED, produce: { hue: "constant", L: "pressed", chroma: "brand" }, floorL: DARK_CTA_MIN_L, chromaMult: 1 }
985
985
  ],
986
986
  ons: ONS
987
987
  };
@@ -2360,6 +2360,8 @@ var toHex = (rgb) => "#" + rgb.map((c) => Math.round(c * 255).toString(16).padSt
2360
2360
  var CTA_CLEARANCE_CAPS = [0.05, 0.92];
2361
2361
  var CHROMATIC_W80_WORST_SHIP_Y = { light: 0.459357, dark: 0.074936 };
2362
2362
  var CHROMATIC_P3_WORST_SHIP_Y = { light: 0.836007, dark: 0.016041 };
2363
+ var NEUTRAL_P3_WORST_SHIP_Y = { light: 0.845015, dark: 0.014247 };
2364
+ var NEUTRAL_W80_WORST_SHIP_Y = { light: 0.506433, dark: 0.074262 };
2363
2365
  function resolveRamp(hex, mode, spec, opts) {
2364
2366
  const o = hexToOklch(hex);
2365
2367
  const seed = { hex, ...o };
@@ -2371,10 +2373,10 @@ function resolveRamp(hex, mode, spec, opts) {
2371
2373
  stops: spec.stops.map((s) => s.stop === 10 && s.require?.metric === "wcag" ? { ...s, require: { ...s.require, target: 6.5 } } : s)
2372
2374
  };
2373
2375
  }
2374
- const ctaReq = spec.roles.find((r) => r.role === "cta");
2375
- const hoverReq = spec.roles.find((r) => r.role === "cta-hover");
2376
- const pressedReq = spec.roles.find((r) => r.role === "cta-pressed");
2377
- if (!ctaReq) throw new Error("spec has no cta role");
2376
+ const ctaReq = spec.roles.find((r) => r.role === STAMP_FILL);
2377
+ const hoverReq = spec.roles.find((r) => r.role === STAMP_FILL_HOVER);
2378
+ const pressedReq = spec.roles.find((r) => r.role === STAMP_FILL_PRESSED);
2379
+ if (!ctaReq) throw new Error(`spec has no ${STAMP_FILL} role`);
2378
2380
  const onFillEnforce = ctx.opts?.enforceOnFillContrast ?? spec.ons.onFill.enforce;
2379
2381
  const dctx = mode === "dark" ? buildDarkContext(ctx, ctaReq.floorL) : null;
2380
2382
  const stops = [];
@@ -2393,8 +2395,6 @@ function resolveRamp(hex, mode, spec, opts) {
2393
2395
  const textGround = ctx.opts?.textGround?.[mode];
2394
2396
  const groundOf = (req, stop) => textGround && stop >= 9 ? textGround : refOf(wcagAnchorStop(req, stop), stop);
2395
2397
  const apcaGroundOf = (req, stop) => textGround && stop >= 9 ? textGround : refOf(declaredAnchor(req), stop);
2396
- const NEUTRAL_P3_WORST_SHIP_Y = { light: 0.845015, dark: 0.014247 };
2397
- const NEUTRAL_W80_WORST_SHIP_Y = { light: 0.506433, dark: 0.074262 };
2398
2398
  const maxLForOf = (req, forWhom, withMargin) => {
2399
2399
  if (req.metric === "wcag") {
2400
2400
  const g2 = groundOf(req, forWhom);
@@ -2579,13 +2579,13 @@ function resolveRamp(hex, mode, spec, opts) {
2579
2579
  repelled = true;
2580
2580
  }
2581
2581
  }
2582
- cta = emitRole("cta", light9L, ctaCFor(light9L) * ctaCMul, ctaH);
2582
+ cta = emitRole(STAMP_FILL, light9L, ctaCFor(light9L) * ctaCMul, ctaH);
2583
2583
  const hCFor = (L) => ctx.cAt("light", L, (hoverReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul;
2584
2584
  const pCFor = (L) => ctx.cAt("light", L, (pressedReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul;
2585
2585
  const hL = stateFillL(light9L, "light", 1);
2586
2586
  const pL = stateFillL(light9L, "light", 2);
2587
- ctaHover = emitRole("cta-hover", hL, hCFor(hL), ctaH);
2588
- ctaPressed = emitRole("cta-pressed", pL, pCFor(pL), ctaH);
2587
+ ctaHover = emitRole(STAMP_FILL_HOVER, hL, hCFor(hL), ctaH);
2588
+ ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, pCFor(pL), ctaH);
2589
2589
  if (light9L !== ctx.scaleL) {
2590
2590
  cta.enforced = true;
2591
2591
  ctaHover.enforced = true;
@@ -2608,19 +2608,19 @@ function resolveRamp(hex, mode, spec, opts) {
2608
2608
  const cta9L = ctx.opts?.darkCtaFlatApp !== void 0 ? flatDarkCtaL(d, (L) => ctx.cAt("dark", L, d.darkC9), ctx.darkCtaH, ctx.opts.darkCtaFlatApp) : d.dark9L;
2609
2609
  const dCFor = (L) => ctx.cAt("dark", L, d.darkC9);
2610
2610
  const dTrio = (baseL) => [stateFillL(baseL, "dark", 1), stateFillL(baseL, "dark", 2)];
2611
- cta = emitRole("cta", cta9L, dCFor(cta9L), ctx.darkCtaH);
2611
+ cta = emitRole(STAMP_FILL, cta9L, dCFor(cta9L), ctx.darkCtaH);
2612
2612
  {
2613
2613
  const [hL, pL] = dTrio(cta9L);
2614
- ctaHover = emitRole("cta-hover", hL, dCFor(hL), ctx.darkCtaH);
2615
- ctaPressed = emitRole("cta-pressed", pL, dCFor(pL), ctx.darkCtaH);
2614
+ ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
2615
+ ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
2616
2616
  }
2617
2617
  onFillIsWhite = onFillIsWhiteDarkAt(cta.L, cta.C, cta.H, enforceLc !== void 0 ? false : onFillEnforce);
2618
2618
  const enforcedL = enforceLc !== void 0 ? ctaDarkEnforcedLApca(ctx, cta, onFillIsWhite, onFillEnforce, enforceLc) : ctaDarkEnforcedL(ctx, cta, onFillIsWhite, onFillEnforce);
2619
2619
  if (enforcedL !== null) {
2620
- cta = emitRole("cta", enforcedL, dCFor(enforcedL), ctx.darkCtaH);
2620
+ cta = emitRole(STAMP_FILL, enforcedL, dCFor(enforcedL), ctx.darkCtaH);
2621
2621
  const [hL, pL] = dTrio(enforcedL);
2622
- ctaHover = emitRole("cta-hover", hL, dCFor(hL), ctx.darkCtaH);
2623
- ctaPressed = emitRole("cta-pressed", pL, dCFor(pL), ctx.darkCtaH);
2622
+ ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
2623
+ ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
2624
2624
  cta.enforced = true;
2625
2625
  ctaHover.enforced = true;
2626
2626
  ctaPressed.enforced = true;
@@ -2629,10 +2629,10 @@ function resolveRamp(hex, mode, spec, opts) {
2629
2629
  const [capLoL, capHiL] = CTA_CLEARANCE_CAPS;
2630
2630
  const clearedL = ctaDarkDualGateL(cta, ctx.darkCtaH, onFillIsWhite, onFillEnforce, coLc + APCA_ENFORCE_MARGIN_LC, capLoL, capHiL);
2631
2631
  if (Math.abs(clearedL - cta.L) > 1e-9) {
2632
- cta = emitRole("cta", clearedL, dCFor(clearedL), ctx.darkCtaH);
2632
+ cta = emitRole(STAMP_FILL, clearedL, dCFor(clearedL), ctx.darkCtaH);
2633
2633
  const [hL, pL] = dTrio(clearedL);
2634
- ctaHover = emitRole("cta-hover", hL, dCFor(hL), ctx.darkCtaH);
2635
- ctaPressed = emitRole("cta-pressed", pL, dCFor(pL), ctx.darkCtaH);
2634
+ ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
2635
+ ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
2636
2636
  cta.enforced = true;
2637
2637
  ctaHover.enforced = true;
2638
2638
  ctaPressed.enforced = true;
@@ -2641,10 +2641,10 @@ function resolveRamp(hex, mode, spec, opts) {
2641
2641
  if (ctx.opts?.ctaSolve) {
2642
2642
  const exitL = solveDarkCtaExit(cta, dCFor, ctx.darkCtaH, ctx.opts.ctaSolve.redDark, enforceLc, coLc);
2643
2643
  if (exitL !== null) {
2644
- cta = emitRole("cta", exitL, dCFor(exitL), ctx.darkCtaH);
2644
+ cta = emitRole(STAMP_FILL, exitL, dCFor(exitL), ctx.darkCtaH);
2645
2645
  const [hL, pL] = dTrio(exitL);
2646
- ctaHover = emitRole("cta-hover", hL, dCFor(hL), ctx.darkCtaH);
2647
- ctaPressed = emitRole("cta-pressed", pL, dCFor(pL), ctx.darkCtaH);
2646
+ ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
2647
+ ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
2648
2648
  cta.enforced = true;
2649
2649
  ctaHover.enforced = true;
2650
2650
  ctaPressed.enforced = true;
@@ -3977,9 +3977,14 @@ function themeToFigma(r, input) {
3977
3977
  // src/engine/requirements/dtcg.ts
3978
3978
  var EXT_KEY = "org.okchroma.requirement";
3979
3979
  var RESOLVER_ID = "okchroma-reqtoken@2";
3980
+ var LEGACY_ROLE_NAMES = {
3981
+ "cta": STAMP_FILL,
3982
+ "cta-hover": STAMP_FILL_HOVER,
3983
+ "cta-pressed": STAMP_FILL_PRESSED
3984
+ };
3980
3985
  var hexToValue = (hex) => {
3981
3986
  const h = hex.replace("#", "");
3982
- const c = (i) => parseInt(h.slice(i, i + 2), 16) / 255;
3987
+ const c = (i) => Math.round(parseInt(h.slice(i, i + 2), 16) / 255 * 1e4) / 1e4;
3983
3988
  return { colorSpace: "srgb", components: [c(0), c(2), c(4)], alpha: 1, hex: hex.toLowerCase() };
3984
3989
  };
3985
3990
  function emitDtcgRamp(hex, mode, groupName, spec) {
@@ -3995,9 +4000,9 @@ function emitDtcgRamp(hex, mode, groupName, spec) {
3995
4000
  group[String(sp.stop)] = { $type: "color", $value: hexToValue(st.hex), $extensions: { [EXT_KEY]: { resolver: RESOLVER_ID, seed: seedRef, mode, ...sp } } };
3996
4001
  }
3997
4002
  const roleVal = {
3998
- "cta": ramp.roles.cta,
3999
- "cta-hover": ramp.roles.ctaHover,
4000
- "cta-pressed": ramp.roles.ctaPressed
4003
+ [STAMP_FILL]: ramp.roles.cta,
4004
+ [STAMP_FILL_HOVER]: ramp.roles.ctaHover,
4005
+ [STAMP_FILL_PRESSED]: ramp.roles.ctaPressed
4001
4006
  };
4002
4007
  for (const rr of spec.roles) {
4003
4008
  const rv = roleVal[rr.role];
@@ -4017,6 +4022,7 @@ function parseToken(token) {
4017
4022
  if (isRole) {
4018
4023
  for (const k of ["role", "produce", "floorL", "chromaMult"]) if (req[k] === void 0) throw new Error(`role requirement missing "${k}"`);
4019
4024
  }
4025
+ if (isRole && LEGACY_ROLE_NAMES[req.role]) req.role = LEGACY_ROLE_NAMES[req.role];
4020
4026
  return { req, seedRef: seed, mode };
4021
4027
  }
4022
4028
  function resolveDtcgRamp(group) {
@@ -840,9 +840,9 @@ var P_LIFT = { hue: "warm-torsion", L: "perceptual-lift", chroma: "ladder" };
840
840
  var P_FIXED = { hue: "warm-torsion", L: "fixed", chroma: "ladder" };
841
841
  var P_TEXT = { hue: "warm-torsion", L: "perceptual", chroma: "brand" };
842
842
  var S8 = { metric: "wcag", against: "paper-5", target: STOP_8_NONTEXT_CONTRAST, level: "AA" };
843
- var T9 = { metric: "wcag", against: "paper-3", target: PENCIL_9_CONTRAST, level: "AA" };
843
+ var T9 = { metric: "wcag", against: "paper-5", target: PENCIL_9_CONTRAST, level: "AA" };
844
844
  var T10 = { metric: "wcag", against: "highlighter-20", target: PEN_10_CONTRAST, level: "AA" };
845
- var T11 = { metric: "wcag", against: "paper-3", target: PEN_11_CONTRAST_FLOOR, level: "AAA" };
845
+ var T11 = { metric: "wcag", against: "paper-5", target: PEN_11_CONTRAST_FLOOR, level: "AAA" };
846
846
  var ONS = { onFill: { metric: "apca-pole", enforce: true, ratioFloor: 4.5, coEnforceLc: 65 } };
847
847
  var LIGHT = {
848
848
  stops: [
@@ -868,9 +868,9 @@ var LIGHT = {
868
868
  { stop: 11, rootL: ROOT_L_LIGHT[11], group: groupOf(11), produce: PL_TEXT, chromaMult: SCALE_C_LIGHT[11].textMult, textMaxC: SCALE_C_LIGHT[11].textMaxC, chromaFloor: SCALE_C_LIGHT[11].chromaFloor, require: T11 }
869
869
  ],
870
870
  roles: [
871
- { role: "cta", produce: { hue: "constant", L: "anchor", chroma: "brand" }, floorL: 0, chromaMult: 1 },
872
- { role: "cta-hover", produce: { hue: "constant", L: "hover", chroma: "brand" }, floorL: 0, chromaMult: 1 },
873
- { role: "cta-pressed", produce: { hue: "constant", L: "pressed", chroma: "brand" }, floorL: 0, chromaMult: 1 }
871
+ { role: STAMP_FILL, produce: { hue: "constant", L: "anchor", chroma: "brand" }, floorL: 0, chromaMult: 1 },
872
+ { role: STAMP_FILL_HOVER, produce: { hue: "constant", L: "hover", chroma: "brand" }, floorL: 0, chromaMult: 1 },
873
+ { role: STAMP_FILL_PRESSED, produce: { hue: "constant", L: "pressed", chroma: "brand" }, floorL: 0, chromaMult: 1 }
874
874
  ],
875
875
  ons: ONS
876
876
  };
@@ -900,9 +900,9 @@ var DARK = {
900
900
  { stop: 11, rootL: ROOT_L_DARK[11], group: groupOf(11), produce: P_TEXT, chromaMult: SCALE_C_DARK[11].textMult, textMaxC: SCALE_C_DARK[11].textMaxC, chromaFloor: SCALE_C_DARK[11].chromaFloor, require: T11 }
901
901
  ],
902
902
  roles: [
903
- { role: "cta", produce: { hue: "constant", L: "anchor", chroma: "brand" }, floorL: DARK_CTA_MIN_L, chromaMult: 1 },
904
- { role: "cta-hover", produce: { hue: "constant", L: "hover", chroma: "brand" }, floorL: DARK_CTA_MIN_L, chromaMult: 1 },
905
- { role: "cta-pressed", produce: { hue: "constant", L: "pressed", chroma: "brand" }, floorL: DARK_CTA_MIN_L, chromaMult: 1 }
903
+ { role: STAMP_FILL, produce: { hue: "constant", L: "anchor", chroma: "brand" }, floorL: DARK_CTA_MIN_L, chromaMult: 1 },
904
+ { role: STAMP_FILL_HOVER, produce: { hue: "constant", L: "hover", chroma: "brand" }, floorL: DARK_CTA_MIN_L, chromaMult: 1 },
905
+ { role: STAMP_FILL_PRESSED, produce: { hue: "constant", L: "pressed", chroma: "brand" }, floorL: DARK_CTA_MIN_L, chromaMult: 1 }
906
906
  ],
907
907
  ons: ONS
908
908
  };
@@ -2281,6 +2281,8 @@ var toHex = (rgb) => "#" + rgb.map((c) => Math.round(c * 255).toString(16).padSt
2281
2281
  var CTA_CLEARANCE_CAPS = [0.05, 0.92];
2282
2282
  var CHROMATIC_W80_WORST_SHIP_Y = { light: 0.459357, dark: 0.074936 };
2283
2283
  var CHROMATIC_P3_WORST_SHIP_Y = { light: 0.836007, dark: 0.016041 };
2284
+ var NEUTRAL_P3_WORST_SHIP_Y = { light: 0.845015, dark: 0.014247 };
2285
+ var NEUTRAL_W80_WORST_SHIP_Y = { light: 0.506433, dark: 0.074262 };
2284
2286
  function resolveRamp(hex, mode, spec, opts) {
2285
2287
  const o = hexToOklch(hex);
2286
2288
  const seed = { hex, ...o };
@@ -2292,10 +2294,10 @@ function resolveRamp(hex, mode, spec, opts) {
2292
2294
  stops: spec.stops.map((s) => s.stop === 10 && s.require?.metric === "wcag" ? { ...s, require: { ...s.require, target: 6.5 } } : s)
2293
2295
  };
2294
2296
  }
2295
- const ctaReq = spec.roles.find((r) => r.role === "cta");
2296
- const hoverReq = spec.roles.find((r) => r.role === "cta-hover");
2297
- const pressedReq = spec.roles.find((r) => r.role === "cta-pressed");
2298
- if (!ctaReq) throw new Error("spec has no cta role");
2297
+ const ctaReq = spec.roles.find((r) => r.role === STAMP_FILL);
2298
+ const hoverReq = spec.roles.find((r) => r.role === STAMP_FILL_HOVER);
2299
+ const pressedReq = spec.roles.find((r) => r.role === STAMP_FILL_PRESSED);
2300
+ if (!ctaReq) throw new Error(`spec has no ${STAMP_FILL} role`);
2299
2301
  const onFillEnforce = ctx.opts?.enforceOnFillContrast ?? spec.ons.onFill.enforce;
2300
2302
  const dctx = mode === "dark" ? buildDarkContext(ctx, ctaReq.floorL) : null;
2301
2303
  const stops = [];
@@ -2314,8 +2316,6 @@ function resolveRamp(hex, mode, spec, opts) {
2314
2316
  const textGround = ctx.opts?.textGround?.[mode];
2315
2317
  const groundOf = (req, stop) => textGround && stop >= 9 ? textGround : refOf(wcagAnchorStop(req, stop), stop);
2316
2318
  const apcaGroundOf = (req, stop) => textGround && stop >= 9 ? textGround : refOf(declaredAnchor(req), stop);
2317
- const NEUTRAL_P3_WORST_SHIP_Y = { light: 0.845015, dark: 0.014247 };
2318
- const NEUTRAL_W80_WORST_SHIP_Y = { light: 0.506433, dark: 0.074262 };
2319
2319
  const maxLForOf = (req, forWhom, withMargin) => {
2320
2320
  if (req.metric === "wcag") {
2321
2321
  const g2 = groundOf(req, forWhom);
@@ -2500,13 +2500,13 @@ function resolveRamp(hex, mode, spec, opts) {
2500
2500
  repelled = true;
2501
2501
  }
2502
2502
  }
2503
- cta = emitRole("cta", light9L, ctaCFor(light9L) * ctaCMul, ctaH);
2503
+ cta = emitRole(STAMP_FILL, light9L, ctaCFor(light9L) * ctaCMul, ctaH);
2504
2504
  const hCFor = (L) => ctx.cAt("light", L, (hoverReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul;
2505
2505
  const pCFor = (L) => ctx.cAt("light", L, (pressedReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul;
2506
2506
  const hL = stateFillL(light9L, "light", 1);
2507
2507
  const pL = stateFillL(light9L, "light", 2);
2508
- ctaHover = emitRole("cta-hover", hL, hCFor(hL), ctaH);
2509
- ctaPressed = emitRole("cta-pressed", pL, pCFor(pL), ctaH);
2508
+ ctaHover = emitRole(STAMP_FILL_HOVER, hL, hCFor(hL), ctaH);
2509
+ ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, pCFor(pL), ctaH);
2510
2510
  if (light9L !== ctx.scaleL) {
2511
2511
  cta.enforced = true;
2512
2512
  ctaHover.enforced = true;
@@ -2529,19 +2529,19 @@ function resolveRamp(hex, mode, spec, opts) {
2529
2529
  const cta9L = ctx.opts?.darkCtaFlatApp !== void 0 ? flatDarkCtaL(d, (L) => ctx.cAt("dark", L, d.darkC9), ctx.darkCtaH, ctx.opts.darkCtaFlatApp) : d.dark9L;
2530
2530
  const dCFor = (L) => ctx.cAt("dark", L, d.darkC9);
2531
2531
  const dTrio = (baseL) => [stateFillL(baseL, "dark", 1), stateFillL(baseL, "dark", 2)];
2532
- cta = emitRole("cta", cta9L, dCFor(cta9L), ctx.darkCtaH);
2532
+ cta = emitRole(STAMP_FILL, cta9L, dCFor(cta9L), ctx.darkCtaH);
2533
2533
  {
2534
2534
  const [hL, pL] = dTrio(cta9L);
2535
- ctaHover = emitRole("cta-hover", hL, dCFor(hL), ctx.darkCtaH);
2536
- ctaPressed = emitRole("cta-pressed", pL, dCFor(pL), ctx.darkCtaH);
2535
+ ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
2536
+ ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
2537
2537
  }
2538
2538
  onFillIsWhite = onFillIsWhiteDarkAt(cta.L, cta.C, cta.H, enforceLc !== void 0 ? false : onFillEnforce);
2539
2539
  const enforcedL = enforceLc !== void 0 ? ctaDarkEnforcedLApca(ctx, cta, onFillIsWhite, onFillEnforce, enforceLc) : ctaDarkEnforcedL(ctx, cta, onFillIsWhite, onFillEnforce);
2540
2540
  if (enforcedL !== null) {
2541
- cta = emitRole("cta", enforcedL, dCFor(enforcedL), ctx.darkCtaH);
2541
+ cta = emitRole(STAMP_FILL, enforcedL, dCFor(enforcedL), ctx.darkCtaH);
2542
2542
  const [hL, pL] = dTrio(enforcedL);
2543
- ctaHover = emitRole("cta-hover", hL, dCFor(hL), ctx.darkCtaH);
2544
- ctaPressed = emitRole("cta-pressed", pL, dCFor(pL), ctx.darkCtaH);
2543
+ ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
2544
+ ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
2545
2545
  cta.enforced = true;
2546
2546
  ctaHover.enforced = true;
2547
2547
  ctaPressed.enforced = true;
@@ -2550,10 +2550,10 @@ function resolveRamp(hex, mode, spec, opts) {
2550
2550
  const [capLoL, capHiL] = CTA_CLEARANCE_CAPS;
2551
2551
  const clearedL = ctaDarkDualGateL(cta, ctx.darkCtaH, onFillIsWhite, onFillEnforce, coLc + APCA_ENFORCE_MARGIN_LC, capLoL, capHiL);
2552
2552
  if (Math.abs(clearedL - cta.L) > 1e-9) {
2553
- cta = emitRole("cta", clearedL, dCFor(clearedL), ctx.darkCtaH);
2553
+ cta = emitRole(STAMP_FILL, clearedL, dCFor(clearedL), ctx.darkCtaH);
2554
2554
  const [hL, pL] = dTrio(clearedL);
2555
- ctaHover = emitRole("cta-hover", hL, dCFor(hL), ctx.darkCtaH);
2556
- ctaPressed = emitRole("cta-pressed", pL, dCFor(pL), ctx.darkCtaH);
2555
+ ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
2556
+ ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
2557
2557
  cta.enforced = true;
2558
2558
  ctaHover.enforced = true;
2559
2559
  ctaPressed.enforced = true;
@@ -2562,10 +2562,10 @@ function resolveRamp(hex, mode, spec, opts) {
2562
2562
  if (ctx.opts?.ctaSolve) {
2563
2563
  const exitL = solveDarkCtaExit(cta, dCFor, ctx.darkCtaH, ctx.opts.ctaSolve.redDark, enforceLc, coLc);
2564
2564
  if (exitL !== null) {
2565
- cta = emitRole("cta", exitL, dCFor(exitL), ctx.darkCtaH);
2565
+ cta = emitRole(STAMP_FILL, exitL, dCFor(exitL), ctx.darkCtaH);
2566
2566
  const [hL, pL] = dTrio(exitL);
2567
- ctaHover = emitRole("cta-hover", hL, dCFor(hL), ctx.darkCtaH);
2568
- ctaPressed = emitRole("cta-pressed", pL, dCFor(pL), ctx.darkCtaH);
2567
+ ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
2568
+ ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
2569
2569
  cta.enforced = true;
2570
2570
  ctaHover.enforced = true;
2571
2571
  ctaPressed.enforced = true;
@@ -3898,9 +3898,14 @@ function themeToFigma(r, input) {
3898
3898
  // src/engine/requirements/dtcg.ts
3899
3899
  var EXT_KEY = "org.okchroma.requirement";
3900
3900
  var RESOLVER_ID = "okchroma-reqtoken@2";
3901
+ var LEGACY_ROLE_NAMES = {
3902
+ "cta": STAMP_FILL,
3903
+ "cta-hover": STAMP_FILL_HOVER,
3904
+ "cta-pressed": STAMP_FILL_PRESSED
3905
+ };
3901
3906
  var hexToValue = (hex) => {
3902
3907
  const h = hex.replace("#", "");
3903
- const c = (i) => parseInt(h.slice(i, i + 2), 16) / 255;
3908
+ const c = (i) => Math.round(parseInt(h.slice(i, i + 2), 16) / 255 * 1e4) / 1e4;
3904
3909
  return { colorSpace: "srgb", components: [c(0), c(2), c(4)], alpha: 1, hex: hex.toLowerCase() };
3905
3910
  };
3906
3911
  function emitDtcgRamp(hex, mode, groupName, spec) {
@@ -3916,9 +3921,9 @@ function emitDtcgRamp(hex, mode, groupName, spec) {
3916
3921
  group[String(sp.stop)] = { $type: "color", $value: hexToValue(st.hex), $extensions: { [EXT_KEY]: { resolver: RESOLVER_ID, seed: seedRef, mode, ...sp } } };
3917
3922
  }
3918
3923
  const roleVal = {
3919
- "cta": ramp.roles.cta,
3920
- "cta-hover": ramp.roles.ctaHover,
3921
- "cta-pressed": ramp.roles.ctaPressed
3924
+ [STAMP_FILL]: ramp.roles.cta,
3925
+ [STAMP_FILL_HOVER]: ramp.roles.ctaHover,
3926
+ [STAMP_FILL_PRESSED]: ramp.roles.ctaPressed
3922
3927
  };
3923
3928
  for (const rr of spec.roles) {
3924
3929
  const rv = roleVal[rr.role];
@@ -3938,6 +3943,7 @@ function parseToken(token) {
3938
3943
  if (isRole) {
3939
3944
  for (const k of ["role", "produce", "floorL", "chromaMult"]) if (req[k] === void 0) throw new Error(`role requirement missing "${k}"`);
3940
3945
  }
3946
+ if (isRole && LEGACY_ROLE_NAMES[req.role]) req.role = LEGACY_ROLE_NAMES[req.role];
3941
3947
  return { req, seedRef: seed, mode };
3942
3948
  }
3943
3949
  function resolveDtcgRamp(group) {
@@ -43,6 +43,7 @@ export type ResolvedRamp = {
43
43
  };
44
44
  };
45
45
  export type { ResolveOpts };
46
+ export declare const CTA_CLEARANCE_CAPS: [number, number];
46
47
  export declare const CHROMATIC_W80_WORST_SHIP_Y: {
47
48
  readonly light: 0.459357;
48
49
  readonly dark: 0.074936;
@@ -51,4 +52,12 @@ export declare const CHROMATIC_P3_WORST_SHIP_Y: {
51
52
  readonly light: 0.836007;
52
53
  readonly dark: 0.016041;
53
54
  };
55
+ export declare const NEUTRAL_P3_WORST_SHIP_Y: {
56
+ readonly light: 0.845015;
57
+ readonly dark: 0.014247;
58
+ };
59
+ export declare const NEUTRAL_W80_WORST_SHIP_Y: {
60
+ readonly light: 0.506433;
61
+ readonly dark: 0.074262;
62
+ };
54
63
  export declare function resolveRamp(hex: string, mode: 'light' | 'dark', spec?: ModeSpec, opts?: ResolveOpts): ResolvedRamp;
@@ -1,3 +1,4 @@
1
+ import { STAMP_FILL, STAMP_FILL_HOVER, STAMP_FILL_PRESSED } from '../tokenNames';
1
2
  export type Group = 'paper' | 'highlighter' | 'crayon' | 'pencil' | 'pen';
2
3
  export declare const textLane: (g: string) => boolean;
3
4
  export type Producer = {
@@ -31,7 +32,7 @@ export type StopReq = {
31
32
  chromaFloor?: number;
32
33
  require?: Require;
33
34
  };
34
- export type RoleName = 'cta' | 'cta-hover' | 'cta-pressed';
35
+ export type RoleName = typeof STAMP_FILL | typeof STAMP_FILL_HOVER | typeof STAMP_FILL_PRESSED;
35
36
  export type RoleReq = {
36
37
  role: RoleName;
37
38
  produce: {
@@ -1,8 +1,25 @@
1
1
  import { type GeneratedScale, type ContrastProfile } from './colorEngine';
2
2
  import type { SignalDef } from './signals';
3
+ type Side = {
4
+ kind: 'swap';
5
+ note: string;
6
+ baseHex: string;
7
+ } | {
8
+ kind: 'shift';
9
+ note: string;
10
+ } | {
11
+ kind: 'none';
12
+ };
13
+ interface SignalShiftRule {
14
+ splitH: number;
15
+ below: Side;
16
+ atOrAbove: Side;
17
+ }
18
+ export declare const SHIFT_RULES: Partial<Record<SignalDef['name'], SignalShiftRule>>;
3
19
  export interface ShiftResult {
4
20
  scale: GeneratedScale;
5
21
  note: string;
6
22
  }
7
23
  export declare function signalSwapVariants(def: SignalDef, contrastProfile?: ContrastProfile): ShiftResult[];
8
24
  export declare function pickSignalShift(brand: GeneratedScale, canonicalSignalScale: GeneratedScale, def: SignalDef, contrastProfile?: ContrastProfile): ShiftResult | null;
25
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okchroma",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "license": "MIT",
5
5
  "description": "Color token engine: one brand hex in, a full WCAG-enforced light+dark token system out (CSS variables, DTCG JSON, Figma variables)",
6
6
  "repository": {
@@ -53,7 +53,8 @@
53
53
  "audit:desc": "esbuild scripts/desc-audit.ts --bundle --platform=node --outfile=dist/desc-audit.js && node dist/desc-audit.js",
54
54
  "name-lint": "esbuild scripts/name-lint.ts --bundle --platform=node --outfile=dist/name-lint.js && node dist/name-lint.js",
55
55
  "typecheck": "tsc --noEmit",
56
- "typecheck:ext": "tsc --noEmit -p plugin-ext"
56
+ "typecheck:ext": "tsc --noEmit -p plugin-ext",
57
+ "docs:lint": "esbuild scripts/docs-lint.ts --bundle --platform=node --outfile=dist/docs-lint.js && node dist/docs-lint.js"
57
58
  },
58
59
  "devDependencies": {
59
60
  "@types/node": "^25.9.2",