@motion-proto/live-tokens 0.49.1 → 0.52.1

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.
Files changed (111) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist-plugin/adjust/index.d.cts +1 -1
  3. package/dist-plugin/adjust/index.d.ts +1 -1
  4. package/dist-plugin/{chunk-76TFDTJO.js → chunk-JIGIIE5P.js} +60 -132
  5. package/dist-plugin/{chunk-44RSTAII.js → chunk-TKZBVIW5.js} +33 -5
  6. package/dist-plugin/chunk-YR6GPXW2.js +129 -0
  7. package/dist-plugin/generateColorsAndType/index.cjs +100 -33
  8. package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
  9. package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
  10. package/dist-plugin/generateColorsAndType/index.js +35 -14
  11. package/dist-plugin/index.cjs +95 -55
  12. package/dist-plugin/index.js +5 -3
  13. package/dist-plugin/{themeTypes-DSwKq-bj.d.cts → themeTypes-Dky03HL9.d.cts} +18 -5
  14. package/dist-plugin/{themeTypes-DSwKq-bj.d.ts → themeTypes-Dky03HL9.d.ts} +18 -5
  15. package/dist-plugin/tokensCssMigrations/index.cjs +71 -5
  16. package/dist-plugin/tokensCssMigrations/index.d.cts +20 -9
  17. package/dist-plugin/tokensCssMigrations/index.d.ts +20 -9
  18. package/dist-plugin/tokensCssMigrations/index.js +4 -1
  19. package/package.json +8 -1
  20. package/src/editor/bootstrap.ts +2 -0
  21. package/src/editor/component-editor/ButtonEditor.svelte +1 -1
  22. package/src/editor/component-editor/CardEditor.svelte +5 -2
  23. package/src/editor/component-editor/IconButtonEditor.svelte +1 -1
  24. package/src/editor/component-editor/ImageEditor.svelte +1 -1
  25. package/src/editor/component-editor/NotificationEditor.svelte +6 -0
  26. package/src/editor/component-editor/SectionDividerEditor.svelte +4 -1
  27. package/src/editor/component-editor/SideNavigationEditor.svelte +6 -0
  28. package/src/editor/component-editor/TabBarEditor.svelte +1 -5
  29. package/src/editor/component-editor/scaffolding/ComponentEditorBase.svelte +8 -1
  30. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +10 -32
  31. package/src/editor/component-editor/scaffolding/StateBlock.svelte +2 -0
  32. package/src/editor/component-editor/scaffolding/TypeEditor.svelte +3 -1
  33. package/src/editor/component-editor/scaffolding/types.ts +3 -0
  34. package/src/editor/core/components/componentConfigService.ts +25 -0
  35. package/src/editor/core/cssVarSync.ts +65 -7
  36. package/src/editor/core/fonts/fontLoader.ts +12 -3
  37. package/src/editor/core/fonts/fontWeightAvailability.ts +97 -0
  38. package/src/editor/core/palettes/oklch.ts +3 -137
  39. package/src/editor/core/palettes/paletteDerivation.ts +79 -37
  40. package/src/editor/core/preview/lookPreview.ts +23 -18
  41. package/src/editor/core/store/editorRenderer.ts +22 -7
  42. package/src/editor/core/store/editorStore.ts +31 -0
  43. package/src/editor/core/store/gradientSource.ts +5 -0
  44. package/src/editor/core/themes/colorsAndTypeService.ts +2 -12
  45. package/src/editor/core/themes/generateColorsAndType.ts +18 -8
  46. package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +4 -1
  47. package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +4 -1
  48. package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +8 -0
  49. package/src/editor/core/themes/slices/fonts.ts +4 -5
  50. package/src/editor/core/themes/themeDocumentSync.ts +66 -0
  51. package/src/editor/core/themes/themeInit.ts +7 -10
  52. package/src/editor/core/themes/themeService.ts +10 -3
  53. package/src/editor/core/themes/themeTypes.ts +18 -6
  54. package/src/editor/docs/content/editing-tokens.md +6 -2
  55. package/src/editor/docs/content/themes-workflow.md +14 -11
  56. package/src/editor/docs/content/where-themes-live.md +2 -2
  57. package/src/editor/docs/content.generated.ts +3 -3
  58. package/src/editor/ui/BezierCurveEditor.svelte +90 -67
  59. package/src/editor/ui/FileLoadList.svelte +1 -0
  60. package/src/editor/ui/FontStackEditor.svelte +1 -2
  61. package/src/editor/ui/GradientEditor.svelte +5 -1
  62. package/src/editor/ui/PaletteEditor.svelte +150 -44
  63. package/src/editor/ui/ProjectFontsSection.svelte +0 -5
  64. package/src/editor/ui/ThemePanel.svelte +206 -42
  65. package/src/editor/ui/Toggle.svelte +14 -2
  66. package/src/editor/ui/UIDialog.svelte +7 -1
  67. package/src/editor/ui/UIFontFamilySelector.svelte +13 -13
  68. package/src/editor/ui/UIFontSizeSelector.svelte +5 -5
  69. package/src/editor/ui/UIFontWeightSelector.svelte +52 -2
  70. package/src/editor/ui/UIOptionItem.svelte +14 -1
  71. package/src/editor/ui/UIPaddingSelector.svelte +4 -3
  72. package/src/editor/ui/UIPaletteSelector.svelte +20 -13
  73. package/src/editor/ui/UIRelinkConfirmDialog.svelte +18 -3
  74. package/src/editor/ui/UITextTransformSelector.svelte +5 -5
  75. package/src/editor/ui/UITokenSelector.svelte +29 -7
  76. package/src/editor/ui/colors/ColorStory.svelte +12 -8
  77. package/src/editor/ui/curveEngine.ts +101 -21
  78. package/src/editor/ui/palette/OverridesPanel.svelte +19 -5
  79. package/src/editor/ui/palette/PaletteBase.svelte +17 -78
  80. package/src/editor/ui/palette/ScaleCurveEditor.svelte +109 -20
  81. package/src/editor/ui/palette/curveSummary.ts +20 -0
  82. package/src/editor/ui/palette/dockMagnify.ts +5 -13
  83. package/src/editor/ui/palette/paletteMath.ts +7 -4
  84. package/src/live-tokens/data/colors-and-type/autumn.json +528 -528
  85. package/src/live-tokens/data/colors-and-type/default.json +247 -247
  86. package/src/live-tokens/data/colors-and-type/halloween.json +532 -532
  87. package/src/live-tokens/data/colors-and-type/midnight-study.json +538 -538
  88. package/src/live-tokens/data/colors-and-type/ocean.json +521 -521
  89. package/src/live-tokens/data/colors-and-type/royal-velvet.json +532 -532
  90. package/src/live-tokens/data/colors-and-type/spring-meadow.json +504 -504
  91. package/src/live-tokens/data/colors-and-type/sunset.json +531 -531
  92. package/src/live-tokens/data/themes/autumn.json +527 -527
  93. package/src/live-tokens/data/themes/halloween.json +531 -531
  94. package/src/live-tokens/data/themes/midnight-study.json +537 -537
  95. package/src/live-tokens/data/themes/ocean.json +520 -520
  96. package/src/live-tokens/data/themes/royal-velvet.json +531 -531
  97. package/src/live-tokens/data/themes/spring-meadow.json +516 -514
  98. package/src/live-tokens/data/themes/sunset.json +531 -531
  99. package/src/live-tokens/data/tokens.generated.css +282 -282
  100. package/src/system/components/Button.svelte +2 -1
  101. package/src/system/components/FloatingTokenTags.css +2 -2
  102. package/src/system/components/FloatingTokenTags.svelte +11 -0
  103. package/src/system/components/IconButton.svelte +2 -1
  104. package/src/system/components/Image.svelte +9 -3
  105. package/src/system/components/ImageLightbox.svelte +2 -0
  106. package/src/system/components/SectionDivider.svelte +20 -2
  107. package/src/system/components/SideNavigation.svelte +15 -1
  108. package/src/system/internal/backgroundContrast.ts +7 -0
  109. package/src/system/internal/oklch.ts +189 -0
  110. package/src/system/styles/fonts.css +6 -6
  111. package/src/system/styles/tokens.css +283 -283
