@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.
- package/dist/brand/index.d.ts +21 -104
- package/dist/brand/index.js +9 -382
- package/dist/brand/index.js.map +1 -1
- package/dist/{chunk-ZX52CQJ4.js → chunk-2UCH4AAO.js} +3 -3
- package/dist/{chunk-ZX52CQJ4.js.map → chunk-2UCH4AAO.js.map} +1 -1
- package/dist/{chunk-RHX7LQRL.js → chunk-ENK5QTUK.js} +390 -128
- package/dist/chunk-ENK5QTUK.js.map +1 -0
- package/dist/{chunk-ZB4RYUKV.js → chunk-HVWE3TTC.js} +3 -3
- package/dist/{chunk-ZB4RYUKV.js.map → chunk-HVWE3TTC.js.map} +1 -1
- package/dist/chunk-UZNRJWN5.js +721 -0
- package/dist/chunk-UZNRJWN5.js.map +1 -0
- package/dist/{chunk-IBBAJXIO.js → chunk-XVGAQ4RK.js} +4 -4
- package/dist/{chunk-IBBAJXIO.js.map → chunk-XVGAQ4RK.js.map} +1 -1
- package/dist/core/derive.js +2 -2
- package/dist/core/gate.js +3 -3
- package/dist/core/index.js +12 -12
- package/dist/core/values.js +3 -3
- package/dist/generative/index.d.ts +474 -79
- package/dist/generative/index.js +1261 -512
- package/dist/generative/index.js.map +1 -1
- package/dist/layout-design.mthds +133 -6
- package/dist/react/index.d.ts +24 -6
- package/dist/react/index.js +7 -7
- package/dist/styles-BoXPBjF4.d.ts +258 -0
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/dist/chunk-QFX332LE.js +0 -22
- package/dist/chunk-QFX332LE.js.map +0 -1
- package/dist/chunk-RHX7LQRL.js.map +0 -1
- package/dist/manifest-DUeAWeqa.d.ts +0 -17
package/dist/brand/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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,
|
|
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 };
|
package/dist/brand/index.js
CHANGED
|
@@ -1,270 +1,7 @@
|
|
|
1
|
-
import { httpUrl, brandManifestSchema } from '../chunk-
|
|
2
|
-
export { brandManifestSchema } from '../chunk-
|
|
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
|
|
278
|
-
*
|
|
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(
|
|
396
|
-
return [` /* ${commentText(
|
|
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
|
-
|
|
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
|