@motion-proto/live-tokens 0.28.1 → 0.29.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.
@@ -154,6 +154,25 @@ var tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup = {
154
154
  }
155
155
  };
156
156
 
157
+ // vite-plugin/tokensCssMigrations/migrations/2026-06-03-transform-scale-additions.ts
158
+ var tokensCssMigration_2026_06_03_transformScaleAdditions = {
159
+ id: "2026-06-03-transform-scale-additions",
160
+ description: "Add the --scale-{sm..2xl} transform-multiplier scale",
161
+ apply(css) {
162
+ return ensureScale(css, {
163
+ sectionComment: "Transform-scale multipliers (e.g. hover zoom). 5% per step.",
164
+ anchorPrefixes: ["--blur-", "--shadow-", "--radius-"],
165
+ entries: [
166
+ { name: "--scale-sm", value: "1.05" },
167
+ { name: "--scale-md", value: "1.1" },
168
+ { name: "--scale-lg", value: "1.15" },
169
+ { name: "--scale-xl", value: "1.2" },
170
+ { name: "--scale-2xl", value: "1.25" }
171
+ ]
172
+ });
173
+ }
174
+ };
175
+
157
176
  // vite-plugin/files/dataPaths.ts
158
177
  import fs from "fs";
159
178
  import path from "path";
@@ -203,7 +222,8 @@ function resolveDataDirs(opts = {}) {
203
222
  // vite-plugin/tokensCssMigrations/index.ts
204
223
  var TOKENS_CSS_MIGRATIONS = [
205
224
  tokensCssMigration_2026_05_29_typographyScaleAdditions,
206
- tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup
225
+ tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup,
226
+ tokensCssMigration_2026_06_03_transformScaleAdditions
207
227
  ];
208
228
  function runTokensCssMigrations(css) {
209
229
  let out = css;
@@ -778,10 +778,30 @@ var tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup = {
778
778
  }
779
779
  };
780
780
 
781
+ // vite-plugin/tokensCssMigrations/migrations/2026-06-03-transform-scale-additions.ts
782
+ var tokensCssMigration_2026_06_03_transformScaleAdditions = {
783
+ id: "2026-06-03-transform-scale-additions",
784
+ description: "Add the --scale-{sm..2xl} transform-multiplier scale",
785
+ apply(css) {
786
+ return ensureScale(css, {
787
+ sectionComment: "Transform-scale multipliers (e.g. hover zoom). 5% per step.",
788
+ anchorPrefixes: ["--blur-", "--shadow-", "--radius-"],
789
+ entries: [
790
+ { name: "--scale-sm", value: "1.05" },
791
+ { name: "--scale-md", value: "1.1" },
792
+ { name: "--scale-lg", value: "1.15" },
793
+ { name: "--scale-xl", value: "1.2" },
794
+ { name: "--scale-2xl", value: "1.25" }
795
+ ]
796
+ });
797
+ }
798
+ };
799
+
781
800
  // vite-plugin/tokensCssMigrations/index.ts
782
801
  var TOKENS_CSS_MIGRATIONS = [
783
802
  tokensCssMigration_2026_05_29_typographyScaleAdditions,
784
- tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup
803
+ tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup,
804
+ tokensCssMigration_2026_06_03_transformScaleAdditions
785
805
  ];
786
806
  function runTokensCssMigrations(css) {
787
807
  let out = css;
@@ -4,7 +4,7 @@ import {
4
4
  resolveDataDirs,
5
5
  runTokensCssMigrations,
6
6
  validateTokensCss
7
- } from "./chunk-CUC32QJ2.js";
7
+ } from "./chunk-2TW77U3O.js";
8
8
 
9
9
  // vite-plugin/themeFileApi.ts
10
10
  import fs2 from "fs";
@@ -199,6 +199,25 @@ var tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup = {
199
199
  }
200
200
  };
201
201
 
202
+ // vite-plugin/tokensCssMigrations/migrations/2026-06-03-transform-scale-additions.ts
203
+ var tokensCssMigration_2026_06_03_transformScaleAdditions = {
204
+ id: "2026-06-03-transform-scale-additions",
205
+ description: "Add the --scale-{sm..2xl} transform-multiplier scale",
206
+ apply(css) {
207
+ return ensureScale(css, {
208
+ sectionComment: "Transform-scale multipliers (e.g. hover zoom). 5% per step.",
209
+ anchorPrefixes: ["--blur-", "--shadow-", "--radius-"],
210
+ entries: [
211
+ { name: "--scale-sm", value: "1.05" },
212
+ { name: "--scale-md", value: "1.1" },
213
+ { name: "--scale-lg", value: "1.15" },
214
+ { name: "--scale-xl", value: "1.2" },
215
+ { name: "--scale-2xl", value: "1.25" }
216
+ ]
217
+ });
218
+ }
219
+ };
220
+
202
221
  // vite-plugin/files/dataPaths.ts
203
222
  var import_fs = __toESM(require("fs"), 1);
204
223
  var import_path = __toESM(require("path"), 1);
