@pipelex/mthds-form 0.29.0 → 0.31.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.
@@ -1,54 +1,6 @@
1
+ import { C as ColorTokenName, B as BrandManifest } from '../styles-BoXPBjF4.js';
2
+ export { A as APP_STYLES, a as APP_STYLE_IDS, b as ActionShadow, c as AppStyle, d as AppStyleId, e as BRAND_CONTRACT, f as BodyTypefaceId, g as COLOR_TOKEN_NAMES, h as CONTRAST_PAIRS, i as ContractToken, j as ContractTokenType, k as ContrastPair, M as MIN_CONTRAST, l as MIN_CONTROL_CONTRAST, P as PanelRoles, R as RoleColor, S as StyleRoles, T as TYPEFACES, m as Typeface, n as TypefaceId, o as brandManifestSchema, p as contractVariable, q as deriveStyleRoles, r as isAppStyle, s as isColorTokenName, t as isTypeface, u as resolveRoleColor } from '../styles-BoXPBjF4.js';
1
3
  import { z } from 'zod';
2
- import { B as BrandManifest } from '../manifest-DUeAWeqa.js';
3
- export { b as brandManifestSchema } from '../manifest-DUeAWeqa.js';
4
-
5
- /**
6
- * The brand contract, as data: which token of a brand's `tokens.json` sets
7
- * which custom property, and what that property paints.
8
- *
9
- * It is the theme contract seen from the other side. `src/styles/theme.css`
10
- * states it as a working default and `docs/theming.md` in prose; this is the
11
- * same list as a table a build can read, and a brand is exactly these tokens
12
- * and no others - the validator refuses a file that adds one, because a token
13
- * nothing consumes is a value with no way to be looked at.
14
- *
15
- * The colour and radius rows are not a second list kept beside `theme.css`:
16
- * they are held to it, by `__tests__/contract.test.ts` over the source and by
17
- * `scripts/assert-bundle.mjs` over the build. The two typography rows are the
18
- * exception, and on purpose. `--font-sans` and `--font-mono` are the names of
19
- * Tailwind's own theme variables, which the prebuilt sheet already declares,
20
- * so `theme.css` does not define them: it is unlayered, and a value there
21
- * would override the typeface of every host that loads it. The stock
22
- * typeface is therefore the host's own, and only a brand's scope sets these
23
- * two. See docs/brand.md.
24
- */
25
- type ContractTokenType = 'color' | 'dimension' | 'fontFamily';
26
- interface ContractToken {
27
- /** The DTCG token id, `color.background`. */
28
- id: string;
29
- /** The custom property it sets, `--background`. */
30
- variable: string;
31
- type: ContractTokenType;
32
- /** What the property paints, in the words a producer is told. */
33
- paints: string;
34
- }
35
- /** The colour tokens, in the order `theme.css` states them. */
36
- declare const COLOR_TOKEN_NAMES: readonly ["background", "foreground", "card", "card-foreground", "popover", "popover-foreground", "primary", "primary-foreground", "secondary", "secondary-foreground", "muted", "muted-foreground", "accent", "accent-foreground", "destructive", "destructive-foreground", "border", "input", "ring"];
37
- type ColorTokenName = (typeof COLOR_TOKEN_NAMES)[number];
38
- declare function isColorTokenName(name: string): name is ColorTokenName;
39
- declare const BRAND_CONTRACT: readonly ContractToken[];
40
- /**
41
- * The pairs that must clear WCAG AA in both modes, as they render: the canvas
42
- * opaque, the ink blended over it by its alpha.
43
- */
44
- declare const CONTRAST_PAIRS: readonly {
45
- foreground: ColorTokenName;
46
- background: ColorTokenName;
47
- }[];
48
- /** The minimum contrast ratio those pairs must reach: WCAG 2 AA for normal text. */
49
- declare const MIN_CONTRAST = 4.5;
50
- /** The custom property a token sets, or a loud failure for a token the contract does not name. */
51
- declare function contractVariable(id: string): string;
52
4
 
