@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.
@@ -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 };