okchroma 0.9.0 → 0.9.2
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 +110 -114
- package/dist-lib/index.mjs +110 -114
- package/dist-lib/types/engine/archetypes.d.ts +1 -1
- package/dist-lib/types/engine/colorMath.d.ts +5 -5
- package/dist-lib/types/engine/p2.d.ts +0 -1
- package/dist-lib/types/engine/requirements/producers.d.ts +4 -0
- package/dist-lib/types/engine/resolve.d.ts +1 -1
- 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);
|
|
@@ -597,14 +597,11 @@ var RED_SOLVE = {
|
|
|
597
597
|
brickCoolGold: -3,
|
|
598
598
|
brickDesat: 0.85,
|
|
599
599
|
brickExtraDeep: 0.02,
|
|
600
|
-
// red's
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
redHuesWarmBrand: [28, 24, 33.3, 38],
|
|
606
|
-
redHuesMagentaBrand: [33.3, 38, 28],
|
|
607
|
-
redHueMagentaDh: -15
|
|
600
|
+
// the red complement's hue sets (resolve.ts redComplementVariant): the canonical hue and
|
|
601
|
+
// the two ends of the error band the field ships, read through this engine (the cool end
|
|
602
|
+
// near Primer's danger, the gold side near USWDS's error). The set furthest from the brand
|
|
603
|
+
// stamp by the side-by-side measure ships.
|
|
604
|
+
setHues: { true: 33.3, cool: 24, "gold-side": 38 }
|
|
608
605
|
};
|
|
609
606
|
function redSolveDist(c, red) {
|
|
610
607
|
const dh = (c.H - red.H + 540) % 360 - 180;
|
|
@@ -2224,6 +2221,20 @@ function ctaDarkDualGateL(base, darkH, onFillTextIsWhite, enforce, coLc, capLoL,
|
|
|
2224
2221
|
const lLc = findLForBlackTextLc(startL, C2, darkH, coLc + APCA_SOLVE_MARGIN_LC, capHiL);
|
|
2225
2222
|
return Math.max(l45, Math.min(lLc, capHiL));
|
|
2226
2223
|
}
|
|
2224
|
+
function stepKeepsPole(mode, white, chromaAt, H2, law) {
|
|
2225
|
+
if (mode === "light") return void 0;
|
|
2226
|
+
if (law.enforceLc !== void 0) {
|
|
2227
|
+
const bar = law.enforceLc;
|
|
2228
|
+
const lcAt = white ? whiteTextLcAt : blackTextLcAt;
|
|
2229
|
+
return (L) => lcAt(L, chromaAt(L), H2) >= bar;
|
|
2230
|
+
}
|
|
2231
|
+
if (law.ratioFloor === void 0) return void 0;
|
|
2232
|
+
const floor = law.ratioFloor, poleY = white ? 1 : 0;
|
|
2233
|
+
return (L) => {
|
|
2234
|
+
const C2 = clampChromaToGamut(L, chromaAt(L), H2);
|
|
2235
|
+
return Math.min(legalRatio(L, C2, H2, poleY), contrastRatio(shippedY(L, C2, H2), poleY)) >= floor;
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2227
2238
|
function onFillIsWhiteDarkAt(L, C2, H2, enforce, ratioFloor) {
|
|
2228
2239
|
return onTextIsWhite(apcaYAt(L, C2, H2), L, C2, H2, enforce, ratioFloor);
|
|
2229
2240
|
}
|
|
@@ -2506,7 +2517,8 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2506
2517
|
const enforceLc = spec.ons.onFill.enforceLc;
|
|
2507
2518
|
const coLc = ctx.opts?.apcaClearance && enforceLc === void 0 ? ctx.opts.apcaClearanceLc ?? spec.ons.onFill.coEnforceLc : void 0;
|
|
2508
2519
|
const onFloor = enforceLc !== void 0 ? void 0 : spec.ons.onFill.ratioFloor;
|
|
2509
|
-
let cta,
|
|
2520
|
+
let cta, onFillIsWhite;
|
|
2521
|
+
let stateC, stateH;
|
|
2510
2522
|
if (mode === "light") {
|
|
2511
2523
|
onFillIsWhite = onFillIsWhiteLight(ctx, enforceLc !== void 0 ? false : onFillEnforce);
|
|
2512
2524
|
let light9L = enforceLc !== void 0 ? ctaLightLApca(ctx, onFillIsWhite, onFillEnforce, enforceLc) : ctaLightL(ctx, onFillIsWhite, onFillEnforce);
|
|
@@ -2538,21 +2550,14 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2538
2550
|
}
|
|
2539
2551
|
}
|
|
2540
2552
|
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
|
-
}
|
|
2553
|
+
stateC = {
|
|
2554
|
+
hover: (L) => ctx.cAt("light", L, (hoverReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul,
|
|
2555
|
+
pressed: (L) => ctx.cAt("light", L, (pressedReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul
|
|
2556
|
+
};
|
|
2557
|
+
stateH = ctaH;
|
|
2558
|
+
if (light9L !== ctx.scaleL) cta.enforced = true;
|
|
2552
2559
|
if (repelled) {
|
|
2553
2560
|
cta.repelled = true;
|
|
2554
|
-
ctaHover.repelled = true;
|
|
2555
|
-
ctaPressed.repelled = true;
|
|
2556
2561
|
onFillIsWhite = onFillIsWhiteDarkAt(
|
|
2557
2562
|
cta.L,
|
|
2558
2563
|
cta.C,
|
|
@@ -2565,50 +2570,29 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2565
2570
|
const d = dctx;
|
|
2566
2571
|
const cta9L = ctx.opts?.darkCtaFlatApp !== void 0 ? flatDarkCtaL(d, (L) => ctx.cAt("dark", L, d.darkC9), ctx.darkCtaH, ctx.opts.darkCtaFlatApp) : d.dark9L;
|
|
2567
2572
|
const dCFor = (L) => ctx.cAt("dark", L, d.darkC9);
|
|
2568
|
-
|
|
2573
|
+
stateC = { hover: dCFor, pressed: dCFor };
|
|
2574
|
+
stateH = ctx.darkCtaH;
|
|
2569
2575
|
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
2576
|
onFillIsWhite = onFillIsWhiteDarkAt(cta.L, cta.C, cta.H, enforceLc !== void 0 ? false : onFillEnforce);
|
|
2576
2577
|
const enforcedL = enforceLc !== void 0 ? ctaDarkEnforcedLApca(ctx, cta, onFillIsWhite, onFillEnforce, enforceLc) : ctaDarkEnforcedL(ctx, cta, onFillIsWhite, onFillEnforce);
|
|
2577
2578
|
if (enforcedL !== null) {
|
|
2578
2579
|
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
2580
|
cta.enforced = true;
|
|
2583
|
-
ctaHover.enforced = true;
|
|
2584
|
-
ctaPressed.enforced = true;
|
|
2585
2581
|
}
|
|
2586
2582
|
if (coLc !== void 0) {
|
|
2587
2583
|
const [capLoL, capHiL] = CTA_CLEARANCE_CAPS;
|
|
2588
2584
|
const clearedL = ctaDarkDualGateL(cta, ctx.darkCtaH, onFillIsWhite, onFillEnforce, coLc + APCA_ENFORCE_MARGIN_LC, capLoL, capHiL);
|
|
2589
2585
|
if (Math.abs(clearedL - cta.L) > 1e-9) {
|
|
2590
2586
|
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
2587
|
cta.enforced = true;
|
|
2595
|
-
ctaHover.enforced = true;
|
|
2596
|
-
ctaPressed.enforced = true;
|
|
2597
2588
|
}
|
|
2598
2589
|
}
|
|
2599
2590
|
if (ctx.opts?.ctaSolve) {
|
|
2600
2591
|
const exitL = solveDarkCtaExit(cta, dCFor, ctx.darkCtaH, ctx.opts.ctaSolve.redDark, enforceLc, coLc);
|
|
2601
2592
|
if (exitL !== null) {
|
|
2602
2593
|
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
2594
|
cta.enforced = true;
|
|
2607
|
-
ctaHover.enforced = true;
|
|
2608
|
-
ctaPressed.enforced = true;
|
|
2609
2595
|
cta.repelled = true;
|
|
2610
|
-
ctaHover.repelled = true;
|
|
2611
|
-
ctaPressed.repelled = true;
|
|
2612
2596
|
onFillIsWhite = onFillIsWhiteDarkAt(
|
|
2613
2597
|
cta.L,
|
|
2614
2598
|
cta.C,
|
|
@@ -2624,6 +2608,19 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2624
2608
|
const other = legalRatio(cta.L, cta.C, cta.H, onFillIsWhite ? 0 : 1);
|
|
2625
2609
|
if (chosen < onFloor && other >= onFloor) onFillIsWhite = !onFillIsWhite;
|
|
2626
2610
|
}
|
|
2611
|
+
const keepsPole = stepKeepsPole(mode, onFillIsWhite, stateC.pressed, stateH, { ratioFloor: onFloor, enforceLc });
|
|
2612
|
+
const hL = stateFillL(cta.L, mode, 1, keepsPole);
|
|
2613
|
+
const pL = stateFillL(cta.L, mode, 2, keepsPole);
|
|
2614
|
+
const ctaHover = emitRole(STAMP_FILL_HOVER, hL, stateC.hover(hL), stateH);
|
|
2615
|
+
const ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, stateC.pressed(pL), stateH);
|
|
2616
|
+
if (cta.enforced) {
|
|
2617
|
+
ctaHover.enforced = true;
|
|
2618
|
+
ctaPressed.enforced = true;
|
|
2619
|
+
}
|
|
2620
|
+
if (cta.repelled) {
|
|
2621
|
+
ctaHover.repelled = true;
|
|
2622
|
+
ctaPressed.repelled = true;
|
|
2623
|
+
}
|
|
2627
2624
|
return { mode, seed, stops, roles: { cta, ctaHover, ctaPressed }, ons: { onFillIsWhite } };
|
|
2628
2625
|
}
|
|
2629
2626
|
|
|
@@ -2706,18 +2703,18 @@ function generateNeutralScale(brandH, level = "default", contrastProfile) {
|
|
|
2706
2703
|
if (popLift > 1e-6) {
|
|
2707
2704
|
scale.ctaDark = makeStop(9, scale.dark[3].L + popLift, scale.dark[3].C, scale.dark[3].H);
|
|
2708
2705
|
}
|
|
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
2706
|
const onEnforce = contrastProfile !== "apca";
|
|
2718
2707
|
const onFloor = 4.5;
|
|
2719
2708
|
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
2709
|
scale.onFillTextIsWhiteDark = onTextIsWhite(apcaY(scale.ctaDark.r, scale.ctaDark.g, scale.ctaDark.b), scale.ctaDark.L, scale.ctaDark.C, scale.ctaDark.H, onEnforce, onFloor);
|
|
2710
|
+
const nState = (stop, mode, rest, k, white) => {
|
|
2711
|
+
const L = stateFillL(rest.L, mode, k, stepKeepsPole(mode, white, (l) => curve(l, mode), rest.H, { ratioFloor: onFloor }));
|
|
2712
|
+
return makeStop(stop, L, curve(L, mode), rest.H);
|
|
2713
|
+
};
|
|
2714
|
+
scale.ctaHover = nState(10, "light", scale.cta, 1, scale.onFillTextIsWhite);
|
|
2715
|
+
scale.ctaPressed = nState(11, "light", scale.cta, 2, scale.onFillTextIsWhite);
|
|
2716
|
+
scale.ctaHoverDark = nState(10, "dark", scale.ctaDark, 1, scale.onFillTextIsWhiteDark);
|
|
2717
|
+
scale.ctaPressedDark = nState(11, "dark", scale.ctaDark, 2, scale.onFillTextIsWhiteDark);
|
|
2721
2718
|
return scale;
|
|
2722
2719
|
}
|
|
2723
2720
|
function generateSubtleSecondary(hex, opts) {
|
|
@@ -2730,10 +2727,6 @@ function generateSubtleSecondary(hex, opts) {
|
|
|
2730
2727
|
});
|
|
2731
2728
|
const asCta = (stop, src) => makeStop(stop, src.L, src.C, src.H);
|
|
2732
2729
|
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
2730
|
if (opts?.ctaL) {
|
|
2738
2731
|
scale.cta = mk(9, opts.ctaL.light, "light");
|
|
2739
2732
|
scale.ctaDark = mk(9, opts.ctaL.dark, "dark");
|
|
@@ -2741,14 +2734,18 @@ function generateSubtleSecondary(hex, opts) {
|
|
|
2741
2734
|
scale.cta = asCta(9, scale.light[3]);
|
|
2742
2735
|
scale.ctaDark = asCta(9, scale.dark[3]);
|
|
2743
2736
|
}
|
|
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
2737
|
const onEnforce = opts?.contrastProfile !== "apca";
|
|
2749
2738
|
const onFloor = 4.5;
|
|
2750
2739
|
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
2740
|
scale.onFillTextIsWhiteDark = onTextIsWhite(apcaY(scale.ctaDark.r, scale.ctaDark.g, scale.ctaDark.b), scale.ctaDark.L, scale.ctaDark.C, scale.ctaDark.H, onEnforce, onFloor);
|
|
2741
|
+
const st = (stop, mode, rest, k, white) => {
|
|
2742
|
+
const L = stateFillL(rest.L, mode, k, stepKeepsPole(mode, white, (l) => curve(l, mode), rest.H, { ratioFloor: onFloor }));
|
|
2743
|
+
return makeStop(stop, L, curve(L, mode), rest.H);
|
|
2744
|
+
};
|
|
2745
|
+
scale.ctaHover = st(10, "light", scale.cta, 1, scale.onFillTextIsWhite);
|
|
2746
|
+
scale.ctaPressed = st(11, "light", scale.cta, 2, scale.onFillTextIsWhite);
|
|
2747
|
+
scale.ctaHoverDark = st(10, "dark", scale.ctaDark, 1, scale.onFillTextIsWhiteDark);
|
|
2748
|
+
scale.ctaPressedDark = st(11, "dark", scale.ctaDark, 2, scale.onFillTextIsWhiteDark);
|
|
2752
2749
|
return scale;
|
|
2753
2750
|
}
|
|
2754
2751
|
|
|
@@ -3112,63 +3109,61 @@ function hueCollisionPending(scale, sigScales) {
|
|
|
3112
3109
|
}
|
|
3113
3110
|
var blackLcAt = (L, C2, H2) => Math.abs(apcaLc(apcaYAt(0, 0, 0), apcaYAt(L, clampChromaToGamut(L, C2, H2), H2)));
|
|
3114
3111
|
var redCleanBeside = (brandCta, c) => p2Diff(brandCta, c) >= (c.L < brandCta.L ? P2_D : P2_D_UP) && redSolveDist(brandCta, c) >= RED_GATE.G + RED_SOLVE.ring;
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3112
|
+
var redSetsCache = /* @__PURE__ */ new Map();
|
|
3113
|
+
var redSetKey = (set, band) => `${set}:${band}`;
|
|
3114
|
+
var bandBelow = (band) => {
|
|
3115
|
+
const i = ARCHETYPES.findIndex((a) => a.name === band);
|
|
3116
|
+
return ARCHETYPES[Math.max(0, i - 1)].name;
|
|
3117
|
+
};
|
|
3118
|
+
function redSetsFor(red, contrastProfile) {
|
|
3119
|
+
const cacheKey = contrastProfile ?? "wcag";
|
|
3120
|
+
const hit = redSetsCache.get(cacheKey);
|
|
3121
|
+
if (hit) return hit;
|
|
3122
|
+
const own = classifyArchetype(red.scale.cta.L);
|
|
3123
|
+
const sets = /* @__PURE__ */ new Map();
|
|
3124
|
+
for (const [set, hue] of Object.entries(RED_SOLVE.setHues)) for (const band of [own, bandBelow(own)]) {
|
|
3125
|
+
sets.set(redSetKey(set, band), generateScale(red.def.hex, red.def.name, band === own ? void 0 : band, {
|
|
3126
|
+
hueShiftDeg: hue - red.def.H,
|
|
3127
|
+
darkChromaCurve,
|
|
3128
|
+
darkCtaC: "signal",
|
|
3129
|
+
darkFillMinL: red.def.darkFillMinL,
|
|
3130
|
+
enforceOnFillContrast: true,
|
|
3131
|
+
suppressRedCool: true,
|
|
3132
|
+
goldBoost: true,
|
|
3133
|
+
signalWarmDrift: true,
|
|
3134
|
+
contrastProfile,
|
|
3135
|
+
apcaClearance: true,
|
|
3136
|
+
apcaClearanceLc: CRITICAL_CLEARANCE_LC
|
|
3137
|
+
}));
|
|
3138
|
+
}
|
|
3139
|
+
redSetsCache.set(cacheKey, sets);
|
|
3140
|
+
return sets;
|
|
3141
|
+
}
|
|
3142
|
+
function redComplementVariant(brandCta, brandWentUp, red, contrastProfile) {
|
|
3126
3143
|
const redCta = red.scale.cta;
|
|
3127
|
-
const at = (L, H2) => ({ L, C: clampChromaToGamut(L, rctx.cAt("light", L, rctx.brandC), H2), H: H2 });
|
|
3128
3144
|
const fireLc = CRITICAL_CLEARANCE_LC + APCA_ENFORCE_MARGIN_LC;
|
|
3129
|
-
const shipsWhite = (c) => onFillIsWhiteDarkAt(c.L, c.C, c.H, true, contrastProfile === "apca" ? void 0 : 4.5);
|
|
3130
3145
|
const poleOk = (c) => {
|
|
3131
|
-
const white =
|
|
3146
|
+
const white = onFillIsWhiteDarkAt(c.L, c.C, c.H, true, contrastProfile === "apca" ? void 0 : 4.5);
|
|
3132
3147
|
const lc = white ? whiteTextLcAt(c.L, c.C, c.H) : blackLcAt(c.L, c.C, c.H);
|
|
3133
3148
|
const legal = contrastProfile === "apca" || legalRatio(c.L, c.C, c.H, white ? 1 : 0) >= 4.5;
|
|
3134
3149
|
return legal && lc >= fireLc;
|
|
3135
3150
|
};
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
return poleOk(c2) ? c2 : null;
|
|
3141
|
-
};
|
|
3142
|
-
const clean = (c) => redCleanBeside(brandCta, c) && poleOk(c);
|
|
3143
|
-
if (!brandWentUp && clean(redCta)) return null;
|
|
3144
|
-
const hues = hueDelta(seed.H, redCta.H) <= RED_SOLVE.redHueMagentaDh ? RED_SOLVE.redHuesMagentaBrand : RED_SOLVE.redHuesWarmBrand;
|
|
3145
|
-
const wantLighter = brandCta.L <= redCta.L;
|
|
3146
|
-
const tiers = wantLighter ? [[...RED_SOLVE.edgeL], RED_SOLVE.coreL.filter((l) => l > brandCta.L)] : [[...RED_SOLVE.coreL], [...RED_SOLVE.edgeL]];
|
|
3151
|
+
if (!brandWentUp && redCleanBeside(brandCta, redCta) && poleOk(redCta)) return null;
|
|
3152
|
+
const own = classifyArchetype(redCta.L);
|
|
3153
|
+
const band = classifyArchetype(brandCta.L) === own ? bandBelow(own) : own;
|
|
3154
|
+
const sets = redSetsFor(red, contrastProfile);
|
|
3147
3155
|
let pick = null;
|
|
3148
|
-
|
|
3149
|
-
const
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
if (!onSide || !clean(c)) continue;
|
|
3155
|
-
pick = c;
|
|
3156
|
-
break outer;
|
|
3157
|
-
}
|
|
3158
|
-
}
|
|
3159
|
-
if (!pick) return null;
|
|
3160
|
-
const cta = makeStop(redCta.stop, pick.L, rctx.cAt("light", pick.L, rctx.brandC), pick.H);
|
|
3161
|
-
const vCFor = (L) => rctx.cAt("light", L, rctx.brandC);
|
|
3162
|
-
const hL = stateFillL(pick.L, "light", 1);
|
|
3163
|
-
const ctaHover = makeStop(red.scale.ctaHover.stop, hL, vCFor(hL), pick.H);
|
|
3164
|
-
const pL = stateFillL(pick.L, "light", 2);
|
|
3165
|
-
const ctaPressed = makeStop(red.scale.ctaPressed.stop, pL, vCFor(pL), pick.H);
|
|
3166
|
-
const onFillTextIsWhite = onFillIsWhiteDarkAt(cta.L, cta.C, cta.H, true, contrastProfile === "apca" ? void 0 : 4.5);
|
|
3156
|
+
for (const set of Object.keys(RED_SOLVE.setHues)) {
|
|
3157
|
+
const scale = sets.get(redSetKey(set, band));
|
|
3158
|
+
const d = p2Diff(brandCta, scale.cta);
|
|
3159
|
+
if (!pick || d > pick.d) pick = { set, scale, d };
|
|
3160
|
+
}
|
|
3161
|
+
const { cta, ctaHover, ctaPressed, onFillTextIsWhite } = pick.scale;
|
|
3167
3162
|
return {
|
|
3168
3163
|
scale: { ...red.scale, cta, ctaHover, ctaPressed, onFillTextIsWhite },
|
|
3169
|
-
//
|
|
3170
|
-
// the
|
|
3171
|
-
note: `red \u2192 ${pick.
|
|
3164
|
+
// the community plugin shows the part after the arrow as a display label; the hex is
|
|
3165
|
+
// the variant's identity there, so the note carries no value
|
|
3166
|
+
note: `red \u2192 ${pick.set} at ${band}`
|
|
3172
3167
|
};
|
|
3173
3168
|
}
|
|
3174
3169
|
function resolveBrand(hex, name, opts) {
|
|
@@ -3214,7 +3209,7 @@ function resolveBrand(hex, name, opts) {
|
|
|
3214
3209
|
const signalOverrides = [];
|
|
3215
3210
|
if (solving || opts?.exact && signalMoves && !opts?.archetypeOverride) {
|
|
3216
3211
|
const brandWentUp = !!scale.ctaRepelled?.light && scale.cta.L > seedO.L + 1e-6;
|
|
3217
|
-
const v2 = redComplementVariant(scale.cta,
|
|
3212
|
+
const v2 = redComplementVariant(scale.cta, brandWentUp, sigScales.get("red"), opts?.contrastProfile);
|
|
3218
3213
|
if (v2) signalOverrides.push({ name: "red", scale: v2.scale, note: v2.note });
|
|
3219
3214
|
}
|
|
3220
3215
|
let warnVariant = null;
|
|
@@ -3324,11 +3319,12 @@ function escapeCtaFamily(nScale, mode, contrastProfile) {
|
|
|
3324
3319
|
if (!strongText) throw new Error("escapeCtaFamily: the neutral scale has no ink-11 stop");
|
|
3325
3320
|
const mk = (stop, L) => makeStop(stop, L, strongText.C, strongText.H);
|
|
3326
3321
|
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
3322
|
const onEnforce = contrastProfile !== "apca";
|
|
3330
3323
|
const onFloor = contrastProfile === "apca" ? void 0 : 4.5;
|
|
3331
3324
|
const onFillIsWhite = onTextIsWhite(apcaY(cta.r, cta.g, cta.b), cta.L, cta.C, cta.H, onEnforce, onFloor);
|
|
3325
|
+
const keepsPole = stepKeepsPole(mode, onFillIsWhite, () => strongText.C, strongText.H, { ratioFloor: onFloor });
|
|
3326
|
+
const ctaHover = mk(10, stateFillL(strongText.L, mode, 1, keepsPole));
|
|
3327
|
+
const ctaPressed = mk(11, stateFillL(strongText.L, mode, 2, keepsPole));
|
|
3332
3328
|
return { cta, ctaHover, ctaPressed, onFillIsWhite };
|
|
3333
3329
|
}
|
|
3334
3330
|
var SECONDARY_DISTINCT_DELTA_E = 0.12;
|
|
@@ -3375,7 +3371,7 @@ function resolveTheme(input) {
|
|
|
3375
3371
|
const red = sigScales.get("red");
|
|
3376
3372
|
const primaryRed = mergedOverrides.find((o) => o.name === "red");
|
|
3377
3373
|
if (!primaryRed) {
|
|
3378
|
-
const v2 = redComplementVariant(secScale.cta,
|
|
3374
|
+
const v2 = redComplementVariant(secScale.cta, false, red, cp2);
|
|
3379
3375
|
if (v2 && redCleanBeside(primary.scale.cta, v2.scale.cta)) adopt("red", v2.scale, `${v2.note} (for the secondary)`);
|
|
3380
3376
|
}
|
|
3381
3377
|
for (const sigName of ["yellow", "green", "blue"]) {
|
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);
|
|
@@ -504,14 +504,11 @@ var RED_SOLVE = {
|
|
|
504
504
|
brickCoolGold: -3,
|
|
505
505
|
brickDesat: 0.85,
|
|
506
506
|
brickExtraDeep: 0.02,
|
|
507
|
-
// red's
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
redHuesWarmBrand: [28, 24, 33.3, 38],
|
|
513
|
-
redHuesMagentaBrand: [33.3, 38, 28],
|
|
514
|
-
redHueMagentaDh: -15
|
|
507
|
+
// the red complement's hue sets (resolve.ts redComplementVariant): the canonical hue and
|
|
508
|
+
// the two ends of the error band the field ships, read through this engine (the cool end
|
|
509
|
+
// near Primer's danger, the gold side near USWDS's error). The set furthest from the brand
|
|
510
|
+
// stamp by the side-by-side measure ships.
|
|
511
|
+
setHues: { true: 33.3, cool: 24, "gold-side": 38 }
|
|
515
512
|
};
|
|
516
513
|
function redSolveDist(c, red) {
|
|
517
514
|
const dh = (c.H - red.H + 540) % 360 - 180;
|
|
@@ -2131,6 +2128,20 @@ function ctaDarkDualGateL(base, darkH, onFillTextIsWhite, enforce, coLc, capLoL,
|
|
|
2131
2128
|
const lLc = findLForBlackTextLc(startL, C2, darkH, coLc + APCA_SOLVE_MARGIN_LC, capHiL);
|
|
2132
2129
|
return Math.max(l45, Math.min(lLc, capHiL));
|
|
2133
2130
|
}
|
|
2131
|
+
function stepKeepsPole(mode, white, chromaAt, H2, law) {
|
|
2132
|
+
if (mode === "light") return void 0;
|
|
2133
|
+
if (law.enforceLc !== void 0) {
|
|
2134
|
+
const bar = law.enforceLc;
|
|
2135
|
+
const lcAt = white ? whiteTextLcAt : blackTextLcAt;
|
|
2136
|
+
return (L) => lcAt(L, chromaAt(L), H2) >= bar;
|
|
2137
|
+
}
|
|
2138
|
+
if (law.ratioFloor === void 0) return void 0;
|
|
2139
|
+
const floor = law.ratioFloor, poleY = white ? 1 : 0;
|
|
2140
|
+
return (L) => {
|
|
2141
|
+
const C2 = clampChromaToGamut(L, chromaAt(L), H2);
|
|
2142
|
+
return Math.min(legalRatio(L, C2, H2, poleY), contrastRatio(shippedY(L, C2, H2), poleY)) >= floor;
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2134
2145
|
function onFillIsWhiteDarkAt(L, C2, H2, enforce, ratioFloor) {
|
|
2135
2146
|
return onTextIsWhite(apcaYAt(L, C2, H2), L, C2, H2, enforce, ratioFloor);
|
|
2136
2147
|
}
|
|
@@ -2413,7 +2424,8 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2413
2424
|
const enforceLc = spec.ons.onFill.enforceLc;
|
|
2414
2425
|
const coLc = ctx.opts?.apcaClearance && enforceLc === void 0 ? ctx.opts.apcaClearanceLc ?? spec.ons.onFill.coEnforceLc : void 0;
|
|
2415
2426
|
const onFloor = enforceLc !== void 0 ? void 0 : spec.ons.onFill.ratioFloor;
|
|
2416
|
-
let cta,
|
|
2427
|
+
let cta, onFillIsWhite;
|
|
2428
|
+
let stateC, stateH;
|
|
2417
2429
|
if (mode === "light") {
|
|
2418
2430
|
onFillIsWhite = onFillIsWhiteLight(ctx, enforceLc !== void 0 ? false : onFillEnforce);
|
|
2419
2431
|
let light9L = enforceLc !== void 0 ? ctaLightLApca(ctx, onFillIsWhite, onFillEnforce, enforceLc) : ctaLightL(ctx, onFillIsWhite, onFillEnforce);
|
|
@@ -2445,21 +2457,14 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2445
2457
|
}
|
|
2446
2458
|
}
|
|
2447
2459
|
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
|
-
}
|
|
2460
|
+
stateC = {
|
|
2461
|
+
hover: (L) => ctx.cAt("light", L, (hoverReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul,
|
|
2462
|
+
pressed: (L) => ctx.cAt("light", L, (pressedReq?.chromaMult ?? 1) * ctx.brandC) * ctaCMul
|
|
2463
|
+
};
|
|
2464
|
+
stateH = ctaH;
|
|
2465
|
+
if (light9L !== ctx.scaleL) cta.enforced = true;
|
|
2459
2466
|
if (repelled) {
|
|
2460
2467
|
cta.repelled = true;
|
|
2461
|
-
ctaHover.repelled = true;
|
|
2462
|
-
ctaPressed.repelled = true;
|
|
2463
2468
|
onFillIsWhite = onFillIsWhiteDarkAt(
|
|
2464
2469
|
cta.L,
|
|
2465
2470
|
cta.C,
|
|
@@ -2472,50 +2477,29 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2472
2477
|
const d = dctx;
|
|
2473
2478
|
const cta9L = ctx.opts?.darkCtaFlatApp !== void 0 ? flatDarkCtaL(d, (L) => ctx.cAt("dark", L, d.darkC9), ctx.darkCtaH, ctx.opts.darkCtaFlatApp) : d.dark9L;
|
|
2474
2479
|
const dCFor = (L) => ctx.cAt("dark", L, d.darkC9);
|
|
2475
|
-
|
|
2480
|
+
stateC = { hover: dCFor, pressed: dCFor };
|
|
2481
|
+
stateH = ctx.darkCtaH;
|
|
2476
2482
|
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
2483
|
onFillIsWhite = onFillIsWhiteDarkAt(cta.L, cta.C, cta.H, enforceLc !== void 0 ? false : onFillEnforce);
|
|
2483
2484
|
const enforcedL = enforceLc !== void 0 ? ctaDarkEnforcedLApca(ctx, cta, onFillIsWhite, onFillEnforce, enforceLc) : ctaDarkEnforcedL(ctx, cta, onFillIsWhite, onFillEnforce);
|
|
2484
2485
|
if (enforcedL !== null) {
|
|
2485
2486
|
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
2487
|
cta.enforced = true;
|
|
2490
|
-
ctaHover.enforced = true;
|
|
2491
|
-
ctaPressed.enforced = true;
|
|
2492
2488
|
}
|
|
2493
2489
|
if (coLc !== void 0) {
|
|
2494
2490
|
const [capLoL, capHiL] = CTA_CLEARANCE_CAPS;
|
|
2495
2491
|
const clearedL = ctaDarkDualGateL(cta, ctx.darkCtaH, onFillIsWhite, onFillEnforce, coLc + APCA_ENFORCE_MARGIN_LC, capLoL, capHiL);
|
|
2496
2492
|
if (Math.abs(clearedL - cta.L) > 1e-9) {
|
|
2497
2493
|
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
2494
|
cta.enforced = true;
|
|
2502
|
-
ctaHover.enforced = true;
|
|
2503
|
-
ctaPressed.enforced = true;
|
|
2504
2495
|
}
|
|
2505
2496
|
}
|
|
2506
2497
|
if (ctx.opts?.ctaSolve) {
|
|
2507
2498
|
const exitL = solveDarkCtaExit(cta, dCFor, ctx.darkCtaH, ctx.opts.ctaSolve.redDark, enforceLc, coLc);
|
|
2508
2499
|
if (exitL !== null) {
|
|
2509
2500
|
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
2501
|
cta.enforced = true;
|
|
2514
|
-
ctaHover.enforced = true;
|
|
2515
|
-
ctaPressed.enforced = true;
|
|
2516
2502
|
cta.repelled = true;
|
|
2517
|
-
ctaHover.repelled = true;
|
|
2518
|
-
ctaPressed.repelled = true;
|
|
2519
2503
|
onFillIsWhite = onFillIsWhiteDarkAt(
|
|
2520
2504
|
cta.L,
|
|
2521
2505
|
cta.C,
|
|
@@ -2531,6 +2515,19 @@ function resolveRamp(hex, mode, spec, opts) {
|
|
|
2531
2515
|
const other = legalRatio(cta.L, cta.C, cta.H, onFillIsWhite ? 0 : 1);
|
|
2532
2516
|
if (chosen < onFloor && other >= onFloor) onFillIsWhite = !onFillIsWhite;
|
|
2533
2517
|
}
|
|
2518
|
+
const keepsPole = stepKeepsPole(mode, onFillIsWhite, stateC.pressed, stateH, { ratioFloor: onFloor, enforceLc });
|
|
2519
|
+
const hL = stateFillL(cta.L, mode, 1, keepsPole);
|
|
2520
|
+
const pL = stateFillL(cta.L, mode, 2, keepsPole);
|
|
2521
|
+
const ctaHover = emitRole(STAMP_FILL_HOVER, hL, stateC.hover(hL), stateH);
|
|
2522
|
+
const ctaPressed = emitRole(STAMP_FILL_PRESSED, pL, stateC.pressed(pL), stateH);
|
|
2523
|
+
if (cta.enforced) {
|
|
2524
|
+
ctaHover.enforced = true;
|
|
2525
|
+
ctaPressed.enforced = true;
|
|
2526
|
+
}
|
|
2527
|
+
if (cta.repelled) {
|
|
2528
|
+
ctaHover.repelled = true;
|
|
2529
|
+
ctaPressed.repelled = true;
|
|
2530
|
+
}
|
|
2534
2531
|
return { mode, seed, stops, roles: { cta, ctaHover, ctaPressed }, ons: { onFillIsWhite } };
|
|
2535
2532
|
}
|
|
2536
2533
|
|
|
@@ -2613,18 +2610,18 @@ function generateNeutralScale(brandH, level = "default", contrastProfile) {
|
|
|
2613
2610
|
if (popLift > 1e-6) {
|
|
2614
2611
|
scale.ctaDark = makeStop(9, scale.dark[3].L + popLift, scale.dark[3].C, scale.dark[3].H);
|
|
2615
2612
|
}
|
|
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
2613
|
const onEnforce = contrastProfile !== "apca";
|
|
2625
2614
|
const onFloor = 4.5;
|
|
2626
2615
|
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
2616
|
scale.onFillTextIsWhiteDark = onTextIsWhite(apcaY(scale.ctaDark.r, scale.ctaDark.g, scale.ctaDark.b), scale.ctaDark.L, scale.ctaDark.C, scale.ctaDark.H, onEnforce, onFloor);
|
|
2617
|
+
const nState = (stop, mode, rest, k, white) => {
|
|
2618
|
+
const L = stateFillL(rest.L, mode, k, stepKeepsPole(mode, white, (l) => curve(l, mode), rest.H, { ratioFloor: onFloor }));
|
|
2619
|
+
return makeStop(stop, L, curve(L, mode), rest.H);
|
|
2620
|
+
};
|
|
2621
|
+
scale.ctaHover = nState(10, "light", scale.cta, 1, scale.onFillTextIsWhite);
|
|
2622
|
+
scale.ctaPressed = nState(11, "light", scale.cta, 2, scale.onFillTextIsWhite);
|
|
2623
|
+
scale.ctaHoverDark = nState(10, "dark", scale.ctaDark, 1, scale.onFillTextIsWhiteDark);
|
|
2624
|
+
scale.ctaPressedDark = nState(11, "dark", scale.ctaDark, 2, scale.onFillTextIsWhiteDark);
|
|
2628
2625
|
return scale;
|
|
2629
2626
|
}
|
|
2630
2627
|
function generateSubtleSecondary(hex, opts) {
|
|
@@ -2637,10 +2634,6 @@ function generateSubtleSecondary(hex, opts) {
|
|
|
2637
2634
|
});
|
|
2638
2635
|
const asCta = (stop, src) => makeStop(stop, src.L, src.C, src.H);
|
|
2639
2636
|
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
2637
|
if (opts?.ctaL) {
|
|
2645
2638
|
scale.cta = mk(9, opts.ctaL.light, "light");
|
|
2646
2639
|
scale.ctaDark = mk(9, opts.ctaL.dark, "dark");
|
|
@@ -2648,14 +2641,18 @@ function generateSubtleSecondary(hex, opts) {
|
|
|
2648
2641
|
scale.cta = asCta(9, scale.light[3]);
|
|
2649
2642
|
scale.ctaDark = asCta(9, scale.dark[3]);
|
|
2650
2643
|
}
|
|
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
2644
|
const onEnforce = opts?.contrastProfile !== "apca";
|
|
2656
2645
|
const onFloor = 4.5;
|
|
2657
2646
|
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
2647
|
scale.onFillTextIsWhiteDark = onTextIsWhite(apcaY(scale.ctaDark.r, scale.ctaDark.g, scale.ctaDark.b), scale.ctaDark.L, scale.ctaDark.C, scale.ctaDark.H, onEnforce, onFloor);
|
|
2648
|
+
const st = (stop, mode, rest, k, white) => {
|
|
2649
|
+
const L = stateFillL(rest.L, mode, k, stepKeepsPole(mode, white, (l) => curve(l, mode), rest.H, { ratioFloor: onFloor }));
|
|
2650
|
+
return makeStop(stop, L, curve(L, mode), rest.H);
|
|
2651
|
+
};
|
|
2652
|
+
scale.ctaHover = st(10, "light", scale.cta, 1, scale.onFillTextIsWhite);
|
|
2653
|
+
scale.ctaPressed = st(11, "light", scale.cta, 2, scale.onFillTextIsWhite);
|
|
2654
|
+
scale.ctaHoverDark = st(10, "dark", scale.ctaDark, 1, scale.onFillTextIsWhiteDark);
|
|
2655
|
+
scale.ctaPressedDark = st(11, "dark", scale.ctaDark, 2, scale.onFillTextIsWhiteDark);
|
|
2659
2656
|
return scale;
|
|
2660
2657
|
}
|
|
2661
2658
|
|
|
@@ -3019,63 +3016,61 @@ function hueCollisionPending(scale, sigScales) {
|
|
|
3019
3016
|
}
|
|
3020
3017
|
var blackLcAt = (L, C2, H2) => Math.abs(apcaLc(apcaYAt(0, 0, 0), apcaYAt(L, clampChromaToGamut(L, C2, H2), H2)));
|
|
3021
3018
|
var redCleanBeside = (brandCta, c) => p2Diff(brandCta, c) >= (c.L < brandCta.L ? P2_D : P2_D_UP) && redSolveDist(brandCta, c) >= RED_GATE.G + RED_SOLVE.ring;
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3019
|
+
var redSetsCache = /* @__PURE__ */ new Map();
|
|
3020
|
+
var redSetKey = (set, band) => `${set}:${band}`;
|
|
3021
|
+
var bandBelow = (band) => {
|
|
3022
|
+
const i = ARCHETYPES.findIndex((a) => a.name === band);
|
|
3023
|
+
return ARCHETYPES[Math.max(0, i - 1)].name;
|
|
3024
|
+
};
|
|
3025
|
+
function redSetsFor(red, contrastProfile) {
|
|
3026
|
+
const cacheKey = contrastProfile ?? "wcag";
|
|
3027
|
+
const hit = redSetsCache.get(cacheKey);
|
|
3028
|
+
if (hit) return hit;
|
|
3029
|
+
const own = classifyArchetype(red.scale.cta.L);
|
|
3030
|
+
const sets = /* @__PURE__ */ new Map();
|
|
3031
|
+
for (const [set, hue] of Object.entries(RED_SOLVE.setHues)) for (const band of [own, bandBelow(own)]) {
|
|
3032
|
+
sets.set(redSetKey(set, band), generateScale(red.def.hex, red.def.name, band === own ? void 0 : band, {
|
|
3033
|
+
hueShiftDeg: hue - red.def.H,
|
|
3034
|
+
darkChromaCurve,
|
|
3035
|
+
darkCtaC: "signal",
|
|
3036
|
+
darkFillMinL: red.def.darkFillMinL,
|
|
3037
|
+
enforceOnFillContrast: true,
|
|
3038
|
+
suppressRedCool: true,
|
|
3039
|
+
goldBoost: true,
|
|
3040
|
+
signalWarmDrift: true,
|
|
3041
|
+
contrastProfile,
|
|
3042
|
+
apcaClearance: true,
|
|
3043
|
+
apcaClearanceLc: CRITICAL_CLEARANCE_LC
|
|
3044
|
+
}));
|
|
3045
|
+
}
|
|
3046
|
+
redSetsCache.set(cacheKey, sets);
|
|
3047
|
+
return sets;
|
|
3048
|
+
}
|
|
3049
|
+
function redComplementVariant(brandCta, brandWentUp, red, contrastProfile) {
|
|
3033
3050
|
const redCta = red.scale.cta;
|
|
3034
|
-
const at = (L, H2) => ({ L, C: clampChromaToGamut(L, rctx.cAt("light", L, rctx.brandC), H2), H: H2 });
|
|
3035
3051
|
const fireLc = CRITICAL_CLEARANCE_LC + APCA_ENFORCE_MARGIN_LC;
|
|
3036
|
-
const shipsWhite = (c) => onFillIsWhiteDarkAt(c.L, c.C, c.H, true, contrastProfile === "apca" ? void 0 : 4.5);
|
|
3037
3052
|
const poleOk = (c) => {
|
|
3038
|
-
const white =
|
|
3053
|
+
const white = onFillIsWhiteDarkAt(c.L, c.C, c.H, true, contrastProfile === "apca" ? void 0 : 4.5);
|
|
3039
3054
|
const lc = white ? whiteTextLcAt(c.L, c.C, c.H) : blackLcAt(c.L, c.C, c.H);
|
|
3040
3055
|
const legal = contrastProfile === "apca" || legalRatio(c.L, c.C, c.H, white ? 1 : 0) >= 4.5;
|
|
3041
3056
|
return legal && lc >= fireLc;
|
|
3042
3057
|
};
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
return poleOk(c2) ? c2 : null;
|
|
3048
|
-
};
|
|
3049
|
-
const clean = (c) => redCleanBeside(brandCta, c) && poleOk(c);
|
|
3050
|
-
if (!brandWentUp && clean(redCta)) return null;
|
|
3051
|
-
const hues = hueDelta(seed.H, redCta.H) <= RED_SOLVE.redHueMagentaDh ? RED_SOLVE.redHuesMagentaBrand : RED_SOLVE.redHuesWarmBrand;
|
|
3052
|
-
const wantLighter = brandCta.L <= redCta.L;
|
|
3053
|
-
const tiers = wantLighter ? [[...RED_SOLVE.edgeL], RED_SOLVE.coreL.filter((l) => l > brandCta.L)] : [[...RED_SOLVE.coreL], [...RED_SOLVE.edgeL]];
|
|
3058
|
+
if (!brandWentUp && redCleanBeside(brandCta, redCta) && poleOk(redCta)) return null;
|
|
3059
|
+
const own = classifyArchetype(redCta.L);
|
|
3060
|
+
const band = classifyArchetype(brandCta.L) === own ? bandBelow(own) : own;
|
|
3061
|
+
const sets = redSetsFor(red, contrastProfile);
|
|
3054
3062
|
let pick = null;
|
|
3055
|
-
|
|
3056
|
-
const
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
if (!onSide || !clean(c)) continue;
|
|
3062
|
-
pick = c;
|
|
3063
|
-
break outer;
|
|
3064
|
-
}
|
|
3065
|
-
}
|
|
3066
|
-
if (!pick) return null;
|
|
3067
|
-
const cta = makeStop(redCta.stop, pick.L, rctx.cAt("light", pick.L, rctx.brandC), pick.H);
|
|
3068
|
-
const vCFor = (L) => rctx.cAt("light", L, rctx.brandC);
|
|
3069
|
-
const hL = stateFillL(pick.L, "light", 1);
|
|
3070
|
-
const ctaHover = makeStop(red.scale.ctaHover.stop, hL, vCFor(hL), pick.H);
|
|
3071
|
-
const pL = stateFillL(pick.L, "light", 2);
|
|
3072
|
-
const ctaPressed = makeStop(red.scale.ctaPressed.stop, pL, vCFor(pL), pick.H);
|
|
3073
|
-
const onFillTextIsWhite = onFillIsWhiteDarkAt(cta.L, cta.C, cta.H, true, contrastProfile === "apca" ? void 0 : 4.5);
|
|
3063
|
+
for (const set of Object.keys(RED_SOLVE.setHues)) {
|
|
3064
|
+
const scale = sets.get(redSetKey(set, band));
|
|
3065
|
+
const d = p2Diff(brandCta, scale.cta);
|
|
3066
|
+
if (!pick || d > pick.d) pick = { set, scale, d };
|
|
3067
|
+
}
|
|
3068
|
+
const { cta, ctaHover, ctaPressed, onFillTextIsWhite } = pick.scale;
|
|
3074
3069
|
return {
|
|
3075
3070
|
scale: { ...red.scale, cta, ctaHover, ctaPressed, onFillTextIsWhite },
|
|
3076
|
-
//
|
|
3077
|
-
// the
|
|
3078
|
-
note: `red \u2192 ${pick.
|
|
3071
|
+
// the community plugin shows the part after the arrow as a display label; the hex is
|
|
3072
|
+
// the variant's identity there, so the note carries no value
|
|
3073
|
+
note: `red \u2192 ${pick.set} at ${band}`
|
|
3079
3074
|
};
|
|
3080
3075
|
}
|
|
3081
3076
|
function resolveBrand(hex, name, opts) {
|
|
@@ -3121,7 +3116,7 @@ function resolveBrand(hex, name, opts) {
|
|
|
3121
3116
|
const signalOverrides = [];
|
|
3122
3117
|
if (solving || opts?.exact && signalMoves && !opts?.archetypeOverride) {
|
|
3123
3118
|
const brandWentUp = !!scale.ctaRepelled?.light && scale.cta.L > seedO.L + 1e-6;
|
|
3124
|
-
const v2 = redComplementVariant(scale.cta,
|
|
3119
|
+
const v2 = redComplementVariant(scale.cta, brandWentUp, sigScales.get("red"), opts?.contrastProfile);
|
|
3125
3120
|
if (v2) signalOverrides.push({ name: "red", scale: v2.scale, note: v2.note });
|
|
3126
3121
|
}
|
|
3127
3122
|
let warnVariant = null;
|
|
@@ -3231,11 +3226,12 @@ function escapeCtaFamily(nScale, mode, contrastProfile) {
|
|
|
3231
3226
|
if (!strongText) throw new Error("escapeCtaFamily: the neutral scale has no ink-11 stop");
|
|
3232
3227
|
const mk = (stop, L) => makeStop(stop, L, strongText.C, strongText.H);
|
|
3233
3228
|
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
3229
|
const onEnforce = contrastProfile !== "apca";
|
|
3237
3230
|
const onFloor = contrastProfile === "apca" ? void 0 : 4.5;
|
|
3238
3231
|
const onFillIsWhite = onTextIsWhite(apcaY(cta.r, cta.g, cta.b), cta.L, cta.C, cta.H, onEnforce, onFloor);
|
|
3232
|
+
const keepsPole = stepKeepsPole(mode, onFillIsWhite, () => strongText.C, strongText.H, { ratioFloor: onFloor });
|
|
3233
|
+
const ctaHover = mk(10, stateFillL(strongText.L, mode, 1, keepsPole));
|
|
3234
|
+
const ctaPressed = mk(11, stateFillL(strongText.L, mode, 2, keepsPole));
|
|
3239
3235
|
return { cta, ctaHover, ctaPressed, onFillIsWhite };
|
|
3240
3236
|
}
|
|
3241
3237
|
var SECONDARY_DISTINCT_DELTA_E = 0.12;
|
|
@@ -3282,7 +3278,7 @@ function resolveTheme(input) {
|
|
|
3282
3278
|
const red = sigScales.get("red");
|
|
3283
3279
|
const primaryRed = mergedOverrides.find((o) => o.name === "red");
|
|
3284
3280
|
if (!primaryRed) {
|
|
3285
|
-
const v2 = redComplementVariant(secScale.cta,
|
|
3281
|
+
const v2 = redComplementVariant(secScale.cta, false, red, cp2);
|
|
3286
3282
|
if (v2 && redCleanBeside(primary.scale.cta, v2.scale.cta)) adopt("red", v2.scale, `${v2.note} (for the secondary)`);
|
|
3287
3283
|
}
|
|
3288
3284
|
for (const sigName of ["yellow", "green", "blue"]) {
|
|
@@ -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;
|
|
@@ -66,11 +66,11 @@ export declare const RED_SOLVE: {
|
|
|
66
66
|
readonly brickCoolGold: -3;
|
|
67
67
|
readonly brickDesat: 0.85;
|
|
68
68
|
readonly brickExtraDeep: 0.02;
|
|
69
|
-
readonly
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
readonly setHues: {
|
|
70
|
+
readonly true: 33.3;
|
|
71
|
+
readonly cool: 24;
|
|
72
|
+
readonly 'gold-side': 38;
|
|
73
|
+
};
|
|
74
74
|
};
|
|
75
75
|
export declare function redSolveDist(c: {
|
|
76
76
|
L: 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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type GeneratedScale, type ContrastProfile, type ColorStop } from './colorEngine';
|
|
2
|
-
import type
|
|
2
|
+
import { type Archetype } from './archetypes';
|
|
3
3
|
import { type SignalDef } from './signals';
|
|
4
4
|
type SignalScales = Map<SignalDef['name'], {
|
|
5
5
|
def: SignalDef;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "okchroma",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
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": {
|