@@ -17,6 +17,8 @@
17
17
  /** Content images default to lazy; pass `'eager'` for above-the-fold heroes. */
18
18
  loading?: 'lazy' | 'eager';
19
19
  decoding?: 'async' | 'sync' | 'auto';
20
+ /** Editor preview hook: paint the hover transform without pointer input. */
21
+ forceHover?: boolean;
20
22
  }
21
23
 
22
24
  let {
@@ -29,7 +31,8 @@
29
31
  srcset = undefined,
30
32
  sizes = undefined,
31
33
  loading = 'lazy',
32
- decoding = 'async'
34
+ decoding = 'async',
35
+ forceHover = false,
33
36
  }: Props = $props();
34
37
 
35
38
  const variantHeights: Record<string, string | undefined> = {
@@ -60,6 +63,7 @@
60
63
 
61
64
  <div
62
65
  class="image"
66
+ class:force-hover={forceHover}
63
67
  style:height={resolvedHeight}
64
68
  style:--image-zoom-hover={contentHover}
65
69
  style:--image-grow-hover={frameHover}
@@ -100,11 +104,13 @@
100
104
  transition: transform var(--duration-300) var(--ease-out-cubic);
101
105
  }
102
106
 
103
- .image:hover {
107
+ .image:hover,
108
+ .image.force-hover {
104
109
  transform: var(--image-grow-hover);
105
110
  }
106
111
 
107
- .image:hover img {
112
+ .image:hover img,
113
+ .image.force-hover img {
108
114
  transform: var(--image-zoom-hover);
109
115
  }
110
116
  </style>
@@ -585,6 +585,8 @@
585
585
  style:--imagelightbox-tile-object-fit={fit}
586
586
  type="button"
587
587
  aria-label={cover?.alt ? `Expand image: ${cover.alt}` : 'Expand image'}
588
+ aria-haspopup="dialog"
589
+ aria-expanded={open}
588
590
  onclick={openLightbox}
589
591
  >
590
592
  <img src={cover?.src} alt={cover?.alt} draggable="false" onload={onCoverLoad} />
@@ -63,11 +63,28 @@
63
63
  });
