@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
package/dist-plugin/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
parseGradientValue,
|
|
6
6
|
reconcilePalettesFromCssVars,
|
|
7
7
|
sanitizeFileName
|
|
8
|
-
} from "./chunk-
|
|
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-
|
|
26
|
+
} from "./chunk-OPYOK2CA.js";
|
|
27
27
|
import {
|
|
28
|
-
hexToOklch
|
|
29
|
-
|
|
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 = [];
|
|
@@ -464,37 +464,15 @@ var tokensCssMigration_2026_08_19_oklchColorValues = {
|
|
|
464
464
|
|
|
465
465
|
// vite-plugin/tokensCssMigrations/migrations/2026-08-25-editorial-type-role.ts
|
|
466
466
|
var STACK = [{ name: "--font-editorial", value: "var(--font-sans)" }];
|
|
467
|
-
var BUNDLE2 = [
|
|
468
|
-
{ name: "--editorial-font-family", value: "var(--font-editorial)" },
|
|
469
|
-
{ name: "--editorial-font-size", value: "var(--font-size-md)" },
|
|
470
|
-
{ name: "--editorial-font-weight", value: "var(--font-weight-normal)" },
|
|
471
|
-
{ name: "--editorial-line-height", value: "var(--line-height-normal)" },
|
|
472
|
-
{ name: "--editorial-letter-spacing", value: "var(--letter-spacing-normal)" }
|
|
473
|
-
];
|
|
474
467
|
var tokensCssMigration_2026_08_25_editorialTypeRole = {
|
|
475
468
|
id: "2026-08-25-editorial-type-role",
|
|
476
469
|
kind: "additive",
|
|
477
|
-
description: "Add the editorial type role (--font-editorial
|
|
470
|
+
description: "Add the editorial type role (--font-editorial)",
|
|
478
471
|
apply(css) {
|
|
479
|
-
|
|
472
|
+
return ensureScale(css, {
|
|
480
473
|
anchorPrefixes: ["--font-mono", "--font-serif", "--font-sans", "--font-display"],
|
|
481
474
|
entries: STACK
|
|
482
475
|
});
|
|
483
|
-
return ensureScale(withStack, {
|
|
484
|
-
sectionComment: "Editorial \u2014 the long-reading role, body face until repointed",
|
|
485
|
-
anchorPrefixes: [
|
|
486
|
-
"--eyebrow-",
|
|
487
|
-
"--code-",
|
|
488
|
-
"--body-",
|
|
489
|
-
"--heading-",
|
|
490
|
-
"--letter-spacing-",
|
|
491
|
-
"--line-height-",
|
|
492
|
-
"--font-weight-",
|
|
493
|
-
"--font-size-",
|
|
494
|
-
"--font-"
|
|
495
|
-
],
|
|
496
|
-
entries: BUNDLE2
|
|
497
|
-
});
|
|
498
476
|
}
|
|
499
477
|
};
|
|
500
478
|
|
|
@@ -517,6 +495,77 @@ var tokensCssMigration_2026_08_26_gradientStops = {
|
|
|
517
495
|
}
|
|
518
496
|
};
|
|
519
497
|
|
|
498
|
+
// vite-plugin/tokensCssMigrations/migrations/2026-08-27-backdrop-polarity.ts
|
|
499
|
+
var MARKER = "[data-backdrop='dark']";
|
|
500
|
+
var RULES = `/* BACKDROP POLARITY \u2014 one attribute says which way a surface leans. State it
|
|
501
|
+
in markup (\`<section data-backdrop="dark">\`) or let \`use:backdrop\` measure
|
|
502
|
+
and stamp it. Either way \`light-dark()\` inside resolves the right half, and
|
|
503
|
+
a rule can key on the attribute for what colour alone cannot carry. */
|
|
504
|
+
|
|
505
|
+
[data-backdrop='light'] {
|
|
506
|
+
color-scheme: light;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
[data-backdrop='dark'] {
|
|
510
|
+
color-scheme: dark;
|
|
511
|
+
}
|
|
512
|
+
`;
|
|
513
|
+
var tokensCssMigration_2026_08_27_backdropPolarity = {
|
|
514
|
+
id: "2026-08-27-backdrop-polarity",
|
|
515
|
+
kind: "additive",
|
|
516
|
+
description: "Add the [data-backdrop] \u2192 color-scheme rules",
|
|
517
|
+
apply(css) {
|
|
518
|
+
if (css.includes(MARKER)) return css;
|
|
519
|
+
const at = css.indexOf(":root");
|
|
520
|
+
if (at === -1) return `${css.trimEnd()}
|
|
521
|
+
|
|
522
|
+
${RULES}`;
|
|
523
|
+
return `${css.slice(0, at)}${RULES}
|
|
524
|
+
${css.slice(at)}`;
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// vite-plugin/tokensCssMigrations/migrations/2026-08-27-editorial-size-steps.ts
|
|
529
|
+
var AXES = ["font-family", "font-size", "font-weight", "line-height", "letter-spacing"];
|
|
530
|
+
var BUNDLE2 = [
|
|
531
|
+
{ name: "--editorial-md-font-family", value: "var(--font-editorial)" },
|
|
532
|
+
{ name: "--editorial-md-font-size", value: "var(--font-size-md)" },
|
|
533
|
+
{ name: "--editorial-md-font-weight", value: "var(--font-weight-normal)" },
|
|
534
|
+
{ name: "--editorial-md-line-height", value: "var(--line-height-normal)" },
|
|
535
|
+
{ name: "--editorial-md-letter-spacing", value: "var(--letter-spacing-normal)" },
|
|
536
|
+
{ name: "--editorial-sm-font-family", value: "var(--font-editorial)" },
|
|
537
|
+
{ name: "--editorial-sm-font-size", value: "var(--font-size-sm)" },
|
|
538
|
+
{ name: "--editorial-sm-font-weight", value: "var(--font-weight-normal)" },
|
|
539
|
+
{ name: "--editorial-sm-line-height", value: "var(--line-height-tight)" },
|
|
540
|
+
{ name: "--editorial-sm-letter-spacing", value: "var(--letter-spacing-normal)" }
|
|
541
|
+
];
|
|
542
|
+
var tokensCssMigration_2026_08_27_editorialSizeSteps = {
|
|
543
|
+
id: "2026-08-27-editorial-size-steps",
|
|
544
|
+
kind: "breaking",
|
|
545
|
+
description: "Split the editorial bundle into --editorial-md-* and --editorial-sm-* steps",
|
|
546
|
+
apply(css) {
|
|
547
|
+
let out = css;
|
|
548
|
+
for (const axis of AXES) {
|
|
549
|
+
out = renameToken(out, `--editorial-${axis}`, `--editorial-md-${axis}`);
|
|
550
|
+
}
|
|
551
|
+
return ensureScale(out, {
|
|
552
|
+
sectionComment: "Editorial \u2014 the long-reading role, body face until repointed",
|
|
553
|
+
anchorPrefixes: [
|
|
554
|
+
"--eyebrow-",
|
|
555
|
+
"--code-",
|
|
556
|
+
"--body-",
|
|
557
|
+
"--heading-",
|
|
558
|
+
"--letter-spacing-",
|
|
559
|
+
"--line-height-",
|
|
560
|
+
"--font-weight-",
|
|
561
|
+
"--font-size-",
|
|
562
|
+
"--font-"
|
|
563
|
+
],
|
|
564
|
+
entries: BUNDLE2
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
520
569
|
// vite-plugin/files/dataPaths.ts
|
|
521
570
|
var import_fs = __toESM(require("fs"), 1);
|
|
522
571
|
var import_path = __toESM(require("path"), 1);
|
|
@@ -562,7 +611,9 @@ var TOKENS_CSS_MIGRATIONS = [
|
|
|
562
611
|
tokensCssMigration_2026_07_20_semanticTextStyles,
|
|
563
612
|
tokensCssMigration_2026_08_19_oklchColorValues,
|
|
564
613
|
tokensCssMigration_2026_08_25_editorialTypeRole,
|
|
565
|
-
tokensCssMigration_2026_08_26_gradientStops
|
|
614
|
+
tokensCssMigration_2026_08_26_gradientStops,
|
|
615
|
+
tokensCssMigration_2026_08_27_backdropPolarity,
|
|
616
|
+
tokensCssMigration_2026_08_27_editorialSizeSteps
|
|
566
617
|
];
|
|
567
618
|
function runTokensCssMigrations(css) {
|
|
568
619
|
return foldMigrations(css, () => true);
|
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
runTokensCssMigrations,
|
|
14
14
|
semverBumpType,
|
|
15
15
|
validateTokensCss
|
|
16
|
-
} from "../chunk-
|
|
17
|
-
import "../chunk-
|
|
16
|
+
} from "../chunk-OPYOK2CA.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.
|
|
3
|
+
"version": "0.62.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/*"
|
package/src/app/site.css
CHANGED
|
@@ -205,6 +205,22 @@ blockquote {
|
|
|
205
205
|
letter-spacing: var(--body-sm-letter-spacing);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
.editorial-md {
|
|
209
|
+
font-family: var(--editorial-md-font-family);
|
|
210
|
+
font-size: var(--editorial-md-font-size);
|
|
211
|
+
font-weight: var(--editorial-md-font-weight);
|
|
212
|
+
line-height: var(--editorial-md-line-height);
|
|
213
|
+
letter-spacing: var(--editorial-md-letter-spacing);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.editorial-sm {
|
|
217
|
+
font-family: var(--editorial-sm-font-family);
|
|
218
|
+
font-size: var(--editorial-sm-font-size);
|
|
219
|
+
font-weight: var(--editorial-sm-font-weight);
|
|
220
|
+
line-height: var(--editorial-sm-line-height);
|
|
221
|
+
letter-spacing: var(--editorial-sm-letter-spacing);
|
|
222
|
+
}
|
|
223
|
+
|
|
208
224
|
.eyebrow {
|
|
209
225
|
font-family: var(--eyebrow-font-family);
|
|
210
226
|
font-size: var(--eyebrow-font-size);
|
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
}
|
|
135
135
|
</script>
|
|
136
136
|
|
|
137
|
-
<ComponentEditorBase {component} title="Card" description="Generic card with icon, title, and slotted body." tokens={allTokens} {linked}>
|
|
137
|
+
<ComponentEditorBase {component} title="Card" description="Generic card with icon, title, and slotted body. The bare variant drops the header and keeps every frame and body token." tokens={allTokens} {linked}>
|
|
138
138
|
<VariantGroup
|
|
139
139
|
name="card"
|
|
140
140
|
title="Card"
|
|
@@ -165,6 +165,9 @@
|
|
|
165
165
|
<Card title="Card title" icon="fas fa-star" class={previewClass}>
|
|
166
166
|
<div class="content-placeholder">Content Placeholder</div>
|
|
167
167
|
</Card>
|
|
168
|
+
<Card variant="bare" class={previewClass}>
|
|
169
|
+
<div class="content-placeholder">Bare variant</div>
|
|
170
|
+
</Card>
|
|
168
171
|
</div>
|
|
169
172
|
{/snippet}
|
|
170
173
|
</VariantGroup>
|
|
@@ -172,6 +175,9 @@
|
|
|
172
175
|
|
|
173
176
|
<style>
|
|
174
177
|
.card-demo {
|
|
178
|
+
display: flex;
|
|
179
|
+
flex-direction: column;
|
|
180
|
+
gap: var(--ui-space-12);
|
|
175
181
|
min-width: 16rem;
|
|
176
182
|
max-width: 28rem;
|
|
177
183
|
}
|
|
@@ -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}>
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import type { CssVarRef } from '../../core/store/editorTypes';
|
|
12
12
|
import { cssStringToRef, refToCss } from '../../core/store/cssVarRef';
|
|
13
13
|
import { getEditorContext } from './editorContext';
|
|
14
|
+
import { setSketchScope } from '../../core/sketch/sketchLayer';
|
|
15
|
+
import { sketchEnabled, sketchSettings } from '../../core/sketch/sketchStore';
|
|
14
16
|
import type { Token, TypeGroupConfig } from './types';
|
|
15
17
|
import type { Sibling } from './siblings';
|
|
16
18
|
|
|
@@ -84,6 +86,15 @@
|
|
|
84
86
|
let bgMode: 'default' | 'image' | 'color' = $state('default');
|
|
85
87
|
let bgVar = $derived(`--backdrop-${component ?? name}-surface`);
|
|
86
88
|
|
|
89
|
+
// The demo shows the component as the page will draw it, sketch included, so
|
|
90
|
+
// the scope attribute goes on this wrapper. It stops here: the tab strips and
|
|
91
|
+
// property rows around it are chrome and run on --ui-* tokens the layer knows
|
|
92
|
+
// nothing about. Only one of the four render sites below is ever mounted.
|
|
93
|
+
let sketchStage = $state<HTMLDivElement | undefined>(undefined);
|
|
94
|
+
$effect(() => {
|
|
95
|
+
setSketchScope(sketchStage ?? null, $sketchEnabled ? $sketchSettings : null);
|
|
96
|
+
});
|
|
97
|
+
|
|
87
98
|
const editorCtx = getEditorContext();
|
|
88
99
|
const linkedOrderStore = editorCtx?.linkedOrder ?? writable<Map<string, number> | null>(null);
|
|
89
100
|
const focusedVariantStore = editorCtx?.focusedVariant ?? writable<string | null>(null);
|
|
@@ -397,7 +408,9 @@
|
|
|
397
408
|
{/if}
|
|
398
409
|
</div>
|
|
399
410
|
{#if unboxedPreview}
|
|
400
|
-
|
|
411
|
+
<div class="sketch-scope" bind:this={sketchStage}>
|
|
412
|
+
{@render children?.({ activeState: activeTab })}
|
|
413
|
+
</div>
|
|
401
414
|
{:else}
|
|
402
415
|
<ShadowBackdrop mode={bgMode} colorVariable={bgVar} padding={backdropPadding}>
|
|
403
416
|
{#snippet controls()}
|
|
@@ -407,7 +420,9 @@
|
|
|
407
420
|
{@render canvasToolbarExtras?.()}
|
|
408
421
|
</div>
|
|
409
422
|
{/snippet}
|
|
423
|
+
<div class="sketch-scope" bind:this={sketchStage}>
|
|
410
424
|
{@render children?.({ activeState: activeTab })}
|
|
425
|
+
</div>
|
|
411
426
|
</ShadowBackdrop>
|
|
412
427
|
{/if}
|
|
413
428
|
|
|
@@ -508,10 +523,14 @@
|
|
|
508
523
|
{/if}
|
|
509
524
|
{:else}
|
|
510
525
|
{#if unboxedPreview}
|
|
511
|
-
|
|
526
|
+
<div class="sketch-scope" bind:this={sketchStage}>
|
|
527
|
+
{@render children?.({ activeState: '' })}
|
|
528
|
+
</div>
|
|
512
529
|
{:else}
|
|
513
530
|
<ShadowBackdrop mode={bgMode} colorVariable={bgVar} padding={backdropPadding}>
|
|
531
|
+
<div class="sketch-scope" bind:this={sketchStage}>
|
|
514
532
|
{@render children?.({ activeState: '' })}
|
|
533
|
+
</div>
|
|
515
534
|
</ShadowBackdrop>
|
|
516
535
|
{/if}
|
|
517
536
|
<TokenLayout
|
|
@@ -572,6 +591,10 @@
|
|
|
572
591
|
pointer-events: none;
|
|
573
592
|
}
|
|
574
593
|
|
|
594
|
+
.sketch-scope {
|
|
595
|
+
display: contents;
|
|
596
|
+
}
|
|
597
|
+
|
|
575
598
|
.preview-header {
|
|
576
599
|
display: flex;
|
|
577
600
|
align-items: center;
|
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
* narrow band around its own midpoint (about 0.24 to 0.77 for fractal noise,
|
|
9
9
|
* whatever the octaves), so a dial that walks a cut across 0 to 1 spends most
|
|
10
10
|
* of its travel outside the field entirely and the little that lands inside
|
|
11
|
-
* comes out as one flat mid-grey.
|
|
12
|
-
*
|
|
13
|
-
* the
|
|
11
|
+
* comes out as one flat mid-grey.
|
|
12
|
+
*
|
|
13
|
+
* Generated here, the tile is auto-levelled before any dial sees it, and the
|
|
14
|
+
* dials that follow are a levels control: Steps quantises the field, Output
|
|
15
|
+
* squeezes it into the density range the mask paints between. Neither ever
|
|
16
|
+
* clips: the field arrives running black to white and every tone survives to
|
|
17
|
+
* the other end in the order it started.
|
|
14
18
|
*
|
|
15
19
|
* Everything is integer maths on a Float32Array with no DOM, so what a test
|
|
16
20
|
* asserts is what the browser paints.
|
|
@@ -29,7 +33,7 @@ const SAMPLE_PX = 2;
|
|
|
29
33
|
const RASTER = MASK_TILE / SAMPLE_PX;
|
|
30
34
|
|
|
31
35
|
/** The stage previews, in the order the field is built. */
|
|
32
|
-
export type MaskStage = 'noise' | '
|
|
36
|
+
export type MaskStage = 'noise' | 'levels' | 'blur';
|
|
33
37
|
|
|
34
38
|
/* ---------------------------------------------------------------- noise --- */
|
|
35
39
|
|
|
@@ -128,41 +132,55 @@ function rawField(s: SketchSettings, seed: number, raster: number, cells: number
|
|
|
128
132
|
out[y * raster + x] = sum / weightSum;
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
|
-
return
|
|
135
|
+
return equalise(out);
|
|
132
136
|
}
|
|
133
137
|
|
|
138
|
+
const BINS = 4096;
|
|
139
|
+
|
|
134
140
|
/**
|
|
135
|
-
*
|
|
141
|
+
* Auto levels: map the field through its own cumulative distribution, so tone
|
|
142
|
+
* is spread evenly from black to white whatever the noise underneath.
|
|
136
143
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* one
|
|
141
|
-
*
|
|
144
|
+
* A black-and-white-point stretch is not enough on its own. Noise piles up
|
|
145
|
+
* around its middle and the veined fold piles it up at the bottom — half a
|
|
146
|
+
* one-layer veined tile sits under 0.29 — so a tile stretched by its extremes
|
|
147
|
+
* still reads as one dark wash, and every dial downstream cuts the range at
|
|
148
|
+
* points most of the field is nowhere near. Equalised, the median lands at 0.5
|
|
149
|
+
* and each fifth of the range holds a fifth of the tile, so a handle at 30
|
|
150
|
+
* addresses the darkest 30% of the field at every grain and octave count.
|
|
142
151
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
152
|
+
* Piecewise-linear through a histogram rather than by rank, so the mapping
|
|
153
|
+
* stays smooth and cannot band a gradient it is meant to spread.
|
|
145
154
|
*/
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
const lo =
|
|
149
|
-
const hi = sorted[Math.ceil(0.995 * (sorted.length - 1))];
|
|
155
|
+
function equalise(f: Float32Array): Float32Array {
|
|
156
|
+
let lo = Infinity, hi = -Infinity;
|
|
157
|
+
for (const v of f) { if (v < lo) lo = v; if (v > hi) hi = v; }
|
|
150
158
|
const span = hi - lo;
|
|
151
159
|
if (span <= 1e-6) return f.fill(0.5);
|
|
152
|
-
|
|
160
|
+
|
|
161
|
+
const count = new Float64Array(BINS);
|
|
162
|
+
for (const v of f) count[Math.min(BINS - 1, Math.floor(((v - lo) / span) * BINS))]++;
|
|
163
|
+
const below = new Float64Array(BINS + 1);
|
|
164
|
+
for (let b = 0; b < BINS; b++) below[b + 1] = below[b] + count[b];
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < f.length; i++) {
|
|
167
|
+
const t = ((f[i] - lo) / span) * BINS;
|
|
168
|
+
const b = Math.min(BINS - 1, Math.floor(t));
|
|
169
|
+
f[i] = (below[b] + (t - b) * count[b]) / f.length;
|
|
170
|
+
}
|
|
153
171
|
return f;
|
|
154
172
|
}
|
|
155
173
|
|
|
156
174
|
/* ------------------------------------------------------------- levelling --- */
|
|
157
175
|
|
|
158
|
-
/** Output levels
|
|
159
|
-
|
|
160
|
-
|
|
176
|
+
/** Output levels, the same squeeze Photoshop's output handles apply. The whole
|
|
177
|
+
field is stretched into the gap between them, so the pair states the range
|
|
178
|
+
the mask paints: nothing is barer than Min or denser than Max, and every
|
|
179
|
+
tone in between keeps its place in the order.
|
|
161
180
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
comes out a flat wash at the floor with a thin bright tail above it. */
|
|
181
|
+
Never a cut. Clipping to the handles instead would make Min the value most
|
|
182
|
+
of the field sits AT rather than its floor, and the fill would come out a
|
|
183
|
+
flat wash at the floor with a thin bright tail above it. */
|
|
166
184
|
function applyOutput(f: Float32Array, min: number, max: number): Float32Array {
|
|
167
185
|
const span = max - min;
|
|
168
186
|
for (let i = 0; i < f.length; i++) f[i] = min + f[i] * span;
|
|
@@ -178,14 +196,30 @@ function posterise(f: Float32Array, steps: number): Float32Array {
|
|
|
178
196
|
return f;
|
|
179
197
|
}
|
|
180
198
|
|
|
181
|
-
/**
|
|
182
|
-
where the tile repeats.
|
|
199
|
+
/** Wrapped box blur, so the blur cannot draw a rim where the tile repeats.
|
|
183
200
|
|
|
184
|
-
|
|
185
|
-
|
|
201
|
+
Three boxes at the width the filter spec derives land on a gaussian of a
|
|
202
|
+
given deviation, which is what makes the dial's px the px it gets. But a box
|
|
203
|
+
radius is a whole number of samples and a sample is two page px, so rounding
|
|
204
|
+
to one put the dial on a 2px ladder with a dead zone at the bottom: every
|
|
205
|
+
setting under 2.5px came out perfectly sharp, and everything from 2.5 to
|
|
206
|
+
4.2px came out identical. Mixing the two radii either side of the exact one
|
|
207
|
+
puts the dial back on a continuous scale, and radius zero is the field
|
|
208
|
+
itself, so the bottom of the travel eases in instead of switching on. */
|
|
186
209
|
function blur(f: Float32Array, raster: number, std: number): Float32Array {
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
210
|
+
const exact = (std * 3 * Math.sqrt(2 * Math.PI) / 4 - 1) / 2;
|
|
211
|
+
if (exact <= 0) return f;
|
|
212
|
+
const lower = Math.floor(exact);
|
|
213
|
+
const mix = exact - lower;
|
|
214
|
+
const low = lower < 1 ? f : boxes(f, raster, lower);
|
|
215
|
+
if (mix < 1e-6) return low;
|
|
216
|
+
const high = boxes(f, raster, lower + 1);
|
|
217
|
+
const out = new Float32Array(f.length);
|
|
218
|
+
for (let i = 0; i < f.length; i++) out[i] = low[i] + (high[i] - low[i]) * mix;
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function boxes(f: Float32Array, raster: number, radius: number): Float32Array {
|
|
189
223
|
let cur = f;
|
|
190
224
|
for (let pass = 0; pass < 3; pass++) cur = boxPass(cur, raster, radius);
|
|
191
225
|
return cur;
|
|
@@ -234,8 +268,10 @@ function cachedRaw(s: SketchSettings, seed: number): Float32Array {
|
|
|
234
268
|
/**
|
|
235
269
|
* The finished field, 0 (bare) to 1 (inked), row-major.
|
|
236
270
|
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
271
|
+
* `through` stops the pipeline early. Nothing in the app asks for a part of it
|
|
272
|
+
* — the tab shows the finished field and nothing else — but the levels are
|
|
273
|
+
* three passes over one array, and the seam is where the tests read what each
|
|
274
|
+
* pass did.
|
|
239
275
|
*/
|
|
240
276
|
export function buildMaskField(
|
|
241
277
|
s: SketchSettings, seed = 9, through?: MaskStage,
|
|
@@ -246,7 +282,7 @@ export function buildMaskField(
|
|
|
246
282
|
const levelled = applyOutput(
|
|
247
283
|
posterise(Float32Array.from(raw), s.maskPosterize), s.maskOutputMin, s.maskOutputMax,
|
|
248
284
|
);
|
|
249
|
-
if (through === '
|
|
285
|
+
if (through === 'levels') return { field: levelled, raster: RASTER };
|
|
250
286
|
|
|
251
287
|
return { field: blur(levelled, RASTER, s.maskSoftness / SAMPLE_PX), raster: RASTER };
|
|
252
288
|
}
|