@motion-proto/live-tokens 0.60.1 → 0.61.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.
Files changed (31) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist-plugin/{chunk-XWXIMTWZ.js → chunk-7TQQTHI6.js} +10 -1
  3. package/dist-plugin/{chunk-LW4SR7AZ.js → chunk-O6GQ6GBH.js} +1 -1
  4. package/dist-plugin/{chunk-E5QYON4L.js → chunk-OIOXU7FR.js} +33 -2
  5. package/dist-plugin/generateColorsAndType/index.js +2 -2
  6. package/dist-plugin/index.cjs +135 -1
  7. package/dist-plugin/index.js +99 -4
  8. package/dist-plugin/tokensCssMigrations/index.cjs +32 -1
  9. package/dist-plugin/tokensCssMigrations/index.js +2 -2
  10. package/package.json +6 -1
  11. package/src/editor/component-editor/ImageLightboxEditor.svelte +2 -1
  12. package/src/editor/docs/Docs.svelte +1 -0
  13. package/src/editor/docs/chapters.ts +1 -0
  14. package/src/editor/docs/content/light-and-dark.md +90 -0
  15. package/src/editor/docs/content.generated.ts +1 -0
  16. package/src/live-tokens/data/themes/autumn.json +1 -0
  17. package/src/live-tokens/data/themes/halloween.json +1 -0
  18. package/src/live-tokens/data/themes/midnight-study.json +1 -0
  19. package/src/live-tokens/data/themes/ocean.json +1 -0
  20. package/src/live-tokens/data/themes/royal-velvet.json +1 -0
  21. package/src/live-tokens/data/themes/sketchy.json +1 -0
  22. package/src/live-tokens/data/themes/spring-meadow.json +1 -0
  23. package/src/live-tokens/data/themes/sunset.json +1 -0
  24. package/src/live-tokens/data/tokens.generated.css +5 -0
  25. package/src/system/backdrop/backdrop.ts +197 -0
  26. package/src/system/backdrop/index.ts +16 -0
  27. package/src/system/backdrop/watch.ts +139 -0
  28. package/src/system/components/FloatingTokenTags.svelte +1 -1
  29. package/src/system/components/ImageLightbox.svelte +4 -3
  30. package/src/system/styles/tokens.css +20 -0
  31. package/src/system/internal/backgroundContrast.ts +0 -62
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.61.0 — A surface says which way it leans
4
+
5
+ ### Added
6
+
7
+ - **One answer to "is this surface light or dark".** A wordmark drawn in white
8
+ disappears on a pale theme, ink that multiplies onto paper vanishes on a dark
9
+ one, and a headline over a photograph is on a dark ground whatever the palette
10
+ says. Every one of those needs the same fact, and every one of them was
11
+ answering it for itself: a luminance probe here, a `--page-bg` poll there, a
12
+ hand-picked token pair somewhere else. `@motion-proto/live-tokens/backdrop`
13
+ answers it once. `data-backdrop` is the single channel — state it in markup
14
+ where the surface knows its own tone, or let `use:backdrop` measure what
15
+ actually paints and stamp it — and `tokens.css` turns the attribute into
16
+ `color-scheme`, so `light-dark()` under it resolves the half that reads. The
17
+ module also answers in JavaScript, for a canvas, a WebGL uniform, or an image
18
+ that comes in two versions: `isLightBackdrop`, `watchBackdrop`, `polarityOf`,
19
+ `contrastTokenFor`, and `cssColorToHex`. A new guide chapter, **Light and
20
+ dark**, covers the whole of it.
21
+ - **A theme's own polarity is baked, not probed.** Nothing in a saved theme
22
+ recorded whether it was light or dark — the generator takes `scheme` as a
23
+ brief and throws it away — so anything that needed to know had to measure at
24
+ runtime and repaint. `tokens.generated.css` now carries a `color-scheme`
25
+ measured from the production theme's `--page-bg`, at zero specificity: it is
26
+ the answer at first paint, and any `data-backdrop` stamp — the editor
27
+ switching themes live, a section stating its own tone — outranks it.
28
+
29
+ - **`ImageLightbox` has a tile fill.** A diagram drawn in light ink, or any
30
+ cut-out with a transparent ground, disappeared into a light page: the tile is
31
+ exactly the image's own box, so nothing stood behind the art to separate it
32
+ from the section. `--imagelightbox-tile-surface` paints behind the image on
33
+ the closed tile and the opened stage alike, inside the radius and border the
34
+ tile already carries. It takes any fill the editor offers, solid or gradient,
35
+ and defaults to none, so every existing lightbox renders as it did.
36
+
3
37
  ## 0.60.1 — A drawn heading is type, not an icon