64
64
 
65
65
  onMount(() => {
66
+ let mounted = true;
67
+ const sync = () => { measure(); syncFilter(); };
66
68
  measure();
67
69
  syncFilter();
68
- const obs = new MutationObserver(() => { measure(); syncFilter(); });
70
+ const obs = new MutationObserver(sync);
69
71
  obs.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });
70
- return () => obs.disconnect();
72
+
73
+ // A font-family variable changes before its webfont necessarily finishes
74
+ // loading. Re-run getBBox when the real face becomes available so the SVG
75
+ // viewBox and outline do not stay sized for the fallback font.
76
+ const fonts = document.fonts;
77
+ const onFontsLoaded = () => {
78
+ if (mounted) sync();
79
+ };
80
+ fonts?.addEventListener('loadingdone', onFontsLoaded);
81
+ void fonts?.ready.then(onFontsLoaded);
82
+
83
+ return () => {
84
+ mounted = false;
85
+ obs.disconnect();
86
+ fonts?.removeEventListener('loadingdone', onFontsLoaded);
87
+ };
71
88
  });
72
89
  </script>
73
90
 
@@ -531,6 +548,7 @@
531
548
  font-family: var(--_divider-title-font-family);
532
549
  font-weight: var(--_divider-title-font-weight);
533
550
  font-size: var(--_divider-title-font-size);
551
+ line-height: var(--_divider-title-line-height);
534
552
  letter-spacing: var(--_divider-title-letter-spacing);
535
553
  fill: currentColor;
536
554
  }
@@ -124,7 +124,7 @@
124
124
  if (forceHoverPart !== 'item') return null;
125
125
  const s = sections[0];
126
126
  if (!s || s.items.length === 0) return null;
127
- return (s.items[1] ?? s.items[0]).path;
127
+ return s.items.find((item) => item.path !== currentPath)?.path ?? s.items[0].path;
128
128
  });
129
129
 
130
130
  let titleActive = $derived(forceActivePart === 'title' || currentPath === '');
@@ -471,6 +471,8 @@
471
471
  --_surface: var(--sidenavigation-title-default-surface);
472
472
  --_border: var(--sidenavigation-title-default-border);
473
473
  --_border-width: var(--sidenavigation-title-default-border-width);
474
+ --_indicator: var(--sidenavigation-title-default-accent);
475
+ --_indicator-width: var(--sidenavigation-title-default-accent-width);
474
476
  --_padding: var(--sidenavigation-title-default-padding);
475
477
  --_label: var(--sidenavigation-title-default-label);
476
478
  --_label-family: var(--sidenavigation-title-default-label-font-family);
@@ -490,6 +492,8 @@
490
492
  justify-content: center;
491
493
  gap: var(--sidenavigation-title-gap);
492
494
  background: var(--_surface);
495
+ border: var(--_border-width) solid var(--_border);
496
+ border-left: var(--_indicator-width) solid var(--_indicator);
493
497
  border-radius: var(--sidenavigation-title-radius);
494
498
  @include themed-padding(--_padding);
495
499
  transition: background var(--duration-150);
