@dryui/theme-wizard 3.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/dist/actions.d.ts +0 -4
- package/dist/actions.js +0 -9
- package/dist/components/AlphaSlider.svelte +0 -13
- package/dist/components/AlphaSlider.svelte.d.ts +0 -9
- package/dist/components/ContrastBadge.svelte +0 -22
- package/dist/components/ContrastBadge.svelte.d.ts +0 -8
- package/dist/components/HsbPicker.svelte +0 -304
- package/dist/components/HsbPicker.svelte.d.ts +0 -9
- package/dist/components/StepIndicator.svelte +0 -87
- package/dist/components/StepIndicator.svelte.d.ts +0 -7
- package/dist/components/TokenPreview.svelte +0 -55
- package/dist/components/TokenPreview.svelte.d.ts +0 -8
- package/dist/components/WizardShell.svelte +0 -140
- package/dist/components/WizardShell.svelte.d.ts +0 -15
- package/dist/engine/derivation.d.ts +0 -282
- package/dist/engine/derivation.js +0 -1445
- package/dist/engine/derivation.test.d.ts +0 -1
- package/dist/engine/derivation.test.js +0 -956
- package/dist/engine/export-css.d.ts +0 -32
- package/dist/engine/export-css.js +0 -90
- package/dist/engine/export-css.test.d.ts +0 -1
- package/dist/engine/export-css.test.js +0 -78
- package/dist/engine/index.d.ts +0 -10
- package/dist/engine/index.js +0 -6
- package/dist/engine/palette.d.ts +0 -16
- package/dist/engine/palette.js +0 -44
- package/dist/engine/presets.d.ts +0 -6
- package/dist/engine/presets.js +0 -34
- package/dist/engine/url-codec.d.ts +0 -53
- package/dist/engine/url-codec.js +0 -243
- package/dist/engine/url-codec.test.d.ts +0 -1
- package/dist/engine/url-codec.test.js +0 -137
- package/dist/index.d.ts +0 -14
- package/dist/index.js +0 -17
- package/dist/state.svelte.d.ts +0 -104
- package/dist/state.svelte.js +0 -574
- package/dist/steps/BrandColor.svelte +0 -218
- package/dist/steps/BrandColor.svelte.d.ts +0 -6
- package/dist/steps/Personality.svelte +0 -319
- package/dist/steps/Personality.svelte.d.ts +0 -3
- package/dist/steps/PreviewExport.svelte +0 -115
- package/dist/steps/PreviewExport.svelte.d.ts +0 -9
- package/dist/steps/Shape.svelte +0 -121
- package/dist/steps/Shape.svelte.d.ts +0 -18
- package/dist/steps/Typography.svelte +0 -115
- package/dist/steps/Typography.svelte.d.ts +0 -18
|
@@ -1,1445 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* derivation.ts — Color Derivation Engine
|
|
3
|
-
*
|
|
4
|
-
* Pure functions, zero UI dependencies.
|
|
5
|
-
* HSB-first approach: brand input is always HSB (Hue/Saturation/Brightness).
|
|
6
|
-
*/
|
|
7
|
-
// ─── Color Conversions ────────────────────────────────────────────────────────
|
|
8
|
-
/**
|
|
9
|
-
* Convert HSB (Hue/Saturation/Brightness) to HSL.
|
|
10
|
-
* h: 0–360, s: 0–1, b: 0–1
|
|
11
|
-
* Returns { h: 0–360, s: 0–1, l: 0–1 }
|
|
12
|
-
*/
|
|
13
|
-
export function hsbToHsl(h, s, b) {
|
|
14
|
-
// HSB → HSL conversion
|
|
15
|
-
// l = b * (1 - s/2)
|
|
16
|
-
// s_hsl = (b === l || l === 1) ? 0 : (b - l) / min(l, 1-l)
|
|
17
|
-
const l = b * (1 - s / 2);
|
|
18
|
-
let sHsl;
|
|
19
|
-
if (l === 0 || l === 1) {
|
|
20
|
-
sHsl = 0;
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
sHsl = (b - l) / Math.min(l, 1 - l);
|
|
24
|
-
}
|
|
25
|
-
return { h, s: sHsl, l };
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Convert HSL to HSB.
|
|
29
|
-
* h: 0–360, s: 0–1, l: 0–1
|
|
30
|
-
* Returns { h: 0–360, s: 0–1, b: 0–1 }
|
|
31
|
-
*/
|
|
32
|
-
export function hslToHsb(h, s, l) {
|
|
33
|
-
// HSL → HSB conversion
|
|
34
|
-
// b = l + s * min(l, 1-l)
|
|
35
|
-
// s_hsb = (b === 0) ? 0 : 2 * (1 - l/b)
|
|
36
|
-
const b = l + s * Math.min(l, 1 - l);
|
|
37
|
-
let sHsb;
|
|
38
|
-
if (b === 0) {
|
|
39
|
-
sHsb = 0;
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
sHsb = 2 * (1 - l / b);
|
|
43
|
-
}
|
|
44
|
-
return { h, s: sHsb, b };
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Convert HSL to RGB.
|
|
48
|
-
* h: 0–360, s: 0–1, l: 0–1
|
|
49
|
-
* Returns [r, g, b] each 0–255
|
|
50
|
-
*/
|
|
51
|
-
export function hslToRgb(h, s, l) {
|
|
52
|
-
if (s === 0) {
|
|
53
|
-
const v = Math.round(l * 255);
|
|
54
|
-
return [v, v, v];
|
|
55
|
-
}
|
|
56
|
-
const hueToRgb = (p, q, t) => {
|
|
57
|
-
if (t < 0)
|
|
58
|
-
t += 1;
|
|
59
|
-
if (t > 1)
|
|
60
|
-
t -= 1;
|
|
61
|
-
if (t < 1 / 6)
|
|
62
|
-
return p + (q - p) * 6 * t;
|
|
63
|
-
if (t < 1 / 2)
|
|
64
|
-
return q;
|
|
65
|
-
if (t < 2 / 3)
|
|
66
|
-
return p + (q - p) * (2 / 3 - t) * 6;
|
|
67
|
-
return p;
|
|
68
|
-
};
|
|
69
|
-
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
70
|
-
const p = 2 * l - q;
|
|
71
|
-
const hNorm = h / 360;
|
|
72
|
-
const r = Math.round(hueToRgb(p, q, hNorm + 1 / 3) * 255);
|
|
73
|
-
const g = Math.round(hueToRgb(p, q, hNorm) * 255);
|
|
74
|
-
const b = Math.round(hueToRgb(p, q, hNorm - 1 / 3) * 255);
|
|
75
|
-
return [r, g, b];
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Convert HSL to hex string (#rrggbb).
|
|
79
|
-
* h: 0–360, s: 0–1, l: 0–1
|
|
80
|
-
*/
|
|
81
|
-
export function hslToHex(h, s, l) {
|
|
82
|
-
const [r, g, b] = hslToRgb(h, s, l);
|
|
83
|
-
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Convert hex string (#rrggbb) to HSL.
|
|
87
|
-
*/
|
|
88
|
-
export function hexToHsl(hex) {
|
|
89
|
-
if (!/^#[0-9a-fA-F]{6}$/.test(hex)) {
|
|
90
|
-
throw new Error(`Invalid hex color: ${hex}`);
|
|
91
|
-
}
|
|
92
|
-
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
93
|
-
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
94
|
-
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
95
|
-
const max = Math.max(r, g, b);
|
|
96
|
-
const min = Math.min(r, g, b);
|
|
97
|
-
const l = (max + min) / 2;
|
|
98
|
-
if (max === min) {
|
|
99
|
-
return { h: 0, s: 0, l };
|
|
100
|
-
}
|
|
101
|
-
const d = max - min;
|
|
102
|
-
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
103
|
-
let hVal;
|
|
104
|
-
switch (max) {
|
|
105
|
-
case r:
|
|
106
|
-
hVal = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
107
|
-
break;
|
|
108
|
-
case g:
|
|
109
|
-
hVal = ((b - r) / d + 2) / 6;
|
|
110
|
-
break;
|
|
111
|
-
default:
|
|
112
|
-
hVal = ((r - g) / d + 4) / 6;
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
return { h: hVal * 360, s, l };
|
|
116
|
-
}
|
|
117
|
-
export function cssColorToRgb(color) {
|
|
118
|
-
const rgba = cssColorToRgba(color);
|
|
119
|
-
if (!rgba) {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
return [rgba.r, rgba.g, rgba.b];
|
|
123
|
-
}
|
|
124
|
-
function cssColorToRgba(color) {
|
|
125
|
-
const normalized = color.trim();
|
|
126
|
-
if (normalized.startsWith('#')) {
|
|
127
|
-
try {
|
|
128
|
-
const { h, s, l } = hexToHsl(normalized);
|
|
129
|
-
const [r, g, b] = hslToRgb(h, s, l);
|
|
130
|
-
return { r, g, b, a: 1 };
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const match = normalized.match(/hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%(?:\s*,\s*([\d.]+))?\s*\)/);
|
|
137
|
-
if (!match?.[1] || !match[2] || !match[3]) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
const [r, g, b] = hslToRgb(Number.parseFloat(match[1]), Number.parseFloat(match[2]) / 100, Number.parseFloat(match[3]) / 100);
|
|
141
|
-
return {
|
|
142
|
-
r,
|
|
143
|
-
g,
|
|
144
|
-
b,
|
|
145
|
-
a: clamp(match[4] ? Number.parseFloat(match[4]) : 1, 0, 1)
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
function compositeOver(foreground, background) {
|
|
149
|
-
const alpha = foreground.a + background.a * (1 - foreground.a);
|
|
150
|
-
if (alpha <= 0) {
|
|
151
|
-
return [0, 0, 0];
|
|
152
|
-
}
|
|
153
|
-
const compositeChannel = (fg, bg) => Math.round((fg * foreground.a + bg * background.a * (1 - foreground.a)) / alpha);
|
|
154
|
-
return [
|
|
155
|
-
compositeChannel(foreground.r, background.r),
|
|
156
|
-
compositeChannel(foreground.g, background.g),
|
|
157
|
-
compositeChannel(foreground.b, background.b)
|
|
158
|
-
];
|
|
159
|
-
}
|
|
160
|
-
function resolveOpaqueBackground(color) {
|
|
161
|
-
if (color.a >= 1) {
|
|
162
|
-
return [color.r, color.g, color.b];
|
|
163
|
-
}
|
|
164
|
-
return compositeOver(color, { r: 255, g: 255, b: 255, a: 1 });
|
|
165
|
-
}
|
|
166
|
-
function resolveCssPair(foreground, background) {
|
|
167
|
-
const foregroundRgba = cssColorToRgba(foreground);
|
|
168
|
-
const backgroundRgba = cssColorToRgba(background);
|
|
169
|
-
if (!foregroundRgba || !backgroundRgba) {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
const backgroundRgb = resolveOpaqueBackground(backgroundRgba);
|
|
173
|
-
const foregroundRgb = foregroundRgba.a >= 1
|
|
174
|
-
? [foregroundRgba.r, foregroundRgba.g, foregroundRgba.b]
|
|
175
|
-
: compositeOver(foregroundRgba, {
|
|
176
|
-
r: backgroundRgb[0],
|
|
177
|
-
g: backgroundRgb[1],
|
|
178
|
-
b: backgroundRgb[2],
|
|
179
|
-
a: 1
|
|
180
|
-
});
|
|
181
|
-
return [foregroundRgb, backgroundRgb];
|
|
182
|
-
}
|
|
183
|
-
// ─── WCAG Contrast ────────────────────────────────────────────────────────────
|
|
184
|
-
/**
|
|
185
|
-
* Compute relative luminance from linear RGB channels (each 0–255).
|
|
186
|
-
* Per WCAG 2.1.
|
|
187
|
-
*/
|
|
188
|
-
export function relativeLuminance(r, g, b) {
|
|
189
|
-
const linearize = (c) => {
|
|
190
|
-
const sRGB = c / 255;
|
|
191
|
-
return sRGB <= 0.04045 ? sRGB / 12.92 : Math.pow((sRGB + 0.055) / 1.055, 2.4);
|
|
192
|
-
};
|
|
193
|
-
return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b);
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Compute WCAG contrast ratio from two luminance values.
|
|
197
|
-
* Returns a value between 1 and 21.
|
|
198
|
-
*/
|
|
199
|
-
export function contrastRatio(lum1, lum2) {
|
|
200
|
-
const lighter = Math.max(lum1, lum2);
|
|
201
|
-
const darker = Math.min(lum1, lum2);
|
|
202
|
-
return (lighter + 0.05) / (darker + 0.05);
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Check if two luminance values meet a contrast threshold.
|
|
206
|
-
*/
|
|
207
|
-
export function meetsContrast(lum1, lum2, threshold) {
|
|
208
|
-
return contrastRatio(lum1, lum2) >= threshold;
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Compute relative luminance from HSL values.
|
|
212
|
-
* h: 0–360, s: 0–1, l: 0–1
|
|
213
|
-
*/
|
|
214
|
-
export function luminanceFromHsl(h, s, l) {
|
|
215
|
-
const [r, g, b] = hslToRgb(h, s, l);
|
|
216
|
-
return relativeLuminance(r, g, b);
|
|
217
|
-
}
|
|
218
|
-
export function contrastBetweenCssColors(first, second) {
|
|
219
|
-
const pair = resolveCssPair(first, second);
|
|
220
|
-
if (!pair) {
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
const [firstRgb, secondRgb] = pair;
|
|
224
|
-
return contrastRatio(relativeLuminance(firstRgb[0], firstRgb[1], firstRgb[2]), relativeLuminance(secondRgb[0], secondRgb[1], secondRgb[2]));
|
|
225
|
-
}
|
|
226
|
-
function spread(values) {
|
|
227
|
-
const numericValues = values.filter((value) => value != null);
|
|
228
|
-
if (numericValues.length < 2) {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
return Math.max(...numericValues) - Math.min(...numericValues);
|
|
232
|
-
}
|
|
233
|
-
export function measureForegroundOnSurface(foreground, surface, thresholds) {
|
|
234
|
-
const contrast = contrastBetweenCssColors(foreground, surface);
|
|
235
|
-
const apca = apcaContrastBetweenCssColors(foreground, surface);
|
|
236
|
-
const apcaMagnitude = apca == null ? null : Math.abs(apca);
|
|
237
|
-
const contrastThreshold = thresholds?.contrast ?? 4.5;
|
|
238
|
-
const apcaThreshold = thresholds?.apca ?? 60;
|
|
239
|
-
return {
|
|
240
|
-
foreground,
|
|
241
|
-
surface,
|
|
242
|
-
contrast,
|
|
243
|
-
apca,
|
|
244
|
-
apcaMagnitude,
|
|
245
|
-
passesContrast: contrast != null && contrast >= contrastThreshold,
|
|
246
|
-
passesApca: apcaMagnitude != null && apcaMagnitude >= apcaThreshold
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
export function compareForegroundAcrossSurfaces(foreground, surfaces, thresholds) {
|
|
250
|
-
const assessments = surfaces.map((surface) => measureForegroundOnSurface(foreground, surface, thresholds));
|
|
251
|
-
return {
|
|
252
|
-
foreground,
|
|
253
|
-
assessments,
|
|
254
|
-
contrastSpread: spread(assessments.map((assessment) => assessment.contrast)),
|
|
255
|
-
apcaMagnitudeSpread: spread(assessments.map((assessment) => assessment.apcaMagnitude))
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
// ─── APCA Contrast ───────────────────────────────────────────────────────────
|
|
259
|
-
const APCA_CONSTANTS = {
|
|
260
|
-
mainTRC: 2.4,
|
|
261
|
-
sRco: 0.2126729,
|
|
262
|
-
sGco: 0.7151522,
|
|
263
|
-
sBco: 0.072175,
|
|
264
|
-
normBG: 0.56,
|
|
265
|
-
normTXT: 0.57,
|
|
266
|
-
revTXT: 0.62,
|
|
267
|
-
revBG: 0.65,
|
|
268
|
-
blkThrs: 0.022,
|
|
269
|
-
blkClmp: 1.414,
|
|
270
|
-
scaleBoW: 1.14,
|
|
271
|
-
scaleWoB: 1.14,
|
|
272
|
-
loBoWoffset: 0.027,
|
|
273
|
-
loWoBoffset: 0.027,
|
|
274
|
-
deltaYmin: 0.0005,
|
|
275
|
-
loClip: 0.1
|
|
276
|
-
};
|
|
277
|
-
export function apcaSrgbToY(rgb) {
|
|
278
|
-
const channelToY = (channel) => Math.pow(clamp(channel, 0, 255) / 255, APCA_CONSTANTS.mainTRC);
|
|
279
|
-
return (APCA_CONSTANTS.sRco * channelToY(rgb[0]) +
|
|
280
|
-
APCA_CONSTANTS.sGco * channelToY(rgb[1]) +
|
|
281
|
-
APCA_CONSTANTS.sBco * channelToY(rgb[2]));
|
|
282
|
-
}
|
|
283
|
-
export function apcaContrast(textY, backgroundY) {
|
|
284
|
-
if (Number.isNaN(textY) ||
|
|
285
|
-
Number.isNaN(backgroundY) ||
|
|
286
|
-
Math.min(textY, backgroundY) < 0 ||
|
|
287
|
-
Math.max(textY, backgroundY) > 1.1) {
|
|
288
|
-
return 0;
|
|
289
|
-
}
|
|
290
|
-
const clampBlack = (value) => value > APCA_CONSTANTS.blkThrs
|
|
291
|
-
? value
|
|
292
|
-
: value + Math.pow(APCA_CONSTANTS.blkThrs - value, APCA_CONSTANTS.blkClmp);
|
|
293
|
-
const text = clampBlack(textY);
|
|
294
|
-
const background = clampBlack(backgroundY);
|
|
295
|
-
if (Math.abs(background - text) < APCA_CONSTANTS.deltaYmin) {
|
|
296
|
-
return 0;
|
|
297
|
-
}
|
|
298
|
-
if (background > text) {
|
|
299
|
-
const raw = (Math.pow(background, APCA_CONSTANTS.normBG) - Math.pow(text, APCA_CONSTANTS.normTXT)) *
|
|
300
|
-
APCA_CONSTANTS.scaleBoW;
|
|
301
|
-
if (raw < APCA_CONSTANTS.loClip) {
|
|
302
|
-
return 0;
|
|
303
|
-
}
|
|
304
|
-
return (raw - APCA_CONSTANTS.loBoWoffset) * 100;
|
|
305
|
-
}
|
|
306
|
-
const raw = (Math.pow(background, APCA_CONSTANTS.revBG) - Math.pow(text, APCA_CONSTANTS.revTXT)) *
|
|
307
|
-
APCA_CONSTANTS.scaleWoB;
|
|
308
|
-
if (raw > -APCA_CONSTANTS.loClip) {
|
|
309
|
-
return 0;
|
|
310
|
-
}
|
|
311
|
-
return (raw + APCA_CONSTANTS.loWoBoffset) * 100;
|
|
312
|
-
}
|
|
313
|
-
export function apcaContrastBetweenCssColors(text, background) {
|
|
314
|
-
const pair = resolveCssPair(text, background);
|
|
315
|
-
if (!pair) {
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
const [textRgb, backgroundRgb] = pair;
|
|
319
|
-
return apcaContrast(apcaSrgbToY(textRgb), apcaSrgbToY(backgroundRgb));
|
|
320
|
-
}
|
|
321
|
-
export function meetsApca(lc, threshold) {
|
|
322
|
-
return lc !== null && Math.abs(lc) >= threshold;
|
|
323
|
-
}
|
|
324
|
-
// ─── Internal Helpers ─────────────────────────────────────────────────────────
|
|
325
|
-
/** Clamp a value between min and max. */
|
|
326
|
-
function clamp(value, min, max) {
|
|
327
|
-
return Math.max(min, Math.min(max, value));
|
|
328
|
-
}
|
|
329
|
-
function requireLayerValue(values, name) {
|
|
330
|
-
const value = values[name];
|
|
331
|
-
if (!value) {
|
|
332
|
-
throw new Error(`Missing layer value ${name}`);
|
|
333
|
-
}
|
|
334
|
-
return value;
|
|
335
|
-
}
|
|
336
|
-
/** Format an HSL value as a CSS hsl() string. */
|
|
337
|
-
function hsl(h, s, l) {
|
|
338
|
-
return `hsl(${Math.round(h)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`;
|
|
339
|
-
}
|
|
340
|
-
/** Format as CSS hsla() string. */
|
|
341
|
-
function hsla(h, s, l, a) {
|
|
342
|
-
return `hsla(${Math.round(h)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%, ${a})`;
|
|
343
|
-
}
|
|
344
|
-
/**
|
|
345
|
-
* Iteratively adjust lightness until contrast >= threshold.
|
|
346
|
-
* direction: 'darken' or 'lighten'
|
|
347
|
-
* Returns the adjusted HSL object.
|
|
348
|
-
*/
|
|
349
|
-
function adjustForContrast(h, s, l, bgLuminance, threshold, direction, floorL, ceilL) {
|
|
350
|
-
const step = 0.05;
|
|
351
|
-
let currentL = l;
|
|
352
|
-
for (let i = 0; i < 40; i++) {
|
|
353
|
-
const lum = luminanceFromHsl(h, s, currentL);
|
|
354
|
-
if (meetsContrast(lum, bgLuminance, threshold)) {
|
|
355
|
-
return { h, s, l: currentL };
|
|
356
|
-
}
|
|
357
|
-
if (direction === 'darken') {
|
|
358
|
-
currentL = Math.max(floorL, currentL - step);
|
|
359
|
-
if (currentL <= floorL)
|
|
360
|
-
break;
|
|
361
|
-
}
|
|
362
|
-
else {
|
|
363
|
-
currentL = Math.min(ceilL, currentL + step);
|
|
364
|
-
if (currentL >= ceilL)
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
return { h, s, l: currentL };
|
|
369
|
-
}
|
|
370
|
-
function adjustCssColorForReadability(h, s, l, background, contrastThreshold, apcaThreshold, direction, floorL, ceilL) {
|
|
371
|
-
const step = 0.05;
|
|
372
|
-
let currentL = l;
|
|
373
|
-
for (let i = 0; i < 40; i++) {
|
|
374
|
-
const current = hsl(h, s, currentL);
|
|
375
|
-
const contrast = contrastBetweenCssColors(current, background);
|
|
376
|
-
const apca = apcaContrastBetweenCssColors(current, background);
|
|
377
|
-
const passesContrast = contrast != null && contrast >= contrastThreshold;
|
|
378
|
-
const passesApca = apca != null && Math.abs(apca) >= apcaThreshold;
|
|
379
|
-
if (passesContrast && passesApca) {
|
|
380
|
-
return { h, s, l: currentL };
|
|
381
|
-
}
|
|
382
|
-
if (direction === 'darken') {
|
|
383
|
-
currentL = Math.max(floorL, currentL - step);
|
|
384
|
-
if (currentL <= floorL)
|
|
385
|
-
break;
|
|
386
|
-
}
|
|
387
|
-
else {
|
|
388
|
-
currentL = Math.min(ceilL, currentL + step);
|
|
389
|
-
if (currentL >= ceilL)
|
|
390
|
-
break;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
return { h, s, l: currentL };
|
|
394
|
-
}
|
|
395
|
-
function passesCssReadability(foreground, background, contrastThreshold, apcaThreshold) {
|
|
396
|
-
const contrast = contrastBetweenCssColors(foreground, background);
|
|
397
|
-
const apca = apcaContrastBetweenCssColors(foreground, background);
|
|
398
|
-
return (contrast != null &&
|
|
399
|
-
contrast >= contrastThreshold &&
|
|
400
|
-
apca != null &&
|
|
401
|
-
Math.abs(apca) >= apcaThreshold);
|
|
402
|
-
}
|
|
403
|
-
function chooseAccessibleOnColor(fill) {
|
|
404
|
-
const fillCss = hsl(fill.h, fill.s, fill.l);
|
|
405
|
-
const darkTint = adjustCssColorForReadability(fill.h, clamp(fill.s * 0.3, 0, 1), 0.12, fillCss, 4.5, 60, 'darken', 0, 0.4);
|
|
406
|
-
const darkTintCss = hsl(darkTint.h, darkTint.s, darkTint.l);
|
|
407
|
-
const whiteContrast = contrastBetweenCssColors('#ffffff', fillCss) ?? 0;
|
|
408
|
-
const darkContrast = contrastBetweenCssColors(darkTintCss, fillCss) ?? 0;
|
|
409
|
-
const whiteApca = Math.abs(apcaContrastBetweenCssColors('#ffffff', fillCss) ?? 0);
|
|
410
|
-
const darkApca = Math.abs(apcaContrastBetweenCssColors(darkTintCss, fillCss) ?? 0);
|
|
411
|
-
const whitePasses = whiteContrast >= 4.5 && whiteApca >= 60;
|
|
412
|
-
const darkPasses = darkContrast >= 4.5 && darkApca >= 60;
|
|
413
|
-
if (whitePasses) {
|
|
414
|
-
return { color: '#ffffff', passes: true };
|
|
415
|
-
}
|
|
416
|
-
if (darkPasses) {
|
|
417
|
-
return { color: darkTintCss, passes: true };
|
|
418
|
-
}
|
|
419
|
-
const candidates = [
|
|
420
|
-
{ color: '#ffffff', contrast: whiteContrast, apca: whiteApca },
|
|
421
|
-
{ color: darkTintCss, contrast: darkContrast, apca: darkApca }
|
|
422
|
-
].sort((left, right) => right.contrast - left.contrast || right.apca - left.apca);
|
|
423
|
-
return { color: candidates[0]?.color ?? '#ffffff', passes: false };
|
|
424
|
-
}
|
|
425
|
-
function adjustFillForOnColor(fill, surface, surfaceContrastThreshold, surfaceApcaThreshold, direction, floorL, ceilL) {
|
|
426
|
-
let current = fill;
|
|
427
|
-
let choice = chooseAccessibleOnColor(current);
|
|
428
|
-
for (let i = 0; i < 40; i++) {
|
|
429
|
-
const fillCss = hsl(current.h, current.s, current.l);
|
|
430
|
-
const fillPasses = passesCssReadability(fillCss, surface, surfaceContrastThreshold, surfaceApcaThreshold);
|
|
431
|
-
if (fillPasses && choice.passes) {
|
|
432
|
-
return { fill: current, onColor: choice.color, onPasses: true };
|
|
433
|
-
}
|
|
434
|
-
const nextL = direction === 'darken'
|
|
435
|
-
? Math.max(floorL, current.l - 0.04)
|
|
436
|
-
: Math.min(ceilL, current.l + 0.04);
|
|
437
|
-
if (nextL === current.l) {
|
|
438
|
-
break;
|
|
439
|
-
}
|
|
440
|
-
current = { ...current, l: nextL };
|
|
441
|
-
choice = chooseAccessibleOnColor(current);
|
|
442
|
-
}
|
|
443
|
-
return { fill: current, onColor: choice.color, onPasses: choice.passes };
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* Pick white or a dark tint for on-color text, choosing whichever meets 4.5:1
|
|
447
|
-
* contrast against the given fill. Returns a CSS color string.
|
|
448
|
-
*/
|
|
449
|
-
function pickOnColor(fillH, fillS, fillL) {
|
|
450
|
-
return chooseAccessibleOnColor({ h: fillH, s: fillS, l: fillL }).color;
|
|
451
|
-
}
|
|
452
|
-
function deriveDarkModeAccent(h, s, b) {
|
|
453
|
-
const adjustedSaturation = clamp(s * 0.55, 0, 0.65);
|
|
454
|
-
const adjustedBrightness = clamp(Math.max(b + 0.18, 0.78), 0, 1);
|
|
455
|
-
return hsbToHsl(h, adjustedSaturation, adjustedBrightness);
|
|
456
|
-
}
|
|
457
|
-
function normalizeHue(hue) {
|
|
458
|
-
const wrapped = hue % 360;
|
|
459
|
-
return wrapped < 0 ? wrapped + 360 : wrapped;
|
|
460
|
-
}
|
|
461
|
-
function hueDistance(a, b) {
|
|
462
|
-
const diff = Math.abs(a - b);
|
|
463
|
-
return Math.min(diff, 360 - diff);
|
|
464
|
-
}
|
|
465
|
-
function lightenOrDarkenFillForContrast(h, s, l, backgroundLuminance, threshold, direction) {
|
|
466
|
-
return adjustForContrast(h, s, l, backgroundLuminance, threshold, direction, 0.08, 0.92);
|
|
467
|
-
}
|
|
468
|
-
function findStatusConflict(hue, statusHues) {
|
|
469
|
-
const entries = [
|
|
470
|
-
['error', statusHues.error ?? 0],
|
|
471
|
-
['warning', statusHues.warning ?? 40],
|
|
472
|
-
['success', statusHues.success ?? 145]
|
|
473
|
-
];
|
|
474
|
-
const closest = entries
|
|
475
|
-
.map(([tone, toneHue]) => ({ tone, toneHue, distance: hueDistance(hue, toneHue) }))
|
|
476
|
-
.sort((left, right) => left.distance - right.distance)[0];
|
|
477
|
-
if (!closest || closest.distance > 18) {
|
|
478
|
-
return null;
|
|
479
|
-
}
|
|
480
|
-
return closest.tone;
|
|
481
|
-
}
|
|
482
|
-
function resolveBrandHue(hue, statusHues) {
|
|
483
|
-
const conflict = findStatusConflict(hue, statusHues);
|
|
484
|
-
if (!conflict) {
|
|
485
|
-
return { hue: normalizeHue(hue), conflict: null, usesHueFallback: false };
|
|
486
|
-
}
|
|
487
|
-
const conflictHue = statusHues[conflict] ?? 0;
|
|
488
|
-
const direction = hue === conflictHue ? 1 : Math.sign(hue - conflictHue);
|
|
489
|
-
return {
|
|
490
|
-
hue: normalizeHue(conflictHue + direction * 24),
|
|
491
|
-
conflict,
|
|
492
|
-
usesHueFallback: true
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
function assessBrandCandidate(id, label, input, neutralMode, statusHues) {
|
|
496
|
-
const resolvedHue = resolveBrandHue(input.h, {
|
|
497
|
-
error: statusHues.error ?? 0,
|
|
498
|
-
warning: statusHues.warning ?? 40,
|
|
499
|
-
success: statusHues.success ?? 145
|
|
500
|
-
});
|
|
501
|
-
const resolvedInput = {
|
|
502
|
-
h: resolvedHue.hue,
|
|
503
|
-
s: input.s,
|
|
504
|
-
b: input.b
|
|
505
|
-
};
|
|
506
|
-
const lightBase = hsbToHsl(resolvedInput.h, resolvedInput.s / 100, resolvedInput.b / 100);
|
|
507
|
-
const lightFill = lightenOrDarkenFillForContrast(lightBase.h, lightBase.s, lightBase.l, 1, 3, 'darken');
|
|
508
|
-
const darkBase = deriveDarkModeAccent(resolvedInput.h, resolvedInput.s / 100, resolvedInput.b / 100);
|
|
509
|
-
const darkBackgroundLuminance = neutralMode === 'neutral'
|
|
510
|
-
? luminanceFromHsl(0, 0, 0.1)
|
|
511
|
-
: luminanceFromHsl(resolvedInput.h, 0.3, 0.1);
|
|
512
|
-
const darkFill = lightenOrDarkenFillForContrast(darkBase.h, darkBase.s, darkBase.l, darkBackgroundLuminance, 3, 'lighten');
|
|
513
|
-
const lightContrast = contrastRatio(luminanceFromHsl(lightFill.h, lightFill.s, lightFill.l), 1);
|
|
514
|
-
const darkContrast = contrastRatio(luminanceFromHsl(darkFill.h, darkFill.s, darkFill.l), darkBackgroundLuminance);
|
|
515
|
-
const minContrast = Math.min(lightContrast, darkContrast);
|
|
516
|
-
const score = minContrast - (resolvedHue.conflict ? 1.5 : 0);
|
|
517
|
-
return {
|
|
518
|
-
id,
|
|
519
|
-
label,
|
|
520
|
-
input,
|
|
521
|
-
resolvedInput,
|
|
522
|
-
usesHueFallback: resolvedHue.usesHueFallback,
|
|
523
|
-
lightFill: hsl(lightFill.h, lightFill.s, lightFill.l),
|
|
524
|
-
darkFill: hsl(darkFill.h, darkFill.s, darkFill.l),
|
|
525
|
-
lightContrast,
|
|
526
|
-
darkContrast,
|
|
527
|
-
minContrast,
|
|
528
|
-
statusConflict: resolvedHue.conflict,
|
|
529
|
-
score,
|
|
530
|
-
role: 'decorative'
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
function buildBrandPolicy(brand, options) {
|
|
534
|
-
const neutralMode = options?.neutralMode ?? 'monochromatic';
|
|
535
|
-
const statusHues = {
|
|
536
|
-
error: options?.statusHues?.error ?? 0,
|
|
537
|
-
warning: options?.statusHues?.warning ?? 40,
|
|
538
|
-
success: options?.statusHues?.success ?? 145,
|
|
539
|
-
info: options?.statusHues?.info ?? 210
|
|
540
|
-
};
|
|
541
|
-
const candidateInputs = [
|
|
542
|
-
{ id: 'primary', label: 'Primary', input: brand },
|
|
543
|
-
...(options?.brandCandidates ?? []).map((candidate, index) => ({
|
|
544
|
-
id: `accent-${index + 1}`,
|
|
545
|
-
label: `Accent ${index + 1}`,
|
|
546
|
-
input: candidate
|
|
547
|
-
}))
|
|
548
|
-
];
|
|
549
|
-
const assessments = candidateInputs.map((candidate) => assessBrandCandidate(candidate.id, candidate.label, candidate.input, neutralMode, statusHues));
|
|
550
|
-
const interactive = [...assessments].sort((left, right) => right.score - left.score)[0] ?? assessments[0];
|
|
551
|
-
const raw = assessments[0] ?? interactive;
|
|
552
|
-
if (!interactive || !raw) {
|
|
553
|
-
throw new Error('Brand policy requires at least one brand candidate');
|
|
554
|
-
}
|
|
555
|
-
return {
|
|
556
|
-
candidates: assessments.map((assessment) => ({
|
|
557
|
-
...assessment,
|
|
558
|
-
role: assessment.id === interactive.id ? 'interactive' : 'decorative'
|
|
559
|
-
})),
|
|
560
|
-
raw,
|
|
561
|
-
interactive: {
|
|
562
|
-
...interactive,
|
|
563
|
-
role: 'interactive'
|
|
564
|
-
},
|
|
565
|
-
multipleBrand: assessments.length > 1,
|
|
566
|
-
fallbackTriggered: interactive.id !== raw.id ||
|
|
567
|
-
raw.statusConflict != null ||
|
|
568
|
-
raw.minContrast < 3 ||
|
|
569
|
-
raw.usesHueFallback,
|
|
570
|
-
statusConflictResolved: raw.statusConflict != null && (interactive.id !== raw.id || interactive.usesHueFallback)
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
function buildLiteralNeutralSteps(hue, neutralMode, mode) {
|
|
574
|
-
if (mode === 'dark') {
|
|
575
|
-
return {
|
|
576
|
-
'1000': '#ffffff',
|
|
577
|
-
'700': hsla(0, 0, 1, 0.78),
|
|
578
|
-
'500': hsla(0, 0, 1, 0.6),
|
|
579
|
-
'100': hsla(0, 0, 1, 0.12),
|
|
580
|
-
'50': hsla(0, 0, 1, 0.06),
|
|
581
|
-
'25': hsla(0, 0, 1, 0.03)
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
const lightHue = neutralMode === 'neutral' ? 0 : hue;
|
|
585
|
-
const lightSaturation = neutralMode === 'neutral' ? 0 : 1;
|
|
586
|
-
return {
|
|
587
|
-
'1000': hsla(lightHue, lightSaturation, 0.15, 0.9),
|
|
588
|
-
'700': hsla(lightHue, lightSaturation, 0.2, 0.65),
|
|
589
|
-
'500': hsla(lightHue, lightSaturation, 0.2, 0.46),
|
|
590
|
-
'100': hsla(lightHue, lightSaturation, 0.2, 0.1),
|
|
591
|
-
'50': hsla(lightHue, lightSaturation, 0.2, 0.04),
|
|
592
|
-
'25': hsla(lightHue, lightSaturation, 0.2, 0.02)
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
function buildLiteralToneSteps(fill) {
|
|
596
|
-
return {
|
|
597
|
-
'1000': hsl(fill.h, fill.s, fill.l),
|
|
598
|
-
'800': hsla(fill.h, fill.s, fill.l, 0.8),
|
|
599
|
-
'200': hsla(fill.h, fill.s, fill.l, 0.2),
|
|
600
|
-
'50': hsla(fill.h, fill.s, fill.l, 0.05)
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
function buildLiteralTransparentPrimitiveLadders(brandPolicy, options) {
|
|
604
|
-
const neutralMode = options?.neutralMode ?? 'monochromatic';
|
|
605
|
-
const interactiveBrand = brandPolicy.interactive.resolvedInput;
|
|
606
|
-
const statusHues = {
|
|
607
|
-
error: options?.statusHues?.error ?? 0,
|
|
608
|
-
warning: options?.statusHues?.warning ?? 40,
|
|
609
|
-
success: options?.statusHues?.success ?? 145,
|
|
610
|
-
info: options?.statusHues?.info ?? 210
|
|
611
|
-
};
|
|
612
|
-
const brandLight = hsbToHsl(interactiveBrand.h, interactiveBrand.s / 100, interactiveBrand.b / 100);
|
|
613
|
-
const brandDark = deriveDarkModeAccent(interactiveBrand.h, interactiveBrand.s / 100, interactiveBrand.b / 100);
|
|
614
|
-
return {
|
|
615
|
-
neutral: {
|
|
616
|
-
light: buildLiteralNeutralSteps(interactiveBrand.h, neutralMode, 'light'),
|
|
617
|
-
dark: buildLiteralNeutralSteps(interactiveBrand.h, neutralMode, 'dark')
|
|
618
|
-
},
|
|
619
|
-
brand: {
|
|
620
|
-
light: buildLiteralToneSteps(brandLight),
|
|
621
|
-
dark: buildLiteralToneSteps(brandDark)
|
|
622
|
-
},
|
|
623
|
-
system: {
|
|
624
|
-
error: {
|
|
625
|
-
light: buildLiteralToneSteps(hsbToHsl(statusHues.error, 0.7, 0.5)),
|
|
626
|
-
dark: buildLiteralToneSteps(hsbToHsl(statusHues.error, 0.65, 0.55))
|
|
627
|
-
},
|
|
628
|
-
warning: {
|
|
629
|
-
light: buildLiteralToneSteps(hsbToHsl(statusHues.warning, 0.7, 0.5)),
|
|
630
|
-
dark: buildLiteralToneSteps(hsbToHsl(statusHues.warning, 0.65, 0.55))
|
|
631
|
-
},
|
|
632
|
-
success: {
|
|
633
|
-
light: buildLiteralToneSteps(hsbToHsl(statusHues.success, 0.7, 0.5)),
|
|
634
|
-
dark: buildLiteralToneSteps(hsbToHsl(statusHues.success, 0.65, 0.55))
|
|
635
|
-
},
|
|
636
|
-
info: {
|
|
637
|
-
light: buildLiteralToneSteps(hsbToHsl(statusHues.info, 0.7, 0.5)),
|
|
638
|
-
dark: buildLiteralToneSteps(hsbToHsl(statusHues.info, 0.65, 0.55))
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
function buildSolidPrimitiveLadders(hue, neutralMode) {
|
|
644
|
-
const lightHue = neutralMode === 'neutral' ? 0 : hue;
|
|
645
|
-
const lightSaturation = neutralMode === 'neutral' ? 0 : 0.02;
|
|
646
|
-
const lightSunken = hsbToHsl(lightHue, lightSaturation, 0.98);
|
|
647
|
-
const darkBaseCss = neutralMode === 'neutral' ? hsl(0, 0, 0.1) : hsl(hue, 0.3, 0.1);
|
|
648
|
-
const darkRaisedCss = neutralMode === 'neutral' ? hsl(0, 0, 0.15) : hsl(hue, 0.25, 0.15);
|
|
649
|
-
const darkOverlayCss = neutralMode === 'neutral' ? hsl(0, 0, 0.2) : hsl(hue, 0.2, 0.2);
|
|
650
|
-
const yellow = '#fec62e';
|
|
651
|
-
const lightSunkenCss = hsl(lightSunken.h, lightSunken.s, lightSunken.l);
|
|
652
|
-
return {
|
|
653
|
-
grey: {
|
|
654
|
-
light: {
|
|
655
|
-
steps: {
|
|
656
|
-
'50': lightSunkenCss,
|
|
657
|
-
'0': '#ffffff'
|
|
658
|
-
},
|
|
659
|
-
roles: {
|
|
660
|
-
sunken: lightSunkenCss,
|
|
661
|
-
base: '#ffffff'
|
|
662
|
-
}
|
|
663
|
-
},
|
|
664
|
-
dark: {
|
|
665
|
-
steps: {
|
|
666
|
-
'1000': '#000000',
|
|
667
|
-
'900': darkBaseCss,
|
|
668
|
-
'850': darkRaisedCss,
|
|
669
|
-
'800': darkOverlayCss
|
|
670
|
-
},
|
|
671
|
-
roles: {
|
|
672
|
-
sunken: '#000000',
|
|
673
|
-
base: darkBaseCss,
|
|
674
|
-
raised: darkRaisedCss,
|
|
675
|
-
overlay: darkOverlayCss
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
},
|
|
679
|
-
yellow: {
|
|
680
|
-
light: { '1000': yellow },
|
|
681
|
-
dark: { '1000': yellow }
|
|
682
|
-
}
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
function buildInteractionStateRecipes(tokens) {
|
|
686
|
-
const buildModeRecipe = (mode) => ({
|
|
687
|
-
neutral: {
|
|
688
|
-
baseFill: requireLayerValue(mode, '--dry-color-fill'),
|
|
689
|
-
hoverOverlay: requireLayerValue(mode, '--dry-color-fill-hover'),
|
|
690
|
-
activeOverlay: requireLayerValue(mode, '--dry-color-fill-active'),
|
|
691
|
-
focusRing: requireLayerValue(mode, '--dry-color-focus-ring'),
|
|
692
|
-
label: requireLayerValue(mode, '--dry-color-text-strong'),
|
|
693
|
-
stroke: requireLayerValue(mode, '--dry-color-stroke-strong'),
|
|
694
|
-
disabledFill: requireLayerValue(mode, '--dry-color-fill'),
|
|
695
|
-
disabledLabel: requireLayerValue(mode, '--dry-color-text-weak'),
|
|
696
|
-
disabledStroke: requireLayerValue(mode, '--dry-color-stroke-weak')
|
|
697
|
-
},
|
|
698
|
-
brand: {
|
|
699
|
-
baseFill: requireLayerValue(mode, '--dry-color-fill-brand'),
|
|
700
|
-
hoverOverlay: requireLayerValue(mode, '--dry-color-fill-hover'),
|
|
701
|
-
activeOverlay: requireLayerValue(mode, '--dry-color-fill-active'),
|
|
702
|
-
focusRing: requireLayerValue(mode, '--dry-color-focus-ring'),
|
|
703
|
-
label: requireLayerValue(mode, '--dry-color-on-brand'),
|
|
704
|
-
stroke: requireLayerValue(mode, '--dry-color-stroke-brand'),
|
|
705
|
-
disabledFill: requireLayerValue(mode, '--dry-color-fill-brand-weak'),
|
|
706
|
-
disabledLabel: requireLayerValue(mode, '--dry-color-text-weak'),
|
|
707
|
-
disabledStroke: requireLayerValue(mode, '--dry-color-stroke-weak')
|
|
708
|
-
},
|
|
709
|
-
system: Object.fromEntries(['error', 'warning', 'success', 'info'].map((tone) => [
|
|
710
|
-
tone,
|
|
711
|
-
{
|
|
712
|
-
baseFill: requireLayerValue(mode, `--dry-color-fill-${tone}`),
|
|
713
|
-
hoverOverlay: requireLayerValue(mode, '--dry-color-fill-hover'),
|
|
714
|
-
activeOverlay: requireLayerValue(mode, '--dry-color-fill-active'),
|
|
715
|
-
focusRing: requireLayerValue(mode, '--dry-color-focus-ring'),
|
|
716
|
-
label: requireLayerValue(mode, `--dry-color-on-${tone}`),
|
|
717
|
-
stroke: requireLayerValue(mode, `--dry-color-stroke-${tone}`),
|
|
718
|
-
disabledFill: requireLayerValue(mode, `--dry-color-fill-${tone}-weak`),
|
|
719
|
-
disabledLabel: requireLayerValue(mode, '--dry-color-text-weak'),
|
|
720
|
-
disabledStroke: requireLayerValue(mode, '--dry-color-stroke-weak')
|
|
721
|
-
}
|
|
722
|
-
]))
|
|
723
|
-
});
|
|
724
|
-
return {
|
|
725
|
-
neutral: {
|
|
726
|
-
light: buildModeRecipe(tokens.light).neutral,
|
|
727
|
-
dark: buildModeRecipe(tokens.dark).neutral
|
|
728
|
-
},
|
|
729
|
-
brand: {
|
|
730
|
-
light: buildModeRecipe(tokens.light).brand,
|
|
731
|
-
dark: buildModeRecipe(tokens.dark).brand
|
|
732
|
-
},
|
|
733
|
-
system: {
|
|
734
|
-
error: {
|
|
735
|
-
light: buildModeRecipe(tokens.light).system.error,
|
|
736
|
-
dark: buildModeRecipe(tokens.dark).system.error
|
|
737
|
-
},
|
|
738
|
-
warning: {
|
|
739
|
-
light: buildModeRecipe(tokens.light).system.warning,
|
|
740
|
-
dark: buildModeRecipe(tokens.dark).system.warning
|
|
741
|
-
},
|
|
742
|
-
success: {
|
|
743
|
-
light: buildModeRecipe(tokens.light).system.success,
|
|
744
|
-
dark: buildModeRecipe(tokens.dark).system.success
|
|
745
|
-
},
|
|
746
|
-
info: {
|
|
747
|
-
light: buildModeRecipe(tokens.light).system.info,
|
|
748
|
-
dark: buildModeRecipe(tokens.dark).system.info
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
function createAuditCheck(id, label, kind, foreground, background, contrastThreshold, apcaThreshold) {
|
|
754
|
-
const assessment = measureForegroundOnSurface(foreground, background, {
|
|
755
|
-
contrast: contrastThreshold,
|
|
756
|
-
apca: apcaThreshold
|
|
757
|
-
});
|
|
758
|
-
return {
|
|
759
|
-
...assessment,
|
|
760
|
-
id,
|
|
761
|
-
label,
|
|
762
|
-
kind,
|
|
763
|
-
contrastThreshold,
|
|
764
|
-
apcaThreshold,
|
|
765
|
-
passes: assessment.passesContrast && assessment.passesApca
|
|
766
|
-
};
|
|
767
|
-
}
|
|
768
|
-
function buildThemeAudit(tokens) {
|
|
769
|
-
const lightBase = requireLayerValue(tokens.light, '--dry-color-bg-base');
|
|
770
|
-
const darkBase = requireLayerValue(tokens.dark, '--dry-color-bg-base');
|
|
771
|
-
const darkRaised = requireLayerValue(tokens.dark, '--dry-color-bg-raised');
|
|
772
|
-
const darkOverlay = requireLayerValue(tokens.dark, '--dry-color-bg-overlay');
|
|
773
|
-
const checks = [
|
|
774
|
-
createAuditCheck('light-text-strong', 'Text strong on light base', 'text', requireLayerValue(tokens.light, '--dry-color-text-strong'), lightBase, 4.5, 60),
|
|
775
|
-
createAuditCheck('light-text-weak', 'Text weak on light base', 'text', requireLayerValue(tokens.light, '--dry-color-text-weak'), lightBase, 4.5, 60),
|
|
776
|
-
createAuditCheck('light-stroke-strong', 'Stroke strong on light base', 'stroke', requireLayerValue(tokens.light, '--dry-color-stroke-strong'), lightBase, 3, 45),
|
|
777
|
-
createAuditCheck('dark-text-strong-base', 'Text strong on dark base', 'text', requireLayerValue(tokens.dark, '--dry-color-text-strong'), darkBase, 4.5, 60),
|
|
778
|
-
createAuditCheck('dark-text-strong-raised', 'Text strong on dark raised', 'text', requireLayerValue(tokens.dark, '--dry-color-text-strong'), darkRaised, 4.5, 60),
|
|
779
|
-
createAuditCheck('dark-text-strong-overlay', 'Text strong on dark overlay', 'text', requireLayerValue(tokens.dark, '--dry-color-text-strong'), darkOverlay, 4.5, 60),
|
|
780
|
-
createAuditCheck('dark-text-weak-base', 'Text weak on dark base', 'text', requireLayerValue(tokens.dark, '--dry-color-text-weak'), darkBase, 4.5, 60),
|
|
781
|
-
createAuditCheck('dark-stroke-strong-base', 'Stroke strong on dark base', 'stroke', requireLayerValue(tokens.dark, '--dry-color-stroke-strong'), darkBase, 3, 45),
|
|
782
|
-
createAuditCheck('brand-shape-light', 'Brand fill on light base', 'shape', requireLayerValue(tokens.light, '--dry-color-fill-brand'), lightBase, 3, 45),
|
|
783
|
-
createAuditCheck('brand-shape-dark', 'Brand fill on dark base', 'shape', requireLayerValue(tokens.dark, '--dry-color-fill-brand'), darkBase, 3, 45),
|
|
784
|
-
createAuditCheck('brand-on-light', 'On-brand text on brand fill (light)', 'text', requireLayerValue(tokens.light, '--dry-color-on-brand'), requireLayerValue(tokens.light, '--dry-color-fill-brand'), 4.5, 60),
|
|
785
|
-
createAuditCheck('brand-on-dark', 'On-brand text on brand fill (dark)', 'text', requireLayerValue(tokens.dark, '--dry-color-on-brand'), requireLayerValue(tokens.dark, '--dry-color-fill-brand'), 4.5, 60)
|
|
786
|
-
];
|
|
787
|
-
for (const tone of ['error', 'warning', 'success', 'info']) {
|
|
788
|
-
checks.push(createAuditCheck(`${tone}-text-light`, `${tone} text on light base`, 'text', requireLayerValue(tokens.light, `--dry-color-text-${tone}`), lightBase, 4.5, 60), createAuditCheck(`${tone}-text-dark`, `${tone} text on dark base`, 'text', requireLayerValue(tokens.dark, `--dry-color-text-${tone}`), darkBase, 4.5, 60), createAuditCheck(`${tone}-shape-light`, `${tone} fill on light base`, 'shape', requireLayerValue(tokens.light, `--dry-color-fill-${tone}`), lightBase, 3, 45), createAuditCheck(`${tone}-shape-dark`, `${tone} fill on dark base`, 'shape', requireLayerValue(tokens.dark, `--dry-color-fill-${tone}`), darkBase, 3, 45), createAuditCheck(`${tone}-on-light`, `On-${tone} text on ${tone} fill (light)`, 'text', requireLayerValue(tokens.light, `--dry-color-on-${tone}`), requireLayerValue(tokens.light, `--dry-color-fill-${tone}`), 4.5, 60), createAuditCheck(`${tone}-on-dark`, `On-${tone} text on ${tone} fill (dark)`, 'text', requireLayerValue(tokens.dark, `--dry-color-on-${tone}`), requireLayerValue(tokens.dark, `--dry-color-fill-${tone}`), 4.5, 60));
|
|
789
|
-
}
|
|
790
|
-
return {
|
|
791
|
-
contextChecks: checks,
|
|
792
|
-
allPass: checks.every((check) => check.passes)
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
function buildPhotoTemperatureGuidance(hue) {
|
|
796
|
-
if (hue >= 330 || hue <= 90) {
|
|
797
|
-
return {
|
|
798
|
-
temperature: 'warm',
|
|
799
|
-
recommendation: 'Use warmer photography or golden grading so imagery feels consistent with the palette instead of fighting it.',
|
|
800
|
-
accentDirection: 'Warm daylight, amber practicals, or subtle golden highlights.'
|
|
801
|
-
};
|
|
802
|
-
}
|
|
803
|
-
if (hue >= 150 && hue <= 270) {
|
|
804
|
-
return {
|
|
805
|
-
temperature: 'cool',
|
|
806
|
-
recommendation: 'Lean towards cooler imagery and cleaner grading so the palette and photos share the same temperature.',
|
|
807
|
-
accentDirection: 'Cool daylight, steel blues, cyan skies, or cooler interior lighting.'
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
return {
|
|
811
|
-
temperature: 'neutral',
|
|
812
|
-
recommendation: 'Keep photography temperature restrained and let the interface colour do the branding work.',
|
|
813
|
-
accentDirection: 'Balanced whites, restrained colour casts, and minimal grading bias.'
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
const THEME_ALIAS_MAP = {
|
|
817
|
-
'Theme/Neutral/Text/Strong': 'neutral.text.strong',
|
|
818
|
-
'Theme/Neutral/Text/Weak': 'neutral.text.weak',
|
|
819
|
-
'Theme/Neutral/Text/Disabled': 'neutral.text.disabled',
|
|
820
|
-
'Theme/Neutral/Icon': 'neutral.icon',
|
|
821
|
-
'Theme/Neutral/Icon/Disabled': 'neutral.icon.disabled',
|
|
822
|
-
'Theme/Neutral/Stroke/Strong': 'neutral.stroke.strong',
|
|
823
|
-
'Theme/Neutral/Stroke/Weak': 'neutral.stroke.weak',
|
|
824
|
-
'Theme/Neutral/Stroke/Focus': 'neutral.stroke.focus',
|
|
825
|
-
'Theme/Neutral/Stroke/Selected': 'neutral.stroke.selected',
|
|
826
|
-
'Theme/Neutral/Stroke/Disabled': 'neutral.stroke.disabled',
|
|
827
|
-
'Theme/Neutral/Fill/Strong': 'neutral.fill.strong',
|
|
828
|
-
'Theme/Neutral/Fill': 'neutral.fill.default',
|
|
829
|
-
'Theme/Neutral/Fill/Weak': 'neutral.fill.weak',
|
|
830
|
-
'Theme/Neutral/Fill/Weaker': 'neutral.fill.weaker',
|
|
831
|
-
'Theme/Neutral/Fill/Hover': 'neutral.fill.hover',
|
|
832
|
-
'Theme/Neutral/Fill/Active': 'neutral.fill.active',
|
|
833
|
-
'Theme/Neutral/Fill/Selected': 'neutral.fill.selected',
|
|
834
|
-
'Theme/Neutral/Fill/Disabled': 'neutral.fill.disabled',
|
|
835
|
-
'Theme/Neutral/Fill/Overlay': 'neutral.fill.overlay',
|
|
836
|
-
'Theme/Neutral/Inverse/Text': 'neutral.inverse.text.default',
|
|
837
|
-
'Theme/Neutral/Inverse/Text/Weak': 'neutral.inverse.text.weak',
|
|
838
|
-
'Theme/Neutral/Inverse/Text/Disabled': 'neutral.inverse.text.disabled',
|
|
839
|
-
'Theme/Neutral/Inverse/Icon': 'neutral.inverse.icon.default',
|
|
840
|
-
'Theme/Neutral/Inverse/Icon/Strong': 'neutral.inverse.icon.strong',
|
|
841
|
-
'Theme/Neutral/Inverse/Icon/Weak': 'neutral.inverse.icon.weak',
|
|
842
|
-
'Theme/Neutral/Inverse/Icon/Disabled': 'neutral.inverse.icon.disabled',
|
|
843
|
-
'Theme/Neutral/Inverse/Stroke': 'neutral.inverse.stroke.default',
|
|
844
|
-
'Theme/Neutral/Inverse/Stroke/Weak': 'neutral.inverse.stroke.weak',
|
|
845
|
-
'Theme/Neutral/Inverse/Fill': 'neutral.inverse.fill.default',
|
|
846
|
-
'Theme/Neutral/Inverse/Fill/Weak': 'neutral.inverse.fill.weak',
|
|
847
|
-
'Theme/Neutral/Inverse/Fill/Hover': 'neutral.inverse.fill.hover',
|
|
848
|
-
'Theme/Neutral/Inverse/Fill/Active': 'neutral.inverse.fill.active',
|
|
849
|
-
'Theme/Neutral/Inverse/Fill/Disabled': 'neutral.inverse.fill.disabled',
|
|
850
|
-
'Theme/Surface/Sunken': 'surface.sunken',
|
|
851
|
-
'Theme/Surface/Base': 'surface.base',
|
|
852
|
-
'Theme/Surface/Alternate': 'surface.alternate',
|
|
853
|
-
'Theme/Surface/Raised': 'surface.raised',
|
|
854
|
-
'Theme/Surface/Overlay': 'surface.overlay',
|
|
855
|
-
'Theme/Surface/Brand': 'surface.brand',
|
|
856
|
-
'Theme/Surface/Inverse': 'surface.inverse',
|
|
857
|
-
'Theme/Surface/Utility/White': 'surface.utility.white',
|
|
858
|
-
'Theme/Surface/Utility/Yellow': 'surface.utility.yellow',
|
|
859
|
-
'Theme/Brand/Base': 'brand.base',
|
|
860
|
-
'Theme/Brand/Text': 'brand.text',
|
|
861
|
-
'Theme/Brand/Icon': 'brand.icon',
|
|
862
|
-
'Theme/Brand/Fill': 'brand.fill',
|
|
863
|
-
'Theme/Brand/Fill/Hover': 'brand.fill.hover',
|
|
864
|
-
'Theme/Brand/Fill/Active': 'brand.fill.active',
|
|
865
|
-
'Theme/Brand/Fill/Weak': 'brand.fill.weak',
|
|
866
|
-
'Theme/Brand/Stroke': 'brand.stroke',
|
|
867
|
-
'Theme/Brand/Stroke/Strong': 'brand.stroke.strong',
|
|
868
|
-
'Theme/Brand/On': 'brand.on',
|
|
869
|
-
'Theme/Brand/FocusRing': 'brand.focus-ring',
|
|
870
|
-
'Theme/Error/Text': 'tone.error.text',
|
|
871
|
-
'Theme/Error/Icon': 'tone.error.icon',
|
|
872
|
-
'Theme/Error/Fill': 'tone.error.fill',
|
|
873
|
-
'Theme/Error/Fill/Hover': 'tone.error.fill.hover',
|
|
874
|
-
'Theme/Error/Fill/Weak': 'tone.error.fill.weak',
|
|
875
|
-
'Theme/Error/Stroke': 'tone.error.stroke',
|
|
876
|
-
'Theme/Error/Stroke/Strong': 'tone.error.stroke.strong',
|
|
877
|
-
'Theme/Error/On': 'tone.error.on',
|
|
878
|
-
'Theme/Warning/Text': 'tone.warning.text',
|
|
879
|
-
'Theme/Warning/Icon': 'tone.warning.icon',
|
|
880
|
-
'Theme/Warning/Fill': 'tone.warning.fill',
|
|
881
|
-
'Theme/Warning/Fill/Hover': 'tone.warning.fill.hover',
|
|
882
|
-
'Theme/Warning/Fill/Weak': 'tone.warning.fill.weak',
|
|
883
|
-
'Theme/Warning/Stroke': 'tone.warning.stroke',
|
|
884
|
-
'Theme/Warning/Stroke/Strong': 'tone.warning.stroke.strong',
|
|
885
|
-
'Theme/Warning/On': 'tone.warning.on',
|
|
886
|
-
'Theme/Success/Text': 'tone.success.text',
|
|
887
|
-
'Theme/Success/Icon': 'tone.success.icon',
|
|
888
|
-
'Theme/Success/Fill': 'tone.success.fill',
|
|
889
|
-
'Theme/Success/Fill/Hover': 'tone.success.fill.hover',
|
|
890
|
-
'Theme/Success/Fill/Weak': 'tone.success.fill.weak',
|
|
891
|
-
'Theme/Success/Stroke': 'tone.success.stroke',
|
|
892
|
-
'Theme/Success/Stroke/Strong': 'tone.success.stroke.strong',
|
|
893
|
-
'Theme/Success/On': 'tone.success.on',
|
|
894
|
-
'Theme/Info/Text': 'tone.info.text',
|
|
895
|
-
'Theme/Info/Icon': 'tone.info.icon',
|
|
896
|
-
'Theme/Info/Fill': 'tone.info.fill',
|
|
897
|
-
'Theme/Info/Fill/Hover': 'tone.info.fill.hover',
|
|
898
|
-
'Theme/Info/Fill/Weak': 'tone.info.fill.weak',
|
|
899
|
-
'Theme/Info/Stroke': 'tone.info.stroke',
|
|
900
|
-
'Theme/Info/Stroke/Strong': 'tone.info.stroke.strong',
|
|
901
|
-
'Theme/Info/On': 'tone.info.on',
|
|
902
|
-
'Theme/Shadow/Raised': 'shadow.raised',
|
|
903
|
-
'Theme/Shadow/Overlay': 'shadow.overlay',
|
|
904
|
-
'Theme/Overlay/Backdrop': 'overlay.backdrop',
|
|
905
|
-
'Theme/Overlay/Backdrop/Strong': 'overlay.backdrop.strong'
|
|
906
|
-
};
|
|
907
|
-
const SEMANTIC_ALIAS_MAP = {
|
|
908
|
-
'--dry-color-text-strong': 'Theme/Neutral/Text/Strong',
|
|
909
|
-
'--dry-color-text-weak': 'Theme/Neutral/Text/Weak',
|
|
910
|
-
'--dry-color-text-disabled': 'Theme/Neutral/Text/Disabled',
|
|
911
|
-
'--dry-color-icon': 'Theme/Neutral/Icon',
|
|
912
|
-
'--dry-color-icon-disabled': 'Theme/Neutral/Icon/Disabled',
|
|
913
|
-
'--dry-color-stroke-strong': 'Theme/Neutral/Stroke/Strong',
|
|
914
|
-
'--dry-color-stroke-weak': 'Theme/Neutral/Stroke/Weak',
|
|
915
|
-
'--dry-color-stroke-focus': 'Theme/Neutral/Stroke/Focus',
|
|
916
|
-
'--dry-color-stroke-selected': 'Theme/Neutral/Stroke/Selected',
|
|
917
|
-
'--dry-color-stroke-disabled': 'Theme/Neutral/Stroke/Disabled',
|
|
918
|
-
'--dry-color-fill-strong': 'Theme/Neutral/Fill/Strong',
|
|
919
|
-
'--dry-color-fill': 'Theme/Neutral/Fill',
|
|
920
|
-
'--dry-color-fill-weak': 'Theme/Neutral/Fill/Weak',
|
|
921
|
-
'--dry-color-fill-weaker': 'Theme/Neutral/Fill/Weaker',
|
|
922
|
-
'--dry-color-fill-hover': 'Theme/Neutral/Fill/Hover',
|
|
923
|
-
'--dry-color-fill-active': 'Theme/Neutral/Fill/Active',
|
|
924
|
-
'--dry-color-fill-selected': 'Theme/Neutral/Fill/Selected',
|
|
925
|
-
'--dry-color-fill-disabled': 'Theme/Neutral/Fill/Disabled',
|
|
926
|
-
'--dry-color-fill-overlay': 'Theme/Neutral/Fill/Overlay',
|
|
927
|
-
'--dry-color-text-inverse': 'Theme/Neutral/Inverse/Text',
|
|
928
|
-
'--dry-color-text-inverse-weak': 'Theme/Neutral/Inverse/Text/Weak',
|
|
929
|
-
'--dry-color-text-inverse-disabled': 'Theme/Neutral/Inverse/Text/Disabled',
|
|
930
|
-
'--dry-color-icon-inverse': 'Theme/Neutral/Inverse/Icon',
|
|
931
|
-
'--dry-color-icon-inverse-strong': 'Theme/Neutral/Inverse/Icon/Strong',
|
|
932
|
-
'--dry-color-icon-inverse-weak': 'Theme/Neutral/Inverse/Icon/Weak',
|
|
933
|
-
'--dry-color-icon-inverse-disabled': 'Theme/Neutral/Inverse/Icon/Disabled',
|
|
934
|
-
'--dry-color-stroke-inverse': 'Theme/Neutral/Inverse/Stroke',
|
|
935
|
-
'--dry-color-stroke-inverse-weak': 'Theme/Neutral/Inverse/Stroke/Weak',
|
|
936
|
-
'--dry-color-fill-inverse': 'Theme/Neutral/Inverse/Fill',
|
|
937
|
-
'--dry-color-fill-inverse-weak': 'Theme/Neutral/Inverse/Fill/Weak',
|
|
938
|
-
'--dry-color-fill-inverse-hover': 'Theme/Neutral/Inverse/Fill/Hover',
|
|
939
|
-
'--dry-color-fill-inverse-active': 'Theme/Neutral/Inverse/Fill/Active',
|
|
940
|
-
'--dry-color-fill-inverse-disabled': 'Theme/Neutral/Inverse/Fill/Disabled',
|
|
941
|
-
'--dry-color-bg-sunken': 'Theme/Surface/Sunken',
|
|
942
|
-
'--dry-color-bg-base': 'Theme/Surface/Base',
|
|
943
|
-
'--dry-color-bg-alternate': 'Theme/Surface/Alternate',
|
|
944
|
-
'--dry-color-bg-raised': 'Theme/Surface/Raised',
|
|
945
|
-
'--dry-color-bg-overlay': 'Theme/Surface/Overlay',
|
|
946
|
-
'--dry-color-bg-brand': 'Theme/Surface/Brand',
|
|
947
|
-
'--dry-color-bg-inverse': 'Theme/Surface/Inverse',
|
|
948
|
-
'--dry-color-fill-white': 'Theme/Surface/Utility/White',
|
|
949
|
-
'--dry-color-fill-yellow': 'Theme/Surface/Utility/Yellow',
|
|
950
|
-
'--dry-color-brand': 'Theme/Brand/Base',
|
|
951
|
-
'--dry-color-text-brand': 'Theme/Brand/Text',
|
|
952
|
-
'--dry-color-icon-brand': 'Theme/Brand/Icon',
|
|
953
|
-
'--dry-color-fill-brand': 'Theme/Brand/Fill',
|
|
954
|
-
'--dry-color-fill-brand-hover': 'Theme/Brand/Fill/Hover',
|
|
955
|
-
'--dry-color-fill-brand-active': 'Theme/Brand/Fill/Active',
|
|
956
|
-
'--dry-color-fill-brand-weak': 'Theme/Brand/Fill/Weak',
|
|
957
|
-
'--dry-color-stroke-brand': 'Theme/Brand/Stroke',
|
|
958
|
-
'--dry-color-stroke-brand-strong': 'Theme/Brand/Stroke/Strong',
|
|
959
|
-
'--dry-color-on-brand': 'Theme/Brand/On',
|
|
960
|
-
'--dry-color-focus-ring': 'Theme/Brand/FocusRing',
|
|
961
|
-
'--dry-color-text-error': 'Theme/Error/Text',
|
|
962
|
-
'--dry-color-icon-error': 'Theme/Error/Icon',
|
|
963
|
-
'--dry-color-fill-error': 'Theme/Error/Fill',
|
|
964
|
-
'--dry-color-fill-error-hover': 'Theme/Error/Fill/Hover',
|
|
965
|
-
'--dry-color-fill-error-weak': 'Theme/Error/Fill/Weak',
|
|
966
|
-
'--dry-color-stroke-error': 'Theme/Error/Stroke',
|
|
967
|
-
'--dry-color-stroke-error-strong': 'Theme/Error/Stroke/Strong',
|
|
968
|
-
'--dry-color-on-error': 'Theme/Error/On',
|
|
969
|
-
'--dry-color-text-warning': 'Theme/Warning/Text',
|
|
970
|
-
'--dry-color-icon-warning': 'Theme/Warning/Icon',
|
|
971
|
-
'--dry-color-fill-warning': 'Theme/Warning/Fill',
|
|
972
|
-
'--dry-color-fill-warning-hover': 'Theme/Warning/Fill/Hover',
|
|
973
|
-
'--dry-color-fill-warning-weak': 'Theme/Warning/Fill/Weak',
|
|
974
|
-
'--dry-color-stroke-warning': 'Theme/Warning/Stroke',
|
|
975
|
-
'--dry-color-stroke-warning-strong': 'Theme/Warning/Stroke/Strong',
|
|
976
|
-
'--dry-color-on-warning': 'Theme/Warning/On',
|
|
977
|
-
'--dry-color-text-success': 'Theme/Success/Text',
|
|
978
|
-
'--dry-color-icon-success': 'Theme/Success/Icon',
|
|
979
|
-
'--dry-color-fill-success': 'Theme/Success/Fill',
|
|
980
|
-
'--dry-color-fill-success-hover': 'Theme/Success/Fill/Hover',
|
|
981
|
-
'--dry-color-fill-success-weak': 'Theme/Success/Fill/Weak',
|
|
982
|
-
'--dry-color-stroke-success': 'Theme/Success/Stroke',
|
|
983
|
-
'--dry-color-stroke-success-strong': 'Theme/Success/Stroke/Strong',
|
|
984
|
-
'--dry-color-on-success': 'Theme/Success/On',
|
|
985
|
-
'--dry-color-text-info': 'Theme/Info/Text',
|
|
986
|
-
'--dry-color-icon-info': 'Theme/Info/Icon',
|
|
987
|
-
'--dry-color-fill-info': 'Theme/Info/Fill',
|
|
988
|
-
'--dry-color-fill-info-hover': 'Theme/Info/Fill/Hover',
|
|
989
|
-
'--dry-color-fill-info-weak': 'Theme/Info/Fill/Weak',
|
|
990
|
-
'--dry-color-stroke-info': 'Theme/Info/Stroke',
|
|
991
|
-
'--dry-color-stroke-info-strong': 'Theme/Info/Stroke/Strong',
|
|
992
|
-
'--dry-color-on-info': 'Theme/Info/On',
|
|
993
|
-
'--dry-shadow-raised': 'Theme/Shadow/Raised',
|
|
994
|
-
'--dry-shadow-overlay': 'Theme/Shadow/Overlay',
|
|
995
|
-
'--dry-color-overlay-backdrop': 'Theme/Overlay/Backdrop',
|
|
996
|
-
'--dry-color-overlay-backdrop-strong': 'Theme/Overlay/Backdrop/Strong'
|
|
997
|
-
};
|
|
998
|
-
function buildPrimitiveLayer(tokens) {
|
|
999
|
-
const primitives = {
|
|
1000
|
-
'neutral.text.strong': requireLayerValue(tokens, '--dry-color-text-strong'),
|
|
1001
|
-
'neutral.text.weak': requireLayerValue(tokens, '--dry-color-text-weak'),
|
|
1002
|
-
'neutral.text.disabled': requireLayerValue(tokens, '--dry-color-text-disabled'),
|
|
1003
|
-
'neutral.icon': requireLayerValue(tokens, '--dry-color-icon'),
|
|
1004
|
-
'neutral.icon.disabled': requireLayerValue(tokens, '--dry-color-icon-disabled'),
|
|
1005
|
-
'neutral.stroke.strong': requireLayerValue(tokens, '--dry-color-stroke-strong'),
|
|
1006
|
-
'neutral.stroke.weak': requireLayerValue(tokens, '--dry-color-stroke-weak'),
|
|
1007
|
-
'neutral.stroke.focus': requireLayerValue(tokens, '--dry-color-stroke-focus'),
|
|
1008
|
-
'neutral.stroke.selected': requireLayerValue(tokens, '--dry-color-stroke-selected'),
|
|
1009
|
-
'neutral.stroke.disabled': requireLayerValue(tokens, '--dry-color-stroke-disabled'),
|
|
1010
|
-
'neutral.fill.strong': requireLayerValue(tokens, '--dry-color-fill-strong'),
|
|
1011
|
-
'neutral.fill.default': requireLayerValue(tokens, '--dry-color-fill'),
|
|
1012
|
-
'neutral.fill.weak': requireLayerValue(tokens, '--dry-color-fill-weak'),
|
|
1013
|
-
'neutral.fill.weaker': requireLayerValue(tokens, '--dry-color-fill-weaker'),
|
|
1014
|
-
'neutral.fill.hover': requireLayerValue(tokens, '--dry-color-fill-hover'),
|
|
1015
|
-
'neutral.fill.active': requireLayerValue(tokens, '--dry-color-fill-active'),
|
|
1016
|
-
'neutral.fill.selected': requireLayerValue(tokens, '--dry-color-fill-selected'),
|
|
1017
|
-
'neutral.fill.disabled': requireLayerValue(tokens, '--dry-color-fill-disabled'),
|
|
1018
|
-
'neutral.fill.overlay': requireLayerValue(tokens, '--dry-color-fill-overlay'),
|
|
1019
|
-
'neutral.inverse.text.default': requireLayerValue(tokens, '--dry-color-text-inverse'),
|
|
1020
|
-
'neutral.inverse.text.weak': requireLayerValue(tokens, '--dry-color-text-inverse-weak'),
|
|
1021
|
-
'neutral.inverse.text.disabled': requireLayerValue(tokens, '--dry-color-text-inverse-disabled'),
|
|
1022
|
-
'neutral.inverse.icon.default': requireLayerValue(tokens, '--dry-color-icon-inverse'),
|
|
1023
|
-
'neutral.inverse.icon.strong': requireLayerValue(tokens, '--dry-color-icon-inverse-strong'),
|
|
1024
|
-
'neutral.inverse.icon.weak': requireLayerValue(tokens, '--dry-color-icon-inverse-weak'),
|
|
1025
|
-
'neutral.inverse.icon.disabled': requireLayerValue(tokens, '--dry-color-icon-inverse-disabled'),
|
|
1026
|
-
'neutral.inverse.stroke.default': requireLayerValue(tokens, '--dry-color-stroke-inverse'),
|
|
1027
|
-
'neutral.inverse.stroke.weak': requireLayerValue(tokens, '--dry-color-stroke-inverse-weak'),
|
|
1028
|
-
'neutral.inverse.fill.default': requireLayerValue(tokens, '--dry-color-fill-inverse'),
|
|
1029
|
-
'neutral.inverse.fill.weak': requireLayerValue(tokens, '--dry-color-fill-inverse-weak'),
|
|
1030
|
-
'neutral.inverse.fill.hover': requireLayerValue(tokens, '--dry-color-fill-inverse-hover'),
|
|
1031
|
-
'neutral.inverse.fill.active': requireLayerValue(tokens, '--dry-color-fill-inverse-active'),
|
|
1032
|
-
'neutral.inverse.fill.disabled': requireLayerValue(tokens, '--dry-color-fill-inverse-disabled'),
|
|
1033
|
-
'surface.sunken': requireLayerValue(tokens, '--dry-color-bg-sunken'),
|
|
1034
|
-
'surface.base': requireLayerValue(tokens, '--dry-color-bg-base'),
|
|
1035
|
-
'surface.alternate': requireLayerValue(tokens, '--dry-color-bg-alternate'),
|
|
1036
|
-
'surface.raised': requireLayerValue(tokens, '--dry-color-bg-raised'),
|
|
1037
|
-
'surface.overlay': requireLayerValue(tokens, '--dry-color-bg-overlay'),
|
|
1038
|
-
'surface.brand': requireLayerValue(tokens, '--dry-color-bg-brand'),
|
|
1039
|
-
'surface.inverse': requireLayerValue(tokens, '--dry-color-bg-inverse'),
|
|
1040
|
-
'surface.utility.white': requireLayerValue(tokens, '--dry-color-fill-white'),
|
|
1041
|
-
'surface.utility.yellow': requireLayerValue(tokens, '--dry-color-fill-yellow'),
|
|
1042
|
-
'brand.base': requireLayerValue(tokens, '--dry-color-brand'),
|
|
1043
|
-
'brand.text': requireLayerValue(tokens, '--dry-color-text-brand'),
|
|
1044
|
-
'brand.icon': requireLayerValue(tokens, '--dry-color-icon-brand'),
|
|
1045
|
-
'brand.fill': requireLayerValue(tokens, '--dry-color-fill-brand'),
|
|
1046
|
-
'brand.fill.hover': requireLayerValue(tokens, '--dry-color-fill-brand-hover'),
|
|
1047
|
-
'brand.fill.active': requireLayerValue(tokens, '--dry-color-fill-brand-active'),
|
|
1048
|
-
'brand.fill.weak': requireLayerValue(tokens, '--dry-color-fill-brand-weak'),
|
|
1049
|
-
'brand.stroke': requireLayerValue(tokens, '--dry-color-stroke-brand'),
|
|
1050
|
-
'brand.stroke.strong': requireLayerValue(tokens, '--dry-color-stroke-brand-strong'),
|
|
1051
|
-
'brand.on': requireLayerValue(tokens, '--dry-color-on-brand'),
|
|
1052
|
-
'brand.focus-ring': requireLayerValue(tokens, '--dry-color-focus-ring'),
|
|
1053
|
-
'shadow.raised': requireLayerValue(tokens, '--dry-shadow-raised'),
|
|
1054
|
-
'shadow.overlay': requireLayerValue(tokens, '--dry-shadow-overlay'),
|
|
1055
|
-
'overlay.backdrop': requireLayerValue(tokens, '--dry-color-overlay-backdrop'),
|
|
1056
|
-
'overlay.backdrop.strong': requireLayerValue(tokens, '--dry-color-overlay-backdrop-strong')
|
|
1057
|
-
};
|
|
1058
|
-
for (const tone of ['error', 'warning', 'success', 'info']) {
|
|
1059
|
-
primitives[`tone.${tone}.text`] = requireLayerValue(tokens, `--dry-color-text-${tone}`);
|
|
1060
|
-
primitives[`tone.${tone}.icon`] = requireLayerValue(tokens, `--dry-color-icon-${tone}`);
|
|
1061
|
-
primitives[`tone.${tone}.fill`] = requireLayerValue(tokens, `--dry-color-fill-${tone}`);
|
|
1062
|
-
primitives[`tone.${tone}.fill.hover`] = requireLayerValue(tokens, `--dry-color-fill-${tone}-hover`);
|
|
1063
|
-
primitives[`tone.${tone}.fill.weak`] = requireLayerValue(tokens, `--dry-color-fill-${tone}-weak`);
|
|
1064
|
-
primitives[`tone.${tone}.stroke`] = requireLayerValue(tokens, `--dry-color-stroke-${tone}`);
|
|
1065
|
-
primitives[`tone.${tone}.stroke.strong`] = requireLayerValue(tokens, `--dry-color-stroke-${tone}-strong`);
|
|
1066
|
-
primitives[`tone.${tone}.on`] = requireLayerValue(tokens, `--dry-color-on-${tone}`);
|
|
1067
|
-
}
|
|
1068
|
-
return primitives;
|
|
1069
|
-
}
|
|
1070
|
-
function buildTransparentNeutralLadder(tokens) {
|
|
1071
|
-
return {
|
|
1072
|
-
textStrong: requireLayerValue(tokens, '--dry-color-text-strong'),
|
|
1073
|
-
textWeak: requireLayerValue(tokens, '--dry-color-text-weak'),
|
|
1074
|
-
icon: requireLayerValue(tokens, '--dry-color-icon'),
|
|
1075
|
-
strokeStrong: requireLayerValue(tokens, '--dry-color-stroke-strong'),
|
|
1076
|
-
strokeWeak: requireLayerValue(tokens, '--dry-color-stroke-weak'),
|
|
1077
|
-
fill: requireLayerValue(tokens, '--dry-color-fill'),
|
|
1078
|
-
fillHover: requireLayerValue(tokens, '--dry-color-fill-hover'),
|
|
1079
|
-
fillActive: requireLayerValue(tokens, '--dry-color-fill-active')
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
function buildTransparentBrandLadder(tokens) {
|
|
1083
|
-
return {
|
|
1084
|
-
brand: requireLayerValue(tokens, '--dry-color-brand'),
|
|
1085
|
-
text: requireLayerValue(tokens, '--dry-color-text-brand'),
|
|
1086
|
-
fill: requireLayerValue(tokens, '--dry-color-fill-brand'),
|
|
1087
|
-
fillHover: requireLayerValue(tokens, '--dry-color-fill-brand-hover'),
|
|
1088
|
-
fillActive: requireLayerValue(tokens, '--dry-color-fill-brand-active'),
|
|
1089
|
-
fillWeak: requireLayerValue(tokens, '--dry-color-fill-brand-weak'),
|
|
1090
|
-
stroke: requireLayerValue(tokens, '--dry-color-stroke-brand'),
|
|
1091
|
-
on: requireLayerValue(tokens, '--dry-color-on-brand'),
|
|
1092
|
-
focusRing: requireLayerValue(tokens, '--dry-color-focus-ring')
|
|
1093
|
-
};
|
|
1094
|
-
}
|
|
1095
|
-
function buildTransparentToneLadder(tokens, tone) {
|
|
1096
|
-
return {
|
|
1097
|
-
text: requireLayerValue(tokens, `--dry-color-text-${tone}`),
|
|
1098
|
-
fill: requireLayerValue(tokens, `--dry-color-fill-${tone}`),
|
|
1099
|
-
fillHover: requireLayerValue(tokens, `--dry-color-fill-${tone}-hover`),
|
|
1100
|
-
fillWeak: requireLayerValue(tokens, `--dry-color-fill-${tone}-weak`),
|
|
1101
|
-
stroke: requireLayerValue(tokens, `--dry-color-stroke-${tone}`),
|
|
1102
|
-
on: requireLayerValue(tokens, `--dry-color-on-${tone}`)
|
|
1103
|
-
};
|
|
1104
|
-
}
|
|
1105
|
-
function buildTransparentPrimitiveLadders(tokens) {
|
|
1106
|
-
return {
|
|
1107
|
-
neutral: {
|
|
1108
|
-
light: buildTransparentNeutralLadder(tokens.light),
|
|
1109
|
-
dark: buildTransparentNeutralLadder(tokens.dark)
|
|
1110
|
-
},
|
|
1111
|
-
brand: {
|
|
1112
|
-
light: buildTransparentBrandLadder(tokens.light),
|
|
1113
|
-
dark: buildTransparentBrandLadder(tokens.dark)
|
|
1114
|
-
},
|
|
1115
|
-
system: {
|
|
1116
|
-
error: {
|
|
1117
|
-
light: buildTransparentToneLadder(tokens.light, 'error'),
|
|
1118
|
-
dark: buildTransparentToneLadder(tokens.dark, 'error')
|
|
1119
|
-
},
|
|
1120
|
-
warning: {
|
|
1121
|
-
light: buildTransparentToneLadder(tokens.light, 'warning'),
|
|
1122
|
-
dark: buildTransparentToneLadder(tokens.dark, 'warning')
|
|
1123
|
-
},
|
|
1124
|
-
success: {
|
|
1125
|
-
light: buildTransparentToneLadder(tokens.light, 'success'),
|
|
1126
|
-
dark: buildTransparentToneLadder(tokens.dark, 'success')
|
|
1127
|
-
},
|
|
1128
|
-
info: {
|
|
1129
|
-
light: buildTransparentToneLadder(tokens.light, 'info'),
|
|
1130
|
-
dark: buildTransparentToneLadder(tokens.dark, 'info')
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
};
|
|
1134
|
-
}
|
|
1135
|
-
function resolveLayer(sources, mapping) {
|
|
1136
|
-
const resolved = {};
|
|
1137
|
-
for (const [name, source] of Object.entries(mapping)) {
|
|
1138
|
-
resolved[name] = {
|
|
1139
|
-
source,
|
|
1140
|
-
value: requireLayerValue(sources, source)
|
|
1141
|
-
};
|
|
1142
|
-
}
|
|
1143
|
-
return resolved;
|
|
1144
|
-
}
|
|
1145
|
-
function flattenResolvedLayer(layer) {
|
|
1146
|
-
const flattened = {};
|
|
1147
|
-
for (const [name, reference] of Object.entries(layer)) {
|
|
1148
|
-
flattened[name] = reference.value;
|
|
1149
|
-
}
|
|
1150
|
-
return flattened;
|
|
1151
|
-
}
|
|
1152
|
-
export function generateThemeModel(brand, options) {
|
|
1153
|
-
const tokens = generateTheme(brand, options);
|
|
1154
|
-
const brandPolicy = buildBrandPolicy(brand, options);
|
|
1155
|
-
const primitives = {
|
|
1156
|
-
light: buildPrimitiveLayer(tokens.light),
|
|
1157
|
-
dark: buildPrimitiveLayer(tokens.dark)
|
|
1158
|
-
};
|
|
1159
|
-
const transparentPrimitives = buildTransparentPrimitiveLadders(tokens);
|
|
1160
|
-
const literalTransparentPrimitives = buildLiteralTransparentPrimitiveLadders(brandPolicy, options);
|
|
1161
|
-
const solidPrimitives = buildSolidPrimitiveLadders(brandPolicy.interactive.resolvedInput.h, options?.neutralMode ?? 'monochromatic');
|
|
1162
|
-
const interactionStates = buildInteractionStateRecipes(tokens);
|
|
1163
|
-
const audit = buildThemeAudit(tokens);
|
|
1164
|
-
const themeAliases = {
|
|
1165
|
-
light: resolveLayer(primitives.light, THEME_ALIAS_MAP),
|
|
1166
|
-
dark: resolveLayer(primitives.dark, THEME_ALIAS_MAP)
|
|
1167
|
-
};
|
|
1168
|
-
const semantic = {
|
|
1169
|
-
light: resolveLayer(flattenResolvedLayer(themeAliases.light), SEMANTIC_ALIAS_MAP),
|
|
1170
|
-
dark: resolveLayer(flattenResolvedLayer(themeAliases.dark), SEMANTIC_ALIAS_MAP)
|
|
1171
|
-
};
|
|
1172
|
-
return {
|
|
1173
|
-
primitives,
|
|
1174
|
-
transparentPrimitives,
|
|
1175
|
-
literalTransparentPrimitives,
|
|
1176
|
-
solidPrimitives,
|
|
1177
|
-
interactionStates,
|
|
1178
|
-
brandPolicy,
|
|
1179
|
-
audit,
|
|
1180
|
-
photoGuidance: buildPhotoTemperatureGuidance(brandPolicy.interactive.resolvedInput.h),
|
|
1181
|
-
_theme: themeAliases,
|
|
1182
|
-
semantic,
|
|
1183
|
-
tokens
|
|
1184
|
-
};
|
|
1185
|
-
}
|
|
1186
|
-
// ─── Full Palette Generation ──────────────────────────────────────────────────
|
|
1187
|
-
/**
|
|
1188
|
-
* Generate a complete design token set for light and dark modes
|
|
1189
|
-
* from a brand color specified in HSB.
|
|
1190
|
-
*
|
|
1191
|
-
* @param brand.h Hue, 0–360
|
|
1192
|
-
* @param brand.s Saturation, 0–100
|
|
1193
|
-
* @param brand.b Brightness, 0–100
|
|
1194
|
-
*/
|
|
1195
|
-
export function generateTheme(brand, options) {
|
|
1196
|
-
// Validate input ranges
|
|
1197
|
-
if (brand.h < 0 || brand.h > 360)
|
|
1198
|
-
throw new Error(`brand.h must be 0–360, got ${brand.h}`);
|
|
1199
|
-
if (brand.s < 0 || brand.s > 100)
|
|
1200
|
-
throw new Error(`brand.s must be 0–100, got ${brand.s}`);
|
|
1201
|
-
if (brand.b < 0 || brand.b > 100)
|
|
1202
|
-
throw new Error(`brand.b must be 0–100, got ${brand.b}`);
|
|
1203
|
-
const brandPolicy = buildBrandPolicy(brand, options);
|
|
1204
|
-
const interactiveBrand = brandPolicy.interactive.resolvedInput;
|
|
1205
|
-
// Normalize 0-100 → 0-1 for s and b; wrap hue into [0, 360)
|
|
1206
|
-
const normH = normalizeHue(interactiveBrand.h);
|
|
1207
|
-
const normS = interactiveBrand.s / 100;
|
|
1208
|
-
const normB = interactiveBrand.b / 100;
|
|
1209
|
-
// Convert brand HSB → HSL
|
|
1210
|
-
const brandHsl = hsbToHsl(normH, normS, normB);
|
|
1211
|
-
const H = brandHsl.h;
|
|
1212
|
-
const S = brandHsl.s;
|
|
1213
|
-
const L = brandHsl.l;
|
|
1214
|
-
const darkBrandBase = deriveDarkModeAccent(normH, normS, normB);
|
|
1215
|
-
const neutralMode = options?.neutralMode ?? 'monochromatic';
|
|
1216
|
-
const light = {};
|
|
1217
|
-
const dark = {};
|
|
1218
|
-
// ── Neutrals (8 tokens) ──────────────────────────────────────────────────
|
|
1219
|
-
// Light: brand-hue-tinted for monochromatic mode, neutral black for neutral mode
|
|
1220
|
-
// Dark: white-based, hsla(0, 0%, 100%, alpha)
|
|
1221
|
-
// text-strong uses lightness 0.15; all others use 0.20
|
|
1222
|
-
const neutralAlphas = {
|
|
1223
|
-
'text-strong': { light: 0.9, dark: 1.0 },
|
|
1224
|
-
'text-weak': { light: 0.65, dark: 0.78 },
|
|
1225
|
-
icon: { light: 0.7, dark: 0.6 },
|
|
1226
|
-
'stroke-strong': { light: 0.7, dark: 0.6 },
|
|
1227
|
-
'stroke-weak': { light: 0.1, dark: 0.12 },
|
|
1228
|
-
fill: { light: 0.04, dark: 0.06 },
|
|
1229
|
-
'fill-hover': { light: 0.04, dark: 0.06 },
|
|
1230
|
-
'fill-active': { light: 0.1, dark: 0.12 }
|
|
1231
|
-
};
|
|
1232
|
-
for (const [name, alphas] of Object.entries(neutralAlphas)) {
|
|
1233
|
-
const lightness = name === 'text-strong' ? 0.15 : 0.2;
|
|
1234
|
-
const lightHue = neutralMode === 'neutral' ? 0 : H;
|
|
1235
|
-
const lightSaturation = neutralMode === 'neutral' ? 0 : 1.0;
|
|
1236
|
-
light[`--dry-color-${name}`] = hsla(lightHue, lightSaturation, lightness, alphas.light);
|
|
1237
|
-
dark[`--dry-color-${name}`] = hsla(0, 0, 1.0, alphas.dark);
|
|
1238
|
-
}
|
|
1239
|
-
// ── Brand core and semantic helpers ──────────────────────────────────────
|
|
1240
|
-
// text-brand: iteratively darken (light) or lighten (dark) for 4.5:1 contrast
|
|
1241
|
-
const whiteLum = 1.0;
|
|
1242
|
-
const darkBgCss = neutralMode === 'neutral' ? hsl(0, 0, 0.1) : hsl(H, 0.3, 0.1);
|
|
1243
|
-
const darkBgLum = neutralMode === 'neutral' ? luminanceFromHsl(0, 0, 0.1) : luminanceFromHsl(H, 0.3, 0.1);
|
|
1244
|
-
const textBrandLight = adjustCssColorForReadability(H, S, L, '#ffffff', 4.5, 60, 'darken', 0.25, 0.8);
|
|
1245
|
-
light['--dry-color-text-brand'] = hsl(textBrandLight.h, textBrandLight.s, textBrandLight.l);
|
|
1246
|
-
const textBrandDark = adjustCssColorForReadability(darkBrandBase.h, darkBrandBase.s, darkBrandBase.l, darkBgCss, 4.5, 60, 'lighten', 0.25, 0.88);
|
|
1247
|
-
dark['--dry-color-text-brand'] = hsl(textBrandDark.h, textBrandDark.s, textBrandDark.l);
|
|
1248
|
-
const fillBrandLight = lightenOrDarkenFillForContrast(H, S, L, whiteLum, 3, 'darken');
|
|
1249
|
-
const fillBrandDark = lightenOrDarkenFillForContrast(darkBrandBase.h, darkBrandBase.s, darkBrandBase.l, darkBgLum, 3, 'lighten');
|
|
1250
|
-
// fill-brand: use the resolved interactive brand and enforce a minimum 3:1 shape contrast.
|
|
1251
|
-
light['--dry-color-brand'] = hsl(fillBrandLight.h, fillBrandLight.s, fillBrandLight.l);
|
|
1252
|
-
dark['--dry-color-brand'] = hsl(fillBrandDark.h, fillBrandDark.s, fillBrandDark.l);
|
|
1253
|
-
light['--dry-color-fill-brand'] = hsl(fillBrandLight.h, fillBrandLight.s, fillBrandLight.l);
|
|
1254
|
-
dark['--dry-color-fill-brand'] = hsl(fillBrandDark.h, fillBrandDark.s, fillBrandDark.l);
|
|
1255
|
-
// fill-brand-hover: darken in light mode, lighten in dark mode.
|
|
1256
|
-
light['--dry-color-fill-brand-hover'] = hsl(fillBrandLight.h, fillBrandLight.s, clamp(fillBrandLight.l - 0.08, 0, 1));
|
|
1257
|
-
dark['--dry-color-fill-brand-hover'] = hsl(fillBrandDark.h, fillBrandDark.s, clamp(fillBrandDark.l + 0.08, 0, 1));
|
|
1258
|
-
// fill-brand-active: L-14% light, L-6% dark (pressed darkens in both modes)
|
|
1259
|
-
light['--dry-color-fill-brand-active'] = hsl(fillBrandLight.h, fillBrandLight.s, clamp(fillBrandLight.l - 0.14, 0, 1));
|
|
1260
|
-
dark['--dry-color-fill-brand-active'] = hsl(fillBrandDark.h, fillBrandDark.s, clamp(fillBrandDark.l - 0.06, 0, 1));
|
|
1261
|
-
// fill-brand-weak: keep the semantic weak brand background separate from the literal primitive ladder.
|
|
1262
|
-
light['--dry-color-fill-brand-weak'] = hsla(fillBrandLight.h, fillBrandLight.s, fillBrandLight.l, 0.1);
|
|
1263
|
-
dark['--dry-color-fill-brand-weak'] = hsla(fillBrandDark.h, fillBrandDark.s, fillBrandDark.l, 0.15);
|
|
1264
|
-
// stroke-brand: keep the brand outline visible against both base surfaces.
|
|
1265
|
-
light['--dry-color-stroke-brand'] = hsla(fillBrandLight.h, fillBrandLight.s, fillBrandLight.l, 0.5);
|
|
1266
|
-
dark['--dry-color-stroke-brand'] = hsla(fillBrandDark.h, fillBrandDark.s, fillBrandDark.l, 0.5);
|
|
1267
|
-
// on-brand: white if whiteContrast>=4.5, else dark tint
|
|
1268
|
-
light['--dry-color-on-brand'] = pickOnColor(fillBrandLight.h, fillBrandLight.s, fillBrandLight.l);
|
|
1269
|
-
dark['--dry-color-on-brand'] = pickOnColor(fillBrandDark.h, fillBrandDark.s, fillBrandDark.l);
|
|
1270
|
-
// focus-ring: light uses hsla(H,S,L,0.4); dark bumps L by +10%
|
|
1271
|
-
light['--dry-color-focus-ring'] = hsla(fillBrandLight.h, fillBrandLight.s, fillBrandLight.l, 0.4);
|
|
1272
|
-
dark['--dry-color-focus-ring'] = hsla(fillBrandDark.h, fillBrandDark.s, clamp(fillBrandDark.l + 0.1, 0, 1), 0.4);
|
|
1273
|
-
light['--dry-color-stroke-brand-strong'] = hsla(fillBrandLight.h, fillBrandLight.s, fillBrandLight.l, 0.8);
|
|
1274
|
-
dark['--dry-color-stroke-brand-strong'] = hsla(fillBrandDark.h, fillBrandDark.s, fillBrandDark.l, 0.8);
|
|
1275
|
-
light['--dry-color-icon-brand'] = light['--dry-color-stroke-brand-strong'];
|
|
1276
|
-
dark['--dry-color-icon-brand'] = dark['--dry-color-stroke-brand-strong'];
|
|
1277
|
-
// ── Background core plus semantic surface helpers ────────────────────────
|
|
1278
|
-
light['--dry-color-bg-base'] = '#ffffff';
|
|
1279
|
-
light['--dry-color-bg-raised'] = '#ffffff';
|
|
1280
|
-
light['--dry-color-bg-overlay'] = '#ffffff';
|
|
1281
|
-
if (options?.darkBg?.base) {
|
|
1282
|
-
dark['--dry-color-bg-base'] = options.darkBg.base;
|
|
1283
|
-
}
|
|
1284
|
-
else {
|
|
1285
|
-
dark['--dry-color-bg-base'] = neutralMode === 'neutral' ? hsl(0, 0, 0.1) : hsl(H, 0.3, 0.1);
|
|
1286
|
-
}
|
|
1287
|
-
if (options?.darkBg?.raised) {
|
|
1288
|
-
dark['--dry-color-bg-raised'] = options.darkBg.raised;
|
|
1289
|
-
}
|
|
1290
|
-
else {
|
|
1291
|
-
dark['--dry-color-bg-raised'] =
|
|
1292
|
-
neutralMode === 'neutral' ? hsl(0, 0, 0.15) : hsl(H, 0.25, 0.15);
|
|
1293
|
-
}
|
|
1294
|
-
if (options?.darkBg?.overlay) {
|
|
1295
|
-
dark['--dry-color-bg-overlay'] = options.darkBg.overlay;
|
|
1296
|
-
}
|
|
1297
|
-
else {
|
|
1298
|
-
dark['--dry-color-bg-overlay'] = neutralMode === 'neutral' ? hsl(0, 0, 0.2) : hsl(H, 0.2, 0.2);
|
|
1299
|
-
}
|
|
1300
|
-
const solidPrimitives = buildSolidPrimitiveLadders(interactiveBrand.h, neutralMode);
|
|
1301
|
-
const lightTextStrong = requireLayerValue(light, '--dry-color-text-strong');
|
|
1302
|
-
const lightTextWeak = requireLayerValue(light, '--dry-color-text-weak');
|
|
1303
|
-
const lightStrokeStrong = requireLayerValue(light, '--dry-color-stroke-strong');
|
|
1304
|
-
const lightStrokeWeak = requireLayerValue(light, '--dry-color-stroke-weak');
|
|
1305
|
-
const lightFill = requireLayerValue(light, '--dry-color-fill');
|
|
1306
|
-
const lightBrand = requireLayerValue(light, '--dry-color-brand');
|
|
1307
|
-
const lightFillBrand = requireLayerValue(light, '--dry-color-fill-brand');
|
|
1308
|
-
const darkTextStrong = requireLayerValue(dark, '--dry-color-text-strong');
|
|
1309
|
-
const darkTextWeak = requireLayerValue(dark, '--dry-color-text-weak');
|
|
1310
|
-
const darkStrokeStrong = requireLayerValue(dark, '--dry-color-stroke-strong');
|
|
1311
|
-
const darkStrokeWeak = requireLayerValue(dark, '--dry-color-stroke-weak');
|
|
1312
|
-
const darkFill = requireLayerValue(dark, '--dry-color-fill');
|
|
1313
|
-
const darkBrand = requireLayerValue(dark, '--dry-color-brand');
|
|
1314
|
-
const darkFillBrand = requireLayerValue(dark, '--dry-color-fill-brand');
|
|
1315
|
-
const darkBgBase = requireLayerValue(dark, '--dry-color-bg-base');
|
|
1316
|
-
const lightBgBase = requireLayerValue(light, '--dry-color-bg-base');
|
|
1317
|
-
light['--dry-color-bg-sunken'] = solidPrimitives.grey.light.roles.sunken;
|
|
1318
|
-
light['--dry-color-bg-alternate'] = solidPrimitives.grey.light.roles.sunken;
|
|
1319
|
-
light['--dry-color-bg-brand'] = lightFillBrand;
|
|
1320
|
-
light['--dry-color-bg-inverse'] = darkBgBase;
|
|
1321
|
-
dark['--dry-color-bg-sunken'] = solidPrimitives.grey.dark.roles.sunken;
|
|
1322
|
-
dark['--dry-color-bg-alternate'] = solidPrimitives.grey.dark.roles.sunken;
|
|
1323
|
-
dark['--dry-color-bg-brand'] = darkFillBrand;
|
|
1324
|
-
dark['--dry-color-bg-inverse'] = lightBgBase;
|
|
1325
|
-
const lightNeutralHue = neutralMode === 'neutral' ? 0 : H;
|
|
1326
|
-
const lightNeutralSaturation = neutralMode === 'neutral' ? 0 : 1.0;
|
|
1327
|
-
const lightWeakerFill = hsla(lightNeutralHue, lightNeutralSaturation, 0.2, 0.02);
|
|
1328
|
-
const darkWeakerFill = hsla(0, 0, 1.0, 0.03);
|
|
1329
|
-
light['--dry-color-text-disabled'] = lightStrokeWeak;
|
|
1330
|
-
dark['--dry-color-text-disabled'] = darkStrokeWeak;
|
|
1331
|
-
light['--dry-color-text-inverse'] = darkTextStrong;
|
|
1332
|
-
light['--dry-color-text-inverse-weak'] = darkTextWeak;
|
|
1333
|
-
light['--dry-color-text-inverse-disabled'] = darkStrokeWeak;
|
|
1334
|
-
dark['--dry-color-text-inverse'] = lightTextStrong;
|
|
1335
|
-
dark['--dry-color-text-inverse-weak'] = lightTextWeak;
|
|
1336
|
-
dark['--dry-color-text-inverse-disabled'] = lightStrokeWeak;
|
|
1337
|
-
light['--dry-color-icon-disabled'] = lightStrokeWeak;
|
|
1338
|
-
dark['--dry-color-icon-disabled'] = darkStrokeWeak;
|
|
1339
|
-
light['--dry-color-icon-inverse'] = darkStrokeStrong;
|
|
1340
|
-
light['--dry-color-icon-inverse-strong'] = darkTextStrong;
|
|
1341
|
-
light['--dry-color-icon-inverse-weak'] = darkTextWeak;
|
|
1342
|
-
light['--dry-color-icon-inverse-disabled'] = darkStrokeWeak;
|
|
1343
|
-
dark['--dry-color-icon-inverse'] = lightStrokeStrong;
|
|
1344
|
-
dark['--dry-color-icon-inverse-strong'] = lightTextStrong;
|
|
1345
|
-
dark['--dry-color-icon-inverse-weak'] = lightTextWeak;
|
|
1346
|
-
dark['--dry-color-icon-inverse-disabled'] = lightStrokeWeak;
|
|
1347
|
-
light['--dry-color-stroke-focus'] = lightBrand;
|
|
1348
|
-
light['--dry-color-stroke-selected'] = lightBrand;
|
|
1349
|
-
light['--dry-color-stroke-disabled'] = lightStrokeWeak;
|
|
1350
|
-
light['--dry-color-stroke-inverse'] = darkStrokeStrong;
|
|
1351
|
-
light['--dry-color-stroke-inverse-weak'] = darkStrokeWeak;
|
|
1352
|
-
dark['--dry-color-stroke-focus'] = darkBrand;
|
|
1353
|
-
dark['--dry-color-stroke-selected'] = darkBrand;
|
|
1354
|
-
dark['--dry-color-stroke-disabled'] = darkStrokeWeak;
|
|
1355
|
-
dark['--dry-color-stroke-inverse'] = lightStrokeStrong;
|
|
1356
|
-
dark['--dry-color-stroke-inverse-weak'] = lightStrokeWeak;
|
|
1357
|
-
light['--dry-color-fill-strong'] = lightTextStrong;
|
|
1358
|
-
light['--dry-color-fill-weak'] = lightFill;
|
|
1359
|
-
light['--dry-color-fill-weaker'] = lightWeakerFill;
|
|
1360
|
-
light['--dry-color-fill-selected'] = lightFillBrand;
|
|
1361
|
-
light['--dry-color-fill-disabled'] = lightStrokeWeak;
|
|
1362
|
-
light['--dry-color-fill-overlay'] = lightStrokeStrong;
|
|
1363
|
-
light['--dry-color-fill-inverse'] = darkTextStrong;
|
|
1364
|
-
light['--dry-color-fill-inverse-weak'] = darkFill;
|
|
1365
|
-
light['--dry-color-fill-inverse-hover'] = darkFill;
|
|
1366
|
-
light['--dry-color-fill-inverse-active'] = darkStrokeWeak;
|
|
1367
|
-
light['--dry-color-fill-inverse-disabled'] = darkWeakerFill;
|
|
1368
|
-
light['--dry-color-fill-white'] = '#ffffff';
|
|
1369
|
-
light['--dry-color-fill-yellow'] = solidPrimitives.yellow.light['1000'];
|
|
1370
|
-
dark['--dry-color-fill-strong'] = darkTextStrong;
|
|
1371
|
-
dark['--dry-color-fill-weak'] = darkFill;
|
|
1372
|
-
dark['--dry-color-fill-weaker'] = darkWeakerFill;
|
|
1373
|
-
dark['--dry-color-fill-selected'] = darkFillBrand;
|
|
1374
|
-
dark['--dry-color-fill-disabled'] = darkStrokeWeak;
|
|
1375
|
-
dark['--dry-color-fill-overlay'] = darkStrokeStrong;
|
|
1376
|
-
dark['--dry-color-fill-inverse'] = lightTextStrong;
|
|
1377
|
-
dark['--dry-color-fill-inverse-weak'] = lightFill;
|
|
1378
|
-
dark['--dry-color-fill-inverse-hover'] = lightFill;
|
|
1379
|
-
dark['--dry-color-fill-inverse-active'] = lightStrokeWeak;
|
|
1380
|
-
dark['--dry-color-fill-inverse-disabled'] = lightWeakerFill;
|
|
1381
|
-
dark['--dry-color-fill-white'] = '#ffffff';
|
|
1382
|
-
dark['--dry-color-fill-yellow'] = solidPrimitives.yellow.dark['1000'];
|
|
1383
|
-
// ── Status core plus semantic helpers ────────────────────────────────────
|
|
1384
|
-
const statusHues = {
|
|
1385
|
-
error: options?.statusHues?.error ?? 0,
|
|
1386
|
-
warning: options?.statusHues?.warning ?? 40,
|
|
1387
|
-
success: options?.statusHues?.success ?? 145,
|
|
1388
|
-
info: options?.statusHues?.info ?? 210
|
|
1389
|
-
};
|
|
1390
|
-
const bgLumLight = 1.0; // white background
|
|
1391
|
-
const bgLumDark = neutralMode === 'neutral' ? luminanceFromHsl(0, 0, 0.1) : luminanceFromHsl(H, 0.3, 0.1);
|
|
1392
|
-
for (const [tone, hue] of Object.entries(statusHues)) {
|
|
1393
|
-
// fill-{tone}: start from the Practical UI hue family, then enforce minimum shape contrast.
|
|
1394
|
-
const fillLightBase = adjustCssColorForReadability(hue, 0.7, 0.5, '#ffffff', 3, 45, 'darken', 0.1, 0.8);
|
|
1395
|
-
const fillLightPair = adjustFillForOnColor(fillLightBase, '#ffffff', 3, 45, 'darken', 0.08, 0.8);
|
|
1396
|
-
const fillDarkBase = adjustCssColorForReadability(hue, 0.65, 0.55, darkBgCss, 3, 45, 'lighten', 0.2, 0.92);
|
|
1397
|
-
const fillDarkPair = adjustFillForOnColor(fillDarkBase, darkBgCss, 3, 45, 'lighten', 0.2, 0.96);
|
|
1398
|
-
const fillLight = fillLightPair.fill;
|
|
1399
|
-
const fillDark = fillDarkPair.fill;
|
|
1400
|
-
light[`--dry-color-fill-${tone}`] = hsl(fillLight.h, fillLight.s, fillLight.l);
|
|
1401
|
-
dark[`--dry-color-fill-${tone}`] = hsl(fillDark.h, fillDark.s, fillDark.l);
|
|
1402
|
-
// text-{tone}: meet both WCAG and APCA on the surrounding surface.
|
|
1403
|
-
const textLight = adjustCssColorForReadability(hue, fillLight.s, fillLight.l, '#ffffff', 4.5, 60, 'darken', 0.1, 0.8);
|
|
1404
|
-
light[`--dry-color-text-${tone}`] = hsl(textLight.h, textLight.s, textLight.l);
|
|
1405
|
-
const textDark = adjustCssColorForReadability(hue, fillDark.s, fillDark.l, darkBgCss, 4.5, 60, 'lighten', 0.2, 0.92);
|
|
1406
|
-
dark[`--dry-color-text-${tone}`] = hsl(textDark.h, textDark.s, textDark.l);
|
|
1407
|
-
// fill-{tone}-hover: 8% darker light, 7% darker dark
|
|
1408
|
-
light[`--dry-color-fill-${tone}-hover`] = hsl(fillLight.h, fillLight.s, clamp(fillLight.l - 0.08, 0, 1));
|
|
1409
|
-
dark[`--dry-color-fill-${tone}-hover`] = hsl(fillDark.h, fillDark.s, clamp(fillDark.l - 0.07, 0, 1));
|
|
1410
|
-
// fill-{tone}-weak: hsla with 0.10/0.15 alpha
|
|
1411
|
-
light[`--dry-color-fill-${tone}-weak`] = hsla(fillLight.h, fillLight.s, fillLight.l, 0.1);
|
|
1412
|
-
dark[`--dry-color-fill-${tone}-weak`] = hsla(fillDark.h, fillDark.s, fillDark.l, 0.15);
|
|
1413
|
-
// stroke-{tone}: use a readable outline colour close to the tone family.
|
|
1414
|
-
const strokeLight = adjustCssColorForReadability(hue, 0.5, 0.7, '#ffffff', 3, 30, 'darken', 0.18, 0.8);
|
|
1415
|
-
const strokeDark = adjustCssColorForReadability(hue, 0.45, 0.55, darkBgCss, 3, 30, 'lighten', 0.2, 0.92);
|
|
1416
|
-
light[`--dry-color-stroke-${tone}`] = hsl(strokeLight.h, strokeLight.s, strokeLight.l);
|
|
1417
|
-
dark[`--dry-color-stroke-${tone}`] = hsl(strokeDark.h, strokeDark.s, strokeDark.l);
|
|
1418
|
-
const lightToneStrokeStrong = hsla(fillLight.h, fillLight.s, fillLight.l, 0.8);
|
|
1419
|
-
const darkToneStrokeStrong = hsla(fillDark.h, fillDark.s, fillDark.l, 0.8);
|
|
1420
|
-
light[`--dry-color-stroke-${tone}-strong`] = lightToneStrokeStrong;
|
|
1421
|
-
dark[`--dry-color-stroke-${tone}-strong`] = darkToneStrokeStrong;
|
|
1422
|
-
light[`--dry-color-icon-${tone}`] = lightToneStrokeStrong;
|
|
1423
|
-
dark[`--dry-color-icon-${tone}`] = darkToneStrokeStrong;
|
|
1424
|
-
// on-{tone}: white if contrast >= 4.5 vs fill, else dark tint
|
|
1425
|
-
light[`--dry-color-on-${tone}`] = fillLightPair.onColor;
|
|
1426
|
-
dark[`--dry-color-on-${tone}`] = fillDarkPair.onColor;
|
|
1427
|
-
}
|
|
1428
|
-
// ── Shadows (2 tokens) ───────────────────────────────────────────────────
|
|
1429
|
-
// Full box-shadow shorthand with brand-hue-tinted colors.
|
|
1430
|
-
// Light: subtle alpha. Dark: stronger alpha.
|
|
1431
|
-
light['--dry-shadow-raised'] =
|
|
1432
|
-
`0 1px 3px hsla(${Math.round(H)}, 20%, 20%, 0.08), 0 1px 2px hsla(${Math.round(H)}, 20%, 20%, 0.06)`;
|
|
1433
|
-
light['--dry-shadow-overlay'] =
|
|
1434
|
-
`0 8px 24px hsla(${Math.round(H)}, 20%, 20%, 0.12), 0 2px 8px hsla(${Math.round(H)}, 20%, 20%, 0.08)`;
|
|
1435
|
-
dark['--dry-shadow-raised'] =
|
|
1436
|
-
`0 1px 3px hsla(${Math.round(H)}, 30%, 5%, 0.4), 0 1px 2px hsla(${Math.round(H)}, 30%, 5%, 0.3)`;
|
|
1437
|
-
dark['--dry-shadow-overlay'] =
|
|
1438
|
-
`0 8px 24px hsla(${Math.round(H)}, 30%, 5%, 0.5), 0 2px 8px hsla(${Math.round(H)}, 30%, 5%, 0.4)`;
|
|
1439
|
-
// ── Overlay Backdrops (2 tokens) ─────────────────────────────────────────
|
|
1440
|
-
light['--dry-color-overlay-backdrop'] = 'hsla(0, 0%, 0%, 0.4)';
|
|
1441
|
-
light['--dry-color-overlay-backdrop-strong'] = 'hsla(0, 0%, 0%, 0.6)';
|
|
1442
|
-
dark['--dry-color-overlay-backdrop'] = 'hsla(0, 0%, 0%, 0.6)';
|
|
1443
|
-
dark['--dry-color-overlay-backdrop-strong'] = 'hsla(0, 0%, 0%, 0.75)';
|
|
1444
|
-
return { light, dark };
|
|
1445
|
-
}
|