@loidolt/theme-tokens 0.6.0 → 0.8.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 CHANGED
@@ -1,50 +1,7 @@
1
- // src/contrast.ts
2
- var contrastRequirements = {
3
- AA: { normal: 4.5, large: 3 },
4
- AAA: { normal: 7, large: 4.5 }
5
- };
6
- var channels = (hex, caller) => {
7
- const match = /^#?([\da-f]{3}|[\da-f]{6})$/i.exec(hex.trim());
8
- if (!match) throw new RangeError(`${caller}: invalid hex colour ${JSON.stringify(hex)}`);
9
- const digits = match[1].length === 3 ? [...match[1]].map((digit) => digit + digit).join("") : match[1];
10
- return [0, 2, 4].map((index) => parseInt(digits.slice(index, index + 2), 16));
11
- };
12
- var luminanceOf = (hex, caller) => {
13
- const [r, g, b] = channels(hex, caller).map((channel) => channel / 255).map((channel) => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4);
14
- return 0.2126 * r + 0.7152 * g + 0.0722 * b;
15
- };
16
- function getLuminance(hex) {
17
- return luminanceOf(hex, "getLuminance");
18
- }
19
- function getContrastRatio(foreground, background) {
20
- const [high, low] = [
21
- luminanceOf(foreground, "getContrastRatio"),
22
- luminanceOf(background, "getContrastRatio")
23
- ].sort((a, b) => b - a);
24
- return (high + 0.05) / (low + 0.05);
25
- }
26
- function meetsContrast(foreground, background, { level = "AA", size = "normal" } = {}) {
27
- return getContrastRatio(foreground, background) >= contrastRequirements[level][size];
28
- }
29
- function validateContrast(foreground, background) {
30
- const ratio = getContrastRatio(foreground, background);
31
- return {
32
- ratio,
33
- ratioString: `${ratio.toFixed(2)}:1`,
34
- passesAANormal: ratio >= contrastRequirements.AA.normal,
35
- passesAALarge: ratio >= contrastRequirements.AA.large,
36
- passesAAANormal: ratio >= contrastRequirements.AAA.normal,
37
- passesAAALarge: ratio >= contrastRequirements.AAA.large
38
- };
39
- }
40
- function getContrastTextColor(background, { dark = "#20231d", light = "#f5f2e9" } = {}) {
41
- return getContrastRatio(dark, background) >= getContrastRatio(light, background) ? dark : light;
42
- }
43
-
44
- // src/index.ts
1
+ // src/tokens.ts
45
2
  var ref = (name) => ({ $ref: `--loidolt-${name}` });
46
3
  var isRef = (value) => typeof value === "object" && value !== null && "$ref" in value;