@@ -500,6 +504,8 @@
500
504
  --_surface: var(--sidenavigation-title-hover-surface);
501
505
  --_border: var(--sidenavigation-title-hover-border);
502
506
  --_border-width: var(--sidenavigation-title-hover-border-width);
507
+ --_indicator: var(--sidenavigation-title-hover-accent);
508
+ --_indicator-width: var(--sidenavigation-title-hover-accent-width);
503
509
  --_padding: var(--sidenavigation-title-hover-padding);
504
510
  --_label: var(--sidenavigation-title-hover-label);
505
511
  --_label-family: var(--sidenavigation-title-hover-label-font-family);
@@ -512,6 +518,8 @@
512
518
  --_surface: var(--sidenavigation-title-active-surface);
513
519
  --_border: var(--sidenavigation-title-active-border);
514
520
  --_border-width: var(--sidenavigation-title-active-border-width);
521
+ --_indicator: var(--sidenavigation-title-active-accent);
522
+ --_indicator-width: var(--sidenavigation-title-active-accent-width);
515
523
  --_padding: var(--sidenavigation-title-active-padding);
516
524
  --_label: var(--sidenavigation-title-active-label);
517
525
  --_label-family: var(--sidenavigation-title-active-label-font-family);
@@ -530,6 +538,7 @@
530
538
 
531
539
  .sn-title-label {
532
540
  background: var(--sidenavigation-title-label-surface);
541
+ border: var(--sidenavigation-title-label-border-width) solid var(--sidenavigation-title-label-border);
533
542
  border-radius: var(--sidenavigation-title-label-radius);
534
543
  @include themed-padding(--sidenavigation-title-label-padding);
535
544
  color: var(--_label);
@@ -652,6 +661,11 @@
652
661
  --_surface: var(--sidenavigation-section-hover-surface);
653
662
  --_indicator: var(--sidenavigation-section-hover-accent);
654
663
  --_indicator-width: var(--sidenavigation-section-hover-accent-width);
664
+ --collapsiblesection-chromeless-default-label: var(--sidenavigation-section-hover-text);
665
+ --collapsiblesection-chromeless-default-label-font-family: var(--sidenavigation-section-hover-text-font-family);
666
+ --collapsiblesection-chromeless-default-label-font-size: var(--sidenavigation-section-hover-text-font-size);
667
+ --collapsiblesection-chromeless-default-label-font-weight: var(--sidenavigation-section-hover-text-font-weight);
668
+ --collapsiblesection-chromeless-default-label-line-height: var(--sidenavigation-section-hover-text-line-height);
655
669
  }
656
670
  .sn-section-header.active {
657
671
  --_surface: var(--sidenavigation-section-active-surface);
@@ -1,8 +1,11 @@
1
+ import { parseOklchCss, oklchToRgb255 } from './oklch';
2
+
1
3
  export type ContrastColorToken = '--color-black' | '--color-white';
2
4
 
3
5
  type Rgb = { r: number; g: number; b: number };
4
6
 
5
7
  const HEX_RE = /#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})\b/gi;
8
+ const OKLCH_RE = /oklch\(\s*[\d.]+%?\s+[\d.]+%?\s+[\d.]+(?:deg)?\s*\)/gi;
6
9
  const RGB_RE = /rgba?\(\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)(?:\s*[,/]\s*[\d.]+%?)?\s*\)/gi;
7
10
 