4
38
 
5
39
  ### Fixed
@@ -113,6 +113,13 @@ function cssColorToOklch(value) {
113
113
  if (HEX6_RE.test(raw)) return hexToOklch(raw);
114
114
  return parseOklchCss(raw);
115
115
  }
116
+ function oklchToRgb255(l, c, h) {
117
+ const clamped = gamutClamp(l, c, h);
118
+ const [L, a, bVal] = oklchToOklab(clamped.l, clamped.c, clamped.h);
119
+ const [r, g, b] = oklabToLinearRgb(L, a, bVal);
120
+ const to255 = (channel) => Math.round(Math.max(0, Math.min(1, linearToSrgb(channel))) * 255);
121
+ return { r: to255(r), g: to255(g), b: to255(b) };
122
+ }
116
123
 
117
124
  export {
118
125
  hexToOklch,
@@ -120,5 +127,7 @@ export {
120
127
  oklchToHexClamped,
121
128
  gamutClamp,
122
129
  oklchToCss,
123
- cssColorToOklch
130
+ parseOklchCss,
131
+ cssColorToOklch,
132
+ oklchToRgb255
124
133
  };
@@ -2,7 +2,7 @@ import {
2
2
  cssColorToOklch,
3
3
  gamutClamp,
4
4
  oklchToCss
5
- } from "./chunk-XWXIMTWZ.js";
5
+ } from "./chunk-7TQQTHI6.js";
6
6
 
7
7
  // src/editor/core/storage/files/versionedFileResourceClient.ts
8
8
  function sanitizeFileName(name) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  hexToOklch,
3
3
  oklchToCss
4
- } from "./chunk-XWXIMTWZ.js";
4
+ } from "./chunk-7TQQTHI6.js";
5
5
 
6
6
  // src/editor/core/themes/parsers/globalRootBlock.ts
7
7
  function extractGlobalRootBody(source) {
@@ -430,6 +430,36 @@ var tokensCssMigration_2026_08_26_gradientStops = {
430
430
  }
431
431
  };
432
432
 
433
+ // vite-plugin/tokensCssMigrations/migrations/2026-08-27-backdrop-polarity.ts
434
+ var MARKER = "[data-backdrop='dark']";
435
+ var RULES = `/* BACKDROP POLARITY \u2014 one attribute says which way a surface leans. State it
436
+ in markup (\`<section data-backdrop="dark">\`) or let \`use:backdrop\` measure
437
+ and stamp it. Either way \`light-dark()\` inside resolves the right half, and
438
+ a rule can key on the attribute for what colour alone cannot carry. */
439
+
440
+ [data-backdrop='light'] {
441
+ color-scheme: light;
442
+ }
443
+
444
+ [data-backdrop='dark'] {
445
+ color-scheme: dark;
446
+ }
447
+ `;
448
+ var tokensCssMigration_2026_08_27_backdropPolarity = {
449
+ id: "2026-08-27-backdrop-polarity",
450
+ kind: "additive",
451
+ description: "Add the [data-backdrop] \u2192 color-scheme rules",
452
+ apply(css) {
453
+ if (css.includes(MARKER)) return css;
454
+ const at = css.indexOf(":root");
455
+ if (at === -1) return `${css.trimEnd()}
456
+
457
+ ${RULES}`;
458
+ return `${css.slice(0, at)}${RULES}
459
+ ${css.slice(at)}`;
460
+ }
461
+ };
462
+
433
463
  // vite-plugin/tokensCssMigrations/index.ts
