@domandigital/craft 0.1.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/CHANGELOG.md +39 -0
- package/LICENSE +202 -0
- package/PRINCIPLES.md +93 -0
- package/README.md +105 -0
- package/STANDARD.md +166 -0
- package/css/craft.css +145 -0
- package/css/craft.tailwind.css +97 -0
- package/dist/index.cjs +1201 -0
- package/dist/index.d.cts +676 -0
- package/dist/index.d.ts +676 -0
- package/dist/index.js +1128 -0
- package/package.json +61 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OKLab / OKLCh conversions, after Björn Ottosson's published M1/M2 matrices.
|
|
3
|
+
* https://bottosson.github.io/posts/oklab/
|
|
4
|
+
*
|
|
5
|
+
* Zero dependencies is a hard constraint for every dd-packages package, so the
|
|
6
|
+
* colour maths is implemented here rather than pulled from culori/colorjs.
|
|
7
|
+
*/
|
|
8
|
+
interface Oklch {
|
|
9
|
+
/** Perceptual lightness, 0..1. */
|
|
10
|
+
l: number;
|
|
11
|
+
/** Chroma, 0..~0.4 in practice. */
|
|
12
|
+
c: number;
|
|
13
|
+
/** Hue angle in degrees, 0..360. */
|
|
14
|
+
h: number;
|
|
15
|
+
}
|
|
16
|
+
interface Rgb {
|
|
17
|
+
/** 0..1 */
|
|
18
|
+
r: number;
|
|
19
|
+
/** 0..1 */
|
|
20
|
+
g: number;
|
|
21
|
+
/** 0..1 */
|
|
22
|
+
b: number;
|
|
23
|
+
}
|
|
24
|
+
type Gamut = "srgb" | "p3";
|
|
25
|
+
/** Parse `#rgb`, `#rrggbb` or `#rrggbbaa` (alpha ignored) into 0..1 channels. */
|
|
26
|
+
declare function parseHex(hex: string): Rgb;
|
|
27
|
+
/** Serialise 0..1 channels as a lowercase `#rrggbb`, clamping out-of-range input. */
|
|
28
|
+
declare function formatHex({ r, g, b }: Rgb): string;
|
|
29
|
+
/** Convert a hex string to OKLCh. */
|
|
30
|
+
declare function hexToOklch(hex: string): Oklch;
|
|
31
|
+
/** Is this OKLCh colour representable in sRGB, within a small epsilon? */
|
|
32
|
+
declare function inSrgbGamut(colour: Oklch, epsilon?: number): boolean;
|
|
33
|
+
/** Is this OKLCh colour representable in Display P3, within a small epsilon? */
|
|
34
|
+
declare function inP3Gamut(colour: Oklch, epsilon?: number): boolean;
|
|
35
|
+
/** Unclamped OKLCh to sRGB, for callers that have already gamut-mapped. */
|
|
36
|
+
declare function oklchToRgb(colour: Oklch): Rgb;
|
|
37
|
+
/**
|
|
38
|
+
* Perceptual distance in OKLab. ~0.02 is the just-noticeable difference used as
|
|
39
|
+
* the house tolerance for a derived colour standing in for a hand-picked one.
|
|
40
|
+
*/
|
|
41
|
+
declare function deltaEOk(a: Oklch, b: Oklch): number;
|
|
42
|
+
/** Convenience: perceptual distance between two hex strings. */
|
|
43
|
+
declare function deltaEOkHex(a: string, b: string): number;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Gamut mapping, per CSS Color 4 §13.2.
|
|
47
|
+
*
|
|
48
|
+
* Split from `oklch.ts` so the dependency runs one way: this module imports the
|
|
49
|
+
* conversions, never the reverse.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
interface OklchToHexOptions {
|
|
53
|
+
/** Which gamut to map into before serialising. Defaults to sRGB. */
|
|
54
|
+
gamut?: Gamut;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* OKLCh to hex, gamut-mapped by chroma reduction so hue survives.
|
|
58
|
+
*
|
|
59
|
+
* Naive per-channel RGB clipping is rejected here: clipping a saturated violet
|
|
60
|
+
* pins blue at 1.0 while red keeps falling, which walks the hue toward magenta.
|
|
61
|
+
* CSS Color 4 §13.2 bisects chroma with lightness and hue held fixed instead.
|
|
62
|
+
*/
|
|
63
|
+
declare function oklchToHex(colour: Oklch, options?: OklchToHexOptions): string;
|
|
64
|
+
/**
|
|
65
|
+
* Reduce chroma until the colour fits the target gamut, per CSS Color 4 §13.2.
|
|
66
|
+
* Lightness and hue are preserved exactly; only chroma moves.
|
|
67
|
+
*/
|
|
68
|
+
declare function toGamut(colour: Oklch, gamut?: Gamut): Oklch;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Eleven-step colour ramps derived in OKLCh.
|
|
72
|
+
*
|
|
73
|
+
* The governing rule is the anchor guarantee: a hex the caller supplied comes
|
|
74
|
+
* back out of the ramp as the *same string*, byte for byte. Everything else on
|
|
75
|
+
* the ramp is generated. That is what lets an existing site adopt craft without
|
|
76
|
+
* a single pixel moving — the colours already shipped stay literal, and only
|
|
77
|
+
* the steps nobody had picked by hand are computed.
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
/** The eleven steps, light to dark. */
|
|
81
|
+
declare const RAMP_STEPS: readonly [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
|
|
82
|
+
type RampStep = (typeof RAMP_STEPS)[number];
|
|
83
|
+
/**
|
|
84
|
+
* Target OKLCh lightness per step. Spacing is tighter at the light end, where
|
|
85
|
+
* the eye resolves smaller differences, and opens up through the shadows.
|
|
86
|
+
*/
|
|
87
|
+
declare const LIGHTNESS_CURVE: Readonly<Record<RampStep, number>>;
|
|
88
|
+
interface Ramp {
|
|
89
|
+
/** Step to hex. The anchor step holds the caller's exact input string. */
|
|
90
|
+
ramp: Record<RampStep, string>;
|
|
91
|
+
/** Which step the seed was placed at. */
|
|
92
|
+
anchor: RampStep;
|
|
93
|
+
/** The seed, in OKLCh. */
|
|
94
|
+
seed: Oklch;
|
|
95
|
+
}
|
|
96
|
+
interface RampOptions {
|
|
97
|
+
/** Force the seed onto this step instead of placing it by lightness. */
|
|
98
|
+
step?: RampStep;
|
|
99
|
+
/** Gamut to map generated steps into. Defaults to sRGB. */
|
|
100
|
+
gamut?: Gamut;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Build an eleven-step ramp from one seed colour.
|
|
104
|
+
*
|
|
105
|
+
* The seed is placed on the step its lightness is nearest (or `options.step`),
|
|
106
|
+
* and that step returns the input string unchanged. Remaining steps hold the
|
|
107
|
+
* seed's hue, take their lightness from `LIGHTNESS_CURVE`, and scale chroma by
|
|
108
|
+
* the envelope normalised so the anchor keeps exactly the chroma it had.
|
|
109
|
+
*/
|
|
110
|
+
declare function ramp(seed: string, options?: RampOptions): Ramp;
|
|
111
|
+
interface RampFromAnchorsOptions {
|
|
112
|
+
gamut?: Gamut;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build a ramp from two or more known steps.
|
|
116
|
+
*
|
|
117
|
+
* Every hex the caller provides is returned unchanged. Gaps between anchors are
|
|
118
|
+
* interpolated in OKLCh — lightness, chroma and hue each moving on the shortest
|
|
119
|
+
* path — and steps outside the anchored span extend the nearest anchor along
|
|
120
|
+
* `LIGHTNESS_CURVE`. This is the entry point Phase 5 uses, where the 400/500/600
|
|
121
|
+
* steps are already shipped values that must not move.
|
|
122
|
+
*/
|
|
123
|
+
declare function rampFromAnchors(anchors: Partial<Record<RampStep, string>>, options?: RampFromAnchorsOptions): Record<RampStep, string>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Contrast: WCAG 2.x ratio and APCA-W3 lightness contrast (Lc).
|
|
127
|
+
*
|
|
128
|
+
* Both, deliberately. WCAG 2.x is what an audit, a client and the law ask for.
|
|
129
|
+
* APCA is what actually tracks readability on dark backgrounds, where WCAG 2.x
|
|
130
|
+
* is known to pass text that is genuinely hard to read. The house bar is a pair:
|
|
131
|
+
* clear 4.5:1 *and* Lc 60, or it is not an accent that carries body text.
|
|
132
|
+
*/
|
|
133
|
+
/** WCAG 2.x contrast ratio, 1..21. Order-independent. */
|
|
134
|
+
declare function wcagContrast(a: string, b: string): number;
|
|
135
|
+
/**
|
|
136
|
+
* APCA-W3 0.1.9 lightness contrast, returned as Lc.
|
|
137
|
+
*
|
|
138
|
+
* Sign carries polarity: positive is dark text on a light background, negative
|
|
139
|
+
* is light text on dark. Callers that only care about legibility take the
|
|
140
|
+
* absolute value; the house bar is |Lc| >= 60 for body text.
|
|
141
|
+
*/
|
|
142
|
+
declare function apcaContrast(text: string, background: string): number;
|
|
143
|
+
interface ContrastCheck {
|
|
144
|
+
text: string;
|
|
145
|
+
background: string;
|
|
146
|
+
/** WCAG 2.x ratio. */
|
|
147
|
+
wcag: number;
|
|
148
|
+
/** APCA Lc, signed. */
|
|
149
|
+
lc: number;
|
|
150
|
+
/** Clears 4.5:1 — WCAG AA for body text. */
|
|
151
|
+
passesAA: boolean;
|
|
152
|
+
/** Clears 3:1 — WCAG AA for large text and non-text UI. */
|
|
153
|
+
passesAALarge: boolean;
|
|
154
|
+
/** Clears |Lc| 60 — the house bar for body text on any surface. */
|
|
155
|
+
passesLc60: boolean;
|
|
156
|
+
/** Human-readable, and the string emitted as a CSS comment beside the pair. */
|
|
157
|
+
note: string;
|
|
158
|
+
}
|
|
159
|
+
/** Measure one text-on-background pair against both models. */
|
|
160
|
+
declare function checkPair(text: string, background: string): ContrastCheck;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Semantic tokens derived from a small set of anchors, with a contrast report.
|
|
164
|
+
*
|
|
165
|
+
* The point of deriving rather than hand-picking is not tidiness. It is that a
|
|
166
|
+
* hand-picked "accessible variant of the accent" is a guess that nobody
|
|
167
|
+
* re-checks when the accent changes. `accentFork` walks the ramp and returns the
|
|
168
|
+
* first step that actually clears the bar on every background it will sit on,
|
|
169
|
+
* and records the measured ratio next to it.
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
interface AccentFork {
|
|
173
|
+
/** The chosen colour. */
|
|
174
|
+
hex: string;
|
|
175
|
+
/** Which ramp step it came from. */
|
|
176
|
+
step: RampStep;
|
|
177
|
+
/** Measurements against every background it was tested on. */
|
|
178
|
+
checks: ContrastCheck[];
|
|
179
|
+
/** Emitted as a CSS comment beside the token, so the ratio is never a guess. */
|
|
180
|
+
note: string;
|
|
181
|
+
/**
|
|
182
|
+
* True when no step cleared both bars and the least-bad was returned. Callers
|
|
183
|
+
* must surface this: a silently-failing accent is the exact defect this
|
|
184
|
+
* function exists to remove.
|
|
185
|
+
*/
|
|
186
|
+
degraded: boolean;
|
|
187
|
+
}
|
|
188
|
+
interface AccentForkOptions {
|
|
189
|
+
/** Minimum WCAG 2.x ratio. Defaults to 4.5 (AA body text). */
|
|
190
|
+
minWcag?: number;
|
|
191
|
+
/** Minimum absolute APCA Lc. Defaults to 60 (house bar for body text). */
|
|
192
|
+
minLc?: number;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The first ramp step that clears both contrast bars against every background.
|
|
196
|
+
*
|
|
197
|
+
* Searched light-to-dark or dark-to-light depending on which direction the
|
|
198
|
+
* backgrounds sit, so the answer is the *closest* passing step rather than the
|
|
199
|
+
* most extreme one — an accent that has been dragged to near-white to pass a
|
|
200
|
+
* checker has stopped being the brand colour.
|
|
201
|
+
*/
|
|
202
|
+
declare function accentFork(rampSteps: Record<RampStep, string>, on: string[], options?: AccentForkOptions): AccentFork;
|
|
203
|
+
interface SemanticInput {
|
|
204
|
+
/** Seed or anchored steps for the accent hue. */
|
|
205
|
+
accent: string | Partial<Record<RampStep, string>>;
|
|
206
|
+
/** Page background, darkest surface first. */
|
|
207
|
+
bgCanvas: string;
|
|
208
|
+
bgBase?: string;
|
|
209
|
+
bgSurface: string;
|
|
210
|
+
bgElevated: string;
|
|
211
|
+
/** Body text ramp. */
|
|
212
|
+
textPrimary: string;
|
|
213
|
+
textSecondary: string;
|
|
214
|
+
textMuted: string;
|
|
215
|
+
/** Hairlines. */
|
|
216
|
+
borderSubtle: string;
|
|
217
|
+
borderStrong: string;
|
|
218
|
+
/** Status hues. */
|
|
219
|
+
success: string;
|
|
220
|
+
warning: string;
|
|
221
|
+
danger: string;
|
|
222
|
+
info: string;
|
|
223
|
+
/** Overrides written verbatim, for values craft cannot derive (rgba, etc). */
|
|
224
|
+
overrides?: Record<string, string>;
|
|
225
|
+
}
|
|
226
|
+
interface ContrastReport {
|
|
227
|
+
checks: ContrastCheck[];
|
|
228
|
+
/** Pairs that fail WCAG AA or the Lc60 house bar. */
|
|
229
|
+
failures: ContrastCheck[];
|
|
230
|
+
accent: AccentFork;
|
|
231
|
+
}
|
|
232
|
+
interface SemanticResult {
|
|
233
|
+
/** The 22 `--craft-*` semantic tokens, name to value. */
|
|
234
|
+
tokens: Record<string, string>;
|
|
235
|
+
/** Per-token provenance comment, emitted beside the declaration. */
|
|
236
|
+
notes: Record<string, string>;
|
|
237
|
+
report: ContrastReport;
|
|
238
|
+
/** The accent ramp, for callers that want the raw steps too. */
|
|
239
|
+
accentRamp: Record<RampStep, string>;
|
|
240
|
+
}
|
|
241
|
+
/** Derive the semantic layer, measuring every text-on-surface pair as it goes. */
|
|
242
|
+
declare function semantic(input: SemanticInput): SemanticResult;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Motion numbers. One set, shared by CSS and by every JS motion library.
|
|
246
|
+
*
|
|
247
|
+
* The divergence this replaces was not theoretical: `packages/ui/tokens/
|
|
248
|
+
* component.css` shipped `--ease-emphasized` byte-identical to `--ease-standard`
|
|
249
|
+
* (so "emphasized" emphasised nothing), and `--ease-exit` as
|
|
250
|
+
* `cubic-bezier(0.4, 0, 1, 1)` — an ease-*in* curve, which starts slow and
|
|
251
|
+
* accelerates out of view. That is the one curve never to use for UI: it makes a
|
|
252
|
+
* dismissal feel like it is being dragged away from the user.
|
|
253
|
+
*
|
|
254
|
+
* `EASE_TUPLE` holds the same four control points as `EASE`, so a Framer Motion
|
|
255
|
+
* or GSAP transition and a CSS transition on the same element cannot drift.
|
|
256
|
+
*/
|
|
257
|
+
/** CSS `cubic-bezier()` strings. */
|
|
258
|
+
declare const EASE: {
|
|
259
|
+
/** Entrances and anything arriving. Decelerates hard into place. */
|
|
260
|
+
readonly out: "cubic-bezier(0.23, 1, 0.32, 1)";
|
|
261
|
+
/** Moves that start and end on screen: a panel resizing, a value morphing. */
|
|
262
|
+
readonly inOut: "cubic-bezier(0.77, 0, 0.175, 1)";
|
|
263
|
+
/** Drawers and sheets — a longer settle than a dropdown wants. */
|
|
264
|
+
readonly drawer: "cubic-bezier(0.32, 0.72, 0, 1)";
|
|
265
|
+
/** Scroll-triggered reveals. Gentler than `out`, which snaps at this length. */
|
|
266
|
+
readonly reveal: "cubic-bezier(0.22, 0.61, 0.36, 1)";
|
|
267
|
+
};
|
|
268
|
+
type EaseName = keyof typeof EASE;
|
|
269
|
+
/** The identical control points, for JS motion libraries. */
|
|
270
|
+
declare const EASE_TUPLE: Readonly<Record<EaseName, readonly [number, number, number, number]>>;
|
|
271
|
+
/**
|
|
272
|
+
* Durations in milliseconds, scaled to distance travelled and to how often the
|
|
273
|
+
* user will sit through it. Nothing in UI exceeds 300ms except a drawer and a
|
|
274
|
+
* one-off reveal.
|
|
275
|
+
*/
|
|
276
|
+
declare const DURATION_MS: {
|
|
277
|
+
readonly press: 120;
|
|
278
|
+
readonly tooltip: 150;
|
|
279
|
+
readonly dropdown: 200;
|
|
280
|
+
readonly modal: 300;
|
|
281
|
+
readonly drawer: 400;
|
|
282
|
+
readonly reveal: 500;
|
|
283
|
+
readonly stagger: 60;
|
|
284
|
+
};
|
|
285
|
+
type DurationName = keyof typeof DURATION_MS;
|
|
286
|
+
/** The same durations in seconds, for libraries that take seconds. */
|
|
287
|
+
declare const DURATION_S: Readonly<Record<DurationName, number>>;
|
|
288
|
+
/** Spring parameters for libraries that prefer physics to a curve. */
|
|
289
|
+
declare const SPRING: Readonly<{
|
|
290
|
+
damping: 0.5;
|
|
291
|
+
stiffness: 0.2;
|
|
292
|
+
}>;
|
|
293
|
+
/** Transform scales. `press` is the tactile dip; `enter` the arrival start. */
|
|
294
|
+
declare const SCALE: Readonly<{
|
|
295
|
+
press: 0.97;
|
|
296
|
+
enter: 0.95;
|
|
297
|
+
}>;
|
|
298
|
+
/**
|
|
299
|
+
* An exit is always faster than the matching entrance.
|
|
300
|
+
*
|
|
301
|
+
* Arriving content is asking to be read, so it takes its time. Leaving content
|
|
302
|
+
* has already been dealt with, and lingering reads as lag.
|
|
303
|
+
*/
|
|
304
|
+
declare const EXIT_RATIO = 0.75;
|
|
305
|
+
/** Milliseconds an exit should take, given its entrance duration. */
|
|
306
|
+
declare function exitDuration(enterMs: number): number;
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Whether a thing should animate at all.
|
|
310
|
+
*
|
|
311
|
+
* The most common motion defect is not a wrong curve, it is animating something
|
|
312
|
+
* that should have been instant. An animation the user triggers a hundred times
|
|
313
|
+
* a day stops being delight and becomes latency they cannot skip, and a
|
|
314
|
+
* keyboard-driven interaction animating at all fights the person using it.
|
|
315
|
+
*/
|
|
316
|
+
type MotionKind = "entrance" | "exit" | "state" | "reveal" | "signature";
|
|
317
|
+
interface MotionDecision {
|
|
318
|
+
animate: boolean;
|
|
319
|
+
/** Suggested duration in ms when `animate`, else 0. */
|
|
320
|
+
durationMs: number;
|
|
321
|
+
/** Why — surfaced in review, so a "no" is arguable rather than mysterious. */
|
|
322
|
+
reason: string;
|
|
323
|
+
}
|
|
324
|
+
interface ShouldAnimateInput {
|
|
325
|
+
/** Roughly how often one user triggers this in a day. */
|
|
326
|
+
usesPerDay: number;
|
|
327
|
+
/** What sets it off. */
|
|
328
|
+
trigger: "pointer" | "keyboard" | "scroll" | "load" | "system";
|
|
329
|
+
kind: MotionKind;
|
|
330
|
+
/** Entrance duration in ms, when the caller has one in mind. */
|
|
331
|
+
enterMs?: number;
|
|
332
|
+
}
|
|
333
|
+
/** Decide, and say why. */
|
|
334
|
+
declare function shouldAnimate(input: ShouldAnimateInput): MotionDecision;
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Fluid space scale and section rhythm, after Utopia.
|
|
338
|
+
*
|
|
339
|
+
* Space uses the same `clamp()` maths as type so the two stay in proportion as
|
|
340
|
+
* the viewport changes. A fixed space scale next to a fluid type scale drifts:
|
|
341
|
+
* padding that framed a heading at 360px swamps it at 1280px.
|
|
342
|
+
*/
|
|
343
|
+
/** Multiples of the base step, small to large. */
|
|
344
|
+
declare const SPACE_STEPS: {
|
|
345
|
+
readonly "3xs": 0.25;
|
|
346
|
+
readonly "2xs": 0.5;
|
|
347
|
+
readonly xs: 0.75;
|
|
348
|
+
readonly s: 1;
|
|
349
|
+
readonly m: 1.5;
|
|
350
|
+
readonly l: 2;
|
|
351
|
+
readonly xl: 3;
|
|
352
|
+
readonly "2xl": 4;
|
|
353
|
+
readonly "3xl": 6;
|
|
354
|
+
};
|
|
355
|
+
type SpaceStep = keyof typeof SPACE_STEPS;
|
|
356
|
+
interface FluidSpaceOptions {
|
|
357
|
+
minViewport?: number;
|
|
358
|
+
maxViewport?: number;
|
|
359
|
+
/** Base space in px at the small viewport. */
|
|
360
|
+
minBase?: number;
|
|
361
|
+
/** Base space in px at the large viewport. */
|
|
362
|
+
maxBase?: number;
|
|
363
|
+
rootPx?: number;
|
|
364
|
+
/** Literal values written through unchanged, for steps a site already ships. */
|
|
365
|
+
pin?: Partial<Record<string, string>>;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Build the space scale.
|
|
369
|
+
*
|
|
370
|
+
* One-up pairs (`s-m`, `m-l`, ...) are emitted alongside the steps because the
|
|
371
|
+
* common real need is a gap that grows *faster* than the scale — a stack that is
|
|
372
|
+
* `s` on mobile and `m` on desktop. Without the pairs that gets hand-written as
|
|
373
|
+
* a bespoke clamp every time, which is how a space scale stops being a scale.
|
|
374
|
+
*/
|
|
375
|
+
declare function fluidSpace(options?: FluidSpaceOptions): Record<string, string>;
|
|
376
|
+
interface SectionRhythmOptions {
|
|
377
|
+
/** Vertical padding for a tight section. Literal CSS, or px min/max. */
|
|
378
|
+
sm?: string | {
|
|
379
|
+
min: number;
|
|
380
|
+
max: number;
|
|
381
|
+
};
|
|
382
|
+
md?: string | {
|
|
383
|
+
min: number;
|
|
384
|
+
max: number;
|
|
385
|
+
};
|
|
386
|
+
lg?: string | {
|
|
387
|
+
min: number;
|
|
388
|
+
max: number;
|
|
389
|
+
};
|
|
390
|
+
minViewport?: number;
|
|
391
|
+
maxViewport?: number;
|
|
392
|
+
rootPx?: number;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Section vertical rhythm: three sizes, not five.
|
|
396
|
+
*
|
|
397
|
+
* The shared layer previously shipped five (`xs` through `xl`) with zero
|
|
398
|
+
* consumers, which is the signature of a scale invented ahead of a need. Three
|
|
399
|
+
* is what section layouts actually reach for — tight, default, and the one that
|
|
400
|
+
* gives a section room to breathe.
|
|
401
|
+
*/
|
|
402
|
+
declare function sectionRhythm(options?: SectionRhythmOptions): Record<string, string>;
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Fluid type scale, after Utopia (https://utopia.fyi).
|
|
406
|
+
*
|
|
407
|
+
* Every step is a `clamp()` interpolating between a size at the small viewport
|
|
408
|
+
* and a size at the large one, so there are no breakpoint jumps in type.
|
|
409
|
+
*
|
|
410
|
+
* The interpolation term is `rem + vw`, never bare `vw`. A bare-`vw` font size
|
|
411
|
+
* ignores the user's browser text-size setting entirely, which fails WCAG 1.4.4
|
|
412
|
+
* — text must survive 200% zoom. The `rem` component is what keeps zoom working.
|
|
413
|
+
*/
|
|
414
|
+
/** Steps emitted, small to large. */
|
|
415
|
+
declare const TYPE_STEPS: readonly [-2, -1, 0, 1, 2, 3, 4, 5];
|
|
416
|
+
type TypeStep = (typeof TYPE_STEPS)[number];
|
|
417
|
+
interface FluidTypeOptions {
|
|
418
|
+
/** Viewport width in px at which the minimum size applies. */
|
|
419
|
+
minViewport?: number;
|
|
420
|
+
/** Viewport width in px at which the maximum size applies. */
|
|
421
|
+
maxViewport?: number;
|
|
422
|
+
/** Step-0 size in px at the small viewport. */
|
|
423
|
+
minBase?: number;
|
|
424
|
+
/** Step-0 size in px at the large viewport. */
|
|
425
|
+
maxBase?: number;
|
|
426
|
+
/** Ratio between steps at the small viewport. */
|
|
427
|
+
minRatio?: number;
|
|
428
|
+
/** Ratio between steps at the large viewport. */
|
|
429
|
+
maxRatio?: number;
|
|
430
|
+
/**
|
|
431
|
+
* Literal values for specific steps, written through unchanged.
|
|
432
|
+
*
|
|
433
|
+
* This is how a live site adopts the scale without its type resizing: pin the
|
|
434
|
+
* steps it already ships, and only the steps it never defined are generated.
|
|
435
|
+
*/
|
|
436
|
+
pin?: Partial<Record<TypeStep, string>>;
|
|
437
|
+
/** Root font size in px. Defaults to 16. */
|
|
438
|
+
rootPx?: number;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* One `clamp()` interpolating a px range across a viewport range.
|
|
442
|
+
*
|
|
443
|
+
* Exported because the space scale needs exactly the same maths.
|
|
444
|
+
*/
|
|
445
|
+
declare function fluidClamp(minPx: number, maxPx: number, minViewport: number, maxViewport: number, rootPx?: number): string;
|
|
446
|
+
interface FluidTypeResult {
|
|
447
|
+
/** `--craft-step--2` .. `--craft-step-5`, name to value. */
|
|
448
|
+
tokens: Record<string, string>;
|
|
449
|
+
/** Which steps were pinned rather than generated. */
|
|
450
|
+
pinned: TypeStep[];
|
|
451
|
+
}
|
|
452
|
+
/** Build the fluid type scale. */
|
|
453
|
+
declare function fluidType(options?: FluidTypeOptions): FluidTypeResult;
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Emit plain CSS custom properties.
|
|
457
|
+
*
|
|
458
|
+
* Plain custom properties, not a Tailwind theme and not a JS object, because the
|
|
459
|
+
* consumers do not agree on a styling engine: the monorepo is on Tailwind v4,
|
|
460
|
+
* one client site is on Tailwind v3, and another has no Tailwind at all. Custom
|
|
461
|
+
* properties are the only output all three can read without a build step.
|
|
462
|
+
*/
|
|
463
|
+
|
|
464
|
+
interface EmitCssOptions {
|
|
465
|
+
/** Selector the block is written under. Defaults to `:root`. */
|
|
466
|
+
selector?: string;
|
|
467
|
+
/** Provenance comments, keyed by token name. */
|
|
468
|
+
notes?: Record<string, string>;
|
|
469
|
+
/** Indent for each declaration. Defaults to two spaces. */
|
|
470
|
+
indent?: string;
|
|
471
|
+
}
|
|
472
|
+
/** Render a token map as one CSS rule, with notes as trailing comments. */
|
|
473
|
+
declare function emitCss(tokens: Record<string, string>, options?: EmitCssOptions): string;
|
|
474
|
+
interface CraftTokensInput extends SemanticInput {
|
|
475
|
+
/** Selector for the emitted block. Defaults to `:root`. */
|
|
476
|
+
selector?: string;
|
|
477
|
+
/** Type scale options. Pass `pin` to reproduce a site's existing sizes exactly. */
|
|
478
|
+
type?: FluidTypeOptions;
|
|
479
|
+
/** Space scale options. */
|
|
480
|
+
space?: FluidSpaceOptions;
|
|
481
|
+
/** Section rhythm overrides. */
|
|
482
|
+
section?: Parameters<typeof sectionRhythm>[0];
|
|
483
|
+
}
|
|
484
|
+
interface CraftTokensResult {
|
|
485
|
+
/** The stylesheet text, ready to write to a `.css` file. */
|
|
486
|
+
css: string;
|
|
487
|
+
/** Every token emitted, name to value. */
|
|
488
|
+
tokens: Record<string, string>;
|
|
489
|
+
report: SemanticResult["report"];
|
|
490
|
+
ramps: {
|
|
491
|
+
accent: Record<RampStep, string>;
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/** Motion tokens are fixed house numbers, identical for every consumer. */
|
|
495
|
+
declare function motionTokens(): Record<string, string>;
|
|
496
|
+
/**
|
|
497
|
+
* The whole emitted layer: colour derived from the caller's anchors, motion from
|
|
498
|
+
* the house constants, and a contrast report the caller is expected to read.
|
|
499
|
+
*/
|
|
500
|
+
declare function craftTokens(input: CraftTokensInput): CraftTokensResult;
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Typographic settings that are house habits rather than per-project choices.
|
|
504
|
+
*
|
|
505
|
+
* These numbers were already being written by hand, identically, across separate
|
|
506
|
+
* client repos — `-0.01em` display tracking appears in two codebases that share
|
|
507
|
+
* no code. Systematising them is the point: quality that depends on somebody
|
|
508
|
+
* remembering to type `-0.01em` tracks per-build effort rather than the standard.
|
|
509
|
+
*/
|
|
510
|
+
declare const HOUSE_TYPE: {
|
|
511
|
+
/**
|
|
512
|
+
* Letter-spacing. Display type is set tighter because tracking that reads as
|
|
513
|
+
* neutral at 16px reads as loose at 48px; buttons and eyebrows go the other
|
|
514
|
+
* way, because short all-caps strings need air to stay countable.
|
|
515
|
+
*/
|
|
516
|
+
readonly tracking: {
|
|
517
|
+
readonly display: "-0.01em";
|
|
518
|
+
readonly heading: "-0.005em";
|
|
519
|
+
readonly body: "0em";
|
|
520
|
+
readonly button: "0.12em";
|
|
521
|
+
readonly eyebrow: "0.24em";
|
|
522
|
+
};
|
|
523
|
+
/** Line height. Tighter as type gets larger, since the eye tracks a shorter return. */
|
|
524
|
+
readonly leading: {
|
|
525
|
+
readonly display: "1.05";
|
|
526
|
+
readonly heading: "1.1";
|
|
527
|
+
readonly subheading: "1.3";
|
|
528
|
+
readonly body: "1.6";
|
|
529
|
+
};
|
|
530
|
+
/**
|
|
531
|
+
* Measure, in `ch`. Below ~45 the eye returns too often; above ~75 it loses
|
|
532
|
+
* the line on the way back.
|
|
533
|
+
*/
|
|
534
|
+
readonly measure: {
|
|
535
|
+
readonly narrow: "45ch";
|
|
536
|
+
readonly body: "65ch";
|
|
537
|
+
readonly wide: "75ch";
|
|
538
|
+
};
|
|
539
|
+
};
|
|
540
|
+
/** Emit the type-feature tokens. */
|
|
541
|
+
declare function typeFeatureTokens(): Record<string, string>;
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Row density for data-dense software surfaces.
|
|
545
|
+
*
|
|
546
|
+
* Three densities, all on a 4px grid, following the convention Linear and Stripe
|
|
547
|
+
* both settled on. A back office and a marketing page have genuinely different
|
|
548
|
+
* needs: a table an operator scans for six hours wants 32px rows, and the same
|
|
549
|
+
* component on a client-facing dashboard wants 48px so it does not read as a
|
|
550
|
+
* spreadsheet.
|
|
551
|
+
*
|
|
552
|
+
* Emitted under `[data-density]`, so a shell sets it once and every table below
|
|
553
|
+
* inherits rather than each one taking a prop.
|
|
554
|
+
*/
|
|
555
|
+
declare const DENSITIES: {
|
|
556
|
+
readonly compact: {
|
|
557
|
+
readonly rowPx: 32;
|
|
558
|
+
readonly padXPx: 8;
|
|
559
|
+
readonly padYPx: 4;
|
|
560
|
+
readonly fontPx: 13;
|
|
561
|
+
};
|
|
562
|
+
readonly comfortable: {
|
|
563
|
+
readonly rowPx: 40;
|
|
564
|
+
readonly padXPx: 12;
|
|
565
|
+
readonly padYPx: 8;
|
|
566
|
+
readonly fontPx: 14;
|
|
567
|
+
};
|
|
568
|
+
readonly spacious: {
|
|
569
|
+
readonly rowPx: 48;
|
|
570
|
+
readonly padXPx: 16;
|
|
571
|
+
readonly padYPx: 12;
|
|
572
|
+
readonly fontPx: 15;
|
|
573
|
+
};
|
|
574
|
+
};
|
|
575
|
+
type DensityName = keyof typeof DENSITIES;
|
|
576
|
+
interface DensityCssOptions {
|
|
577
|
+
/** Which density the bare `:root` block carries. Defaults to comfortable. */
|
|
578
|
+
base?: DensityName;
|
|
579
|
+
rootPx?: number;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Emit the density tokens: a default set plus one `[data-density="..."]` block
|
|
583
|
+
* per density.
|
|
584
|
+
*/
|
|
585
|
+
declare function densityCss(options?: DensityCssOptions): string;
|
|
586
|
+
/** The density tokens as a flat map, for callers emitting their own CSS. */
|
|
587
|
+
declare function densityTokens(name: DensityName, rootPx?: number): Record<string, string>;
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Restraint: the budget that separates designed from decorated.
|
|
591
|
+
*
|
|
592
|
+
* Differentiation guards can only tell you a site differs from its siblings.
|
|
593
|
+
* They pass anything ugly, so long as it is distinctively ugly. What they cannot
|
|
594
|
+
* see is the commonest quality failure in practice, which is not sameness but
|
|
595
|
+
* accumulation: eighteen font sizes, six shadows, four competing accent hues,
|
|
596
|
+
* each added reasonably, together reading as noise.
|
|
597
|
+
*
|
|
598
|
+
* Pure over strings. No filesystem, no parser, no dependencies — so a guard, a
|
|
599
|
+
* CI check and an editor can all run it on the same input and agree.
|
|
600
|
+
*/
|
|
601
|
+
interface RestraintBudget {
|
|
602
|
+
maxFontSizes: number;
|
|
603
|
+
maxFontWeights: number;
|
|
604
|
+
/** Excludes the monospace face, which is a functional choice not a stylistic one. */
|
|
605
|
+
maxFontFamilies: number;
|
|
606
|
+
maxRadii: number;
|
|
607
|
+
maxShadows: number;
|
|
608
|
+
/** Accent hues must cluster within this many degrees in OKLCh. */
|
|
609
|
+
accentHueClusterDeg: number;
|
|
610
|
+
/** Longest permitted UI transition/animation, in ms. */
|
|
611
|
+
maxUiDurationMs: number;
|
|
612
|
+
}
|
|
613
|
+
declare const HOUSE_BUDGET: RestraintBudget;
|
|
614
|
+
type RestraintSeverity = "error" | "warning";
|
|
615
|
+
interface RestraintViolation {
|
|
616
|
+
rule: string;
|
|
617
|
+
severity: RestraintSeverity;
|
|
618
|
+
message: string;
|
|
619
|
+
/** The offending values, so the message is actionable rather than a count. */
|
|
620
|
+
found: string[];
|
|
621
|
+
}
|
|
622
|
+
interface RestraintReport {
|
|
623
|
+
ok: boolean;
|
|
624
|
+
violations: RestraintViolation[];
|
|
625
|
+
/** What was counted, for a report that shows headroom as well as failure. */
|
|
626
|
+
counts: Record<string, number>;
|
|
627
|
+
}
|
|
628
|
+
interface CheckRestraintInput {
|
|
629
|
+
/** Concatenated stylesheet text. */
|
|
630
|
+
css: string;
|
|
631
|
+
/** Concatenated markup or component source, for class-level rules. */
|
|
632
|
+
markup?: string;
|
|
633
|
+
budget?: Partial<RestraintBudget>;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Check a stylesheet against the restraint budget.
|
|
637
|
+
*
|
|
638
|
+
* Returns every violation rather than the first, because a budget report is only
|
|
639
|
+
* useful if it shows the whole picture at once.
|
|
640
|
+
*/
|
|
641
|
+
declare function checkRestraint(input: CheckRestraintInput): RestraintReport;
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Tailwind adapters.
|
|
645
|
+
*
|
|
646
|
+
* Two, because the estate is on two major versions and neither is going to move
|
|
647
|
+
* for the other. Both point at the same `--craft-*` custom properties rather
|
|
648
|
+
* than restating values, so a Tailwind utility and a hand-written rule cannot
|
|
649
|
+
* disagree.
|
|
650
|
+
*/
|
|
651
|
+
|
|
652
|
+
interface TailwindPreset {
|
|
653
|
+
theme: {
|
|
654
|
+
extend: Record<string, unknown>;
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* A Tailwind v3 preset, for consumers on the older config format.
|
|
659
|
+
*
|
|
660
|
+
* Every entry is a `var()` reference. That matters: a preset that inlined the
|
|
661
|
+
* computed values would freeze them at build time, and the point of the token
|
|
662
|
+
* layer is that a site can override an anchor without rebuilding the preset.
|
|
663
|
+
*/
|
|
664
|
+
declare function tailwindV3Preset(): TailwindPreset;
|
|
665
|
+
/**
|
|
666
|
+
* The Tailwind v4 `@theme inline` block.
|
|
667
|
+
*
|
|
668
|
+
* `inline` is required rather than optional: without it Tailwind copies the
|
|
669
|
+
* value at build time, so a runtime override of the underlying custom property
|
|
670
|
+
* would not reach the generated utility.
|
|
671
|
+
*/
|
|
672
|
+
declare function tailwindV4Theme(): string;
|
|
673
|
+
/** Density names, re-exported so a consumer can type its shell prop. */
|
|
674
|
+
declare const DENSITY_NAMES: DensityName[];
|
|
675
|
+
|
|
676
|
+
export { type AccentFork, type AccentForkOptions, type CheckRestraintInput, type ContrastCheck, type ContrastReport, type CraftTokensInput, type CraftTokensResult, DENSITIES, DENSITY_NAMES, DURATION_MS, DURATION_S, type DensityCssOptions, type DensityName, type DurationName, EASE, EASE_TUPLE, EXIT_RATIO, type EaseName, type EmitCssOptions, type FluidSpaceOptions, type FluidTypeOptions, type FluidTypeResult, type Gamut, HOUSE_BUDGET, HOUSE_TYPE, LIGHTNESS_CURVE, type MotionDecision, type MotionKind, type Oklch, type OklchToHexOptions, RAMP_STEPS, type Ramp, type RampFromAnchorsOptions, type RampOptions, type RampStep, type RestraintBudget, type RestraintReport, type RestraintSeverity, type RestraintViolation, type Rgb, SCALE, SPACE_STEPS, SPRING, type SectionRhythmOptions, type SemanticInput, type SemanticResult, type ShouldAnimateInput, type SpaceStep, TYPE_STEPS, type TailwindPreset, type TypeStep, accentFork, apcaContrast, checkPair, checkRestraint, craftTokens, deltaEOk, deltaEOkHex, densityCss, densityTokens, emitCss, exitDuration, fluidClamp, fluidSpace, fluidType, formatHex, hexToOklch, inP3Gamut, inSrgbGamut, motionTokens, oklchToHex, oklchToRgb, parseHex, ramp, rampFromAnchors, sectionRhythm, semantic, shouldAnimate, tailwindV3Preset, tailwindV4Theme, toGamut, typeFeatureTokens, wcagContrast };
|