@motion-proto/live-tokens 0.61.0 → 0.63.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-adjust-geometry/SKILL.md +4 -4
- package/.claude/skills/live-tokens-build-page/SKILL.md +21 -3
- package/.claude/skills/live-tokens-create-component/SKILL.md +16 -48
- package/.claude/skills/live-tokens-create-component/references/fixed-overlays.md +10 -1
- package/.claude/skills/live-tokens-create-component/references/intrinsics.md +7 -5
- package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +1 -1
- package/.claude/skills/live-tokens-create-component/references/token-naming.md +50 -0
- package/.claude/skills/live-tokens-generate-theme/SKILL.md +16 -9
- package/.claude/skills/live-tokens-pair-fonts/SKILL.md +8 -6
- package/.claude/skills/live-tokens-pick-component/SKILL.md +19 -5
- package/CHANGELOG.md +114 -0
- package/bin/migrate.mjs +6 -2
- package/dist-plugin/adjust/index.cjs +1 -1
- package/dist-plugin/adjust/index.d.cts +1 -1
- package/dist-plugin/adjust/index.d.ts +1 -1
- package/dist-plugin/adjust/index.js +1 -1
- package/dist-plugin/{chunk-232GZGQU.js → chunk-NDJJORKJ.js} +342 -5
- package/dist-plugin/{chunk-Y5CNFSSV.js → chunk-RVE3MNKM.js} +1 -1
- package/dist-plugin/{chunk-OIOXU7FR.js → chunk-ZHPX7ZYQ.js} +83 -25
- package/dist-plugin/{dataPaths-CRfD1LdA.d.ts → dataPaths-DZUzVv8H.d.cts} +3 -3
- package/dist-plugin/{dataPaths-CRfD1LdA.d.cts → dataPaths-DZUzVv8H.d.ts} +3 -3
- package/dist-plugin/fontPairing/index.cjs +1 -1
- package/dist-plugin/fontPairing/index.d.cts +1 -1
- package/dist-plugin/fontPairing/index.d.ts +1 -1
- package/dist-plugin/fontPairing/index.js +1 -1
- package/dist-plugin/generateColorsAndType/index.cjs +1 -1
- package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
- package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
- package/dist-plugin/generateColorsAndType/index.js +1 -1
- package/dist-plugin/index.cjs +461 -58
- package/dist-plugin/index.d.cts +1 -1
- package/dist-plugin/index.d.ts +1 -1
- package/dist-plugin/index.js +31 -23
- package/dist-plugin/migrateData/index.cjs +350 -9
- package/dist-plugin/migrateData/index.d.cts +1 -1
- package/dist-plugin/migrateData/index.d.ts +1 -1
- package/dist-plugin/migrateData/index.js +9 -5
- package/dist-plugin/tokensCssMigrations/index.cjs +83 -25
- package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
- package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
- package/dist-plugin/tokensCssMigrations/index.js +2 -2
- package/package.json +4 -2
- package/src/app/site.css +32 -0
- package/src/editor/component-editor/CardEditor.svelte +7 -1
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +25 -2
- package/src/editor/core/preview/lookPreview.ts +46 -5
- package/src/editor/core/productionPulse.ts +6 -2
- package/src/editor/core/sketch/maskField.ts +75 -39
- package/src/editor/core/sketch/sketchLayer.ts +32 -11
- package/src/editor/core/sketch/sketchStore.ts +311 -86
- package/src/editor/core/sketch/{sketchPresetService.ts → sketchStyleService.ts} +14 -14
- package/src/editor/core/sketch/{sketchPresets.ts → sketchStyles.ts} +27 -28
- package/src/editor/core/themes/themeDocumentSync.ts +2 -0
- package/src/editor/core/themes/themeInit.ts +19 -1
- package/src/editor/core/themes/themeService.ts +7 -2
- package/src/editor/core/themes/themeTypes.ts +5 -0
- package/src/editor/docs/content/editing-tokens.md +1 -1
- package/src/editor/docs/content/sketch-mode.md +34 -18
- package/src/editor/docs/content/themes-workflow.md +30 -21
- package/src/editor/docs/content/where-themes-live.md +9 -5
- package/src/editor/docs/content.generated.ts +4 -4
- package/src/editor/overlay/LiveTokensRouter.svelte +8 -0
- package/src/editor/ui/EditorViewSwitcher.svelte +3 -3
- package/src/editor/ui/ThemePanel.svelte +47 -1
- package/src/editor/ui/sections/textStyles.ts +29 -1
- package/src/editor/ui/sketch/SketchPreview.svelte +3 -3
- package/src/editor/ui/sketch/SketchTab.svelte +120 -134
- package/src/live-tokens/data/colors-and-type/midnight-study.json +40 -30
- package/src/live-tokens/data/themes/autumn.json +3 -3
- package/src/live-tokens/data/themes/halloween.json +3 -3
- package/src/live-tokens/data/themes/midnight-study.json +61 -49
- package/src/live-tokens/data/themes/ocean.json +3 -3
- package/src/live-tokens/data/themes/royal-velvet.json +3 -3
- package/src/live-tokens/data/themes/sketchy.json +3 -3
- package/src/live-tokens/data/themes/spring-meadow.json +3 -3
- package/src/live-tokens/data/themes/sunset.json +3 -3
- package/src/system/components/Card.svelte +27 -9
- package/src/system/components/FloatingTokenTags.css +10 -8
- package/src/system/components/ImageLightbox.svelte +5 -2
- package/src/system/components/SectionDivider.svelte +3 -3
- package/src/system/components/SegmentedControl.svelte +11 -9
- package/src/system/styles/tokens.css +28 -5
- package/template/src/pages/Home.svelte +1 -11
|
@@ -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
|
|
|
@@ -547,6 +525,84 @@ ${css.slice(at)}`;
|
|
|
547
525
|
}
|
|
548
526
|
};
|
|
549
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
|
+
|
|
569
|
+
// vite-plugin/tokensCssMigrations/migrations/2026-08-27-editorial-large-steps.ts
|
|
570
|
+
var BUNDLE3 = [
|
|
571
|
+
{ name: "--editorial-xl-font-family", value: "var(--font-editorial)" },
|
|
572
|
+
{ name: "--editorial-xl-font-size", value: "var(--font-size-xl)" },
|
|
573
|
+
{ name: "--editorial-xl-font-weight", value: "var(--font-weight-normal)" },
|
|
574
|
+
{ name: "--editorial-xl-line-height", value: "var(--line-height-tighter)" },
|
|
575
|
+
{ name: "--editorial-xl-letter-spacing", value: "var(--letter-spacing-normal)" },
|
|
576
|
+
{ name: "--editorial-lg-font-family", value: "var(--font-editorial)" },
|
|
577
|
+
{ name: "--editorial-lg-font-size", value: "var(--font-size-lg)" },
|
|
578
|
+
{ name: "--editorial-lg-font-weight", value: "var(--font-weight-normal)" },
|
|
579
|
+
{ name: "--editorial-lg-line-height", value: "var(--line-height-tight)" },
|
|
580
|
+
{ name: "--editorial-lg-letter-spacing", value: "var(--letter-spacing-normal)" }
|
|
581
|
+
];
|
|
582
|
+
var tokensCssMigration_2026_08_27_editorialLargeSteps = {
|
|
583
|
+
id: "2026-08-27-editorial-large-steps",
|
|
584
|
+
kind: "additive",
|
|
585
|
+
description: "Add the --editorial-lg-* and --editorial-xl-* steps above the reading size",
|
|
586
|
+
apply(css) {
|
|
587
|
+
return ensureScale(css, {
|
|
588
|
+
sectionComment: "Editorial \u2014 the steps above the reading size",
|
|
589
|
+
anchorPrefixes: [
|
|
590
|
+
"--editorial-",
|
|
591
|
+
"--eyebrow-",
|
|
592
|
+
"--code-",
|
|
593
|
+
"--body-",
|
|
594
|
+
"--heading-",
|
|
595
|
+
"--letter-spacing-",
|
|
596
|
+
"--line-height-",
|
|
597
|
+
"--font-weight-",
|
|
598
|
+
"--font-size-",
|
|
599
|
+
"--font-"
|
|
600
|
+
],
|
|
601
|
+
entries: BUNDLE3
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
550
606
|
// vite-plugin/files/dataPaths.ts
|
|
551
607
|
var import_fs = __toESM(require("fs"), 1);
|
|
552
608
|
var import_path = __toESM(require("path"), 1);
|
|
@@ -593,7 +649,9 @@ var TOKENS_CSS_MIGRATIONS = [
|
|
|
593
649
|
tokensCssMigration_2026_08_19_oklchColorValues,
|
|
594
650
|
tokensCssMigration_2026_08_25_editorialTypeRole,
|
|
595
651
|
tokensCssMigration_2026_08_26_gradientStops,
|
|
596
|
-
tokensCssMigration_2026_08_27_backdropPolarity
|
|
652
|
+
tokensCssMigration_2026_08_27_backdropPolarity,
|
|
653
|
+
tokensCssMigration_2026_08_27_editorialSizeSteps,
|
|
654
|
+
tokensCssMigration_2026_08_27_editorialLargeSteps
|
|
597
655
|
];
|
|
598
656
|
function runTokensCssMigrations(css) {
|
|
599
657
|
return foldMigrations(css, () => true);
|
|
@@ -13,11 +13,11 @@ import {
|
|
|
13
13
|
runTokensCssMigrations,
|
|
14
14
|
semverBumpType,
|
|
15
15
|
validateTokensCss
|
|
16
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-ZHPX7ZYQ.js";
|
|
17
17
|
import "../chunk-7TQQTHI6.js";
|
|
18
18
|
import {
|
|
19
19
|
readLiveTokensConfig
|
|
20
|
-
} from "../chunk-
|
|
20
|
+
} from "../chunk-RVE3MNKM.js";
|
|
21
21
|
export {
|
|
22
22
|
TOKENS_CSS_MIGRATIONS,
|
|
23
23
|
collectDefinedTokens,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motion-proto/live-tokens",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
|
|
6
6
|
"keywords": [
|
|
@@ -137,13 +137,15 @@
|
|
|
137
137
|
"check:token-contract": "node scripts/check-token-contract.mjs",
|
|
138
138
|
"check:preset-themes": "node scripts/check-preset-themes.mjs",
|
|
139
139
|
"check:skills": "node scripts/check-skills.mjs",
|
|
140
|
+
"check:skill-atlas": "node scripts/sync-skill-atlas.mjs",
|
|
140
141
|
"sync:component-defaults": "node scripts/sync-component-defaults.mjs --write",
|
|
141
142
|
"sync:docs": "node scripts/sync-docs.mjs --write",
|
|
143
|
+
"sync:skill-atlas": "node scripts/sync-skill-atlas.mjs --write",
|
|
142
144
|
"seed:preset-theme": "node scripts/seed-preset-theme.mjs",
|
|
143
145
|
"collapse:theme": "node scripts/collapse-theme-to-default.mjs",
|
|
144
146
|
"check:smoke-install": "bash scripts/smoke-install.sh",
|
|
145
147
|
"check:smoke-create": "bash scripts/smoke-create.sh",
|
|
146
|
-
"prepublishOnly": "npm run check:no-style-imports && npm run check:no-tooling-imports && npm run check:slot-prose && npm run check:overlay-portal && npm run check:editor-font-isolation && npm run check:component-defaults && npm run check:production-is-default && npm run check:docs-content && npm run build:lib && npm run check:token-contract && npm run check:preset-themes && npm run check:skills && npm run check:smoke-install && npm run check:smoke-create"
|
|
148
|
+
"prepublishOnly": "npm run check:no-style-imports && npm run check:no-tooling-imports && npm run check:slot-prose && npm run check:overlay-portal && npm run check:editor-font-isolation && npm run check:component-defaults && npm run check:production-is-default && npm run check:docs-content && npm run build:lib && npm run check:token-contract && npm run check:preset-themes && npm run check:skills && npm run check:skill-atlas && npm run check:smoke-install && npm run check:smoke-create"
|
|
147
149
|
},
|
|
148
150
|
"peerDependencies": {
|
|
149
151
|
"@sveltejs/vite-plugin-svelte": "^7.0",
|
package/src/app/site.css
CHANGED
|
@@ -205,6 +205,38 @@ blockquote {
|
|
|
205
205
|
letter-spacing: var(--body-sm-letter-spacing);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
.editorial-xl {
|
|
209
|
+
font-family: var(--editorial-xl-font-family);
|
|
210
|
+
font-size: var(--editorial-xl-font-size);
|
|
211
|
+
font-weight: var(--editorial-xl-font-weight);
|
|
212
|
+
line-height: var(--editorial-xl-line-height);
|
|
213
|
+
letter-spacing: var(--editorial-xl-letter-spacing);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.editorial-lg {
|
|
217
|
+
font-family: var(--editorial-lg-font-family);
|
|
218
|
+
font-size: var(--editorial-lg-font-size);
|
|
219
|
+
font-weight: var(--editorial-lg-font-weight);
|
|
220
|
+
line-height: var(--editorial-lg-line-height);
|
|
221
|
+
letter-spacing: var(--editorial-lg-letter-spacing);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.editorial-md {
|
|
225
|
+
font-family: var(--editorial-md-font-family);
|
|
226
|
+
font-size: var(--editorial-md-font-size);
|
|
227
|
+
font-weight: var(--editorial-md-font-weight);
|
|
228
|
+
line-height: var(--editorial-md-line-height);
|
|
229
|
+
letter-spacing: var(--editorial-md-letter-spacing);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.editorial-sm {
|
|
233
|
+
font-family: var(--editorial-sm-font-family);
|
|
234
|
+
font-size: var(--editorial-sm-font-size);
|
|
235
|
+
font-weight: var(--editorial-sm-font-weight);
|
|
236
|
+
line-height: var(--editorial-sm-line-height);
|
|
237
|
+
letter-spacing: var(--editorial-sm-letter-spacing);
|
|
238
|
+
}
|
|
239
|
+
|
|
208
240
|
.eyebrow {
|
|
209
241
|
font-family: var(--eyebrow-font-family);
|
|
210
242
|
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
|
}
|
|
@@ -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;
|
|
@@ -6,6 +6,7 @@ import { batchCssVarChanges, setCssVar, removeCssVar } from '../cssVarSync';
|
|
|
6
6
|
import { applyFontSources } from '../fonts/fontLoader';
|
|
7
7
|
import { migrateColorsAndTypeFonts } from '../fonts/fontMigration';
|
|
8
8
|
import { loadTheme } from '../themes/themeService';
|
|
9
|
+
import { previewSketchStyle, revertSketchStylePreview } from '../sketch/sketchStore';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Client-side rendering of a saved file, for browsing looks without committing
|
|
@@ -20,6 +21,11 @@ import { loadTheme } from '../themes/themeService';
|
|
|
20
21
|
* of the same theme produces, and a revert re-derives from the live store
|
|
21
22
|
* rather than replaying DOM values scraped before the preview.
|
|
22
23
|
*
|
|
24
|
+
* A theme's sketchstyle rides along the same way: `previewTheme` paints it
|
|
25
|
+
* through `previewSketchStyle`, which reaches the page without touching the
|
|
26
|
+
* live sketch buffer, and a revert re-derives from that buffer exactly as the
|
|
27
|
+
* CSS vars do.
|
|
28
|
+
*
|
|
23
29
|
* One preview is live at a time, whichever kind it is: painting reverts first,
|
|
24
30
|
* so every look is a diff against the user's real state.
|
|
25
31
|
*/
|
|
@@ -76,6 +82,14 @@ export function liveLook(): RenderedLook {
|
|
|
76
82
|
|
|
77
83
|
let livePreview: RenderedLook | null = null;
|
|
78
84
|
let defaultsPromise: Promise<Theme> | null = null;
|
|
85
|
+
// Whether the current preview session has a sketchstyle painted over the live
|
|
86
|
+
// buffer, so a colors-only preview (which never previews sketch) and revert
|
|
87
|
+
// know whether there is anything to hand back.
|
|
88
|
+
let sketchPreviewActive = false;
|
|
89
|
+
// Bumped whenever a preview session ends or is superseded. `previewTheme` awaits
|
|
90
|
+
// the defaults theme, and a Cancel landing inside that await was overwritten when
|
|
91
|
+
// it resolved, stranding a preview the picker believed it had already taken down.
|
|
92
|
+
let generation = 0;
|
|
79
93
|
|
|
80
94
|
function loadDefaults(): Promise<Theme> {
|
|
81
95
|
// A rejected promise must not be memoized, or one failed fetch (a dev-server
|
|
@@ -109,30 +123,55 @@ function applyPreview(look: RenderedLook): void {
|
|
|
109
123
|
livePreview = look;
|
|
110
124
|
}
|
|
111
125
|
|
|
112
|
-
/** Paint a whole look: the theme's colors and type
|
|
113
|
-
* it carries
|
|
126
|
+
/** Paint a whole look: the theme's colors and type, every component config
|
|
127
|
+
* it carries, and its sketchstyle — present or not, since a theme with none
|
|
128
|
+
* paints crisp regardless of what is live (invariant 3). */
|
|
114
129
|
export async function previewTheme(theme: Theme): Promise<void> {
|
|
115
|
-
|
|
130
|
+
const gen = generation;
|
|
131
|
+
const defaults = await loadDefaults();
|
|
132
|
+
if (gen !== generation) return;
|
|
133
|
+
applyPreview(themeLook(theme, defaults));
|
|
134
|
+
previewSketchStyle(theme.sketchStyle);
|
|
135
|
+
sketchPreviewActive = true;
|
|
116
136
|
}
|
|
117
137
|
|
|
118
|
-
/** Paint colors and type over the components as they stand.
|
|
138
|
+
/** Paint colors and type over the components as they stand. Not a whole
|
|
139
|
+
* look, so the sketchstyle stays live too — reverting a sketch preview a
|
|
140
|
+
* prior row left painted, if one is running. */
|
|
119
141
|
export function previewColorsAndType(colorsAndType: ColorsAndType): void {
|
|
142
|
+
generation++;
|
|
120
143
|
applyPreview(colorsAndTypeLook(colorsAndType));
|
|
144
|
+
if (sketchPreviewActive) {
|
|
145
|
+
revertSketchStylePreview();
|
|
146
|
+
sketchPreviewActive = false;
|
|
147
|
+
}
|
|
121
148
|
}
|
|
122
149
|
|
|
123
150
|
/** Restore the live editor state. No-op when no preview is running. */
|
|
124
151
|
export function revertPreview(): void {
|
|
152
|
+
generation++;
|
|
125
153
|
if (!livePreview) return;
|
|
126
154
|
paint(liveLook(), livePreview);
|
|
127
155
|
livePreview = null;
|
|
156
|
+
if (sketchPreviewActive) {
|
|
157
|
+
revertSketchStylePreview();
|
|
158
|
+
sketchPreviewActive = false;
|
|
159
|
+
}
|
|
128
160
|
}
|
|
129
161
|
|
|
130
162
|
/** Release the preview without repainting. The caller must immediately load
|
|
131
163
|
* the exact look being previewed into the store. This is the Save handoff: the
|
|
132
164
|
* selected theme is already on screen, so restoring the old live look before
|
|
133
|
-
* applying it would add work and create a visible flash across the request.
|
|
165
|
+
* applying it would add work and create a visible flash across the request.
|
|
166
|
+
* The sketchstyle stays painted for the same reason, though the handoff is not
|
|
167
|
+
* quite free: when the live state was crisp, `openThemeSketchStyle` writes the
|
|
168
|
+
* settings before the flag, so the sheet and the filter bank come down and go
|
|
169
|
+
* straight back up in between. That order is still the right one, since writing
|
|
170
|
+
* the flag first would paint the OLD dials for a frame. */
|
|
134
171
|
export function commitPreview(): void {
|
|
172
|
+
generation++;
|
|
135
173
|
livePreview = null;
|
|
174
|
+
sketchPreviewActive = false;
|
|
136
175
|
}
|
|
137
176
|
|
|
138
177
|
export function isPreviewing(): boolean {
|
|
@@ -141,6 +180,8 @@ export function isPreviewing(): boolean {
|
|
|
141
180
|
|
|
142
181
|
/** Test-only: drop the live preview and the cached defaults theme. */
|
|
143
182
|
export function __resetPreviewForTests(): void {
|
|
183
|
+
generation++;
|
|
144
184
|
livePreview = null;
|
|
145
185
|
defaultsPromise = null;
|
|
186
|
+
sketchPreviewActive = false;
|
|
146
187
|
}
|
|
@@ -35,8 +35,12 @@ export function bumpComponentActiveRevision(): void {
|
|
|
35
35
|
* component editor's Adopt has to clear it for the panel.
|
|
36
36
|
*
|
|
37
37
|
* Set by the client writes that move the live look (`writeWorkingColorsAndType`,
|
|
38
|
-
* `writeWorkingComponentConfig`, `saveActiveTheme`, `saveAsTheme
|
|
39
|
-
*
|
|
38
|
+
* `writeWorkingComponentConfig`, `saveActiveTheme`, `saveAsTheme`, and every
|
|
39
|
+
* sketch gesture that changes what the effect paints: `setSketchEnabled`,
|
|
40
|
+
* `updateSketchSettings`, `selectSketchStyle`, `selectSavedSketchStyle`);
|
|
41
|
+
* cleared by `adoptLook` and by `hydrateAppliedTheme`, on this document and on
|
|
42
|
+
* every peer an Apply broadcasts to. Module-level, so it survives the
|
|
43
|
+
* remounts a view switch causes.
|
|
40
44
|
*/
|
|
41
45
|
export const liveMovedSinceBake = writable(false);
|
|
42
46
|
|
|
@@ -8,14 +8,18 @@
|
|
|
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.
|
|
17
21
|
*/
|
|
18
|
-
import type {
|
|
22
|
+
import type { SketchStyle } from './sketchStyles';
|
|
19
23
|
|
|
20
24
|
/** What the tile spans in page px. Blobs are fitted a whole number to the
|
|
21
25
|
tile, which is what lets the lattice wrap; the dial's px reading is the
|
|
@@ -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
|
|
|
@@ -107,7 +111,7 @@ function makePerlin(seed: number): (x: number, y: number, period: number) => num
|
|
|
107
111
|
* wherever the octave crossed zero, which is the marbled look; plain sum is
|
|
108
112
|
* cloud.
|
|
109
113
|
*/
|
|
110
|
-
function rawField(s:
|
|
114
|
+
function rawField(s: SketchStyle, seed: number, raster: number, cells: number): Float32Array {
|
|
111
115
|
const noise = makePerlin(seed);
|
|
112
116
|
const veined = s.maskGrain === 'turbulence';
|
|
113
117
|
const octaves = Math.max(1, Math.round(s.maskOctaves));
|
|
@@ -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;
|
|
@@ -222,7 +256,7 @@ function boxPass(f: Float32Array, n: number, radius: number): Float32Array {
|
|
|
222
256
|
all four; the levels, the posterising and the blur are one pass each. */
|
|
223
257
|
let rawCache: { key: string; field: Float32Array } | null = null;
|
|
224
258
|
|
|
225
|
-
function cachedRaw(s:
|
|
259
|
+
function cachedRaw(s: SketchStyle, seed: number): Float32Array {
|
|
226
260
|
const key = [s.maskBlob, s.maskOctaves, s.maskGrain, seed].join('|');
|
|
227
261
|
if (rawCache?.key !== key) {
|
|
228
262
|
const cells = Math.max(1, Math.round(MASK_TILE / s.maskBlob));
|
|
@@ -234,11 +268,13 @@ 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
|
-
s:
|
|
277
|
+
s: SketchStyle, seed = 9, through?: MaskStage,
|
|
242
278
|
): { field: Float32Array; raster: number } {
|
|
243
279
|
const raw = cachedRaw(s, seed);
|
|
244
280
|
if (through === 'noise') return { field: raw, raster: RASTER };
|
|
@@ -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
|
}
|
|
@@ -370,7 +406,7 @@ const CACHE_MAX = 6;
|
|
|
370
406
|
const cache = new Map<string, string>();
|
|
371
407
|
|
|
372
408
|
/** The field as a `url(...)` for `mask-image`. */
|
|
373
|
-
export function buildMaskUri(s:
|
|
409
|
+
export function buildMaskUri(s: SketchStyle, seed = 9, through?: MaskStage): string {
|
|
374
410
|
const key = [...KEYS.map((k) => s[k]), seed, through ?? 'all'].join('|');
|
|
375
411
|
const hit = cache.get(key);
|
|
376
412
|
if (hit) return hit;
|