@pipelex/mthds-form 0.30.0 → 0.32.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.
@@ -0,0 +1,693 @@
1
+ import { z } from 'zod';
2
+
3
+ // src/brand/contract.ts
4
+ var COLOR_TOKEN_NAMES = [
5
+ "background",
6
+ "foreground",
7
+ "card",
8
+ "card-foreground",
9
+ "popover",
10
+ "popover-foreground",
11
+ "primary",
12
+ "primary-foreground",
13
+ "secondary",
14
+ "secondary-foreground",
15
+ "muted",
16
+ "muted-foreground",
17
+ "accent",
18
+ "accent-foreground",
19
+ "destructive",
20
+ "destructive-foreground",
21
+ "border",
22
+ "input",
23
+ "ring"
24
+ ];
25
+ function isColorTokenName(name) {
26
+ return COLOR_TOKEN_NAMES.includes(name);
27
+ }
28
+ var COLOR_PAINTS = {
29
+ background: "The page canvas.",
30
+ foreground: "Text on the canvas.",
31
+ card: "A raised surface on the canvas: the rail's panel, a list's rows. May be translucent.",
32
+ "card-foreground": "Text on a card.",
33
+ popover: "A floating surface: a select's menu, a tooltip. Opaque.",
34
+ "popover-foreground": "Text on a popover.",
35
+ primary: "The accent: the run button, a selected pill, a section number, a highlight.",
36
+ "primary-foreground": "Text on the accent.",
37
+ secondary: "A quiet filled surface: a secondary button, an unselected segment.",
38
+ "secondary-foreground": "Text on a secondary surface.",
39
+ muted: "A subdued fill: a table's header band, an inactive tab strip.",
40
+ "muted-foreground": "Secondary text: descriptions, hints, a summary row's label. Most of the text on the page that is not a heading or a value.",
41
+ accent: "A hover fill: a menu item under the pointer.",
42
+ "accent-foreground": "Text on an accent fill.",
43
+ destructive: "The error colour: an invalid mark, a remove button.",
44
+ "destructive-foreground": "Text on the destructive colour.",
45
+ border: "The hairline: every border and divider.",
46
+ input: "The control surface, a field's own fill - a step off the canvas, so fields read as one family.",
47
+ ring: "The focus ring."
48
+ };
49
+ var BRAND_CONTRACT = [
50
+ ...COLOR_TOKEN_NAMES.map((name) => ({
51
+ id: `color.${name}`,
52
+ variable: `--${name}`,
53
+ type: "color",
54
+ paints: COLOR_PAINTS[name]
55
+ })),
56
+ {
57
+ id: "radius.base",
58
+ variable: "--radius",
59
+ type: "dimension",
60
+ paints: "The corner radius of a control; cards take one and a half of it."
61
+ },
62
+ {
63
+ id: "font.sans",
64
+ variable: "--font-sans",
65
+ type: "fontFamily",
66
+ paints: "The typeface of everything on the page."
67
+ },
68
+ {
69
+ id: "font.mono",
70
+ variable: "--font-mono",
71
+ type: "fontFamily",
72
+ paints: "Tags, numbers, the receipt."
73
+ }
74
+ ];
75
+ var MIN_CONTRAST = 4.5;
76
+ var MIN_CONTROL_CONTRAST = 3;
77
+ var CONTRAST_PAIRS = [
78
+ { foreground: "foreground", background: "background", minimum: MIN_CONTRAST },
79
+ { foreground: "muted-foreground", background: "background", minimum: MIN_CONTRAST },
80
+ {
81
+ foreground: "muted-foreground",
82
+ background: "input",
83
+ over: "background",
84
+ minimum: MIN_CONTROL_CONTRAST
85
+ },
86
+ { foreground: "destructive", background: "background", minimum: MIN_CONTROL_CONTRAST },
87
+ {
88
+ foreground: "destructive",
89
+ background: "card",
90
+ over: "background",
91
+ minimum: MIN_CONTROL_CONTRAST
92
+ }
93
+ ];
94
+ var byId = new Map(BRAND_CONTRACT.map((token) => [token.id, token]));
95
+ function contractVariable(id) {
96
+ const token = byId.get(id);
97
+ if (!token) throw new Error(`The brand contract has no token ${id}.`);
98
+ return token.variable;
99
+ }
100
+ var component = z.number().min(0).max(1);
101
+ var srgbColorSchema = z.strictObject({
102
+ colorSpace: z.literal("srgb"),
103
+ components: z.tuple([component, component, component]),
104
+ alpha: z.number().min(0).max(1),
105
+ hex: z.string().regex(/^#[0-9a-f]{6}$/i, "a hex colour is #rrggbb").optional()
106
+ });
107
+ var colorAliasSchema = z.string().regex(/^\{color\.[a-z][a-z-]*\}$/, 'an alias is "{color.<name>}"');
108
+ var colorValueSchema = z.union([srgbColorSchema, colorAliasSchema]);
109
+ var description = z.string().min(1, "every token carries a $description");
110
+ function isControlCharacter(char) {
111
+ const code = char.codePointAt(0);
112
+ return code < 32 || code === 127;
113
+ }
114
+ var colorTokenSchema = z.strictObject({
115
+ $value: colorValueSchema,
116
+ $description: description,
117
+ $extensions: z.strictObject({
118
+ mode: z.strictObject({ dark: colorValueSchema })
119
+ }).optional()
120
+ }).refine((token) => token.$extensions !== void 0 || typeof token.$value === "string", {
121
+ message: "a colour carries $extensions.mode.dark; only an alias may stand for both modes",
122
+ path: ["$extensions"]
123
+ });
124
+ var dimensionTokenSchema = z.strictObject({
125
+ $value: z.strictObject({ value: z.number().min(0), unit: z.enum(["rem", "px"]) }),
126
+ $description: description
127
+ });
128
+ var fontFamilyTokenSchema = z.strictObject({
129
+ $value: z.array(
130
+ z.string().min(1).refine((name) => ![...name].some(isControlCharacter), {
131
+ message: "a family name holds no control characters"
132
+ })
133
+ ).min(1),
134
+ $description: description
135
+ });
136
+ var brandTokensSchema = z.strictObject({
137
+ color: z.strictObject({
138
+ $type: z.literal("color"),
139
+ ...Object.fromEntries(COLOR_TOKEN_NAMES.map((name) => [name, colorTokenSchema]))
140
+ }),
141
+ radius: z.strictObject({ $type: z.literal("dimension"), base: dimensionTokenSchema }),
142
+ font: z.strictObject({
143
+ $type: z.literal("fontFamily"),
144
+ sans: fontFamilyTokenSchema,
145
+ mono: fontFamilyTokenSchema
146
+ })
147
+ });
148
+ function aliasTarget(value) {
149
+ return typeof value === "string" ? value.slice("{color.".length, -1) : null;
150
+ }
151
+ function darkValue(token) {
152
+ return token.$extensions?.mode.dark ?? token.$value;
153
+ }
154
+ function modeValue(token, mode) {
155
+ return mode === "light" ? token.$value : darkValue(token);
156
+ }
157
+ function colorHex(color) {
158
+ return `#${color.components.map(
159
+ (channel) => Math.round(channel * 255).toString(16).padStart(2, "0")
160
+ ).join("")}`;
161
+ }
162
+ function colorIsHex(color, hex) {
163
+ return colorHex(color) === hex.toLowerCase();
164
+ }
165
+ function asWritten(color) {
166
+ const alpha = Number(Math.min(color.alpha, 1).toFixed(3));
167
+ return {
168
+ colorSpace: "srgb",
169
+ components: color.components.map((channel) => Math.round(channel * 255) / 255),
170
+ alpha
171
+ };
172
+ }
173
+ function checkHexAgreement(name, mode, value, problems) {
174
+ if (typeof value === "string" || !value.hex) return;
175
+ if (!colorIsHex(value, value.hex)) {
176
+ problems.push(
177
+ `color.${name} (${mode}): hex ${value.hex} does not agree with components [${value.components.join(", ")}]`
178
+ );
179
+ }
180
+ }
181
+ function checkAliases(tokens, mode, problems) {
182
+ for (const name of COLOR_TOKEN_NAMES) {
183
+ const seen = /* @__PURE__ */ new Set([name]);
184
+ let current = name;
185
+ for (; ; ) {
186
+ const target = aliasTarget(modeValue(tokens.color[current], mode));
187
+ if (target === null) break;
188
+ if (!isColorTokenName(target)) {
189
+ problems.push(
190
+ `color.${name} (${mode}): aliases {color.${target}}, which the contract has no token for`
191
+ );
192
+ break;
193
+ }
194
+ if (seen.has(target)) {
195
+ problems.push(`color.${name} (${mode}): alias cycle through {color.${target}}`);
196
+ break;
197
+ }
198
+ seen.add(target);
199
+ current = target;
200
+ }
201
+ }
202
+ }
203
+ function resolveColor(tokens, name, mode) {
204
+ const seen = /* @__PURE__ */ new Set();
205
+ let current = name;
206
+ for (; ; ) {
207
+ if (seen.has(current)) return null;
208
+ seen.add(current);
209
+ const value = modeValue(tokens.color[current], mode);
210
+ const target = aliasTarget(value);
211
+ if (target === null) return value;
212
+ if (!isColorTokenName(target)) return null;
213
+ current = target;
214
+ }
215
+ }
216
+ function linearChannel(channel) {
217
+ return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
218
+ }
219
+ function luminance(color) {
220
+ const [r, g, b] = color.components.map(linearChannel);
221
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
222
+ }
223
+ function contrastRatio(a, b) {
224
+ const la = luminance(a);
225
+ const lb = luminance(b);
226
+ const [light, dark] = la > lb ? [la, lb] : [lb, la];
227
+ return (light + 0.05) / (dark + 0.05);
228
+ }
229
+ function composite(color, over) {
230
+ const components = color.components.map(
231
+ (channel, index) => channel * color.alpha + over.components[index] * (1 - color.alpha)
232
+ );
233
+ return { colorSpace: "srgb", components, alpha: 1 };
234
+ }
235
+ function checkContrast(tokens, mode, problems) {
236
+ for (const pair of CONTRAST_PAIRS) {
237
+ const canvasName = pair.over ?? pair.background;
238
+ const foreground = resolveColor(tokens, pair.foreground, mode);
239
+ const background = resolveColor(tokens, pair.background, mode);
240
+ const canvasColor = resolveColor(tokens, canvasName, mode);
241
+ if (!foreground || !background || !canvasColor) continue;
242
+ if (canvasColor.alpha < 1) {
243
+ const problem = `color.${canvasName} (${mode}): alpha ${canvasColor.alpha}, but text is measured against it, so it must be opaque (alpha 1)`;
244
+ if (!problems.includes(problem)) problems.push(problem);
245
+ continue;
246
+ }
247
+ const canvas = asWritten(canvasColor);
248
+ const surface = pair.over ? composite(asWritten(background), canvas) : canvas;
249
+ const ratio = contrastRatio(composite(asWritten(foreground), surface), surface);
250
+ if (ratio < pair.minimum) {
251
+ const on = pair.over ? `color.${pair.background} over color.${pair.over}` : `color.${pair.background}`;
252
+ const level = pair.minimum >= MIN_CONTRAST ? "WCAG AA" : "WCAG AA for a control";
253
+ problems.push(
254
+ `color.${pair.foreground} on ${on} (${mode}): contrast ${ratio.toFixed(2)}, expected ${pair.minimum} (${level})`
255
+ );
256
+ }
257
+ }
258
+ }
259
+ function validateBrandTokens(doc) {
260
+ const parsed = brandTokensSchema.safeParse(doc);
261
+ if (!parsed.success) {
262
+ return {
263
+ ok: false,
264
+ problems: parsed.error.issues.map((issue) => {
265
+ const where = issue.path.map(String).join(".") || "(root)";
266
+ return `${where}: ${issue.message}`;
267
+ })
268
+ };
269
+ }
270
+ const tokens = parsed.data;
271
+ const problems = [];
272
+ for (const name of COLOR_TOKEN_NAMES) {
273
+ const token = tokens.color[name];
274
+ checkHexAgreement(name, "light", token.$value, problems);
275
+ checkHexAgreement(name, "dark", darkValue(token), problems);
276
+ }
277
+ checkAliases(tokens, "light", problems);
278
+ checkAliases(tokens, "dark", problems);
279
+ if (problems.length === 0) {
280
+ checkContrast(tokens, "light", problems);
281
+ checkContrast(tokens, "dark", problems);
282
+ }
283
+ return problems.length === 0 ? { ok: true, tokens } : { ok: false, problems };
284
+ }
285
+
286
+ // src/brand/seed-roles.ts
287
+ var ACCENT_FILL_STRENGTH = 0.12;
288
+ var SEARCH_STEPS = 32;
289
+ function deriveBrandRoles(seeds, page, surfaces = []) {
290
+ const canvas = parseHex(page, "The page colour");
291
+ const extreme = textOn(canvas) === "#000000" ? BLACK : WHITE;
292
+ const others = surfaces.map((surface, index) => parseHex(surface, `Surface ${index + 1}`));
293
+ const action = seeds?.action == null ? null : parseHex(seeds.action, "The action seed");
294
+ const accent = seeds?.accent == null ? action : parseHex(seeds.accent, "The accent seed");
295
+ const header = seeds?.header == null ? null : parseHex(seeds.header, "The header seed");
296
+ const actionRoles = action === null ? {} : { action: colorHex(action), onAction: textOn(action) };
297
+ const accentRoles = accent === null ? {} : accentRolesOf(accent, canvas, extreme, others);
298
+ const headerRoles = header === null ? {} : { header: colorHex(header), onHeader: textOn(header) };
299
+ return { ...actionRoles, ...accentRoles, ...headerRoles };
300
+ }
301
+ function accentRolesOf(accent, canvas, extreme, surfaces) {
302
+ const accentFill = paleFill(accent, canvas, extreme);
303
+ const backgrounds = [canvas, accentFill, ...surfaces];
304
+ const accentInk = inkFor(accent, backgrounds, extreme);
305
+ if (accentInk === void 0) {
306
+ throw new Error(
307
+ `No accent ink can read on every surface at once: no shade of ${colorHex(accent)} reaches ${MIN_CONTRAST}:1 on the page ${colorHex(canvas)}, the pale fill ${colorHex(accentFill)} and ${surfaces.map(colorHex).join(", ")}.`
308
+ );
309
+ }
310
+ return {
311
+ accent: colorHex(accent),
312
+ accentFill: colorHex(accentFill),
313
+ accentInk: colorHex(accentInk)
314
+ };
315
+ }
316
+ function isActionFaint(seeds, page) {
317
+ const canvas = parseHex(page, "The page colour");
318
+ if (seeds?.action == null) return false;
319
+ return contrastRatio(parseHex(seeds.action, "The action seed"), canvas) < MIN_CONTROL_CONTRAST;
320
+ }
321
+ function isAccentAdjusted(seeds, page, surfaces = []) {
322
+ const roles = deriveBrandRoles(seeds, page, surfaces);
323
+ return roles.accentInk !== void 0 && roles.accentInk !== roles.accent;
324
+ }
325
+ function resolveDarkSeeds(colors, darkColors) {
326
+ return {
327
+ action: darkColors?.action ?? colors?.action ?? null,
328
+ accent: darkColors?.accent ?? colors?.accent ?? null,
329
+ header: darkColors?.header ?? colors?.header ?? null
330
+ };
331
+ }
332
+ function readableTextOn(fill) {
333
+ return textOn(parseHex(fill, "The fill"));
334
+ }
335
+ var BLACK = { components: [0, 0, 0]};
336
+ var WHITE = { components: [1, 1, 1]};
337
+ function textOn(fill) {
338
+ return contrastRatio(BLACK, fill) >= contrastRatio(WHITE, fill) ? "#000000" : "#ffffff";
339
+ }
340
+ function parseHex(value, what) {
341
+ if (typeof value !== "string" || !/^#[0-9a-f]{6}$/i.test(value)) {
342
+ throw new Error(`${what} must be a #rrggbb colour, not ${JSON.stringify(value)}.`);
343
+ }
344
+ const channel = (at) => parseInt(value.slice(at, at + 2), 16) / 255;
345
+ return { colorSpace: "srgb", components: [channel(1), channel(3), channel(5)], alpha: 1 };
346
+ }
347
+ function snapped(components) {
348
+ const [r, g, b] = components.map(
349
+ (channel) => Math.round(Math.min(1, Math.max(0, channel)) * 255) / 255
350
+ );
351
+ return { colorSpace: "srgb", components: [r, g, b], alpha: 1 };
352
+ }
353
+ function paleFill(accent, page, extreme) {
354
+ const at = (strength) => snapped(
355
+ page.components.map(
356
+ (channel, index) => channel + (accent.components[index] - channel) * strength
357
+ )
358
+ );
359
+ const nominal = at(ACCENT_FILL_STRENGTH);
360
+ if (contrastRatio(extreme, nominal) >= MIN_CONTRAST) return nominal;
361
+ let enough = 0;
362
+ let short = ACCENT_FILL_STRENGTH;
363
+ for (let step = 0; step < SEARCH_STEPS; step++) {
364
+ const middle = (enough + short) / 2;
365
+ if (contrastRatio(extreme, at(middle)) >= MIN_CONTRAST) enough = middle;
366
+ else short = middle;
367
+ }
368
+ return at(enough);
369
+ }
370
+ function shadeLightnesses(hue, saturation) {
371
+ const bounds = /* @__PURE__ */ new Set([0, 0.5, 1]);
372
+ for (const [low, high] of [
373
+ [0, 0.5],
374
+ [0.5, 1]
375
+ ]) {
376
+ const from = fromHsl(hue, saturation, low);
377
+ const to = fromHsl(hue, saturation, high);
378
+ for (let channel = 0; channel < 3; channel++) {
379
+ const start = from[channel] * 255;
380
+ const rise = to[channel] * 255 - start;
381
+ if (rise === 0) continue;
382
+ for (let byte = 0; byte < 255; byte++) {
383
+ const at = low + (byte + 0.5 - start) / rise * (high - low);
384
+ if (at > low && at < high) bounds.add(at);
385
+ }
386
+ }
387
+ }
388
+ const sorted = [...bounds].sort((a, b) => a - b);
389
+ return sorted.slice(1).map((bound, index) => (sorted[index] + bound) / 2);
390
+ }
391
+ function inkFor(accent, backgrounds, extreme) {
392
+ const reads = (color) => backgrounds.every((background) => contrastRatio(color, background) >= MIN_CONTRAST);
393
+ if (reads(accent)) return accent;
394
+ const [hue, saturation, lightness] = toHsl(accent.components);
395
+ const at = (value) => snapped(fromHsl(hue, saturation, value));
396
+ const narrowed = (short, enough) => {
397
+ for (let step = 0; step < SEARCH_STEPS; step++) {
398
+ const middle = (short + enough) / 2;
399
+ if (reads(at(middle))) enough = middle;
400
+ else short = middle;
401
+ }
402
+ return at(enough);
403
+ };
404
+ const toward = extreme === BLACK ? 0 : 1;
405
+ if (reads(extreme)) return narrowed(lightness, toward);
406
+ const shades = shadeLightnesses(hue, saturation);
407
+ const darker = shades.filter((value) => value < lightness).reverse();
408
+ const lighter = shades.filter((value) => value > lightness);
409
+ for (const side of toward === 0 ? [darker, lighter] : [lighter, darker]) {
410
+ for (const value of side) {
411
+ const shade = at(value);
412
+ if (reads(shade)) return shade;
413
+ }
414
+ }
415
+ return void 0;
416
+ }
417
+ function toHsl(components) {
418
+ const [r, g, b] = components;
419
+ const max = Math.max(r, g, b);
420
+ const min = Math.min(r, g, b);
421
+ const lightness = (max + min) / 2;
422
+ const chroma = max - min;
423
+ if (chroma === 0) return [0, 0, lightness];
424
+ const saturation = chroma / (1 - Math.abs(2 * lightness - 1));
425
+ const sector = max === r ? ((g - b) / chroma + 6) % 6 : max === g ? (b - r) / chroma + 2 : (r - g) / chroma + 4;
426
+ return [sector * 60, Math.min(1, saturation), lightness];
427
+ }
428
+ function fromHsl(hue, saturation, lightness) {
429
+ const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;
430
+ const sector = hue / 60;
431
+ const second = chroma * (1 - Math.abs(sector % 2 - 1));
432
+ const [r, g, b] = sector < 1 ? [chroma, second, 0] : sector < 2 ? [second, chroma, 0] : sector < 3 ? [0, chroma, second] : sector < 4 ? [0, second, chroma] : sector < 5 ? [second, 0, chroma] : [chroma, 0, second];
433
+ const offset = lightness - chroma / 2;
434
+ return [r + offset, g + offset, b + offset];
435
+ }
436
+
437
+ // src/brand/styles.ts
438
+ var SANS_FALLBACK = 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif';
439
+ var SERIF_FALLBACK = 'ui-serif, Georgia, Cambria, "Times New Roman", serif';
440
+ var WEIGHTS = [300, 400, 500, 600, 700];
441
+ function face(id, family, category, use) {
442
+ const fallback = category === "serif" ? SERIF_FALLBACK : SANS_FALLBACK;
443
+ return {
444
+ id,
445
+ family,
446
+ category,
447
+ weights: WEIGHTS,
448
+ fallback,
449
+ stack: `"${family}", ${fallback}`,
450
+ use
451
+ };
452
+ }
453
+ var TYPEFACES = deepFreeze([
454
+ face("inter", "Inter", "sans-serif", "body"),
455
+ face("dm-sans", "DM Sans", "sans-serif", "body"),
456
+ face("plus-jakarta-sans", "Plus Jakarta Sans", "sans-serif", "body"),
457
+ face("manrope", "Manrope", "sans-serif", "body"),
458
+ face("ibm-plex-sans", "IBM Plex Sans", "sans-serif", "body"),
459
+ face("source-sans-3", "Source Sans 3", "sans-serif", "body"),
460
+ face("work-sans", "Work Sans", "sans-serif", "body"),
461
+ face("space-grotesk", "Space Grotesk", "sans-serif", "body"),
462
+ face("newsreader", "Newsreader", "serif", "heading")
463
+ ]);
464
+ var FACES = new Map(TYPEFACES.map((candidate) => [candidate.id, candidate]));
465
+ function isTypeface(value) {
466
+ return typeof value === "string" && FACES.get(value)?.use === "body";
467
+ }
468
+ function faceOf(id) {
469
+ return FACES.get(id);
470
+ }
471
+ var role = (name, more = {}) => ({
472
+ role: name,
473
+ ...more
474
+ });
475
+ var DEFINITIONS = {
476
+ // The host's own look, named: it sets nothing, and a team's typeface is its
477
+ // body and its headings.
478
+ quiet: {
479
+ name: "Quiet",
480
+ direction: "The host\u2019s own look, unstyled: its light titles and hairline panels, generous air, nothing decorated.",
481
+ bodyFace: null,
482
+ headingFace: "body",
483
+ roles: {}
484
+ },
485
+ soft: {
486
+ name: "Soft",
487
+ direction: "Friendly and rounded: generous corners, panels that float on a soft shadow with no outline, warm medium-weight titles.",
488
+ bodyFace: "dm-sans",
489
+ headingFace: "body",
490
+ roles: {
491
+ headingWeight: 500,
492
+ headingTracking: "-0.015em",
493
+ radius: 12,
494
+ panel: {
495
+ light: {
496
+ fill: role("card"),
497
+ edge: null,
498
+ edgeWidth: 1,
499
+ shadow: "0 1px 2px rgb(16 24 40 / 0.05), 0 12px 32px -10px rgb(16 24 40 / 0.16)",
500
+ field: role("card")
501
+ },
502
+ // On a dark page a shadow barely shows, so the panel is lifted by its
503
+ // fill instead, the card a little toward the foreground, with a faint
504
+ // highlight along its top edge where light would catch it, and its
505
+ // fields sink back to the page's own background.
506
+ dark: {
507
+ fill: role("card", { toward: { role: "foreground", share: 0.07 } }),
508
+ edge: null,
509
+ edgeWidth: 1,
510
+ shadow: "inset 0 1px 0 rgb(255 255 255 / 0.07), 0 2px 4px rgb(0 0 0 / 0.55), 0 24px 56px -12px rgb(0 0 0 / 0.95)",
511
+ field: role("background")
512
+ }
513
+ },
514
+ // A short shadow in the button colour on a light page, kept within the
515
+ // gap a host leaves under its button. On a dark page the panel is lifted
516
+ // by its fill, and a glow in a light button colour reads as a smear.
517
+ actionShadow: {
518
+ light: { x: 0, y: 6, blur: 14, spread: -6, color: role("primary", { alpha: 0.5 }) },
519
+ dark: "none"
520
+ },
521
+ density: 1.08
522
+ }
523
+ },
524
+ editorial: {
525
+ name: "Editorial",
526
+ direction: "Considered and literary: serif display titles, fine hairlines, wide margins and a reading rhythm.",
527
+ bodyFace: "source-sans-3",
528
+ headingFace: "newsreader",
529
+ roles: {
530
+ headingWeight: 500,
531
+ headingTracking: "-0.01em",
532
+ radius: 2,
533
+ // Hairlines all the way in: the fields inside the panel take its own
534
+ // fill, so each reads as its fine edge rather than as a grey block.
535
+ panel: {
536
+ light: {
537
+ fill: role("card"),
538
+ edge: role("border"),
539
+ edgeWidth: 1,
540
+ shadow: null,
541
+ field: role("card")
542
+ },
543
+ dark: {
544
+ fill: role("card"),
545
+ edge: role("border"),
546
+ edgeWidth: 1,
547
+ shadow: null,
548
+ field: role("card")
549
+ }
550
+ },
551
+ actionShadow: { light: "none", dark: "none" },
552
+ density: 1.25
553
+ }
554
+ },
555
+ bold: {
556
+ name: "Bold",
557
+ direction: "Confident and graphic: heavy, tightly set titles, solid panels with firm edges, a compact rhythm.",
558
+ bodyFace: "space-grotesk",
559
+ headingFace: "body",
560
+ roles: {
561
+ headingWeight: 700,
562
+ headingTracking: "-0.03em",
563
+ radius: 6,
564
+ // One firm edge around a solid panel, softer on a dark page. The panel
565
+ // is the muted fill, which the host's fields all but match, so fields
566
+ // set into it take the card on a light page and the page's background
567
+ // on a dark one.
568
+ panel: {
569
+ light: {
570
+ fill: role("muted"),
571
+ edge: role("foreground", { alpha: 0.62 }),
572
+ edgeWidth: 2,
573
+ shadow: null,
574
+ field: role("card")
575
+ },
576
+ dark: {
577
+ fill: role("muted"),
578
+ edge: role("foreground", { alpha: 0.3 }),
579
+ edgeWidth: 2,
580
+ shadow: null,
581
+ field: role("background")
582
+ }
583
+ },
584
+ actionShadow: { light: "none", dark: "none" },
585
+ density: 0.85
586
+ }
587
+ }
588
+ };
589
+ var APP_STYLE_IDS = Object.freeze([
590
+ "quiet",
591
+ "soft",
592
+ "editorial",
593
+ "bold"
594
+ ]);
595
+ function isAppStyle(value) {
596
+ return typeof value === "string" && Object.prototype.hasOwnProperty.call(DEFINITIONS, value);
597
+ }
598
+ function deriveStyleRoles(style, typeface) {
599
+ const definition = DEFINITIONS[isAppStyle(style) ? style : "quiet"];
600
+ const roles = {};
601
+ const body = isTypeface(typeface) ? faceOf(typeface) : definition.bodyFace === null ? void 0 : faceOf(definition.bodyFace);
602
+ if (body) roles.bodyFace = body;
603
+ const heading = definition.headingFace === "body" ? body : faceOf(definition.headingFace);
604
+ if (heading) roles.headingFace = heading;
605
+ return copyData({ ...roles, ...definition.roles });
606
+ }
607
+ var APP_STYLES = deepFreeze(
608
+ Object.fromEntries(
609
+ APP_STYLE_IDS.map((id) => [
610
+ id,
611
+ {
612
+ id,
613
+ name: DEFINITIONS[id].name,
614
+ direction: DEFINITIONS[id].direction,
615
+ roles: deriveStyleRoles(id)
616
+ }
617
+ ])
618
+ )
619
+ );
620
+ function copyData(value) {
621
+ if (Array.isArray(value)) return value.map(copyData);
622
+ if (value !== null && typeof value === "object") {
623
+ return Object.fromEntries(
624
+ Object.entries(value).map(([key, inner]) => [key, copyData(inner)])
625
+ );
626
+ }
627
+ return value;
628
+ }
629
+ function deepFreeze(value) {
630
+ if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
631
+ for (const inner of Object.values(value)) deepFreeze(inner);
632
+ Object.freeze(value);
633
+ }
634
+ return value;
635
+ }
636
+ function resolveRoleColor(color, palette, behind) {
637
+ const of = (name) => {
638
+ const value = palette[name];
639
+ if (!isColorTokenName(name) || value === void 0) {
640
+ throw new Error(`The palette has no ${name} to resolve a style's colour against.`);
641
+ }
642
+ return parseHex(value, `The palette's ${name}`);
643
+ };
644
+ let rgb = of(color.role).components;
645
+ if (color.toward) {
646
+ const share = clampUnit(color.toward.share);
647
+ rgb = mixOklab(rgb, of(color.toward.role).components, share);
648
+ }
649
+ if (color.alpha !== void 0 && color.alpha < 1) {
650
+ if (behind === void 0) {
651
+ throw new Error(`A translucent ${color.role} has no one colour: say what it lies over.`);
652
+ }
653
+ const under = parseHex(behind, "The colour behind").components;
654
+ const alpha = clampUnit(color.alpha);
655
+ rgb = rgb.map((channel, index) => under[index] + (channel - under[index]) * alpha);
656
+ }
657
+ return colorHex({ components: rgb});
658
+ }
659
+ function clampUnit(value) {
660
+ return Math.min(1, Math.max(0, value));
661
+ }
662
+ function mixOklab(a, b, share) {
663
+ const [l1, a1, b1] = toOklab(a);
664
+ const [l2, a2, b2] = toOklab(b);
665
+ const at = (from, to) => from + (to - from) * share;
666
+ return fromOklab([at(l1, l2), at(a1, a2), at(b1, b2)]).map(clampUnit);
667
+ }
668
+ var fromLinear = (c) => c <= 31308e-7 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055;
669
+ function toOklab(rgb) {
670
+ const [r, g, b] = rgb.map(linearChannel);
671
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
672
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
673
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
674
+ return [
675
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
676
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
677
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
678
+ ];
679
+ }
680
+ function fromOklab([lightness, a, b]) {
681
+ const l = (lightness + 0.3963377774 * a + 0.2158037573 * b) ** 3;
682
+ const m = (lightness - 0.1055613458 * a - 0.0638541728 * b) ** 3;
683
+ const s = (lightness - 0.0894841775 * a - 1.291485548 * b) ** 3;
684
+ return [
685
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
686
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
687
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
688
+ ].map((channel) => fromLinear(clampUnit(channel)));
689
+ }
690
+
691
+ export { APP_STYLES, APP_STYLE_IDS, BRAND_CONTRACT, COLOR_TOKEN_NAMES, CONTRAST_PAIRS, MIN_CONTRAST, MIN_CONTROL_CONTRAST, TYPEFACES, aliasTarget, asWritten, colorHex, colorIsHex, contractVariable, contrastRatio, deriveBrandRoles, deriveStyleRoles, isAccentAdjusted, isActionFaint, isAppStyle, isColorTokenName, isControlCharacter, isTypeface, modeValue, readableTextOn, resolveColor, resolveDarkSeeds, resolveRoleColor, srgbColorSchema, validateBrandTokens };
692
+ //# sourceMappingURL=chunk-3GO65HWP.js.map
693
+ //# sourceMappingURL=chunk-3GO65HWP.js.map