@cueplusplus/theme-base 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/base.css +201 -0
- package/dist/base.json +58 -0
- package/dist/contrast.d.mts +123 -0
- package/dist/contrast.mjs +226 -0
- package/dist/index.d.mts +182 -0
- package/dist/index.mjs +393 -0
- package/dist/manifest.schema.json +741 -0
- package/package.json +64 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { converter, parse } from "culori";
|
|
2
|
+
//#region src/contrast.ts
|
|
3
|
+
/**
|
|
4
|
+
* WCAG contrast, and the eleven pairs a theme is judged on.
|
|
5
|
+
*
|
|
6
|
+
* A generator that only derived colours would be a colour toy: the reason to
|
|
7
|
+
* compute a palette instead of picking one is that the computer can then *check*
|
|
8
|
+
* it, on every build, against the one part of visual design that is not a matter
|
|
9
|
+
* of taste. So `createTheme()` never hands back a theme without a report, and
|
|
10
|
+
* the report is the deliverable — a failing pair is information, not an error to
|
|
11
|
+
* swallow.
|
|
12
|
+
*
|
|
13
|
+
* The maths is WCAG 2.x relative luminance (sRGB, linearised, 0.2126/0.7152/
|
|
14
|
+
* 0.0722), deliberately not APCA. APCA is better, and it is also not what
|
|
15
|
+
* anyone's compliance checklist says; a theme that has to survive an audit needs
|
|
16
|
+
* the number the audit will compute.
|
|
17
|
+
*/
|
|
18
|
+
const toRgb = converter("rgb");
|
|
19
|
+
/** Non-text and large-text AA: UI components, graphics, status dots. */
|
|
20
|
+
const WCAG_AA_NON_TEXT = 3;
|
|
21
|
+
/** Body-text AA. The floor for anything a reader is expected to read. */
|
|
22
|
+
const WCAG_AA_TEXT = 4.5;
|
|
23
|
+
/** Body-text AAA. What this portfolio holds its primary ink to. */
|
|
24
|
+
const WCAG_AAA_TEXT = 7;
|
|
25
|
+
/** sRGB gamma decode, per WCAG's definition of relative luminance. */
|
|
26
|
+
function linearize(channel) {
|
|
27
|
+
return channel <= .04045 ? channel / 12.92 : ((channel + .055) / 1.055) ** 2.4;
|
|
28
|
+
}
|
|
29
|
+
/** Parse to sRGB, or say which value could not be read. */
|
|
30
|
+
function toSrgb(input, label = "colour") {
|
|
31
|
+
const color = toRgb(parse(input));
|
|
32
|
+
if (color === void 0) throw new TypeError(`${label} is not a colour this can measure: ${JSON.stringify(input)}`);
|
|
33
|
+
return color;
|
|
34
|
+
}
|
|
35
|
+
/** Source-over composite of `over` onto `under`, in (non-linear) sRGB. */
|
|
36
|
+
function composite(over, under) {
|
|
37
|
+
const alpha = over.alpha ?? 1;
|
|
38
|
+
if (alpha >= 1) return over;
|
|
39
|
+
return {
|
|
40
|
+
r: over.r * alpha + under.r * (1 - alpha),
|
|
41
|
+
g: over.g * alpha + under.g * (1 - alpha),
|
|
42
|
+
b: over.b * alpha + under.b * (1 - alpha)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function luminanceOf(color) {
|
|
46
|
+
return .2126 * linearize(color.r) + .7152 * linearize(color.g) + .0722 * linearize(color.b);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* WCAG relative luminance: 0 for black, 1 for white.
|
|
50
|
+
*
|
|
51
|
+
* Alpha is ignored — a translucent colour has no luminance of its own, only one
|
|
52
|
+
* in front of something. {@link contrastRatio} is where that composite happens.
|
|
53
|
+
*
|
|
54
|
+
* @param color - Any CSS colour string.
|
|
55
|
+
* @returns Relative luminance in `[0, 1]`.
|
|
56
|
+
* @throws TypeError if the value is not a colour.
|
|
57
|
+
* @example
|
|
58
|
+
* relativeLuminance("#767676"); // → 0.1845…
|
|
59
|
+
*/
|
|
60
|
+
function relativeLuminance(color) {
|
|
61
|
+
return luminanceOf(toSrgb(color));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* WCAG contrast ratio between two colours, from 1:1 to 21:1.
|
|
65
|
+
*
|
|
66
|
+
* Symmetric, because a ratio has no direction — the arguments are named for
|
|
67
|
+
* readability, not for order. A **translucent foreground is composited over the
|
|
68
|
+
* background first**, which is not decoration: half this system's `fg-muted`
|
|
69
|
+
* tokens are white-alpha ink, and measuring them raw would report the ratio of
|
|
70
|
+
* pure white and pass everything.
|
|
71
|
+
*
|
|
72
|
+
* @param foreground - The ink. Composited over `background` if it has alpha.
|
|
73
|
+
* @param background - The ground. Its own alpha is ignored — nothing is behind it.
|
|
74
|
+
* @returns The ratio, `>= 1`.
|
|
75
|
+
* @throws TypeError naming whichever value could not be read.
|
|
76
|
+
* @example
|
|
77
|
+
* contrastRatio("#767676", "#ffffff"); // → 4.5422…
|
|
78
|
+
*/
|
|
79
|
+
function contrastRatio(foreground, background) {
|
|
80
|
+
const ground = toSrgb(background, "background");
|
|
81
|
+
const [high, low] = [luminanceOf(composite(toSrgb(foreground, "foreground"), ground)), luminanceOf(ground)].toSorted((a, b) => b - a);
|
|
82
|
+
return (high + .05) / (low + .05);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The eleven pairs that decide whether a theme is usable, and the nine that
|
|
86
|
+
* warn about it.
|
|
87
|
+
*
|
|
88
|
+
* Not every pair in the palette: the ones a reader's comprehension actually
|
|
89
|
+
* depends on. Primary ink is held to AAA because it carries the prose; secondary
|
|
90
|
+
* ink and accent ink to AA because they carry labels; the five status colours
|
|
91
|
+
* and the live hue to the non-text AA floor because they are read as *signals*
|
|
92
|
+
* — a dot, a rim, a bar — never as body copy.
|
|
93
|
+
*
|
|
94
|
+
* `danger` and `warn` each appear twice, and they are the only tones that do.
|
|
95
|
+
* `ok`, `busy`, `info` and `stream` are *only* ever signals, so the non-text
|
|
96
|
+
* floor is the whole truth about them. The red and the amber are also grounds:
|
|
97
|
+
* they are the two tones this system fills a control with, and what they fill is
|
|
98
|
+
* the confirm on something irreversible and the confirm on something merely
|
|
99
|
+
* consequential. Text on either fill is text, so its ink is held to the text
|
|
100
|
+
* floor against the tone it sits on rather than against the page.
|
|
101
|
+
*
|
|
102
|
+
* `stream` is measured for the same reason `ok` is: `createTheme()` darkens it
|
|
103
|
+
* to this floor when it has to invent a light block, and a report that did not
|
|
104
|
+
* check what the derivation targets would be checking the wrong thing.
|
|
105
|
+
*/
|
|
106
|
+
/**
|
|
107
|
+
* Every ground a control can paint a ring over.
|
|
108
|
+
*
|
|
109
|
+
* Module-local on purpose. It is the same set the preset gate has always walked,
|
|
110
|
+
* and it is a fact about where controls sit rather than part of the contract a
|
|
111
|
+
* consumer reads — exporting it would invite a caller to hold their own themes
|
|
112
|
+
* to a list this file is free to grow.
|
|
113
|
+
*
|
|
114
|
+
* None of the eleven required pairs involves `sunken` or any `surface-*` rung,
|
|
115
|
+
* which is the other half of why the ring went unmeasured: a consumer overriding
|
|
116
|
+
* `surfaces`, which `ThemeAnchors` invites, moves the grounds the ring is painted
|
|
117
|
+
* on without moving a single measured pair.
|
|
118
|
+
*/
|
|
119
|
+
const RING_GROUNDS = [
|
|
120
|
+
"sunken",
|
|
121
|
+
"bg",
|
|
122
|
+
"surface-1",
|
|
123
|
+
"surface-2",
|
|
124
|
+
"surface-3"
|
|
125
|
+
];
|
|
126
|
+
const CONTRAST_REQUIREMENTS = [
|
|
127
|
+
{
|
|
128
|
+
id: "fg/bg",
|
|
129
|
+
foreground: "fg",
|
|
130
|
+
background: "bg",
|
|
131
|
+
minimum: 7,
|
|
132
|
+
reason: "Primary ink carries the prose, and this portfolio holds it to AAA."
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: "fg-muted/bg",
|
|
136
|
+
foreground: "fg-muted",
|
|
137
|
+
background: "bg",
|
|
138
|
+
minimum: WCAG_AA_TEXT,
|
|
139
|
+
reason: "Secondary ink is still text: labels, captions, table headers."
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "accent-fg/accent",
|
|
143
|
+
foreground: "accent-fg",
|
|
144
|
+
background: "accent",
|
|
145
|
+
minimum: WCAG_AA_TEXT,
|
|
146
|
+
reason: "The label on a solid accent button has to be readable."
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "danger-fg/danger",
|
|
150
|
+
foreground: "danger-fg",
|
|
151
|
+
background: "danger",
|
|
152
|
+
minimum: WCAG_AA_TEXT,
|
|
153
|
+
reason: "The label on a solid destructive fill is read before something irreversible happens."
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "warn-fg/warn",
|
|
157
|
+
foreground: "warn-fg",
|
|
158
|
+
background: "warn",
|
|
159
|
+
minimum: WCAG_AA_TEXT,
|
|
160
|
+
reason: "The label on a solid warning fill is read before a consequential press lands."
|
|
161
|
+
},
|
|
162
|
+
...[
|
|
163
|
+
"ok",
|
|
164
|
+
"busy",
|
|
165
|
+
"warn",
|
|
166
|
+
"danger",
|
|
167
|
+
"info",
|
|
168
|
+
"stream"
|
|
169
|
+
].map((status) => ({
|
|
170
|
+
id: `${status}/bg`,
|
|
171
|
+
foreground: status,
|
|
172
|
+
background: "bg",
|
|
173
|
+
minimum: 3,
|
|
174
|
+
reason: `The ${status} tone is read as a signal — dot, rim, bar — not as body copy.`
|
|
175
|
+
})),
|
|
176
|
+
...["focus", "danger"].flatMap((ring) => RING_GROUNDS.filter((ground) => !(ring === "danger" && ground === "bg")).map((ground) => ({
|
|
177
|
+
id: `${ring}/${ground}`,
|
|
178
|
+
foreground: ring,
|
|
179
|
+
background: ground,
|
|
180
|
+
minimum: 3,
|
|
181
|
+
advisory: true,
|
|
182
|
+
reason: ring === "focus" ? "The focus ring is the only thing telling a keyboard user where they are (WCAG 1.4.11)." : "An invalid field's ring replaces its rim, so the ring itself carries the state."
|
|
183
|
+
})))
|
|
184
|
+
];
|
|
185
|
+
/**
|
|
186
|
+
* Measure every requirement against every block handed in.
|
|
187
|
+
*
|
|
188
|
+
* A pair whose tokens are missing is **skipped**, not failed: the report says
|
|
189
|
+
* what it measured, and a caller checking a partial palette should not be told
|
|
190
|
+
* that a colour it never supplied is unreadable.
|
|
191
|
+
*
|
|
192
|
+
* @param inputs - One entry per block, dark first by convention.
|
|
193
|
+
* @returns The report.
|
|
194
|
+
* @example
|
|
195
|
+
* contrastReport([{ mode: "dark", tokens: { fg: "#fff", bg: "#000" } }]).passes; // → true
|
|
196
|
+
*/
|
|
197
|
+
function contrastReport(inputs) {
|
|
198
|
+
const checks = [];
|
|
199
|
+
for (const { mode, tokens, provisional = false } of inputs) for (const requirement of CONTRAST_REQUIREMENTS) {
|
|
200
|
+
const foregroundValue = tokens[requirement.foreground];
|
|
201
|
+
const backgroundValue = tokens[requirement.background];
|
|
202
|
+
if (foregroundValue === void 0 || backgroundValue === void 0) continue;
|
|
203
|
+
const ratio = contrastRatio(foregroundValue, backgroundValue);
|
|
204
|
+
checks.push({
|
|
205
|
+
...requirement,
|
|
206
|
+
mode,
|
|
207
|
+
foregroundValue,
|
|
208
|
+
backgroundValue,
|
|
209
|
+
ratio,
|
|
210
|
+
passes: ratio >= requirement.minimum,
|
|
211
|
+
provisional
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
const missed = checks.filter((check) => !check.passes);
|
|
215
|
+
const failures = missed.filter((check) => check.advisory !== true);
|
|
216
|
+
const advisories = missed.filter((check) => check.advisory === true);
|
|
217
|
+
return {
|
|
218
|
+
passes: failures.length === 0,
|
|
219
|
+
provisional: checks.some((check) => check.provisional),
|
|
220
|
+
checks,
|
|
221
|
+
failures,
|
|
222
|
+
advisories
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
export { CONTRAST_REQUIREMENTS, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport, relativeLuminance };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { COLOR_CONTRACT, ColorToken, ColorToken as ColorToken$1, DENSITIES, Density as Density$1, FONTS, FONT_TOKENS, FontName as FontName$1, FontToken, GEOMETRY_CONTRACT, GeometryToken, GeometryToken as GeometryToken$1, MODES, Mode } from "@cueplusplus/tokens";
|
|
2
|
+
//#region src/contract.d.ts
|
|
3
|
+
/** The manifest format this package reads. Bumped only for a breaking shape change. */
|
|
4
|
+
declare const MANIFEST_SCHEMA_VERSION: 1;
|
|
5
|
+
/**
|
|
6
|
+
* What may be interpolated into `[data-theme="…"]`. The same grammar
|
|
7
|
+
* `@cueplusplus/ui/theming`'s serializer has always used: an attribute selector
|
|
8
|
+
* is the one place a theme name is written into CSS, and a name that could
|
|
9
|
+
* close the quote there is not a name.
|
|
10
|
+
*/
|
|
11
|
+
declare const THEME_NAME_PATTERN: RegExp;
|
|
12
|
+
/**
|
|
13
|
+
* The two halves of the shared monospace token, and the resolution of them.
|
|
14
|
+
*
|
|
15
|
+
* `--cue-font-mono` is owned jointly by two axes that land on **different
|
|
16
|
+
* elements**: a provider stamps `data-theme` on a `<div>` inside the `<html>`
|
|
17
|
+
* the pre-paint script stamped `data-font` on, and for an inherited property the
|
|
18
|
+
* nearer declaration wins whatever the stylesheet's order is. So neither axis
|
|
19
|
+
* writes the shared token — the theme publishes `--cue-font-theme-mono`, a
|
|
20
|
+
* pairing publishes `--cue-font-pairing-mono`, and every block that owns a half
|
|
21
|
+
* restates `--cue-font-mono` as the `var()` chain below, pairing first.
|
|
22
|
+
*
|
|
23
|
+
* Three packages write CSS carrying that chain — `tokens`' own build,
|
|
24
|
+
* `theme-tools`' emitter and `@cueplusplus/ui`'s serializer — and none of them
|
|
25
|
+
* imports the others, so until this release each spelled it for itself and
|
|
26
|
+
* nothing compared the three. `tokens` still needs the literal to emit
|
|
27
|
+
* `axes.css`, so it stays the author and publishes the strings in `base.json`;
|
|
28
|
+
* this package re-exports them because it is the one every theme-side consumer
|
|
29
|
+
* already depends on. `tokens/test/fonts.test.mjs` holds the emitted stylesheet
|
|
30
|
+
* to the same values, so the data and the CSS cannot come apart either.
|
|
31
|
+
*/
|
|
32
|
+
declare const PAIRING_MONO: string;
|
|
33
|
+
/** The theme axis's half. See {@link PAIRING_MONO}. */
|
|
34
|
+
declare const THEME_MONO: string;
|
|
35
|
+
/** What every block that owns a half resolves `--cue-font-mono` to. See {@link PAIRING_MONO}. */
|
|
36
|
+
declare const RESOLVED_MONO: string;
|
|
37
|
+
/**
|
|
38
|
+
* The theme registry: an empty interface a theme package augments.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* declare module "@cueplusplus/theme-base" {
|
|
42
|
+
* interface ThemeRegistry { "foo": typeof manifest }
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* Quoted, as `theme-tools` emits it: `THEME_NAME_PATTERN` allows a hyphen, and
|
|
47
|
+
* `acme-brand` unquoted is not an interface key TypeScript can parse.
|
|
48
|
+
*
|
|
49
|
+
* `ThemeName` is its keys, or `string` in an app that installed no theme —
|
|
50
|
+
* `keyof {}` is `never`, and a `never`-typed prop would make `ThemeProvider`
|
|
51
|
+
* unusable until the first theme arrived, which is the wrong first experience.
|
|
52
|
+
*/
|
|
53
|
+
interface ThemeRegistry {}
|
|
54
|
+
type ThemeName = keyof ThemeRegistry extends never ? string : keyof ThemeRegistry;
|
|
55
|
+
/** The density ladder: the five base rungs, plus whatever a theme adds by augmentation. */
|
|
56
|
+
interface DensityRegistry extends Record<Density$1, true> {}
|
|
57
|
+
type Density = keyof DensityRegistry;
|
|
58
|
+
/** The font pairings: the eight base ones, plus whatever a theme adds by augmentation. */
|
|
59
|
+
interface FontRegistry extends Record<FontName$1, true> {}
|
|
60
|
+
type FontName = keyof FontRegistry;
|
|
61
|
+
/** A complete colour block: every token in {@link COLOR_CONTRACT}, resolved. */
|
|
62
|
+
type ColorBlock = Readonly<Record<ColorToken$1, string>>;
|
|
63
|
+
/** A complete geometry block: every token in {@link GEOMETRY_CONTRACT}. */
|
|
64
|
+
type GeometryBlock = Readonly<Record<GeometryToken$1, string>>;
|
|
65
|
+
/** A partial geometry block: only what an override changes. */
|
|
66
|
+
type GeometryPatch = Readonly<Partial<Record<GeometryToken$1, string>>>;
|
|
67
|
+
/** The stacks a pairing names. A pairing must name a sans; the rest are optional. */
|
|
68
|
+
interface FontStacks {
|
|
69
|
+
readonly "font-sans": string;
|
|
70
|
+
readonly "font-mono"?: string;
|
|
71
|
+
readonly "font-display"?: string;
|
|
72
|
+
}
|
|
73
|
+
interface ManifestColors {
|
|
74
|
+
readonly dark: ColorBlock;
|
|
75
|
+
/** Absent exactly when {@link ThemeManifest.supportsLight} is false. */
|
|
76
|
+
readonly light?: ColorBlock;
|
|
77
|
+
}
|
|
78
|
+
interface ManifestDensities {
|
|
79
|
+
/** Rungs this theme adds. Each carries the full geometry contract. */
|
|
80
|
+
readonly adds: Readonly<Record<string, GeometryBlock>>;
|
|
81
|
+
/** Base rungs this theme retunes, scoped to itself. Each carries only what changed. */
|
|
82
|
+
readonly overrides: Readonly<Partial<Record<Density$1, GeometryPatch>>>;
|
|
83
|
+
/** The rung this theme prefers when the consumer names none. Base default when absent. */
|
|
84
|
+
readonly default?: string;
|
|
85
|
+
}
|
|
86
|
+
interface ManifestFontPairings {
|
|
87
|
+
readonly adds: Readonly<Record<string, FontStacks>>;
|
|
88
|
+
readonly overrides: Readonly<Partial<Record<FontName$1, Partial<FontStacks>>>>;
|
|
89
|
+
readonly default?: string;
|
|
90
|
+
}
|
|
91
|
+
interface ManifestContrast {
|
|
92
|
+
/** Every required pair cleared its floor. False is emitted only when every failure is waived. */
|
|
93
|
+
readonly passes: boolean;
|
|
94
|
+
/** Advisory pairs that missed. Reported, never fatal. */
|
|
95
|
+
readonly advisories: number;
|
|
96
|
+
/** Required pairs the theme named as known-bad. Counted, never silent: `passes` stays false. */
|
|
97
|
+
readonly waived: number;
|
|
98
|
+
/** `sha256-…` over the resolved colours; what a production `ThemeProvider` trusts. */
|
|
99
|
+
readonly digest: string;
|
|
100
|
+
}
|
|
101
|
+
/** The runtime's whole view of a theme. CSS paints; this enumerates, validates and types. */
|
|
102
|
+
interface ThemeManifest {
|
|
103
|
+
readonly schemaVersion: typeof MANIFEST_SCHEMA_VERSION;
|
|
104
|
+
/** Matches {@link THEME_NAME_PATTERN}; becomes `data-theme`. */
|
|
105
|
+
readonly name: string;
|
|
106
|
+
readonly package: string;
|
|
107
|
+
/** The `theme-base` range this manifest was resolved against, e.g. `@cueplusplus/theme-base@^1`. */
|
|
108
|
+
readonly extends: string;
|
|
109
|
+
/** `delta`: `theme.css` holds only what differs from base. `complete`: it stands alone. */
|
|
110
|
+
readonly mode: "delta" | "complete";
|
|
111
|
+
readonly supportsLight: boolean;
|
|
112
|
+
readonly colors: ManifestColors;
|
|
113
|
+
/** The theme's overrides of the base stacks (a preset's `font-mono`, typically). */
|
|
114
|
+
readonly fonts: Readonly<Partial<FontStacks>>;
|
|
115
|
+
readonly densities: ManifestDensities;
|
|
116
|
+
readonly fontPairings: ManifestFontPairings;
|
|
117
|
+
readonly contrast: ManifestContrast;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/validate.d.ts
|
|
121
|
+
/** One thing wrong with a manifest, where it is. */
|
|
122
|
+
interface ManifestProblem {
|
|
123
|
+
/** Dotted path from the root, `$` for the root itself. */
|
|
124
|
+
path: string;
|
|
125
|
+
message: string;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Every problem with a manifest, in document order.
|
|
129
|
+
*
|
|
130
|
+
* Hand-written rather than run through a JSON Schema library because this
|
|
131
|
+
* executes inside `ThemeProvider` in development and a schema engine is not a
|
|
132
|
+
* dependency a component library should carry for a check that runs once. The
|
|
133
|
+
* schema file beside it says the same rules in the language other tools read,
|
|
134
|
+
* and `test/validate.test.ts` holds the two to identical verdicts — on the six
|
|
135
|
+
* fixtures, and on a few hundred generated mutations of the valid one.
|
|
136
|
+
*
|
|
137
|
+
* Every rule is a sentence a theme author can act on: the message names the
|
|
138
|
+
* token that is missing, the rung that collides, the field that contradicts.
|
|
139
|
+
*
|
|
140
|
+
* The three names a theme brings — its own, its added rungs, its added pairings
|
|
141
|
+
* — are each held to {@link THEME_NAME_PATTERN}, because each one is written
|
|
142
|
+
* into an attribute selector (`[data-theme="…"]`, `[data-density="…"]`,
|
|
143
|
+
* `[data-font="…"]`) and a name that could close that quote is not a name. That
|
|
144
|
+
* pattern is case-insensitive, so a base rung or pairing is reserved in any
|
|
145
|
+
* case: `Compact` beside `compact` would be two rungs the CSS cannot tell apart.
|
|
146
|
+
*/
|
|
147
|
+
declare function validateManifest(value: unknown): ManifestProblem[];
|
|
148
|
+
/** {@link validateManifest}, as a throw. */
|
|
149
|
+
declare function assertManifest(value: unknown): asserts value is ThemeManifest;
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/resolve.d.ts
|
|
152
|
+
interface ResolveOptions {
|
|
153
|
+
mode: "dark" | "light";
|
|
154
|
+
density?: string;
|
|
155
|
+
font?: string;
|
|
156
|
+
}
|
|
157
|
+
interface ResolvedTheme {
|
|
158
|
+
colors: ColorBlock;
|
|
159
|
+
geometry: GeometryBlock;
|
|
160
|
+
fonts: Required<FontStacks>;
|
|
161
|
+
/** The rung actually used, after fallback. */
|
|
162
|
+
density: string;
|
|
163
|
+
/** The pairing actually used, after fallback. */
|
|
164
|
+
font: string;
|
|
165
|
+
}
|
|
166
|
+
declare function resolve(manifest: ThemeManifest | null, options: ResolveOptions): ResolvedTheme;
|
|
167
|
+
interface DensityEntry {
|
|
168
|
+
name: string;
|
|
169
|
+
owner: "base" | string;
|
|
170
|
+
geometry: GeometryBlock;
|
|
171
|
+
}
|
|
172
|
+
/** The rungs an app may offer under the active theme: base first, then the theme's additions. */
|
|
173
|
+
declare function resolveDensities(manifests: readonly ThemeManifest[], active: string | null): readonly DensityEntry[];
|
|
174
|
+
interface FontEntry {
|
|
175
|
+
name: string;
|
|
176
|
+
owner: "base" | string;
|
|
177
|
+
stacks: Required<FontStacks>;
|
|
178
|
+
}
|
|
179
|
+
/** The pairings an app may offer under the active theme: the same rule, one axis over. */
|
|
180
|
+
declare function resolveFonts(manifests: readonly ThemeManifest[], active: string | null): readonly FontEntry[];
|
|
181
|
+
//#endregion
|
|
182
|
+
export { COLOR_CONTRACT, ColorBlock, type ColorToken, DENSITIES, Density, type DensityEntry, DensityRegistry, FONTS, FONT_TOKENS, type FontEntry, FontName, FontRegistry, FontStacks, type FontToken, GEOMETRY_CONTRACT, GeometryBlock, GeometryPatch, type GeometryToken, MANIFEST_SCHEMA_VERSION, MODES, ManifestColors, ManifestContrast, ManifestDensities, ManifestFontPairings, type ManifestProblem, type Mode, PAIRING_MONO, RESOLVED_MONO, type ResolveOptions, type ResolvedTheme, THEME_MONO, THEME_NAME_PATTERN, ThemeManifest, ThemeName, ThemeRegistry, assertManifest, resolve, resolveDensities, resolveFonts, validateManifest };
|