@motion-proto/live-tokens 0.74.0 → 0.76.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-check-compliance/SKILL.md +33 -38
- package/.claude/skills/live-tokens-create-component/SKILL copy.md +196 -0
- package/.claude/skills/live-tokens-create-component/SKILL.md +198 -146
- package/.claude/skills/live-tokens-create-component/references/contract-tests.md +95 -52
- package/.claude/skills/live-tokens-create-component/references/intrinsics.md +2 -2
- package/.claude/skills/live-tokens-create-component/references/linked-siblings.md +2 -2
- package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +12 -12
- package/.claude/skills/live-tokens-create-component/references/token-naming.md +2 -1
- package/.claude/skills/live-tokens-create-page/SKILL.md +187 -0
- package/.claude/skills/live-tokens-create-page/references/interaction-sources.md +66 -0
- package/.claude/skills/live-tokens-create-page/references/layout-sources.md +87 -0
- package/.claude/skills/live-tokens-create-theme/SKILL.md +52 -48
- package/.claude/skills/live-tokens-create-theme/references/design-directions.md +1 -1
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +69 -60
- package/.claude/skills/live-tokens-pick-component/SKILL.md +64 -79
- package/.claude/skills/live-tokens-set-colors/SKILL.md +47 -38
- package/.claude/skills/live-tokens-set-geometry/SKILL.md +58 -35
- package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +5 -3
- package/.claude/skills/live-tokens-set-type/SKILL.md +30 -30
- package/CHANGELOG.md +138 -0
- package/README.md +17 -7
- package/bin/check-component.mjs +91 -19
- package/bin/check-page.mjs +102 -21
- package/bin/cli.mjs +87 -113
- package/bin/contractRunner.mjs +945 -0
- package/bin/create.mjs +1 -1
- package/bin/lib/catalogue.mjs +37 -30
- package/bin/lib/findings.mjs +46 -15
- package/bin/lib/report.mjs +3 -3
- package/bin/lib/tokenVocabulary.mjs +4 -4
- package/bin/migrate-routes.mjs +5 -5
- package/bin/migrate.mjs +4 -4
- package/bin/save-theme.mjs +8 -9
- package/bin/set-colors.mjs +9 -11
- package/bin/set-geometry.mjs +7 -7
- package/bin/set-type.mjs +5 -7
- package/bin/setup-claude.mjs +110 -0
- package/dist-plugin/{chunk-W6Y4BWFB.js → chunk-6WGFOJXO.js} +22 -0
- package/dist-plugin/{chunk-7VRTBGJT.js → chunk-PDNL4NC5.js} +9 -2
- package/dist-plugin/{chunk-V3YF6CGT.js → chunk-SWXRVZKT.js} +43 -1
- package/dist-plugin/{dataPaths-BhWzd5cL.d.cts → dataPaths-BpIK_Obx.d.cts} +1 -0
- package/dist-plugin/{dataPaths-BhWzd5cL.d.ts → dataPaths-BpIK_Obx.d.ts} +1 -0
- package/dist-plugin/index.cjs +310 -147
- package/dist-plugin/index.d.cts +1 -1
- package/dist-plugin/index.d.ts +1 -1
- package/dist-plugin/index.js +218 -122
- package/dist-plugin/migrateData/index.cjs +66 -2
- package/dist-plugin/migrateData/index.d.cts +1 -1
- package/dist-plugin/migrateData/index.d.ts +1 -1
- package/dist-plugin/migrateData/index.js +3 -3
- package/dist-plugin/setColors/index.cjs +61 -4
- package/dist-plugin/setColors/index.d.cts +1 -1
- package/dist-plugin/setColors/index.d.ts +1 -1
- package/dist-plugin/setColors/index.js +5 -5
- package/dist-plugin/setGeometry/index.cjs +107 -44
- package/dist-plugin/setGeometry/index.d.cts +5 -5
- package/dist-plugin/setGeometry/index.d.ts +5 -5
- package/dist-plugin/setGeometry/index.js +51 -45
- package/dist-plugin/setType/index.cjs +15 -0
- package/dist-plugin/setType/index.d.cts +1 -1
- package/dist-plugin/setType/index.d.ts +1 -1
- package/dist-plugin/setType/index.js +1 -1
- package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
- package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
- package/dist-plugin/tokensCssMigrations/index.js +1 -1
- package/package.json +35 -8
- package/src/app/site.css +19 -9
- package/src/editor/bootstrap.ts +2 -0
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +4 -4
- package/src/editor/component-editor/DialogEditor.svelte +4 -4
- package/src/editor/component-editor/MenuSelectEditor.svelte +4 -1
- package/src/editor/component-editor/SegmentedControlEditor.svelte +6 -1
- package/src/editor/component-editor/TabBarEditor.svelte +1 -1
- package/src/editor/component-editor/TableEditor.svelte +2 -2
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +6 -3
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +41 -20
- package/src/editor/core/components/adjustAliases.ts +59 -45
- package/src/editor/core/components/aliasKinds.ts +9 -5
- package/src/editor/core/preview/themePreview.ts +9 -2
- package/src/editor/core/sketch/sketchLayer.ts +22 -0
- package/src/editor/core/store/editorStore.ts +10 -1
- package/src/editor/core/themes/buildColors.ts +3 -3
- package/src/editor/core/themes/liveStateStream.ts +26 -0
- package/src/editor/core/themes/migrations/2026-09-07-stroke-role-renames.ts +52 -0
- package/src/editor/core/themes/migrations/index.ts +2 -0
- package/src/editor/core/themes/themeDocumentSync.ts +22 -11
- package/src/editor/core/themes/themeService.ts +9 -2
- package/src/editor/pages/ComponentEditorPage.svelte +17 -1
- package/src/editor/pages/liveTokensEditorHandle.ts +23 -0
- package/src/editor/skill-atlas/SkillAtlas.svelte +75 -573
- package/src/editor/skill-atlas/TreeCanvas.svelte +263 -0
- package/src/editor/skill-atlas/TreeNodeCard.svelte +136 -53
- package/src/editor/skill-atlas/edges.ts +31 -0
- package/src/editor/skill-atlas/skillSources.generated.ts +20 -19
- package/src/editor/skill-atlas/skillTrees.ts +19 -3840
- package/src/editor/skill-atlas/trees/check-compliance.ts +183 -0
- package/src/editor/skill-atlas/trees/create-component.ts +275 -0
- package/src/editor/skill-atlas/trees/create-page.ts +320 -0
- package/src/editor/skill-atlas/trees/create-theme.ts +255 -0
- package/src/editor/skill-atlas/trees/fix-findings.ts +469 -0
- package/src/editor/skill-atlas/trees/pick-component.ts +300 -0
- package/src/editor/skill-atlas/trees/set-colors.ts +148 -0
- package/src/editor/skill-atlas/trees/set-geometry.ts +136 -0
- package/src/editor/skill-atlas/trees/set-type.ts +142 -0
- package/src/editor/skill-atlas/types.ts +3 -4
- package/src/editor/skill-atlas/wireLayout.ts +287 -0
- package/src/live-tokens/data/themes/autumn.json +15 -15
- package/src/live-tokens/data/themes/halloween.json +15 -15
- package/src/live-tokens/data/themes/midnight-study.json +15 -15
- package/src/live-tokens/data/themes/ocean.json +15 -15
- package/src/live-tokens/data/themes/royal-velvet.json +15 -15
- package/src/live-tokens/data/themes/sketchy.json +15 -15
- package/src/live-tokens/data/themes/spring-meadow.json +15 -15
- package/src/live-tokens/data/themes/sunset.json +15 -15
- package/src/system/components/Badge.svelte +7 -0
- package/src/system/components/Button.svelte +7 -0
- package/src/system/components/Callout.svelte +10 -6
- package/src/system/components/Card.svelte +23 -4
- package/src/system/components/CodeSnippet.svelte +4 -3
- package/src/system/components/CollapsibleSection.svelte +23 -8
- package/src/system/components/CornerBadge.svelte +6 -0
- package/src/system/components/Dialog.svelte +13 -6
- package/src/system/components/IconButton.svelte +9 -0
- package/src/system/components/Image.svelte +8 -0
- package/src/system/components/ImageLightbox.svelte +6 -0
- package/src/system/components/InlineEditActions.svelte +7 -0
- package/src/system/components/Input.svelte +7 -0
- package/src/system/components/MenuSelect.svelte +7 -0
- package/src/system/components/Notification.svelte +7 -0
- package/src/system/components/Panel.svelte +6 -0
- package/src/system/components/ProgressBar.svelte +5 -0
- package/src/system/components/RadioButton.svelte +11 -5
- package/src/system/components/SectionDivider.svelte +8 -0
- package/src/system/components/SegmentedControl.svelte +6 -0
- package/src/system/components/SideNavigation.svelte +6 -0
- package/src/system/components/Slider.svelte +7 -4
- package/src/system/components/TabBar.svelte +15 -9
- package/src/system/components/Table.svelte +8 -3
- package/src/system/components/Toggle.svelte +4 -4
- package/src/system/components/Tooltip.svelte +6 -0
- package/src/testing-js/chunk-AO7EZHYV.js +776 -0
- package/src/testing-js/chunk-AO7EZHYV.js.map +1 -0
- package/src/testing-js/chunk-FAFOAWYL.js +39 -0
- package/src/testing-js/chunk-FAFOAWYL.js.map +1 -0
- package/src/testing-js/chunk-L73N4NSO.js +23 -0
- package/src/testing-js/chunk-L73N4NSO.js.map +1 -0
- package/src/testing-js/chunk-LXR3MN6N.js +3063 -0
- package/src/testing-js/chunk-LXR3MN6N.js.map +1 -0
- package/src/testing-js/chunk-ZMSX6CXR.js +53 -0
- package/src/testing-js/chunk-ZMSX6CXR.js.map +1 -0
- package/src/testing-js/component-alias.contract.js +81 -0
- package/src/testing-js/component-alias.contract.js.map +1 -0
- package/src/testing-js/component-editor.contract.js +62 -0
- package/src/testing-js/component-editor.contract.js.map +1 -0
- package/src/testing-js/component-render.contract.js +568 -0
- package/src/testing-js/component-render.contract.js.map +1 -0
- package/src/testing-js/index.d.ts +293 -0
- package/src/testing-js/index.js +222 -0
- package/src/testing-js/index.js.map +1 -0
- package/src/testing-js/registry.contract.js +39 -0
- package/src/testing-js/registry.contract.js.map +1 -0
- package/src/testing-js/vitest-BE6uGF31.d.ts +73 -0
- package/src/testing-js/vitest.d.ts +3 -0
- package/src/testing-js/vitest.js +13 -0
- package/src/testing-js/vitest.js.map +1 -0
- package/template/README.md +13 -0
- package/template/_gitignore +6 -0
- package/template/package.json +3 -1
- package/template/src/pages/Home.svelte +4 -18
- package/.claude/skills/live-tokens-build-page/SKILL.md +0 -103
- package/.claude/skills/live-tokens-build-page/references/layout-sources.md +0 -48
|
@@ -407,24 +407,26 @@
|
|
|
407
407
|
</div>
|
|
408
408
|
{/if}
|
|
409
409
|
</div>
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
{@render children?.({ activeState: activeTab })}
|
|
413
|
-
</div>
|
|
414
|
-
{:else}
|
|
415
|
-
<ShadowBackdrop mode={bgMode} colorVariable={bgVar} padding={backdropPadding}>
|
|
416
|
-
{#snippet controls()}
|
|
417
|
-
<div class="canvas-toolbar">
|
|
418
|
-
<span class="canvas-toolbar-eyebrow">Background</span>
|
|
419
|
-
<ShadowBackdropControls bind:mode={bgMode} colorVariable={bgVar} modes={backdropModes ?? ['default', 'image', 'color']} />
|
|
420
|
-
{@render canvasToolbarExtras?.()}
|
|
421
|
-
</div>
|
|
422
|
-
{/snippet}
|
|
410
|
+
<div class="preview-stage">
|
|
411
|
+
{#if unboxedPreview}
|
|
423
412
|
<div class="sketch-scope" bind:this={sketchStage}>
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
413
|
+
{@render children?.({ activeState: activeTab })}
|
|
414
|
+
</div>
|
|
415
|
+
{:else}
|
|
416
|
+
<ShadowBackdrop mode={bgMode} colorVariable={bgVar} padding={backdropPadding}>
|
|
417
|
+
{#snippet controls()}
|
|
418
|
+
<div class="canvas-toolbar">
|
|
419
|
+
<span class="canvas-toolbar-eyebrow">Background</span>
|
|
420
|
+
<ShadowBackdropControls bind:mode={bgMode} colorVariable={bgVar} modes={backdropModes ?? ['default', 'image', 'color']} />
|
|
421
|
+
{@render canvasToolbarExtras?.()}
|
|
422
|
+
</div>
|
|
423
|
+
{/snippet}
|
|
424
|
+
<div class="sketch-scope" bind:this={sketchStage}>
|
|
425
|
+
{@render children?.({ activeState: activeTab })}
|
|
426
|
+
</div>
|
|
427
|
+
</ShadowBackdrop>
|
|
428
|
+
{/if}
|
|
429
|
+
</div>
|
|
428
430
|
|
|
429
431
|
{#if tabsStripVisible}
|
|
430
432
|
<div class="tabs-states-block">
|
|
@@ -556,9 +558,13 @@
|
|
|
556
558
|
/* Pin the preview + state-tab strip to the top of the page scroll so
|
|
557
559
|
property edits stay visually connected to the preview without scrolling.
|
|
558
560
|
The card background extends through the sticky band so the property grid
|
|
559
|
-
scrolls cleanly behind it.
|
|
560
|
-
|
|
561
|
-
|
|
561
|
+
scrolls cleanly behind it.
|
|
562
|
+
|
|
563
|
+
The cap is load-bearing: a sticky band taller than the viewport covers the
|
|
564
|
+
property controls at every scroll position, which the tallest previews
|
|
565
|
+
(SideNavigation, Image, Card) reach on a 13-inch laptop. Capping the band
|
|
566
|
+
and scrolling the stage inside it keeps the header and the state strip
|
|
567
|
+
pinned and always leaves the controls a viewport to live in. */
|
|
562
568
|
.tabs-preview {
|
|
563
569
|
position: sticky;
|
|
564
570
|
top: 0;
|
|
@@ -566,6 +572,7 @@
|
|
|
566
572
|
display: flex;
|
|
567
573
|
flex-direction: column;
|
|
568
574
|
gap: var(--ui-space-20);
|
|
575
|
+
max-height: 50vh;
|
|
569
576
|
background: var(--ui-surface-low);
|
|
570
577
|
/* Bleed the background up through the card's top padding so content
|
|
571
578
|
scrolling behind doesn't peek between the viewport edge and the
|
|
@@ -577,6 +584,20 @@
|
|
|
577
584
|
border-radius: var(--ui-radius-md) var(--ui-radius-md) 0 0;
|
|
578
585
|
}
|
|
579
586
|
|
|
587
|
+
.preview-stage {
|
|
588
|
+
display: flex;
|
|
589
|
+
flex-direction: column;
|
|
590
|
+
gap: var(--ui-space-20);
|
|
591
|
+
flex: 1 1 auto;
|
|
592
|
+
min-height: 0;
|
|
593
|
+
overflow: auto;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.preview-header,
|
|
597
|
+
.tabs-states-block {
|
|
598
|
+
flex: none;
|
|
599
|
+
}
|
|
600
|
+
|
|
580
601
|
/* Soft fade at the bottom of the sticky band so property rows scrolling
|
|
581
602
|
up don't sharply cut against the pinned preview. Greyscale (no accent).
|
|
582
603
|
Sits below the preview body, above scrolling content. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { matchesKind, stripSide } from './aliasKinds';
|
|
2
2
|
import type { AliasDiskValue, ComponentConfig } from '../themes/themeTypes';
|
|
3
3
|
|
|
4
|
-
export type AdjustKind = 'radius' | 'padding' | 'gap' | 'border-width';
|
|
4
|
+
export type AdjustKind = 'radius' | 'padding' | 'gap' | 'border-width' | 'divider-width' | 'accent-width';
|
|
5
5
|
|
|
6
6
|
export interface AdjustOp {
|
|
7
7
|
/** Component id; omitted applies the op to every config. */
|
|
@@ -9,11 +9,11 @@ export interface AdjustOp {
|
|
|
9
9
|
kind: AdjustKind;
|
|
10
10
|
set?: string;
|
|
11
11
|
shift?: number;
|
|
12
|
-
/** Radius shifts only: admits `--radius-full` as the
|
|
12
|
+
/** Radius shifts only: admits `--radius-full` as the top step of the scale. */
|
|
13
13
|
full?: boolean;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export type SkipReason = 'raw-value' | 'off-
|
|
16
|
+
export type SkipReason = 'raw-value' | 'off-scale' | 'clamped' | 'pill-preserved' | 'none-preserved';
|
|
17
17
|
|
|
18
18
|
export interface AliasChange {
|
|
19
19
|
variable: string;
|
|
@@ -44,7 +44,7 @@ export interface AdjustResult {
|
|
|
44
44
|
|
|
45
45
|
const RADIUS_FULL = '--radius-full';
|
|
46
46
|
|
|
47
|
-
const
|
|
47
|
+
const RADIUS_STEPS = [
|
|
48
48
|
'--radius-none', '--radius-sm', '--radius-md', '--radius-lg', '--radius-xl',
|
|
49
49
|
'--radius-2xl', '--radius-3xl', '--radius-4xl',
|
|
50
50
|
];
|
|
@@ -57,43 +57,56 @@ const SPACE_SETTABLE = [
|
|
|
57
57
|
'--space-12', '--space-16', '--space-20', '--space-24', '--space-32', '--space-48',
|
|
58
58
|
];
|
|
59
59
|
|
|
60
|
-
const
|
|
60
|
+
const SPACE_STEPS = SPACE_SETTABLE;
|
|
61
61
|
|
|
62
62
|
/** Content insets stop at `--space-4`. Below it the text sits against its own
|
|
63
63
|
edge, which is a deliberate choice rather than somewhere a relative
|
|
64
64
|
"tighter" should deposit you. Outer space is exempt: a 2px gap between an
|
|
65
65
|
icon and its label, or a 2px margin under a bar, is ordinary design. Both
|
|
66
|
-
|
|
67
|
-
const
|
|
66
|
+
steps stay pickable by hand and settable by name. */
|
|
67
|
+
const INSET_STEPS = SPACE_SETTABLE.slice(SPACE_SETTABLE.indexOf('--space-4'));
|
|
68
68
|
|
|
69
|
-
/** Padding that wraps a line of type stops a
|
|
69
|
+
/** Padding that wraps a line of type stops a step higher. Those components
|
|
70
70
|
double it horizontally (`themed-padding($h: 2)`), so `--space-4` is 4px
|
|
71
71
|
over an 18px line and 8px inside a pill's own corner: squeezed rather than
|
|
72
72
|
compact. No shipped default puts text below `--space-6`. */
|
|
73
|
-
const
|
|
73
|
+
const TEXT_INSET_STEPS = SPACE_SETTABLE.slice(SPACE_SETTABLE.indexOf('--space-6'));
|
|
74
74
|
|
|
75
75
|
const SPACE_FAMILY = [...SPACE_SETTABLE, '--space-40', '--space-64', '--space-96', '--space-128'];
|
|
76
76
|
|
|
77
|
-
const
|
|
78
|
-
|
|
77
|
+
const BORDER_WIDTH_NONE = '--border-width-0';
|
|
78
|
+
|
|
79
|
+
const BORDER_WIDTH_SETTABLE = [
|
|
80
|
+
BORDER_WIDTH_NONE, '--border-width-1', '--border-width-2', '--border-width-3',
|
|
79
81
|
'--border-width-4', '--border-width-5', '--border-width-6', '--border-width-8',
|
|
80
82
|
'--border-width-10', '--border-width-12', '--border-width-16', '--border-width-20',
|
|
81
83
|
'--border-width-24',
|
|
82
84
|
];
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
/** A line that is not drawn is a decision, so a shift never draws one and
|
|
87
|
+
never erases one. Zero is reached by `set` alone, like `--radius-full`. */
|
|
88
|
+
const STROKE_STEPS = BORDER_WIDTH_SETTABLE.slice(1);
|
|
89
|
+
|
|
90
|
+
/** Three stroke roles ride the one `--border-width-*` scale. A border
|
|
91
|
+
encloses, a divider separates, an accent emphasises, and an intent
|
|
92
|
+
such as "hairline rules" names one role without the others. */
|
|
93
|
+
const STROKE_SCALE = {
|
|
94
|
+
steps: STROKE_STEPS,
|
|
95
|
+
settable: BORDER_WIDTH_SETTABLE,
|
|
96
|
+
family: BORDER_WIDTH_SETTABLE,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const SCALES: Record<AdjustKind, { steps: string[]; settable: string[]; family: string[] }> = {
|
|
85
100
|
radius: {
|
|
86
|
-
|
|
87
|
-
settable: [...
|
|
88
|
-
family: [...
|
|
89
|
-
},
|
|
90
|
-
padding: { rungs: SPACE_RUNGS, settable: SPACE_SETTABLE, family: SPACE_FAMILY },
|
|
91
|
-
gap: { rungs: SPACE_RUNGS, settable: SPACE_SETTABLE, family: SPACE_FAMILY },
|
|
92
|
-
'border-width': {
|
|
93
|
-
rungs: BORDER_WIDTH_RUNGS,
|
|
94
|
-
settable: BORDER_WIDTH_RUNGS,
|
|
95
|
-
family: BORDER_WIDTH_RUNGS,
|
|
101
|
+
steps: RADIUS_STEPS,
|
|
102
|
+
settable: [...RADIUS_STEPS, RADIUS_FULL],
|
|
103
|
+
family: [...RADIUS_STEPS, RADIUS_FULL],
|
|
96
104
|
},
|
|
105
|
+
padding: { steps: SPACE_STEPS, settable: SPACE_SETTABLE, family: SPACE_FAMILY },
|
|
106
|
+
gap: { steps: SPACE_STEPS, settable: SPACE_SETTABLE, family: SPACE_FAMILY },
|
|
107
|
+
'border-width': STROKE_SCALE,
|
|
108
|
+
'divider-width': STROKE_SCALE,
|
|
109
|
+
'accent-width': STROKE_SCALE,
|
|
97
110
|
};
|
|
98
111
|
|
|
99
112
|
const TOKEN_NAME = /^--[a-z0-9-]+$/;
|
|
@@ -104,28 +117,28 @@ type Outcome = { from: string; to: string } | { skip: SkipReason };
|
|
|
104
117
|
rather than the kind. Which floor depends on what the padding surrounds,
|
|
105
118
|
and the config says so itself: a variant that also declares a
|
|
106
119
|
`-text-font-size` is holding type. */
|
|
107
|
-
function
|
|
108
|
-
if (op.kind === 'radius') return op.full ? [...
|
|
109
|
-
if (op.kind !== 'padding') return
|
|
120
|
+
function stepsFor(op: AdjustOp, variable: string, aliases: ComponentConfig['aliases']): string[] {
|
|
121
|
+
if (op.kind === 'radius') return op.full ? [...RADIUS_STEPS, RADIUS_FULL] : RADIUS_STEPS;
|
|
122
|
+
if (op.kind !== 'padding') return SCALES[op.kind].steps;
|
|
110
123
|
const base = stripSide(variable);
|
|
111
|
-
if (!base.endsWith('-padding')) return
|
|
124
|
+
if (!base.endsWith('-padding')) return SCALES.padding.steps;
|
|
112
125
|
const variant = base.slice(0, -'-padding'.length);
|
|
113
|
-
return `${variant}-text-font-size` in aliases ?
|
|
126
|
+
return `${variant}-text-font-size` in aliases ? TEXT_INSET_STEPS : INSET_STEPS;
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
function spaceStep(token: string): number {
|
|
117
130
|
return Number(token.slice('--space-'.length));
|
|
118
131
|
}
|
|
119
132
|
|
|
120
|
-
/** An off-
|
|
133
|
+
/** An off-scale value has no step to count from, so landing on the first step
|
|
121
134
|
the shift points at is itself that shift's opening step. Snapping to the
|
|
122
|
-
nearest
|
|
123
|
-
Returns -1 when the
|
|
124
|
-
function
|
|
135
|
+
nearest step instead would spend two visible steps on a one-step request.
|
|
136
|
+
Returns -1 when the scale holds nothing in that direction. */
|
|
137
|
+
function snapStep(token: string, steps: string[], direction: number): number {
|
|
125
138
|
const step = spaceStep(token);
|
|
126
|
-
if (direction > 0) return
|
|
127
|
-
for (let index =
|
|
128
|
-
if (spaceStep(
|
|
139
|
+
if (direction > 0) return steps.findIndex((candidate) => spaceStep(candidate) > step);
|
|
140
|
+
for (let index = steps.length - 1; index >= 0; index--) {
|
|
141
|
+
if (spaceStep(steps[index]) < step) return index;
|
|
129
142
|
}
|
|
130
143
|
return -1;
|
|
131
144
|
}
|
|
@@ -137,28 +150,29 @@ function resolve(
|
|
|
137
150
|
aliases: ComponentConfig['aliases'],
|
|
138
151
|
): Outcome {
|
|
139
152
|
if (typeof value !== 'string' || !TOKEN_NAME.test(value)) return { skip: 'raw-value' };
|
|
140
|
-
if (!
|
|
153
|
+
if (!SCALES[op.kind].family.includes(value)) return { skip: 'off-scale' };
|
|
141
154
|
if (op.set !== undefined) return { from: value, to: op.set };
|
|
142
155
|
|
|
143
|
-
const
|
|
144
|
-
let index =
|
|
156
|
+
const steps = stepsFor(op, variable, aliases);
|
|
157
|
+
let index = steps.indexOf(value);
|
|
145
158
|
let shift = op.shift!;
|
|
146
159
|
if (index < 0) {
|
|
147
160
|
if (value === RADIUS_FULL) return { skip: 'pill-preserved' };
|
|
148
|
-
|
|
161
|
+
if (value === BORDER_WIDTH_NONE) return { skip: 'none-preserved' };
|
|
162
|
+
index = snapStep(value, steps, shift);
|
|
149
163
|
if (index < 0) return { skip: 'clamped' };
|
|
150
164
|
shift -= Math.sign(shift);
|
|
151
165
|
}
|
|
152
166
|
|
|
153
167
|
const wanted = index + shift;
|
|
154
|
-
const landed = Math.min(Math.max(wanted, 0),
|
|
155
|
-
if (landed !== wanted &&
|
|
156
|
-
return { from: value, to:
|
|
168
|
+
const landed = Math.min(Math.max(wanted, 0), steps.length - 1);
|
|
169
|
+
if (landed !== wanted && steps[landed] === value) return { skip: 'clamped' };
|
|
170
|
+
return { from: value, to: steps[landed] };
|
|
157
171
|
}
|
|
158
172
|
|
|
159
173
|
function validateOp(op: AdjustOp, configs: Record<string, ComponentConfig>): void {
|
|
160
|
-
const
|
|
161
|
-
if (!
|
|
174
|
+
const scale = SCALES[op.kind];
|
|
175
|
+
if (!scale) throw new Error(`Unknown kind "${op.kind}" (expected ${Object.keys(SCALES).join(', ')})`);
|
|
162
176
|
if (op.target !== undefined && !(op.target in configs)) {
|
|
163
177
|
throw new Error(`Unknown target component "${op.target}"`);
|
|
164
178
|
}
|
|
@@ -166,9 +180,9 @@ function validateOp(op: AdjustOp, configs: Record<string, ComponentConfig>): voi
|
|
|
166
180
|
const hasSet = op.set !== undefined;
|
|
167
181
|
const hasShift = op.shift !== undefined;
|
|
168
182
|
if (hasSet === hasShift) throw new Error(`A ${op.kind} op needs exactly one of "set" or "shift"`);
|
|
169
|
-
const settable =
|
|
183
|
+
const settable = scale.settable;
|
|
170
184
|
if (hasSet && !settable.includes(op.set!)) {
|
|
171
|
-
throw new Error(`"${op.set}" is not on the ${op.kind}
|
|
185
|
+
throw new Error(`"${op.set}" is not on the ${op.kind} scale`);
|
|
172
186
|
}
|
|
173
187
|
if (hasShift && !Number.isInteger(op.shift)) {
|
|
174
188
|
throw new Error(`"shift" must be a whole number of steps, got ${op.shift}`);
|
|
@@ -2,8 +2,9 @@ export type TokenKind =
|
|
|
2
2
|
| 'surface'
|
|
3
3
|
| 'border'
|
|
4
4
|
| 'border-width'
|
|
5
|
-
| 'radius'
|
|
6
5
|
| 'divider-width'
|
|
6
|
+
| 'accent-width'
|
|
7
|
+
| 'radius'
|
|
7
8
|
| 'divider-height'
|
|
8
9
|
| 'divider-inset'
|
|
9
10
|
| 'dot-size'
|
|
@@ -28,7 +29,7 @@ export type TokenKind =
|
|
|
28
29
|
`suffix:` arrays out of this file, which is why they are plain literals.
|
|
29
30
|
|
|
30
31
|
Order matters: `-text` must run before `-border`/`-surface`, and
|
|
31
|
-
`-accent-width` before `-accent`, because the first match wins. A variable
|
|
32
|
+
`-accent-width` before `-accent` and `-width`, because the first match wins. A variable
|
|
32
33
|
matching nothing falls through to `text-color` (a palette picker), but that
|
|
33
34
|
fall-through is a smell — `check-component` rejects an unrecognised suffix,
|
|
34
35
|
so add the name here rather than letting it drift. */
|
|
@@ -47,7 +48,10 @@ export const KIND_RULES: ReadonlyArray<{
|
|
|
47
48
|
'-description', '-hint', '-error', '-placeholder', '-value'],
|
|
48
49
|
prefix: ['--text-'] },
|
|
49
50
|
{ kind: 'radius', suffix: ['-radius'], prefix: ['--radius-'] },
|
|
50
|
-
|
|
51
|
+
// Three stroke roles share the `--border-width-*` scale: a border encloses,
|
|
52
|
+
// a divider separates, an accent emphasises. `set-geometry` moves each role
|
|
53
|
+
// on its own, so a suffix has to say which line it names.
|
|
54
|
+
{ kind: 'divider-width', suffix: ['-divider-width', '-divider-thickness', '-hairline-thickness', '-thickness'] },
|
|
51
55
|
{ kind: 'divider-height', suffix: ['-divider-height', '-track-height'] },
|
|
52
56
|
{ kind: 'divider-inset', suffix: ['-divider-inset', '-inset'] },
|
|
53
57
|
{ kind: 'dot-size', suffix: ['-dot-size'] },
|
|
@@ -58,8 +62,8 @@ export const KIND_RULES: ReadonlyArray<{
|
|
|
58
62
|
{ kind: 'gap', suffix: ['-gap'] },
|
|
59
63
|
{ kind: 'duration', suffix: ['-duration'], prefix: ['--duration-'] },
|
|
60
64
|
{ kind: 'easing', suffix: ['-easing'], prefix: ['--ease-'] },
|
|
61
|
-
{ kind: '
|
|
62
|
-
|
|
65
|
+
{ kind: 'accent-width', suffix: ['-accent-width', '-indicator-width'] },
|
|
66
|
+
{ kind: 'border-width', suffix: ['-border-width'], prefix: ['--border-width-'] },
|
|
63
67
|
{ kind: 'border', suffix: ['-border'], prefix: ['--border-'] },
|
|
64
68
|
// A dimension with no more specific name behind it — a panel's width, an
|
|
65
69
|
// avatar's size. Last of the geometry rules, so every `-border-width`,
|
|
@@ -48,12 +48,19 @@ export function renderTheme(theme: Theme, defaults: Theme): RenderedTheme {
|
|
|
48
48
|
migrateColorsAndTypeFonts(colorsAndType);
|
|
49
49
|
const state = colorsAndTypeToState(colorsAndType);
|
|
50
50
|
state.components = {};
|
|
51
|
+
// The stamp is the theme's, never the entry's: `normalizeTheme` strips a
|
|
52
|
+
// per-entry `schemaVersion` on every read, so reading one back yields
|
|
53
|
+
// undefined and replays every migration over already-current data.
|
|
51
54
|
for (const [comp, config] of Object.entries(defaults.componentConfigs)) {
|
|
52
|
-
state.components[comp] = toComponentSlice(
|
|
55
|
+
state.components[comp] = toComponentSlice(
|
|
56
|
+
comp, config.aliases, config.config, defaults.componentSchemaVersion,
|
|
57
|
+
);
|
|
53
58
|
}
|
|
54
59
|
for (const [comp, config] of Object.entries(theme.componentConfigs)) {
|
|
55
60
|
if (!(comp in state.components)) continue;
|
|
56
|
-
state.components[comp] = toComponentSlice(
|
|
61
|
+
state.components[comp] = toComponentSlice(
|
|
62
|
+
comp, config.aliases, config.config, theme.componentSchemaVersion,
|
|
63
|
+
);
|
|
57
64
|
}
|
|
58
65
|
const vars = deriveCssVars(state);
|
|
59
66
|
return { vars, fontSources: colorsAndType.fontSources ?? [] };
|
|
@@ -119,6 +119,15 @@ const PART_SPECS: readonly PartSpec[] = [
|
|
|
119
119
|
sel: '.icon-button.outline', fill: 'transparent', stroke: 'var(--iconbutton-outline-border)',
|
|
120
120
|
radius: 'var(--iconbutton-outline-radius, 0px)',
|
|
121
121
|
},
|
|
122
|
+
// The dot names its colours `-fill` and `-border-color`, so it supplies
|
|
123
|
+
// both explicitly rather than through a `-surface`/`-border` stem.
|
|
124
|
+
{
|
|
125
|
+
sel: '.radio-dot',
|
|
126
|
+
fill: 'var(--radiobutton-default-dot-fill)',
|
|
127
|
+
stroke: 'var(--radiobutton-default-dot-border-color)',
|
|
128
|
+
},
|
|
129
|
+
{ sel: '.save-btn', stem: 'inlineeditactions-save-default' },
|
|
130
|
+
{ sel: '.cancel-btn', stem: 'inlineeditactions-cancel-default' },
|
|
122
131
|
|
|
123
132
|
// Chips
|
|
124
133
|
...BADGE_VARIANTS.map((v) => ({ sel: `.badge-${v}`, stem: `badge-${v}` })),
|
|
@@ -172,6 +181,17 @@ const PART_SPECS: readonly PartSpec[] = [
|
|
|
172
181
|
},
|
|
173
182
|
// The arrow is the tooltip's own ::after, so the box takes the fill only.
|
|
174
183
|
{ sel: '.tooltip', stem: 'tooltip', positioned: true, strokeless: true, unmasked: true },
|
|
184
|
+
// Absolutely positioned inside its own wrapper, and clips the picture to the
|
|
185
|
+
// frame the same way `.image` does.
|
|
186
|
+
{ sel: '.image-lightbox-thumb', stem: 'imagelightbox-tile', positioned: true, clips: true },
|
|
187
|
+
// A fixed full-viewport scrim behind the open modal; no border of its own.
|
|
188
|
+
{
|
|
189
|
+
sel: '.image-lightbox-overlay', fill: 'var(--imagelightbox-overlay-surface)',
|
|
190
|
+
stroke: 'transparent', positioned: true, unmasked: true,
|
|
191
|
+
},
|
|
192
|
+
// Close and the two nav chevrons all carry this class; each is fixed and
|
|
193
|
+
// floats over the photo rather than a scrim of its own.
|
|
194
|
+
{ sel: '.image-lightbox-chrome', stem: 'imagelightbox-chrome', positioned: true, unmasked: true },
|
|
175
195
|
|
|
176
196
|
// Status blocks
|
|
177
197
|
...STATUS_VARIANTS.map((v) => ({ sel: `.callout-${v}`, stem: `callout-${v}` })),
|
|
@@ -300,6 +320,8 @@ const STATE_COLOURS: readonly StateSpec[] = [
|
|
|
300
320
|
hoverPair('.button.outline', 'button-outline'),
|
|
301
321
|
...FILLED_BUTTON_VARIANTS.map((v) => hoverPair(`.icon-button.${v}`, `iconbutton-${v}`)),
|
|
302
322
|
hoverPair('.icon-button.outline', 'iconbutton-outline'),
|
|
323
|
+
hoverPair('.save-btn', 'inlineeditactions-save'),
|
|
324
|
+
hoverPair('.cancel-btn', 'inlineeditactions-cancel'),
|
|
303
325
|
hoverPair('.tab', 'tabbar'),
|
|
304
326
|
// A menu's hover belongs to the item under the pointer. The item is not a
|
|
305
327
|
// drawn part, so it keeps its own background and lights up on its own.
|
|
@@ -327,15 +327,24 @@ export function seedComponentsFromApi(
|
|
|
327
327
|
* separately, causing two complete renderer passes and briefly exposing a
|
|
328
328
|
* mixed old-components/new-tokens state. Build the final state first and emit
|
|
329
329
|
* it once instead.
|
|
330
|
+
*
|
|
331
|
+
* A payload mixes sources: a working buffer and a shipped default each carry
|
|
332
|
+
* their own stamp, while a config resolved from the open theme has had its
|
|
333
|
+
* stamp stripped by `normalizeTheme` and is covered by the theme-level
|
|
334
|
+
* `componentSchemaVersion`. Defaulting a stripped entry to 0 replays every
|
|
335
|
+
* migration over already-current data.
|
|
330
336
|
*/
|
|
331
337
|
export function loadThemeFromApi(
|
|
332
338
|
colorsAndType: ColorsAndType,
|
|
333
339
|
configs: Record<string, ComponentSeed>,
|
|
340
|
+
themeComponentSchemaVersion: number,
|
|
334
341
|
): void {
|
|
335
342
|
const next = colorsAndTypeToState(colorsAndType);
|
|
336
343
|
next.components = {};
|
|
337
344
|
for (const [comp, cfg] of Object.entries(configs)) {
|
|
338
|
-
const split = toComponentSlice(
|
|
345
|
+
const split = toComponentSlice(
|
|
346
|
+
comp, cfg.aliases, cfg.config, cfg.schemaVersion ?? themeComponentSchemaVersion,
|
|
347
|
+
);
|
|
339
348
|
next.components[comp] = { aliases: { ...split.aliases }, config: { ...split.config } };
|
|
340
349
|
setSavedComponentBaseline(comp, componentBaseline(split));
|
|
341
350
|
}
|
|
@@ -307,7 +307,7 @@ function runContrastGate(
|
|
|
307
307
|
const hint = scheme === 'dark'
|
|
308
308
|
? `raise the ${label} base color lightness (derived text tops out at 2× its L)`
|
|
309
309
|
: `raise the ${label} base color lightness or reduce its chroma`;
|
|
310
|
-
return `${c.textVar} reaches ${c.ratio.toFixed(2)}:1 vs ${c.against} (floor ${c.floor}:1)
|
|
310
|
+
return `${c.textVar} reaches ${c.ratio.toFixed(2)}:1 vs ${c.against} (floor ${c.floor}:1): ${hint}`;
|
|
311
311
|
});
|
|
312
312
|
|
|
313
313
|
return { scheme, checks, failures };
|
|
@@ -371,7 +371,7 @@ function applyCanvasGradient(canvas: PaletteConfig, scheme: SchemeDirection): st
|
|
|
371
371
|
? Math.min(anchor + 2, labels.length - 1)
|
|
372
372
|
: Math.max(anchor - 2, 0);
|
|
373
373
|
if (sky === anchor) {
|
|
374
|
-
return `skipped
|
|
374
|
+
return `skipped: the Canvas base color anchors at the ramp edge (${labels[anchor]}), leaving no room for a sky; commit the canvas further from ${scheme === 'dark' ? 'black' : 'white'}`;
|
|
375
375
|
}
|
|
376
376
|
canvas.emptyMode = 'gradient';
|
|
377
377
|
canvas.gradientStyle = 'linear';
|
|
@@ -382,7 +382,7 @@ function applyCanvasGradient(canvas: PaletteConfig, scheme: SchemeDirection): st
|
|
|
382
382
|
{ position: 100, paletteLabel: labels[anchor] },
|
|
383
383
|
];
|
|
384
384
|
canvas.gradientSize = 'window';
|
|
385
|
-
return `on, ${labels[sky]}
|
|
385
|
+
return `on, ${labels[sky]} to ${labels[anchor]}`;
|
|
386
386
|
}
|
|
387
387
|
|
|
388
388
|
/** The tokens.css scale, the starting point when a theme carries no shadows.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { API_BASE } from '../storage/apiBase';
|
|
2
|
+
import { hydrateAppliedTheme } from './themeDocumentSync';
|
|
3
|
+
import type { LiveState } from './themeService';
|
|
4
|
+
|
|
5
|
+
export const LIVE_STATE_EVENT = 'live-state';
|
|
6
|
+
|
|
7
|
+
let source: EventSource | null = null;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Follow the dev server's live state. A buffer or the active pointer written
|
|
11
|
+
* from outside the page (a CLI verb, a branch switch) arrives as one frame,
|
|
12
|
+
* and this document hydrates from it the way it does from an Apply.
|
|
13
|
+
*/
|
|
14
|
+
export function init(): void {
|
|
15
|
+
if (source || typeof EventSource === 'undefined') return;
|
|
16
|
+
source = new EventSource(`${API_BASE}/events`);
|
|
17
|
+
source.addEventListener(LIVE_STATE_EVENT, (event) => {
|
|
18
|
+
const state = JSON.parse((event as MessageEvent<string>).data) as LiveState;
|
|
19
|
+
hydrateAppliedTheme(state.fileName, state);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function __resetForTests(): void {
|
|
24
|
+
source?.close();
|
|
25
|
+
source = null;
|
|
26
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stroke role renames (2026-09-07).
|
|
5
|
+
*
|
|
6
|
+
* Three stroke roles share the `--border-width-*` scale: a border encloses, a
|
|
7
|
+
* divider separates, an accent emphasises. `set-geometry` moves each role on
|
|
8
|
+
* its own, and `aliasKinds` reads the role off the suffix, so a divider that
|
|
9
|
+
* was still named `-border` moved with the borders. These four components
|
|
10
|
+
* renamed their dividers and accents to say which line they are. Values are
|
|
11
|
+
* unchanged, so this is a pure key rename.
|
|
12
|
+
*/
|
|
13
|
+
const RENAMES: Record<string, Record<string, string>> = {
|
|
14
|
+
tabbar: Object.fromEntries(
|
|
15
|
+
['default', 'hover', 'active', 'disabled'].map((s) => [
|
|
16
|
+
`--tabbar-${s}-indicator-border-width`,
|
|
17
|
+
`--tabbar-${s}-indicator-width`,
|
|
18
|
+
]),
|
|
19
|
+
),
|
|
20
|
+
collapsiblesection: Object.fromEntries(
|
|
21
|
+
['default', 'hover'].flatMap((s) => [
|
|
22
|
+
[`--collapsiblesection-divider-${s}-border`, `--collapsiblesection-divider-${s}-hairline-color`],
|
|
23
|
+
[`--collapsiblesection-divider-${s}-border-width`, `--collapsiblesection-divider-${s}-hairline-thickness`],
|
|
24
|
+
]),
|
|
25
|
+
),
|
|
26
|
+
dialog: Object.fromEntries(
|
|
27
|
+
['header', 'footer'].flatMap((part) => [
|
|
28
|
+
[`--dialog-${part}-border`, `--dialog-${part}-divider`],
|
|
29
|
+
[`--dialog-${part}-border-width`, `--dialog-${part}-divider-width`],
|
|
30
|
+
]),
|
|
31
|
+
),
|
|
32
|
+
table: {
|
|
33
|
+
'--table-default-header-border': '--table-default-header-divider',
|
|
34
|
+
'--table-default-header-border-width': '--table-default-header-divider-width',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const componentMigration_2026_09_07_strokeRoleRenames: Migration = {
|
|
39
|
+
id: '2026-09-07-stroke-role-renames',
|
|
40
|
+
fromVersion: 26,
|
|
41
|
+
toVersion: 27,
|
|
42
|
+
appliesTo: 'component-config',
|
|
43
|
+
apply(rawVars, meta) {
|
|
44
|
+
const renames = meta.component ? RENAMES[meta.component] : undefined;
|
|
45
|
+
if (!renames) return { ...rawVars };
|
|
46
|
+
const out: Record<string, string> = {};
|
|
47
|
+
for (const [key, value] of Object.entries(rawVars)) {
|
|
48
|
+
out[renames[key] ?? key] = value;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -74,6 +74,7 @@ import { componentMigration_2026_09_01_tabbarActiveTint } from './2026-09-01-tab
|
|
|
74
74
|
import { componentMigration_2026_09_01_gateSuffixEnabled } from './2026-09-01-gate-suffix-enabled';
|
|
75
75
|
import { componentMigration_2026_09_02_sectiondividerDropTitleOutline } from './2026-09-02-sectiondivider-drop-title-outline';
|
|
76
76
|
import { colorsAndTypeMigration_2026_09_03_dropLegacyComponentKeys } from './2026-09-03-drop-legacy-component-keys';
|
|
77
|
+
import { componentMigration_2026_09_07_strokeRoleRenames } from './2026-09-07-stroke-role-renames';
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
80
|
* Registered migrations. Order in this array does not matter — the runner
|
|
@@ -114,6 +115,7 @@ export const MIGRATIONS: Migration[] = [
|
|
|
114
115
|
componentMigration_2026_09_01_gateSuffixEnabled,
|
|
115
116
|
componentMigration_2026_09_02_sectiondividerDropTitleOutline,
|
|
116
117
|
colorsAndTypeMigration_2026_09_03_dropLegacyComponentKeys,
|
|
118
|
+
componentMigration_2026_09_07_strokeRoleRenames,
|
|
117
119
|
];
|
|
118
120
|
|
|
119
121
|
function countFor(kind: 'colors-and-type' | 'component-config'): number {
|
|
@@ -3,20 +3,20 @@ import { openThemeSlug } from '../store/editorConfigStore';
|
|
|
3
3
|
import { loadThemeFromApi } from '../store/editorStore';
|
|
4
4
|
import { migrateColorsAndTypeFonts } from '../fonts/fontMigration';
|
|
5
5
|
import { openThemeSketchSettings } from '../sketch/sketchStore';
|
|
6
|
-
import type {
|
|
6
|
+
import type { LiveState } from './themeService';
|
|
7
7
|
|
|
8
8
|
const CHANNEL_NAME = 'live-tokens:active-theme:v1';
|
|
9
9
|
export const THEME_APPLIED_EVENT = 'live-tokens:theme-applied';
|
|
10
10
|
|
|
11
11
|
export interface AppliedThemeDetail {
|
|
12
12
|
fileName: string;
|
|
13
|
-
result:
|
|
13
|
+
result: LiveState;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
interface AppliedThemeMessage {
|
|
17
17
|
type: 'theme-applied';
|
|
18
18
|
fileName: string;
|
|
19
|
-
result:
|
|
19
|
+
result: LiveState;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
let channel: BroadcastChannel | null = null;
|
|
@@ -32,19 +32,30 @@ function isAppliedThemeMessage(value: unknown): value is AppliedThemeMessage {
|
|
|
32
32
|
&& typeof message.result.componentConfigs === 'object';
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** A layer answered from a buffer: the live state is past the open theme. */
|
|
36
|
+
function hasUnsavedLayer(state: LiveState): boolean {
|
|
37
|
+
return state.colorsAndType._source === 'working'
|
|
38
|
+
|| Object.values(state.componentConfigs).some((cfg) => cfg._source === 'working');
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
/**
|
|
36
|
-
* Replace this document's editor state with the complete state
|
|
37
|
-
*
|
|
38
|
-
* store, so writing CSS into another document is
|
|
39
|
-
* editor must hydrate the typed palette and
|
|
42
|
+
* Replace this document's editor state with the complete live state the
|
|
43
|
+
* server resolved, after an Apply or an outside write. Each same-origin
|
|
44
|
+
* document owns its own Svelte store, so writing CSS into another document is
|
|
45
|
+
* not enough: the receiving editor must hydrate the typed palette and
|
|
46
|
+
* component layers as well.
|
|
40
47
|
*/
|
|
41
|
-
export function hydrateAppliedTheme(fileName: string, result:
|
|
48
|
+
export function hydrateAppliedTheme(fileName: string, result: LiveState): void {
|
|
42
49
|
const colorsAndType = structuredClone(result.colorsAndType);
|
|
43
50
|
migrateColorsAndTypeFonts(colorsAndType);
|
|
44
|
-
loadThemeFromApi(
|
|
51
|
+
loadThemeFromApi(
|
|
52
|
+
colorsAndType,
|
|
53
|
+
structuredClone(result.componentConfigs),
|
|
54
|
+
result.theme.componentSchemaVersion,
|
|
55
|
+
);
|
|
45
56
|
openThemeSlug.set(fileName);
|
|
46
57
|
openThemeSketchSettings(result.theme.sketchSettings);
|
|
47
|
-
liveMovedSinceBake.set(
|
|
58
|
+
liveMovedSinceBake.set(hasUnsavedLayer(result));
|
|
48
59
|
if (typeof document !== 'undefined') {
|
|
49
60
|
document.dispatchEvent(new CustomEvent<AppliedThemeDetail>(THEME_APPLIED_EVENT, {
|
|
50
61
|
detail: { fileName, result },
|
|
@@ -63,6 +74,6 @@ export function init(): void {
|
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
/** Notify every already-open editor/host document after the server switches. */
|
|
66
|
-
export function broadcastAppliedTheme(fileName: string, result:
|
|
77
|
+
export function broadcastAppliedTheme(fileName: string, result: LiveState): void {
|
|
67
78
|
channel?.postMessage({ type: 'theme-applied', fileName, result } satisfies AppliedThemeMessage);
|
|
68
79
|
}
|
|
@@ -59,11 +59,18 @@ export function freshName(base: string, taken: Set<string>): string {
|
|
|
59
59
|
return `${base}_${Date.now()}`;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
/** The whole live state as the server resolves it: the open theme and every
|
|
63
|
+
* layer read through the live doors. An Apply answers with it, and the event
|
|
64
|
+
* stream sends it after an outside write. */
|
|
65
|
+
export interface LiveState {
|
|
66
|
+
fileName: string;
|
|
64
67
|
theme: Theme;
|
|
65
68
|
colorsAndType: ColorsAndType;
|
|
66
69
|
componentConfigs: Record<string, ComponentConfig>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ApplyThemeResult extends LiveState {
|
|
73
|
+
ok: boolean;
|
|
67
74
|
/** Components the theme carries data for that this install doesn't have. */
|
|
68
75
|
skippedComponents: string[];
|
|
69
76
|
/** What the completeness fill (`normalizeTheme`) had to add to reach this
|