@motion-proto/live-tokens 0.1.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +168 -21
  2. package/dist-plugin/index.cjs +823 -336
  3. package/dist-plugin/index.d.cts +9 -7
  4. package/dist-plugin/index.d.ts +9 -7
  5. package/dist-plugin/index.js +822 -335
  6. package/package.json +46 -20
  7. package/src/assets/newspaper.webp +0 -0
  8. package/src/assets/offering.webp +0 -0
  9. package/src/component-editor/BadgeEditor.svelte +170 -0
  10. package/src/component-editor/CalloutEditor.svelte +103 -0
  11. package/src/component-editor/CardEditor.svelte +184 -0
  12. package/src/component-editor/CollapsibleSectionEditor.svelte +167 -0
  13. package/src/component-editor/CornerBadgeEditor.svelte +207 -0
  14. package/src/component-editor/DialogEditor.svelte +172 -0
  15. package/src/component-editor/ImageEditor.svelte +72 -0
  16. package/src/component-editor/InlineEditActionsEditor.svelte +83 -0
  17. package/src/component-editor/NotificationEditor.svelte +160 -0
  18. package/src/component-editor/ProgressBarEditor.svelte +124 -0
  19. package/src/component-editor/RadioButtonEditor.svelte +140 -0
  20. package/src/component-editor/SectionDividerEditor.svelte +263 -0
  21. package/src/component-editor/SegmentedControlEditor.svelte +154 -0
  22. package/src/component-editor/StandardButtonsEditor.svelte +178 -0
  23. package/src/component-editor/TabBarEditor.svelte +137 -0
  24. package/src/component-editor/TableEditor.svelte +128 -0
  25. package/src/component-editor/TooltipEditor.svelte +122 -0
  26. package/src/component-editor/editorTokens.test.ts +93 -0
  27. package/src/component-editor/groupKeySlots.test.ts +67 -0
  28. package/src/component-editor/groupKeySnapshot.test.ts +52 -0
  29. package/src/component-editor/index.ts +5 -0
  30. package/src/component-editor/registry.ts +246 -0
  31. package/src/component-editor/scaffolding/AngleDial.svelte +185 -0
  32. package/src/component-editor/scaffolding/ComponentEditorBase.svelte +96 -0
  33. package/src/component-editor/scaffolding/ComponentFileManager.svelte +682 -0
  34. package/src/component-editor/scaffolding/ComponentFileMenu.svelte +312 -0
  35. package/src/component-editor/scaffolding/ComponentsTab.svelte +69 -0
  36. package/src/component-editor/scaffolding/CopyFromMenu.svelte +246 -0
  37. package/src/component-editor/scaffolding/DemoHeader.svelte +21 -0
  38. package/src/component-editor/scaffolding/DividerEditor.svelte +81 -0
  39. package/src/component-editor/scaffolding/FieldsetWrapper.svelte +46 -0
  40. package/src/component-editor/scaffolding/GradientCard.svelte +291 -0
  41. package/src/component-editor/scaffolding/LinkageChart.svelte +297 -0
  42. package/src/component-editor/scaffolding/LinkedBlock.svelte +418 -0
  43. package/src/component-editor/scaffolding/NonStylableConfig.svelte +57 -0
  44. package/src/component-editor/scaffolding/SaveAsDialog.svelte +177 -0
  45. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +25 -0
  46. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +56 -0
  47. package/src/component-editor/scaffolding/StateBlock.svelte +115 -0
  48. package/src/component-editor/scaffolding/TokenLayout.svelte +511 -0
  49. package/src/component-editor/scaffolding/TypeEditor.svelte +82 -0
  50. package/src/component-editor/scaffolding/VariantGroup.svelte +277 -0
  51. package/src/component-editor/scaffolding/buildTypeGroupTokens.ts +97 -0
  52. package/src/component-editor/scaffolding/componentSectionType.ts +8 -0
  53. package/src/component-editor/scaffolding/componentSources.ts +9 -0
  54. package/src/component-editor/scaffolding/defaultSections.ts +16 -0
  55. package/src/component-editor/scaffolding/editorContext.ts +44 -0
  56. package/src/component-editor/scaffolding/linkedBlock.ts +226 -0
  57. package/src/component-editor/scaffolding/siblings.ts +33 -0
  58. package/src/component-editor/scaffolding/types.ts +39 -0
  59. package/src/components/Badge.svelte +231 -42
  60. package/src/components/Button.svelte +324 -124
  61. package/src/components/Callout.svelte +145 -0
  62. package/src/components/Card.svelte +123 -25
  63. package/src/components/CollapsibleSection.svelte +213 -35
  64. package/src/components/CornerBadge.svelte +224 -0
  65. package/src/components/Dialog.svelte +137 -114
  66. package/src/components/Image.svelte +43 -0
  67. package/src/components/InlineEditActions.svelte +74 -14
  68. package/src/components/Notification.svelte +184 -163
  69. package/src/components/ProgressBar.svelte +216 -22
  70. package/src/components/RadioButton.svelte +110 -40
  71. package/src/components/SectionDivider.svelte +428 -74
  72. package/src/components/SegmentedControl.svelte +203 -0
  73. package/src/components/TabBar.svelte +146 -21
  74. package/src/components/Table.svelte +102 -0
  75. package/src/components/Tooltip.svelte +45 -19
  76. package/src/components/types.ts +51 -0
  77. package/src/data/google-fonts.json +75 -0
  78. package/src/lib/ColumnsOverlay.svelte +20 -7
  79. package/src/lib/LiveEditorOverlay.svelte +257 -78
  80. package/src/lib/columnsOverlay.ts +21 -17
  81. package/src/lib/componentConfig.test.ts +204 -0
  82. package/src/lib/componentConfigKeys.ts +19 -0
  83. package/src/lib/componentConfigService.ts +88 -0
  84. package/src/lib/copyPopover.ts +30 -0
  85. package/src/lib/cssVarSync.ts +59 -7
  86. package/src/lib/editorConfigStore.ts +0 -10
  87. package/src/lib/editorCore.ts +402 -0
  88. package/src/lib/editorKeybindings.ts +52 -0
  89. package/src/lib/editorPersistence.ts +106 -0
  90. package/src/lib/editorRenderer.ts +74 -0
  91. package/src/lib/editorStore.test.ts +328 -0
  92. package/src/lib/editorStore.ts +412 -0
  93. package/src/lib/editorTypes.ts +100 -0
  94. package/src/lib/editorViewStore.ts +55 -0
  95. package/src/lib/files/versionedFileResource.ts +140 -0
  96. package/src/lib/fontLoader.ts +130 -0
  97. package/src/lib/fontMigration.ts +140 -0
  98. package/src/lib/fontParse.ts +168 -0
  99. package/src/lib/index.ts +48 -30
  100. package/src/lib/lazyConfig.test.ts +54 -0
  101. package/src/lib/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +64 -0
  102. package/src/lib/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +71 -0
  103. package/src/lib/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +43 -0
  104. package/src/lib/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +68 -0
  105. package/src/lib/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +35 -0
  106. package/src/lib/migrations/2026-05-10-sectiondivider-gradient-stops.ts +50 -0
  107. package/src/lib/migrations/2026-05-13-primary-to-brand.ts +90 -0
  108. package/src/lib/migrations/index.ts +93 -0
  109. package/src/lib/migrations/migrations.test.ts +341 -0
  110. package/src/lib/navLinkTypes.ts +1 -0
  111. package/src/lib/overlayState.ts +3 -0
  112. package/src/lib/paletteDerivation.ts +300 -0
  113. package/src/lib/parentRouteStore.ts +42 -0
  114. package/src/lib/parsers/globalRootBlock.ts +32 -0
  115. package/src/lib/presetService.ts +94 -0
  116. package/src/lib/router.ts +42 -10
  117. package/src/lib/scrollSection.ts +45 -0
  118. package/src/lib/slices/columns.ts +59 -0
  119. package/src/lib/slices/components.ts +362 -0
  120. package/src/lib/slices/domainVars.ts +15 -0
  121. package/src/lib/slices/fonts.ts +30 -0
  122. package/src/lib/slices/gradients.ts +153 -0
  123. package/src/lib/slices/overlays.ts +132 -0
  124. package/src/lib/slices/palettes.ts +26 -0
  125. package/src/lib/slices/shadows.ts +123 -0
  126. package/src/lib/storage.ts +88 -0
  127. package/src/lib/themeInit.ts +74 -0
  128. package/src/lib/themeService.ts +101 -0
  129. package/src/lib/themeTypes.ts +146 -0
  130. package/src/lib/tokenRegistry.ts +148 -0
  131. package/src/pages/ComponentEditorPage.svelte +384 -0
  132. package/src/pages/ComponentEditorPage.svelte.d.ts +2 -0
  133. package/src/pages/Editor.svelte +98 -0
  134. package/src/pages/Editor.svelte.d.ts +2 -0
  135. package/src/pages/EditorShell.svelte +348 -0
  136. package/src/styles/_padding.scss +34 -0
  137. package/src/styles/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
  138. package/src/styles/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
  139. package/src/styles/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
  140. package/src/styles/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
  141. package/src/styles/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
  142. package/src/styles/fonts/Manrope/Manrope-latin.woff2 +0 -0
  143. package/src/styles/fonts.css +22 -10
  144. package/src/styles/form-controls.css +14 -16
  145. package/src/styles/tokens.css +1322 -0
  146. package/src/styles/ui-editor.css +126 -0
  147. package/src/{showcase → ui}/BezierCurveEditor.svelte +14 -14
  148. package/src/{showcase → ui}/ColorEditPanel.svelte +42 -36
  149. package/src/ui/EditorViewSwitcher.svelte +180 -0
  150. package/src/ui/FontStackEditor.svelte +360 -0
  151. package/src/ui/GradientEditor.svelte +461 -0
  152. package/src/ui/GradientStopPicker.svelte +74 -0
  153. package/src/ui/PaletteEditor.svelte +1590 -0
  154. package/src/ui/PaletteEditor.test.ts +108 -0
  155. package/src/ui/PresetFileManager.svelte +567 -0
  156. package/src/ui/ProjectFontsSection.svelte +645 -0
  157. package/src/{showcase → ui}/SurfacesTab.svelte +39 -39
  158. package/src/{showcase → ui}/TextTab.svelte +27 -27
  159. package/src/{showcase/TokenFileManager.svelte → ui/ThemeFileManager.svelte} +196 -112
  160. package/src/ui/Toggle.svelte +108 -0
  161. package/src/ui/UICopyPopover.svelte +78 -0
  162. package/src/{showcase/EditorDialog.svelte → ui/UIDialog.svelte} +66 -25
  163. package/src/ui/UIFontFamilySelector.svelte +309 -0
  164. package/src/ui/UIFontSizeSelector.svelte +165 -0
  165. package/src/ui/UIFontWeightSelector.svelte +52 -0
  166. package/src/ui/UILineHeightSelector.svelte +47 -0
  167. package/src/ui/UILinkToggle.svelte +60 -0
  168. package/src/ui/UIOptionItem.svelte +74 -0
  169. package/src/ui/UIOptionList.svelte +27 -0
  170. package/src/ui/UIPaddingSelector.svelte +661 -0
  171. package/src/ui/UIPaletteSelector.svelte +1084 -0
  172. package/src/ui/UIRadio.svelte +72 -0
  173. package/src/ui/UIRadioGroup.svelte +59 -0
  174. package/src/ui/UIRelinkConfirmPopover.svelte +235 -0
  175. package/src/ui/UITokenSelector.svelte +509 -0
  176. package/src/ui/UIVariantSelector.svelte +145 -0
  177. package/src/ui/VariablesTab.svelte +252 -0
  178. package/src/ui/index.ts +31 -0
  179. package/src/ui/keepInViewport.ts +84 -0
  180. package/src/ui/palette/GradientStopEditor.svelte +482 -0
  181. package/src/ui/palette/OverridesPanel.svelte +526 -0
  182. package/src/ui/palette/PaletteBase.svelte +165 -0
  183. package/src/ui/palette/ScaleCurveEditor.svelte +38 -0
  184. package/src/ui/palette/paletteEditorState.ts +89 -0
  185. package/src/ui/sections/ColumnsSection.svelte +273 -0
  186. package/src/ui/sections/GradientsSection.svelte +147 -0
  187. package/src/ui/sections/OverlaysSection.svelte +670 -0
  188. package/src/ui/sections/ShadowsSection.svelte +1250 -0
  189. package/src/ui/sections/TokenScaleTable.svelte +332 -0
  190. package/src/ui/sections/tokenScales.ts +81 -0
  191. package/src/ui/variantScales.ts +108 -0
  192. package/src/components/DetailNav.svelte +0 -78
  193. package/src/components/Toggle.svelte +0 -86
  194. package/src/lib/tokenInit.ts +0 -29
  195. package/src/lib/tokenService.ts +0 -144
  196. package/src/lib/tokenTypes.ts +0 -45
  197. package/src/pages/Admin.svelte +0 -100
  198. package/src/pages/ShowcasePage.svelte +0 -144
  199. package/src/showcase/BackupBrowser.svelte +0 -617
  200. package/src/showcase/ComponentsTab.svelte +0 -105
  201. package/src/showcase/PaletteEditor.svelte +0 -2579
  202. package/src/showcase/PaletteSelector.svelte +0 -627
  203. package/src/showcase/TokenMap.svelte +0 -54
  204. package/src/showcase/VariablesTab.svelte +0 -2655
  205. package/src/showcase/VisualsTab.svelte +0 -231
  206. package/src/showcase/demos/BadgeDemo.svelte +0 -56
  207. package/src/showcase/demos/CardDemo.svelte +0 -50
  208. package/src/showcase/demos/ChoiceButtonsDemo.svelte +0 -192
  209. package/src/showcase/demos/CollapsibleSectionDemo.svelte +0 -54
  210. package/src/showcase/demos/DialogDemo.svelte +0 -42
  211. package/src/showcase/demos/InlineEditActionsDemo.svelte +0 -25
  212. package/src/showcase/demos/NotificationDemo.svelte +0 -147
  213. package/src/showcase/demos/ProgressBarDemo.svelte +0 -54
  214. package/src/showcase/demos/RadioButtonDemo.svelte +0 -56
  215. package/src/showcase/demos/SectionDividerDemo.svelte +0 -77
  216. package/src/showcase/demos/StandardButtonsDemo.svelte +0 -455
  217. package/src/showcase/demos/TabBarDemo.svelte +0 -58
  218. package/src/showcase/demos/TooltipDemo.svelte +0 -52
  219. package/src/showcase/editor.css +0 -93
  220. package/src/showcase/index.ts +0 -17
  221. package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
  222. package/src/styles/fonts/Domine/OFL.txt +0 -97
  223. package/src/styles/fonts/Domine/README.txt +0 -66
  224. /package/src/{showcase → ui}/curveEngine.ts +0 -0
