@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/dist/index.js ADDED
@@ -0,0 +1,1128 @@
1
+ // src/color/oklch.ts
2
+ var clamp01 = (n) => n < 0 ? 0 : n > 1 ? 1 : n;
3
+ function srgbToLinear(c) {
4
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
5
+ }
6
+ function linearToSrgb(c) {
7
+ return c <= 31308e-7 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
8
+ }
9
+ function parseHex(hex) {
10
+ const raw = hex.trim().replace(/^#/, "");
11
+ const expanded = raw.length === 3 || raw.length === 4 ? raw.slice(0, 3).split("").map((ch) => ch + ch).join("") : raw.slice(0, 6);
12
+ if (!/^[0-9a-fA-F]{6}$/.test(expanded)) {
13
+ throw new Error(`craft: not a hex colour: ${JSON.stringify(hex)}`);
14
+ }
15
+ return {
16
+ r: parseInt(expanded.slice(0, 2), 16) / 255,
17
+ g: parseInt(expanded.slice(2, 4), 16) / 255,
18
+ b: parseInt(expanded.slice(4, 6), 16) / 255
19
+ };
20
+ }
21
+ function formatHex({ r, g, b }) {
22
+ const byte = (n) => Math.round(clamp01(n) * 255).toString(16).padStart(2, "0");
23
+ return `#${byte(r)}${byte(g)}${byte(b)}`;
24
+ }
25
+ function linearRgbToOklab(r, g, b) {
26
+ const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
27
+ const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
28
+ const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
29
+ const l_ = Math.cbrt(l);
30
+ const m_ = Math.cbrt(m);
31
+ const s_ = Math.cbrt(s);
32
+ return [
33
+ 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
34
+ 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
35
+ 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_
36
+ ];
37
+ }
38
+ function oklabToLinearRgb(L, A, B) {
39
+ const l_ = L + 0.3963377774 * A + 0.2158037573 * B;
40
+ const m_ = L - 0.1055613458 * A - 0.0638541728 * B;
41
+ const s_ = L - 0.0894841775 * A - 1.291485548 * B;
42
+ const l = l_ * l_ * l_;
43
+ const m = m_ * m_ * m_;
44
+ const s = s_ * s_ * s_;
45
+ return [
46
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
47
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
48
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
49
+ ];
50
+ }
51
+ function oklabToXyz(L, A, B) {
52
+ const l_ = L + 0.3963377774 * A + 0.2158037573 * B;
53
+ const m_ = L - 0.1055613458 * A - 0.0638541728 * B;
54
+ const s_ = L - 0.0894841775 * A - 1.291485548 * B;
55
+ const l = l_ * l_ * l_;
56
+ const m = m_ * m_ * m_;
57
+ const s = s_ * s_ * s_;
58
+ return [
59
+ 1.2270138511 * l - 0.5577999807 * m + 0.281256149 * s,
60
+ -0.0405801784 * l + 1.1122568696 * m - 0.0716766787 * s,
61
+ -0.0763812845 * l - 0.4214819784 * m + 1.5861632204 * s
62
+ ];
63
+ }
64
+ function xyzToLinearP3(x, y, z) {
65
+ return [
66
+ 2.4934969119 * x - 0.9313836179 * y - 0.4027107845 * z,
67
+ -0.8294889696 * x + 1.7626640603 * y + 0.0236246858 * z,
68
+ 0.0358458302 * x - 0.0761723893 * y + 0.956884524 * z
69
+ ];
70
+ }
71
+ var RAD = 180 / Math.PI;
72
+ function hexToOklch(hex) {
73
+ const { r, g, b } = parseHex(hex);
74
+ const [L, A, B] = linearRgbToOklab(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b));
75
+ const c = Math.sqrt(A * A + B * B);
76
+ const h = c < 1e-7 ? 0 : (Math.atan2(B, A) * RAD % 360 + 360) % 360;
77
+ return { l: L, c, h };
78
+ }
79
+ function toAb({ c, h }) {
80
+ const rad = h / RAD;
81
+ return [c * Math.cos(rad), c * Math.sin(rad)];
82
+ }
83
+ function inSrgbGamut(colour, epsilon = 1e-5) {
84
+ const [A, B] = toAb(colour);
85
+ const rgb = oklabToLinearRgb(colour.l, A, B);
86
+ return rgb.every((ch) => ch >= -epsilon && ch <= 1 + epsilon);
87
+ }
88
+ function inP3Gamut(colour, epsilon = 1e-5) {
89
+ const [A, B] = toAb(colour);
90
+ const [x, y, z] = oklabToXyz(colour.l, A, B);
91
+ const rgb = xyzToLinearP3(x, y, z);
92
+ return rgb.every((ch) => ch >= -epsilon && ch <= 1 + epsilon);
93
+ }
94
+ function oklchToRgb(colour) {
95
+ const [A, B] = toAb(colour);
96
+ const [r, g, b] = oklabToLinearRgb(colour.l, A, B);
97
+ return { r: linearToSrgb(r), g: linearToSrgb(g), b: linearToSrgb(b) };
98
+ }
99
+ function deltaEOk(a, b) {
100
+ const [aA, aB] = toAb(a);
101
+ const [bA, bB] = toAb(b);
102
+ const dL = a.l - b.l;
103
+ const dA = aA - bA;
104
+ const dB = aB - bB;
105
+ return Math.sqrt(dL * dL + dA * dA + dB * dB);
106
+ }
107
+ function deltaEOkHex(a, b) {
108
+ return deltaEOk(hexToOklch(a), hexToOklch(b));
109
+ }
110
+
111
+ // src/color/gamut.ts
112
+ function oklchToHex(colour, options = {}) {
113
+ const gamut = options.gamut ?? "srgb";
114
+ return formatHex(oklchToRgb(toGamut(colour, gamut)));
115
+ }
116
+ function toGamut(colour, gamut = "srgb") {
117
+ const fits = gamut === "p3" ? inP3Gamut : inSrgbGamut;
118
+ if (colour.l <= 0) return { l: 0, c: 0, h: colour.h };
119
+ if (colour.l >= 1) return { l: 1, c: 0, h: colour.h };
120
+ if (fits(colour)) return colour;
121
+ let lo = 0;
122
+ let hi = colour.c;
123
+ for (let i = 0; i < 25 && hi - lo > 1e-6; i += 1) {
124
+ const mid = (lo + hi) / 2;
125
+ if (fits({ ...colour, c: mid })) lo = mid;
126
+ else hi = mid;
127
+ }
128
+ return { ...colour, c: lo };
129
+ }
130
+
131
+ // src/color/ramp.ts
132
+ var RAMP_STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
133
+ var LIGHTNESS_CURVE = Object.freeze({
134
+ 50: 0.971,
135
+ 100: 0.936,
136
+ 200: 0.885,
137
+ 300: 0.828,
138
+ 400: 0.746,
139
+ 500: 0.658,
140
+ 600: 0.586,
141
+ 700: 0.51,
142
+ 800: 0.432,
143
+ 900: 0.352,
144
+ 950: 0.253
145
+ });
146
+ function chromaEnvelope(lightness) {
147
+ return 4 * lightness * (1 - lightness);
148
+ }
149
+ function warpLightness(curveValue, anchorCurve, anchorActual) {
150
+ if (anchorCurve <= 0 || anchorCurve >= 1) return curveValue;
151
+ if (anchorActual <= 0 || anchorActual >= 1) return curveValue;
152
+ return curveValue >= anchorCurve ? anchorActual + (curveValue - anchorCurve) * (1 - anchorActual) / (1 - anchorCurve) : anchorActual * curveValue / anchorCurve;
153
+ }
154
+ function nearestStep(lightness) {
155
+ let best = RAMP_STEPS[0];
156
+ let bestDistance = Infinity;
157
+ for (const step of RAMP_STEPS) {
158
+ const distance = Math.abs(LIGHTNESS_CURVE[step] - lightness);
159
+ if (distance < bestDistance) {
160
+ bestDistance = distance;
161
+ best = step;
162
+ }
163
+ }
164
+ return best;
165
+ }
166
+ function ramp(seed, options = {}) {
167
+ const seedOklch = hexToOklch(seed);
168
+ const anchor = options.step ?? nearestStep(seedOklch.l);
169
+ const gamut = options.gamut ?? "srgb";
170
+ const anchorEnvelope = chromaEnvelope(seedOklch.l);
171
+ const out = {};
172
+ for (const step of RAMP_STEPS) {
173
+ if (step === anchor) {
174
+ out[step] = seed;
175
+ continue;
176
+ }
177
+ const lightness = warpLightness(LIGHTNESS_CURVE[step], LIGHTNESS_CURVE[anchor], seedOklch.l);
178
+ const scale = anchorEnvelope === 0 ? 0 : chromaEnvelope(lightness) / anchorEnvelope;
179
+ out[step] = formatHex(
180
+ oklchToRgb(toGamut({ l: lightness, c: seedOklch.c * scale, h: seedOklch.h }, gamut))
181
+ );
182
+ }
183
+ return { ramp: out, anchor, seed: seedOklch };
184
+ }
185
+ function hueDelta(from, to) {
186
+ return (to - from + 540) % 360 - 180;
187
+ }
188
+ function rampFromAnchors(anchors, options = {}) {
189
+ const gamut = options.gamut ?? "srgb";
190
+ const given = RAMP_STEPS.filter((step) => typeof anchors[step] === "string");
191
+ if (given.length === 0) {
192
+ throw new Error("craft: rampFromAnchors needs at least one anchor");
193
+ }
194
+ const parsed = /* @__PURE__ */ new Map();
195
+ for (const step of given) parsed.set(step, hexToOklch(anchors[step]));
196
+ const out = {};
197
+ for (const step of RAMP_STEPS) {
198
+ const supplied = anchors[step];
199
+ if (typeof supplied === "string") {
200
+ out[step] = supplied;
201
+ continue;
202
+ }
203
+ const below = [...given].reverse().find((s) => s < step);
204
+ const above = given.find((s) => s > step);
205
+ let colour;
206
+ if (below !== void 0 && above !== void 0) {
207
+ const lo = parsed.get(below);
208
+ const hi = parsed.get(above);
209
+ const span = LIGHTNESS_CURVE[below] - LIGHTNESS_CURVE[above];
210
+ const t = span === 0 ? 0.5 : (LIGHTNESS_CURVE[below] - LIGHTNESS_CURVE[step]) / span;
211
+ colour = {
212
+ // Between two anchors, lightness runs between what those anchors
213
+ // actually are, not what the curve wishes they were. Anchors are the
214
+ // shipped truth; the curve only sets spacing.
215
+ l: lo.l + (hi.l - lo.l) * t,
216
+ c: lo.c + (hi.c - lo.c) * t,
217
+ h: ((lo.h + hueDelta(lo.h, hi.h) * t) % 360 + 360) % 360
218
+ };
219
+ } else {
220
+ const nearest = below ?? above;
221
+ const base = parsed.get(nearest);
222
+ const lightness = warpLightness(
223
+ LIGHTNESS_CURVE[step],
224
+ LIGHTNESS_CURVE[nearest],
225
+ base.l
226
+ );
227
+ const baseEnvelope = chromaEnvelope(base.l);
228
+ const scale = baseEnvelope === 0 ? 0 : chromaEnvelope(lightness) / baseEnvelope;
229
+ colour = { l: lightness, c: base.c * scale, h: base.h };
230
+ }
231
+ out[step] = formatHex(oklchToRgb(toGamut(colour, gamut)));
232
+ }
233
+ return out;
234
+ }
235
+
236
+ // src/color/contrast.ts
237
+ function relativeLuminance(hex) {
238
+ const { r, g, b } = parseHex(hex);
239
+ const channel = (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
240
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
241
+ }
242
+ function wcagContrast(a, b) {
243
+ const la = relativeLuminance(a);
244
+ const lb = relativeLuminance(b);
245
+ const lighter = Math.max(la, lb);
246
+ const darker = Math.min(la, lb);
247
+ return (lighter + 0.05) / (darker + 0.05);
248
+ }
249
+ var APCA = {
250
+ mainTRC: 2.4,
251
+ Rco: 0.2126729,
252
+ Gco: 0.7151522,
253
+ Bco: 0.072175,
254
+ normBG: 0.56,
255
+ normTXT: 0.57,
256
+ revTXT: 0.62,
257
+ revBG: 0.65,
258
+ blkThrs: 0.022,
259
+ blkClmp: 1.414,
260
+ scaleBoW: 1.14,
261
+ scaleWoB: 1.14,
262
+ loBoWoffset: 0.027,
263
+ loWoBoffset: 0.027,
264
+ deltaYmin: 5e-4,
265
+ loClip: 0.1
266
+ };
267
+ function apcaLuminance(hex) {
268
+ const { r, g, b } = parseHex(hex);
269
+ return APCA.Rco * Math.pow(r, APCA.mainTRC) + APCA.Gco * Math.pow(g, APCA.mainTRC) + APCA.Bco * Math.pow(b, APCA.mainTRC);
270
+ }
271
+ function softClamp(y) {
272
+ return y < APCA.blkThrs ? y + Math.pow(APCA.blkThrs - y, APCA.blkClmp) : y;
273
+ }
274
+ function apcaContrast(text, background) {
275
+ const yTxt = softClamp(apcaLuminance(text));
276
+ const yBg = softClamp(apcaLuminance(background));
277
+ if (Math.abs(yBg - yTxt) < APCA.deltaYmin) return 0;
278
+ let output;
279
+ if (yBg > yTxt) {
280
+ const sapc = (Math.pow(yBg, APCA.normBG) - Math.pow(yTxt, APCA.normTXT)) * APCA.scaleBoW;
281
+ output = sapc < APCA.loClip ? 0 : sapc - APCA.loBoWoffset;
282
+ } else {
283
+ const sapc = (Math.pow(yBg, APCA.revBG) - Math.pow(yTxt, APCA.revTXT)) * APCA.scaleWoB;
284
+ output = sapc > -APCA.loClip ? 0 : sapc + APCA.loWoBoffset;
285
+ }
286
+ return output * 100;
287
+ }
288
+ function checkPair(text, background) {
289
+ const wcag = wcagContrast(text, background);
290
+ const lc = apcaContrast(text, background);
291
+ return {
292
+ text,
293
+ background,
294
+ wcag,
295
+ lc,
296
+ passesAA: wcag >= 4.5,
297
+ passesAALarge: wcag >= 3,
298
+ passesLc60: Math.abs(lc) >= 60,
299
+ note: `${text} on ${background}: ${wcag.toFixed(2)}:1, Lc ${lc.toFixed(1)}`
300
+ };
301
+ }
302
+
303
+ // src/color/semantic.ts
304
+ function accentFork(rampSteps, on, options = {}) {
305
+ const minWcag = options.minWcag ?? 4.5;
306
+ const minLc = options.minLc ?? 60;
307
+ if (on.length === 0) throw new Error("craft: accentFork needs at least one background");
308
+ const meanBackgroundLuminance = on.reduce((sum, bg) => sum + checkPair("#ffffff", bg).wcag, 0) / on.length;
309
+ const darkBackgrounds = meanBackgroundLuminance > 4.5;
310
+ const order = darkBackgrounds ? [...RAMP_STEPS].reverse() : [...RAMP_STEPS];
311
+ let best = null;
312
+ for (const step of order) {
313
+ const hex = rampSteps[step];
314
+ const checks = on.map((bg) => checkPair(hex, bg));
315
+ const clears = checks.every((c) => c.wcag >= minWcag && Math.abs(c.lc) >= minLc);
316
+ if (clears) {
317
+ return {
318
+ hex,
319
+ step,
320
+ checks,
321
+ note: checks.map((c) => c.note).join("; "),
322
+ degraded: false
323
+ };
324
+ }
325
+ const margin = Math.min(
326
+ ...checks.map((c) => Math.min(c.wcag / minWcag, Math.abs(c.lc) / minLc))
327
+ );
328
+ if (best === null || margin > best.margin) best = { step, checks, margin };
329
+ }
330
+ const fallback = best;
331
+ return {
332
+ hex: rampSteps[fallback.step],
333
+ step: fallback.step,
334
+ checks: fallback.checks,
335
+ note: `NO STEP CLEARS ${minWcag}:1 + Lc ${minLc} \u2014 closest: ${fallback.checks.map((c) => c.note).join("; ")}`,
336
+ degraded: true
337
+ };
338
+ }
339
+ function semantic(input) {
340
+ const accentRamp = typeof input.accent === "string" ? ramp(input.accent).ramp : rampFromAnchors(input.accent);
341
+ const surfaces = [input.bgCanvas, input.bgSurface, input.bgElevated];
342
+ const fork = accentFork(accentRamp, surfaces);
343
+ const bgBase = input.bgBase ?? input.bgCanvas;
344
+ const tokens = {
345
+ "--craft-bg-canvas": input.bgCanvas,
346
+ "--craft-bg-base": bgBase,
347
+ "--craft-bg-surface": input.bgSurface,
348
+ "--craft-bg-elevated": input.bgElevated,
349
+ "--craft-text-primary": input.textPrimary,
350
+ "--craft-text-secondary": input.textSecondary,
351
+ "--craft-text-muted": input.textMuted,
352
+ "--craft-text-on-accent": accentRamp[50],
353
+ "--craft-border-subtle": input.borderSubtle,
354
+ "--craft-border-strong": input.borderStrong,
355
+ "--craft-accent": accentRamp[500],
356
+ "--craft-accent-hover": accentRamp[600],
357
+ "--craft-accent-soft": accentRamp[900],
358
+ "--craft-accent-text": fork.hex,
359
+ "--craft-action-primary": accentRamp[500],
360
+ "--craft-action-primary-hover": accentRamp[600],
361
+ "--craft-focus-ring": fork.hex,
362
+ "--craft-success": input.success,
363
+ "--craft-warning": input.warning,
364
+ "--craft-danger": input.danger,
365
+ "--craft-info": input.info,
366
+ "--craft-text-on-warning": checkPair("#111111", input.warning).wcag >= 4.5 ? "#111111" : "#ffffff"
367
+ };
368
+ const notes = {
369
+ "--craft-accent-text": `${fork.note} (ramp step ${fork.step})`,
370
+ "--craft-focus-ring": `derived from accent step ${fork.step}`,
371
+ "--craft-text-on-warning": checkPair(
372
+ tokens["--craft-text-on-warning"],
373
+ input.warning
374
+ ).note
375
+ };
376
+ for (const [name, value] of Object.entries(input.overrides ?? {})) {
377
+ tokens[name] = value;
378
+ notes[name] = "override: supplied verbatim, not derived";
379
+ }
380
+ const textTokens = ["--craft-text-primary", "--craft-text-secondary", "--craft-text-muted"];
381
+ const checks = [];
382
+ for (const name of textTokens) {
383
+ const value = tokens[name];
384
+ if (!value.startsWith("#")) continue;
385
+ for (const surface of surfaces) {
386
+ if (!surface.startsWith("#")) continue;
387
+ checks.push(checkPair(value, surface));
388
+ }
389
+ }
390
+ checks.push(...fork.checks);
391
+ return {
392
+ tokens,
393
+ notes,
394
+ accentRamp,
395
+ report: {
396
+ checks,
397
+ failures: checks.filter((c) => !c.passesAA || !c.passesLc60),
398
+ accent: fork
399
+ }
400
+ };
401
+ }
402
+
403
+ // src/motion/tokens.ts
404
+ var EASE = {
405
+ /** Entrances and anything arriving. Decelerates hard into place. */
406
+ out: "cubic-bezier(0.23, 1, 0.32, 1)",
407
+ /** Moves that start and end on screen: a panel resizing, a value morphing. */
408
+ inOut: "cubic-bezier(0.77, 0, 0.175, 1)",
409
+ /** Drawers and sheets — a longer settle than a dropdown wants. */
410
+ drawer: "cubic-bezier(0.32, 0.72, 0, 1)",
411
+ /** Scroll-triggered reveals. Gentler than `out`, which snaps at this length. */
412
+ reveal: "cubic-bezier(0.22, 0.61, 0.36, 1)"
413
+ };
414
+ var EASE_TUPLE = Object.freeze({
415
+ out: [0.23, 1, 0.32, 1],
416
+ inOut: [0.77, 0, 0.175, 1],
417
+ drawer: [0.32, 0.72, 0, 1],
418
+ reveal: [0.22, 0.61, 0.36, 1]
419
+ });
420
+ var DURATION_MS = {
421
+ press: 120,
422
+ tooltip: 150,
423
+ dropdown: 200,
424
+ modal: 300,
425
+ drawer: 400,
426
+ reveal: 500,
427
+ stagger: 60
428
+ };
429
+ var DURATION_S = Object.freeze(
430
+ Object.fromEntries(
431
+ Object.entries(DURATION_MS).map(([name, ms]) => [name, ms / 1e3])
432
+ )
433
+ );
434
+ var SPRING = Object.freeze({ damping: 0.5, stiffness: 0.2 });
435
+ var SCALE = Object.freeze({ press: 0.97, enter: 0.95 });
436
+ var EXIT_RATIO = 0.75;
437
+ function exitDuration(enterMs) {
438
+ return Math.round(enterMs * EXIT_RATIO);
439
+ }
440
+
441
+ // src/motion/decide.ts
442
+ var DEFAULT_MS = {
443
+ entrance: 200,
444
+ exit: 150,
445
+ state: 120,
446
+ reveal: 500,
447
+ signature: 700
448
+ };
449
+ function shouldAnimate(input) {
450
+ const base = input.enterMs ?? DEFAULT_MS[input.kind];
451
+ if (input.trigger === "keyboard") {
452
+ return {
453
+ animate: false,
454
+ durationMs: 0,
455
+ reason: "keyboard-triggered: someone navigating by keyboard is moving faster than the animation"
456
+ };
457
+ }
458
+ if (input.usesPerDay >= 100) {
459
+ return {
460
+ animate: false,
461
+ durationMs: 0,
462
+ reason: `${input.usesPerDay} uses/day: at this frequency motion reads as latency, not polish`
463
+ };
464
+ }
465
+ if (input.kind === "exit") {
466
+ return {
467
+ animate: true,
468
+ durationMs: Math.round(base * 0.75),
469
+ reason: "exit runs at 0.75x the entrance \u2014 leaving content lingering reads as lag"
470
+ };
471
+ }
472
+ if (input.kind === "signature") {
473
+ return {
474
+ animate: true,
475
+ durationMs: base,
476
+ reason: "signature moment: allowed once per page, must be gated and must never be the LCP element"
477
+ };
478
+ }
479
+ return { animate: true, durationMs: base, reason: `${input.kind} at ${base}ms` };
480
+ }
481
+
482
+ // src/type/scale.ts
483
+ var TYPE_STEPS = [-2, -1, 0, 1, 2, 3, 4, 5];
484
+ var round = (n) => Math.round(n * 1e4) / 1e4;
485
+ function fluidClamp(minPx, maxPx, minViewport, maxViewport, rootPx = 16) {
486
+ const minRem = round(minPx / rootPx);
487
+ const maxRem = round(maxPx / rootPx);
488
+ if (maxViewport === minViewport) return `${minRem}rem`;
489
+ const slope = (maxPx - minPx) / (maxViewport - minViewport);
490
+ const interceptRem = round((minPx - slope * minViewport) / rootPx);
491
+ const slopeVw = round(slope * 100);
492
+ const preferred = interceptRem === 0 ? `${slopeVw}vw` : `${interceptRem}rem + ${slopeVw}vw`;
493
+ const lower = Math.min(minRem, maxRem);
494
+ const upper = Math.max(minRem, maxRem);
495
+ return `clamp(${lower}rem, ${preferred}, ${upper}rem)`;
496
+ }
497
+ function fluidType(options = {}) {
498
+ const {
499
+ minViewport = 360,
500
+ maxViewport = 1280,
501
+ minBase = 16,
502
+ maxBase = 18,
503
+ minRatio = 1.2,
504
+ maxRatio = 1.25,
505
+ pin = {},
506
+ rootPx = 16
507
+ } = options;
508
+ const tokens = {};
509
+ const pinned = [];
510
+ for (const step of TYPE_STEPS) {
511
+ const name = `--craft-step-${step < 0 ? `-${Math.abs(step)}` : step}`;
512
+ const literal = pin[step];
513
+ if (typeof literal === "string") {
514
+ tokens[name] = literal;
515
+ pinned.push(step);
516
+ continue;
517
+ }
518
+ tokens[name] = fluidClamp(
519
+ minBase * Math.pow(minRatio, step),
520
+ maxBase * Math.pow(maxRatio, step),
521
+ minViewport,
522
+ maxViewport,
523
+ rootPx
524
+ );
525
+ }
526
+ return { tokens, pinned };
527
+ }
528
+
529
+ // src/space/scale.ts
530
+ var SPACE_STEPS = {
531
+ "3xs": 0.25,
532
+ "2xs": 0.5,
533
+ xs: 0.75,
534
+ s: 1,
535
+ m: 1.5,
536
+ l: 2,
537
+ xl: 3,
538
+ "2xl": 4,
539
+ "3xl": 6
540
+ };
541
+ var ORDER = Object.keys(SPACE_STEPS);
542
+ function fluidSpace(options = {}) {
543
+ const {
544
+ minViewport = 360,
545
+ maxViewport = 1280,
546
+ minBase = 16,
547
+ maxBase = 20,
548
+ rootPx = 16,
549
+ pin = {}
550
+ } = options;
551
+ const tokens = {};
552
+ for (const step of ORDER) {
553
+ const name = `--craft-space-${step}`;
554
+ tokens[name] = pin[step] ?? fluidClamp(
555
+ minBase * SPACE_STEPS[step],
556
+ maxBase * SPACE_STEPS[step],
557
+ minViewport,
558
+ maxViewport,
559
+ rootPx
560
+ );
561
+ }
562
+ for (let i = 0; i < ORDER.length - 1; i += 1) {
563
+ const from = ORDER[i];
564
+ const to = ORDER[i + 1];
565
+ const name = `--craft-space-${from}-${to}`;
566
+ tokens[name] = pin[`${from}-${to}`] ?? fluidClamp(
567
+ minBase * SPACE_STEPS[from],
568
+ maxBase * SPACE_STEPS[to],
569
+ minViewport,
570
+ maxViewport,
571
+ rootPx
572
+ );
573
+ }
574
+ return tokens;
575
+ }
576
+ function sectionRhythm(options = {}) {
577
+ const { minViewport = 360, maxViewport = 1280, rootPx = 16 } = options;
578
+ const defaults = {
579
+ sm: { min: 40, max: 64 },
580
+ md: { min: 56, max: 96 },
581
+ // Matches the `clamp(5rem, 10vw, 9rem)` a client site arrived at by hand.
582
+ lg: { min: 80, max: 144 }
583
+ };
584
+ const tokens = {};
585
+ for (const size of ["sm", "md", "lg"]) {
586
+ const supplied = options[size];
587
+ if (typeof supplied === "string") {
588
+ tokens[`--craft-section-${size}`] = supplied;
589
+ continue;
590
+ }
591
+ const { min, max } = supplied ?? defaults[size];
592
+ tokens[`--craft-section-${size}`] = fluidClamp(min, max, minViewport, maxViewport, rootPx);
593
+ }
594
+ return tokens;
595
+ }
596
+
597
+ // src/type/features.ts
598
+ var HOUSE_TYPE = {
599
+ /**
600
+ * Letter-spacing. Display type is set tighter because tracking that reads as
601
+ * neutral at 16px reads as loose at 48px; buttons and eyebrows go the other
602
+ * way, because short all-caps strings need air to stay countable.
603
+ */
604
+ tracking: {
605
+ display: "-0.01em",
606
+ heading: "-0.005em",
607
+ body: "0em",
608
+ button: "0.12em",
609
+ eyebrow: "0.24em"
610
+ },
611
+ /** Line height. Tighter as type gets larger, since the eye tracks a shorter return. */
612
+ leading: {
613
+ display: "1.05",
614
+ heading: "1.1",
615
+ subheading: "1.3",
616
+ body: "1.6"
617
+ },
618
+ /**
619
+ * Measure, in `ch`. Below ~45 the eye returns too often; above ~75 it loses
620
+ * the line on the way back.
621
+ */
622
+ measure: {
623
+ narrow: "45ch",
624
+ body: "65ch",
625
+ wide: "75ch"
626
+ }
627
+ };
628
+ function typeFeatureTokens() {
629
+ const tokens = {};
630
+ for (const [name, value] of Object.entries(HOUSE_TYPE.tracking)) {
631
+ tokens[`--craft-tracking-${name}`] = value;
632
+ }
633
+ for (const [name, value] of Object.entries(HOUSE_TYPE.leading)) {
634
+ tokens[`--craft-leading-${name}`] = value;
635
+ }
636
+ for (const [name, value] of Object.entries(HOUSE_TYPE.measure)) {
637
+ tokens[`--craft-measure-${name}`] = value;
638
+ }
639
+ return tokens;
640
+ }
641
+
642
+ // src/css/emit.ts
643
+ function emitCss(tokens, options = {}) {
644
+ const selector = options.selector ?? ":root";
645
+ const indent = options.indent ?? " ";
646
+ const notes = options.notes ?? {};
647
+ const lines = Object.entries(tokens).map(([name, value]) => {
648
+ const note = notes[name];
649
+ return `${indent}${name}: ${value};${note ? ` /* ${note} */` : ""}`;
650
+ });
651
+ return `${selector} {
652
+ ${lines.join("\n")}
653
+ }
654
+ `;
655
+ }
656
+ function motionTokens() {
657
+ const tokens = {};
658
+ for (const [name, value] of Object.entries(EASE)) {
659
+ tokens[`--craft-ease-${kebab(name)}`] = value;
660
+ }
661
+ for (const [name, ms] of Object.entries(DURATION_MS)) {
662
+ tokens[`--craft-duration-${kebab(name)}`] = `${ms}ms`;
663
+ }
664
+ tokens["--craft-exit-ratio"] = String(EXIT_RATIO);
665
+ tokens["--craft-scale-press"] = String(SCALE.press);
666
+ tokens["--craft-scale-enter"] = String(SCALE.enter);
667
+ tokens["--craft-spring-damping"] = String(SPRING.damping);
668
+ tokens["--craft-spring-stiffness"] = String(SPRING.stiffness);
669
+ return tokens;
670
+ }
671
+ function kebab(name) {
672
+ return name.replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
673
+ }
674
+ function craftTokens(input) {
675
+ const derived = semantic(input);
676
+ const tokens = {
677
+ ...derived.tokens,
678
+ ...motionTokens(),
679
+ ...fluidType(input.type).tokens,
680
+ ...typeFeatureTokens(),
681
+ ...fluidSpace(input.space),
682
+ ...sectionRhythm(input.section)
683
+ };
684
+ const header = [
685
+ "/* Generated by @domandigital/craft. Do not edit by hand.",
686
+ " * Anchors are supplied by the consuming repo; every other value is derived,",
687
+ " * and every contrast-sensitive pair carries its measured ratio as a comment.",
688
+ " */"
689
+ ].join("\n");
690
+ return {
691
+ css: `${header}
692
+ ${emitCss(tokens, {
693
+ selector: input.selector ?? ":root",
694
+ notes: derived.notes
695
+ })}`,
696
+ tokens,
697
+ report: derived.report,
698
+ ramps: { accent: derived.accentRamp }
699
+ };
700
+ }
701
+
702
+ // src/density/index.ts
703
+ var DENSITIES = {
704
+ compact: { rowPx: 32, padXPx: 8, padYPx: 4, fontPx: 13 },
705
+ comfortable: { rowPx: 40, padXPx: 12, padYPx: 8, fontPx: 14 },
706
+ spacious: { rowPx: 48, padXPx: 16, padYPx: 12, fontPx: 15 }
707
+ };
708
+ function densityCss(options = {}) {
709
+ const { base = "comfortable", rootPx = 16 } = options;
710
+ const declare = (name, indent = " ") => {
711
+ const d = DENSITIES[name];
712
+ return [
713
+ `${indent}--craft-density-row: ${d.rowPx / rootPx}rem;`,
714
+ `${indent}--craft-density-pad-x: ${d.padXPx / rootPx}rem;`,
715
+ `${indent}--craft-density-pad-y: ${d.padYPx / rootPx}rem;`,
716
+ `${indent}--craft-density-font: ${d.fontPx / rootPx}rem;`
717
+ ].join("\n");
718
+ };
719
+ const blocks = [`:root {
720
+ ${declare(base)}
721
+ }`];
722
+ for (const name of Object.keys(DENSITIES)) {
723
+ blocks.push(`[data-density="${name}"] {
724
+ ${declare(name)}
725
+ }`);
726
+ }
727
+ return `${blocks.join("\n\n")}
728
+ `;
729
+ }
730
+ function densityTokens(name, rootPx = 16) {
731
+ const d = DENSITIES[name];
732
+ return {
733
+ "--craft-density-row": `${d.rowPx / rootPx}rem`,
734
+ "--craft-density-pad-x": `${d.padXPx / rootPx}rem`,
735
+ "--craft-density-pad-y": `${d.padYPx / rootPx}rem`,
736
+ "--craft-density-font": `${d.fontPx / rootPx}rem`
737
+ };
738
+ }
739
+
740
+ // src/restraint/index.ts
741
+ var HOUSE_BUDGET = {
742
+ maxFontSizes: 10,
743
+ maxFontWeights: 3,
744
+ maxFontFamilies: 2,
745
+ maxRadii: 4,
746
+ maxShadows: 4,
747
+ accentHueClusterDeg: 15,
748
+ maxUiDurationMs: 300
749
+ };
750
+ function stripComments(css) {
751
+ return css.replace(/\/\*[\s\S]*?\*\//g, "");
752
+ }
753
+ function distinctValues(css, property) {
754
+ const pattern = new RegExp(`(?:^|[;{\\s])${property}\\s*:\\s*([^;}]+)`, "gi");
755
+ const seen = /* @__PURE__ */ new Set();
756
+ for (const match of css.matchAll(pattern)) {
757
+ const value = match[1].trim().replace(/\s+/g, " ").toLowerCase();
758
+ if (value.startsWith("var(") || value === "inherit" || value === "initial") continue;
759
+ seen.add(value);
760
+ }
761
+ return [...seen];
762
+ }
763
+ function durationsMs(css) {
764
+ const out = [];
765
+ for (const match of css.matchAll(/([\d.]+)(ms|s)\b/g)) {
766
+ const n = Number(match[1]);
767
+ if (!Number.isFinite(n)) continue;
768
+ out.push({ value: match[0], ms: match[2] === "s" ? n * 1e3 : n });
769
+ }
770
+ return out;
771
+ }
772
+ function checkRestraint(input) {
773
+ const budget = { ...HOUSE_BUDGET, ...input.budget };
774
+ const css = stripComments(input.css);
775
+ const violations = [];
776
+ const push = (rule, severity, message, found) => {
777
+ violations.push({ rule, severity, message, found });
778
+ };
779
+ const fontSizes = distinctValues(css, "font-size");
780
+ if (fontSizes.length > budget.maxFontSizes) {
781
+ push(
782
+ "font-sizes",
783
+ "error",
784
+ `${fontSizes.length} distinct font sizes, budget ${budget.maxFontSizes}. A type scale stops being a scale once nobody can name its steps.`,
785
+ fontSizes
786
+ );
787
+ }
788
+ const weights = distinctValues(css, "font-weight").filter((w) => w !== "normal" && w !== "bold");
789
+ const namedWeights = distinctValues(css, "font-weight").filter(
790
+ (w) => w === "normal" || w === "bold"
791
+ );
792
+ const allWeights = [.../* @__PURE__ */ new Set([...weights, ...namedWeights])];
793
+ if (allWeights.length > budget.maxFontWeights) {
794
+ push(
795
+ "font-weights",
796
+ "error",
797
+ `${allWeights.length} font weights, budget ${budget.maxFontWeights}.`,
798
+ allWeights
799
+ );
800
+ }
801
+ const families = distinctValues(css, "font-family").filter(
802
+ (f) => !/\bmonospace\b|\bmono\b/.test(f)
803
+ );
804
+ if (families.length > budget.maxFontFamilies) {
805
+ push(
806
+ "font-families",
807
+ "error",
808
+ `${families.length} non-mono font families, budget ${budget.maxFontFamilies}.`,
809
+ families
810
+ );
811
+ }
812
+ const radii = distinctValues(css, "border-radius").filter((r) => r !== "0" && r !== "0px");
813
+ if (radii.length > budget.maxRadii) {
814
+ push("radii", "error", `${radii.length} corner radii, budget ${budget.maxRadii}.`, radii);
815
+ }
816
+ const shadows = distinctValues(css, "box-shadow").filter((s) => s !== "none");
817
+ if (shadows.length > budget.maxShadows) {
818
+ push(
819
+ "shadows",
820
+ "error",
821
+ `${shadows.length} shadows, budget ${budget.maxShadows}. More than four elevations means no elevation reads as meaningful.`,
822
+ shadows
823
+ );
824
+ }
825
+ const accentHexes = [
826
+ ...css.matchAll(/--[a-z0-9-]*accent[a-z0-9-]*\s*:\s*(#[0-9a-fA-F]{3,8})/g)
827
+ ].map((m) => m[1]);
828
+ if (accentHexes.length > 1) {
829
+ const hues = accentHexes.map((hex) => {
830
+ try {
831
+ const { c, h } = hexToOklch(hex);
832
+ return c > 0.02 ? h : null;
833
+ } catch {
834
+ return null;
835
+ }
836
+ }).filter((h) => h !== null);
837
+ if (hues.length > 1) {
838
+ const spread = hueSpread(hues);
839
+ if (spread > budget.accentHueClusterDeg) {
840
+ push(
841
+ "accent-hues",
842
+ "error",
843
+ `Accent hues span ${spread.toFixed(1)}deg, budget ${budget.accentHueClusterDeg}deg. Two accents is two brands.`,
844
+ accentHexes
845
+ );
846
+ }
847
+ }
848
+ }
849
+ if (/transition\s*:\s*all\b/i.test(css)) {
850
+ push(
851
+ "transition-all",
852
+ "error",
853
+ "`transition: all` animates properties you did not choose, including layout ones that cannot be composited.",
854
+ ["transition: all"]
855
+ );
856
+ }
857
+ const easeIn = [
858
+ ...css.matchAll(/(?:transition|animation)[^;}]*\b(ease-in)\b(?!-out)/gi),
859
+ ...css.matchAll(/cubic-bezier\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,/gi)
860
+ ].map((m) => {
861
+ if (m[1] === "ease-in") return "ease-in";
862
+ const x1 = Number(m[1]);
863
+ const y1 = Number(m[2]);
864
+ return Number.isFinite(x1) && Number.isFinite(y1) && y1 < x1 ? m[0] : null;
865
+ }).filter((v) => v !== null);
866
+ if (easeIn.length > 0) {
867
+ push(
868
+ "ease-in",
869
+ "error",
870
+ "An ease-in curve accelerates away from the user. Exits use a decelerating curve, faster.",
871
+ [...new Set(easeIn)]
872
+ );
873
+ }
874
+ const overBudget = durationsMs(css).filter((d) => d.ms > budget.maxUiDurationMs);
875
+ if (overBudget.length > 0) {
876
+ push(
877
+ "duration",
878
+ "warning",
879
+ `${overBudget.length} duration(s) over ${budget.maxUiDurationMs}ms. Allowed for drawers and one gated reveal; not for UI state.`,
880
+ [...new Set(overBudget.map((d) => d.value))]
881
+ );
882
+ }
883
+ if (/scale\(\s*0\s*\)|scale3d\(\s*0\s*,\s*0/i.test(css)) {
884
+ push(
885
+ "scale-zero",
886
+ "error",
887
+ "Entering from `scale(0)` collapses the element to a point, so its text reflows during the animation.",
888
+ ["scale(0)"]
889
+ );
890
+ }
891
+ const reducedMotionAt = css.search(/@media[^{]*prefers-reduced-motion:\s*reduce/i);
892
+ if (reducedMotionAt === -1) {
893
+ if (/@keyframes|animation\s*:|transition\s*:/.test(css)) {
894
+ push(
895
+ "reduced-motion-missing",
896
+ "error",
897
+ "Motion is defined but no `prefers-reduced-motion: reduce` block exists.",
898
+ []
899
+ );
900
+ }
901
+ } else {
902
+ const block = css.slice(reducedMotionAt);
903
+ if (/animation\s*:\s*none/i.test(block)) {
904
+ push(
905
+ "reduced-motion-animation-none",
906
+ "error",
907
+ "`animation: none` under reduced motion drops the `forwards` fill, stranding a forwards-filled entrance at opacity 0 for exactly the users who asked for less motion. Collapse the duration instead.",
908
+ ["animation: none"]
909
+ );
910
+ }
911
+ }
912
+ const markup = input.markup ?? "";
913
+ if (markup.length > 0) {
914
+ const hasNumericCells = /data-numeric|craft-numeric/.test(markup);
915
+ const declaresTabular = /tabular-nums/.test(css) || /tabular-nums/.test(markup);
916
+ if (hasNumericCells && !declaresTabular) {
917
+ push(
918
+ "tabular-nums",
919
+ "error",
920
+ "Numeric cells are marked but nothing sets `tabular-nums`. Proportional figures in a column do not line up.",
921
+ []
922
+ );
923
+ }
924
+ }
925
+ return {
926
+ ok: violations.every((v) => v.severity !== "error"),
927
+ violations,
928
+ counts: {
929
+ fontSizes: fontSizes.length,
930
+ fontWeights: allWeights.length,
931
+ fontFamilies: families.length,
932
+ radii: radii.length,
933
+ shadows: shadows.length,
934
+ accentHexes: accentHexes.length
935
+ }
936
+ };
937
+ }
938
+ function hueSpread(hues) {
939
+ const sorted = [...hues].sort((a, b) => a - b);
940
+ let widestGap = 0;
941
+ for (let i = 0; i < sorted.length; i += 1) {
942
+ const next = sorted[(i + 1) % sorted.length];
943
+ const gap = i === sorted.length - 1 ? next + 360 - sorted[i] : next - sorted[i];
944
+ widestGap = Math.max(widestGap, gap);
945
+ }
946
+ return 360 - widestGap;
947
+ }
948
+
949
+ // src/css/tailwind.ts
950
+ function stepName(step) {
951
+ return step < 0 ? `-${Math.abs(step)}` : String(step);
952
+ }
953
+ function tailwindV3Preset() {
954
+ const fontSize = {};
955
+ for (const step of TYPE_STEPS) {
956
+ fontSize[`step-${stepName(step)}`] = `var(--craft-step-${stepName(step)})`;
957
+ }
958
+ const spacing = {};
959
+ for (const step of Object.keys(SPACE_STEPS)) {
960
+ spacing[step] = `var(--craft-space-${step})`;
961
+ }
962
+ for (const size of ["sm", "md", "lg"]) {
963
+ spacing[`section-${size}`] = `var(--craft-section-${size})`;
964
+ }
965
+ const transitionTimingFunction = {};
966
+ for (const name of Object.keys(EASE)) {
967
+ const kebab2 = name.replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
968
+ transitionTimingFunction[kebab2] = `var(--craft-ease-${kebab2})`;
969
+ }
970
+ const transitionDuration = {};
971
+ for (const name of Object.keys(DURATION_MS)) {
972
+ transitionDuration[name] = `var(--craft-duration-${name})`;
973
+ }
974
+ const colors = {};
975
+ for (const token of [
976
+ "bg-canvas",
977
+ "bg-base",
978
+ "bg-surface",
979
+ "bg-elevated",
980
+ "text-primary",
981
+ "text-secondary",
982
+ "text-muted",
983
+ "text-on-accent",
984
+ "border-subtle",
985
+ "border-strong",
986
+ "accent",
987
+ "accent-hover",
988
+ "accent-soft",
989
+ "accent-text",
990
+ "success",
991
+ "warning",
992
+ "danger",
993
+ "info"
994
+ ]) {
995
+ colors[token] = `var(--craft-${token})`;
996
+ }
997
+ const letterSpacing = {};
998
+ for (const name of Object.keys(HOUSE_TYPE.tracking)) {
999
+ letterSpacing[name] = `var(--craft-tracking-${name})`;
1000
+ }
1001
+ const lineHeight = {};
1002
+ for (const name of Object.keys(HOUSE_TYPE.leading)) {
1003
+ lineHeight[name] = `var(--craft-leading-${name})`;
1004
+ }
1005
+ const maxWidth = {};
1006
+ for (const name of Object.keys(HOUSE_TYPE.measure)) {
1007
+ maxWidth[`measure-${name}`] = `var(--craft-measure-${name})`;
1008
+ }
1009
+ const minHeight = { row: "var(--craft-density-row)" };
1010
+ return {
1011
+ theme: {
1012
+ extend: {
1013
+ colors,
1014
+ fontSize,
1015
+ spacing,
1016
+ letterSpacing,
1017
+ lineHeight,
1018
+ maxWidth,
1019
+ minHeight,
1020
+ transitionTimingFunction,
1021
+ transitionDuration
1022
+ }
1023
+ }
1024
+ };
1025
+ }
1026
+ function tailwindV4Theme() {
1027
+ const lines = ["@theme inline {"];
1028
+ const add = (name, value) => {
1029
+ lines.push(` ${name}: ${value};`);
1030
+ };
1031
+ for (const token of [
1032
+ "bg-canvas",
1033
+ "bg-base",
1034
+ "bg-surface",
1035
+ "bg-elevated",
1036
+ "text-primary",
1037
+ "text-secondary",
1038
+ "text-muted",
1039
+ "text-on-accent",
1040
+ "border-subtle",
1041
+ "border-strong",
1042
+ "accent",
1043
+ "accent-hover",
1044
+ "accent-soft",
1045
+ "accent-text",
1046
+ "success",
1047
+ "warning",
1048
+ "danger",
1049
+ "info"
1050
+ ]) {
1051
+ add(`--color-${token}`, `var(--craft-${token})`);
1052
+ }
1053
+ for (const step of TYPE_STEPS) {
1054
+ add(`--text-step-${stepName(step)}`, `var(--craft-step-${stepName(step)})`);
1055
+ }
1056
+ for (const step of Object.keys(SPACE_STEPS)) {
1057
+ add(`--spacing-${step}`, `var(--craft-space-${step})`);
1058
+ }
1059
+ for (const size of ["sm", "md", "lg"]) {
1060
+ add(`--spacing-section-${size}`, `var(--craft-section-${size})`);
1061
+ }
1062
+ for (const name of Object.keys(EASE)) {
1063
+ const kebab2 = name.replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
1064
+ add(`--ease-${kebab2}`, `var(--craft-ease-${kebab2})`);
1065
+ }
1066
+ for (const name of Object.keys(DURATION_MS)) {
1067
+ add(`--duration-${name}`, `var(--craft-duration-${name})`);
1068
+ }
1069
+ for (const name of Object.keys(HOUSE_TYPE.tracking)) {
1070
+ add(`--tracking-${name}`, `var(--craft-tracking-${name})`);
1071
+ }
1072
+ for (const name of Object.keys(HOUSE_TYPE.leading)) {
1073
+ add(`--leading-${name}`, `var(--craft-leading-${name})`);
1074
+ }
1075
+ lines.push("}");
1076
+ return `${lines.join("\n")}
1077
+ `;
1078
+ }
1079
+ var DENSITY_NAMES = Object.keys(DENSITIES);
1080
+ export {
1081
+ DENSITIES,
1082
+ DENSITY_NAMES,
1083
+ DURATION_MS,
1084
+ DURATION_S,
1085
+ EASE,
1086
+ EASE_TUPLE,
1087
+ EXIT_RATIO,
1088
+ HOUSE_BUDGET,
1089
+ HOUSE_TYPE,
1090
+ LIGHTNESS_CURVE,
1091
+ RAMP_STEPS,
1092
+ SCALE,
1093
+ SPACE_STEPS,
1094
+ SPRING,
1095
+ TYPE_STEPS,
1096
+ accentFork,
1097
+ apcaContrast,
1098
+ checkPair,
1099
+ checkRestraint,
1100
+ craftTokens,
1101
+ deltaEOk,
1102
+ deltaEOkHex,
1103
+ densityCss,
1104
+ densityTokens,
1105
+ emitCss,
1106
+ exitDuration,
1107
+ fluidClamp,
1108
+ fluidSpace,
1109
+ fluidType,
1110
+ formatHex,
1111
+ hexToOklch,
1112
+ inP3Gamut,
1113
+ inSrgbGamut,
1114
+ motionTokens,
1115
+ oklchToHex,
1116
+ oklchToRgb,
1117
+ parseHex,
1118
+ ramp,
1119
+ rampFromAnchors,
1120
+ sectionRhythm,
1121
+ semantic,
1122
+ shouldAnimate,
1123
+ tailwindV3Preset,
1124
+ tailwindV4Theme,
1125
+ toGamut,
1126
+ typeFeatureTokens,
1127
+ wcagContrast
1128
+ };