@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.
- package/CHANGELOG.md +72 -0
- package/dist-plugin/adjust/index.d.cts +1 -1
- package/dist-plugin/adjust/index.d.ts +1 -1
- package/dist-plugin/{chunk-76TFDTJO.js → chunk-JIGIIE5P.js} +60 -132
- package/dist-plugin/{chunk-44RSTAII.js → chunk-TKZBVIW5.js} +33 -5
- package/dist-plugin/chunk-YR6GPXW2.js +129 -0
- package/dist-plugin/generateColorsAndType/index.cjs +100 -33
- package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
- package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
- package/dist-plugin/generateColorsAndType/index.js +35 -14
- package/dist-plugin/index.cjs +95 -55
- package/dist-plugin/index.js +5 -3
- package/dist-plugin/{themeTypes-DSwKq-bj.d.cts → themeTypes-Dky03HL9.d.cts} +18 -5
- package/dist-plugin/{themeTypes-DSwKq-bj.d.ts → themeTypes-Dky03HL9.d.ts} +18 -5
- package/dist-plugin/tokensCssMigrations/index.cjs +71 -5
- package/dist-plugin/tokensCssMigrations/index.d.cts +20 -9
- package/dist-plugin/tokensCssMigrations/index.d.ts +20 -9
- package/dist-plugin/tokensCssMigrations/index.js +4 -1
- package/package.json +8 -1
- package/src/editor/bootstrap.ts +2 -0
- package/src/editor/component-editor/ButtonEditor.svelte +1 -1
- package/src/editor/component-editor/CardEditor.svelte +5 -2
- package/src/editor/component-editor/IconButtonEditor.svelte +1 -1
- package/src/editor/component-editor/ImageEditor.svelte +1 -1
- package/src/editor/component-editor/NotificationEditor.svelte +6 -0
- package/src/editor/component-editor/SectionDividerEditor.svelte +4 -1
- package/src/editor/component-editor/SideNavigationEditor.svelte +6 -0
- package/src/editor/component-editor/TabBarEditor.svelte +1 -5
- package/src/editor/component-editor/scaffolding/ComponentEditorBase.svelte +8 -1
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +10 -32
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +2 -0
- package/src/editor/component-editor/scaffolding/TypeEditor.svelte +3 -1
- package/src/editor/component-editor/scaffolding/types.ts +3 -0
- package/src/editor/core/components/componentConfigService.ts +25 -0
- package/src/editor/core/cssVarSync.ts +65 -7
- package/src/editor/core/fonts/fontLoader.ts +12 -3
- package/src/editor/core/fonts/fontWeightAvailability.ts +97 -0
- package/src/editor/core/palettes/oklch.ts +3 -137
- package/src/editor/core/palettes/paletteDerivation.ts +79 -37
- package/src/editor/core/preview/lookPreview.ts +23 -18
- package/src/editor/core/store/editorRenderer.ts +22 -7
- package/src/editor/core/store/editorStore.ts +31 -0
- package/src/editor/core/store/gradientSource.ts +5 -0
- package/src/editor/core/themes/colorsAndTypeService.ts +2 -12
- package/src/editor/core/themes/generateColorsAndType.ts +18 -8
- package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +4 -1
- package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +4 -1
- package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +8 -0
- package/src/editor/core/themes/slices/fonts.ts +4 -5
- package/src/editor/core/themes/themeDocumentSync.ts +66 -0
- package/src/editor/core/themes/themeInit.ts +7 -10
- package/src/editor/core/themes/themeService.ts +10 -3
- package/src/editor/core/themes/themeTypes.ts +18 -6
- package/src/editor/docs/content/editing-tokens.md +6 -2
- package/src/editor/docs/content/themes-workflow.md +14 -11
- package/src/editor/docs/content/where-themes-live.md +2 -2
- package/src/editor/docs/content.generated.ts +3 -3
- package/src/editor/ui/BezierCurveEditor.svelte +90 -67
- package/src/editor/ui/FileLoadList.svelte +1 -0
- package/src/editor/ui/FontStackEditor.svelte +1 -2
- package/src/editor/ui/GradientEditor.svelte +5 -1
- package/src/editor/ui/PaletteEditor.svelte +150 -44
- package/src/editor/ui/ProjectFontsSection.svelte +0 -5
- package/src/editor/ui/ThemePanel.svelte +206 -42
- package/src/editor/ui/Toggle.svelte +14 -2
- package/src/editor/ui/UIDialog.svelte +7 -1
- package/src/editor/ui/UIFontFamilySelector.svelte +13 -13
- package/src/editor/ui/UIFontSizeSelector.svelte +5 -5
- package/src/editor/ui/UIFontWeightSelector.svelte +52 -2
- package/src/editor/ui/UIOptionItem.svelte +14 -1
- package/src/editor/ui/UIPaddingSelector.svelte +4 -3
- package/src/editor/ui/UIPaletteSelector.svelte +20 -13
- package/src/editor/ui/UIRelinkConfirmDialog.svelte +18 -3
- package/src/editor/ui/UITextTransformSelector.svelte +5 -5
- package/src/editor/ui/UITokenSelector.svelte +29 -7
- package/src/editor/ui/colors/ColorStory.svelte +12 -8
- package/src/editor/ui/curveEngine.ts +101 -21
- package/src/editor/ui/palette/OverridesPanel.svelte +19 -5
- package/src/editor/ui/palette/PaletteBase.svelte +17 -78
- package/src/editor/ui/palette/ScaleCurveEditor.svelte +109 -20
- package/src/editor/ui/palette/curveSummary.ts +20 -0
- package/src/editor/ui/palette/dockMagnify.ts +5 -13
- package/src/editor/ui/palette/paletteMath.ts +7 -4
- package/src/live-tokens/data/colors-and-type/autumn.json +528 -528
- package/src/live-tokens/data/colors-and-type/default.json +247 -247
- package/src/live-tokens/data/colors-and-type/halloween.json +532 -532
- package/src/live-tokens/data/colors-and-type/midnight-study.json +538 -538
- package/src/live-tokens/data/colors-and-type/ocean.json +521 -521
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +532 -532
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +504 -504
- package/src/live-tokens/data/colors-and-type/sunset.json +531 -531
- package/src/live-tokens/data/themes/autumn.json +527 -527
- package/src/live-tokens/data/themes/halloween.json +531 -531
- package/src/live-tokens/data/themes/midnight-study.json +537 -537
- package/src/live-tokens/data/themes/ocean.json +520 -520
- package/src/live-tokens/data/themes/royal-velvet.json +531 -531
- package/src/live-tokens/data/themes/spring-meadow.json +516 -514
- package/src/live-tokens/data/themes/sunset.json +531 -531
- package/src/live-tokens/data/tokens.generated.css +282 -282
- package/src/system/components/Button.svelte +2 -1
- package/src/system/components/FloatingTokenTags.css +2 -2
- package/src/system/components/FloatingTokenTags.svelte +11 -0
- package/src/system/components/IconButton.svelte +2 -1
- package/src/system/components/Image.svelte +9 -3
- package/src/system/components/ImageLightbox.svelte +2 -0
- package/src/system/components/SectionDivider.svelte +20 -2
- package/src/system/components/SideNavigation.svelte +15 -1
- package/src/system/internal/backgroundContrast.ts +7 -0
- package/src/system/internal/oklch.ts +189 -0
- package/src/system/styles/fonts.css +6 -6
- 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(
|
|
70
|
+
const obs = new MutationObserver(sync);
|
|
69
71
|
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });
|
|
70
|
-
|
|
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
|
|
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');
|