53
5
  declare const srgbColorSchema: z.ZodObject<{
54
6
  colorSpace: z.ZodLiteral<"srgb">;
@@ -284,55 +236,6 @@ declare function compileBrand(tokens: BrandTokens, scope: string): string;
284
236
  /** Every custom property a compiled brand sets on its scope, in contract order. */
285
237
  declare const BRAND_VARIABLES: readonly string[];
286
238
 
287
- /**
288
- * Up to three seed colours in, the colour roles a page paints with out.
289
- *
290
- * A team brands its pages with at most three colours, each optional, each a
291
- * `#rrggbb` a person picked: the button colour (`action`), the accent
292
- * (`accent`) and the header band (`header`). A person picks a colour for how
293
- * it looks, never for how text reads on it, so nothing here assumes a seed is
294
- * readable. What this derives is everything a page needs around the seeds,
295
- * with every text role readable by construction:
296
- *
297
- * - **Fills are the seeds, exactly.** `action`, `accent` and `header` come
298
- * back as they were given, in lower case, so the button and the band are the
299
- * colours the person chose.
300
- * - **Text on a fill is pure black or pure white**, whichever contrasts more
301
- * (`onAction`, `onHeader`). One of the two reaches at least 4.58:1 on any
302
- * colour, so every label clears WCAG AA. Only the two extremes carry that
303
- * guarantee: a warm near-black or an off-white falls below 4.5:1 on some
304
- * mid-tone, so no other colour is ever returned for text.
305
- * - **The accent as text is ink** (`accentInk`): the accent darkened on a
306
- * light page or lightened on a dark one, keeping its hue and saturation,
307
- * just until it reads at 4.5:1 on the page and on the pale accent fill, the
308
- * two backgrounds a host draws accent text on. An accent that already reads
309
- * is its own ink.
310
- * - **The pale accent fill** (`accentFill`) is the accent laid over the page
311
- * at a low strength: a pale tint on a light page, a deep one on a dark page.
312
- *
313
- * **An unset seed produces no role.** The roles a seed paints are present
314
- * when it is set and absent when it is not, so a host keeps its own token for
315
- * whatever the team left unset: the team's colours where it chose some, and
316
- * the host's palette everywhere else. The one seed that reaches further is the
317
- * button colour, which the accent follows when the accent is unset, so a red
318
- * button never sits beside links in another hue. With no seed at all, there
319
- * is no colour role to return.
320
- *
321
- * The page colour is an argument because every text role is measured against
322
- * it: a host renders a light and a dark theme, and it calls this once per
323
- * theme with that theme's page colour and that theme's seeds. Which seeds a
324
- * theme uses is the host's to decide; `resolveDarkSeeds` is the rule for a
325
- * dark set that falls back, colour by colour, to the light one.
326
- *
327
- * It returns roles, never custom property names: each host maps the roles onto
328
- * its own tokens. See docs/brand.md § "Three seed colours".
329
- */
330
- /**
331
- * The contrast a control's outline needs against what surrounds it: WCAG 2's
332
- * level for a user interface component (1.4.11). A button colour below it
333
- * against the page is a button whose shape is hard to see.
334
- */
335
- declare const MIN_CONTROL_CONTRAST = 3;
336
239
  /**
337
240
  * Up to three seed colours, each `#rrggbb` in any case, or unset. `null` and
338
241
  * `undefined` both mean unset, so a host can pass a team's stored colours as
@@ -361,7 +264,10 @@ interface AccentRoles {
361
264
  accent: string;
362
265
  /** A pale accent fill (deep on a dark page), behind accent ink: a pill, a badge. */
363
266
  accentFill: string;
364
- /** The accent as text, reading at 4.5:1 or more on the page and on `accentFill`. */
267
+ /**
268
+ * The accent as text, reading at 4.5:1 or more on the page, on `accentFill`
269
+ * and on every other surface the host named (`deriveBrandRoles`' `surfaces`).
270
+ */
365
271
  accentInk: string;
366
272
  }
367
273
  /** The header band's roles, present when the header is set. */