434
464
  var TOKENS_CSS_MIGRATIONS = [
435
465
  tokensCssMigration_2026_05_29_typographyScaleAdditions,
@@ -441,7 +471,8 @@ var TOKENS_CSS_MIGRATIONS = [
441
471
  tokensCssMigration_2026_07_20_semanticTextStyles,
442
472
  tokensCssMigration_2026_08_19_oklchColorValues,
443
473
  tokensCssMigration_2026_08_25_editorialTypeRole,
444
- tokensCssMigration_2026_08_26_gradientStops
474
+ tokensCssMigration_2026_08_26_gradientStops,
475
+ tokensCssMigration_2026_08_27_backdropPolarity
445
476
  ];
446
477
  function runTokensCssMigrations(css) {
447
478
  return foldMigrations(css, () => true);
@@ -15,7 +15,7 @@ import {
15
15
  scaleToCssVar,
16
16
  setCurveAnchor,
17
17
  syncBaseAnchor
18
- } from "../chunk-LW4SR7AZ.js";
18
+ } from "../chunk-O6GQ6GBH.js";
19
19
  import {
20
20
  CURRENT_COLORS_AND_TYPE_SCHEMA_VERSION,
21
21
  CURRENT_COMPONENT_SCHEMA_VERSION
@@ -26,7 +26,7 @@ import {
26
26
  hexToOklch,
27
27
  oklchToHex,
28
28
  oklchToHexClamped
29
- } from "../chunk-XWXIMTWZ.js";
29
+ } from "../chunk-7TQQTHI6.js";
30
30
  import {
31
31
  readLiveTokensConfig,
32
32
  resolveDataDirs
@@ -203,6 +203,9 @@ function sanitizeFileName(name) {
203
203
  function srgbToLinear(c) {
204
204
  return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
205
205
  }
206
+ function linearToSrgb(c) {
207
+ return c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
208
+ }
206
209
  function hexToLinearRgb(hex) {
207
210
  const r = parseInt(hex.slice(1, 3), 16) / 255;
208
211
  const g = parseInt(hex.slice(3, 5), 16) / 255;
@@ -295,6 +298,13 @@ function cssColorToOklch(value) {
295
298
  if (HEX6_RE.test(raw)) return hexToOklch(raw);
296
299
  return parseOklchCss(raw);
297
300
  }
301
+ function oklchToRgb255(l, c, h) {
302
+ const clamped = gamutClamp(l, c, h);
303
+ const [L, a, bVal] = oklchToOklab(clamped.l, clamped.c, clamped.h);
304
+ const [r, g, b] = oklabToLinearRgb(L, a, bVal);
305
+ const to255 = (channel2) => Math.round(Math.max(0, Math.min(1, linearToSrgb(channel2))) * 255);
306
+ return { r: to255(r), g: to255(g), b: to255(b) };
307
+ }
298
308
 
299
309
  // src/editor/ui/curveEngine.ts
300
310
  function makeAnchor(x, y, tangentLen = 15) {
@@ -1520,6 +1530,87 @@ function runMigrations(kind, fileVersion, rawVars, meta = {}) {
1520
1530
  return out;
1521
1531
  }
1522
1532
 
1533
+ // src/system/backdrop/backdrop.ts
1534
+ var LIGHT_BACKDROP = 0.2;
1535
+ var OPAQUE = 0.98;
1536
+ var HEX_RE = /#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})\b/gi;
1537
+ var OKLCH_RE2 = /oklch\(\s*[\d.]+%?\s+[\d.]+%?\s+[\d.]+(?:deg)?\s*(?:\/\s*([\d.]+%?)\s*)?\)/gi;
1538
+ var RGB_RE = /rgba?\(\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)(?:\s*[,/]\s*([\d.]+%?))?\s*\)/gi;
1539
+ var EXOTIC_RE = /\b(?:color-mix|hwb|lab|lch|oklab|color)\([^()]*\)/gi;
1540
+ function channel(value) {
1541
+ const parsed = Number.parseFloat(value);
1542
+ return value.endsWith("%") ? parsed * 2.55 : parsed;
1543
+ }
1544
+ function alpha(value) {
1545
+ if (value === void 0) return 1;
1546
+ const parsed = Number.parseFloat(value);
1547
+ return value.endsWith("%") ? parsed / 100 : parsed;
1548
+ }
1549
+ function parseHex(hex) {
1550
+ const digits = hex.slice(1);
1551
+ const full = digits.length <= 4 ? digits.split("").map((part) => part + part).join("") : digits;
1552
+ return {
1553
+ r: Number.parseInt(full.slice(0, 2), 16),
1554
+ g: Number.parseInt(full.slice(2, 4), 16),
1555
+ b: Number.parseInt(full.slice(4, 6), 16),
1556
+ a: full.length >= 8 ? Number.parseInt(full.slice(6, 8), 16) / 255 : 1
1557
+ };
1558
+ }
1559
+ function colorsIn(background) {
1560
+ const colors = [];
1561
+ for (const match of background.matchAll(HEX_RE)) colors.push(parseHex(match[0]));
1562
+ for (const match of background.matchAll(OKLCH_RE2)) {
1563
+ const parsed = parseOklchCss(match[0]);
1564
+ if (parsed) colors.push({ ...oklchToRgb255(parsed.l, parsed.c, parsed.h), a: alpha(match[1]) });
1565
+ }
1566
+ for (const match of background.matchAll(RGB_RE)) {
1567
+ colors.push({
1568
+ r: channel(match[1]),
1569
+ g: channel(match[2]),
1570
+ b: channel(match[3]),
1571
+ a: alpha(match[4])
1572
+ });
1573
+ }
1574
+ for (const match of background.matchAll(EXOTIC_RE)) {
1575
+ const resolved = viaCanvas(match[0]);
1576
+ if (resolved) colors.push(resolved);
1577
+ }
1578
+ return colors;
1579
+ }
1580
+ function linearChannel(value) {
1581
+ const srgb = Math.max(0, Math.min(255, value)) / 255;
1582
+ return srgb <= 0.04045 ? srgb / 12.92 : ((srgb + 0.055) / 1.055) ** 2.4;
1583
+ }
1584
+ function luminance({ r, g, b }) {
1585
+ return 0.2126 * linearChannel(r) + 0.7152 * linearChannel(g) + 0.0722 * linearChannel(b);
1586
+ }
1587
+ var SEED = "#010203";
1588
+ var probe;
1589
+ function viaCanvas(value) {
1590
+ if (typeof document === "undefined") return null;
1591
+ if (probe === void 0) {
1592
+ probe = document.createElement("canvas").getContext?.("2d", { willReadFrequently: true }) ?? null;
1593
+ }
1594
+ if (!probe) return null;
1595
+ probe.fillStyle = SEED;
1596
+ probe.fillStyle = value;
1597
+ if (probe.fillStyle === SEED) return null;
1598
+ probe.clearRect(0, 0, 1, 1);
1599
+ probe.fillRect(0, 0, 1, 1);
1600
+ const [r, g, b, a] = probe.getImageData(0, 0, 1, 1).data;
1601
+ return { r, g, b, a: a / 255 };
1602
+ }
1603
+ function backgroundLuminance(background) {
1604
+ const opaque = colorsIn(background).filter((color) => color.a >= OPAQUE);
1605
+ if (!opaque.length) return null;
1606
+ return opaque.reduce((total, color) => total + luminance(color), 0) / opaque.length;
1607
+ }
1608
+ function polarityOfBackground(background) {
1609
+ const measured = backgroundLuminance(background);
1610
+ if (measured === null) return null;
1611
+ return measured >= LIGHT_BACKDROP ? "light" : "dark";
1612
+ }
1613
+
1523
1614
  // vite-plugin/files/versionedFileResourceServer.ts
1524
1615
  var import_fs = __toESM(require("fs"), 1);
1525
1616
  var import_path = __toESM(require("path"), 1);
@@ -2472,6 +2563,36 @@ var tokensCssMigration_2026_08_26_gradientStops = {
2472
2563
  }
2473
2564
  };
2474
2565
 
2566
+ // vite-plugin/tokensCssMigrations/migrations/2026-08-27-backdrop-polarity.ts
2567
+ var MARKER = "[data-backdrop='dark']";
2568
+ var RULES = `/* BACKDROP POLARITY \u2014 one attribute says which way a surface leans. State it
2569
+ in markup (\`<section data-backdrop="dark">\`) or let \`use:backdrop\` measure
2570
+ and stamp it. Either way \`light-dark()\` inside resolves the right half, and
2571
+ a rule can key on the attribute for what colour alone cannot carry. */
2572
+
2573
+ [data-backdrop='light'] {
2574
+ color-scheme: light;
2575
+ }
2576
+
2577
+ [data-backdrop='dark'] {
2578
+ color-scheme: dark;
2579
+ }
2580
+ `;
2581
+ var tokensCssMigration_2026_08_27_backdropPolarity = {
2582
+ id: "2026-08-27-backdrop-polarity",
2583
+ kind: "additive",
2584
+ description: "Add the [data-backdrop] \u2192 color-scheme rules",
2585
+ apply(css) {
2586
+ if (css.includes(MARKER)) return css;
2587
+ const at = css.indexOf(":root");
2588
+ if (at === -1) return `${css.trimEnd()}
2589
+
2590
+ ${RULES}`;
2591
+ return `${css.slice(0, at)}${RULES}
2592
+ ${css.slice(at)}`;
2593
+ }
2594
+ };
2595
+
2475
2596
  // vite-plugin/tokensCssMigrations/index.ts
2476
2597
  var TOKENS_CSS_MIGRATIONS = [
2477
2598
  tokensCssMigration_2026_05_29_typographyScaleAdditions,
@@ -2483,7 +2604,8 @@ var TOKENS_CSS_MIGRATIONS = [
2483
2604
  tokensCssMigration_2026_07_20_semanticTextStyles,
2484
2605
  tokensCssMigration_2026_08_19_oklchColorValues,
2485
2606
  tokensCssMigration_2026_08_25_editorialTypeRole,
2486
- tokensCssMigration_2026_08_26_gradientStops
2607
+ tokensCssMigration_2026_08_26_gradientStops,
2608
+ tokensCssMigration_2026_08_27_backdropPolarity
2487
2609
  ];
2488
2610
  function runTokensCssMigrations(css) {
2489
2611
  return foldMigrations(css, () => true);
@@ -2724,6 +2846,7 @@ function themeFileApi(opts) {
2724
2846
  }
2725
2847
  const colorsAndTypeData = productionTheme?.colorsAndType;
2726
2848
  let colorsAndTypeVarCount = 0;
2849
+ let pageBackground = "";
2727
2850
  if (colorsAndTypeData) {
2728
2851
  const cssVars = { ...colorsAndTypeData.cssVariables || {} };
2729
2852
  Object.assign(cssVars, palettesToVars(migratePaletteColorsToOklch(colorsAndTypeData.editorConfigs ?? {})));
@@ -2731,6 +2854,7 @@ function themeFileApi(opts) {
2731
2854
  for (const [name, value] of Object.entries(resolvedFontVars)) {
2732
2855
  cssVars[name] = value;
2733
2856
  }
2857
+ pageBackground = cssVars["--page-bg"] ?? "";
2734
2858
  colorsAndTypeVarCount = Object.keys(cssVars).length;
2735
2859
  if (colorsAndTypeVarCount > 0) {
2736
2860
  lines.push(`/* Production theme: ${productionThemeName} */`);
@@ -2742,6 +2866,16 @@ function themeFileApi(opts) {
2742
2866
  lines.push("");
2743
2867
  }
2744
2868
  }
2869
+ {
2870
+ const polarity = pageBackground ? polarityOfBackground(pageBackground) : null;
2871
+ if (polarity) {
2872
+ lines.push(`/* Page polarity, from --page-bg */`);
2873
+ lines.push(":where(:root) {");
2874
+ lines.push(` color-scheme: ${polarity};`);
2875
+ lines.push("}");
2876
+ lines.push("");
2877
+ }
2878
+ }
2745
2879
  let componentOverrideCount = 0;
2746
2880
  if (import_fs4.default.existsSync(COMPONENT_CONFIGS_DIR)) {
2747
2881
  const blocks = [];
@@ -5,7 +5,7 @@ import {
5
5
  parseGradientValue,
6
6
  reconcilePalettesFromCssVars,
7
7
  sanitizeFileName
8
- } from "./chunk-LW4SR7AZ.js";
8
+ } from "./chunk-O6GQ6GBH.js";
9
9
  import {
10
10
  THEME_SCHEMA_VERSION,
11
11
  detectLegacyLayout,
@@ -23,10 +23,12 @@ import {
23
23
  runAdditiveTokensCssMigrations,
24
24
  runTokensCssMigrations,
25
25
  validateTokensCss
26
- } from "./chunk-E5QYON4L.js";
26
+ } from "./chunk-OIOXU7FR.js";
27
27
  import {
28
- hexToOklch
29
- } from "./chunk-XWXIMTWZ.js";
28
+ hexToOklch,
29
+ oklchToRgb255,
30
+ parseOklchCss
31
+ } from "./chunk-7TQQTHI6.js";
30
32
  import {
31
33
  resolveDataDirs
32
34
  } from "./chunk-Y5CNFSSV.js";
@@ -55,6 +57,87 @@ function migratePaletteColorsToOklch(editorConfigs) {
55
57
  return out;
56
58
  }
57
59
 
60
+ // src/system/backdrop/backdrop.ts
61
+ var LIGHT_BACKDROP = 0.2;
62
+ var OPAQUE = 0.98;
63
+ var HEX_RE = /#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})\b/gi;
64
+ var OKLCH_RE = /oklch\(\s*[\d.]+%?\s+[\d.]+%?\s+[\d.]+(?:deg)?\s*(?:\/\s*([\d.]+%?)\s*)?\)/gi;
65
+ var RGB_RE = /rgba?\(\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)(?:\s*[,/]\s*([\d.]+%?))?\s*\)/gi;
66
+ var EXOTIC_RE = /\b(?:color-mix|hwb|lab|lch|oklab|color)\([^()]*\)/gi;
67
+ function channel(value) {
68
+ const parsed = Number.parseFloat(value);
69
+ return value.endsWith("%") ? parsed * 2.55 : parsed;
70
+ }
71
+ function alpha(value) {
72
+ if (value === void 0) return 1;
73
+ const parsed = Number.parseFloat(value);
74
+ return value.endsWith("%") ? parsed / 100 : parsed;
75
+ }
76
+ function parseHex(hex) {
77
+ const digits = hex.slice(1);
78
+ const full = digits.length <= 4 ? digits.split("").map((part) => part + part).join("") : digits;
79
+ return {
80
+ r: Number.parseInt(full.slice(0, 2), 16),
81
+ g: Number.parseInt(full.slice(2, 4), 16),
82
+ b: Number.parseInt(full.slice(4, 6), 16),
83
+ a: full.length >= 8 ? Number.parseInt(full.slice(6, 8), 16) / 255 : 1
84
+ };
85
+ }
86
+ function colorsIn(background) {
87
+ const colors = [];
88
+ for (const match of background.matchAll(HEX_RE)) colors.push(parseHex(match[0]));
89
+ for (const match of background.matchAll(OKLCH_RE)) {
90
+ const parsed = parseOklchCss(match[0]);
91
+ if (parsed) colors.push({ ...oklchToRgb255(parsed.l, parsed.c, parsed.h), a: alpha(match[1]) });
92
+ }
93
+ for (const match of background.matchAll(RGB_RE)) {
94
+ colors.push({
95
+ r: channel(match[1]),
96
+ g: channel(match[2]),
97
+ b: channel(match[3]),
98
+ a: alpha(match[4])
99
+ });
100
+ }
101
+ for (const match of background.matchAll(EXOTIC_RE)) {
102
+ const resolved = viaCanvas(match[0]);
103
+ if (resolved) colors.push(resolved);
104
+ }
105
+ return colors;
106
+ }
107
+ function linearChannel(value) {
108
+ const srgb = Math.max(0, Math.min(255, value)) / 255;
109
+ return srgb <= 0.04045 ? srgb / 12.92 : ((srgb + 0.055) / 1.055) ** 2.4;
110
+ }
111
+ function luminance({ r, g, b }) {
112
+ return 0.2126 * linearChannel(r) + 0.7152 * linearChannel(g) + 0.0722 * linearChannel(b);
113
+ }
114
+ var SEED = "#010203";
115
+ var probe;
116
+ function viaCanvas(value) {
117
+ if (typeof document === "undefined") return null;
118
+ if (probe === void 0) {
119
+ probe = document.createElement("canvas").getContext?.("2d", { willReadFrequently: true }) ?? null;
120
+ }
121
+ if (!probe) return null;
122
+ probe.fillStyle = SEED;
123
+ probe.fillStyle = value;
124
+ if (probe.fillStyle === SEED) return null;
125
+ probe.clearRect(0, 0, 1, 1);
126
+ probe.fillRect(0, 0, 1, 1);
127
+ const [r, g, b, a] = probe.getImageData(0, 0, 1, 1).data;
128
+ return { r, g, b, a: a / 255 };
129
+ }
130
+ function backgroundLuminance(background) {
131
+ const opaque = colorsIn(background).filter((color) => color.a >= OPAQUE);
132
+ if (!opaque.length) return null;
133
+ return opaque.reduce((total, color) => total + luminance(color), 0) / opaque.length;
134
+ }
135
+ function polarityOfBackground(background) {
136
+ const measured = backgroundLuminance(background);
137
+ if (measured === null) return null;
138
+ return measured >= LIGHT_BACKDROP ? "light" : "dark";
139
+ }
140
+
58
141
  // vite-plugin/files/routeTable.ts
59
142
  async function dispatch(req, res, routes) {
60
143
  const url = req.url || "";
@@ -277,6 +360,7 @@ function themeFileApi(opts) {
277
360
  }
278
361
  const colorsAndTypeData = productionTheme?.colorsAndType;
279
362
  let colorsAndTypeVarCount = 0;
363
+ let pageBackground = "";
280
364
  if (colorsAndTypeData) {
281
365
  const cssVars = { ...colorsAndTypeData.cssVariables || {} };
282
366
  Object.assign(cssVars, palettesToVars(migratePaletteColorsToOklch(colorsAndTypeData.editorConfigs ?? {})));
@@ -284,6 +368,7 @@ function themeFileApi(opts) {
284
368
  for (const [name, value] of Object.entries(resolvedFontVars)) {
285
369
  cssVars[name] = value;
286
370
  }
371
+ pageBackground = cssVars["--page-bg"] ?? "";
287
372
  colorsAndTypeVarCount = Object.keys(cssVars).length;
288
373
  if (colorsAndTypeVarCount > 0) {
289
374
  lines.push(`/* Production theme: ${productionThemeName} */`);
@@ -295,6 +380,16 @@ function themeFileApi(opts) {
295
380
  lines.push("");
296
381
  }
297
382
  }
383
+ {
384
+ const polarity = pageBackground ? polarityOfBackground(pageBackground) : null;
385
+ if (polarity) {
386
+ lines.push(`/* Page polarity, from --page-bg */`);
387
+ lines.push(":where(:root) {");
388
+ lines.push(` color-scheme: ${polarity};`);
389
+ lines.push("}");
390
+ lines.push("");
391
+ }
392
+ }
298
393
  let componentOverrideCount = 0;
299
394
  if (fs.existsSync(COMPONENT_CONFIGS_DIR)) {
300
395
  const blocks = [];
@@ -517,6 +517,36 @@ var tokensCssMigration_2026_08_26_gradientStops = {
517
517
  }
518
518
  };
519
519
 
520
+ // vite-plugin/tokensCssMigrations/migrations/2026-08-27-backdrop-polarity.ts
521
+ var MARKER = "[data-backdrop='dark']";
522
+ var RULES = `/* BACKDROP POLARITY \u2014 one attribute says which way a surface leans. State it
523
+ in markup (\`<section data-backdrop="dark">\`) or let \`use:backdrop\` measure
524
+ and stamp it. Either way \`light-dark()\` inside resolves the right half, and
525
+ a rule can key on the attribute for what colour alone cannot carry. */
526
+
527
+ [data-backdrop='light'] {
528
+ color-scheme: light;
529
+ }
530
+
531
+ [data-backdrop='dark'] {
532
+ color-scheme: dark;
533
+ }
534
+ `;
535
+ var tokensCssMigration_2026_08_27_backdropPolarity = {
536
+ id: "2026-08-27-backdrop-polarity",
537
+ kind: "additive",
538
+ description: "Add the [data-backdrop] \u2192 color-scheme rules",
539
+ apply(css) {
540
+ if (css.includes(MARKER)) return css;
541
+ const at = css.indexOf(":root");
542
+ if (at === -1) return `${css.trimEnd()}
543
+
544
+ ${RULES}`;
545
+ return `${css.slice(0, at)}${RULES}
546
+ ${css.slice(at)}`;
547
+ }
548
+ };
549
+
520
550
  // vite-plugin/files/dataPaths.ts
521
551
  var import_fs = __toESM(require("fs"), 1);
522
552
  var import_path = __toESM(require("path"), 1);
@@ -562,7 +592,8 @@ var TOKENS_CSS_MIGRATIONS = [
562
592
  tokensCssMigration_2026_07_20_semanticTextStyles,
563
593
  tokensCssMigration_2026_08_19_oklchColorValues,
564
594
  tokensCssMigration_2026_08_25_editorialTypeRole,
565
- tokensCssMigration_2026_08_26_gradientStops
595
+ tokensCssMigration_2026_08_26_gradientStops,
596
+ tokensCssMigration_2026_08_27_backdropPolarity
566
597
  ];
567
598
  function runTokensCssMigrations(css) {
568
599
  return foldMigrations(css, () => true);
@@ -13,8 +13,8 @@ import {
13
13
  runTokensCssMigrations,
14
14
  semverBumpType,
15
15
  validateTokensCss
16
- } from "../chunk-E5QYON4L.js";
17
- import "../chunk-XWXIMTWZ.js";
16
+ } from "../chunk-OIOXU7FR.js";
17
+ import "../chunk-7TQQTHI6.js";
18
18
  import {
19
19
  readLiveTokensConfig
20
20
  } from "../chunk-Y5CNFSSV.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.60.1",
3
+ "version": "0.61.0",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -90,6 +90,11 @@
90
90
  "svelte": "./src/editor/docs/Docs.svelte",
91
91
  "default": "./src/editor/docs/Docs.svelte"
92
92
  },
93
+ "./backdrop": {
94
+ "svelte": "./src/system/backdrop/index.ts",
95
+ "types": "./src/system/backdrop/index.ts",
96
+ "default": "./src/system/backdrop/index.ts"
97
+ },
93
98
  "./components/*": {
94
99
  "svelte": "./src/system/components/*",
95
100
  "default": "./src/system/components/*"
@@ -8,6 +8,7 @@
8
8
  // for the interactive controls. No second variant.
9
9
  const states: Record<string, Token[]> = {
10
10
  tile: [
11
+ { label: 'fill color', groupKey: 'surface', variable: '--imagelightbox-tile-surface' },
11
12
  { label: 'corner radius', groupKey: 'radius', variable: '--imagelightbox-tile-radius' },
12
13
  { label: 'border color', groupKey: 'border', variable: '--imagelightbox-tile-border' },
13
14
  { label: 'border width', groupKey: 'width', variable: '--imagelightbox-tile-border-width' },
@@ -50,7 +51,7 @@
50
51
  <ComponentEditorBase
51
52
  {component}
52
53
  title="Image Lightbox"
53
- description="Click an inline image to expand it into a centered modal with a backdrop. Pass multiple images for a gallery (chevrons + counter). Extended mode adds zoom controls and drag panning. `shadow` picks where the one tile-shadow token falls: `box` casts it from the tile rectangle, `content` from the image's own alpha, or `none`."
54
+ description="Click an inline image to expand it into a centered modal with a backdrop. Pass multiple images for a gallery (chevrons + counter). Extended mode adds zoom controls and drag panning. `shadow` picks where the one tile-shadow token falls: `box` casts it from the tile rectangle, `content` from the image's own alpha, or `none`. The tile fill sits behind the image in both states: none by default, so cut-out art floats on the page, and any fill you set frames it instead."
54
55
  tokens={allTokens}
55
56
  >
56
57
  <VariantGroup name="imagelightbox" title="Image Lightbox" {states} {component}>
@@ -101,6 +101,7 @@
101
101
  { path: 'sketch-mode', title: 'Sketch mode' },
102
102
  { path: 'themes-workflow', title: 'Themes' },
103
103
  { path: 'where-themes-live', title: 'Where themes live' },
104
+ { path: 'light-and-dark', title: 'Light and dark' },
104
105
  { path: 'creating-components', title: 'Creating components' },
105
106
  ],
106
107
  },
@@ -16,6 +16,7 @@ export const chapters: Chapter[] = [
16
16
  { id: 'sketch-mode', title: 'Sketch mode' },
17
17
  { id: 'themes-workflow', title: 'Themes' },
18
18
  { id: 'where-themes-live', title: 'Where themes live' },
19
+ { id: 'light-and-dark', title: 'Light and dark' },
19
20
  { id: 'creating-components', title: 'Creating components' },
20
21
  ];
21
22