okchroma 0.2.0 → 0.3.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/README.md +73 -112
- package/dist-lib/index.cjs +56 -35
- package/dist-lib/index.mjs +56 -35
- package/dist-lib/types/engine/colorEngine.d.ts +8 -0
- package/dist-lib/types/engine/requirements/resolve.d.ts +17 -0
- package/dist-lib/types/engine/requirements/spec.d.ts +2 -1
- package/dist-lib/types/engine/signalShift.d.ts +17 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,134 +1,95 @@
|
|
|
1
1
|
# OKChroma
|
|
2
2
|
|
|
3
|
-
**A color-system engine.** Give it one
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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, Figma variables, or DTCG requirement tokens.
|
|
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-26` reads on every paper
|
|
14
|
+
at 3:1 (the non-text bar), `pencil-47` at 4.5:1, `pen-58` and `pen-70` at 4.5:1 on every
|
|
15
|
+
paper and highlighter in both directions; the stamp's text passes 4.5:1 on the fill and
|
|
16
|
+
brand and signal fills clear APCA Lc 65 on top. 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
|
|
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
|
-
##
|
|
23
|
+
## Install
|
|
42
24
|
|
|
43
25
|
```bash
|
|
44
26
|
npm install okchroma
|
|
45
27
|
```
|
|
46
28
|
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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`, the DTCG requirement tokens from
|
|
45
|
+
`emitDtcgRamp`. Signatures, the full input, and an end-to-end example:
|
|
46
|
+
[Install and API](https://egerrity.github.io/okchroma/#/docs/install).
|
|
47
|
+
|
|
48
|
+
## Run from source
|
|
94
49
|
|
|
95
50
|
```bash
|
|
96
|
-
npm install
|
|
51
|
+
npm install
|
|
52
|
+
npm run demo:build # writes dist/signals.css and bundles the demo
|
|
53
|
+
npx serve . # open http://localhost:3000/demo/index.html
|
|
54
|
+
npm run dev # watch mode
|
|
97
55
|
```
|
|
98
56
|
|
|
99
|
-
|
|
100
|
-
`
|
|
101
|
-
|
|
102
|
-
|
|
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.
|
|
57
|
+
`npm run typecheck` runs the compiler; the audit gates (`npm run req:audit`,
|
|
58
|
+
`npm run audit:guarantee`, and the rest of `package.json`) each sweep agnostic seeds and
|
|
59
|
+
fail on the worst case. What each proves:
|
|
60
|
+
[How it is verified](https://egerrity.github.io/okchroma/#/docs/guarantees/how-it-is-verified).
|
|
124
61
|
|
|
125
62
|
## Documentation
|
|
126
63
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
64
|
+
The docs site is the source: [egerrity.github.io/okchroma/#/docs](https://egerrity.github.io/okchroma/#/docs).
|
|
65
|
+
|
|
66
|
+
- [Overview](https://egerrity.github.io/okchroma/#/docs/overview): what goes in, what comes out, the token roster.
|
|
67
|
+
- [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.
|
|
68
|
+
- [Guarantees](https://egerrity.github.io/okchroma/#/docs/guarantees): every claim stated exactly, and how it is verified.
|
|
69
|
+
- [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.
|
|
70
|
+
- [Signals and companions](https://egerrity.github.io/okchroma/#/docs/signals), [Requirement tokens](https://egerrity.github.io/okchroma/#/docs/token-schema), [Reference](https://egerrity.github.io/okchroma/#/docs/reference).
|
|
71
|
+
|
|
72
|
+
In the repo: [docs/architecture.md](docs/architecture.md) (the maintainer's map: modules,
|
|
73
|
+
pipeline stages, data structures, the extended plugin's zones),
|
|
74
|
+
[docs/scale.md](docs/scale.md) (the scale and its declared targets),
|
|
75
|
+
[docs/schema.md](docs/schema.md) (the requirement-token format in the repo),
|
|
76
|
+
[docs/agents.md](docs/agents.md) (how a coding agent consuming the tokens should read
|
|
77
|
+
them), and [CHANGELOG.md](CHANGELOG.md).
|
|
78
|
+
|
|
79
|
+
## The Figma plugins
|
|
80
|
+
|
|
81
|
+
- **OKChroma Extended** (`plugin-ext/`) is the shipped Figma front-end. It requires the
|
|
82
|
+
Figma desktop app and a Figma Enterprise plan: it writes extended variable collections,
|
|
83
|
+
one base collection with light and dark modes plus one extension per brand that
|
|
84
|
+
overrides only what differs. Download and install steps:
|
|
85
|
+
[install page](https://egerrity.github.io/okchroma/install.html). Build from source with
|
|
86
|
+
`npm run plugin-ext:build`; see [plugin-ext/README.md](plugin-ext/README.md).
|
|
87
|
+
- **OKChroma** (`plugin/`, the community plugin) is withdrawn from download until it
|
|
88
|
+
carries the rename table that migrates an existing file across the scale change of July
|
|
89
|
+
2026. It still builds from source with `npm run plugin:build` and imports from
|
|
90
|
+
`plugin/manifest.json`; a fresh file gets the current shape.
|
|
91
|
+
|
|
92
|
+
The demo and the plugins are front-ends. The product is the engine and what it emits.
|
|
132
93
|
|
|
133
94
|
## License
|
|
134
95
|
|
package/dist-lib/index.cjs
CHANGED
|
@@ -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:
|
|
951
|
-
{ role:
|
|
952
|
-
{ role:
|
|
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:
|
|
983
|
-
{ role:
|
|
984
|
-
{ role:
|
|
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
|
};
|
|
@@ -2358,6 +2358,10 @@ function solveDarkCtaExit(cur, cFor, darkH, redDark, enforceLc, coLc) {
|
|
|
2358
2358
|
var oklchToSrgb = (L, C2, H2) => Object.values(srgbEmitChannels({ L, C: C2, H: H2 })).map((c) => Math.max(0, Math.min(1, c)));
|
|
2359
2359
|
var toHex = (rgb) => "#" + rgb.map((c) => Math.round(c * 255).toString(16).padStart(2, "0")).join("");
|
|
2360
2360
|
var CTA_CLEARANCE_CAPS = [0.05, 0.92];
|
|
2361
|
+
var CHROMATIC_W80_WORST_SHIP_Y = { light: 0.459357, dark: 0.074936 };
|
|
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 };
|
|
2361
2365
|
function resolveRamp(hex, mode, spec, opts) {
|
|
2362
2366
|
const o = hexToOklch(hex);
|
|
2363
2367
|
const seed = { hex, ...o };
|
|
@@ -2369,10 +2373,10 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2369
2373
|
stops: spec.stops.map((s) => s.stop === 10 && s.require?.metric === "wcag" ? { ...s, require: { ...s.require, target: 6.5 } } : s)
|
|
2370
2374
|
};
|
|
2371
2375
|
}
|
|
2372
|
-
const ctaReq = spec.roles.find((r) => r.role ===
|
|
2373
|
-
const hoverReq = spec.roles.find((r) => r.role ===
|
|
2374
|
-
const pressedReq = spec.roles.find((r) => r.role ===
|
|
2375
|
-
if (!ctaReq) throw new Error(
|
|
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`);
|
|
2376
2380
|
const onFillEnforce = ctx.opts?.enforceOnFillContrast ?? spec.ons.onFill.enforce;
|
|
2377
2381
|
const dctx = mode === "dark" ? buildDarkContext(ctx, ctaReq.floorL) : null;
|
|
2378
2382
|
const stops = [];
|
|
@@ -2391,8 +2395,6 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2391
2395
|
const textGround = ctx.opts?.textGround?.[mode];
|
|
2392
2396
|
const groundOf = (req, stop) => textGround && stop >= 9 ? textGround : refOf(wcagAnchorStop(req, stop), stop);
|
|
2393
2397
|
const apcaGroundOf = (req, stop) => textGround && stop >= 9 ? textGround : refOf(declaredAnchor(req), stop);
|
|
2394
|
-
const NEUTRAL_P3_WORST_SHIP_Y = { light: 0.845015, dark: 0.014247 };
|
|
2395
|
-
const NEUTRAL_W80_WORST_SHIP_Y = { light: 0.506433, dark: 0.074262 };
|
|
2396
2398
|
const maxLForOf = (req, forWhom, withMargin) => {
|
|
2397
2399
|
if (req.metric === "wcag") {
|
|
2398
2400
|
const g2 = groundOf(req, forWhom);
|
|
@@ -2501,11 +2503,18 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2501
2503
|
const own = contrastRatio(y2, anchorShipY);
|
|
2502
2504
|
return paperBound ? Math.min(own, contrastRatio(y2, crossBoundY)) : own;
|
|
2503
2505
|
};
|
|
2504
|
-
|
|
2506
|
+
const highlighterBoundY = ctx.opts?.crossHighlighterBoundY && sp.stop >= 10 && !textGround ? ctx.opts.crossHighlighterBoundY[mode] : void 0;
|
|
2507
|
+
const highlighterRatio = (L) => highlighterBoundY === void 0 ? Infinity : contrastRatio(shippedY(L, placed.C, placed.H), highlighterBoundY);
|
|
2508
|
+
const paperBoundY = ctx.opts?.crossPaperBoundY && sp.stop >= 8 && !textGround ? ctx.opts.crossPaperBoundY[mode] : void 0;
|
|
2509
|
+
const paperBar = sp.stop === 8 ? STOP_8_NONTEXT_CONTRAST : sp.stop === 9 ? PENCIL_9_CONTRAST : PEN_10_CONTRAST;
|
|
2510
|
+
const paperRatio = (L) => paperBoundY === void 0 ? Infinity : contrastRatio(shippedY(L, placed.C, placed.H), paperBoundY);
|
|
2511
|
+
const target = sp.require.target;
|
|
2512
|
+
const clears = (L) => shipRatio(L) >= target && highlighterRatio(L) >= PEN_10_CONTRAST && paperRatio(L) >= paperBar;
|
|
2513
|
+
if (!clears(placed.L)) {
|
|
2505
2514
|
const away = shippedY(placed.L, placed.C, placed.H) < anchorShipY ? -1 : 1;
|
|
2506
2515
|
let L2 = placed.L;
|
|
2507
|
-
for (let i = 0; i < 100 &&
|
|
2508
|
-
if (
|
|
2516
|
+
for (let i = 0; i < 100 && !clears(L2); i++) L2 += away * 1e-3;
|
|
2517
|
+
if (clears(L2)) {
|
|
2509
2518
|
placed = { ...placed, L: L2 };
|
|
2510
2519
|
clamped = true;
|
|
2511
2520
|
}
|
|
@@ -2516,6 +2525,8 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2516
2525
|
shipRatio(placed.L)
|
|
2517
2526
|
);
|
|
2518
2527
|
if (got < sp.require.target - 1e-3) unresolvable = `stop ${sp.stop}: contrast ${got.toFixed(2)} < required ${sp.require.target}`;
|
|
2528
|
+
else if (highlighterRatio(placed.L) < PEN_10_CONTRAST - 1e-3) unresolvable = `stop ${sp.stop}: contrast ${highlighterRatio(placed.L).toFixed(2)} < required ${PEN_10_CONTRAST} on the worst chromatic highlighter-20`;
|
|
2529
|
+
else if (paperRatio(placed.L) < paperBar - 1e-3) unresolvable = `stop ${sp.stop}: contrast ${paperRatio(placed.L).toFixed(2)} < required ${paperBar} on the worst chromatic paper-5`;
|
|
2519
2530
|
} else if (sp.require?.metric === "apca") {
|
|
2520
2531
|
const ga = apcaGroundOf(sp.require, sp.stop);
|
|
2521
2532
|
const refA = apcaYAt(ga.L, ga.C, ga.H);
|
|
@@ -2568,13 +2579,13 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2568
2579
|
repelled = true;
|
|
2569
2580
|
}
|
|
2570
2581
|
}
|
|
2571
|
-
cta = emitRole(
|
|
2582
|
+
cta = emitRole(STAMP_FILL, light9L, ctaCFor(light9L) * ctaCMul, ctaH);
|
|
2572
2583
|
const hCFor = (L) => ctx.cAt("light", L, (hoverReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul;
|
|
2573
2584
|
const pCFor = (L) => ctx.cAt("light", L, (pressedReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul;
|
|
2574
2585
|
const hL = stateFillL(light9L, "light", 1);
|
|
2575
2586
|
const pL = stateFillL(light9L, "light", 2);
|
|
2576
|
-
ctaHover = emitRole(
|
|
2577
|
-
ctaPressed = emitRole(
|
|
2587
|
+
ctaHover = emitRole(STAMP_FILL_HOVER, hL, hCFor(hL), ctaH);
|
|
2588
|
+
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, pCFor(pL), ctaH);
|
|
2578
2589
|
if (light9L !== ctx.scaleL) {
|
|
2579
2590
|
cta.enforced = true;
|
|
2580
2591
|
ctaHover.enforced = true;
|
|
@@ -2597,19 +2608,19 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2597
2608
|
const cta9L = ctx.opts?.darkCtaFlatApp !== void 0 ? flatDarkCtaL(d, (L) => ctx.cAt("dark", L, d.darkC9), ctx.darkCtaH, ctx.opts.darkCtaFlatApp) : d.dark9L;
|
|
2598
2609
|
const dCFor = (L) => ctx.cAt("dark", L, d.darkC9);
|
|
2599
2610
|
const dTrio = (baseL) => [stateFillL(baseL, "dark", 1), stateFillL(baseL, "dark", 2)];
|
|
2600
|
-
cta = emitRole(
|
|
2611
|
+
cta = emitRole(STAMP_FILL, cta9L, dCFor(cta9L), ctx.darkCtaH);
|
|
2601
2612
|
{
|
|
2602
2613
|
const [hL, pL] = dTrio(cta9L);
|
|
2603
|
-
ctaHover = emitRole(
|
|
2604
|
-
ctaPressed = emitRole(
|
|
2614
|
+
ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
|
|
2615
|
+
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
|
|
2605
2616
|
}
|
|
2606
2617
|
onFillIsWhite = onFillIsWhiteDarkAt(cta.L, cta.C, cta.H, enforceLc !== void 0 ? false : onFillEnforce);
|
|
2607
2618
|
const enforcedL = enforceLc !== void 0 ? ctaDarkEnforcedLApca(ctx, cta, onFillIsWhite, onFillEnforce, enforceLc) : ctaDarkEnforcedL(ctx, cta, onFillIsWhite, onFillEnforce);
|
|
2608
2619
|
if (enforcedL !== null) {
|
|
2609
|
-
cta = emitRole(
|
|
2620
|
+
cta = emitRole(STAMP_FILL, enforcedL, dCFor(enforcedL), ctx.darkCtaH);
|
|
2610
2621
|
const [hL, pL] = dTrio(enforcedL);
|
|
2611
|
-
ctaHover = emitRole(
|
|
2612
|
-
ctaPressed = emitRole(
|
|
2622
|
+
ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
|
|
2623
|
+
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
|
|
2613
2624
|
cta.enforced = true;
|
|
2614
2625
|
ctaHover.enforced = true;
|
|
2615
2626
|
ctaPressed.enforced = true;
|
|
@@ -2618,10 +2629,10 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2618
2629
|
const [capLoL, capHiL] = CTA_CLEARANCE_CAPS;
|
|
2619
2630
|
const clearedL = ctaDarkDualGateL(cta, ctx.darkCtaH, onFillIsWhite, onFillEnforce, coLc + APCA_ENFORCE_MARGIN_LC, capLoL, capHiL);
|
|
2620
2631
|
if (Math.abs(clearedL - cta.L) > 1e-9) {
|
|
2621
|
-
cta = emitRole(
|
|
2632
|
+
cta = emitRole(STAMP_FILL, clearedL, dCFor(clearedL), ctx.darkCtaH);
|
|
2622
2633
|
const [hL, pL] = dTrio(clearedL);
|
|
2623
|
-
ctaHover = emitRole(
|
|
2624
|
-
ctaPressed = emitRole(
|
|
2634
|
+
ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
|
|
2635
|
+
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
|
|
2625
2636
|
cta.enforced = true;
|
|
2626
2637
|
ctaHover.enforced = true;
|
|
2627
2638
|
ctaPressed.enforced = true;
|
|
@@ -2630,10 +2641,10 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2630
2641
|
if (ctx.opts?.ctaSolve) {
|
|
2631
2642
|
const exitL = solveDarkCtaExit(cta, dCFor, ctx.darkCtaH, ctx.opts.ctaSolve.redDark, enforceLc, coLc);
|
|
2632
2643
|
if (exitL !== null) {
|
|
2633
|
-
cta = emitRole(
|
|
2644
|
+
cta = emitRole(STAMP_FILL, exitL, dCFor(exitL), ctx.darkCtaH);
|
|
2634
2645
|
const [hL, pL] = dTrio(exitL);
|
|
2635
|
-
ctaHover = emitRole(
|
|
2636
|
-
ctaPressed = emitRole(
|
|
2646
|
+
ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
|
|
2647
|
+
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
|
|
2637
2648
|
cta.enforced = true;
|
|
2638
2649
|
ctaHover.enforced = true;
|
|
2639
2650
|
ctaPressed.enforced = true;
|
|
@@ -2715,7 +2726,11 @@ function generateNeutralScale(brandH, level = "default", contrastProfile) {
|
|
|
2715
2726
|
const scale = generateScale(grayHex, "neutral", "light", {
|
|
2716
2727
|
chromaCurve: curve,
|
|
2717
2728
|
enforceOnFillContrast: true,
|
|
2718
|
-
contrastProfile
|
|
2729
|
+
contrastProfile,
|
|
2730
|
+
// T13: the neutral's pens clear every chromatic family's highlighter-20 (the symmetric
|
|
2731
|
+
// pen guarantee); the chromatic ramps clear the neutral's through NEUTRAL_W80_WORST_SHIP_Y
|
|
2732
|
+
crossHighlighterBoundY: CHROMATIC_W80_WORST_SHIP_Y,
|
|
2733
|
+
crossPaperBoundY: CHROMATIC_P3_WORST_SHIP_Y
|
|
2719
2734
|
});
|
|
2720
2735
|
const asCta = (stop, src) => makeStop(stop, src.L, src.C, src.H);
|
|
2721
2736
|
scale.cta = asCta(9, scale.light[3]);
|
|
@@ -3962,9 +3977,14 @@ function themeToFigma(r, input) {
|
|
|
3962
3977
|
// src/engine/requirements/dtcg.ts
|
|
3963
3978
|
var EXT_KEY = "org.okchroma.requirement";
|
|
3964
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
|
+
};
|
|
3965
3985
|
var hexToValue = (hex) => {
|
|
3966
3986
|
const h = hex.replace("#", "");
|
|
3967
|
-
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;
|
|
3968
3988
|
return { colorSpace: "srgb", components: [c(0), c(2), c(4)], alpha: 1, hex: hex.toLowerCase() };
|
|
3969
3989
|
};
|
|
3970
3990
|
function emitDtcgRamp(hex, mode, groupName, spec) {
|
|
@@ -3980,9 +4000,9 @@ function emitDtcgRamp(hex, mode, groupName, spec) {
|
|
|
3980
4000
|
group[String(sp.stop)] = { $type: "color", $value: hexToValue(st.hex), $extensions: { [EXT_KEY]: { resolver: RESOLVER_ID, seed: seedRef, mode, ...sp } } };
|
|
3981
4001
|
}
|
|
3982
4002
|
const roleVal = {
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
4003
|
+
[STAMP_FILL]: ramp.roles.cta,
|
|
4004
|
+
[STAMP_FILL_HOVER]: ramp.roles.ctaHover,
|
|
4005
|
+
[STAMP_FILL_PRESSED]: ramp.roles.ctaPressed
|
|
3986
4006
|
};
|
|
3987
4007
|
for (const rr of spec.roles) {
|
|
3988
4008
|
const rv = roleVal[rr.role];
|
|
@@ -4002,6 +4022,7 @@ function parseToken(token) {
|
|
|
4002
4022
|
if (isRole) {
|
|
4003
4023
|
for (const k of ["role", "produce", "floorL", "chromaMult"]) if (req[k] === void 0) throw new Error(`role requirement missing "${k}"`);
|
|
4004
4024
|
}
|
|
4025
|
+
if (isRole && LEGACY_ROLE_NAMES[req.role]) req.role = LEGACY_ROLE_NAMES[req.role];
|
|
4005
4026
|
return { req, seedRef: seed, mode };
|
|
4006
4027
|
}
|
|
4007
4028
|
function resolveDtcgRamp(group) {
|
package/dist-lib/index.mjs
CHANGED
|
@@ -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:
|
|
872
|
-
{ role:
|
|
873
|
-
{ role:
|
|
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:
|
|
904
|
-
{ role:
|
|
905
|
-
{ role:
|
|
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
|
};
|
|
@@ -2279,6 +2279,10 @@ function solveDarkCtaExit(cur, cFor, darkH, redDark, enforceLc, coLc) {
|
|
|
2279
2279
|
var oklchToSrgb = (L, C2, H2) => Object.values(srgbEmitChannels({ L, C: C2, H: H2 })).map((c) => Math.max(0, Math.min(1, c)));
|
|
2280
2280
|
var toHex = (rgb) => "#" + rgb.map((c) => Math.round(c * 255).toString(16).padStart(2, "0")).join("");
|
|
2281
2281
|
var CTA_CLEARANCE_CAPS = [0.05, 0.92];
|
|
2282
|
+
var CHROMATIC_W80_WORST_SHIP_Y = { light: 0.459357, dark: 0.074936 };
|
|
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 };
|
|
2282
2286
|
function resolveRamp(hex, mode, spec, opts) {
|
|
2283
2287
|
const o = hexToOklch(hex);
|
|
2284
2288
|
const seed = { hex, ...o };
|
|
@@ -2290,10 +2294,10 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2290
2294
|
stops: spec.stops.map((s) => s.stop === 10 && s.require?.metric === "wcag" ? { ...s, require: { ...s.require, target: 6.5 } } : s)
|
|
2291
2295
|
};
|
|
2292
2296
|
}
|
|
2293
|
-
const ctaReq = spec.roles.find((r) => r.role ===
|
|
2294
|
-
const hoverReq = spec.roles.find((r) => r.role ===
|
|
2295
|
-
const pressedReq = spec.roles.find((r) => r.role ===
|
|
2296
|
-
if (!ctaReq) throw new Error(
|
|
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`);
|
|
2297
2301
|
const onFillEnforce = ctx.opts?.enforceOnFillContrast ?? spec.ons.onFill.enforce;
|
|
2298
2302
|
const dctx = mode === "dark" ? buildDarkContext(ctx, ctaReq.floorL) : null;
|
|
2299
2303
|
const stops = [];
|
|
@@ -2312,8 +2316,6 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2312
2316
|
const textGround = ctx.opts?.textGround?.[mode];
|
|
2313
2317
|
const groundOf = (req, stop) => textGround && stop >= 9 ? textGround : refOf(wcagAnchorStop(req, stop), stop);
|
|
2314
2318
|
const apcaGroundOf = (req, stop) => textGround && stop >= 9 ? textGround : refOf(declaredAnchor(req), stop);
|
|
2315
|
-
const NEUTRAL_P3_WORST_SHIP_Y = { light: 0.845015, dark: 0.014247 };
|
|
2316
|
-
const NEUTRAL_W80_WORST_SHIP_Y = { light: 0.506433, dark: 0.074262 };
|
|
2317
2319
|
const maxLForOf = (req, forWhom, withMargin) => {
|
|
2318
2320
|
if (req.metric === "wcag") {
|
|
2319
2321
|
const g2 = groundOf(req, forWhom);
|
|
@@ -2422,11 +2424,18 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2422
2424
|
const own = contrastRatio(y2, anchorShipY);
|
|
2423
2425
|
return paperBound ? Math.min(own, contrastRatio(y2, crossBoundY)) : own;
|
|
2424
2426
|
};
|
|
2425
|
-
|
|
2427
|
+
const highlighterBoundY = ctx.opts?.crossHighlighterBoundY && sp.stop >= 10 && !textGround ? ctx.opts.crossHighlighterBoundY[mode] : void 0;
|
|
2428
|
+
const highlighterRatio = (L) => highlighterBoundY === void 0 ? Infinity : contrastRatio(shippedY(L, placed.C, placed.H), highlighterBoundY);
|
|
2429
|
+
const paperBoundY = ctx.opts?.crossPaperBoundY && sp.stop >= 8 && !textGround ? ctx.opts.crossPaperBoundY[mode] : void 0;
|
|
2430
|
+
const paperBar = sp.stop === 8 ? STOP_8_NONTEXT_CONTRAST : sp.stop === 9 ? PENCIL_9_CONTRAST : PEN_10_CONTRAST;
|
|
2431
|
+
const paperRatio = (L) => paperBoundY === void 0 ? Infinity : contrastRatio(shippedY(L, placed.C, placed.H), paperBoundY);
|
|
2432
|
+
const target = sp.require.target;
|
|
2433
|
+
const clears = (L) => shipRatio(L) >= target && highlighterRatio(L) >= PEN_10_CONTRAST && paperRatio(L) >= paperBar;
|
|
2434
|
+
if (!clears(placed.L)) {
|
|
2426
2435
|
const away = shippedY(placed.L, placed.C, placed.H) < anchorShipY ? -1 : 1;
|
|
2427
2436
|
let L2 = placed.L;
|
|
2428
|
-
for (let i = 0; i < 100 &&
|
|
2429
|
-
if (
|
|
2437
|
+
for (let i = 0; i < 100 && !clears(L2); i++) L2 += away * 1e-3;
|
|
2438
|
+
if (clears(L2)) {
|
|
2430
2439
|
placed = { ...placed, L: L2 };
|
|
2431
2440
|
clamped = true;
|
|
2432
2441
|
}
|
|
@@ -2437,6 +2446,8 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2437
2446
|
shipRatio(placed.L)
|
|
2438
2447
|
);
|
|
2439
2448
|
if (got < sp.require.target - 1e-3) unresolvable = `stop ${sp.stop}: contrast ${got.toFixed(2)} < required ${sp.require.target}`;
|
|
2449
|
+
else if (highlighterRatio(placed.L) < PEN_10_CONTRAST - 1e-3) unresolvable = `stop ${sp.stop}: contrast ${highlighterRatio(placed.L).toFixed(2)} < required ${PEN_10_CONTRAST} on the worst chromatic highlighter-20`;
|
|
2450
|
+
else if (paperRatio(placed.L) < paperBar - 1e-3) unresolvable = `stop ${sp.stop}: contrast ${paperRatio(placed.L).toFixed(2)} < required ${paperBar} on the worst chromatic paper-5`;
|
|
2440
2451
|
} else if (sp.require?.metric === "apca") {
|
|
2441
2452
|
const ga = apcaGroundOf(sp.require, sp.stop);
|
|
2442
2453
|
const refA = apcaYAt(ga.L, ga.C, ga.H);
|
|
@@ -2489,13 +2500,13 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2489
2500
|
repelled = true;
|
|
2490
2501
|
}
|
|
2491
2502
|
}
|
|
2492
|
-
cta = emitRole(
|
|
2503
|
+
cta = emitRole(STAMP_FILL, light9L, ctaCFor(light9L) * ctaCMul, ctaH);
|
|
2493
2504
|
const hCFor = (L) => ctx.cAt("light", L, (hoverReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul;
|
|
2494
2505
|
const pCFor = (L) => ctx.cAt("light", L, (pressedReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul;
|
|
2495
2506
|
const hL = stateFillL(light9L, "light", 1);
|
|
2496
2507
|
const pL = stateFillL(light9L, "light", 2);
|
|
2497
|
-
ctaHover = emitRole(
|
|
2498
|
-
ctaPressed = emitRole(
|
|
2508
|
+
ctaHover = emitRole(STAMP_FILL_HOVER, hL, hCFor(hL), ctaH);
|
|
2509
|
+
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, pCFor(pL), ctaH);
|
|
2499
2510
|
if (light9L !== ctx.scaleL) {
|
|
2500
2511
|
cta.enforced = true;
|
|
2501
2512
|
ctaHover.enforced = true;
|
|
@@ -2518,19 +2529,19 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2518
2529
|
const cta9L = ctx.opts?.darkCtaFlatApp !== void 0 ? flatDarkCtaL(d, (L) => ctx.cAt("dark", L, d.darkC9), ctx.darkCtaH, ctx.opts.darkCtaFlatApp) : d.dark9L;
|
|
2519
2530
|
const dCFor = (L) => ctx.cAt("dark", L, d.darkC9);
|
|
2520
2531
|
const dTrio = (baseL) => [stateFillL(baseL, "dark", 1), stateFillL(baseL, "dark", 2)];
|
|
2521
|
-
cta = emitRole(
|
|
2532
|
+
cta = emitRole(STAMP_FILL, cta9L, dCFor(cta9L), ctx.darkCtaH);
|
|
2522
2533
|
{
|
|
2523
2534
|
const [hL, pL] = dTrio(cta9L);
|
|
2524
|
-
ctaHover = emitRole(
|
|
2525
|
-
ctaPressed = emitRole(
|
|
2535
|
+
ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
|
|
2536
|
+
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
|
|
2526
2537
|
}
|
|
2527
2538
|
onFillIsWhite = onFillIsWhiteDarkAt(cta.L, cta.C, cta.H, enforceLc !== void 0 ? false : onFillEnforce);
|
|
2528
2539
|
const enforcedL = enforceLc !== void 0 ? ctaDarkEnforcedLApca(ctx, cta, onFillIsWhite, onFillEnforce, enforceLc) : ctaDarkEnforcedL(ctx, cta, onFillIsWhite, onFillEnforce);
|
|
2529
2540
|
if (enforcedL !== null) {
|
|
2530
|
-
cta = emitRole(
|
|
2541
|
+
cta = emitRole(STAMP_FILL, enforcedL, dCFor(enforcedL), ctx.darkCtaH);
|
|
2531
2542
|
const [hL, pL] = dTrio(enforcedL);
|
|
2532
|
-
ctaHover = emitRole(
|
|
2533
|
-
ctaPressed = emitRole(
|
|
2543
|
+
ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
|
|
2544
|
+
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
|
|
2534
2545
|
cta.enforced = true;
|
|
2535
2546
|
ctaHover.enforced = true;
|
|
2536
2547
|
ctaPressed.enforced = true;
|
|
@@ -2539,10 +2550,10 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2539
2550
|
const [capLoL, capHiL] = CTA_CLEARANCE_CAPS;
|
|
2540
2551
|
const clearedL = ctaDarkDualGateL(cta, ctx.darkCtaH, onFillIsWhite, onFillEnforce, coLc + APCA_ENFORCE_MARGIN_LC, capLoL, capHiL);
|
|
2541
2552
|
if (Math.abs(clearedL - cta.L) > 1e-9) {
|
|
2542
|
-
cta = emitRole(
|
|
2553
|
+
cta = emitRole(STAMP_FILL, clearedL, dCFor(clearedL), ctx.darkCtaH);
|
|
2543
2554
|
const [hL, pL] = dTrio(clearedL);
|
|
2544
|
-
ctaHover = emitRole(
|
|
2545
|
-
ctaPressed = emitRole(
|
|
2555
|
+
ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
|
|
2556
|
+
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
|
|
2546
2557
|
cta.enforced = true;
|
|
2547
2558
|
ctaHover.enforced = true;
|
|
2548
2559
|
ctaPressed.enforced = true;
|
|
@@ -2551,10 +2562,10 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2551
2562
|
if (ctx.opts?.ctaSolve) {
|
|
2552
2563
|
const exitL = solveDarkCtaExit(cta, dCFor, ctx.darkCtaH, ctx.opts.ctaSolve.redDark, enforceLc, coLc);
|
|
2553
2564
|
if (exitL !== null) {
|
|
2554
|
-
cta = emitRole(
|
|
2565
|
+
cta = emitRole(STAMP_FILL, exitL, dCFor(exitL), ctx.darkCtaH);
|
|
2555
2566
|
const [hL, pL] = dTrio(exitL);
|
|
2556
|
-
ctaHover = emitRole(
|
|
2557
|
-
ctaPressed = emitRole(
|
|
2567
|
+
ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
|
|
2568
|
+
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
|
|
2558
2569
|
cta.enforced = true;
|
|
2559
2570
|
ctaHover.enforced = true;
|
|
2560
2571
|
ctaPressed.enforced = true;
|
|
@@ -2636,7 +2647,11 @@ function generateNeutralScale(brandH, level = "default", contrastProfile) {
|
|
|
2636
2647
|
const scale = generateScale(grayHex, "neutral", "light", {
|
|
2637
2648
|
chromaCurve: curve,
|
|
2638
2649
|
enforceOnFillContrast: true,
|
|
2639
|
-
contrastProfile
|
|
2650
|
+
contrastProfile,
|
|
2651
|
+
// T13: the neutral's pens clear every chromatic family's highlighter-20 (the symmetric
|
|
2652
|
+
// pen guarantee); the chromatic ramps clear the neutral's through NEUTRAL_W80_WORST_SHIP_Y
|
|
2653
|
+
crossHighlighterBoundY: CHROMATIC_W80_WORST_SHIP_Y,
|
|
2654
|
+
crossPaperBoundY: CHROMATIC_P3_WORST_SHIP_Y
|
|
2640
2655
|
});
|
|
2641
2656
|
const asCta = (stop, src) => makeStop(stop, src.L, src.C, src.H);
|
|
2642
2657
|
scale.cta = asCta(9, scale.light[3]);
|
|
@@ -3883,9 +3898,14 @@ function themeToFigma(r, input) {
|
|
|
3883
3898
|
// src/engine/requirements/dtcg.ts
|
|
3884
3899
|
var EXT_KEY = "org.okchroma.requirement";
|
|
3885
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
|
+
};
|
|
3886
3906
|
var hexToValue = (hex) => {
|
|
3887
3907
|
const h = hex.replace("#", "");
|
|
3888
|
-
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;
|
|
3889
3909
|
return { colorSpace: "srgb", components: [c(0), c(2), c(4)], alpha: 1, hex: hex.toLowerCase() };
|
|
3890
3910
|
};
|
|
3891
3911
|
function emitDtcgRamp(hex, mode, groupName, spec) {
|
|
@@ -3901,9 +3921,9 @@ function emitDtcgRamp(hex, mode, groupName, spec) {
|
|
|
3901
3921
|
group[String(sp.stop)] = { $type: "color", $value: hexToValue(st.hex), $extensions: { [EXT_KEY]: { resolver: RESOLVER_ID, seed: seedRef, mode, ...sp } } };
|
|
3902
3922
|
}
|
|
3903
3923
|
const roleVal = {
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3924
|
+
[STAMP_FILL]: ramp.roles.cta,
|
|
3925
|
+
[STAMP_FILL_HOVER]: ramp.roles.ctaHover,
|
|
3926
|
+
[STAMP_FILL_PRESSED]: ramp.roles.ctaPressed
|
|
3907
3927
|
};
|
|
3908
3928
|
for (const rr of spec.roles) {
|
|
3909
3929
|
const rv = roleVal[rr.role];
|
|
@@ -3923,6 +3943,7 @@ function parseToken(token) {
|
|
|
3923
3943
|
if (isRole) {
|
|
3924
3944
|
for (const k of ["role", "produce", "floorL", "chromaMult"]) if (req[k] === void 0) throw new Error(`role requirement missing "${k}"`);
|
|
3925
3945
|
}
|
|
3946
|
+
if (isRole && LEGACY_ROLE_NAMES[req.role]) req.role = LEGACY_ROLE_NAMES[req.role];
|
|
3926
3947
|
return { req, seedRef: seed, mode };
|
|
3927
3948
|
}
|
|
3928
3949
|
function resolveDtcgRamp(group) {
|
|
@@ -89,6 +89,14 @@ export interface GenerateOptions {
|
|
|
89
89
|
H: number;
|
|
90
90
|
};
|
|
91
91
|
};
|
|
92
|
+
crossHighlighterBoundY?: {
|
|
93
|
+
light: number;
|
|
94
|
+
dark: number;
|
|
95
|
+
};
|
|
96
|
+
crossPaperBoundY?: {
|
|
97
|
+
light: number;
|
|
98
|
+
dark: number;
|
|
99
|
+
};
|
|
92
100
|
deltaHKPlace?: boolean;
|
|
93
101
|
deltaChromaEq?: boolean;
|
|
94
102
|
deltaLiftFloor?: boolean;
|
|
@@ -43,4 +43,21 @@ export type ResolvedRamp = {
|
|
|
43
43
|
};
|
|
44
44
|
};
|
|
45
45
|
export type { ResolveOpts };
|
|
46
|
+
export declare const CTA_CLEARANCE_CAPS: [number, number];
|
|
47
|
+
export declare const CHROMATIC_W80_WORST_SHIP_Y: {
|
|
48
|
+
readonly light: 0.459357;
|
|
49
|
+
readonly dark: 0.074936;
|
|
50
|
+
};
|
|
51
|
+
export declare const CHROMATIC_P3_WORST_SHIP_Y: {
|
|
52
|
+
readonly light: 0.836007;
|
|
53
|
+
readonly dark: 0.016041;
|
|
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
|
+
};
|
|
46
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 =
|
|
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.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|