@@ -385,8 +291,18 @@ type BrandRoles = (ActionRoles | Absent<ActionRoles>) & (AccentRoles | Absent<Ac
385
291
  * The roles a page paints with, from up to three seeds and the colour of the
386
292
  * page they render on. Throws when a seed or the page is not a `#rrggbb`
387
293
  * colour: a host validates what a person typed before it reaches here.
294
+ *
295
+ * `surfaces` are the other colours accent text sits on besides the page and
296
+ * the pale fill, each `#rrggbb`: a style's panel and the fields inside it
297
+ * (`resolveRoleColor` turns a style's role colour into one), or a canvas
298
+ * behind the page. The ink is moved until it reads on all of them, and an
299
+ * accent that already reads on every one is its own ink. When there is an
300
+ * accent to derive and no shade of it reads on the page, the pale fill and
301
+ * every surface at once, which a surface on the far side of the page's
302
+ * lightness can cause, it throws, naming them. With no accent, no ink is
303
+ * derived, and the surfaces are only parsed.
388
304
  */
389
- declare function deriveBrandRoles(seeds: BrandSeeds | null | undefined, page: string): BrandRoles;
305
+ declare function deriveBrandRoles(seeds: BrandSeeds | null | undefined, page: string, surfaces?: readonly string[]): BrandRoles;
390
306
  /**
391
307
  * Whether the button colour sits below 3:1 against the page, so a button may
392
308
  * be hard to see in that theme. Only a colour the person set is judged: with
@@ -395,11 +311,12 @@ declare function deriveBrandRoles(seeds: BrandSeeds | null | undefined, page: st
395
311
  declare function isActionFaint(seeds: BrandSeeds | null | undefined, page: string): boolean;
396
312
  /**
397
313
  * Whether the accent had to be darkened or lightened to become ink on the
398
- * page, so links read in a shade of it rather than in the colour itself. Only
314
+ * page (and on the `surfaces`, as `deriveBrandRoles` takes them), so links read
315
+ * in a shade of it rather than in the colour itself. Only
399
316
  * a colour the person set is judged, and the accent counts as set when it
400
317
  * follows a button colour the person set: with neither, the answer is `false`.
401
318
  */
402
- declare function isAccentAdjusted(seeds: BrandSeeds | null | undefined, page: string): boolean;
319
+ declare function isAccentAdjusted(seeds: BrandSeeds | null | undefined, page: string, surfaces?: readonly string[]): boolean;
403
320
  /**
404
321
  * The seeds a dark theme uses: each dark colour where one is set, and the
405
322
  * light one where it is not. Colour by colour, before the accent follows the
@@ -411,4 +328,4 @@ declare function resolveDarkSeeds(colors: BrandSeeds | null | undefined, darkCol
411
328
  /** Pure black or pure white, whichever reads better on a `#rrggbb` fill. */
412
329
  declare function readableTextOn(fill: string): TextOnFill;
413
330
 
414
- export { type AssembleResult, type AssembledBrand, BRAND_CONTRACT, BRAND_PRODUCERS, BRAND_VARIABLES, BrandManifest, type BrandProducer, type BrandProvenance, type BrandRoles, type BrandSeeds, type BrandSource, type BrandTokens, COLOR_TOKEN_NAMES, CONTRAST_PAIRS, type ColorMode, type ColorToken, type ColorTokenName, type ColorValue, type ContractToken, type ContractTokenType, type DimensionToken, type FontFamilyToken, MIN_CONTRAST, MIN_CONTROL_CONTRAST, type SrgbColor, type StatedFacts, type TextOnFill, type TokensValidation, assembleBrand, brandProducerId, brandProvenanceSchema, brandScope, colorHex, colorIsHex, compileBrand, contractVariable, contrastRatio, deriveBrandRoles, isAccentAdjusted, isActionFaint, isColorTokenName, readableTextOn, resolveColor, resolveDarkSeeds, srgbColorSchema, statedFactProblems, statedFactsSchema, validateBrandTokens };
331
+ export { type AssembleResult, type AssembledBrand, BRAND_PRODUCERS, BRAND_VARIABLES, BrandManifest, type BrandProducer, type BrandProvenance, type BrandRoles, type BrandSeeds, type BrandSource, type BrandTokens, type ColorMode, type ColorToken, ColorTokenName, type ColorValue, type DimensionToken, type FontFamilyToken, type SrgbColor, type StatedFacts, type TextOnFill, type TokensValidation, assembleBrand, brandProducerId, brandProvenanceSchema, brandScope, colorHex, colorIsHex, compileBrand, contrastRatio, deriveBrandRoles, isAccentAdjusted, isActionFaint, readableTextOn, resolveColor, resolveDarkSeeds, srgbColorSchema, statedFactProblems, statedFactsSchema, validateBrandTokens };
@@ -1,270 +1,7 @@
1
- import { httpUrl, brandManifestSchema } from '../chunk-QFX332LE.js';
2
- export { brandManifestSchema } from '../chunk-QFX332LE.js';
1
+ import { httpUrl, BRAND_CONTRACT, resolveColor, colorIsHex, colorHex, COLOR_TOKEN_NAMES, modeValue, aliasTarget, asWritten, isControlCharacter, brandManifestSchema, validateBrandTokens } from '../chunk-UZNRJWN5.js';
2
+ export { APP_STYLES, APP_STYLE_IDS, BRAND_CONTRACT, COLOR_TOKEN_NAMES, CONTRAST_PAIRS, MIN_CONTRAST, MIN_CONTROL_CONTRAST, TYPEFACES, brandManifestSchema, colorHex, colorIsHex, contractVariable, contrastRatio, deriveBrandRoles, deriveStyleRoles, isAccentAdjusted, isActionFaint, isAppStyle, isColorTokenName, isTypeface, readableTextOn, resolveColor, resolveDarkSeeds, resolveRoleColor, srgbColorSchema, validateBrandTokens } from '../chunk-UZNRJWN5.js';
3
3
  import { z } from 'zod';
4
4
 
5
- // src/brand/contract.ts
6
- var COLOR_TOKEN_NAMES = [
7
- "background",
8
- "foreground",
9
- "card",
10
- "card-foreground",
11
- "popover",
12
- "popover-foreground",
13
- "primary",
14
- "primary-foreground",
15
- "secondary",
16
- "secondary-foreground",
17
- "muted",
18
- "muted-foreground",
19
- "accent",
20
- "accent-foreground",
21
- "destructive",
22
- "destructive-foreground",
23
- "border",
24
- "input",
25
- "ring"
26
- ];
27
- function isColorTokenName(name) {
28
- return COLOR_TOKEN_NAMES.includes(name);
29
- }
30
- var COLOR_PAINTS = {
31
- background: "The page canvas.",
32
- foreground: "Text on the canvas.",
33
- card: "A raised surface on the canvas: the rail's panel, a list's rows. May be translucent.",
34
- "card-foreground": "Text on a card.",
35
- popover: "A floating surface: a select's menu, a tooltip. Opaque.",
36
- "popover-foreground": "Text on a popover.",
37
- primary: "The accent: the run button, a selected pill, a section number, a highlight.",
38
- "primary-foreground": "Text on the accent.",
39
- secondary: "A quiet filled surface: a secondary button, an unselected segment.",
40
- "secondary-foreground": "Text on a secondary surface.",
41
- muted: "A subdued fill: a table's header band, an inactive tab strip.",
42
- "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.",
43
- accent: "A hover fill: a menu item under the pointer.",
44
- "accent-foreground": "Text on an accent fill.",
45
- destructive: "The error colour: an invalid mark, a remove button.",
46
- "destructive-foreground": "Text on the destructive colour.",
47
- border: "The hairline: every border and divider.",
48
- input: "The control surface, a field's own fill - a step off the canvas, so fields read as one family.",
49
- ring: "The focus ring."
50
- };
51
- var BRAND_CONTRACT = [
52
- ...COLOR_TOKEN_NAMES.map((name) => ({
53
- id: `color.${name}`,
54
- variable: `--${name}`,
55
- type: "color",
56
- paints: COLOR_PAINTS[name]
57
- })),
58
- {
59
- id: "radius.base",
60
- variable: "--radius",
61
- type: "dimension",
62
- paints: "The corner radius of a control; cards take one and a half of it."
63
- },
64
- {
65
- id: "font.sans",
66
- variable: "--font-sans",
67
- type: "fontFamily",
68
- paints: "The typeface of everything on the page."
69
- },
70
- {
71
- id: "font.mono",
72
- variable: "--font-mono",
73
- type: "fontFamily",
74
- paints: "Tags, numbers, the receipt."
75
- }
76
- ];
77
- var CONTRAST_PAIRS = [
78
- { foreground: "foreground", background: "background" },
79
- { foreground: "muted-foreground", background: "background" }
80
- ];
81
- var MIN_CONTRAST = 4.5;
82
- var byId = new Map(BRAND_CONTRACT.map((token) => [token.id, token]));
83
- function contractVariable(id) {
84
- const token = byId.get(id);
85
- if (!token) throw new Error(`The brand contract has no token ${id}.`);
86
- return token.variable;
87
- }
88
- var component = z.number().min(0).max(1);
89
- var srgbColorSchema = z.strictObject({
90
- colorSpace: z.literal("srgb"),
91
- components: z.tuple([component, component, component]),
92
- alpha: z.number().min(0).max(1),
93
- hex: z.string().regex(/^#[0-9a-f]{6}$/i, "a hex colour is #rrggbb").optional()
94
- });
95
- var colorAliasSchema = z.string().regex(/^\{color\.[a-z][a-z-]*\}$/, 'an alias is "{color.<name>}"');
96
- var colorValueSchema = z.union([srgbColorSchema, colorAliasSchema]);
97
- var description = z.string().min(1, "every token carries a $description");
98
- function isControlCharacter(char) {
99
- const code = char.codePointAt(0);
100
- return code < 32 || code === 127;
101
- }
102
- var colorTokenSchema = z.strictObject({
103
- $value: colorValueSchema,
104
- $description: description,
105
- $extensions: z.strictObject({
106
- mode: z.strictObject({ dark: colorValueSchema })
107
- }).optional()
108
- }).refine((token) => token.$extensions !== void 0 || typeof token.$value === "string", {
109
- message: "a colour carries $extensions.mode.dark; only an alias may stand for both modes",
110
- path: ["$extensions"]
111
- });
112
- var dimensionTokenSchema = z.strictObject({
113
- $value: z.strictObject({ value: z.number().min(0), unit: z.enum(["rem", "px"]) }),
114
- $description: description
115
- });
116
- var fontFamilyTokenSchema = z.strictObject({
117
- $value: z.array(
118
- z.string().min(1).refine((name) => ![...name].some(isControlCharacter), {
119
- message: "a family name holds no control characters"
120
- })
121
- ).min(1),
122
- $description: description
123
- });
124
- var brandTokensSchema = z.strictObject({
125
- color: z.strictObject({
126
- $type: z.literal("color"),
127
- ...Object.fromEntries(COLOR_TOKEN_NAMES.map((name) => [name, colorTokenSchema]))
128
- }),
129
- radius: z.strictObject({ $type: z.literal("dimension"), base: dimensionTokenSchema }),
130
- font: z.strictObject({
131
- $type: z.literal("fontFamily"),
132
- sans: fontFamilyTokenSchema,
133
- mono: fontFamilyTokenSchema
134
- })
135
- });
136
- function aliasTarget(value) {
137
- return typeof value === "string" ? value.slice("{color.".length, -1) : null;
138
- }
139
- function darkValue(token) {
140
- return token.$extensions?.mode.dark ?? token.$value;
141
- }
142
- function modeValue(token, mode) {
143
- return mode === "light" ? token.$value : darkValue(token);
144
- }
145
- function colorHex(color) {
146
- return `#${color.components.map(
147
- (channel) => Math.round(channel * 255).toString(16).padStart(2, "0")
148
- ).join("")}`;
149
- }
150
- function colorIsHex(color, hex) {
151
- return colorHex(color) === hex.toLowerCase();
152
- }
153
- function asWritten(color) {
154
- const alpha = Number(Math.min(color.alpha, 1).toFixed(3));
155
- return {
156
- colorSpace: "srgb",
157
- components: color.components.map((channel) => Math.round(channel * 255) / 255),
158
- alpha
159
- };
160
- }
161
- function checkHexAgreement(name, mode, value, problems) {
162
- if (typeof value === "string" || !value.hex) return;
163
- if (!colorIsHex(value, value.hex)) {
164
- problems.push(
165
- `color.${name} (${mode}): hex ${value.hex} does not agree with components [${value.components.join(", ")}]`
166
- );
167
- }
168
- }
169
- function checkAliases(tokens, mode, problems) {
170
- for (const name of COLOR_TOKEN_NAMES) {
171
- const seen = /* @__PURE__ */ new Set([name]);
172
- let current = name;
173
- for (; ; ) {
174
- const target = aliasTarget(modeValue(tokens.color[current], mode));
175
- if (target === null) break;
176
- if (!isColorTokenName(target)) {
177
- problems.push(
178
- `color.${name} (${mode}): aliases {color.${target}}, which the contract has no token for`
179
- );
180
- break;
181
- }
182
- if (seen.has(target)) {
183
- problems.push(`color.${name} (${mode}): alias cycle through {color.${target}}`);
184
- break;
185
- }
186
- seen.add(target);
187
- current = target;
188
- }
189
- }
190
- }
191
- function resolveColor(tokens, name, mode) {
192
- const seen = /* @__PURE__ */ new Set();
193
- let current = name;
194
- for (; ; ) {
195
- if (seen.has(current)) return null;
196
- seen.add(current);
197
- const value = modeValue(tokens.color[current], mode);
198
- const target = aliasTarget(value);
199
- if (target === null) return value;
200
- if (!isColorTokenName(target)) return null;
201
- current = target;
202
- }
203
- }
204
- function luminance(color) {
205
- const [r, g, b] = color.components.map(
206
- (channel) => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4
207
- );
208
- return 0.2126 * r + 0.7152 * g + 0.0722 * b;
209
- }
210
- function contrastRatio(a, b) {
211
- const la = luminance(a);
212
- const lb = luminance(b);
213
- const [light, dark] = la > lb ? [la, lb] : [lb, la];
214
- return (light + 0.05) / (dark + 0.05);
215
- }
216
- function composite(color, over) {
217
- const components = color.components.map(
218
- (channel, index) => channel * color.alpha + over.components[index] * (1 - color.alpha)
219
- );
220
- return { colorSpace: "srgb", components, alpha: 1 };
221
- }
222
- function checkContrast(tokens, mode, problems) {
223
- for (const pair of CONTRAST_PAIRS) {
224
- const foreground = resolveColor(tokens, pair.foreground, mode);
225
- const background = resolveColor(tokens, pair.background, mode);
226
- if (!foreground || !background) continue;
227
- if (background.alpha < 1) {
228
- problems.push(
229
- `color.${pair.background} (${mode}): alpha ${background.alpha}, but text is measured against it, so it must be opaque (alpha 1)`
230
- );
231
- continue;
232
- }
233
- const canvas = asWritten(background);
234
- const ratio = contrastRatio(composite(asWritten(foreground), canvas), canvas);
235
- if (ratio < MIN_CONTRAST) {
236
- problems.push(
237
- `color.${pair.foreground} on color.${pair.background} (${mode}): contrast ${ratio.toFixed(2)}, expected ${MIN_CONTRAST} (WCAG AA)`
238
- );
239
- }
240
- }
241
- }
242
- function validateBrandTokens(doc) {
243
- const parsed = brandTokensSchema.safeParse(doc);
244
- if (!parsed.success) {
245
- return {
246
- ok: false,
247
- problems: parsed.error.issues.map((issue) => {
248
- const where = issue.path.map(String).join(".") || "(root)";
249
- return `${where}: ${issue.message}`;
250
- })
251
- };
252
- }
253
- const tokens = parsed.data;
254
- const problems = [];
255
- for (const name of COLOR_TOKEN_NAMES) {
256
- const token = tokens.color[name];
257
- checkHexAgreement(name, "light", token.$value, problems);
258
- checkHexAgreement(name, "dark", darkValue(token), problems);
259
- }
260
- checkAliases(tokens, "light", problems);
261
- checkAliases(tokens, "dark", problems);
262
- if (problems.length === 0) {
263
- checkContrast(tokens, "light", problems);
264
- checkContrast(tokens, "dark", problems);
265
- }
266
- return problems.length === 0 ? { ok: true, tokens } : { ok: false, problems };
267
- }
268
5
  var statedHex = z.string().regex(/^#[0-9a-f]{6}$/i, "a stated accent is #rrggbb");
269
6
  var statedFactsSchema = z.strictObject({
270
7
  /** The accent per mode, `#rrggbb`: what `color.primary` resolves to in that mode. */
@@ -274,8 +11,10 @@ var statedFactsSchema = z.strictObject({
274
11
  }).optional(),
275
12
  /**
276
13
  * The logo for a canvas: `logo.onLight` or `logo.onDark` in the manifest,
277
- * which the brand must carry exactly, so it obeys the manifest's rule (http
278
- * or https) or no brand could ever honour it.
14
+ * which the brand must carry exactly. It is held to the http(s) half of the
15
+ * manifest's logo rule, so a manifest can always carry it; the manifest's
16
+ * other half, a root-relative path, names a file on the host serving the
17
+ * page, which means nothing as a fact stated beside another site's URL.
279
18
  */
280
19
  logo: z.strictObject({
281
20
  onLight: httpUrl.optional(),
@@ -392,8 +131,8 @@ function compileBrand(tokens, scope) {
392
131
  return [`.${scope} {`, ...light, "}", "", `.dark .${scope} {`, ...dark, "}", ""].join("\n");
393
132
  }
394
133
  var BRAND_VARIABLES = BRAND_CONTRACT.map((token) => token.variable);
395
- function declaration(description2, property, value) {
396
- return [` /* ${commentText(description2)} */`, ` ${property}: ${value};`];
134
+ function declaration(description, property, value) {
135
+ return [` /* ${commentText(description)} */`, ` ${property}: ${value};`];
397
136
  }
398
137
  function commentText(text) {
399
138
  return text.replace(/\s+/g, " ").replace(/\*\//g, "* /").replace(/<\//g, "< /").trim();
@@ -482,118 +221,6 @@ function assembleBrand(source) {
482
221
  };
483
222
  }
484
223
 
485
- // src/brand/seed-roles.ts
486
- var MIN_CONTROL_CONTRAST = 3;
487
- var ACCENT_FILL_STRENGTH = 0.12;
488
- var SEARCH_STEPS = 32;
489
- function deriveBrandRoles(seeds, page) {
490
- const canvas = parseHex(page, "The page colour");
491
- const action = seeds?.action == null ? null : parseHex(seeds.action, "The action seed");
492
- const accent = seeds?.accent == null ? action : parseHex(seeds.accent, "The accent seed");
493
- const header = seeds?.header == null ? null : parseHex(seeds.header, "The header seed");
494
- const actionRoles = action === null ? {} : { action: colorHex(action), onAction: textOn(action) };
495
- const accentRoles = accent === null ? {} : accentRolesOf(accent, canvas);
496
- const headerRoles = header === null ? {} : { header: colorHex(header), onHeader: textOn(header) };
497
- return { ...actionRoles, ...accentRoles, ...headerRoles };
498
- }
499
- function accentRolesOf(accent, canvas) {
500
- const extreme = textOn(canvas) === "#000000" ? BLACK : WHITE;
501
- const accentFill = paleFill(accent, canvas, extreme);
502
- const accentInk = inkFor(accent, [canvas, accentFill], extreme);
503
- return {
504
- accent: colorHex(accent),
505
- accentFill: colorHex(accentFill),
506
- accentInk: colorHex(accentInk)
507
- };
508
- }
509
- function isActionFaint(seeds, page) {
510
- const canvas = parseHex(page, "The page colour");
511
- if (seeds?.action == null) return false;
512
- return contrastRatio(parseHex(seeds.action, "The action seed"), canvas) < MIN_CONTROL_CONTRAST;
513
- }
514
- function isAccentAdjusted(seeds, page) {
515
- const roles = deriveBrandRoles(seeds, page);
516
- return roles.accentInk !== void 0 && roles.accentInk !== roles.accent;
517
- }
518
- function resolveDarkSeeds(colors, darkColors) {
519
- return {
520
- action: darkColors?.action ?? colors?.action ?? null,
521
- accent: darkColors?.accent ?? colors?.accent ?? null,
522
- header: darkColors?.header ?? colors?.header ?? null
523
- };
524
- }
525
- function readableTextOn(fill) {
526
- return textOn(parseHex(fill, "The fill"));
527
- }
528
- var BLACK = { components: [0, 0, 0]};
529
- var WHITE = { components: [1, 1, 1]};
530
- function textOn(fill) {
531
- return contrastRatio(BLACK, fill) >= contrastRatio(WHITE, fill) ? "#000000" : "#ffffff";
532
- }
533
- function parseHex(value, what) {
534
- if (typeof value !== "string" || !/^#[0-9a-f]{6}$/i.test(value)) {
535
- throw new Error(`${what} must be a #rrggbb colour, not ${JSON.stringify(value)}.`);
536
- }
537
- const channel = (at) => parseInt(value.slice(at, at + 2), 16) / 255;
538
- return { colorSpace: "srgb", components: [channel(1), channel(3), channel(5)], alpha: 1 };
539
- }
540
- function snapped(components) {
541
- const [r, g, b] = components.map(
542
- (channel) => Math.round(Math.min(1, Math.max(0, channel)) * 255) / 255
543
- );
544
- return { colorSpace: "srgb", components: [r, g, b], alpha: 1 };
545
- }
546
- function paleFill(accent, page, extreme) {
547
- const at = (strength) => snapped(
548
- page.components.map(
549
- (channel, index) => channel + (accent.components[index] - channel) * strength
550
- )
551
- );
552
- const nominal = at(ACCENT_FILL_STRENGTH);
553
- if (contrastRatio(extreme, nominal) >= MIN_CONTRAST) return nominal;
554
- let enough = 0;
555
- let short = ACCENT_FILL_STRENGTH;
556
- for (let step = 0; step < SEARCH_STEPS; step++) {
557
- const middle = (enough + short) / 2;
558
- if (contrastRatio(extreme, at(middle)) >= MIN_CONTRAST) enough = middle;
559
- else short = middle;
560
- }
561
- return at(enough);
562
- }
563
- function inkFor(accent, backgrounds, extreme) {
564
- const reads = (color) => backgrounds.every((background) => contrastRatio(color, background) >= MIN_CONTRAST);
565
- if (reads(accent)) return accent;
566
- const [hue, saturation, lightness] = toHsl(accent.components);
567
- const at = (value) => snapped(fromHsl(hue, saturation, value));
568
- let short = lightness;
569
- let enough = extreme === BLACK ? 0 : 1;
570
- for (let step = 0; step < SEARCH_STEPS; step++) {
571
- const middle = (short + enough) / 2;
572
- if (reads(at(middle))) enough = middle;
573
- else short = middle;
574
- }
575
- return at(enough);
576
- }
577
- function toHsl(components) {
578
- const [r, g, b] = components;
579
- const max = Math.max(r, g, b);
580
- const min = Math.min(r, g, b);
581
- const lightness = (max + min) / 2;
582
- const chroma = max - min;
583
- if (chroma === 0) return [0, 0, lightness];
584
- const saturation = chroma / (1 - Math.abs(2 * lightness - 1));
585
- const sector = max === r ? ((g - b) / chroma + 6) % 6 : max === g ? (b - r) / chroma + 2 : (r - g) / chroma + 4;
586
- return [sector * 60, Math.min(1, saturation), lightness];
587
- }
588
- function fromHsl(hue, saturation, lightness) {
589
- const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;
590
- const sector = hue / 60;
591
- const second = chroma * (1 - Math.abs(sector % 2 - 1));
592
- 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];
593
- const offset = lightness - chroma / 2;
594
- return [r + offset, g + offset, b + offset];
595
- }
596
-
597
- export { BRAND_CONTRACT, BRAND_PRODUCERS, BRAND_VARIABLES, COLOR_TOKEN_NAMES, CONTRAST_PAIRS, MIN_CONTRAST, MIN_CONTROL_CONTRAST, assembleBrand, brandProducerId, brandProvenanceSchema, brandScope, colorHex, colorIsHex, compileBrand, contractVariable, contrastRatio, deriveBrandRoles, isAccentAdjusted, isActionFaint, isColorTokenName, readableTextOn, resolveColor, resolveDarkSeeds, srgbColorSchema, statedFactProblems, statedFactsSchema, validateBrandTokens };
224
+ export { BRAND_PRODUCERS, BRAND_VARIABLES, assembleBrand, brandProducerId, brandProvenanceSchema, brandScope, compileBrand, statedFactProblems, statedFactsSchema };
598
225
  //# sourceMappingURL=index.js.map
599
226
  //# sourceMappingURL=index.js.map