@@ -0,0 +1,418 @@
1
+ <script lang="ts">
2
+ import { fade, slide } from 'svelte/transition';
3
+ import TokenLayout from './TokenLayout.svelte';
4
+ import LinkageChart from './LinkageChart.svelte';
5
+ import { getEditorContext } from './editorContext';
6
+ import type { LinkedBlockResult, LinkedGroup } from './linkedBlock';
7
+
8
+ /** Honor prefers-reduced-motion: matches the rest of the editor's motion
9
+ vocabulary (UITokenSelector pop-bar gates its transitions the same way).
10
+ Read once at module mount; user can toggle in OS preferences and reload. */
11
+ const reduceMotion = typeof window !== 'undefined'
12
+ && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
13
+ const fadeDur = reduceMotion ? 0 : 140;
14
+ const slideDur = reduceMotion ? 0 : 200;
15
+
16
+ export let component: string;
17
+ export let linked: LinkedBlockResult;
18
+
19
+ const editorCtx = getEditorContext();
20
+ const focusedVariant = editorCtx?.focusedVariant;
21
+ const focusedState = editorCtx?.focusedState;
22
+ const hoveredLinkedVariable = editorCtx?.hoveredLinkedVariable;
23
+
24
+ /** Forward a chart row click to whichever tab strip the label belongs to. The chart
25
+ doesn't know if its rows are variants (top-level tab strip) or states (per-VariantGroup
26
+ state tabs), so we set both stores; each consumer adopts the value only if it names
27
+ one of its own tabs. */
28
+ function handleChartSelect(e: CustomEvent<string>) {
29
+ editorCtx?.focusedVariant.set(e.detail);
30
+ editorCtx?.focusedState.set(e.detail);
31
+ }
32
+
33
+ /** Pick the sibling that backs the cell the user is currently focused on, so the row
34
+ reflects that specific variant×state. Tries the most specific match first
35
+ (`"variant state"`), then either dimension alone, finally the group's default rep. */
36
+ function pickFocusedVariable(g: LinkedGroup, variant: string | null, state: string | null): string {
37
+ const map = g.contextToVariable;
38
+ if (variant && state) {
39
+ const both = map.get(`${variant} ${state}`);
40
+ if (both) return both;
41
+ }
42
+ if (variant) {
43
+ const v = map.get(variant);
44
+ if (v) return v;
45
+ }
46
+ if (state) {
47
+ const s = map.get(state);
48
+ if (s) return s;
49
+ }
50
+ return g.token.variable;
51
+ }
52
+
53
+ /** Caption for a card's chart describing the linkage scope. 2-token context
54
+ labels (e.g. "primary default") imply variants × states; single-token
55
+ labels imply one axis. Tuned to typography linking across variants only. */
56
+ function captionFor(contexts: string[]): string {
57
+ if (contexts.length === 0) return '';
58
+ const has2D = contexts.some((c) => /\s/.test(c));
59
+ if (has2D) return 'Links across variants and states';
60
+ return 'Links across variants';
61
+ }
62
+
63
+ /** Format the bracket text on the drill-down row. ≤2 names → comma-list;
64
+ >2 names → numeric ("3 unlinked"). The names ARE the divergence info,
65
+ so this is the primary signal when broken; the chart is drill-down detail. */
66
+ function formatUnlinked(brokenContexts: string[]): string {
67
+ if (brokenContexts.length === 0) return '';
68
+ if (brokenContexts.length <= 2) {
69
+ return `${brokenContexts.join(', ')} unlinked`;
70
+ }
71
+ return `${brokenContexts.length} unlinked`;
72
+ }
73
+
74
+ $: focusedV = (focusedVariant ? $focusedVariant : null) ?? null;
75
+ $: focusedS = (focusedState ? $focusedState : null) ?? null;
76
+ $: hoveredVar = (hoveredLinkedVariable ? $hoveredLinkedVariable : null) ?? null;
77
+
78
+ function setHover(variable: string | null) {
79
+ hoveredLinkedVariable?.set(variable);
80
+ }
81
+
82
+ /** One card per LinkedGroup. Stable order = editor-declared order. */
83
+ $: cards = linked.groups.map((g) => ({
84
+ contexts: g.contexts,
85
+ brokenContexts: g.brokenContexts,
86
+ row: { ...g.token, variable: pickFocusedVariable(g, focusedV, focusedS) },
87
+ caption: captionFor(g.contexts),
88
+ unlinkedText: formatUnlinked(g.brokenContexts),
89
+ isBroken: g.brokenContexts.length > 0,
90
+ }));
91
+
92
+ /** Section-level summary: count of properties with any broken peers. The
93
+ header tells the user *whether to look*; the per-card text tells them
94
+ *what's broken*. Two layers, two granularities. */
95
+ $: brokenPropertyCount = cards.filter((c) => c.isBroken).length;
96
+ $: hasAnyBroken = brokenPropertyCount > 0;
97
+
98
+ /** Default closed; the section header's summary count + "in sync / N unlinked"
99
+ text is the at-a-glance signal, so users opt into the matrix only when
100
+ they need it. */
101
+ let sectionToggleOverride: boolean | null = null;
102
+ $: sectionExpanded = sectionToggleOverride ?? false;
103
+ function toggleSection() {
104
+ sectionToggleOverride = !sectionExpanded;
105
+ }
106
+
107
+ /** Per-card chart expand state, keyed by the card's row variable (which is
108
+ stable per LinkedGroup since pickFocusedVariable runs against the same
109
+ group on every render). */
110
+ let expandedCards: Record<string, boolean> = {};
111
+ function toggleCard(variable: string) {
112
+ expandedCards = { ...expandedCards, [variable]: !expandedCards[variable] };
113
+ }
114
+ </script>
115
+
116
+ {#if cards.length > 0}
117
+ <section class="linked-block">
118
+ <button
119
+ type="button"
120
+ class="section-header"
121
+ class:expanded={sectionExpanded}
122
+ aria-expanded={sectionExpanded}
123
+ on:click={toggleSection}
124
+ >
125
+ <i class="fas fa-chevron-right chevron"></i>
126
+ <span class="section-title">Linked properties</span>
127
+ <span class="section-summary">
128
+ <span class="section-summary-sep">·</span>
129
+ <span class="section-summary-count">{cards.length}</span>
130
+ <span class="section-summary-sep">·</span>
131
+ {#if hasAnyBroken}
132
+ <span class="section-summary-broken">{brokenPropertyCount} unlinked</span>
133
+ {:else}
134
+ <span class="section-summary-ok">in sync</span>
135
+ {/if}
136
+ </span>
137
+ </button>
138
+ {#if sectionExpanded}
139
+ <div class="linked-grid" transition:slide|local={{ duration: slideDur }}>
140
+ {#each cards as card (card.row.variable)}
141
+ {@const cardExpanded = expandedCards[card.row.variable] === true}
142
+ {@const cardHovered = hoveredVar === card.row.variable}
143
+ <article
144
+ class="linked-card"
145
+ class:broken={card.isBroken}
146
+ class:hovered={cardHovered}
147
+ on:mouseenter={() => setHover(card.row.variable)}
148
+ on:mouseleave={() => setHover(null)}
149
+ >
150
+ <h4 class="property-name">{card.row.label}</h4>
151
+ <div class="control-row">
152
+ <TokenLayout
153
+ tokens={[card.row]}
154
+ {component}
155
+ linkedOrder={linked.linkedOrder}
156
+ isLinkedBlock
157
+ on:change
158
+ />
159
+ </div>
160
+ <button
161
+ type="button"
162
+ class="drill-down"
163
+ class:expanded={cardExpanded}
164
+ aria-expanded={cardExpanded}
165
+ on:click={() => toggleCard(card.row.variable)}
166
+ >
167
+ <i class="fas fa-chevron-right chevron"></i>
168
+ <span class="drill-label">Links</span>
169
+ {#if !cardExpanded && card.isBroken}
170
+ <span
171
+ class="unlinked-bracket"
172
+ transition:fade|local={{ duration: fadeDur }}
173
+ >({card.unlinkedText})</span>
174
+ {/if}
175
+ </button>
176
+ {#if cardExpanded}
177
+ <div class="chart-wrap" transition:slide|local={{ duration: slideDur }}>
178
+ <LinkageChart
179
+ contexts={card.contexts}
180
+ broken={card.brokenContexts}
181
+ caption={card.caption}
182
+ selectedRow={focusedV}
183
+ selectedCol={focusedS}
184
+ on:select={handleChartSelect}
185
+ />
186
+ </div>
187
+ {/if}
188
+ </article>
189
+ {/each}
190
+ </div>
191
+ {/if}
192
+ </section>
193
+ {/if}
194
+
195
+ <style>
196
+ /* Panel chrome matches NonStylableConfig (the "Configuration" panel) so the
197
+ three sibling sections — config, variants, linked — read as a coherent
198
+ family. Background + full border + radius give the section the same
199
+ presence as its siblings even when collapsed to just the header row. */
200
+ .linked-block {
201
+ margin-top: var(--ui-space-12);
202
+ padding: var(--ui-space-12);
203
+ border: 1px solid var(--ui-border-faint);
204
+ border-radius: var(--ui-radius-md);
205
+ background: var(--ui-surface-low);
206
+ }
207
+
208
+ /* Section header — clickable strip that doubles as the collapsed-state
209
+ summary. Reads "Linked properties · 7 · in sync" or "· 2 unlinked"; the
210
+ count + status are the at-a-glance signal. The header sits at the top of
211
+ the panel; when the body opens, it slides in below the header (the panel
212
+ grows downward, the header stays put). */
213
+ .section-header {
214
+ display: flex;
215
+ align-items: center;
216
+ gap: var(--ui-space-8);
217
+ width: 100%;
218
+ padding: 0;
219
+ background: none;
220
+ border: 0;
221
+ color: inherit;
222
+ font: inherit;
223
+ text-align: left;
224
+ cursor: pointer;
225
+ }
226
+ .section-header .chevron {
227
+ font-size: 0.625rem;
228
+ color: var(--ui-text-tertiary);
229
+ transition: transform var(--ui-transition-fast);
230
+ width: 0.75rem;
231
+ }
232
+ .section-header.expanded .chevron {
233
+ transform: rotate(90deg);
234
+ }
235
+ .section-title {
236
+ font-size: var(--ui-font-size-md);
237
+ font-weight: 500;
238
+ color: var(--ui-text-primary);
239
+ }
240
+ .section-summary {
241
+ display: inline-flex;
242
+ align-items: center;
243
+ gap: var(--ui-space-6);
244
+ font-size: var(--ui-font-size-sm);
245
+ color: var(--ui-text-tertiary);
246
+ }
247
+ .section-summary-sep {
248
+ color: var(--ui-border-default);
249
+ }
250
+ .section-summary-count {
251
+ font-family: var(--ui-font-mono);
252
+ font-variant-numeric: tabular-nums;
253
+ color: var(--ui-text-secondary);
254
+ }
255
+ .section-summary-ok {
256
+ color: var(--ui-text-tertiary);
257
+ }
258
+ .section-summary-broken {
259
+ color: var(--ui-link-broken);
260
+ }
261
+
262
+ .linked-grid {
263
+ display: flex;
264
+ flex-direction: row;
265
+ flex-wrap: wrap;
266
+ align-items: flex-start;
267
+ gap: var(--ui-space-12);
268
+ margin-top: var(--ui-space-16);
269
+ }
270
+
271
+ /* Card — vertical stack: label heading · control row · drill-down · chart.
272
+ Chrome is the property's visual envelope. The card grows vertically when
273
+ drilled down; adjacent cards keep their position. */
274
+ .linked-card {
275
+ flex: 0 0 auto;
276
+ display: flex;
277
+ flex-direction: column;
278
+ gap: var(--ui-space-8);
279
+ border: 1px solid var(--ui-border-faint);
280
+ border-radius: var(--ui-radius-lg);
281
+ padding: var(--ui-space-12) var(--ui-space-16);
282
+ min-width: 14rem;
283
+ }
284
+ /* Broken cards adopt the deep amber link-broken color directly — same hue
285
+ as the section-summary text and the inline bracket, so the cue carries
286
+ across all three layers (header → card border → bracket text). */
287
+ .linked-card.broken {
288
+ border-color: var(--ui-link-broken);
289
+ }
290
+ /* Bidirectional hover cue: when the user hovers this card, or the matching
291
+ row in the per-state Properties grid, both surfaces light up so the
292
+ linkage is legible at a glance. Background lift + border bump; broken
293
+ cards keep their amber border so the link-state cue isn't overwritten. */
294
+ .linked-card {
295
+ transition: background var(--ui-transition-fast), border-color var(--ui-transition-fast);
296
+ }
297
+ .linked-card.hovered {
298
+ background: var(--ui-hover-lowest);
299
+ border-color: var(--ui-border-default);
300
+ }
301
+ .linked-card.hovered.broken {
302
+ border-color: var(--ui-link-broken);
303
+ }
304
+ /* Mirror the trigger-bg suppression from TokenLayout: while the card is
305
+ hovered, the inner trigger drops its own surface fill so the card's
306
+ hover band reads as one strip. Direct trigger hover keeps its style. */
307
+ .linked-card.hovered :global(.ui-ts-trigger:not(:hover)) {
308
+ background: transparent;
309
+ }
310
+
311
+ .property-name {
312
+ margin: 0;
313
+ font-size: var(--ui-font-size-md);
314
+ font-weight: var(--ui-font-weight-medium);
315
+ color: var(--ui-text-primary);
316
+ letter-spacing: 0.005em;
317
+ }
318
+
319
+ /* Control row hosts a single TokenLayout-rendered selector + resolved value.
320
+ TokenLayout's inline label is suppressed here (we render our own heading
321
+ above), and the grid is reduced to selector + value columns. */
322
+ .control-row {
323
+ display: flex;
324
+ flex-direction: column;
325
+ }
326
+ .control-row :global(.token-row .token-label) {
327
+ display: none;
328
+ }
329
+ .control-row :global(.token-group .token-grid) {
330
+ /* Inherit --token-selector-w from TokenLayout's default (8rem) so the
331
+ trigger renders at the same fixed width here as in per-variant view.
332
+ The earlier 9rem override existed to give the chain badge breathing
333
+ room inside the trigger; the badge is gone now, so the wider column
334
+ just leaves dead space and visually misaligns the two contexts. */
335
+ grid-template-columns: var(--token-selector-w) max-content;
336
+ column-gap: var(--ui-space-8);
337
+ padding: 0;
338
+ /* Linked-block grid skips the label column (we render the property name
339
+ above as a heading), so padding-single-row should start at col 1 here. */
340
+ --padding-row-start: 1;
341
+ }
342
+ .control-row :global(.token-group .token-grid .ui-token-selector) {
343
+ grid-column: span 2;
344
+ }
345
+ /* Linked-block parent grid only has 2 cols (selector + value), so
346
+ UIPaddingSelector's .padding-sides-block can't subgrid into the
347
+ [side-label][trigger][value] layout it uses in per-variant view —
348
+ auto-placement would push each <UITokenSelector> onto its own row.
349
+ Override to a self-contained 3-col grid so each side row keeps the
350
+ [name][dropdown][value] alignment side by side. The `span 2` from the
351
+ rule above still lands the dropdown in cols 2-3 of this local grid,
352
+ after the side label takes col 1. */
353
+ .control-row :global(.padding-sides-block) {
354
+ grid-template-columns: max-content var(--token-selector-w) max-content;
355
+ column-gap: var(--ui-space-8);
356
+ }
357
+
358
+ /* Drill-down row — chevron at the front, "Links" label, optional amber
359
+ bracket of unlinked context names. Whole row is the click target. The
360
+ bracket is hidden when expanded (the chart becomes the source of truth)
361
+ and when nothing is broken (chart still drillable for inspection). */
362
+ .drill-down {
363
+ display: inline-flex;
364
+ align-items: center;
365
+ gap: var(--ui-space-6);
366
+ padding: var(--ui-space-4) 0;
367
+ background: none;
368
+ border: 0;
369
+ color: var(--ui-text-tertiary);
370
+ font: inherit;
371
+ font-size: var(--ui-font-size-sm);
372
+ text-align: left;
373
+ cursor: pointer;
374
+ transition: color var(--ui-transition-fast);
375
+ }
376
+ .drill-down:hover {
377
+ color: var(--ui-text-secondary);
378
+ }
379
+ .drill-down .chevron {
380
+ font-size: 0.625rem;
381
+ color: var(--ui-text-tertiary);
382
+ transition: transform var(--ui-transition-fast);
383
+ width: 0.75rem;
384
+ }
385
+ .drill-down.expanded .chevron {
386
+ transform: rotate(90deg);
387
+ }
388
+ .drill-down .drill-label {
389
+ color: var(--ui-text-secondary);
390
+ }
391
+ .unlinked-bracket {
392
+ color: var(--ui-link-broken);
393
+ font-family: var(--ui-font-mono);
394
+ font-size: var(--ui-font-size-xs);
395
+ }
396
+
397
+ /* Chart sits in its own wrapper so the column gap above the chart is the
398
+ drill-down row's natural spacing, not the card's main gap. */
399
+ .chart-wrap {
400
+ display: flex;
401
+ flex-direction: column;
402
+ }
403
+
404
+ .linked-card :global(.chart .chart-grid-wrap .grid > *) {
405
+ padding: var(--ui-space-4) var(--ui-space-8);
406
+ }
407
+ .linked-card :global(.chart .chart-grid-wrap .grid-1d) {
408
+ grid-template-columns: auto 28px;
409
+ }
410
+ .linked-card :global(.chart) {
411
+ gap: var(--ui-space-6);
412
+ }
413
+ .linked-card :global(.chart .chart-label) {
414
+ font-size: var(--ui-font-size-sm);
415
+ font-weight: 400;
416
+ color: var(--ui-text-tertiary);
417
+ }
418
+ </style>
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ /** Section that gathers render-time toggles a component accepts as props
3
+ (e.g. dismissible, action buttons, hover shimmer, show icons). */
4
+ </script>
5
+
6
+ <section class="config-block">
7
+ <header class="config-header">
8
+ <h3 class="config-title">Configuration</h3>
9
+ <p class="config-description">Component accepts multiple properties that can change its layout. Use these to preview.</p>
10
+ </header>
11
+ <div class="config-controls">
12
+ <slot />
13
+ </div>
14
+ </section>
15
+
16
+ <style>
17
+ .config-block {
18
+ margin-top: var(--ui-space-12);
19
+ padding: var(--ui-space-12);
20
+ border: 1px solid var(--ui-border-faint);
21
+ border-radius: var(--ui-radius-md);
22
+ background: var(--ui-surface-low);
23
+ }
24
+
25
+ .config-header {
26
+ margin-bottom: var(--ui-space-12);
27
+ }
28
+
29
+ .config-title {
30
+ margin: 0;
31
+ font-size: var(--ui-font-size-md);
32
+ font-weight: 500;
33
+ color: var(--ui-text-primary);
34
+ }
35
+
36
+ .config-description {
37
+ margin: var(--ui-space-2) 0 0;
38
+ font-size: var(--ui-font-size-sm);
39
+ color: var(--ui-text-secondary);
40
+ }
41
+
42
+ .config-controls {
43
+ display: flex;
44
+ flex-wrap: wrap;
45
+ gap: var(--ui-space-16);
46
+ align-items: center;
47
+ }
48
+
49
+ .config-controls :global(label) {
50
+ display: inline-flex;
51
+ align-items: center;
52
+ gap: var(--ui-space-6);
53
+ font-size: var(--ui-font-size-sm);
54
+ color: var(--ui-text-secondary);
55
+ cursor: pointer;
56
+ }
57
+ </style>
@@ -0,0 +1,177 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from 'svelte';
3
+ import type { ComponentConfigMeta } from '../../lib/themeTypes';
4
+ import { sanitizeFileName } from '../../lib/themeService';
5
+ import UIDialog from '../../ui/UIDialog.svelte';
6
+
7
+ /** Two-way bound: parent toggles to open/close. */
8
+ export let show: boolean = false;
9
+ /** Display name to seed the input with when the dialog opens. */
10
+ export let currentDisplayName: string = '';
11
+ /** Existing files used by the increment helper to find the next available `_NN` suffix. */
12
+ export let files: ComponentConfigMeta[] = [];
13
+
14
+ const dispatch = createEventDispatcher<{
15
+ save: { displayName: string; fileName: string };
16
+ }>();
17
+
18
+ let saveAsName = '';
19
+ let saveAsInput: HTMLInputElement;
20
+
21
+ // Seed and select the input whenever the dialog opens. setTimeout(..., 0)
22
+ // matches the original parent's behaviour: it runs as a macrotask, after
23
+ // UIDialog's microtask-queued focus on the confirm button — so the input
24
+ // ends up focused-and-selected, not the button.
25
+ $: if (show) {
26
+ saveAsName =
27
+ sanitizeFileName(currentDisplayName) === 'default'
28
+ ? nextIncrementName(currentDisplayName).displayName
29
+ : currentDisplayName;
30
+ setTimeout(() => saveAsInput?.select(), 0);
31
+ }
32
+
33
+ $: saveAsError = (() => {
34
+ const trimmed = saveAsName.trim();
35
+ if (!trimmed) return '';
36
+ if (sanitizeFileName(trimmed) === 'default') {
37
+ return 'The name "default" is reserved for the core component definition.';
38
+ }
39
+ return '';
40
+ })();
41
+
42
+ function nextIncrementName(baseDisplay: string): { displayName: string; fileName: string } {
43
+ const baseName = baseDisplay.replace(/_\d+$/, '');
44
+ const baseFileName = sanitizeFileName(baseName);
45
+ const existingNums = files
46
+ .filter(
47
+ (f) =>
48
+ f.fileName === baseFileName ||
49
+ f.fileName.match(new RegExp(`^${baseFileName}_\\d+$`)),
50
+ )
51
+ .map((f) => {
52
+ const m = f.fileName.match(/_(\d+)$/);
53
+ return m ? parseInt(m[1], 10) : 0;
54
+ });
55
+ const next = (existingNums.length > 0 ? Math.max(...existingNums) : 0) + 1;
56
+ const suffix = String(next).padStart(2, '0');
57
+ return { displayName: `${baseName}_${suffix}`, fileName: `${baseFileName}_${suffix}` };
58
+ }
59
+
60
+ function incrementSaveAsName() {
61
+ saveAsName = nextIncrementName(saveAsName).displayName;
62
+ setTimeout(() => saveAsInput?.select(), 0);
63
+ }
64
+
65
+ function confirmSaveAs() {
66
+ const displayName = saveAsName.trim();
67
+ if (!displayName || saveAsError) return;
68
+ const fileName = sanitizeFileName(displayName);
69
+ show = false;
70
+ dispatch('save', { displayName, fileName });
71
+ }
72
+
73
+ function handleKeydown(e: KeyboardEvent) {
74
+ if (e.key === 'Enter') confirmSaveAs();
75
+ }
76
+ </script>
77
+
78
+ <UIDialog
79
+ bind:show
80
+ title="Save As"
81
+ cancelLabel="Cancel"
82
+ confirmLabel="Save"
83
+ confirmDisabled={!saveAsName.trim() || !!saveAsError}
84
+ on:confirm={confirmSaveAs}
85
+ width="360px"
86
+ >
87
+ <div class="save-as-dialog">
88
+ <div class="save-as-row">
89
+ <input
90
+ class="save-as-input"
91
+ class:invalid={!!saveAsError}
92
+ type="text"
93
+ bind:value={saveAsName}
94
+ bind:this={saveAsInput}
95
+ on:keydown={handleKeydown}
96
+ placeholder="Config name…"
97
+ />
98
+ <button
99
+ type="button"
100
+ class="save-as-increment"
101
+ on:click={incrementSaveAsName}
102
+ title="Increment filename"
103
+ >
104
+ <i class="fas fa-plus"></i>
105
+ </button>
106
+ </div>
107
+ {#if saveAsError}
108
+ <p class="save-as-error" role="alert">{saveAsError}</p>
109
+ {/if}
110
+ </div>
111
+ </UIDialog>
112
+
113
+ <style>
114
+ .save-as-dialog {
115
+ display: flex;
116
+ flex-direction: column;
117
+ gap: var(--ui-space-8);
118
+ }
119
+
120
+ .save-as-row {
121
+ display: flex;
122
+ align-items: stretch;
123
+ gap: var(--ui-space-6);
124
+ }
125
+
126
+ .save-as-input {
127
+ flex: 1;
128
+ min-width: 0;
129
+ padding: var(--ui-space-8) var(--ui-space-10);
130
+ background: var(--ui-surface-lowest);
131
+ border: 1px solid var(--ui-border-subtle);
132
+ border-radius: var(--ui-radius-md);
133
+ color: var(--ui-text-primary);
134
+ font-size: var(--ui-font-size-md);
135
+ outline: none;
136
+ }
137
+
138
+ .save-as-increment {
139
+ display: inline-flex;
140
+ align-items: center;
141
+ justify-content: center;
142
+ width: 2.25rem;
143
+ padding: 0;
144
+ background: var(--ui-surface-low);
145
+ border: 1px solid var(--ui-border-subtle);
146
+ border-radius: var(--ui-radius-md);
147
+ color: var(--ui-text-secondary);
148
+ font-size: var(--ui-font-size-md);
149
+ cursor: pointer;
150
+ transition: all var(--ui-transition-fast);
151
+ }
152
+
153
+ .save-as-increment:hover {
154
+ background: var(--ui-surface);
155
+ border-color: var(--ui-border-default);
156
+ color: var(--ui-text-primary);
157
+ }
158
+
159
+ .save-as-input:focus {
160
+ border-color: var(--ui-border-medium);
161
+ }
162
+
163
+ .save-as-input.invalid,
164
+ .save-as-input.invalid:focus {
165
+ border-color: var(--ui-highlight);
166
+ }
167
+
168
+ .save-as-input::placeholder {
169
+ color: var(--ui-text-muted);
170
+ }
171
+
172
+ .save-as-error {
173
+ margin: 0;
174
+ font-size: var(--ui-font-size-xs);
175
+ color: var(--ui-highlight);
176
+ }
177
+ </style>
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import newspaperBg from '../../assets/newspaper.webp';
3
+
4
+ export let mode: 'image' | 'color' = 'image';
5
+ /** CSS var name (set by ShadowBackdropControls) the backdrop reads when in color mode. */
6
+ export let colorVariable: string;
7
+ /** Padding around the slotted preview. Set to '0' when the slotted component should cover the full backdrop area (e.g. dialog overlay). */
8
+ export let padding: string = '128px';
9
+
10
+ $: backgroundStyle =
11
+ mode === 'image'
12
+ ? `padding: ${padding}; background-image: url(${newspaperBg}); background-size: cover; background-position: center; background-repeat: no-repeat;`
13
+ : `padding: ${padding}; background: var(${colorVariable}, #1a1a1a);`;
14
+ </script>
15
+
16
+ <div class="shadow-backdrop" style={backgroundStyle}>
17
+ <slot />
18
+ </div>
19
+
20
+ <style>
21
+ .shadow-backdrop {
22
+ border-radius: var(--ui-radius-md);
23
+ overflow: hidden;
24
+ }
25
+ </style>