okchroma 0.9.0 → 0.9.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 +136 -105
- package/dist-lib/index.cjs +58 -57
- package/dist-lib/index.mjs +58 -57
- package/dist-lib/types/engine/archetypes.d.ts +1 -1
- package/dist-lib/types/engine/requirements/producers.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,30 +1,18 @@
|
|
|
1
1
|
# OKChroma
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Every family (neutral, brand, brand-alt, critical, warning, positive, info) carries the
|
|
9
|
-
same scale: 3 papers, 4 chalks, 1 highlighter, 1 pencil, 2 pens, plus the stamp (the
|
|
10
|
-
solid fill at rest and at one and two steps, its edge, and its text). The neutral adds
|
|
11
|
-
the two poles. Two link trios and the two brand seeds complete the set. Light and dark
|
|
12
|
-
resolve together and ship on the same names, so a token is mapped once and holds for any
|
|
13
|
-
brand.
|
|
14
|
-
|
|
15
|
-
Contrast is built into the math, not checked afterwards: highlighter reads on every paper at
|
|
16
|
-
3:1 (the non-text bar), pencil at 4.5:1, pen at 4.5:1 on every paper and chalk in
|
|
17
|
-
both directions; the stamp's text passes 4.5:1 on its fill. APCA is used once, as a booster
|
|
18
|
-
that nudges the stamp fill until its text reads at Lc 65. Each claim, its scope, and the audit that
|
|
19
|
-
proves it: [Guarantees](https://egerrity.github.io/okchroma/#/docs/guarantees).
|
|
20
|
-
|
|
21
|
-
The engine emits primitives only: values it calculates from the seed. Elevation planes,
|
|
22
|
-
shadows, opacity ladders and state layers are a design system's to author on top of these,
|
|
23
|
-
and [docs/agents.md](docs/agents.md) says which primitive each of those decisions reads.
|
|
3
|
+
okchroma is a color engine. A fixed set of formulas generates a color system's base
|
|
4
|
+
tokens from a seed color and a set of options. Each token gets a light and a dark value,
|
|
5
|
+
and the contrast requirements are solved while the values are generated. The set is
|
|
6
|
+
emitted as CSS custom properties, as DTCG documents, and as Figma variables.
|
|
24
7
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
8
|
+
The seed is one brand hex. The options add a second seed or derive a companion from the
|
|
9
|
+
first, pin a family's fill to a lightness anchor, and pick the contrast profile. They are
|
|
10
|
+
listed with the API: [Install and API](https://egerrity.github.io/okchroma/#/docs/install).
|
|
11
|
+
|
|
12
|
+
okchroma writes base tokens, designed for use under any semantic layer, including one
|
|
13
|
+
that must cover many themes. It writes no semantic tokens. The semantic tokens in this
|
|
14
|
+
repo, in [demo/semantic.css](demo/semantic.css), style the demo site and are not engine
|
|
15
|
+
output.
|
|
28
16
|
|
|
29
17
|
## Install
|
|
30
18
|
|
|
@@ -32,8 +20,9 @@ and [docs/agents.md](docs/agents.md) says which primitive each of those decision
|
|
|
32
20
|
npm install okchroma
|
|
33
21
|
```
|
|
34
22
|
|
|
35
|
-
ESM and CommonJS builds with TypeScript declarations
|
|
36
|
-
|
|
23
|
+
The package ships ESM and CommonJS builds with TypeScript declarations. It has no
|
|
24
|
+
runtime dependencies: the one library the engine uses, for perceptual color distance, is
|
|
25
|
+
bundled at build time. Node 18 or later; no DOM.
|
|
37
26
|
|
|
38
27
|
```ts
|
|
39
28
|
import { resolveTheme, brandCss, signalsCss, themeTokens, tokensToDtcg, neutralTintHue } from 'okchroma'
|
|
@@ -58,107 +47,149 @@ const { light, dark } = tokensToDtcg(tokens)
|
|
|
58
47
|
The Figma tree comes from `themeToFigma`. Signatures, the full input, and an end-to-end
|
|
59
48
|
example: [Install and API](https://egerrity.github.io/okchroma/#/docs/install).
|
|
60
49
|
|
|
61
|
-
##
|
|
62
|
-
|
|
63
|
-
Every
|
|
64
|
-
`neutral
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
`
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
|
74
|
-
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
hexes (or `rgba()` where a token carries alpha); stops whose chroma exceeds sRGB get a
|
|
102
|
-
`color(display-p3 …)` override behind a browser gate. The repo's `npm run generate`
|
|
103
|
-
writes the signal block to `dist/signals.css`.
|
|
104
|
-
- The structured emit: `themeTokens` reads the CSS emission back into one object per
|
|
105
|
-
mode with every reference resolved, for consumers with no cascade.
|
|
106
|
-
- DTCG: `tokensToDtcg` returns one Design Tokens Format Module 2025.10 document per mode,
|
|
107
|
-
every token with `$type`, an sRGB `$value` whose components and hex are the same 8-bit
|
|
108
|
-
color (or an alias where the CSS writes a reference), and a `$description`. The two join
|
|
109
|
-
through a resolver document. The format: [docs/schema.md](docs/schema.md). The repo's
|
|
110
|
-
`npm run tokens:emit -- '#E93D82' acme` writes both files. Pass `{ rootGroup: 'color' }`,
|
|
111
|
-
or `--root color` to the script, to nest every path and alias under that group.
|
|
112
|
-
- Figma: `themeToFigma` returns a light and a dark group tree on the same paths; the
|
|
113
|
-
extended plugin writes it into a file.
|
|
50
|
+
## The tokens
|
|
51
|
+
|
|
52
|
+
Every family emits the same set of names, so a name says the token's job whichever
|
|
53
|
+
family carries it. The families are `neutral`, `brand`, `brand-alt`, `critical`,
|
|
54
|
+
`warning`, `positive` and `info`; the neutral adds the two poles. The link rows and the
|
|
55
|
+
seeds sit outside the families.
|
|
56
|
+
|
|
57
|
+
| Token | Job | Contrast guarantee |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `{family}/paper-1` | background | none; the contrast stops are measured against the papers |
|
|
60
|
+
| `{family}/paper-3` | background | none |
|
|
61
|
+
| `{family}/paper-5` | background; the paper the contrast stops are solved against | none |
|
|
62
|
+
| `{family}/chalk-8` | decorative border, illustration, ground for inverted text; never text | none; the pens are measured against the chalks |
|
|
63
|
+
| `{family}/chalk-11` | the same | none |
|
|
64
|
+
| `{family}/chalk-15` | the same | none |
|
|
65
|
+
| `{family}/chalk-20` | the same | none |
|
|
66
|
+
| `{family}/highlighter-26` | focus ring, icon, large text; at a translucency, the state layer | 3:1 on the papers, the non-text bar |
|
|
67
|
+
| `{family}/pencil-47` | regular text; the emphasis fill | 4.5:1 on the papers |
|
|
68
|
+
| `{family}/pen-58` | text; inverted background | 4.5:1 on the papers and the chalks, both directions |
|
|
69
|
+
| `{family}/pen-70` | heavy-emphasis text; inverted background | 4.5:1 on the papers and the chalks, both directions |
|
|
70
|
+
| `neutral/paper-0` | white in light; in dark, the plane one seam below `paper-1` | none stated |
|
|
71
|
+
| `neutral/pen-100` | black in light, white in dark; the strongest text | none stated |
|
|
72
|
+
| `{family}/stamp-0` | the call-to-action fill at rest, solved per family and mode | `stamp-on` reads on it at 4.5:1 |
|
|
73
|
+
| `{family}/stamp-1` | the fill one step from rest | `stamp-on` reads on it at 4.5:1 |
|
|
74
|
+
| `{family}/stamp-2` | the fill two steps from rest | `stamp-on` reads on it at 4.5:1 |
|
|
75
|
+
| `{family}/stamp-edge` | a stroke: an offset where the fill sits close to the page, transparent elsewhere, so a component always renders it | none stated |
|
|
76
|
+
| `{family}/stamp-on` | the text on the fill, whichever pole passes; at alpha on a quiet fill | 4.5:1 on the three fills |
|
|
77
|
+
| `link/default-0` | link text on the papers, at rest | none stated |
|
|
78
|
+
| `link/default-1` | one step from rest | none stated |
|
|
79
|
+
| `link/default-2` | two steps from rest | none stated |
|
|
80
|
+
| `link/inverse-0` | link text on the pen ground, at rest | none stated |
|
|
81
|
+
| `link/inverse-1` | one step from rest | none stated |
|
|
82
|
+
| `link/inverse-2` | two steps from rest | none stated |
|
|
83
|
+
| `absolute/brand` | the seed as given; a reference value, not a UI color | none stated |
|
|
84
|
+
| `absolute/brand-alt` | the second seed as given | none stated |
|
|
85
|
+
|
|
86
|
+
The papers and chalks a stop is measured against are its own family's and the neutral's.
|
|
87
|
+
APCA is used once: the stamp fill also moves until its text clears Lc 65. The full
|
|
88
|
+
claims and their audits: [Guarantees](https://egerrity.github.io/okchroma/#/docs/guarantees).
|
|
89
|
+
The lightness targets behind the numbers: [docs/scale.md](docs/scale.md).
|
|
114
90
|
|
|
115
|
-
|
|
91
|
+
> The reserved-role-per-stop model is a conceptual nod to
|
|
92
|
+
> [Radix Colors](https://www.radix-ui.com/colors/docs/palette-composition/understanding-the-scale).
|
|
93
|
+
> It is not a dependency and does not touch the math; the color computation is original.
|
|
94
|
+
|
|
95
|
+
## The grammar
|
|
96
|
+
|
|
97
|
+
A name is a group and a leaf:
|
|
116
98
|
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
npm run demo:build # writes dist/signals.css and bundles the demo
|
|
120
|
-
npx serve . # open http://localhost:3000/demo/index.html
|
|
121
|
-
npm run dev # watch mode
|
|
99
|
+
```
|
|
100
|
+
group/leaf
|
|
122
101
|
```
|
|
123
102
|
|
|
124
|
-
`
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
103
|
+
The group is a family, `link`, or `absolute`. The leaf names a position, never a state;
|
|
104
|
+
a consumer's semantic layer names the states and aliases the positions.
|
|
105
|
+
|
|
106
|
+
| Leaf | Pattern | Example |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| a scale stop | `{instrument}-{number}` | `pen-58` |
|
|
109
|
+
| a stamp fill | `stamp-{step}` | `stamp-0` |
|
|
110
|
+
| the stamp's edge and text | `stamp-edge`, `stamp-on` | `stamp-on` |
|
|
111
|
+
| a link | `{posture}-{step}`, the postures `default` and `inverse` | `default-1` |
|
|
112
|
+
| a seed | the family word | `brand-alt` |
|
|
113
|
+
|
|
114
|
+
The instrument is the job. On the scale (paper, chalk, highlighter, pencil, pen) the
|
|
115
|
+
number is 100 minus the light-mode lightness target, so a bigger number is a stronger
|
|
116
|
+
color. In the brand identity (stamp, link) the number is the step from rest: 0 at rest,
|
|
117
|
+
1 one step away, 2 two steps.
|
|
118
|
+
|
|
119
|
+
Each output spells the same name its own way:
|
|
120
|
+
|
|
121
|
+
| Output | Spelling | `brand/stamp-0` becomes |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| CSS custom property | `--group-leaf` | `--brand-stamp-0` |
|
|
124
|
+
| Figma variable | `group/leaf`; the extended plugin writes it under `color/` | `color/brand/stamp-0` |
|
|
125
|
+
| DTCG token | nested as `group.leaf`, referenced as `{group.leaf}`; `rootGroup: 'color'` nests it under `color` | `{brand.stamp-0}` |
|
|
126
|
+
|
|
127
|
+
Which token to use for which job: [docs/agents.md](docs/agents.md).
|
|
128
|
+
|
|
129
|
+
## The outputs
|
|
130
|
+
|
|
131
|
+
- CSS: `brandCss` writes a `[data-brand="your-brand"]` block and a
|
|
132
|
+
`[data-brand="your-brand"][data-theme="dark"]` block; `signalsCss` writes the
|
|
133
|
+
brand-independent signal block at `:root`. Values are sRGB hexes, or `rgba()` where a
|
|
134
|
+
token carries alpha. A stop whose chroma exceeds sRGB gets a `color(display-p3 …)`
|
|
135
|
+
override behind a browser gate. In the repo, `npm run generate` writes the signal block
|
|
136
|
+
to `dist/signals.css`.
|
|
137
|
+
- Objects: `themeTokens` returns one object per mode with every reference resolved, for a
|
|
138
|
+
consumer with no cascade.
|
|
139
|
+
- DTCG: `tokensToDtcg` returns one document per mode. Each token has a `$type`, an sRGB
|
|
140
|
+
`$value` (or an alias where the CSS writes a reference) and a `$description`; a
|
|
141
|
+
resolver document joins the two modes. The format: [docs/schema.md](docs/schema.md).
|
|
142
|
+
In the repo, `npm run tokens:emit -- '#E93D82' your-brand` writes both files; `--root color`
|
|
143
|
+
(or `{ rootGroup: 'color' }` in code) nests the paths under that group.
|
|
144
|
+
- Figma: `themeToFigma` returns a light and a dark group tree on the same paths; the
|
|
145
|
+
extended plugin writes them into a file.
|
|
128
146
|
|
|
129
147
|
## Documentation
|
|
130
148
|
|
|
131
149
|
The docs site renders the mechanisms with live values from the engine:
|
|
132
150
|
[egerrity.github.io/okchroma/#/docs](https://egerrity.github.io/okchroma/#/docs).
|
|
133
151
|
|
|
134
|
-
- [Overview](https://egerrity.github.io/okchroma/#/docs/overview):
|
|
152
|
+
- [Overview](https://egerrity.github.io/okchroma/#/docs/overview): the input the engine needs, and the token roster it makes from it.
|
|
135
153
|
- [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.
|
|
136
|
-
- [Guarantees](https://egerrity.github.io/okchroma/#/docs/guarantees):
|
|
154
|
+
- [Guarantees](https://egerrity.github.io/okchroma/#/docs/guarantees): each claim as stated, and how it is verified.
|
|
137
155
|
- [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.
|
|
138
156
|
- [Signals and companions](https://egerrity.github.io/okchroma/#/docs/signals) and the [Reference](https://egerrity.github.io/okchroma/#/docs/reference): glossary, constants, option types.
|
|
139
157
|
|
|
140
|
-
In the repo:
|
|
141
|
-
|
|
142
|
-
[docs/
|
|
143
|
-
[docs/
|
|
144
|
-
|
|
145
|
-
[
|
|
158
|
+
In the repo:
|
|
159
|
+
|
|
160
|
+
- [docs/agents.md](docs/agents.md): the consumer contract, what each token means and the rules for using it.
|
|
161
|
+
- [docs/schema.md](docs/schema.md): the DTCG documents.
|
|
162
|
+
- [docs/scale.md](docs/scale.md): the scale and its declared targets.
|
|
163
|
+
- [docs/architecture.md](docs/architecture.md): the maintainer's map, modules, pipeline stages, data structures, the extended plugin's write path.
|
|
164
|
+
- [CHANGELOG.md](CHANGELOG.md).
|
|
165
|
+
|
|
166
|
+
## Run from source
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npm install
|
|
170
|
+
npm run demo:build # writes dist/signals.css and bundles the demo
|
|
171
|
+
npx serve . # open http://localhost:3000/demo/index.html
|
|
172
|
+
npm run dev # watch mode
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`npm run typecheck` runs the compiler. The audit scripts in `package.json`
|
|
176
|
+
(`npm run req:audit`, `npm run audit:guarantee`, `npm run audit:dtcg`, and the rest)
|
|
177
|
+
check the guarantees and the outputs. What each proves:
|
|
178
|
+
[How it is verified](https://egerrity.github.io/okchroma/#/docs/guarantees/how-it-is-verified).
|
|
146
179
|
|
|
147
180
|
## The Figma plugins
|
|
148
181
|
|
|
149
|
-
-
|
|
150
|
-
|
|
151
|
-
one
|
|
182
|
+
- OKChroma Extended (`plugin-ext/`) is the shipped Figma plugin. It needs the Figma
|
|
183
|
+
desktop app and a Figma Enterprise plan, because it writes extended variable
|
|
184
|
+
collections: one collection with light and dark modes, and an extension per brand that
|
|
152
185
|
overrides only what differs. Download and install steps:
|
|
153
186
|
[install page](https://egerrity.github.io/okchroma/install.html). Build from source with
|
|
154
187
|
`npm run plugin-ext:build`; see [plugin-ext/README.md](plugin-ext/README.md).
|
|
155
|
-
-
|
|
156
|
-
|
|
157
|
-
|
|
188
|
+
- OKChroma (`plugin/`, the community plugin) is not offered for download. It has no
|
|
189
|
+
rename table for the last scale change, so updating an installed copy would leave the
|
|
190
|
+
old names in the file. It builds with `npm run plugin:build` and imports from
|
|
158
191
|
`plugin/manifest.json`; a fresh file gets the current shape.
|
|
159
192
|
|
|
160
|
-
The demo and the plugins are front-ends. The product is the engine and what it emits.
|
|
161
|
-
|
|
162
193
|
## License
|
|
163
194
|
|
|
164
195
|
MIT
|
package/dist-lib/index.cjs
CHANGED
|
@@ -114,13 +114,13 @@ function stateStepL(restL, _mode, k) {
|
|
|
114
114
|
}
|
|
115
115
|
var OVERRIDE_LIGHT_BELOW = ARCHETYPES.find((a) => a.name === "rich").min;
|
|
116
116
|
var OVERRIDE_DARK_ABOVE = ARCHETYPES.find((a) => a.name === "light").min;
|
|
117
|
-
function stateFillL(restL, mode, k) {
|
|
117
|
+
function stateFillL(restL, mode, k, keepsPole) {
|
|
118
118
|
const natural = mode === "dark" ? 1 : -1;
|
|
119
119
|
const overridden = mode === "light" ? restL < OVERRIDE_LIGHT_BELOW : restL > OVERRIDE_DARK_ABOVE;
|
|
120
120
|
const preferred = overridden ? -natural : natural;
|
|
121
121
|
const fits = (d) => {
|
|
122
122
|
const p = restL + d * stateStepL(restL, mode, 2);
|
|
123
|
-
return p >= STATE_L_MIN && p <= STATE_L_MAX;
|
|
123
|
+
return p >= STATE_L_MIN && p <= STATE_L_MAX && (keepsPole?.(p) ?? true);
|
|
124
124
|
};
|
|
125
125
|
const dir = fits(preferred) ? preferred : fits(-preferred) ? -preferred : preferred;
|
|
126
126
|
const L = restL + dir * stateStepL(restL, mode, k);
|
|
@@ -2224,6 +2224,20 @@ function ctaDarkDualGateL(base, darkH, onFillTextIsWhite, enforce, coLc, capLoL,
|
|
|
2224
2224
|
const lLc = findLForBlackTextLc(startL, C2, darkH, coLc + APCA_SOLVE_MARGIN_LC, capHiL);
|
|
2225
2225
|
return Math.max(l45, Math.min(lLc, capHiL));
|
|
2226
2226
|
}
|
|
2227
|
+
function stepKeepsPole(mode, white, chromaAt, H2, law) {
|
|
2228
|
+
if (mode === "light") return void 0;
|
|
2229
|
+
if (law.enforceLc !== void 0) {
|
|
2230
|
+
const bar = law.enforceLc;
|
|
2231
|
+
const lcAt = white ? whiteTextLcAt : blackTextLcAt;
|
|
2232
|
+
return (L) => lcAt(L, chromaAt(L), H2) >= bar;
|
|
2233
|
+
}
|
|
2234
|
+
if (law.ratioFloor === void 0) return void 0;
|
|
2235
|
+
const floor = law.ratioFloor, poleY = white ? 1 : 0;
|
|
2236
|
+
return (L) => {
|
|
2237
|
+
const C2 = clampChromaToGamut(L, chromaAt(L), H2);
|
|
2238
|
+
return Math.min(legalRatio(L, C2, H2, poleY), contrastRatio(shippedY(L, C2, H2), poleY)) >= floor;
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2227
2241
|
function onFillIsWhiteDarkAt(L, C2, H2, enforce, ratioFloor) {
|
|
2228
2242
|
return onTextIsWhite(apcaYAt(L, C2, H2), L, C2, H2, enforce, ratioFloor);
|
|
2229
2243
|
}
|
|
@@ -2506,7 +2520,8 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2506
2520
|
const enforceLc = spec.ons.onFill.enforceLc;
|
|
2507
2521
|
const coLc = ctx.opts?.apcaClearance && enforceLc === void 0 ? ctx.opts.apcaClearanceLc ?? spec.ons.onFill.coEnforceLc : void 0;
|
|
2508
2522
|
const onFloor = enforceLc !== void 0 ? void 0 : spec.ons.onFill.ratioFloor;
|
|
2509
|
-
let cta,
|
|
2523
|
+
let cta, onFillIsWhite;
|
|
2524
|
+
let stateC, stateH;
|
|
2510
2525
|
if (mode === "light") {
|
|
2511
2526
|
onFillIsWhite = onFillIsWhiteLight(ctx, enforceLc !== void 0 ? false : onFillEnforce);
|
|
2512
2527
|
let light9L = enforceLc !== void 0 ? ctaLightLApca(ctx, onFillIsWhite, onFillEnforce, enforceLc) : ctaLightL(ctx, onFillIsWhite, onFillEnforce);
|
|
@@ -2538,21 +2553,14 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2538
2553
|
}
|
|
2539
2554
|
}
|
|
2540
2555
|
cta = emitRole(STAMP_FILL, light9L, ctaCFor(light9L) * ctaCMul, ctaH);
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
if (light9L !== ctx.scaleL) {
|
|
2548
|
-
cta.enforced = true;
|
|
2549
|
-
ctaHover.enforced = true;
|
|
2550
|
-
ctaPressed.enforced = true;
|
|
2551
|
-
}
|
|
2556
|
+
stateC = {
|
|
2557
|
+
hover: (L) => ctx.cAt("light", L, (hoverReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul,
|
|
2558
|
+
pressed: (L) => ctx.cAt("light", L, (pressedReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul
|
|
2559
|
+
};
|
|
2560
|
+
stateH = ctaH;
|
|
2561
|
+
if (light9L !== ctx.scaleL) cta.enforced = true;
|
|
2552
2562
|
if (repelled) {
|
|
2553
2563
|
cta.repelled = true;
|
|
2554
|
-
ctaHover.repelled = true;
|
|
2555
|
-
ctaPressed.repelled = true;
|
|
2556
2564
|
onFillIsWhite = onFillIsWhiteDarkAt(
|
|
2557
2565
|
cta.L,
|
|
2558
2566
|
cta.C,
|
|
@@ -2565,50 +2573,29 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2565
2573
|
const d = dctx;
|
|
2566
2574
|
const cta9L = ctx.opts?.darkCtaFlatApp !== void 0 ? flatDarkCtaL(d, (L) => ctx.cAt("dark", L, d.darkC9), ctx.darkCtaH, ctx.opts.darkCtaFlatApp) : d.dark9L;
|
|
2567
2575
|
const dCFor = (L) => ctx.cAt("dark", L, d.darkC9);
|
|
2568
|
-
|
|
2576
|
+
stateC = { hover: dCFor, pressed: dCFor };
|
|
2577
|
+
stateH = ctx.darkCtaH;
|
|
2569
2578
|
cta = emitRole(STAMP_FILL, cta9L, dCFor(cta9L), ctx.darkCtaH);
|
|
2570
|
-
{
|
|
2571
|
-
const [hL, pL] = dTrio(cta9L);
|
|
2572
|
-
ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
|
|
2573
|
-
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
|
|
2574
|
-
}
|
|
2575
2579
|
onFillIsWhite = onFillIsWhiteDarkAt(cta.L, cta.C, cta.H, enforceLc !== void 0 ? false : onFillEnforce);
|
|
2576
2580
|
const enforcedL = enforceLc !== void 0 ? ctaDarkEnforcedLApca(ctx, cta, onFillIsWhite, onFillEnforce, enforceLc) : ctaDarkEnforcedL(ctx, cta, onFillIsWhite, onFillEnforce);
|
|
2577
2581
|
if (enforcedL !== null) {
|
|
2578
2582
|
cta = emitRole(STAMP_FILL, enforcedL, dCFor(enforcedL), ctx.darkCtaH);
|
|
2579
|
-
const [hL, pL] = dTrio(enforcedL);
|
|
2580
|
-
ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
|
|
2581
|
-
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
|
|
2582
2583
|
cta.enforced = true;
|
|
2583
|
-
ctaHover.enforced = true;
|
|
2584
|
-
ctaPressed.enforced = true;
|
|
2585
2584
|
}
|
|
2586
2585
|
if (coLc !== void 0) {
|
|
2587
2586
|
const [capLoL, capHiL] = CTA_CLEARANCE_CAPS;
|
|
2588
2587
|
const clearedL = ctaDarkDualGateL(cta, ctx.darkCtaH, onFillIsWhite, onFillEnforce, coLc + APCA_ENFORCE_MARGIN_LC, capLoL, capHiL);
|
|
2589
2588
|
if (Math.abs(clearedL - cta.L) > 1e-9) {
|
|
2590
2589
|
cta = emitRole(STAMP_FILL, clearedL, dCFor(clearedL), ctx.darkCtaH);
|
|
2591
|
-
const [hL, pL] = dTrio(clearedL);
|
|
2592
|
-
ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
|
|
2593
|
-
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
|
|
2594
2590
|
cta.enforced = true;
|
|
2595
|
-
ctaHover.enforced = true;
|
|
2596
|
-
ctaPressed.enforced = true;
|
|
2597
2591
|
}
|
|
2598
2592
|
}
|
|
2599
2593
|
if (ctx.opts?.ctaSolve) {
|
|
2600
2594
|
const exitL = solveDarkCtaExit(cta, dCFor, ctx.darkCtaH, ctx.opts.ctaSolve.redDark, enforceLc, coLc);
|
|
2601
2595
|
if (exitL !== null) {
|
|
2602
2596
|
cta = emitRole(STAMP_FILL, exitL, dCFor(exitL), ctx.darkCtaH);
|
|
2603
|
-
const [hL, pL] = dTrio(exitL);
|
|
2604
|
-
ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
|
|
2605
|
-
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
|
|
2606
2597
|
cta.enforced = true;
|
|
2607
|
-
ctaHover.enforced = true;
|
|
2608
|
-
ctaPressed.enforced = true;
|
|
2609
2598
|
cta.repelled = true;
|
|
2610
|
-
ctaHover.repelled = true;
|
|
2611
|
-
ctaPressed.repelled = true;
|
|
2612
2599
|
onFillIsWhite = onFillIsWhiteDarkAt(
|
|
2613
2600
|
cta.L,
|
|
2614
2601
|
cta.C,
|
|
@@ -2624,6 +2611,19 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2624
2611
|
const other = legalRatio(cta.L, cta.C, cta.H, onFillIsWhite ? 0 : 1);
|
|
2625
2612
|
if (chosen < onFloor && other >= onFloor) onFillIsWhite = !onFillIsWhite;
|
|
2626
2613
|
}
|
|
2614
|
+
const keepsPole = stepKeepsPole(mode, onFillIsWhite, stateC.pressed, stateH, { ratioFloor: onFloor, enforceLc });
|
|
2615
|
+
const hL = stateFillL(cta.L, mode, 1, keepsPole);
|
|
2616
|
+
const pL = stateFillL(cta.L, mode, 2, keepsPole);
|
|
2617
|
+
const ctaHover = emitRole(STAMP_FILL_HOVER, hL, stateC.hover(hL), stateH);
|
|
2618
|
+
const ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, stateC.pressed(pL), stateH);
|
|
2619
|
+
if (cta.enforced) {
|
|
2620
|
+
ctaHover.enforced = true;
|
|
2621
|
+
ctaPressed.enforced = true;
|
|
2622
|
+
}
|
|
2623
|
+
if (cta.repelled) {
|
|
2624
|
+
ctaHover.repelled = true;
|
|
2625
|
+
ctaPressed.repelled = true;
|
|
2626
|
+
}
|
|
2627
2627
|
return { mode, seed, stops, roles: { cta, ctaHover, ctaPressed }, ons: { onFillIsWhite } };
|
|
2628
2628
|
}
|
|
2629
2629
|
|
|
@@ -2706,18 +2706,18 @@ function generateNeutralScale(brandH, level = "default", contrastProfile) {
|
|
|
2706
2706
|
if (popLift > 1e-6) {
|
|
2707
2707
|
scale.ctaDark = makeStop(9, scale.dark[3].L + popLift, scale.dark[3].C, scale.dark[3].H);
|
|
2708
2708
|
}
|
|
2709
|
-
const nState = (stop, mode, rest, k) => {
|
|
2710
|
-
const L = stateFillL(rest.L, mode, k);
|
|
2711
|
-
return makeStop(stop, L, curve(L, mode), rest.H);
|
|
2712
|
-
};
|
|
2713
|
-
scale.ctaHover = nState(10, "light", scale.cta, 1);
|
|
2714
|
-
scale.ctaPressed = nState(11, "light", scale.cta, 2);
|
|
2715
|
-
scale.ctaHoverDark = nState(10, "dark", scale.ctaDark, 1);
|
|
2716
|
-
scale.ctaPressedDark = nState(11, "dark", scale.ctaDark, 2);
|
|
2717
2709
|
const onEnforce = contrastProfile !== "apca";
|
|
2718
2710
|
const onFloor = 4.5;
|
|
2719
2711
|
scale.onFillTextIsWhite = onTextIsWhite(apcaY(scale.cta.r, scale.cta.g, scale.cta.b), scale.cta.L, scale.cta.C, scale.cta.H, onEnforce, onFloor);
|
|
2720
2712
|
scale.onFillTextIsWhiteDark = onTextIsWhite(apcaY(scale.ctaDark.r, scale.ctaDark.g, scale.ctaDark.b), scale.ctaDark.L, scale.ctaDark.C, scale.ctaDark.H, onEnforce, onFloor);
|
|
2713
|
+
const nState = (stop, mode, rest, k, white) => {
|
|
2714
|
+
const L = stateFillL(rest.L, mode, k, stepKeepsPole(mode, white, (l) => curve(l, mode), rest.H, { ratioFloor: onFloor }));
|
|
2715
|
+
return makeStop(stop, L, curve(L, mode), rest.H);
|
|
2716
|
+
};
|
|
2717
|
+
scale.ctaHover = nState(10, "light", scale.cta, 1, scale.onFillTextIsWhite);
|
|
2718
|
+
scale.ctaPressed = nState(11, "light", scale.cta, 2, scale.onFillTextIsWhite);
|
|
2719
|
+
scale.ctaHoverDark = nState(10, "dark", scale.ctaDark, 1, scale.onFillTextIsWhiteDark);
|
|
2720
|
+
scale.ctaPressedDark = nState(11, "dark", scale.ctaDark, 2, scale.onFillTextIsWhiteDark);
|
|
2721
2721
|
return scale;
|
|
2722
2722
|
}
|
|
2723
2723
|
function generateSubtleSecondary(hex, opts) {
|
|
@@ -2730,10 +2730,6 @@ function generateSubtleSecondary(hex, opts) {
|
|
|
2730
2730
|
});
|
|
2731
2731
|
const asCta = (stop, src) => makeStop(stop, src.L, src.C, src.H);
|
|
2732
2732
|
const mk = (stop, L, mode) => makeStop(stop, L, curve(L, mode), scale.brandH);
|
|
2733
|
-
const st = (stop, mode, rest, k) => {
|
|
2734
|
-
const L = stateFillL(rest.L, mode, k);
|
|
2735
|
-
return makeStop(stop, L, curve(L, mode), rest.H);
|
|
2736
|
-
};
|
|
2737
2733
|
if (opts?.ctaL) {
|
|
2738
2734
|
scale.cta = mk(9, opts.ctaL.light, "light");
|
|
2739
2735
|
scale.ctaDark = mk(9, opts.ctaL.dark, "dark");
|
|
@@ -2741,14 +2737,18 @@ function generateSubtleSecondary(hex, opts) {
|
|
|
2741
2737
|
scale.cta = asCta(9, scale.light[3]);
|
|
2742
2738
|
scale.ctaDark = asCta(9, scale.dark[3]);
|
|
2743
2739
|
}
|
|
2744
|
-
scale.ctaHover = st(10, "light", scale.cta, 1);
|
|
2745
|
-
scale.ctaPressed = st(11, "light", scale.cta, 2);
|
|
2746
|
-
scale.ctaHoverDark = st(10, "dark", scale.ctaDark, 1);
|
|
2747
|
-
scale.ctaPressedDark = st(11, "dark", scale.ctaDark, 2);
|
|
2748
2740
|
const onEnforce = opts?.contrastProfile !== "apca";
|
|
2749
2741
|
const onFloor = 4.5;
|
|
2750
2742
|
scale.onFillTextIsWhite = onTextIsWhite(apcaY(scale.cta.r, scale.cta.g, scale.cta.b), scale.cta.L, scale.cta.C, scale.cta.H, onEnforce, onFloor);
|
|
2751
2743
|
scale.onFillTextIsWhiteDark = onTextIsWhite(apcaY(scale.ctaDark.r, scale.ctaDark.g, scale.ctaDark.b), scale.ctaDark.L, scale.ctaDark.C, scale.ctaDark.H, onEnforce, onFloor);
|
|
2744
|
+
const st = (stop, mode, rest, k, white) => {
|
|
2745
|
+
const L = stateFillL(rest.L, mode, k, stepKeepsPole(mode, white, (l) => curve(l, mode), rest.H, { ratioFloor: onFloor }));
|
|
2746
|
+
return makeStop(stop, L, curve(L, mode), rest.H);
|
|
2747
|
+
};
|
|
2748
|
+
scale.ctaHover = st(10, "light", scale.cta, 1, scale.onFillTextIsWhite);
|
|
2749
|
+
scale.ctaPressed = st(11, "light", scale.cta, 2, scale.onFillTextIsWhite);
|
|
2750
|
+
scale.ctaHoverDark = st(10, "dark", scale.ctaDark, 1, scale.onFillTextIsWhiteDark);
|
|
2751
|
+
scale.ctaPressedDark = st(11, "dark", scale.ctaDark, 2, scale.onFillTextIsWhiteDark);
|
|
2752
2752
|
return scale;
|
|
2753
2753
|
}
|
|
2754
2754
|
|
|
@@ -3324,11 +3324,12 @@ function escapeCtaFamily(nScale, mode, contrastProfile) {
|
|
|
3324
3324
|
if (!strongText) throw new Error("escapeCtaFamily: the neutral scale has no ink-11 stop");
|
|
3325
3325
|
const mk = (stop, L) => makeStop(stop, L, strongText.C, strongText.H);
|
|
3326
3326
|
const cta = mk(9, strongText.L);
|
|
3327
|
-
const ctaHover = mk(10, stateFillL(strongText.L, mode, 1));
|
|
3328
|
-
const ctaPressed = mk(11, stateFillL(strongText.L, mode, 2));
|
|
3329
3327
|
const onEnforce = contrastProfile !== "apca";
|
|
3330
3328
|
const onFloor = contrastProfile === "apca" ? void 0 : 4.5;
|
|
3331
3329
|
const onFillIsWhite = onTextIsWhite(apcaY(cta.r, cta.g, cta.b), cta.L, cta.C, cta.H, onEnforce, onFloor);
|
|
3330
|
+
const keepsPole = stepKeepsPole(mode, onFillIsWhite, () => strongText.C, strongText.H, { ratioFloor: onFloor });
|
|
3331
|
+
const ctaHover = mk(10, stateFillL(strongText.L, mode, 1, keepsPole));
|
|
3332
|
+
const ctaPressed = mk(11, stateFillL(strongText.L, mode, 2, keepsPole));
|
|
3332
3333
|
return { cta, ctaHover, ctaPressed, onFillIsWhite };
|
|
3333
3334
|
}
|
|
3334
3335
|
var SECONDARY_DISTINCT_DELTA_E = 0.12;
|
package/dist-lib/index.mjs
CHANGED
|
@@ -21,13 +21,13 @@ function stateStepL(restL, _mode, k) {
|
|
|
21
21
|
}
|
|
22
22
|
var OVERRIDE_LIGHT_BELOW = ARCHETYPES.find((a) => a.name === "rich").min;
|
|
23
23
|
var OVERRIDE_DARK_ABOVE = ARCHETYPES.find((a) => a.name === "light").min;
|
|
24
|
-
function stateFillL(restL, mode, k) {
|
|
24
|
+
function stateFillL(restL, mode, k, keepsPole) {
|
|
25
25
|
const natural = mode === "dark" ? 1 : -1;
|
|
26
26
|
const overridden = mode === "light" ? restL < OVERRIDE_LIGHT_BELOW : restL > OVERRIDE_DARK_ABOVE;
|
|
27
27
|
const preferred = overridden ? -natural : natural;
|
|
28
28
|
const fits = (d) => {
|
|
29
29
|
const p = restL + d * stateStepL(restL, mode, 2);
|
|
30
|
-
return p >= STATE_L_MIN && p <= STATE_L_MAX;
|
|
30
|
+
return p >= STATE_L_MIN && p <= STATE_L_MAX && (keepsPole?.(p) ?? true);
|
|
31
31
|
};
|
|
32
32
|
const dir = fits(preferred) ? preferred : fits(-preferred) ? -preferred : preferred;
|
|
33
33
|
const L = restL + dir * stateStepL(restL, mode, k);
|
|
@@ -2131,6 +2131,20 @@ function ctaDarkDualGateL(base, darkH, onFillTextIsWhite, enforce, coLc, capLoL,
|
|
|
2131
2131
|
const lLc = findLForBlackTextLc(startL, C2, darkH, coLc + APCA_SOLVE_MARGIN_LC, capHiL);
|
|
2132
2132
|
return Math.max(l45, Math.min(lLc, capHiL));
|
|
2133
2133
|
}
|
|
2134
|
+
function stepKeepsPole(mode, white, chromaAt, H2, law) {
|
|
2135
|
+
if (mode === "light") return void 0;
|
|
2136
|
+
if (law.enforceLc !== void 0) {
|
|
2137
|
+
const bar = law.enforceLc;
|
|
2138
|
+
const lcAt = white ? whiteTextLcAt : blackTextLcAt;
|
|
2139
|
+
return (L) => lcAt(L, chromaAt(L), H2) >= bar;
|
|
2140
|
+
}
|
|
2141
|
+
if (law.ratioFloor === void 0) return void 0;
|
|
2142
|
+
const floor = law.ratioFloor, poleY = white ? 1 : 0;
|
|
2143
|
+
return (L) => {
|
|
2144
|
+
const C2 = clampChromaToGamut(L, chromaAt(L), H2);
|
|
2145
|
+
return Math.min(legalRatio(L, C2, H2, poleY), contrastRatio(shippedY(L, C2, H2), poleY)) >= floor;
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2134
2148
|
function onFillIsWhiteDarkAt(L, C2, H2, enforce, ratioFloor) {
|
|
2135
2149
|
return onTextIsWhite(apcaYAt(L, C2, H2), L, C2, H2, enforce, ratioFloor);
|
|
2136
2150
|
}
|
|
@@ -2413,7 +2427,8 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2413
2427
|
const enforceLc = spec.ons.onFill.enforceLc;
|
|
2414
2428
|
const coLc = ctx.opts?.apcaClearance && enforceLc === void 0 ? ctx.opts.apcaClearanceLc ?? spec.ons.onFill.coEnforceLc : void 0;
|
|
2415
2429
|
const onFloor = enforceLc !== void 0 ? void 0 : spec.ons.onFill.ratioFloor;
|
|
2416
|
-
let cta,
|
|
2430
|
+
let cta, onFillIsWhite;
|
|
2431
|
+
let stateC, stateH;
|
|
2417
2432
|
if (mode === "light") {
|
|
2418
2433
|
onFillIsWhite = onFillIsWhiteLight(ctx, enforceLc !== void 0 ? false : onFillEnforce);
|
|
2419
2434
|
let light9L = enforceLc !== void 0 ? ctaLightLApca(ctx, onFillIsWhite, onFillEnforce, enforceLc) : ctaLightL(ctx, onFillIsWhite, onFillEnforce);
|
|
@@ -2445,21 +2460,14 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2445
2460
|
}
|
|
2446
2461
|
}
|
|
2447
2462
|
cta = emitRole(STAMP_FILL, light9L, ctaCFor(light9L) * ctaCMul, ctaH);
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
if (light9L !== ctx.scaleL) {
|
|
2455
|
-
cta.enforced = true;
|
|
2456
|
-
ctaHover.enforced = true;
|
|
2457
|
-
ctaPressed.enforced = true;
|
|
2458
|
-
}
|
|
2463
|
+
stateC = {
|
|
2464
|
+
hover: (L) => ctx.cAt("light", L, (hoverReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul,
|
|
2465
|
+
pressed: (L) => ctx.cAt("light", L, (pressedReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul
|
|
2466
|
+
};
|
|
2467
|
+
stateH = ctaH;
|
|
2468
|
+
if (light9L !== ctx.scaleL) cta.enforced = true;
|
|
2459
2469
|
if (repelled) {
|
|
2460
2470
|
cta.repelled = true;
|
|
2461
|
-
ctaHover.repelled = true;
|
|
2462
|
-
ctaPressed.repelled = true;
|
|
2463
2471
|
onFillIsWhite = onFillIsWhiteDarkAt(
|
|
2464
2472
|
cta.L,
|
|
2465
2473
|
cta.C,
|
|
@@ -2472,50 +2480,29 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2472
2480
|
const d = dctx;
|
|
2473
2481
|
const cta9L = ctx.opts?.darkCtaFlatApp !== void 0 ? flatDarkCtaL(d, (L) => ctx.cAt("dark", L, d.darkC9), ctx.darkCtaH, ctx.opts.darkCtaFlatApp) : d.dark9L;
|
|
2474
2482
|
const dCFor = (L) => ctx.cAt("dark", L, d.darkC9);
|
|
2475
|
-
|
|
2483
|
+
stateC = { hover: dCFor, pressed: dCFor };
|
|
2484
|
+
stateH = ctx.darkCtaH;
|
|
2476
2485
|
cta = emitRole(STAMP_FILL, cta9L, dCFor(cta9L), ctx.darkCtaH);
|
|
2477
|
-
{
|
|
2478
|
-
const [hL, pL] = dTrio(cta9L);
|
|
2479
|
-
ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
|
|
2480
|
-
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
|
|
2481
|
-
}
|
|
2482
2486
|
onFillIsWhite = onFillIsWhiteDarkAt(cta.L, cta.C, cta.H, enforceLc !== void 0 ? false : onFillEnforce);
|
|
2483
2487
|
const enforcedL = enforceLc !== void 0 ? ctaDarkEnforcedLApca(ctx, cta, onFillIsWhite, onFillEnforce, enforceLc) : ctaDarkEnforcedL(ctx, cta, onFillIsWhite, onFillEnforce);
|
|
2484
2488
|
if (enforcedL !== null) {
|
|
2485
2489
|
cta = emitRole(STAMP_FILL, enforcedL, dCFor(enforcedL), ctx.darkCtaH);
|
|
2486
|
-
const [hL, pL] = dTrio(enforcedL);
|
|
2487
|
-
ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
|
|
2488
|
-
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
|
|
2489
2490
|
cta.enforced = true;
|
|
2490
|
-
ctaHover.enforced = true;
|
|
2491
|
-
ctaPressed.enforced = true;
|
|
2492
2491
|
}
|
|
2493
2492
|
if (coLc !== void 0) {
|
|
2494
2493
|
const [capLoL, capHiL] = CTA_CLEARANCE_CAPS;
|
|
2495
2494
|
const clearedL = ctaDarkDualGateL(cta, ctx.darkCtaH, onFillIsWhite, onFillEnforce, coLc + APCA_ENFORCE_MARGIN_LC, capLoL, capHiL);
|
|
2496
2495
|
if (Math.abs(clearedL - cta.L) > 1e-9) {
|
|
2497
2496
|
cta = emitRole(STAMP_FILL, clearedL, dCFor(clearedL), ctx.darkCtaH);
|
|
2498
|
-
const [hL, pL] = dTrio(clearedL);
|
|
2499
|
-
ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
|
|
2500
|
-
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
|
|
2501
2497
|
cta.enforced = true;
|
|
2502
|
-
ctaHover.enforced = true;
|
|
2503
|
-
ctaPressed.enforced = true;
|
|
2504
2498
|
}
|
|
2505
2499
|
}
|
|
2506
2500
|
if (ctx.opts?.ctaSolve) {
|
|
2507
2501
|
const exitL = solveDarkCtaExit(cta, dCFor, ctx.darkCtaH, ctx.opts.ctaSolve.redDark, enforceLc, coLc);
|
|
2508
2502
|
if (exitL !== null) {
|
|
2509
2503
|
cta = emitRole(STAMP_FILL, exitL, dCFor(exitL), ctx.darkCtaH);
|
|
2510
|
-
const [hL, pL] = dTrio(exitL);
|
|
2511
|
-
ctaHover = emitRole(STAMP_FILL_HOVER, hL, dCFor(hL), ctx.darkCtaH);
|
|
2512
|
-
ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, dCFor(pL), ctx.darkCtaH);
|
|
2513
2504
|
cta.enforced = true;
|
|
2514
|
-
ctaHover.enforced = true;
|
|
2515
|
-
ctaPressed.enforced = true;
|
|
2516
2505
|
cta.repelled = true;
|
|
2517
|
-
ctaHover.repelled = true;
|
|
2518
|
-
ctaPressed.repelled = true;
|
|
2519
2506
|
onFillIsWhite = onFillIsWhiteDarkAt(
|
|
2520
2507
|
cta.L,
|
|
2521
2508
|
cta.C,
|
|
@@ -2531,6 +2518,19 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2531
2518
|
const other = legalRatio(cta.L, cta.C, cta.H, onFillIsWhite ? 0 : 1);
|
|
2532
2519
|
if (chosen < onFloor && other >= onFloor) onFillIsWhite = !onFillIsWhite;
|
|
2533
2520
|
}
|
|
2521
|
+
const keepsPole = stepKeepsPole(mode, onFillIsWhite, stateC.pressed, stateH, { ratioFloor: onFloor, enforceLc });
|
|
2522
|
+
const hL = stateFillL(cta.L, mode, 1, keepsPole);
|
|
2523
|
+
const pL = stateFillL(cta.L, mode, 2, keepsPole);
|
|
2524
|
+
const ctaHover = emitRole(STAMP_FILL_HOVER, hL, stateC.hover(hL), stateH);
|
|
2525
|
+
const ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, stateC.pressed(pL), stateH);
|
|
2526
|
+
if (cta.enforced) {
|
|
2527
|
+
ctaHover.enforced = true;
|
|
2528
|
+
ctaPressed.enforced = true;
|
|
2529
|
+
}
|
|
2530
|
+
if (cta.repelled) {
|
|
2531
|
+
ctaHover.repelled = true;
|
|
2532
|
+
ctaPressed.repelled = true;
|
|
2533
|
+
}
|
|
2534
2534
|
return { mode, seed, stops, roles: { cta, ctaHover, ctaPressed }, ons: { onFillIsWhite } };
|
|
2535
2535
|
}
|
|
2536
2536
|
|
|
@@ -2613,18 +2613,18 @@ function generateNeutralScale(brandH, level = "default", contrastProfile) {
|
|
|
2613
2613
|
if (popLift > 1e-6) {
|
|
2614
2614
|
scale.ctaDark = makeStop(9, scale.dark[3].L + popLift, scale.dark[3].C, scale.dark[3].H);
|
|
2615
2615
|
}
|
|
2616
|
-
const nState = (stop, mode, rest, k) => {
|
|
2617
|
-
const L = stateFillL(rest.L, mode, k);
|
|
2618
|
-
return makeStop(stop, L, curve(L, mode), rest.H);
|
|
2619
|
-
};
|
|
2620
|
-
scale.ctaHover = nState(10, "light", scale.cta, 1);
|
|
2621
|
-
scale.ctaPressed = nState(11, "light", scale.cta, 2);
|
|
2622
|
-
scale.ctaHoverDark = nState(10, "dark", scale.ctaDark, 1);
|
|
2623
|
-
scale.ctaPressedDark = nState(11, "dark", scale.ctaDark, 2);
|
|
2624
2616
|
const onEnforce = contrastProfile !== "apca";
|
|
2625
2617
|
const onFloor = 4.5;
|
|
2626
2618
|
scale.onFillTextIsWhite = onTextIsWhite(apcaY(scale.cta.r, scale.cta.g, scale.cta.b), scale.cta.L, scale.cta.C, scale.cta.H, onEnforce, onFloor);
|
|
2627
2619
|
scale.onFillTextIsWhiteDark = onTextIsWhite(apcaY(scale.ctaDark.r, scale.ctaDark.g, scale.ctaDark.b), scale.ctaDark.L, scale.ctaDark.C, scale.ctaDark.H, onEnforce, onFloor);
|
|
2620
|
+
const nState = (stop, mode, rest, k, white) => {
|
|
2621
|
+
const L = stateFillL(rest.L, mode, k, stepKeepsPole(mode, white, (l) => curve(l, mode), rest.H, { ratioFloor: onFloor }));
|
|
2622
|
+
return makeStop(stop, L, curve(L, mode), rest.H);
|
|
2623
|
+
};
|
|
2624
|
+
scale.ctaHover = nState(10, "light", scale.cta, 1, scale.onFillTextIsWhite);
|
|
2625
|
+
scale.ctaPressed = nState(11, "light", scale.cta, 2, scale.onFillTextIsWhite);
|
|
2626
|
+
scale.ctaHoverDark = nState(10, "dark", scale.ctaDark, 1, scale.onFillTextIsWhiteDark);
|
|
2627
|
+
scale.ctaPressedDark = nState(11, "dark", scale.ctaDark, 2, scale.onFillTextIsWhiteDark);
|
|
2628
2628
|
return scale;
|
|
2629
2629
|
}
|
|
2630
2630
|
function generateSubtleSecondary(hex, opts) {
|
|
@@ -2637,10 +2637,6 @@ function generateSubtleSecondary(hex, opts) {
|
|
|
2637
2637
|
});
|
|
2638
2638
|
const asCta = (stop, src) => makeStop(stop, src.L, src.C, src.H);
|
|
2639
2639
|
const mk = (stop, L, mode) => makeStop(stop, L, curve(L, mode), scale.brandH);
|
|
2640
|
-
const st = (stop, mode, rest, k) => {
|
|
2641
|
-
const L = stateFillL(rest.L, mode, k);
|
|
2642
|
-
return makeStop(stop, L, curve(L, mode), rest.H);
|
|
2643
|
-
};
|
|
2644
2640
|
if (opts?.ctaL) {
|
|
2645
2641
|
scale.cta = mk(9, opts.ctaL.light, "light");
|
|
2646
2642
|
scale.ctaDark = mk(9, opts.ctaL.dark, "dark");
|
|
@@ -2648,14 +2644,18 @@ function generateSubtleSecondary(hex, opts) {
|
|
|
2648
2644
|
scale.cta = asCta(9, scale.light[3]);
|
|
2649
2645
|
scale.ctaDark = asCta(9, scale.dark[3]);
|
|
2650
2646
|
}
|
|
2651
|
-
scale.ctaHover = st(10, "light", scale.cta, 1);
|
|
2652
|
-
scale.ctaPressed = st(11, "light", scale.cta, 2);
|
|
2653
|
-
scale.ctaHoverDark = st(10, "dark", scale.ctaDark, 1);
|
|
2654
|
-
scale.ctaPressedDark = st(11, "dark", scale.ctaDark, 2);
|
|
2655
2647
|
const onEnforce = opts?.contrastProfile !== "apca";
|
|
2656
2648
|
const onFloor = 4.5;
|
|
2657
2649
|
scale.onFillTextIsWhite = onTextIsWhite(apcaY(scale.cta.r, scale.cta.g, scale.cta.b), scale.cta.L, scale.cta.C, scale.cta.H, onEnforce, onFloor);
|
|
2658
2650
|
scale.onFillTextIsWhiteDark = onTextIsWhite(apcaY(scale.ctaDark.r, scale.ctaDark.g, scale.ctaDark.b), scale.ctaDark.L, scale.ctaDark.C, scale.ctaDark.H, onEnforce, onFloor);
|
|
2651
|
+
const st = (stop, mode, rest, k, white) => {
|
|
2652
|
+
const L = stateFillL(rest.L, mode, k, stepKeepsPole(mode, white, (l) => curve(l, mode), rest.H, { ratioFloor: onFloor }));
|
|
2653
|
+
return makeStop(stop, L, curve(L, mode), rest.H);
|
|
2654
|
+
};
|
|
2655
|
+
scale.ctaHover = st(10, "light", scale.cta, 1, scale.onFillTextIsWhite);
|
|
2656
|
+
scale.ctaPressed = st(11, "light", scale.cta, 2, scale.onFillTextIsWhite);
|
|
2657
|
+
scale.ctaHoverDark = st(10, "dark", scale.ctaDark, 1, scale.onFillTextIsWhiteDark);
|
|
2658
|
+
scale.ctaPressedDark = st(11, "dark", scale.ctaDark, 2, scale.onFillTextIsWhiteDark);
|
|
2659
2659
|
return scale;
|
|
2660
2660
|
}
|
|
2661
2661
|
|
|
@@ -3231,11 +3231,12 @@ function escapeCtaFamily(nScale, mode, contrastProfile) {
|
|
|
3231
3231
|
if (!strongText) throw new Error("escapeCtaFamily: the neutral scale has no ink-11 stop");
|
|
3232
3232
|
const mk = (stop, L) => makeStop(stop, L, strongText.C, strongText.H);
|
|
3233
3233
|
const cta = mk(9, strongText.L);
|
|
3234
|
-
const ctaHover = mk(10, stateFillL(strongText.L, mode, 1));
|
|
3235
|
-
const ctaPressed = mk(11, stateFillL(strongText.L, mode, 2));
|
|
3236
3234
|
const onEnforce = contrastProfile !== "apca";
|
|
3237
3235
|
const onFloor = contrastProfile === "apca" ? void 0 : 4.5;
|
|
3238
3236
|
const onFillIsWhite = onTextIsWhite(apcaY(cta.r, cta.g, cta.b), cta.L, cta.C, cta.H, onEnforce, onFloor);
|
|
3237
|
+
const keepsPole = stepKeepsPole(mode, onFillIsWhite, () => strongText.C, strongText.H, { ratioFloor: onFloor });
|
|
3238
|
+
const ctaHover = mk(10, stateFillL(strongText.L, mode, 1, keepsPole));
|
|
3239
|
+
const ctaPressed = mk(11, stateFillL(strongText.L, mode, 2, keepsPole));
|
|
3239
3240
|
return { cta, ctaHover, ctaPressed, onFillIsWhite };
|
|
3240
3241
|
}
|
|
3241
3242
|
var SECONDARY_DISTINCT_DELTA_E = 0.12;
|
|
@@ -10,4 +10,4 @@ export declare function medianLForArchetype(archetype: Archetype): number;
|
|
|
10
10
|
export declare function hoverL(L: number): number;
|
|
11
11
|
export declare function pressedL(L: number): number;
|
|
12
12
|
export declare function stateStepL(restL: number, _mode: 'light' | 'dark', k: 1 | 2): number;
|
|
13
|
-
export declare function stateFillL(restL: number, mode: 'light' | 'dark', k: 1 | 2): number;
|
|
13
|
+
export declare function stateFillL(restL: number, mode: 'light' | 'dark', k: 1 | 2, keepsPole?: (L: number) => boolean): number;
|
|
@@ -133,6 +133,10 @@ export declare function ctaDarkDualGateL(base: {
|
|
|
133
133
|
L: number;
|
|
134
134
|
C: number;
|
|
135
135
|
}, darkH: number, onFillTextIsWhite: boolean, enforce: boolean, coLc: number, capLoL: number, capHiL: number): number;
|
|
136
|
+
export declare function stepKeepsPole(mode: 'light' | 'dark', white: boolean, chromaAt: (L: number) => number, H: number, law: {
|
|
137
|
+
ratioFloor?: number;
|
|
138
|
+
enforceLc?: number;
|
|
139
|
+
}): ((L: number) => boolean) | undefined;
|
|
136
140
|
export declare function onFillIsWhiteDarkAt(L: number, C: number, H: number, enforce: boolean, ratioFloor?: number): boolean;
|
|
137
141
|
export interface BrandExitLanding {
|
|
138
142
|
L: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "okchroma",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Color-system engine: one brand hex in, a light and dark set of color primitives out, with the contrast requirements solved during generation; emitted as CSS custom properties, DTCG documents, and Figma variables",
|
|
6
6
|
"repository": {
|