@@ -235,7 +254,8 @@ function readLiveTokensConfig() {
235
254
  // vite-plugin/tokensCssMigrations/index.ts
236
255
  var TOKENS_CSS_MIGRATIONS = [
237
256
  tokensCssMigration_2026_05_29_typographyScaleAdditions,
238
- tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup
257
+ tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup,
258
+ tokensCssMigration_2026_06_03_transformScaleAdditions
239
259
  ];
240
260
  function runTokensCssMigrations(css) {
241
261
  let out = css;
@@ -9,7 +9,7 @@ import {
9
9
  renameToken,
10
10
  runTokensCssMigrations,
11
11
  validateTokensCss
12
- } from "../chunk-CUC32QJ2.js";
12
+ } from "../chunk-2TW77U3O.js";
13
13
  export {
14
14
  TOKENS_CSS_MIGRATIONS,
15
15
  collectDefinedTokens,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.28.1",
3
+ "version": "0.29.0",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -94,6 +94,7 @@
94
94
  "build:lib": "npm run build:plugin",
95
95
  "deploy:local": "bash scripts/deploy-local.sh",
96
96
  "check:no-style-imports": "node scripts/check-no-style-imports.mjs",
97
+ "check:slot-prose": "node scripts/check-slot-prose.mjs",
97
98
  "check:editor-font-isolation": "node scripts/check-editor-font-isolation.mjs",
98
99
  "check:component-defaults": "node scripts/sync-component-defaults.mjs --check",
99
100
  "check:production-is-default": "node scripts/check-production-is-default.mjs",
@@ -103,7 +104,7 @@
103
104
  "collapse:manifest": "node scripts/collapse-manifest-to-default.mjs",
104
105
  "check:smoke-install": "bash scripts/smoke-install.sh",
105
106
  "check:smoke-create": "bash scripts/smoke-create.sh",
106
- "prepublishOnly": "npm run check:no-style-imports && 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:smoke-install && npm run check:smoke-create"
107
+ "prepublishOnly": "npm run check:no-style-imports && npm run check:slot-prose && 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:smoke-install && npm run check:smoke-create"
107
108
  },
108
109
  "peerDependencies": {
109
110
  "@sveltejs/vite-plugin-svelte": "^7.0",
@@ -1,6 +1,6 @@
1
1
  <script module lang="ts">
2
2
  import { buildTypeGroupColorTokens } from './scaffolding/buildTypeGroupTokens';
3
- import type { Token, TypeGroupConfig } from './scaffolding/types';
3
+ import type { Token, TypeGroupConfig, IntrinsicSpec } from './scaffolding/types';
4
4
 
5
5
  export const component = 'card';
6
6
 
@@ -83,13 +83,34 @@
83
83
  ...buildTypeGroupColorTokens(typeGroups, { component, variants: ['default'] }),
84
84
  ...typeGroupTokens,
85
85
  ];
86
+
87
+ // Global "Use hover" default. The two `*-active` vars flip together: off = resting
88
+ // tokens (no visible hover), on = the `--card-hover-*` tokens. Stored per-component.
89
+ const HOVER_OFF = { border: 'var(--card-default-border)', shadow: 'var(--card-default-shadow)' };
90
+ const HOVER_ON = { border: 'var(--card-hover-border)', shadow: 'var(--card-hover-shadow)' };
91
+ export const intrinsics: IntrinsicSpec[] = [
92
+ {
93
+ key: 'hover-border',
94
+ variants: ['default'],
95
+ variable: () => '--card-hover-border-active',
96
+ values: [HOVER_OFF.border, HOVER_ON.border],
97
+ default: { default: HOVER_OFF.border },
98
+ },
99
+ {
100
+ key: 'hover-shadow',
101
+ variants: ['default'],
102
+ variable: () => '--card-hover-shadow-active',
103
+ values: [HOVER_OFF.shadow, HOVER_ON.shadow],
104
+ default: { default: HOVER_OFF.shadow },
105
+ },
106
+ ];
86
107
  </script>
87
108
 
88
109
  <script lang="ts">
89
110
  import Card from '../../system/components/Card.svelte';
90
111
  import VariantGroup from './scaffolding/VariantGroup.svelte';
91
112
  import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
92
- import { editorState } from '../core/store/editorStore';
113
+ import { editorState, setComponentAlias } from '../core/store/editorStore';
93
114
  import { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
94
115
  let linked = $derived(computeLinkedBlock(component, linkableContexts, allTokens, $editorState));
95
116
 
@@ -97,7 +118,20 @@
97
118
  Object.entries(states).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),
98
119
  ) as Record<string, Token[]>);
99
120
 
100
- let hoverEnabled = $state(true);
121
+ let aliases = $derived(
122
+ ($editorState.components[component]?.aliases ?? {}) as Record<string, import('../core/store/editorTypes').CssVarRef>,
123
+ );
124
+ let hoverEnabled = $derived.by(() => {
125
+ const ref = aliases['--card-hover-border-active'];
126
+ const raw = ref?.kind === 'literal' ? ref.value : HOVER_OFF.border;
127
+ return raw === HOVER_ON.border;
128
+ });
129
+
130
+ function setHoverEnabled(checked: boolean) {
131
+ const v = checked ? HOVER_ON : HOVER_OFF;
132
+ setComponentAlias(component, '--card-hover-border-active', { kind: 'literal', value: v.border });
133
+ setComponentAlias(component, '--card-hover-shadow-active', { kind: 'literal', value: v.shadow });
134
+ }
101
135
  </script>
102
136
 
