@motion-proto/live-tokens 0.60.1 → 0.62.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/.claude/skills/live-tokens-build-page/SKILL.md +1 -1
- package/.claude/skills/live-tokens-pair-fonts/SKILL.md +1 -1
- package/CHANGELOG.md +107 -0
- package/dist-plugin/{chunk-XWXIMTWZ.js → chunk-7TQQTHI6.js} +10 -1
- package/dist-plugin/{chunk-LW4SR7AZ.js → chunk-O6GQ6GBH.js} +1 -1
- package/dist-plugin/{chunk-E5QYON4L.js → chunk-OPYOK2CA.js} +77 -26
- package/dist-plugin/generateColorsAndType/index.js +2 -2
- package/dist-plugin/index.cjs +179 -25
- package/dist-plugin/index.js +99 -4
- package/dist-plugin/tokensCssMigrations/index.cjs +76 -25
- package/dist-plugin/tokensCssMigrations/index.js +2 -2
- package/package.json +6 -1
- package/src/app/site.css +16 -0
- package/src/editor/component-editor/CardEditor.svelte +7 -1
- package/src/editor/component-editor/ImageLightboxEditor.svelte +2 -1
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +25 -2
- package/src/editor/core/sketch/maskField.ts +70 -34
- package/src/editor/core/sketch/sketchLayer.ts +24 -3
- package/src/editor/core/sketch/sketchPresets.ts +10 -11
- package/src/editor/core/sketch/sketchStore.ts +89 -18
- package/src/editor/docs/Docs.svelte +1 -0
- package/src/editor/docs/chapters.ts +1 -0
- package/src/editor/docs/content/light-and-dark.md +90 -0
- package/src/editor/docs/content/sketch-mode.md +7 -2
- package/src/editor/docs/content.generated.ts +2 -1
- package/src/editor/overlay/LiveTokensRouter.svelte +8 -0
- package/src/editor/ui/sections/textStyles.ts +15 -1
- package/src/editor/ui/sketch/SketchTab.svelte +33 -81
- package/src/live-tokens/data/colors-and-type/midnight-study.json +40 -30
- package/src/live-tokens/data/themes/autumn.json +4 -3
- package/src/live-tokens/data/themes/halloween.json +4 -3
- package/src/live-tokens/data/themes/midnight-study.json +62 -49
- package/src/live-tokens/data/themes/ocean.json +4 -3
- package/src/live-tokens/data/themes/royal-velvet.json +4 -3
- package/src/live-tokens/data/themes/sketchy.json +4 -3
- package/src/live-tokens/data/themes/spring-meadow.json +4 -3
- package/src/live-tokens/data/themes/sunset.json +4 -3
- package/src/live-tokens/data/tokens.generated.css +5 -0
- package/src/system/backdrop/backdrop.ts +197 -0
- package/src/system/backdrop/index.ts +16 -0
- package/src/system/backdrop/watch.ts +139 -0
- package/src/system/components/Card.svelte +27 -9
- package/src/system/components/FloatingTokenTags.svelte +1 -1
- package/src/system/components/ImageLightbox.svelte +8 -4
- package/src/system/components/SectionDivider.svelte +3 -3
- package/src/system/components/SegmentedControl.svelte +11 -9
- package/src/system/styles/tokens.css +33 -5
- package/template/src/pages/Home.svelte +1 -11
- package/src/system/internal/backgroundContrast.ts +0 -62
|
@@ -3179,6 +3179,7 @@
|
|
|
3179
3179
|
"createdAt": "2026-05-26T02:03:38.863Z",
|
|
3180
3180
|
"updatedAt": "2026-08-25T01:13:11.520Z",
|
|
3181
3181
|
"aliases": {
|
|
3182
|
+
"--imagelightbox-tile-surface": "--color-transparent",
|
|
3182
3183
|
"--imagelightbox-tile-radius": "--radius-none",
|
|
3183
3184
|
"--imagelightbox-tile-border": "--color-transparent",
|
|
3184
3185
|
"--imagelightbox-tile-border-width": "--border-width-1",
|
|
@@ -3542,7 +3543,7 @@
|
|
|
3542
3543
|
"--sectiondivider-lg-description": "--text-alternate",
|
|
3543
3544
|
"--sectiondivider-lg-eyebrow": "--text-brand",
|
|
3544
3545
|
"--sectiondivider-lg-border": "--color-transparent",
|
|
3545
|
-
"--sectiondivider-lg-title-outline-color": "
|
|
3546
|
+
"--sectiondivider-lg-title-outline-color": "transparent",
|
|
3546
3547
|
"--sectiondivider-lg-hairline-color": "--border-brand-medium",
|
|
3547
3548
|
"--sectiondivider-md-title-font-family": "--font-display",
|
|
3548
3549
|
"--sectiondivider-md-title-font-weight": "--font-weight-bold",
|
|
@@ -3571,7 +3572,7 @@
|
|
|
3571
3572
|
"--sectiondivider-md-description": "--text-secondary",
|
|
3572
3573
|
"--sectiondivider-md-eyebrow": "--text-tertiary",
|
|
3573
3574
|
"--sectiondivider-md-border": "--color-transparent",
|
|
3574
|
-
"--sectiondivider-md-title-outline-color": "
|
|
3575
|
+
"--sectiondivider-md-title-outline-color": "transparent",
|
|
3575
3576
|
"--sectiondivider-md-hairline-color": "--border-brand-medium",
|
|
3576
3577
|
"--sectiondivider-sm-title-font-family": "--font-display",
|
|
3577
3578
|
"--sectiondivider-sm-title-font-weight": "--font-weight-medium",
|
|
@@ -3600,7 +3601,7 @@
|
|
|
3600
3601
|
"--sectiondivider-sm-description": "--text-secondary",
|
|
3601
3602
|
"--sectiondivider-sm-eyebrow": "--text-tertiary",
|
|
3602
3603
|
"--sectiondivider-sm-border": "--color-transparent",
|
|
3603
|
-
"--sectiondivider-sm-title-outline-color": "
|
|
3604
|
+
"--sectiondivider-sm-title-outline-color": "transparent",
|
|
3604
3605
|
"--sectiondivider-sm-hairline-color": "color-mix(in srgb, var(--border-brand-medium) 50%, transparent)"
|
|
3605
3606
|
}
|
|
3606
3607
|
},
|
|
@@ -3152,6 +3152,7 @@
|
|
|
3152
3152
|
"createdAt": "2026-05-26T02:03:38.863Z",
|
|
3153
3153
|
"updatedAt": "2026-08-19T22:39:32.785Z",
|
|
3154
3154
|
"aliases": {
|
|
3155
|
+
"--imagelightbox-tile-surface": "--color-transparent",
|
|
3155
3156
|
"--imagelightbox-tile-radius": "--radius-3xl",
|
|
3156
3157
|
"--imagelightbox-tile-border": "--color-transparent",
|
|
3157
3158
|
"--imagelightbox-tile-border-width": "--border-width-0",
|
|
@@ -3515,7 +3516,7 @@
|
|
|
3515
3516
|
"--sectiondivider-lg-description": "--text-alternate",
|
|
3516
3517
|
"--sectiondivider-lg-eyebrow": "--text-brand",
|
|
3517
3518
|
"--sectiondivider-lg-border": "--color-transparent",
|
|
3518
|
-
"--sectiondivider-lg-title-outline-color": "
|
|
3519
|
+
"--sectiondivider-lg-title-outline-color": "transparent",
|
|
3519
3520
|
"--sectiondivider-lg-hairline-color": "--border-brand-medium",
|
|
3520
3521
|
"--sectiondivider-md-title-font-family": "--font-display",
|
|
3521
3522
|
"--sectiondivider-md-title-font-weight": "--font-weight-bold",
|
|
@@ -3544,7 +3545,7 @@
|
|
|
3544
3545
|
"--sectiondivider-md-description": "--text-secondary",
|
|
3545
3546
|
"--sectiondivider-md-eyebrow": "--text-tertiary",
|
|
3546
3547
|
"--sectiondivider-md-border": "--color-transparent",
|
|
3547
|
-
"--sectiondivider-md-title-outline-color": "
|
|
3548
|
+
"--sectiondivider-md-title-outline-color": "transparent",
|
|
3548
3549
|
"--sectiondivider-md-hairline-color": "--border-brand-medium",
|
|
3549
3550
|
"--sectiondivider-sm-title-font-family": "--font-display",
|
|
3550
3551
|
"--sectiondivider-sm-title-font-weight": "--font-weight-medium",
|
|
@@ -3573,7 +3574,7 @@
|
|
|
3573
3574
|
"--sectiondivider-sm-description": "--text-secondary",
|
|
3574
3575
|
"--sectiondivider-sm-eyebrow": "--text-tertiary",
|
|
3575
3576
|
"--sectiondivider-sm-border": "--color-transparent",
|
|
3576
|
-
"--sectiondivider-sm-title-outline-color": "
|
|
3577
|
+
"--sectiondivider-sm-title-outline-color": "transparent",
|
|
3577
3578
|
"--sectiondivider-sm-hairline-color": "color-mix(in srgb, var(--border-brand-medium) 50%, transparent)"
|
|
3578
3579
|
}
|
|
3579
3580
|
},
|
|
@@ -3204,6 +3204,7 @@
|
|
|
3204
3204
|
"createdAt": "2026-05-26T02:03:38.863Z",
|
|
3205
3205
|
"updatedAt": "2026-08-19T22:39:32.785Z",
|
|
3206
3206
|
"aliases": {
|
|
3207
|
+
"--imagelightbox-tile-surface": "--color-transparent",
|
|
3207
3208
|
"--imagelightbox-tile-radius": "--radius-3xl",
|
|
3208
3209
|
"--imagelightbox-tile-border": "--color-transparent",
|
|
3209
3210
|
"--imagelightbox-tile-border-width": "--border-width-0",
|
|
@@ -3567,7 +3568,7 @@
|
|
|
3567
3568
|
"--sectiondivider-lg-description": "--text-alternate",
|
|
3568
3569
|
"--sectiondivider-lg-eyebrow": "--text-brand",
|
|
3569
3570
|
"--sectiondivider-lg-border": "--color-transparent",
|
|
3570
|
-
"--sectiondivider-lg-title-outline-color": "
|
|
3571
|
+
"--sectiondivider-lg-title-outline-color": "transparent",
|
|
3571
3572
|
"--sectiondivider-lg-hairline-color": "--border-brand-medium",
|
|
3572
3573
|
"--sectiondivider-md-title-font-family": "--font-display",
|
|
3573
3574
|
"--sectiondivider-md-title-font-weight": "--font-weight-bold",
|
|
@@ -3596,7 +3597,7 @@
|
|
|
3596
3597
|
"--sectiondivider-md-description": "--text-secondary",
|
|
3597
3598
|
"--sectiondivider-md-eyebrow": "--text-tertiary",
|
|
3598
3599
|
"--sectiondivider-md-border": "--color-transparent",
|
|
3599
|
-
"--sectiondivider-md-title-outline-color": "
|
|
3600
|
+
"--sectiondivider-md-title-outline-color": "transparent",
|
|
3600
3601
|
"--sectiondivider-md-hairline-color": "--border-brand-medium",
|
|
3601
3602
|
"--sectiondivider-sm-title-font-family": "--font-display",
|
|
3602
3603
|
"--sectiondivider-sm-title-font-weight": "--font-weight-medium",
|
|
@@ -3625,7 +3626,7 @@
|
|
|
3625
3626
|
"--sectiondivider-sm-description": "--text-secondary",
|
|
3626
3627
|
"--sectiondivider-sm-eyebrow": "--text-tertiary",
|
|
3627
3628
|
"--sectiondivider-sm-border": "--color-transparent",
|
|
3628
|
-
"--sectiondivider-sm-title-outline-color": "
|
|
3629
|
+
"--sectiondivider-sm-title-outline-color": "transparent",
|
|
3629
3630
|
"--sectiondivider-sm-hairline-color": "color-mix(in srgb, var(--border-brand-medium) 50%, transparent)"
|
|
3630
3631
|
}
|
|
3631
3632
|
},
|
|
@@ -347,3 +347,8 @@
|
|
|
347
347
|
--font-mono: "fira-code", ui-monospace, "SF Mono", Menlo, Consolas, monospace, monospace;
|
|
348
348
|
--font-editorial: "Manrope", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, sans-serif;
|
|
349
349
|
}
|
|
350
|
+
|
|
351
|
+
/* Page polarity, from --page-bg */
|
|
352
|
+
:where(:root) {
|
|
353
|
+
color-scheme: dark;
|
|
354
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What paints behind a thing, and which way it leans.
|
|
3
|
+
*
|
|
4
|
+
* A theme is not light or dark by declaration — nothing in a saved theme
|
|
5
|
+
* records it — so polarity is either stated on the markup (`data-backdrop`) or
|
|
6
|
+
* measured from the colours that actually paint. Both answers arrive through
|
|
7
|
+
* `polarityOf`, and every consumer that used to hand-roll one of the two reads
|
|
8
|
+
* it from here.
|
|
9
|
+
*/
|
|
10
|
+
import { parseOklchCss, oklchToRgb255 } from '../internal/oklch';
|
|
11
|
+
|
|
12
|
+
export type Polarity = 'light' | 'dark';
|
|
13
|
+
export type ContrastColorToken = '--color-black' | '--color-white';
|
|
14
|
+
|
|
15
|
+
export const BACKDROP_ATTRIBUTE = 'data-backdrop';
|
|
16
|
+
|
|
17
|
+
/** Relative luminance at or above which a backdrop counts as light. Sits
|
|
18
|
+
between the palest shipped dark theme (~0.03) and the darkest light one
|
|
19
|
+
(~0.6), so no shipped look lands near the line. */
|
|
20
|
+
export const LIGHT_BACKDROP = 0.2;
|
|
21
|
+
|
|
22
|
+
/** A fill this see-through says nothing about what shows through it, so the
|
|
23
|
+
walk keeps looking up the tree. */
|
|
24
|
+
const OPAQUE = 0.98;
|
|
25
|
+
|
|
26
|
+
type Rgba = { r: number; g: number; b: number; a: number };
|
|
27
|
+
|
|
28
|
+
const HEX_RE = /#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})\b/gi;
|
|
29
|
+
const OKLCH_RE = /oklch\(\s*[\d.]+%?\s+[\d.]+%?\s+[\d.]+(?:deg)?\s*(?:\/\s*([\d.]+%?)\s*)?\)/gi;
|
|
30
|
+
const RGB_RE = /rgba?\(\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)(?:\s*[,/]\s*([\d.]+%?))?\s*\)/gi;
|
|
31
|
+
/** Anything the parsers above miss — `color-mix()`, a named colour, `lab()`.
|
|
32
|
+
Carries no nested parens in any value a computed style hands back. */
|
|
33
|
+
const EXOTIC_RE = /\b(?:color-mix|hwb|lab|lch|oklab|color)\([^()]*\)/gi;
|
|
34
|
+
|
|
35
|
+
function channel(value: string): number {
|
|
36
|
+
const parsed = Number.parseFloat(value);
|
|
37
|
+
return value.endsWith('%') ? parsed * 2.55 : parsed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function alpha(value: string | undefined): number {
|
|
41
|
+
if (value === undefined) return 1;
|
|
42
|
+
const parsed = Number.parseFloat(value);
|
|
43
|
+
return value.endsWith('%') ? parsed / 100 : parsed;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseHex(hex: string): Rgba {
|
|
47
|
+
const digits = hex.slice(1);
|
|
48
|
+
const full = digits.length <= 4 ? digits.split('').map((part) => part + part).join('') : digits;
|
|
49
|
+
return {
|
|
50
|
+
r: Number.parseInt(full.slice(0, 2), 16),
|
|
51
|
+
g: Number.parseInt(full.slice(2, 4), 16),
|
|
52
|
+
b: Number.parseInt(full.slice(4, 6), 16),
|
|
53
|
+
a: full.length >= 8 ? Number.parseInt(full.slice(6, 8), 16) / 255 : 1,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Every colour a background value names, gradient stops included. */
|
|
58
|
+
export function colorsIn(background: string): Rgba[] {
|
|
59
|
+
const colors: Rgba[] = [];
|
|
60
|
+
for (const match of background.matchAll(HEX_RE)) colors.push(parseHex(match[0]));
|
|
61
|
+
for (const match of background.matchAll(OKLCH_RE)) {
|
|
62
|
+
const parsed = parseOklchCss(match[0]);
|
|
63
|
+
if (parsed) colors.push({ ...oklchToRgb255(parsed.l, parsed.c, parsed.h), a: alpha(match[1]) });
|
|
64
|
+
}
|
|
65
|
+
for (const match of background.matchAll(RGB_RE)) {
|
|
66
|
+
colors.push({
|
|
67
|
+
r: channel(match[1]),
|
|
68
|
+
g: channel(match[2]),
|
|
69
|
+
b: channel(match[3]),
|
|
70
|
+
a: alpha(match[4]),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
for (const match of background.matchAll(EXOTIC_RE)) {
|
|
74
|
+
const resolved = viaCanvas(match[0]);
|
|
75
|
+
if (resolved) colors.push(resolved);
|
|
76
|
+
}
|
|
77
|
+
return colors;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function linearChannel(value: number): number {
|
|
81
|
+
const srgb = Math.max(0, Math.min(255, value)) / 255;
|
|
82
|
+
return srgb <= 0.04045 ? srgb / 12.92 : ((srgb + 0.055) / 1.055) ** 2.4;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function luminance({ r, g, b }: Rgba): number {
|
|
86
|
+
return 0.2126 * linearChannel(r) + 0.7152 * linearChannel(g) + 0.0722 * linearChannel(b);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const SEED = '#010203';
|
|
90
|
+
let probe: CanvasRenderingContext2D | null | undefined;
|
|
91
|
+
|
|
92
|
+
/** The browser as a colour parser: a 2D canvas resolves anything it renders,
|
|
93
|
+
* including the `color-mix()` a computed style can still hand back, and
|
|
94
|
+
* reports failure rather than resolving an unknown value to black. */
|
|
95
|
+
function viaCanvas(value: string): Rgba | null {
|
|
96
|
+
if (typeof document === 'undefined') return null;
|
|
97
|
+
if (probe === undefined) {
|
|
98
|
+
probe = document.createElement('canvas').getContext?.('2d', { willReadFrequently: true }) ?? null;
|
|
99
|
+
}
|
|
100
|
+
if (!probe) return null;
|
|
101
|
+
probe.fillStyle = SEED;
|
|
102
|
+
probe.fillStyle = value;
|
|
103
|
+
if (probe.fillStyle === SEED) return null;
|
|
104
|
+
probe.clearRect(0, 0, 1, 1);
|
|
105
|
+
probe.fillRect(0, 0, 1, 1);
|
|
106
|
+
const [r, g, b, a] = probe.getImageData(0, 0, 1, 1).data;
|
|
107
|
+
return { r, g, b, a: a / 255 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Any CSS colour the browser can render, as `#rrggbb` — for consumers that
|
|
111
|
+
* cannot read CSS themselves: a canvas, a WebGL uniform, an SVG attribute.
|
|
112
|
+
* Null for a value that is not a colour, or is see-through enough that a flat
|
|
113
|
+
* hex would lie about it. */
|
|
114
|
+
export function cssColorToHex(value: string): string | null {
|
|
115
|
+
const color = viaCanvas(value.trim());
|
|
116
|
+
if (!color || color.a < OPAQUE) return null;
|
|
117
|
+
const packed = (Math.round(color.r) << 16) | (Math.round(color.g) << 8) | Math.round(color.b);
|
|
118
|
+
return `#${packed.toString(16).padStart(6, '0')}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Mean luminance of a background value, or null when it names no colour or
|
|
122
|
+
* nothing opaque enough to hide what sits behind it. */
|
|
123
|
+
export function backgroundLuminance(background: string): number | null {
|
|
124
|
+
const opaque = colorsIn(background).filter((color) => color.a >= OPAQUE);
|
|
125
|
+
if (!opaque.length) return null;
|
|
126
|
+
return opaque.reduce((total, color) => total + luminance(color), 0) / opaque.length;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Which way a background value leans, or null when it names no opaque colour. */
|
|
130
|
+
export function polarityOfBackground(background: string): Polarity | null {
|
|
131
|
+
const measured = backgroundLuminance(background);
|
|
132
|
+
if (measured === null) return null;
|
|
133
|
+
return measured >= LIGHT_BACKDROP ? 'light' : 'dark';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Pick the invariant black/white token with the stronger minimum contrast
|
|
138
|
+
* against every colour in a solid or gradient background. Alpha is ignored:
|
|
139
|
+
* the caller passes an authored value, not a computed one.
|
|
140
|
+
*/
|
|
141
|
+
export function contrastTokenForBackground(background: string): ContrastColorToken {
|
|
142
|
+
const colors = colorsIn(background);
|
|
143
|
+
if (colors.length === 0) return '--color-white';
|
|
144
|
+
const luminances = colors.map(luminance);
|
|
145
|
+
const blackMinimum = Math.min(...luminances.map((value) => (value + 0.05) / 0.05));
|
|
146
|
+
const whiteMinimum = Math.min(...luminances.map((value) => 1.05 / (value + 0.05)));
|
|
147
|
+
return blackMinimum >= whiteMinimum ? '--color-black' : '--color-white';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Elements a watcher stamped. Its own stamp is an answer, not a declaration,
|
|
151
|
+
* so the next measurement must not read it back as one. */
|
|
152
|
+
const stamped = new WeakSet<Element>();
|
|
153
|
+
|
|
154
|
+
export function markStamped(el: Element): void {
|
|
155
|
+
stamped.add(el);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function clearStamped(el: Element): void {
|
|
159
|
+
stamped.delete(el);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The polarity the markup states: the nearest `data-backdrop` on or above the
|
|
163
|
+
* element that a watcher did not write. */
|
|
164
|
+
export function declaredPolarity(el: Element | null): Polarity | null {
|
|
165
|
+
for (let node: Element | null = el; node; node = node.parentElement) {
|
|
166
|
+
if (stamped.has(node)) continue;
|
|
167
|
+
const stated = node.getAttribute?.(BACKDROP_ATTRIBUTE);
|
|
168
|
+
if (stated === 'light' || stated === 'dark') return stated;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Mean luminance of whatever actually paints behind `el` — the nearest
|
|
174
|
+
* ancestor with an opaque fill, averaged across its gradient stops, falling
|
|
175
|
+
* back to the theme's page canvas. */
|
|
176
|
+
export function backdropLuminance(el: Element | null): number {
|
|
177
|
+
if (typeof document === 'undefined') return 0;
|
|
178
|
+
for (let node: Element | null = el ?? document.body; node; node = node.parentElement) {
|
|
179
|
+
const style = getComputedStyle(node);
|
|
180
|
+
const measured = backgroundLuminance(`${style.backgroundImage} ${style.backgroundColor}`);
|
|
181
|
+
if (measured !== null) return measured;
|
|
182
|
+
}
|
|
183
|
+
const page = getComputedStyle(document.documentElement).getPropertyValue('--page-bg');
|
|
184
|
+
return backgroundLuminance(page) ?? 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Which way the backdrop behind `el` leans. A `data-backdrop` on or above it
|
|
188
|
+
* is the answer; otherwise the paint is measured. */
|
|
189
|
+
export function polarityOf(el: Element | null): Polarity {
|
|
190
|
+
return declaredPolarity(el) ?? (backdropLuminance(el) >= LIGHT_BACKDROP ? 'light' : 'dark');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const isLightBackdrop = (el: Element | null): boolean => polarityOf(el) === 'light';
|
|
194
|
+
|
|
195
|
+
/** The black/white token that reads against `el`'s backdrop. */
|
|
196
|
+
export const contrastTokenFor = (el: Element | null): ContrastColorToken =>
|
|
197
|
+
polarityOf(el) === 'light' ? '--color-black' : '--color-white';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export {
|
|
2
|
+
BACKDROP_ATTRIBUTE,
|
|
3
|
+
LIGHT_BACKDROP,
|
|
4
|
+
backdropLuminance,
|
|
5
|
+
backgroundLuminance,
|
|
6
|
+
contrastTokenFor,
|
|
7
|
+
cssColorToHex,
|
|
8
|
+
contrastTokenForBackground,
|
|
9
|
+
declaredPolarity,
|
|
10
|
+
isLightBackdrop,
|
|
11
|
+
polarityOf,
|
|
12
|
+
polarityOfBackground,
|
|
13
|
+
type ContrastColorToken,
|
|
14
|
+
type Polarity,
|
|
15
|
+
} from './backdrop';
|
|
16
|
+
export { backdrop, syncDocumentBackdrop, watchBackdrop, type BackdropOptions } from './watch';
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeping a stamp current.
|
|
3
|
+
*
|
|
4
|
+
* A page's polarity moves when the theme changes, and the editor changes a
|
|
5
|
+
* theme by writing inline custom properties on `:root` — no reload, no class
|
|
6
|
+
* flip. One observer watches for that and tells every live watcher to measure
|
|
7
|
+
* again, so a consumer never writes its own.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
BACKDROP_ATTRIBUTE,
|
|
11
|
+
clearStamped,
|
|
12
|
+
markStamped,
|
|
13
|
+
polarityOf,
|
|
14
|
+
polarityOfBackground,
|
|
15
|
+
type Polarity,
|
|
16
|
+
} from './backdrop';
|
|
17
|
+
|
|
18
|
+
export interface BackdropOptions {
|
|
19
|
+
/** Called with each new answer, including the first. */
|
|
20
|
+
onChange?: (polarity: Polarity) => void;
|
|
21
|
+
/** Write `data-backdrop` on the element, so CSS can key on it and
|
|
22
|
+
`light-dark()` inside it resolves the right half. Default true. */
|
|
23
|
+
stamp?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Watcher = () => void;
|
|
27
|
+
|
|
28
|
+
const watchers = new Set<Watcher>();
|
|
29
|
+
let themeObserver: MutationObserver | null = null;
|
|
30
|
+
let resizeBound = false;
|
|
31
|
+
|
|
32
|
+
function notify(): void {
|
|
33
|
+
for (const watcher of watchers) watcher();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function startListening(): void {
|
|
37
|
+
if (typeof document === 'undefined') return;
|
|
38
|
+
if (!themeObserver) {
|
|
39
|
+
// The editor writes the live theme as inline vars on <html>; a switch to a
|
|
40
|
+
// different stylesheet lands in <head>.
|
|
41
|
+
themeObserver = new MutationObserver(notify);
|
|
42
|
+
themeObserver.observe(document.documentElement, { attributeFilter: ['style', 'class'] });
|
|
43
|
+
themeObserver.observe(document.head, { childList: true });
|
|
44
|
+
}
|
|
45
|
+
if (!resizeBound) {
|
|
46
|
+
window.addEventListener('resize', notify, { passive: true });
|
|
47
|
+
resizeBound = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function stopListening(): void {
|
|
52
|
+
if (watchers.size) return;
|
|
53
|
+
themeObserver?.disconnect();
|
|
54
|
+
themeObserver = null;
|
|
55
|
+
if (resizeBound) {
|
|
56
|
+
window.removeEventListener('resize', notify);
|
|
57
|
+
resizeBound = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Track the polarity behind `el` until the returned function is called. A
|
|
63
|
+
* `data-backdrop` stated on or above the element wins over measurement, which
|
|
64
|
+
* is what lets a section declare a tone the paint cannot show — art directed
|
|
65
|
+
* over a photograph, or a band whose fill has not rendered yet.
|
|
66
|
+
*/
|
|
67
|
+
export function watchBackdrop(el: Element, options: BackdropOptions = {}): () => void {
|
|
68
|
+
const { onChange, stamp = true } = options;
|
|
69
|
+
let last: Polarity | null = null;
|
|
70
|
+
|
|
71
|
+
const measure = () => {
|
|
72
|
+
const polarity = polarityOf(el);
|
|
73
|
+
if (polarity === last) return;
|
|
74
|
+
last = polarity;
|
|
75
|
+
if (stamp) {
|
|
76
|
+
markStamped(el);
|
|
77
|
+
el.setAttribute(BACKDROP_ATTRIBUTE, polarity);
|
|
78
|
+
}
|
|
79
|
+
onChange?.(polarity);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
watchers.add(measure);
|
|
83
|
+
startListening();
|
|
84
|
+
measure();
|
|
85
|
+
|
|
86
|
+
return () => {
|
|
87
|
+
watchers.delete(measure);
|
|
88
|
+
if (stamp) {
|
|
89
|
+
clearStamped(el);
|
|
90
|
+
el.removeAttribute(BACKDROP_ATTRIBUTE);
|
|
91
|
+
}
|
|
92
|
+
stopListening();
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Svelte action: `<section use:backdrop>` stamps the element and keeps the
|
|
97
|
+
* stamp current. Pass `{ onChange }` when JS needs the answer too. */
|
|
98
|
+
export function backdrop(el: Element, options: BackdropOptions = {}) {
|
|
99
|
+
let stop = watchBackdrop(el, options);
|
|
100
|
+
return {
|
|
101
|
+
update(next: BackdropOptions = {}) {
|
|
102
|
+
stop();
|
|
103
|
+
stop = watchBackdrop(el, next);
|
|
104
|
+
},
|
|
105
|
+
destroy() {
|
|
106
|
+
stop();
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Stamp `<html>` from the theme's page canvas, so a rule can key on
|
|
113
|
+
* `[data-backdrop='dark']` anywhere on the page and a section that declares its
|
|
114
|
+
* own tone still overrides it. Call once at boot; returns a stop function.
|
|
115
|
+
*/
|
|
116
|
+
export function syncDocumentBackdrop(): () => void {
|
|
117
|
+
if (typeof document === 'undefined') return () => {};
|
|
118
|
+
const root = document.documentElement;
|
|
119
|
+
const stated = root.getAttribute(BACKDROP_ATTRIBUTE);
|
|
120
|
+
// A root the page states for itself is the page's answer; leave it alone.
|
|
121
|
+
if (stated === 'light' || stated === 'dark') return () => {};
|
|
122
|
+
const measure = () => {
|
|
123
|
+
const page = getComputedStyle(root).getPropertyValue('--page-bg');
|
|
124
|
+
const polarity = polarityOfBackground(page);
|
|
125
|
+
if (polarity) root.setAttribute(BACKDROP_ATTRIBUTE, polarity);
|
|
126
|
+
};
|
|
127
|
+
// The root's own stamp is an answer, not a declaration — without this every
|
|
128
|
+
// measurement below it would short-circuit on the page's polarity and no
|
|
129
|
+
// local band could ever read as its own.
|
|
130
|
+
markStamped(root);
|
|
131
|
+
watchers.add(measure);
|
|
132
|
+
startListening();
|
|
133
|
+
measure();
|
|
134
|
+
return () => {
|
|
135
|
+
watchers.delete(measure);
|
|
136
|
+
clearStamped(root);
|
|
137
|
+
stopListening();
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
icon?: string;
|
|
4
4
|
iconColor?: string;
|
|
5
5
|
title?: string;
|
|
6
|
+
/** `bare` drops the header. The frame and body keep every token the default
|
|
7
|
+
variant uses, so padding, stroke and fill stay editable. */
|
|
8
|
+
variant?: 'default' | 'bare';
|
|
6
9
|
size?: 'default' | 'compact';
|
|
7
10
|
/** false → the card stops pinning body typography so the consumer fully owns slotted content's styling. */
|
|
8
11
|
prose?: boolean;
|
|
@@ -20,6 +23,7 @@
|
|
|
20
23
|
icon = '',
|
|
21
24
|
iconColor = 'var(--text-secondary)',
|
|
22
25
|
title = '',
|
|
26
|
+
variant = 'default',
|
|
23
27
|
size = 'default',
|
|
24
28
|
prose = true,
|
|
25
29
|
flush = false,
|
|
@@ -35,16 +39,25 @@
|
|
|
35
39
|
let hoverShadow = $derived(
|
|
36
40
|
hover === undefined ? undefined : hover ? 'var(--card-hover-shadow)' : 'var(--card-default-shadow)',
|
|
37
41
|
);
|
|
42
|
+
|
|
43
|
+
let showHeader = $derived(variant !== 'bare' && Boolean(icon || title));
|
|
44
|
+
|
|
45
|
+
$effect(() => {
|
|
46
|
+
if (import.meta.env.DEV && variant === 'default' && !icon && !title) {
|
|
47
|
+
console.warn('[live-tokens] <Card> has no icon or title. Use variant="bare" to declare a headerless card.');
|
|
48
|
+
}
|
|
49
|
+
});
|
|
38
50
|
</script>
|
|
39
51
|
|
|
40
52
|
<div
|
|
41
53
|
class="card {className}"
|
|
54
|
+
class:bare={variant === 'bare'}
|
|
42
55
|
class:compact={size === 'compact'}
|
|
43
56
|
style:--card-color={iconColor}
|
|
44
57
|
style:--card-hover-border-active={hoverBorder}
|
|
45
58
|
style:--card-hover-shadow-active={hoverShadow}
|
|
46
59
|
>
|
|
47
|
-
{#if
|
|
60
|
+
{#if showHeader}
|
|
48
61
|
<div class="card-header">
|
|
49
62
|
{#if icon}
|
|
50
63
|
<i class="{icon} card-icon"></i>
|
|
@@ -68,17 +81,22 @@
|
|
|
68
81
|
--card-default-border: var(--border-neutral);
|
|
69
82
|
--card-default-border-width: var(--border-width-1);
|
|
70
83
|
--card-default-radius: var(--radius-lg);
|
|
84
|
+
/* Every side tracks its base. A declared side always beats the fallback
|
|
85
|
+
themed-padding emits, so naming a literal step here left the base token
|
|
86
|
+
inert: merging split padding back to one value cleared the side aliases
|
|
87
|
+
and the card kept the old geometry. The asymmetry is a themed choice now,
|
|
88
|
+
carried by per-side aliases that override these outright. */
|
|
71
89
|
--card-default-header-padding: var(--space-16);
|
|
72
|
-
--card-default-header-padding-top: var(--
|
|
73
|
-
--card-default-header-padding-right: var(--
|
|
74
|
-
--card-default-header-padding-bottom: var(--
|
|
75
|
-
--card-default-header-padding-left: var(--
|
|
90
|
+
--card-default-header-padding-top: var(--card-default-header-padding);
|
|
91
|
+
--card-default-header-padding-right: var(--card-default-header-padding);
|
|
92
|
+
--card-default-header-padding-bottom: var(--card-default-header-padding);
|
|
93
|
+
--card-default-header-padding-left: var(--card-default-header-padding);
|
|
76
94
|
--card-default-header-gap: var(--space-8);
|
|
77
95
|
--card-default-body-padding: var(--space-16);
|
|
78
|
-
--card-default-body-padding-top: var(--
|
|
79
|
-
--card-default-body-padding-right: var(--
|
|
80
|
-
--card-default-body-padding-bottom: var(--
|
|
81
|
-
--card-default-body-padding-left: var(--
|
|
96
|
+
--card-default-body-padding-top: var(--card-default-body-padding);
|
|
97
|
+
--card-default-body-padding-right: var(--card-default-body-padding);
|
|
98
|
+
--card-default-body-padding-bottom: var(--card-default-body-padding);
|
|
99
|
+
--card-default-body-padding-left: var(--card-default-body-padding);
|
|
82
100
|
--card-default-shadow: var(--shadow-sm);
|
|
83
101
|
--card-default-blur: var(--blur-none);
|
|
84
102
|
--card-default-header-surface: color-mix(in srgb, var(--surface-neutral-lowest) 80%, transparent);
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
<script lang="ts">
|
|
36
36
|
import MenuSelect from './MenuSelect.svelte';
|
|
37
37
|
import { SvelteMap } from 'svelte/reactivity';
|
|
38
|
-
import { contrastTokenForBackground } from '../
|
|
38
|
+
import { contrastTokenForBackground } from '../backdrop';
|
|
39
39
|
// `.ftt-tag` is hand-rolled (not Badge) so editing badge-* tokens doesn't
|
|
40
40
|
// repaint the playground. The dropdown uses MenuSelect on purpose.
|
|
41
41
|
import './FloatingTokenTags.css';
|
|
@@ -630,7 +630,7 @@
|
|
|
630
630
|
class="image-lightbox-stage"
|
|
631
631
|
class:shadow-box={shadow === 'box'}
|
|
632
632
|
class:shadow-content={shadow === 'content'}
|
|
633
|
-
|
|
633
|
+
role="presentation"
|
|
634
634
|
tabindex="-1"
|
|
635
635
|
onclick={onStageClick}
|
|
636
636
|
onpointerdown={onPointerDown}
|
|
@@ -728,7 +728,9 @@
|
|
|
728
728
|
|
|
729
729
|
<style>
|
|
730
730
|
:global(:root) {
|
|
731
|
-
/* thumbnail (closed inline) + animated modal stage
|
|
731
|
+
/* thumbnail (closed inline) + animated modal stage; the fill is the
|
|
732
|
+
thumbnail's alone — the open stage rides the overlay */
|
|
733
|
+
--imagelightbox-tile-surface: var(--color-transparent);
|
|
732
734
|
--imagelightbox-tile-radius: var(--radius-2xl);
|
|
733
735
|
--imagelightbox-tile-border: var(--color-transparent);
|
|
734
736
|
--imagelightbox-tile-border-width: var(--border-width-0);
|
|
@@ -759,7 +761,7 @@
|
|
|
759
761
|
cursor: zoom-in;
|
|
760
762
|
border: var(--imagelightbox-tile-border-width) solid var(--imagelightbox-tile-border);
|
|
761
763
|
border-radius: var(--imagelightbox-tile-radius);
|
|
762
|
-
background:
|
|
764
|
+
background: var(--imagelightbox-tile-surface);
|
|
763
765
|
overflow: hidden;
|
|
764
766
|
transition: transform 250ms ease;
|
|
765
767
|
}
|
|
@@ -812,13 +814,15 @@
|
|
|
812
814
|
border: 0;
|
|
813
815
|
}
|
|
814
816
|
|
|
817
|
+
/* No tile fill here: the open stage sits on the near-opaque overlay, which
|
|
818
|
+
already backs transparent art. Painting the fill too would band the scrim
|
|
819
|
+
wherever the image's aspect leaves the stage box unfilled. */
|
|
815
820
|
.image-lightbox-stage {
|
|
816
821
|
position: fixed;
|
|
817
822
|
z-index: var(--z-modal);
|
|
818
823
|
cursor: zoom-out;
|
|
819
824
|
border: var(--imagelightbox-tile-border-width) solid var(--imagelightbox-tile-border);
|
|
820
825
|
border-radius: var(--imagelightbox-tile-radius);
|
|
821
|
-
background: transparent;
|
|
822
826
|
}
|
|
823
827
|
|
|
824
828
|
.image-lightbox-clip {
|
|
@@ -243,7 +243,7 @@
|
|
|
243
243
|
--sectiondivider-lg-description: var(--text-alternate);
|
|
244
244
|
--sectiondivider-lg-eyebrow: var(--text-brand);
|
|
245
245
|
--sectiondivider-lg-border: var(--color-transparent);
|
|
246
|
-
--sectiondivider-lg-title-outline-color: var(--
|
|
246
|
+
--sectiondivider-lg-title-outline-color: var(--color-transparent);
|
|
247
247
|
--sectiondivider-lg-hairline-color: var(--border-brand-medium);
|
|
248
248
|
|
|
249
249
|
/* Medium */
|
|
@@ -274,7 +274,7 @@
|
|
|
274
274
|
--sectiondivider-md-description: var(--text-secondary);
|
|
275
275
|
--sectiondivider-md-eyebrow: var(--text-tertiary);
|
|
276
276
|
--sectiondivider-md-border: var(--color-transparent);
|
|
277
|
-
--sectiondivider-md-title-outline-color: var(--
|
|
277
|
+
--sectiondivider-md-title-outline-color: var(--color-transparent);
|
|
278
278
|
--sectiondivider-md-hairline-color: var(--border-brand-medium);
|
|
279
279
|
|
|
280
280
|
/* Small */
|
|
@@ -305,7 +305,7 @@
|
|
|
305
305
|
--sectiondivider-sm-description: var(--text-secondary);
|
|
306
306
|
--sectiondivider-sm-eyebrow: var(--text-tertiary);
|
|
307
307
|
--sectiondivider-sm-border: var(--color-transparent);
|
|
308
|
-
--sectiondivider-sm-title-outline-color:
|
|
308
|
+
--sectiondivider-sm-title-outline-color: var(--color-transparent);
|
|
309
309
|
--sectiondivider-sm-hairline-color: color-mix(in srgb, var(--border-brand-medium) 50%, transparent);
|
|
310
310
|
|
|
311
311
|
/* Intrinsic defaults. These keys cascade to `:root` via the editor's
|
|
@@ -152,25 +152,27 @@
|
|
|
152
152
|
icon size) all read through these default names, so rebinding them
|
|
153
153
|
here is enough — no per-state small token explosion. Per-side padding
|
|
154
154
|
overrides are rebound too so split-padding edits at small don't leak
|
|
155
|
-
through to default size
|
|
155
|
+
through to default size; each carries the same fallback themed-padding
|
|
156
|
+
emits, so merging the small padding back to one value leaves the base
|
|
157
|
+
token in charge instead of binding to a name nothing sets. Font-weight and color are intentionally NOT
|
|
156
158
|
rebound: weight differences are state-driven (selected = semibold),
|
|
157
159
|
colors are state-driven too. */
|
|
158
160
|
.segmented-control.small {
|
|
159
161
|
--segmentedcontrol-bar-padding: var(--segmentedcontrol-bar-small-padding);
|
|
160
|
-
--segmentedcontrol-bar-padding-top: var(--segmentedcontrol-bar-small-padding-top);
|
|
161
|
-
--segmentedcontrol-bar-padding-right: var(--segmentedcontrol-bar-small-padding-right);
|
|
162
|
-
--segmentedcontrol-bar-padding-bottom: var(--segmentedcontrol-bar-small-padding-bottom);
|
|
163
|
-
--segmentedcontrol-bar-padding-left: var(--segmentedcontrol-bar-small-padding-left);
|
|
162
|
+
--segmentedcontrol-bar-padding-top: var(--segmentedcontrol-bar-small-padding-top, var(--segmentedcontrol-bar-small-padding));
|
|
163
|
+
--segmentedcontrol-bar-padding-right: var(--segmentedcontrol-bar-small-padding-right, var(--segmentedcontrol-bar-small-padding));
|
|
164
|
+
--segmentedcontrol-bar-padding-bottom: var(--segmentedcontrol-bar-small-padding-bottom, var(--segmentedcontrol-bar-small-padding));
|
|
165
|
+
--segmentedcontrol-bar-padding-left: var(--segmentedcontrol-bar-small-padding-left, var(--segmentedcontrol-bar-small-padding));
|
|
164
166
|
--segmentedcontrol-bar-radius: var(--segmentedcontrol-bar-small-radius);
|
|
165
167
|
|
|
166
168
|
--segmentedcontrol-divider-inset: var(--segmentedcontrol-small-divider-inset);
|
|
167
169
|
--segmentedcontrol-divider-thickness: var(--segmentedcontrol-small-divider-thickness);
|
|
168
170
|
|
|
169
171
|
--segmentedcontrol-option-padding: var(--segmentedcontrol-option-small-padding);
|
|
170
|
-
--segmentedcontrol-option-padding-top: var(--segmentedcontrol-option-small-padding-top);
|
|
171
|
-
--segmentedcontrol-option-padding-right: var(--segmentedcontrol-option-small-padding-right);
|
|
172
|
-
--segmentedcontrol-option-padding-bottom: var(--segmentedcontrol-option-small-padding-bottom);
|
|
173
|
-
--segmentedcontrol-option-padding-left: var(--segmentedcontrol-option-small-padding-left);
|
|
172
|
+
--segmentedcontrol-option-padding-top: var(--segmentedcontrol-option-small-padding-top, var(--segmentedcontrol-option-small-padding));
|
|
173
|
+
--segmentedcontrol-option-padding-right: var(--segmentedcontrol-option-small-padding-right, var(--segmentedcontrol-option-small-padding));
|
|
174
|
+
--segmentedcontrol-option-padding-bottom: var(--segmentedcontrol-option-small-padding-bottom, var(--segmentedcontrol-option-small-padding));
|
|
175
|
+
--segmentedcontrol-option-padding-left: var(--segmentedcontrol-option-small-padding-left, var(--segmentedcontrol-option-small-padding));
|
|
174
176
|
--segmentedcontrol-option-gap: var(--segmentedcontrol-option-small-gap);
|
|
175
177
|
|
|
176
178
|
/* Icon size has a single source-of-truth token now; text size/line-height
|