8
11
  function channel(value: string): number {
@@ -24,6 +27,10 @@ function parseHex(hex: string): Rgb {
24
27
  function colorsIn(background: string): Rgb[] {
25
28
  const colors: Rgb[] = [];
26
29
  for (const match of background.matchAll(HEX_RE)) colors.push(parseHex(match[0]));
30
+ for (const match of background.matchAll(OKLCH_RE)) {
31
+ const parsed = parseOklchCss(match[0]);
32
+ if (parsed) colors.push(oklchToRgb255(parsed.l, parsed.c, parsed.h));
33
+ }
27
34
  for (const match of background.matchAll(RGB_RE)) {
28
35
  colors.push({ r: channel(match[1]), g: channel(match[2]), b: channel(match[3]) });
29
36
  }
@@ -0,0 +1,189 @@
1
+ // OKLCH color space conversions — hand-rolled, no dependencies
2
+ // Pipeline: sRGB hex → linear RGB → OKLab (via M1×M2 + cube root) → OKLCH (polar)
3
+
4
+ export interface Oklch {
5
+ l: number; // 0..1
6
+ c: number; // 0..~0.4
7
+ h: number; // 0..360
8
+ }
9
+
10
+ // --- sRGB gamma ---
11
+
12
+ function srgbToLinear(c: number): number {
13
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
14
+ }
15
+
16
+ function linearToSrgb(c: number): number {
17
+ return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
18
+ }
19
+
20
+ // --- Hex ↔ linear RGB ---
21
+
22
+ function hexToLinearRgb(hex: string): [number, number, number] {
23
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
24
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
25
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
26
+ return [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)];
27
+ }
28
+
29
+ function linearRgbToHex(r: number, g: number, b: number): string {
30
+ const toHex = (c: number) => {
31
+ const v = Math.round(Math.max(0, Math.min(1, linearToSrgb(c))) * 255);
32
+ return v.toString(16).padStart(2, '0');
33
+ };
34
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
35
+ }
36
+
37
+ // --- Linear RGB ↔ OKLab ---
38
+ // Uses the two-matrix approach from Björn Ottosson's blog
39
+
40
+ function linearRgbToOklab(r: number, g: number, b: number): [number, number, number] {
41
+ const l_ = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
42
+ const m_ = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
43
+ const s_ = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
44
+
45
+ const l = Math.cbrt(l_);
46
+ const m = Math.cbrt(m_);
47
+ const s = Math.cbrt(s_);
48
+
49
+ return [
50
+ 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
51
+ 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
52
+ 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s,
53
+ ];
54
+ }
55
+
56
+ function oklabToLinearRgb(L: number, a: number, b: number): [number, number, number] {
57
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
58
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
59
+ const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
60
+
61
+ const l = l_ * l_ * l_;
62
+ const m = m_ * m_ * m_;
63
+ const s = s_ * s_ * s_;
64
+
65
+ return [
66
+ +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
67
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
68
+ -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
69
+ ];
70
+ }
71
+
72
+ // --- OKLab ↔ OKLCH ---
73
+
74
+ function oklabToOklch(L: number, a: number, b: number): Oklch {
75
+ const c = Math.sqrt(a * a + b * b);
76
+ let h = (Math.atan2(b, a) * 180) / Math.PI;
77
+ if (h < 0) h += 360;
78
+ return { l: L, c, h };
79
+ }
80
+
81
+ function oklchToOklab(l: number, c: number, h: number): [number, number, number] {
82
+ const hRad = (h * Math.PI) / 180;
83
+ return [l, c * Math.cos(hRad), c * Math.sin(hRad)];
84
+ }
85
+
86
+ // --- Public API ---
87
+
88
+ export function hexToOklch(hex: string): Oklch {
89
+ const [r, g, b] = hexToLinearRgb(hex);
90
+ const [L, a, bVal] = linearRgbToOklab(r, g, b);
91
+ return oklabToOklch(L, a, bVal);
92
+ }
93
+
94
+ export function oklchToHex(l: number, c: number, h: number): string {
95
+ const [L, a, b] = oklchToOklab(l, c, h);
96
+ const [r, g, bVal] = oklabToLinearRgb(L, a, b);
97
+ return linearRgbToHex(r, g, bVal);
98
+ }
99
+
100
+ function isInGamut(r: number, g: number, b: number): boolean {
101
+ const eps = 0.0001;
102
+ return r >= -eps && r <= 1 + eps && g >= -eps && g <= 1 + eps && b >= -eps && b <= 1 + eps;
103
+ }
104
+
105
+ /** The clamped sRGB projection: reduce chroma into gamut, then serialize to hex.
106
+ * This is the correct projection for unclamped stored intent — naive
107
+ * `oklchToHex` per-channel clips (shifting hue), this preserves hue and L. */
108
+ export function oklchToHexClamped(l: number, c: number, h: number): string {
109
+ const g = gamutClamp(l, c, h);
110
+ return oklchToHex(g.l, g.c, g.h);
111
+ }
112
+
113
+ export function gamutClamp(l: number, c: number, h: number): Oklch {
114
+ // Clamp lightness
115
+ if (l <= 0) return { l: 0, c: 0, h };
116
+ if (l >= 1) return { l: 1, c: 0, h };
117
+
118
+ // Binary search: reduce chroma until in sRGB gamut
119
+ const [L, a, b] = oklchToOklab(l, c, h);
120
+ const [r, g, bVal] = oklabToLinearRgb(L, a, b);
121
+
122
+ if (isInGamut(r, g, bVal)) return { l, c, h };
123
+
124
+ let lo = 0;
125
+ let hi = c;
126
+ for (let i = 0; i < 20; i++) {
127
+ const mid = (lo + hi) / 2;
128
+ const [La, aa, ba] = oklchToOklab(l, mid, h);
129
+ const [rr, gg, bb] = oklabToLinearRgb(La, aa, ba);
130
+ if (isInGamut(rr, gg, bb)) {
131
+ lo = mid;
132
+ } else {
133
+ hi = mid;
134
+ }
135
+ }
136
+ return { l, c: lo, h };
137
+ }
138
+
139
+ // --- CSS serialization + parsing ---
140
+
141
+ function trim(value: number, decimals: number): string {
142
+ return Number(value.toFixed(decimals)).toString();
143
+ }
144
+
145
+ export function oklchToCss(l: number, c: number, h: number): string {
146
+ const cs = trim(c, 4);
147
+ // Hue is meaningless without chroma, and the OKLab round trip leaves noise
148
+ // there (#ffffff lands on h≈89.88). Pin it so achromatic values read as
149
+ // achromatic instead of arbitrary.
150
+ return `oklch(${trim(l, 4)} ${cs} ${cs === '0' ? '0' : trim(h, 2)})`;
151
+ }
152
+
153
+ /** The clamped sRGB projection as an `oklch()` string — the CSS counterpart of
154
+ * `oklchToHexClamped`. Clamping keeps flip-day output visually identical to
155
+ * the hex it replaces; emitting unclamped (wide-gamut) values is a separate
156
+ * decision. */
157
+ export function oklchToCssClamped(l: number, c: number, h: number): string {
158
+ const g = gamutClamp(l, c, h);
159
+ return oklchToCss(g.l, g.c, g.h);
160
+ }
161
+
162
+ const OKLCH_RE = /^oklch\(\s*([\d.]+)(%?)\s+([\d.]+)(%?)\s+([\d.]+)(?:deg)?\s*\)$/i;
163
+ const HEX6_RE = /^#[0-9a-f]{6}$/i;
164
+
165
+ export function parseOklchCss(value: string): Oklch | null {
166
+ const m = OKLCH_RE.exec(value.trim());
167
+ if (!m) return null;
168
+ const l = Number.parseFloat(m[1]) / (m[2] === '%' ? 100 : 1);
169
+ // Percentage chroma is relative to 0.4 per the CSS Color 4 definition.
170
+ const c = Number.parseFloat(m[3]) * (m[4] === '%' ? 0.004 : 1);
171
+ return { l, c, h: Number.parseFloat(m[5]) };
172
+ }
173
+
174
+ /** Input boundary for color strings arriving from CSS: accepts the `oklch()`
175
+ * this system now emits and the 6-digit hex it emitted before the flip, so
176
+ * themes promoted either side of it still reconcile. */
177
+ export function cssColorToOklch(value: string): Oklch | null {
178
+ const raw = value.trim();
179
+ if (HEX6_RE.test(raw)) return hexToOklch(raw);
180
+ return parseOklchCss(raw);
181
+ }
182
+
183
+ export function oklchToRgb255(l: number, c: number, h: number): { r: number; g: number; b: number } {
184
+ const clamped = gamutClamp(l, c, h);
185
+ const [L, a, bVal] = oklchToOklab(clamped.l, clamped.c, clamped.h);
186
+ const [r, g, b] = oklabToLinearRgb(L, a, bVal);
187
+ const to255 = (channel: number) => Math.round(Math.max(0, Math.min(1, linearToSrgb(channel))) * 255);
188
+ return { r: to255(r), g: to255(g), b: to255(b) };
189
+ }
@@ -4,11 +4,11 @@
4
4
  /* Adobe Typekit — fira-code */
5
5
  @import url('https://use.typekit.net/jes8oow.css');
6
6
 
7
- /* Google Fonts — Manrope */
8
- @import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&display=swap');
9
-
10
- /* Google Fonts — Arvo */
11
- @import url('https://fonts.googleapis.com/css2?family=Arvo:ital,wght@0,400;0,700;1,400;1,700&display=swap');
12
-
13
7
  /* Google Fonts — GFS Didot */
14
8
  @import url('https://fonts.googleapis.com/css2?family=GFS+Didot&display=swap');
9
+
10
+ /* Google Fonts — Comfortaa */
11
+ @import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@300..700&display=swap');
12
+
13
+ /* Google Fonts — Figtree */
14
+ @import url('https://fonts.googleapis.com/css2?family=Figtree:wght@300..900&display=swap');