103
137
  <ComponentEditorBase {component} title="Card" description="Generic card with icon, title, and slotted body." tokens={allTokens} {linked}>
@@ -110,17 +144,23 @@
110
144
  >
111
145
  {#snippet stateActions(stateName)}
112
146
  {#if stateName === 'hover'}
113
- <label class="hover-enable">
114
- <input type="checkbox" bind:checked={hoverEnabled} />
115
- <span>Use hover</span>
116
- </label>
147
+ <div class="hover-control">
148
+ <label class="hover-enable">
149
+ <input type="checkbox" checked={hoverEnabled} onchange={(e) => setHoverEnabled(e.currentTarget.checked)} />
150
+ <span>Use hover</span>
151
+ </label>
152
+ <p class="hover-help">
153
+ Checked: every card lifts on hover. Unchecked: hover is off by default and a page turns it on
154
+ per card with the <code>hover</code> prop.
155
+ </p>
156
+ </div>
117
157
  {/if}
118
158
  {/snippet}
119
159
  {#snippet children({ activeState })}
120
- {@const previewClass = activeState === 'hover' ? 'force-hover' : (hoverEnabled ? '' : 'no-hover')}
160
+ {@const previewClass = activeState === 'hover' ? 'force-hover' : ''}
121
161
  <div class="card-demo">
122
162
  <Card title="Card title" class={previewClass}>
123
- <p style="margin: 0;">Slotted body content. Hover the card (or switch the editor to the Hover state) to preview hover styling.</p>
163
+ <div class="content-placeholder">Content Placeholder</div>
124
164
  </Card>
125
165
  </div>
126
166
  {/snippet}
@@ -129,6 +169,21 @@
129
169
 
130
170
  <style>
131
171
  .card-demo {
172
+ min-width: 16rem;
173
+ max-width: 28rem;
174
+ }
175
+ .content-placeholder {
176
+ display: flex;
177
+ align-items: center;
178
+ justify-content: center;
179
+ width: 100%;
180
+ min-height: 6rem;
181
+ background: color-mix(in srgb, white 8%, transparent);
182
+ border-radius: var(--ui-radius-sm);
183
+ font-size: var(--ui-font-size-sm);
184
+ color: var(--ui-text-secondary);
185
+ }
186
+ .hover-control {
132
187
  max-width: 28rem;
133
188
  }
134
189
  .hover-enable {
@@ -139,4 +194,12 @@
139
194
  color: var(--ui-text-secondary);
140
195
  cursor: pointer;
141
196
  }
197
+ .hover-help {
198
+ margin: var(--ui-space-4) 0 0;
199
+ font-size: var(--ui-font-size-sm);
200
+ color: var(--ui-text-tertiary);
201
+ }
202
+ .hover-help code {
203
+ font-family: var(--ui-font-mono);
204
+ }
142
205
  </style>
@@ -1,5 +1,5 @@
1
1
  <script module lang="ts">
2
- import type { Token } from './scaffolding/types';
2
+ import type { Token, IntrinsicSpec } from './scaffolding/types';
3
3
 
4
4
  export const component = 'image';
5
5
 
@@ -10,10 +10,23 @@
10
10
  { label: 'border width', groupKey: 'width', variable: '--image-default-border-width' },
11
11
  { label: 'corner radius', groupKey: 'radius', variable: '--image-default-radius' },
12
12
  { label: 'image shadow', groupKey: 'shadow', variable: '--image-default-shadow' },
13
+ { label: 'zoom amount', groupKey: 'scale', variable: '--image-zoom-scale' },
13
14
  ],
14
15
  };
15
16
 
16
17
  export const allTokens: Token[] = Object.values(states).flat();
18
+
19
+ // Global "Use zoom" default. `none` = off; `scale(...)` = every image zooms on hover.
20
+ const ZOOM_ON = 'scale(var(--image-zoom-scale))';
21
+ export const intrinsics: IntrinsicSpec[] = [
22
+ {
23
+ key: 'zoom',
24
+ variants: ['default'],
25
+ variable: () => '--image-zoom-hover',
26
+ values: ['none', ZOOM_ON],
27
+ default: { default: 'none' },
28
+ },
29
+ ];
17
30
  </script>
18
31
 
19
32
  <script lang="ts">
@@ -21,10 +34,53 @@
21
34
  import VariantGroup from './scaffolding/VariantGroup.svelte';
22
35
  import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
23
36
  import demoImageUrl from '../../system/assets/offering.webp';
37
+ import { editorState, setComponentAlias } from '../core/store/editorStore';
38
+
39
+ let aliases = $derived(
40
+ ($editorState.components[component]?.aliases ?? {}) as Record<string, import('../core/store/editorTypes').CssVarRef>,
41
+ );
42
+ let useZoom = $derived.by(() => {
43
+ const ref = aliases['--image-zoom-hover'];
44
+ const raw = ref?.kind === 'literal' ? ref.value : intrinsics[0].default.default;
45
+ return raw === ZOOM_ON;
46
+ });
47
+
48
+ function setUseZoom(checked: boolean) {
49
+ setComponentAlias(component, '--image-zoom-hover', { kind: 'literal', value: checked ? ZOOM_ON : 'none' });
50
+ }
24
51
  </script>
25
52
 
26
53
  <ComponentEditorBase {component} title="Image" description="Framed image with rounded corners, border, and shadow." tokens={allTokens}>
27
54
  <VariantGroup name="image" title="Image" {states} {component}>
28
- <Image src={demoImageUrl} alt="Demo" variant="banner" />
55
+ {#snippet stateActions()}
56
+ <label class="zoom-enable">
57
+ <input type="checkbox" checked={useZoom} onchange={(e) => setUseZoom(e.currentTarget.checked)} />
58
+ <span>Use zoom on hover</span>
59
+ </label>
60
+ {/snippet}
61
+ <Image src={demoImageUrl} alt="Demo" variant="banner" zoom={useZoom ? undefined : false} />
62
+ <p class="zoom-help">
63
+ Checked: every image zooms its contents on hover. Unchecked: zoom is off by default and a page turns it
64
+ on per image with the <code>zoom</code> prop.
65
+ </p>
29
66
  </VariantGroup>
30
67
  </ComponentEditorBase>
68
+
69
+ <style>
70
+ .zoom-enable {
71
+ display: inline-flex;
72
+ align-items: center;
73
+ gap: var(--ui-space-4);
74
+ font-size: var(--ui-font-size-sm);
75
+ color: var(--ui-text-secondary);
76
+ cursor: pointer;
77
+ }
78
+ .zoom-help {
79
+ margin: var(--ui-space-8) 0 0;
80
+ font-size: var(--ui-font-size-sm);
81
+ color: var(--ui-text-tertiary);
82
+ }
83
+ .zoom-help code {
84
+ font-family: var(--ui-font-mono);
85
+ }
86
+ </style>
@@ -58,8 +58,7 @@
58
58
  {#snippet children()}
59
59
  <div class="panel-demo">
60
60
  <Panel minHeight="6rem">
61
- <span class="demo-chip">Stage content</span>
62
- <span class="demo-chip">Stage content</span>
61
+ <div class="content-placeholder">Content Placeholder</div>
63
62
  </Panel>
64
63
  </div>
65
64
  {/snippet}
@@ -71,9 +70,13 @@
71
70
  width: 100%;
72
71
  max-width: 34rem;
73
72
  }
74
- .demo-chip {
75
- padding: var(--ui-space-4) var(--ui-space-8);
76
- border: 1px solid var(--ui-border);
73
+ .content-placeholder {
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ align-self: stretch;
78
+ width: 100%;
79
+ background: color-mix(in srgb, white 8%, transparent);
77
80
  border-radius: var(--ui-radius-sm);
78
81
  font-size: var(--ui-font-size-sm);
79
82
  color: var(--ui-text-secondary);
@@ -6,11 +6,11 @@ import BadgeEditor, { allTokens as badgeTokens } from './BadgeEditor.svelte';
6
6
  import CalloutEditor, { allTokens as calloutTokens } from './CalloutEditor.svelte';
7
7
  import CornerBadgeEditor, { allTokens as cornerBadgeTokens } from './CornerBadgeEditor.svelte';
8
8
  import ButtonEditor, { allTokens as buttonTokens } from './ButtonEditor.svelte';
9
- import CardEditor, { allTokens as cardTokens } from './CardEditor.svelte';
9
+ import CardEditor, { allTokens as cardTokens, intrinsics as cardIntrinsics } from './CardEditor.svelte';
10
10
  import CodeSnippetEditor, { allTokens as codeSnippetTokens } from './CodeSnippetEditor.svelte';
11
11
  import CollapsibleSectionEditor, { allTokens as collapsibleSectionTokens } from './CollapsibleSectionEditor.svelte';
12
12
  import DialogEditor, { allTokens as dialogTokens } from './DialogEditor.svelte';
13
- import ImageEditor, { allTokens as imageTokens } from './ImageEditor.svelte';
13
+ import ImageEditor, { allTokens as imageTokens, intrinsics as imageIntrinsics } from './ImageEditor.svelte';
14
14
  import ImageLightboxEditor, { allTokens as imageLightboxTokens } from './ImageLightboxEditor.svelte';
15
15
  import InlineEditActionsEditor, { allTokens as inlineEditActionsTokens } from './InlineEditActionsEditor.svelte';
16
16
  import InputEditor, { allTokens as inputTokens } from './InputEditor.svelte';
@@ -142,6 +142,7 @@ const builtInRegistry: Readonly<Record<BuiltInComponentId, RegistryEntry>> = Obj
142
142
  sourceFile: 'src/system/components/Card.svelte',
143
143
  editorComponent: CardEditor,
144
144
  schema: cardTokens,
145
+ intrinsics: cardIntrinsics,
145
146
  origin: 'system',
146
147
  },
147
148
  badge: {
@@ -187,6 +188,7 @@ const builtInRegistry: Readonly<Record<BuiltInComponentId, RegistryEntry>> = Obj
187
188
  sourceFile: 'src/system/components/Image.svelte',
188
189
  editorComponent: ImageEditor,
189
190
  schema: imageTokens,
191
+ intrinsics: imageIntrinsics,
190
192
  origin: 'system',
191
193
  },
192
194
  imagelightbox: {
@@ -9,7 +9,7 @@
9
9
  import UIPaddingSelector from '../../ui/UIPaddingSelector.svelte';
10
10
  import UILetterSpacingSelector from '../../ui/UILetterSpacingSelector.svelte';
11
11
  import UIEasingSelector from '../../ui/UIEasingSelector.svelte';
12
- import { BLUR, BORDER_WIDTH, DOT_SIZE, DURATION, RADIUS, SHADOW, DIVIDER_HEIGHT, DIVIDER_INSET } from '../../ui/variantScales';
12
+ import { BLUR, BORDER_WIDTH, DOT_SIZE, DURATION, RADIUS, SCALE, SHADOW, DIVIDER_HEIGHT, DIVIDER_INSET } from '../../ui/variantScales';
13
13
  import {
14
14
  editorState,
15
15
  getComponentPropertySiblings,
@@ -31,6 +31,7 @@
31
31
  | 'divider-inset'
32
32
  | 'dot-size'
33
33
  | 'blur'
34
+ | 'scale'
34
35
  | 'shadow'
35
36
  | 'font-family'
36
37
  | 'font-weight'
@@ -99,6 +100,7 @@
99
100
  { kind: 'divider-inset', matches: (v) => v.endsWith('-divider-inset') },
100
101
  { kind: 'dot-size', matches: (v) => v.endsWith('-dot-size') },
101
102
  { kind: 'blur', matches: (v) => v.endsWith('-blur') || v.startsWith('--blur-') },
103
+ { kind: 'scale', matches: (v) => v.endsWith('-scale') || v.startsWith('--scale-') },
102
104
  { kind: 'shadow', matches: (v) => v.endsWith('-shadow') || v.startsWith('--shadow-') },
103
105
  { kind: 'padding', matches: (v) => v.endsWith('-padding') || v.endsWith('-margin') },
104
106
  { kind: 'gap', matches: (v) => v.endsWith('-gap') },
@@ -134,6 +136,7 @@
134
136
  'duration',
135
137
  'easing',
136
138
  'blur',
139
+ 'scale',
137
140
  'shadow',
138
141
  'text-color',
139
142
  'surface',
@@ -205,6 +208,7 @@
205
208
  'duration': { component: UIVariantSelector, extra: () => ({ ...DURATION }) },
206
209
  'easing': { component: UIEasingSelector },
207
210
  'blur': { component: UIVariantSelector, extra: () => ({ ...BLUR }) },
211
+ 'scale': { component: UIVariantSelector, extra: () => ({ ...SCALE }) },
208
212
  'shadow': { component: UIVariantSelector, extra: () => ({ ...SHADOW }) },
209
213
  'surface': { component: UIPaletteSelector },
210
214
  'border': { component: UIPaletteSelector },
@@ -234,6 +238,7 @@
234
238
  'duration',
235
239
  'easing',
236
240
  'blur',
241
+ 'scale',
237
242
  'shadow',
238
243
  'surface',
239
244
  'border-width',
@@ -804,15 +804,20 @@
804
804
  gap: var(--ui-space-8);
805
805
  }
806
806
 
807
+ /* Baseline (not center) so a tall stateActions block — e.g. a checkbox with a
808
+ wrapping help paragraph — keeps the part strip and its first line pinned at
809
+ the top and grows downward, pushing Properties down rather than recentering
810
+ the strip and shifting it on every state switch. */
807
811
  .tabs-selectors {
808
812
  display: flex;
809
- align-items: center;
813
+ align-items: baseline;
810
814
  gap: var(--ui-space-12);
811
815
  }
812
816
 
813
817
  /* Sub-strip sits flush under the parts strip when a part has interaction states.
814
818
  The eyebrow label distinguishes it from the parts row above. */
815
819
  .tabs-selectors.substrip {
820
+ align-items: center;
816
821
  margin-top: var(--ui-space-6);
817
822
  }
818
823
  .tabs-selectors.substrip .state-eyebrow {
@@ -33,6 +33,18 @@ export const BLUR: VariantScaleEntry = {
33
33
  ],
34
34
  };
35
35
 
36
+ /** Transform-scale multipliers (e.g. image zoom-on-hover). 5% per step. */
37
+ export const SCALE: VariantScaleEntry = {
38
+ varPrefix: '--scale-',
39
+ options: [
40
+ { key: 'sm', label: 'Small', value: '1.05' },
41
+ { key: 'md', label: 'Medium', value: '1.1' },
42
+ { key: 'lg', label: 'Large', value: '1.15' },
43
+ { key: 'xl', label: 'X-Large', value: '1.2' },
44
+ { key: '2xl', label: '2X-Large', value: '1.25' },
45
+ ],
46
+ };
47
+
36
48
  export const BORDER_WIDTH: VariantScaleEntry = {
37
49
  varPrefix: '--border-width-',
38
50
  options: [
@@ -4,6 +4,11 @@
4
4
  iconColor?: string;
5
5
  title?: string;
6
6
  size?: 'default' | 'compact';
7
+ /** false → the card stops pinning body typography so the consumer fully owns slotted content's styling. */
8
+ prose?: boolean;
9
+ /** Apply hover styling on pointer-over. `undefined` inherits the editor's global "Use hover"
10
+ default; `true`/`false` force this instance on/off. */
11
+ hover?: boolean | undefined;
7
12
  class?: string;
8
13
  children?: import('svelte').Snippet;
9
14
  }
@@ -13,16 +18,27 @@
13
18
  iconColor = 'var(--text-secondary)',
14
19
  title = '',
15
20
  size = 'default',
21
+ prose = true,
22
+ hover = undefined,
16
23
  class: className = '',
17
24
  children
18
25
  }: Props = $props();
19
-
26
+
27
+ // Per-instance override of the global hover intrinsic; undefined leaves :root in charge.
28
+ let hoverBorder = $derived(
29
+ hover === undefined ? undefined : hover ? 'var(--card-hover-border)' : 'var(--card-default-border)',
30
+ );
31
+ let hoverShadow = $derived(
32
+ hover === undefined ? undefined : hover ? 'var(--card-hover-shadow)' : 'var(--card-default-shadow)',
33
+ );
20
34
  </script>
21
35
 
22
36
  <div
23
37
  class="card {className}"
24
38
  class:compact={size === 'compact'}
25
- style="--card-color: {iconColor};"
39
+ style:--card-color={iconColor}
40
+ style:--card-hover-border-active={hoverBorder}
41
+ style:--card-hover-shadow-active={hoverShadow}
26
42
  >
27
43
  {#if icon || title}
28
44
  <div class="card-header">
@@ -34,13 +50,14 @@
34
50
  {/if}
35
51
  </div>
36
52
  {/if}
37
- <div class="card-body">
53
+ <div class="card-body" class:prose>
38
54
  {@render children?.()}
39
55
  </div>
40
56
  </div>
41
57
 
42
58
  <style lang="scss">
43
59
  @use '../styles/padding' as *;
60
+ @use '../styles/slot-prose' as *;
44
61
 
45
62
  :global(:root) {
46
63
  --card-default-surface: color-mix(in srgb, var(--surface-neutral-lower) 70%, transparent);
@@ -78,6 +95,12 @@
78
95
 
79
96
  --card-hover-border: var(--border-neutral-strong);
80
97
  --card-hover-shadow: var(--shadow-md);
98
+
99
+ /* Global "Use hover" intrinsic: the active hover values default to the resting
100
+ tokens (hover off). The editor checkbox / a per-instance `hover` prop swap
101
+ these to the `--card-hover-*` tokens to turn hover on. */
102
+ --card-hover-border-active: var(--card-default-border);
103
+ --card-hover-shadow-active: var(--card-default-shadow);
81
104
  }
82
105
 
83
106
  .card {
@@ -92,7 +115,12 @@
92
115
  overflow: hidden;
93
116
  }
94
117
 
95
- .card:not(.no-hover):hover,
118
+ .card:hover {
119
+ border-color: var(--card-hover-border-active);
120
+ box-shadow: var(--card-hover-shadow-active);
121
+ }
122
+
123
+ /* Editor preview hook: paint hover tokens directly, ignoring the on/off gate. */
96
124
  .card.force-hover {
97
125
  border-color: var(--card-hover-border);
98
126
  box-shadow: var(--card-hover-shadow);
@@ -139,35 +167,8 @@
139
167
  font-weight: var(--card-default-body-font-weight);
140
168
  line-height: var(--card-default-body-line-height);
141
169
  @include themed-padding(--card-default-body-padding);
142
- }
143
-
144
- /* Slot content inherits the card's body typography so consumer-side global
145
- element rules (e.g. site.css `p { font-family: serif }`) don't override
146
- the card-body-font-family alias. */
147
- .card-body :global(p),
148
- .card-body :global(ul),
149
- .card-body :global(ol),
150
- .card-body :global(li) {
151
- font: inherit;
152
- color: inherit;
153
- }
154
-
155
- .card-body :global(p) {
156
- margin: 0 0 var(--space-12);
157
- }
158
-
159
- .card-body :global(p:last-child) {
160
- margin-bottom: 0;
161
- }
162
-
163
- .card-body :global(ul),
164
- .card-body :global(ol) {
165
- margin: var(--space-12) 0;
166
- padding-left: var(--space-24);
167
- }
168
170
 
169
- .card-body :global(li) {
170
- margin-bottom: var(--space-4);
171
+ @include slot-prose;
171
172
  }
172
173
 
173
174
  .card.compact .card-body {
@@ -6,6 +6,8 @@
6
6
  expanded?: boolean;
7
7
  href?: string | undefined;
8
8
  variant?: 'chromeless' | 'divider' | 'container';
9
+ /** false → the section stops pinning body typography so the consumer fully owns slotted content's styling. */
10
+ prose?: boolean;
9
11
  class?: string;
10
12
  /** Toggle callback. Preferred over `on:toggle` from 0.5.0 onward. */
11
13
  ontoggle?: () => void;
@@ -18,6 +20,7 @@
18
20
  expanded = false,
19
21
  href = undefined,
20
22
  variant = 'container',
23
+ prose = true,
21
24
  class: className = '',
22
25
  ontoggle,
23
26
  summary,
@@ -67,7 +70,7 @@
67
70
  </div>
68
71
  {/if}
69
72
  {#if expanded && children}
70
- <div class="section-content">
73
+ <div class="section-content" class:prose>
71
74
  {@render children?.()}
72
75
  </div>
73
76
  {/if}
@@ -75,6 +78,7 @@
75
78
 
76
79
  <style lang="scss">
77
80
  @use '../styles/padding' as *;
81
+ @use '../styles/slot-prose' as *;
78
82
 
79
83
  :global(:root) {
80
84
  /* Chromeless — default */
@@ -285,24 +289,7 @@
285
289
  color: var(--text-secondary);
286
290
  font-size: var(--font-size-md);
287
291
  line-height: var(--line-height-md);
288
- }
289
-
290
- /* Slot content inherits the section's body typography so consumer-side
291
- global element rules (e.g. site.css `p { font-family: serif }`) don't
292
- override the collapsible's intended type styling. */
293
- .section-content :global(p),
294
- .section-content :global(ul),
295
- .section-content :global(ol),
296
- .section-content :global(li) {
297
- font: inherit;
298
- color: inherit;
299
- }
300
-
301
- .section-content :global(p) {
302
- margin: 0 0 var(--space-12);
303
- }
304
292
 
305
- .section-content :global(p:last-child) {
306
- margin-bottom: 0;
293
+ @include slot-prose;
307
294
  }
308
295
  </style>
@@ -4,13 +4,17 @@
4
4
  alt: string;
5
5
  variant?: 'default' | 'banner' | 'medium' | 'compact';
6
6
  height?: string | undefined;
7
+ /** Zoom the contents on hover (frame stays fixed). `undefined` inherits the editor's
8
+ global "Use zoom" default; `true`/`false` force this instance on/off. */
9
+ zoom?: boolean | undefined;
7
10
  }
8
11
 
9
12
  let {
10
13
  src,
11
14
  alt,
12
15
  variant = 'default',
13
- height = undefined
16
+ height = undefined,
17
+ zoom = undefined
14
18
  }: Props = $props();
15
19
 
16
20
  const variantHeights: Record<string, string | undefined> = {
@@ -21,9 +25,14 @@
21
25
  };
22
26
 
23
27
  let resolvedHeight = $derived(height ?? variantHeights[variant]);
28
+
29
+ // Per-instance override of the global zoom intrinsic; undefined leaves :root in charge.
30
+ let zoomOverride = $derived(
31
+ zoom === undefined ? undefined : zoom ? 'scale(var(--image-zoom-scale))' : 'none',
32
+ );
24
33
  </script>
25
34
 
26
- <div class="image" style:height={resolvedHeight}>
35
+ <div class="image" style:height={resolvedHeight} style:--image-zoom-hover={zoomOverride}>
27
36
  <img {src} {alt} />
28
37
  </div>
29
38
 
@@ -33,6 +42,9 @@
33
42
  --image-default-border: var(--border-neutral);
34
43
  --image-default-border-width: var(--border-width-1);
35
44
  --image-default-shadow: var(--shadow-md);
45
+ --image-zoom-scale: var(--scale-sm);
46
+ /* Global "Use zoom" intrinsic: `none` (off) or `scale(var(--image-zoom-scale))` (on). */
47
+ --image-zoom-hover: none;
36
48
  }
37
49
 
38
50
  .image {
@@ -48,5 +60,11 @@
48
60
  display: block;
49
61
  object-fit: cover;
50
62
  object-position: center;
63
+ transform-origin: center;
64
+ transition: transform var(--duration-300) var(--ease-out-cubic);
65
+ }
66
+
67
+ .image:hover img {
68
+ transform: var(--image-zoom-hover);
51
69
  }
52
70
  </style>
@@ -24,7 +24,7 @@
24
24
 
25
25
  --panel-stage-surface: linear-gradient(0deg, color-mix(in srgb, var(--surface-neutral-low) 40%, transparent) 0.5%, color-mix(in srgb, var(--surface-neutral-lowest) 75%, transparent) 100%);
26
26
  --panel-stage-padding: var(--space-16);
27
- --panel-stage-inline-padding: var(--space-32);
27
+ --panel-stage-inline-padding: var(--space-16);
28
28
  --panel-stage-gap: var(--space-16);
29
29
  }
30
30
 
@@ -11,7 +11,7 @@ Each file plays a distinct role. Don't merge them without understanding why they
11
11
  | `tokens.css` | Themed pages | Defines `--color-*`, `--surface-*`, `--text-*`, `--space-*`, … | `main.ts` | **Runtime-edited.** The token editor rewrites this file via the `themeFileApi` Vite plugin. Starter content; consumers replace at will. |
12
12
  | `ui-editor.css` | Editor chrome only | Defines `--ui-*` (opaque grayscale, system fonts) | JS-imported by `Editor.svelte` and `ComponentEditorPage.svelte` | Deliberately isolated from the theme system so editor surfaces stay neutral while live theme edits flow through the components being edited. Never load globally. |
13
13
  | `ui-form-controls.css` | Editor chrome only | Consumes `--ui-*` only | JS-imported by `Editor.svelte` and `ComponentEditorPage.svelte` | `.ui-form-select` / `.ui-form-input` / `.ui-form-field-*` classes. Used by `ProjectFontsSection`, `FontStackEditor`, `DialogEditor`, `NotificationEditor`. Theme-immune — must not reference `--font-*`, `--surface-*`, etc. |
14
- | `site.css` | Themed pages | Consumes theme tokens | Page-imported by `Home.svelte` | Consumer-facing starter typography for the landing page (unscoped `h1`, `p`, `a`, …). Never load on editor pages. Replaceable starter content — users edit or replace this file to style their own site. Components that take slot content (Card body, CollapsibleSection content) defend their typography aliases against these global rules via `:global(p) { font: inherit; color: inherit; }`. The demo page no longer imports it; the demo uses scoped classes and component-owned slot typography instead. |
14
+ | `site.css` | Themed pages | Consumes theme tokens | Page-imported by `Home.svelte` | Consumer-facing starter typography for the landing page (unscoped `h1`, `p`, `a`, …). Never load on editor pages. Replaceable starter content — users edit or replace this file to style their own site. Components that slot consumer content (Card body, CollapsibleSection content, SectionKit) defend their body typography against these global rules with `@include slot-prose` (from `styles/_slot-prose.scss`): the mixin pins only the owned type axes (font-family, font-size, font-weight, line-height, color) plus list spacing onto slotted `p`/`ul`/`ol`/`li`, while `a`, `strong`, font-style, letter-spacing and text-transform are left to inherit the page (links stay links, a consumer's italics survive). `prose={false}` opts a slot out entirely, and the `check:slot-prose` guard fails the build on a hand-rolled copy of the pin. The demo page no longer imports it; the demo uses scoped classes and component-owned slot typography instead. |
15
15
  | `fonts.css` + `fonts/` | Themed pages only | n/a (`@font-face` only) | `main.ts` | Build-special-cased: copied directly to `dist/` without processing so Vite doesn't inline woff2 files as base64. Starter content; editor's font invariant means these never affect chrome. |
16
16
 
17
17
  ### Publishing layout
@@ -0,0 +1,52 @@
1
+ // Slot-prose defense.
2
+ //
3
+ // Components render the consumer's raw HTML as slot content in the *same* light
4
+ // DOM tree, so a consumer's global element rules (site.css `p`, `ul li`, …)
5
+ // directly match the slotted `<p>`/`<li>` and beat the container's inherited
6
+ // body typography. Svelte's scope class never reaches slotted content, so there
7
+ // is no automatic protection — the container must pin the type axes it owns.
8
+ //
9
+ // This mixin is the single definition of that pin. It covers p/ul/ol/li and
10
+ // pins ONLY the axes the component owns: font-family, font-size, font-weight,
11
+ // line-height, color, plus the component's spacing rhythm. font-style,
12
+ // letter-spacing and text-transform stay free, so a consumer's italics or
13
+ // tracking survive; `a` and `strong` are left untouched, so links keep looking
14
+ // like links inside a card. `&.prose` gates the whole thing: `prose={false}`
15
+ // drops the pin and the consumer fully owns the slot's content styling.
16
+ //
17
+ // Usage:
18
+ // .card-body { /* owns the baseline axes */ @include slot-prose; }
19
+ // <div class="card-body" class:prose>…</div>
20
+
21
+ @mixin slot-prose {
22
+ &.prose {
23
+ :global(p),
24
+ :global(ul),
25
+ :global(ol),
26
+ :global(li) {
27
+ font-family: inherit;
28
+ font-size: inherit;
29
+ font-weight: inherit;
30
+ line-height: inherit;
31
+ color: inherit;
32
+ }
33
+
34
+ :global(p) {
35
+ margin: 0 0 var(--space-12);
36
+ }
37
+
38
+ :global(p:last-child) {
39
+ margin-bottom: 0;
40
+ }
41
+
42
+ :global(ul),
43
+ :global(ol) {
44
+ margin: var(--space-12) 0;
45
+ padding-left: var(--space-24);
46
+ }
47
+
48
+ :global(li) {
49
+ margin-bottom: var(--space-4);
50
+ }
51
+ }
52
+ }
@@ -1,16 +1,14 @@
1
1
  /* Generated from the production theme by syncFontsToCss. Do not edit. */
2
+ /* Both fonts.css and fonts/ are in dist/, so relative paths work at runtime. */
2
3
 
3
4
  /* Adobe Typekit — fira-code */
4
5
  @import url('https://use.typekit.net/jes8oow.css');
5
6
 
7
+ /* Google Fonts — Manrope */
8
+ @import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&display=swap');
9
+
6
10
  /* Google Fonts — Arvo */
7
11
  @import url('https://fonts.googleapis.com/css2?family=Arvo:ital,wght@0,400;0,700;1,400;1,700&display=swap');
8
12
 
9
13
  /* Google Fonts — GFS Didot */
10
14
  @import url('https://fonts.googleapis.com/css2?family=GFS+Didot&display=swap');
11
-
12
- /* Google Fonts — Fraunces */
13
- @import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,100..900;1,9..144,100..900&display=swap');
14
-
15
- /* Google Fonts — Manrope */
16
- @import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&display=swap');
@@ -458,6 +458,13 @@
458
458
  --blur-xl: 24px;
459
459
  --blur-2xl: 32px;
460
460
 
461
+ /* Transform-scale multipliers (e.g. hover zoom). 5% per step. */
462
+ --scale-sm: 1.05;
463
+ --scale-md: 1.1;
464
+ --scale-lg: 1.15;
465
+ --scale-xl: 1.2;
466
+ --scale-2xl: 1.25;
467
+
461
468
  /* COLUMN LAYOUT — 12-col grid. Drives the dev overlay; consume via
462
469
  grid-template-columns: repeat(var(--columns-count), 1fr) etc.
463
470
  --columns-margin = side gutters (0 = flush). */