47
- var kebab = (value) => value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
4
+ var kebab = (value) => value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`).replace(/([a-z])(\d)/g, "$1-$2");
48
5
  var cssVarName = (path) => `--loidolt-${kebab(path.join("-"))}`;
49
6
  var colors = {
50
7
  paper: "#ebe7dc",
@@ -66,7 +23,60 @@ var colors = {
66
23
  errorDark: "#7f1f1a",
67
24
  success: "#3f5139",
68
25
  warning: "#7c4d17",
69
- info: "#265162"
26
+ info: "#265162",
27
+ /**
28
+ * Categorical series for charts, in the order they are handed out. Earth tones that stay apart
29
+ * from each other and hold 3:1 against every surface, so a line or bar needs no outline.
30
+ */
31
+ series: {
32
+ 1: "#c65224",
33
+ 2: "#2f6f8a",
34
+ 3: "#5b7a3a",
35
+ 4: "#9a6a12",
36
+ 5: "#7a4a78",
37
+ 6: "#4d5a6a",
38
+ 7: "#2f7d6d",
39
+ 8: "#8a5a3c"
40
+ },
41
+ /** One warm hue from little to most: heatmaps, choropleths, cluster sizes. */
42
+ sequential: {
43
+ 1: "#ecd3b8",
44
+ 2: "#dea67a",
45
+ 3: "#c97a45",
46
+ 4: "#a4521f",
47
+ 5: "#6e3314"
48
+ },
49
+ /** Below and above a midpoint: slate for below, orange for above, the neutral paper between. */
50
+ diverging: {
51
+ 1: "#265162",
52
+ 2: "#4f8196",
53
+ 3: "#9cbac3",
54
+ 4: "#ddd7c9",
55
+ 5: "#e0a986",
56
+ 6: "#c96f3d",
57
+ 7: "#9b3a17"
58
+ },
59
+ /**
60
+ * Code highlighting. Code sits on the inverse surface — dark in the light theme — so these are
61
+ * the light inks; the dark theme's code block is light, and takes dark ones.
62
+ */
63
+ syntax: {
64
+ keyword: "#f0895c",
65
+ string: "#a3c197",
66
+ comment: "#b5ae9c",
67
+ constant: "#e2b56c",
68
+ function: "#8fc0d4",
69
+ parameter: "#d3a8cf",
70
+ punctuation: "#d8d2c4"
71
+ },
72
+ /** Basemap fills. Quiet by design: the data drawn on top carries the colour. */
73
+ map: {
74
+ water: "#b9cdd3",
75
+ park: "#d3d8bd",
76
+ road: "#f5f2e9",
77
+ roadMajor: "#ffffff",
78
+ building: "#d8d3c7"
79
+ }
70
80
  };
71
81
  var typography = {
72
82
  display: '"Jost", "Avenir Next", sans-serif',
@@ -181,7 +191,107 @@ var semanticDefs = {
181
191
  textInfo: ref("color-info"),
182
192
  focusRing: ref("color-orange"),
183
193
  shadowColor: ref("color-deep"),
184
- scrim: "color-mix(in srgb, var(--loidolt-shadow-color) 72%, transparent)"
194
+ scrim: "color-mix(in srgb, var(--loidolt-shadow-color) 72%, transparent)",
195
+ /*
196
+ * Data visualisation. `chart1`…`chart8` are categorical series; the ramps run from "least" (1)
197
+ * to "most" (last), whatever the theme; the diverging midpoint (4) is the neutral.
198
+ */
199
+ chart1: ref("color-series-1"),
200
+ chart2: ref("color-series-2"),
201
+ chart3: ref("color-series-3"),
202
+ chart4: ref("color-series-4"),
203
+ chart5: ref("color-series-5"),
204
+ chart6: ref("color-series-6"),
205
+ chart7: ref("color-series-7"),
206
+ chart8: ref("color-series-8"),
207
+ chartSequential1: ref("color-sequential-1"),
208
+ chartSequential2: ref("color-sequential-2"),
209
+ chartSequential3: ref("color-sequential-3"),
210
+ chartSequential4: ref("color-sequential-4"),
211
+ chartSequential5: ref("color-sequential-5"),
212
+ chartDiverging1: ref("color-diverging-1"),
213
+ chartDiverging2: ref("color-diverging-2"),
214
+ chartDiverging3: ref("color-diverging-3"),
215
+ chartDiverging4: ref("color-diverging-4"),
216
+ chartDiverging5: ref("color-diverging-5"),
217
+ chartDiverging6: ref("color-diverging-6"),
218
+ chartDiverging7: ref("color-diverging-7"),
219
+ /** Gains and losses: candlestick up/down, positive/negative bars. */
220
+ chartPositive: ref("color-success"),
221
+ chartNegative: ref("color-error"),
222
+ /* Basemap roles, read by the map style generator. */
223
+ mapLand: ref("color-paper"),
224
+ mapWater: ref("color-map-water"),
225
+ mapPark: ref("color-map-park"),
226
+ mapRoad: ref("color-map-road"),
227
+ mapRoadMajor: ref("color-map-road-major"),
228
+ mapBuilding: ref("color-map-building"),
229
+ mapBoundary: ref("color-line-strong"),
230
+ mapLabel: ref("color-deep"),
231
+ mapLabelHalo: ref("color-panel"),
232
+ /* Code highlighting, on the inverse surface where code blocks sit. */
233
+ syntaxKeyword: ref("color-syntax-keyword"),
234
+ syntaxString: ref("color-syntax-string"),
235
+ syntaxComment: ref("color-syntax-comment"),
236
+ syntaxConstant: ref("color-syntax-constant"),
237
+ syntaxFunction: ref("color-syntax-function"),
238
+ syntaxParameter: ref("color-syntax-parameter"),
239
+ syntaxPunctuation: ref("color-syntax-punctuation")
240
+ };
241
+ var roleDefs = {
242
+ /** Buttons, boxed fields, toggle groups, switches, swatches, tooltips. */
243
+ radiusControl: ref("border-radius"),
244
+ /** Rows and items inside a rounded container (menu items, list rows, filmstrip frames). */
245
+ radiusInner: "0px",
246
+ /** Cards, panels, tables, alerts, code blocks, thumbnails. */
247
+ radiusSurface: ref("border-radius"),
248
+ /** Dialogs, popovers, menus, toasts. Drawers stay square: they are anchored to an edge. */
249
+ radiusOverlay: ref("border-radius"),
250
+ /** Width of the keyboard focus ring. WCAG 2.4.13 wants at least 2px; keep it there. */
251
+ strokeFocus: "2px",
252
+ /** Selected-tab, active-nav and selected-row indicators. */
253
+ strokeIndicator: "2px",
254
+ /** The status bar on the leading edge of an alert or toast. */
255
+ strokeAccent: "3px",
256
+ /*
257
+ * Density. Discrete roles rather than one runtime multiplier: computed values stay readable in
258
+ * devtools, and a preset can move one role without dragging the rest. `definePreset({ density })`
259
+ * scales them all at build time when one knob is what you want.
260
+ */
261
+ padControlX: "0.85rem",
262
+ padControlXSm: "0.65rem",
263
+ padControlXLg: "1.2rem",
264
+ padControlY: "0.5rem",
265
+ padControlYSm: "0.38rem",
266
+ /** Underlined fields: vertical, then horizontal. Boxed fields use `pad-control-x-sm` inline. */
267
+ padField: "0.45rem 0.15rem",
268
+ padItem: "0.65rem 0.55rem",
269
+ padCell: "0.7rem 0.8rem",
270
+ padCallout: "0.8rem 1rem",
271
+ padSurface: ref("space-4"),
272
+ padOverlay: "1rem 1.25rem",
273
+ padPopover: "0.45rem",
274
+ padBarX: "1.35rem",
275
+ padPane: "1.25rem",
276
+ padPage: ref("space-6"),
277
+ /** Between a field's label, control and help text. */
278
+ gapField: "0.35rem",
279
+ /** Between the buttons of a dialog footer or an empty state. */
280
+ gapActions: ref("space-2"),
281
+ /*
282
+ * Voice: how labels and controls speak. The default shouts — small uppercase utility type,
283
+ * tracked out — and a preset can bring it back to sentence case. Tracking roles keep a unit
284
+ * because stylesheets derive from them with `calc()`.
285
+ */
286
+ labelTransform: "uppercase",
287
+ labelTracking: ref("font-tracking-utility"),
288
+ labelTrackingWide: ref("font-tracking-wide"),
289
+ labelWeight: "400",
290
+ controlTransform: "uppercase",
291
+ controlTracking: "0.08em",
292
+ controlWeight: "400",
293
+ headingWeight: "500",
294
+ strongWeight: "600"
185
295
  };
186
296
  var PRIMITIVE_GROUPS = [
187
297
  ["color", colors],
@@ -204,6 +314,7 @@ function flattenTokens() {
204
314
  const out = [];
205
315
  for (const [prefix, tree] of PRIMITIVE_GROUPS) flattenInto([prefix], tree, out);
206
316
  for (const [name, value] of Object.entries(semanticDefs)) out.push([cssVarName([name]), value]);
317
+ for (const [name, value] of Object.entries(roleDefs)) out.push([cssVarName([name]), value]);
207
318
  return out;
208
319
  }
209
320
  function generateCss() {
@@ -221,12 +332,14 @@ function generateCss() {
221
332
  var resolved = new Map(
222
333
  flattenTokens().filter((entry) => typeof entry[1] === "string")
223
334
  );
224
- var semantic = Object.fromEntries(
225
- Object.entries(semanticDefs).map(([name, value]) => [
335
+ var resolveDefs = (defs) => Object.fromEntries(
336
+ Object.entries(defs).map(([name, value]) => [
226
337
  name,
227
338
  isRef(value) ? resolved.get(value.$ref) ?? "" : value
228
339
  ])
229
340
  );
341
+ var semantic = resolveDefs(semanticDefs);
342
+ var roles = resolveDefs(roleDefs);
230
343
  var darkSemantic = {
231
344
  background: "#161814",
232
345
  surface: "#1e211c",
@@ -262,8 +375,115 @@ var darkSemantic = {
262
375
  textInfo: "#85b6ca",
263
376
  focusRing: "#f0895c",
264
377
  shadowColor: "#000000",
265
- scrim: "color-mix(in srgb, var(--loidolt-shadow-color) 78%, transparent)"
378
+ scrim: "color-mix(in srgb, var(--loidolt-shadow-color) 78%, transparent)",
379
+ chart1: "#e0672f",
380
+ chart2: "#6ea5bd",
381
+ chart3: "#93b287",
382
+ chart4: "#d9ab63",
383
+ chart5: "#c49ac0",
384
+ chart6: "#b8ad96",
385
+ chart7: "#6fb8a6",
386
+ chart8: "#c79a78",
387
+ chartSequential1: "#3a2a1d",
388
+ chartSequential2: "#6b4225",
389
+ chartSequential3: "#a4602f",
390
+ chartSequential4: "#d98a4d",
391
+ chartSequential5: "#f2bd8a",
392
+ chartDiverging1: "#85b6ca",
393
+ chartDiverging2: "#5a8ea3",
394
+ chartDiverging3: "#35596a",
395
+ chartDiverging4: "#3a3d34",
396
+ chartDiverging5: "#7a4428",
397
+ chartDiverging6: "#bf6a3a",
398
+ chartDiverging7: "#f0895c",
399
+ chartPositive: "#93b287",
400
+ chartNegative: "#e88b81",
401
+ mapLand: "#161814",
402
+ mapWater: "#1d2b31",
403
+ mapPark: "#1f271c",
404
+ mapRoad: "#2b2e26",
405
+ mapRoadMajor: "#3a3d34",
406
+ mapBuilding: "#24271f",
407
+ mapBoundary: "#787c6b",
408
+ mapLabel: "#ebe7dc",
409
+ mapLabelHalo: "#161814",
410
+ syntaxKeyword: "#a9441d",
411
+ syntaxString: "#3f5139",
412
+ syntaxComment: "#5f5b50",
413
+ syntaxConstant: "#7c4d17",
414
+ syntaxFunction: "#265162",
415
+ syntaxParameter: "#7a4a78",
416
+ syntaxPunctuation: "#474a42"
417
+ };
418
+ var roleVar = (role) => cssVarName([role]);
419
+ var roleValue = (role, scheme = "light") => scheme === "dark" ? darkSemantic[role] : semantic[role];
420
+ function resolveRoles(spec, scheme = "light", read = (role) => roleValue(role, scheme)) {
421
+ return Object.fromEntries(
422
+ Object.entries(spec).map(([key, value]) => [
423
+ key,
424
+ typeof value === "string" ? read(value) : value.map((role) => read(role))
425
+ ])
426
+ );
427
+ }
428
+ var chartRoles = {
429
+ categorical: ["chart1", "chart2", "chart3", "chart4", "chart5", "chart6", "chart7", "chart8"],
430
+ sequential: [
431
+ "chartSequential1",
432
+ "chartSequential2",
433
+ "chartSequential3",
434
+ "chartSequential4",
435
+ "chartSequential5"
436
+ ],
437
+ diverging: [
438
+ "chartDiverging1",
439
+ "chartDiverging2",
440
+ "chartDiverging3",
441
+ "chartDiverging4",
442
+ "chartDiverging5",
443
+ "chartDiverging6",
444
+ "chartDiverging7"
445
+ ],
446
+ positive: "chartPositive",
447
+ negative: "chartNegative",
448
+ text: "text",
449
+ muted: "textMuted",
450
+ axis: "border",
451
+ grid: "borderSoft",
452
+ surface: "surface",
453
+ background: "background",
454
+ accent: "accent"
266
455
  };
456
+ var mapRoles = {
457
+ land: "mapLand",
458
+ water: "mapWater",
459
+ park: "mapPark",
460
+ road: "mapRoad",
461
+ roadMajor: "mapRoadMajor",
462
+ building: "mapBuilding",
463
+ boundary: "mapBoundary",
464
+ label: "mapLabel",
465
+ labelHalo: "mapLabelHalo",
466
+ accent: "accent",
467
+ text: "text",
468
+ surface: "surface",
469
+ focus: "focusRing",
470
+ categorical: chartRoles.categorical,
471
+ sequential: chartRoles.sequential
472
+ };
473
+ var syntaxRoles = {
474
+ background: "surfaceInverse",
475
+ text: "textInverse",
476
+ keyword: "syntaxKeyword",
477
+ string: "syntaxString",
478
+ comment: "syntaxComment",
479
+ constant: "syntaxConstant",
480
+ function: "syntaxFunction",
481
+ parameter: "syntaxParameter",
482
+ punctuation: "syntaxPunctuation"
483
+ };
484
+ var syntaxColors = (scheme = "light") => resolveRoles(syntaxRoles, scheme);
485
+ var chartColors = (scheme = "light") => resolveRoles(chartRoles, scheme);
486
+ var mapColors = (scheme = "light") => resolveRoles(mapRoles, scheme);
267
487
  var darkDeclarations = () => Object.entries(darkSemantic).map(([name, value]) => ` ${cssVarName([name])}: ${value};`);
268
488
  function generateDarkCss(scope = "attribute") {
269
489
  const body = [" color-scheme: dark;", ...darkDeclarations()];
@@ -298,30 +518,464 @@ var tokens = {
298
518
  breakpoints,
299
519
  aspectRatio,
300
520
  semantic,
301
- darkSemantic
521
+ darkSemantic,
522
+ roles
523
+ };
524
+
525
+ // src/contrast.ts
526
+ var contrastRequirements = {
527
+ AA: { normal: 4.5, large: 3 },
528
+ AAA: { normal: 7, large: 4.5 }
529
+ };
530
+ var channels = (hex, caller) => {
531
+ const match = /^#?([\da-f]{3}|[\da-f]{6})$/i.exec(hex.trim());
532
+ if (!match) throw new RangeError(`${caller}: invalid hex colour ${JSON.stringify(hex)}`);
533
+ const digits = match[1].length === 3 ? [...match[1]].map((digit) => digit + digit).join("") : match[1];
534
+ return [0, 2, 4].map((index) => parseInt(digits.slice(index, index + 2), 16));
535
+ };
536
+ var luminanceOf = (hex, caller) => {
537
+ const [r, g, b] = channels(hex, caller).map((channel) => channel / 255).map((channel) => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4);
538
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
539
+ };
540
+ function getLuminance(hex) {
541
+ return luminanceOf(hex, "getLuminance");
542
+ }
543
+ function getContrastRatio(foreground, background) {
544
+ const [high, low] = [
545
+ luminanceOf(foreground, "getContrastRatio"),
546
+ luminanceOf(background, "getContrastRatio")
547
+ ].sort((a, b) => b - a);
548
+ return (high + 0.05) / (low + 0.05);
549
+ }
550
+ function meetsContrast(foreground, background, { level = "AA", size = "normal" } = {}) {
551
+ return getContrastRatio(foreground, background) >= contrastRequirements[level][size];
552
+ }
553
+ function validateContrast(foreground, background) {
554
+ const ratio = getContrastRatio(foreground, background);
555
+ return {
556
+ ratio,
557
+ ratioString: `${ratio.toFixed(2)}:1`,
558
+ passesAANormal: ratio >= contrastRequirements.AA.normal,
559
+ passesAALarge: ratio >= contrastRequirements.AA.large,
560
+ passesAAANormal: ratio >= contrastRequirements.AAA.normal,
561
+ passesAAALarge: ratio >= contrastRequirements.AAA.large
562
+ };
563
+ }
564
+ function getContrastTextColor(background, { dark = "#20231d", light = "#f5f2e9" } = {}) {
565
+ return getContrastRatio(dark, background) >= getContrastRatio(light, background) ? dark : light;
566
+ }
567
+ function contrastPairs(t) {
568
+ return [
569
+ ["accent text on background", t.textAccent, t.background, 4.5],
570
+ ["accent text on surface", t.textAccent, t.surface, 4.5],
571
+ ["accent text on surface-alt", t.textAccent, t.surfaceAlt, 4.5],
572
+ ["muted text on background", t.textMuted, t.background, 4.5],
573
+ ["muted text on surface", t.textMuted, t.surface, 4.5],
574
+ // The recessed fill a default badge sits on.
575
+ ["muted text on the sunken surface", t.textMuted, t.surfaceSunken, 4.5],
576
+ ["body text on background", t.text, t.background, 4.5],
577
+ ["body text on surface", t.text, t.surface, 4.5],
578
+ ["inverse text on the inverse surface", t.textInverse, t.surfaceInverse, 4.5],
579
+ ["on-accent over accent", t.onAccent, t.accent, 4.5],
580
+ ["on-accent over accent hover", t.onAccent, t.accentHover, 4.5],
581
+ ["on-danger over danger", t.onDanger, t.danger, 4.5],
582
+ ["on-danger over danger hover", t.onDanger, t.dangerHover, 4.5],
583
+ ["on-success over success", t.onSuccess, t.success, 4.5],
584
+ ["on-warning over warning", t.onWarning, t.warning, 4.5],
585
+ ["on-info over info", t.onInfo, t.info, 4.5],
586
+ ["danger text on surface", t.textDanger, t.surface, 4.5],
587
+ ["danger text on surface-alt", t.textDanger, t.surfaceAlt, 4.5],
588
+ ["success text on surface", t.textSuccess, t.surface, 4.5],
589
+ ["warning text on surface", t.textWarning, t.surface, 4.5],
590
+ ["info text on surface", t.textInfo, t.surface, 4.5],
591
+ ["control border on the input surface", t.borderControl, t.surfaceInput, 3],
592
+ ["control border on background", t.borderControl, t.background, 3],
593
+ ["control border on surface", t.borderControl, t.surface, 3],
594
+ ["control border on surface-alt", t.borderControl, t.surfaceAlt, 3],
595
+ ["focus ring on background", t.focusRing, t.background, 3],
596
+ ["focus ring on surface", t.focusRing, t.surface, 3]
597
+ ];
598
+ }
599
+ function auditContrast(colors2) {
600
+ return contrastPairs(colors2).map(([name, foreground, background, required]) => {
601
+ const ratio = getContrastRatio(foreground, background);
602
+ return { name, foreground, background, ratio, required, pass: ratio >= required };
603
+ });
604
+ }
605
+
606
+ // src/presets/define.ts
607
+ var PRIMITIVE_GROUPS2 = { typography, spacing, sizing, borders, shadows, motion };
608
+ var PRIMITIVE_PREFIX = {
609
+ typography: "font",
610
+ spacing: "space",
611
+ sizing: "size",
612
+ borders: "border",
613
+ shadows: "shadow",
614
+ motion: "motion"
615
+ };
616
+ var flatten = (prefix, tree) => {
617
+ const out = [];
618
+ flattenInto([prefix], tree, out);
619
+ return out;
620
+ };
621
+ var baseState = () => ({
622
+ primitives: new Map(
623
+ Object.keys(PRIMITIVE_GROUPS2).flatMap(
624
+ (group) => flatten(PRIMITIVE_PREFIX[group], PRIMITIVE_GROUPS2[group])
625
+ )
626
+ ),
627
+ roles: { ...roleDefs },
628
+ light: { ...semanticDefs },
629
+ dark: { ...darkSemantic }
630
+ });
631
+ var states = /* @__PURE__ */ new WeakMap();
632
+ var NAME = /^[a-z][a-z0-9-]*$/;
633
+ var DENSITY_ROLE = /^(pad|gap)[A-Z]/;
634
+ var LENGTH = /(-?\d*\.?\d+)(rem|px|em)\b/g;
635
+ var round = (value) => String(Number(value.toFixed(4)));
636
+ var scaleLengths = (value, factor) => value.replace(
637
+ LENGTH,
638
+ (_, amount, unit) => `${round(parseFloat(amount) * factor)}${unit}`
639
+ );
640
+ var roleName = (name) => cssVarName([name]);
641
+ var cssValue = (value) => isRef(value) ? `var(${value.$ref})` : value;
642
+ var defaultValues = new Map(flattenTokens());
643
+ var resolveIn = (state) => {
644
+ const lookup = (name) => state.primitives.get(name) ?? defaultValues.get(name);
645
+ const resolve = (value, seen = /* @__PURE__ */ new Set()) => {
646
+ if (!isRef(value)) return value;
647
+ if (seen.has(value.$ref)) throw new Error(`Circular token reference through ${value.$ref}`);
648
+ const target = lookup(value.$ref);
649
+ if (target === void 0) throw new Error(`Unknown token reference ${value.$ref}`);
650
+ return resolve(target, /* @__PURE__ */ new Set([...seen, value.$ref]));
651
+ };
652
+ return resolve;
302
653
  };
654
+ var assertKnownKeys = (what, given, known) => {
655
+ for (const key of Object.keys(given ?? {})) {
656
+ if (!(key in known)) throw new Error(`Unknown ${what} "${key}"`);
657
+ }
658
+ };
659
+ function definePreset(definition) {
660
+ const { name, primitives = {}, roles: roles2 = {}, density, colors: colors2, fontImport } = definition;
661
+ if (!NAME.test(name)) {
662
+ throw new Error(`Preset name "${name}" must be lowercase letters, digits and dashes`);
663
+ }
664
+ assertKnownKeys("primitive group", primitives, PRIMITIVE_GROUPS2);
665
+ assertKnownKeys("role", roles2, roleDefs);
666
+ assertKnownKeys("colour role", colors2?.light, semanticDefs);
667
+ assertKnownKeys("colour role", colors2?.dark, semanticDefs);
668
+ if (colors2) {
669
+ const missing = Object.keys(colors2.light).filter((key) => !(key in colors2.dark));
670
+ if (missing.length) {
671
+ throw new Error(`Preset "${name}" sets ${missing.join(", ")} for light but not for dark`);
672
+ }
673
+ }
674
+ if (density !== void 0 && !(density >= 0.5 && density <= 1.5)) {
675
+ throw new Error(`Preset "${name}" density must be between 0.5 and 1.5, got ${density}`);
676
+ }
677
+ const parent = definition.extends ? states.get(definition.extends) : baseState();
678
+ if (!parent) throw new Error(`Preset "${name}" extends something that is not a preset`);
679
+ const state = {
680
+ primitives: new Map(parent.primitives),
681
+ roles: { ...parent.roles },
682
+ light: { ...parent.light, ...colors2?.light },
683
+ dark: { ...parent.dark, ...colors2?.dark }
684
+ };
685
+ for (const [group, overrides] of Object.entries(primitives)) {
686
+ for (const [property, value] of flatten(PRIMITIVE_PREFIX[group], overrides)) {
687
+ if (!state.primitives.has(property)) throw new Error(`Unknown primitive ${property}`);
688
+ state.primitives.set(property, value);
689
+ }
690
+ }
691
+ const resolve = resolveIn(state);
692
+ for (const role of Object.keys(state.roles)) {
693
+ const explicit = roles2[role];
694
+ if (explicit !== void 0) state.roles[role] = explicit;
695
+ else if (density !== void 0 && density !== 1 && DENSITY_ROLE.test(role)) {
696
+ state.roles[role] = scaleLengths(resolve(state.roles[role]), density);
697
+ }
698
+ }
699
+ for (const role of Object.keys(state.roles)) {
700
+ const value = state.roles[role];
701
+ if (value === "0" && role.startsWith("stroke")) state.roles[role] = "0px";
702
+ if (value === "0" && role.endsWith("Tracking")) state.roles[role] = "0em";
703
+ }
704
+ const focus = /^([\d.]+)px$/.exec(resolve(state.roles.strokeFocus));
705
+ if (focus && parseFloat(focus[1]) < 2) {
706
+ throw new Error(`Preset "${name}" focus ring must stay at least 2px wide (WCAG 2.4.13)`);
707
+ }
708
+ const resolveAll = (record) => Object.fromEntries(
709
+ Object.entries(record).map(([key, value]) => [key, resolve(value)])
710
+ );
711
+ const preset = {
712
+ name,
713
+ label: definition.label ?? name.charAt(0).toUpperCase() + name.slice(1),
714
+ description: definition.description,
715
+ fontImports: [fontImport ?? []].flat(),
716
+ declarations: {
717
+ shape: [
718
+ ...state.primitives,
719
+ ...Object.entries(state.roles).map(
720
+ ([role, value]) => [roleName(role), cssValue(value)]
721
+ )
722
+ ],
723
+ light: Object.entries(state.light).map(
724
+ ([role, value]) => [roleName(role), cssValue(value)]
725
+ ),
726
+ dark: Object.entries(state.dark).map(([role, value]) => [roleName(role), value])
727
+ },
728
+ resolved: {
729
+ roles: resolveAll(state.roles),
730
+ light: resolveAll(state.light),
731
+ dark: resolveAll(state.dark)
732
+ }
733
+ };
734
+ states.set(preset, state);
735
+ return preset;
736
+ }
737
+ var block = (selector, lines, indent = "") => [
738
+ `${indent}${selector} {`,
739
+ ...lines.map((line) => `${indent} ${line}`),
740
+ `${indent}}`
741
+ ];
742
+ var declare = (entries) => entries.map(([name, value]) => `${name}: ${value};`);
743
+ var selectors = (name, scope) => {
744
+ if (scope === "root") {
745
+ return {
746
+ shape: ":root",
747
+ light: ":root:not([data-theme='dark'])",
748
+ dark: ":root[data-theme='dark']",
749
+ auto: ":root:root:not([data-theme='light'])"
750
+ };
751
+ }
752
+ const p = `[data-preset='${name}']`;
753
+ return {
754
+ shape: p,
755
+ light: `${p}:not([data-theme='dark'], [data-theme='dark'] *)`,
756
+ dark: `${p}[data-theme='dark'],
757
+ [data-theme='dark'] ${p}`,
758
+ auto: `${p}:root:not([data-theme='light']),
759
+ :root:not([data-theme='light']) ${p}:not([data-theme='light'], [data-theme='light'] *)`
760
+ };
761
+ };
762
+ var comment = (text) => text.replaceAll("*/", "* /");
763
+ function generatePresetCss(preset, options = {}) {
764
+ const { scope = "attribute", scheme = "light" } = options;
765
+ const select = selectors(preset.name, scope);
766
+ const header = [
767
+ `/* ${comment(`${preset.label} preset${preset.description ? ` \u2014 ${preset.description}` : ""}`)}`,
768
+ " Generated by definePreset(). Do not edit directly. */"
769
+ ];
770
+ if (scheme === "light") {
771
+ return `${[
772
+ ...preset.fontImports.map((url) => `@import url(${JSON.stringify(url)});`),
773
+ ...header,
774
+ ...block(select.shape, declare(preset.declarations.shape)),
775
+ ...block(select.light, declare(preset.declarations.light))
776
+ ].join("\n")}
777
+ `;
778
+ }
779
+ const dark = ["color-scheme: dark;", ...declare(preset.declarations.dark)];
780
+ const lines = [...header, ...block(select.dark, dark)];
781
+ if (scheme === "dark-auto") {
782
+ lines.push(
783
+ "",
784
+ "@media (prefers-color-scheme: dark) {",
785
+ ...block(select.auto.replaceAll("\n", "\n "), dark, " "),
786
+ "}"
787
+ );
788
+ }
789
+ return `${lines.join("\n")}
790
+ `;
791
+ }
792
+ function presetStylesheets(preset) {
793
+ const files = {};
794
+ for (const scope of ["attribute", "root"]) {
795
+ const base = scope === "root" ? `${preset.name}.root` : preset.name;
796
+ files[`${base}.css`] = generatePresetCss(preset, { scope, scheme: "light" });
797
+ files[`${base}-dark.css`] = generatePresetCss(preset, { scope, scheme: "dark" });
798
+ files[`${base}-dark-auto.css`] = generatePresetCss(preset, { scope, scheme: "dark-auto" });
799
+ }
800
+ return files;
801
+ }
802
+
803
+ // src/presets/compact.ts
804
+ var compact = definePreset({
805
+ name: "compact",
806
+ label: "Compact",
807
+ description: "The Loidolt look with tighter padding and smaller controls.",
808
+ density: 0.75,
809
+ primitives: {
810
+ sizing: {
811
+ // 28px: still above the 24px WCAG 2.5.8 minimum.
812
+ controlSm: "1.75rem",
813
+ control: "2.125rem",
814
+ controlLg: "2.5rem",
815
+ topbar: "3.25rem",
816
+ contextbar: "2.25rem"
817
+ }
818
+ }
819
+ });
820
+
821
+ // src/presets/loidolt.ts
822
+ var loidolt = definePreset({
823
+ name: "loidolt",
824
+ label: "Loidolt",
825
+ description: "Square corners, tracked uppercase labels, warm paper and orange."
826
+ });
827
+
828
+ // src/presets/soft.ts
829
+ var systemSans = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
830
+ var soft = definePreset({
831
+ name: "soft",
832
+ label: "Soft",
833
+ description: "Rounded corners, sentence-case labels, cool neutrals and a blue accent.",
834
+ density: 1.15,
835
+ primitives: {
836
+ typography: {
837
+ display: systemSans,
838
+ utility: systemSans,
839
+ // Sentence case at 11px is harder to read than uppercase at 11px; give it the extra pixel.
840
+ size: { xs: "0.75rem" }
841
+ },
842
+ borders: { radius: "6px" }
843
+ },
844
+ roles: {
845
+ radiusControl: "6px",
846
+ radiusInner: "4px",
847
+ radiusSurface: "8px",
848
+ radiusOverlay: "10px",
849
+ labelTransform: "none",
850
+ labelTracking: "0em",
851
+ labelTrackingWide: "0.02em",
852
+ labelWeight: "500",
853
+ controlTransform: "none",
854
+ controlTracking: "0em",
855
+ controlWeight: "500",
856
+ headingWeight: "600"
857
+ },
858
+ colors: {
859
+ light: {
860
+ background: "#f4f6f8",
861
+ surface: "#ffffff",
862
+ surfaceAlt: "#f8fafc",
863
+ surfaceSunken: "#e9edf2",
864
+ surfacePaper: "#ffffff",
865
+ surfaceInverse: "#1c2430",
866
+ surfaceInput: "#ffffff",
867
+ text: "#1c2430",
868
+ textMuted: "#556070",
869
+ textInverse: "#f4f6f8",
870
+ textAccent: "#1d4ed8",
871
+ border: "#d5dbe3",
872
+ borderSoft: "#e4e8ee",
873
+ borderStrong: "#8792a2",
874
+ borderControl: "#7b8696",
875
+ borderSunken: "#d5dbe3",
876
+ accent: "#2563eb",
877
+ accentHover: "#1d4ed8",
878
+ onAccent: "#ffffff",
879
+ danger: "#c62828",
880
+ dangerHover: "#a51d1d",
881
+ onDanger: "#ffffff",
882
+ textDanger: "#b42318",
883
+ success: "#1f7a4d",
884
+ onSuccess: "#ffffff",
885
+ textSuccess: "#1a6a42",
886
+ warning: "#a15c07",
887
+ onWarning: "#ffffff",
888
+ textWarning: "#8a4b06",
889
+ info: "#0e6b8c",
890
+ onInfo: "#ffffff",
891
+ textInfo: "#0b5c78",
892
+ focusRing: "#2563eb",
893
+ shadowColor: "#1c2430"
894
+ },
895
+ dark: {
896
+ background: "#11151b",
897
+ surface: "#181d25",
898
+ surfaceAlt: "#1e2430",
899
+ surfaceSunken: "#0c0f13",
900
+ surfacePaper: "#ffffff",
901
+ surfaceInverse: "#e6eaf0",
902
+ surfaceInput: "#1e2430",
903
+ text: "#e6eaf0",
904
+ textMuted: "#9aa5b4",
905
+ textInverse: "#11151b",
906
+ textAccent: "#8ab4ff",
907
+ border: "#2d3542",
908
+ borderSoft: "#232a35",
909
+ borderStrong: "#5b6677",
910
+ borderControl: "#6f7a8b",
911
+ borderSunken: "#1f2630",
912
+ accent: "#6aa1ff",
913
+ accentHover: "#8ab4ff",
914
+ onAccent: "#0c1220",
915
+ danger: "#f07167",
916
+ dangerHover: "#f4928a",
917
+ onDanger: "#11151b",
918
+ textDanger: "#f59a92",
919
+ success: "#5cc08a",
920
+ onSuccess: "#11151b",
921
+ textSuccess: "#74cf9c",
922
+ warning: "#e0a24a",
923
+ onWarning: "#11151b",
924
+ textWarning: "#e8b468",
925
+ info: "#5bb5d6",
926
+ onInfo: "#11151b",
927
+ textInfo: "#7cc6e2",
928
+ focusRing: "#8ab4ff",
929
+ shadowColor: "#000000"
930
+ }
931
+ }
932
+ });
933
+
934
+ // src/presets/index.ts
935
+ var builtInPresets = [loidolt, soft, compact];
936
+
937
+ // src/index.ts
303
938
  var index_default = tokens;
304
939
  export {
305
940
  aspectRatio,
941
+ auditContrast,
306
942
  borders,
307
943
  breakpoints,
944
+ builtInPresets,
945
+ chartColors,
946
+ chartRoles,
308
947
  colors,
948
+ compact,
949
+ contrastPairs,
309
950
  contrastRequirements,
310
951
  cssVarName,
311
952
  darkSemantic,
312
953
  index_default as default,
954
+ definePreset,
313
955
  flattenTokens,
314
956
  generateCss,
315
957
  generateDarkCss,
958
+ generatePresetCss,
316
959
  getContrastRatio,
317
960
  getContrastTextColor,
318
961
  getLuminance,
962
+ loidolt,
963
+ mapColors,
964
+ mapRoles,
319
965
  meetsContrast,
320
966
  motion,
967
+ presetStylesheets,
968
+ resolveRoles,
969
+ roleValue,
970
+ roleVar,
971
+ roles,
321
972
  semantic,
322
973
  shadows,
323
974
  sizing,
975
+ soft,
324
976
  spacing,
977
+ syntaxColors,
978
+ syntaxRoles,
325
979
  tokens,
326
980
  typography,
327
981
  validateContrast,