@motion-proto/live-tokens 0.5.0 → 0.6.1

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.
@@ -1,7 +1,7 @@
1
1
  import type { Preset, PresetMeta, Theme, ComponentConfig } from './themeTypes';
2
2
  import { versionedFileResource } from './files/versionedFileResource';
3
3
  import { listComponents } from './componentConfigService';
4
- import { getActiveTheme } from './themeService';
4
+ import { getActiveTheme, getProductionInfo } from './themeService';
5
5
 
6
6
  /**
7
7
  * REST client for preset (bundle) manifest files. Each preset file references
@@ -92,3 +92,123 @@ export async function captureCurrentAsPreset(
92
92
  await savePreset(fileName, manifest);
93
93
  await setActivePreset(fileName);
94
94
  }
95
+
96
+ // ── Production preset ──────────────────────────────────────────────────────
97
+ //
98
+ // The production preset is the manifest whose references describe what
99
+ // tokens.css + the component overrides block currently encode. A separate
100
+ // pointer (`presets/_production.json`) from the active preset lets the editor
101
+ // run experiments without disturbing what end users see. Applying a preset to
102
+ // production flips every per-artifact `_production.json` pointer to match the
103
+ // manifest and re-bakes css.
104
+
105
+ export interface PresetProductionInfo {
106
+ fileName: string;
107
+ name: string;
108
+ theme: string;
109
+ componentConfigs: Record<string, string>;
110
+ updatedAt: string;
111
+ }
112
+
113
+ export async function getProductionPreset(): Promise<PresetProductionInfo | null> {
114
+ try {
115
+ const res = await fetch('/api/presets/production');
116
+ if (!res.ok) return null;
117
+ return res.json();
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ export async function applyPresetToProduction(fileName: string): Promise<PresetProductionInfo> {
124
+ const res = await fetch('/api/presets/production', {
125
+ method: 'PUT',
126
+ headers: { 'Content-Type': 'application/json' },
127
+ body: JSON.stringify({ name: fileName }),
128
+ });
129
+ if (!res.ok) {
130
+ const err = await res.json().catch(() => ({ error: 'Apply to production failed' }));
131
+ throw new Error(err.error || 'Apply to production failed');
132
+ }
133
+ return res.json();
134
+ }
135
+
136
+ /**
137
+ * Snapshot whatever each artifact's `_production.json` currently points at
138
+ * into a new preset manifest. Recovery path when individual component Adopts
139
+ * have drifted production away from the production preset and the user wants
140
+ * to preserve that state under a name rather than re-converge to a preset.
141
+ *
142
+ * Does NOT flip `presets/_production.json` — the new preset's references
143
+ * already match production by construction, but tagging it as the production
144
+ * preset is a separate UX decision left to the caller.
145
+ */
146
+ export async function captureProductionAsPreset(
147
+ fileName: string,
148
+ displayName: string,
149
+ ): Promise<void> {
150
+ const themeInfo = await getProductionInfo();
151
+ const components = await listComponents();
152
+ const componentConfigs: Record<string, string> = {};
153
+ for (const c of components) {
154
+ componentConfigs[c.name] = c.productionFile || 'default';
155
+ }
156
+ const now = new Date().toISOString();
157
+ const manifest: Preset = {
158
+ name: displayName,
159
+ createdAt: now,
160
+ updatedAt: now,
161
+ theme: themeInfo.fileName,
162
+ componentConfigs,
163
+ };
164
+ await savePreset(fileName, manifest);
165
+ }
166
+
167
+ /**
168
+ * Three states comparing the active preset against current production:
169
+ * - 'in-production' — active preset IS the production preset AND every
170
+ * manifest reference matches the per-artifact production pointer.
171
+ * - 'editor-only' — the production preset is a different preset; applying
172
+ * this one would flip pointers.
173
+ * - 'diverged' — this IS the production preset, but per-artifact
174
+ * production has drifted (individual component Adopt clicks since the
175
+ * last apply). Two recovery paths: re-apply to converge, or capture
176
+ * production as a new preset to name the divergence.
177
+ */
178
+ export type ProductionPresetStatus = 'in-production' | 'editor-only' | 'diverged';
179
+
180
+ export interface ProductionComparison {
181
+ status: ProductionPresetStatus;
182
+ productionPreset: PresetProductionInfo | null;
183
+ themeDrift: boolean;
184
+ driftedComponents: string[];
185
+ }
186
+
187
+ export async function compareActiveToProduction(
188
+ activePreset: Preset,
189
+ ): Promise<ProductionComparison> {
190
+ const [productionPreset, themeInfo, components] = await Promise.all([
191
+ getProductionPreset(),
192
+ getProductionInfo(),
193
+ listComponents(),
194
+ ]);
195
+ const componentProdMap = new Map(components.map((c) => [c.name, c.productionFile]));
196
+ const themeDrift = themeInfo.fileName !== activePreset.theme;
197
+ const driftedComponents: string[] = [];
198
+ for (const [comp, file] of Object.entries(activePreset.componentConfigs)) {
199
+ const prod = componentProdMap.get(comp);
200
+ if (prod !== file) driftedComponents.push(comp);
201
+ }
202
+ const manifestMatches = !themeDrift && driftedComponents.length === 0;
203
+ const activeFile = activePreset._fileName;
204
+ const isActiveProductionPreset =
205
+ !!activeFile && !!productionPreset && productionPreset.fileName === activeFile;
206
+
207
+ let status: ProductionPresetStatus;
208
+ if (!isActiveProductionPreset) {
209
+ status = 'editor-only';
210
+ } else {
211
+ status = manifestMatches ? 'in-production' : 'diverged';
212
+ }
213
+ return { status, productionPreset, themeDrift, driftedComponents };
214
+ }
@@ -0,0 +1,32 @@
1
+ import { writable } from 'svelte/store';
2
+ import type { ProductionInfo } from './themeService';
3
+ import type { ProductionComparison } from './presetService';
4
+
5
+ /**
6
+ * Monotonic counter that ticks every time a production pointer flips —
7
+ * theme production, a component's production, or a preset applied to
8
+ * production. UI surfaces that compare against current production state
9
+ * (notably `PresetFileManager`) subscribe to this so an Adopt click in a
10
+ * sibling manager refreshes their derived status without per-pair wiring.
11
+ *
12
+ * Bumpers: `ThemeFileManager.handleApplyToProduction`,
13
+ * `ComponentFileManager.handleUpdateProduction`,
14
+ * `presetService.applyPresetToProduction` (called from `PresetFileManager`).
15
+ * Anyone setting `_production.json` should bump.
16
+ */
17
+ export const productionRevision = writable(0);
18
+
19
+ export function bumpProductionRevision(): void {
20
+ productionRevision.update((n) => n + 1);
21
+ }
22
+
23
+ /**
24
+ * Cached production-state stores. The Theme and Preset file managers live in
25
+ * the sidebar footer, swapping in/out of the DOM as the user toggles between
26
+ * the tokens and components views. Keeping the last-known production state in
27
+ * module-level Svelte stores means a remount renders the correct Adopt-button
28
+ * state on the first frame instead of flashing through "not in sync" while a
29
+ * fresh fetch resolves.
30
+ */
31
+ export const themeProductionInfo = writable<ProductionInfo | null>(null);
32
+ export const presetProductionComparison = writable<ProductionComparison | null>(null);
@@ -8,6 +8,15 @@
8
8
  import { componentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
9
9
  import { listComponents } from '../lib/componentConfigService';
10
10
  import { selectedComponent } from '../lib/editorViewStore';
11
+ import { componentDirty } from '../lib/editorStore';
12
+ // Editor chrome + form controls + icon font must be JS imports (not @import
13
+ // inside the style block) so Vite resolves them via the module graph
14
+ // regardless of how the consumer compiles Svelte CSS (external ?lang.css vs
15
+ // injected); otherwise the @import URLs leak to the browser and 404 against
16
+ // the consumer's root.
17
+ import '../styles/ui-editor.css';
18
+ import '../styles/ui-form-controls.css';
19
+ import '@fortawesome/fontawesome-free/css/all.min.css';
11
20
 
12
21
  let drawerOpen = $state(true);
13
22
 
@@ -141,12 +150,16 @@
141
150
  <button
142
151
  class="nav-item"
143
152
  class:active={$selectedComponent === item.id}
153
+ class:dirty={$componentDirty[item.id]}
144
154
  onmouseenter={(e) => showHint(item.label, e.currentTarget)}
145
155
  onmouseleave={hideHint}
146
156
  onclick={() => selectComponent(item.id)}
147
157
  >
148
158
  <i class={item.icon}></i>
149
159
  <span class="rail-label">{item.label}</span>
160
+ {#if $componentDirty[item.id]}
161
+ <span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
162
+ {/if}
150
163
  </button>
151
164
  {/each}
152
165
  </div>
@@ -167,7 +180,6 @@
167
180
  </div>
168
181
 
169
182
  <style>
170
- @import '../styles/ui-editor.css';
171
183
  .components-shell {
172
184
  --rail-w: 48px;
173
185
  display: grid;
@@ -327,6 +339,7 @@
327
339
  }
328
340
 
329
341
  .nav-item {
342
+ position: relative;
330
343
  display: grid;
331
344
  grid-template-columns: 48px 1fr;
332
345
  align-items: center;
@@ -362,6 +375,25 @@
362
375
  opacity: 0.85;
363
376
  }
364
377
 
378
+ /* Amber dot indicating unsaved changes. Anchored to the top-right of the
379
+ icon column so it stays visible whether the rail is collapsed (icon-only)
380
+ or expanded (icon + label). */
381
+ .dirty-dot {
382
+ position: absolute;
383
+ top: 8px;
384
+ left: 30px;
385
+ width: 7px;
386
+ height: 7px;
387
+ border-radius: 50%;
388
+ background: var(--ui-highlight);
389
+ box-shadow: 0 0 0 2px black;
390
+ pointer-events: none;
391
+ }
392
+
393
+ .nav-item.dirty {
394
+ color: var(--ui-text-secondary);
395
+ }
396
+
365
397
  .content {
366
398
  padding: 0 var(--ui-space-32);
367
399
  background: black;
@@ -5,6 +5,14 @@
5
5
  import { installEditorKeybindings } from '../lib/editorKeybindings';
6
6
  import { initializeEditorStore } from '../lib/editorStore';
7
7
  import { storageKey } from '../lib/editorConfig';
8
+ // Editor chrome + form controls + icon font must be JS imports (not @import
9
+ // inside the style block) so Vite resolves them via the module graph
10
+ // regardless of how the consumer compiles Svelte CSS (external ?lang.css vs
11
+ // injected); otherwise the @import URLs leak to the browser and 404 against
12
+ // the consumer's root.
13
+ import '../styles/ui-editor.css';
14
+ import '../styles/ui-form-controls.css';
15
+ import '@fortawesome/fontawesome-free/css/all.min.css';
8
16
 
9
17
  const inOverlay = typeof window !== 'undefined' && window.parent !== window;
10
18
 
@@ -46,8 +54,6 @@
46
54
  </div>
47
55
 
48
56
  <style>
49
- @import '../styles/ui-editor.css';
50
-
51
57
  .editor-page {
52
58
  min-height: 100vh;
53
59
  background: black;
@@ -12,6 +12,7 @@
12
12
  import { scrollSectionIntoView } from '../lib/scrollSection';
13
13
  import { editorState } from '../lib/editorStore';
14
14
  import { editorView, sidebarCondensed, selectedComponent } from '../lib/editorViewStore';
15
+ import { componentDirty } from '../lib/editorStore';
15
16
  import { componentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
16
17
  import { listComponents } from '../lib/componentConfigService';
17
18
 
@@ -170,12 +171,16 @@
170
171
  <button
171
172
  class="nav-item"
172
173
  class:active={$selectedComponent === item.id}
174
+ class:dirty={$componentDirty[item.id]}
173
175
  onmouseenter={(e) => showHint(item.label, e.currentTarget)}
174
176
  onmouseleave={hideHint}
175
177
  onclick={() => selectComponent(item.id)}
176
178
  >
177
179
  <i class={item.icon}></i>
178
180
  <span class="nav-label">{item.label}</span>
181
+ {#if $componentDirty[item.id]}
182
+ <span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
183
+ {/if}
179
184
  </button>
180
185
  {/each}
181
186
  </div>
@@ -262,6 +267,7 @@
262
267
  }
263
268
 
264
269
  .nav-item {
270
+ position: relative;
265
271
  display: grid;
266
272
  grid-template-columns: 48px 1fr;
267
273
  align-items: center;
@@ -286,6 +292,25 @@
286
292
  opacity: 0.85;
287
293
  }
288
294
 
295
+ /* Amber dot indicating unsaved changes. Anchored to the top-right of the
296
+ icon column so it stays visible whether the sidebar is condensed
297
+ (icon-only) or expanded (icon + label). */
298
+ .dirty-dot {
299
+ position: absolute;
300
+ top: 8px;
301
+ left: 30px;
302
+ width: 7px;
303
+ height: 7px;
304
+ border-radius: 50%;
305
+ background: var(--ui-highlight);
306
+ box-shadow: 0 0 0 2px black;
307
+ pointer-events: none;
308
+ }
309
+
310
+ .nav-item.dirty {
311
+ color: var(--ui-text-secondary);
312
+ }
313
+
289
314
  .nav-label {
290
315
  white-space: nowrap;
291
316
  overflow: hidden;
@@ -0,0 +1,138 @@
1
+ /*
2
+ * Site Styles — global typography for the themed pages
3
+ *
4
+ * Unscoped element selectors (h1, h2, p, a, ul li, …) consume the design
5
+ * tokens in tokens.css and recolor with the user's theme. Loaded globally
6
+ * from main.ts.
7
+ *
8
+ * Pair with: ui-editor.css (--ui-* chrome, opposite scope — neutral and
9
+ * theme-immune; only loaded on editor pages).
10
+ */
11
+
12
+ h1 {
13
+ font-family: var(--font-display);
14
+ font-size: var(--font-size-4xl);
15
+ font-weight: var(--font-weight-semibold);
16
+ color: var(--text-primary);
17
+ margin: 0 0 var(--space-12);
18
+ line-height: var(--line-height-sm);
19
+ overflow-wrap: break-word;
20
+ }
21
+
22
+ h2 {
23
+ font-family: var(--font-serif);
24
+ font-size: var(--font-size-2xl);
25
+ font-weight: var(--font-weight-semibold);
26
+ color: var(--text-primary);
27
+ letter-spacing: 0.01em;
28
+ margin: var(--space-32) 0 var(--space-12);
29
+ line-height: var(--line-height-sm);
30
+ overflow-wrap: break-word;
31
+ }
32
+
33
+ h3 {
34
+ font-family: var(--font-serif);
35
+ font-size: var(--font-size-xl);
36
+ font-weight: var(--font-weight-normal);
37
+ color: var(--text-primary);
38
+ margin: var(--space-24) 0 var(--space-8);
39
+ line-height: var(--line-height-sm);
40
+ overflow-wrap: break-word;
41
+ }
42
+
43
+ @media (max-width: 768px) {
44
+ h1 {
45
+ line-height: 1.1;
46
+ margin-bottom: var(--space-8);
47
+ }
48
+
49
+ h2 {
50
+ line-height: 1.15;
51
+ margin-top: var(--space-24);
52
+ }
53
+
54
+ h3 {
55
+ line-height: 1.2;
56
+ margin-top: var(--space-20);
57
+ }
58
+ }
59
+
60
+ p {
61
+ font-family: var(--font-serif);
62
+ font-size: var(--font-size-md);
63
+ color: var(--text-secondary);
64
+ line-height: var(--line-height-md);
65
+ margin: 0 0 14px;
66
+ }
67
+
68
+ p:last-child {
69
+ margin-bottom: 0;
70
+ }
71
+
72
+ a {
73
+ color: var(--text-brand);
74
+ text-decoration: none;
75
+ transition: color var(--duration-150);
76
+ }
77
+
78
+ a:hover {
79
+ color: var(--color-brand-300);
80
+ text-decoration: underline;
81
+ }
82
+
83
+ strong {
84
+ color: var(--text-primary);
85
+ font-weight: var(--font-weight-semibold);
86
+ }
87
+
88
+ ul {
89
+ padding-left: var(--space-24);
90
+ margin: var(--space-12) 0;
91
+ }
92
+
93
+ ul li {
94
+ font-family: var(--font-serif);
95
+ font-size: var(--font-size-md);
96
+ color: var(--text-secondary);
97
+ line-height: 1.75;
98
+ margin-bottom: var(--space-4);
99
+ }
100
+
101
+ ul li::marker {
102
+ color: var(--text-tertiary);
103
+ }
104
+
105
+ ol {
106
+ padding-left: var(--space-24);
107
+ margin: var(--space-12) 0;
108
+ }
109
+
110
+ ol li {
111
+ font-family: var(--font-sans);
112
+ font-size: var(--font-size-md);
113
+ color: var(--text-secondary);
114
+ line-height: 1.6;
115
+ margin-bottom: var(--space-4);
116
+ }
117
+
118
+ ol li::marker {
119
+ color: var(--text-tertiary);
120
+ font-weight: var(--font-weight-semibold);
121
+ }
122
+
123
+ hr {
124
+ border: none;
125
+ border-top: 1px solid var(--border-neutral-faint);
126
+ margin: var(--space-32) 0;
127
+ }
128
+
129
+ blockquote {
130
+ margin: var(--space-16) 0;
131
+ padding: var(--space-12) var(--space-20);
132
+ border: 1px solid var(--color-brand-500);
133
+ border-left: 4px solid var(--color-brand-500);
134
+ background: var(--surface-neutral-high);
135
+ border-radius: 0 var(--radius-md) var(--radius-md) 0;
136
+ color: var(--text-secondary);
137
+ font-style: italic;
138
+ }
@@ -1298,3 +1298,27 @@
1298
1298
  --sectiondivider-special-description-font-weight: var(--font-weight-normal);
1299
1299
  --sectiondivider-special-description-line-height: var(--line-height-md);
1300
1300
  }
1301
+
1302
+ /* component-aliases:start */
1303
+ :root:root {
1304
+ /* button (default_01) */
1305
+ --button-primary-radius: var(--radius-full);
1306
+ --button-primary-hover-radius: var(--radius-full);
1307
+ --button-primary-disabled-radius: var(--radius-full);
1308
+ --button-secondary-radius: var(--radius-full);
1309
+ --button-secondary-hover-radius: var(--radius-full);
1310
+ --button-secondary-disabled-radius: var(--radius-full);
1311
+ --button-outline-radius: var(--radius-full);
1312
+ --button-outline-hover-radius: var(--radius-full);
1313
+ --button-outline-disabled-radius: var(--radius-full);
1314
+ --button-success-radius: var(--radius-full);
1315
+ --button-success-hover-radius: var(--radius-full);
1316
+ --button-success-disabled-radius: var(--radius-full);
1317
+ --button-danger-radius: var(--radius-full);
1318
+ --button-danger-hover-radius: var(--radius-full);
1319
+ --button-danger-disabled-radius: var(--radius-full);
1320
+ --button-warning-radius: var(--radius-full);
1321
+ --button-warning-hover-radius: var(--radius-full);
1322
+ --button-warning-disabled-radius: var(--radius-full);
1323
+ }
1324
+ /* component-aliases:end */
@@ -0,0 +1,186 @@
1
+ /* Editor form controls — `--ui-*` tokens only. Theme-immune; see CONVENTIONS.md. */
2
+
3
+ /* ========================================
4
+ Form Field Layouts
5
+ ======================================== */
6
+
7
+ /* Vertical Layout - Label Above Input/Select */
8
+ /* Usage: Add .ui-form-field-vertical to container div wrapping label + input/select */
9
+ .ui-form-field-vertical {
10
+ display: flex;
11
+ flex-direction: column;
12
+ align-items: stretch;
13
+ gap: var(--ui-space-4);
14
+ }
15
+
16
+ .ui-form-label {
17
+ margin-bottom: 0;
18
+ font-size: var(--ui-font-size-md);
19
+ color: var(--ui-text-primary);
20
+ font-weight: var(--ui-font-weight-normal);
21
+ }
22
+
23
+ /* Horizontal Layout - Label Beside Input/Select */
24
+ /* Usage: Add .ui-form-field-horizontal to container div wrapping label + input/select */
25
+ .ui-form-field-horizontal {
26
+ display: flex;
27
+ justify-content: space-between;
28
+ align-items: center;
29
+ gap: var(--ui-space-8);
30
+ }
31
+
32
+ .ui-form-field-horizontal .ui-form-label {
33
+ white-space: nowrap;
34
+ }
35
+
36
+ /* Inline Layout - Label and Input/Select Close Together */
37
+ /* Usage: Add .ui-form-field-inline to container div wrapping label + input/select */
38
+ .ui-form-field-inline {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: var(--ui-space-12);
42
+ }
43
+
44
+ .ui-form-field-inline .ui-form-label {
45
+ white-space: nowrap;
46
+ flex-shrink: 0;
47
+ }
48
+
49
+ .ui-form-field-inline .ui-form-select,
50
+ .ui-form-field-inline .ui-form-input {
51
+ flex-shrink: 0;
52
+ }
53
+
54
+ /* ========================================
55
+ Form Control Styling
56
+ ======================================== */
57
+
58
+ /* Select/Dropdown Styling */
59
+ .ui-form-select {
60
+ /* No vertical padding - let min-height and line-height center text naturally */
61
+ padding: 0 var(--ui-space-16);
62
+ min-height: 2.375rem; /* ~38px to match button height */
63
+ background: var(--ui-surface-lowest) !important;
64
+ border: 1px solid var(--ui-border-default) !important;
65
+ border-radius: var(--ui-radius-md);
66
+ color: var(--ui-text-primary) !important;
67
+ font-family: var(--ui-font-sans);
68
+ font-size: var(--ui-font-size-md);
69
+ font-weight: var(--ui-font-weight-normal);
70
+ line-height: var(--ui-line-height-normal);
71
+ vertical-align: middle;
72
+ cursor: pointer;
73
+ transition: all var(--ui-transition-fast);
74
+ /* Prevent clipping */
75
+ overflow: visible !important;
76
+ box-sizing: border-box !important;
77
+ /* Reset browser defaults */
78
+ -webkit-appearance: none;
79
+ -moz-appearance: none;
80
+ appearance: none;
81
+ /* Custom dropdown arrow */
82
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M6 8L1 3h10z'/%3E%3C/svg%3E") !important;
83
+ background-repeat: no-repeat !important;
84
+ background-position: right var(--ui-space-12) center !important;
85
+ padding-right: var(--ui-space-32) !important;
86
+ }
87
+
88
+ .ui-form-select:hover:not(:disabled) {
89
+ background-color: var(--ui-surface-low) !important;
90
+ border-color: var(--ui-border-medium) !important;
91
+ }
92
+
93
+ .ui-form-select:focus {
94
+ outline: none;
95
+ border-color: var(--ui-border-strong) !important;
96
+ box-shadow: 0 0 0 2px hsla(0, 58%, 50%, 0.2);
97
+ }
98
+
99
+ .ui-form-select:focus-visible {
100
+ outline: 2px solid var(--ui-highlight);
101
+ outline-offset: 2px;
102
+ }
103
+
104
+ .ui-form-select:active:not(:disabled) {
105
+ background-color: var(--ui-surface) !important;
106
+ }
107
+
108
+ .ui-form-select:disabled {
109
+ background-color: var(--ui-surface-lowest) !important;
110
+ border-color: var(--ui-border-faint) !important;
111
+ color: var(--ui-text-disabled) !important;
112
+ cursor: not-allowed;
113
+ }
114
+
115
+ /* Option Styling - Critical for Legibility */
116
+ /* Note: Most option styling is controlled by browser/OS and cannot be fully overridden */
117
+ /* These styles apply where browsers allow (limited support in Chrome/Firefox) */
118
+ .ui-form-select option {
119
+ background-color: var(--ui-surface-lowest) !important;
120
+ color: var(--ui-text-primary) !important;
121
+ padding: var(--ui-space-8) var(--ui-space-12);
122
+ min-height: 2rem;
123
+ font-size: var(--ui-font-size-md);
124
+ font-family: var(--ui-font-sans);
125
+ line-height: var(--ui-line-height-normal);
126
+ }
127
+
128
+ /* Disabled options */
129
+ .ui-form-select option:disabled {
130
+ color: var(--ui-text-disabled) !important;
131
+ }
132
+
133
+ /* Input Field Styling */
134
+ .ui-form-input {
135
+ padding: var(--ui-space-8);
136
+ background: var(--ui-surface-lowest);
137
+ border: 1px solid var(--ui-border-default);
138
+ border-radius: var(--ui-radius-md);
139
+ color: var(--ui-text-primary);
140
+ font-family: var(--ui-font-sans);
141
+ font-size: var(--ui-font-size-md);
142
+ transition: border-color var(--ui-transition-fast);
143
+ }
144
+
145
+ .ui-form-input:hover:not(:disabled) {
146
+ border-color: var(--ui-border-strong);
147
+ background: var(--ui-surface-low);
148
+ }
149
+
150
+ .ui-form-input:focus {
151
+ outline: none;
152
+ border-color: var(--ui-border-strong);
153
+ box-shadow: 0 0 0 3px hsla(240, 5%, 38%, 0.2);
154
+ }
155
+
156
+ .ui-form-input:disabled {
157
+ background: var(--ui-surface-lowest);
158
+ border-color: var(--ui-border-faint);
159
+ color: var(--ui-text-disabled);
160
+ cursor: not-allowed;
161
+ }
162
+
163
+ /* Placeholder text styling */
164
+ .ui-form-input::placeholder {
165
+ color: var(--ui-text-muted);
166
+ }
167
+
168
+ /* Number input spinner buttons */
169
+ .ui-form-input[type="number"]::-webkit-inner-spin-button,
170
+ .ui-form-input[type="number"]::-webkit-outer-spin-button {
171
+ opacity: 1;
172
+ }
173
+
174
+ /* Checkbox and Radio Styling */
175
+ .ui-form-checkbox,
176
+ .ui-form-radio {
177
+ cursor: pointer;
178
+ width: var(--ui-space-16);
179
+ height: var(--ui-space-16);
180
+ accent-color: var(--ui-gray-600);
181
+ }
182
+
183
+ .ui-form-checkbox:disabled,
184
+ .ui-form-radio:disabled {
185
+ cursor: not-allowed;
186
+ }
@@ -189,7 +189,7 @@
189
189
  style="font-family: {slotCssValue(slot)};{stack.variable === '--font-display' ? ' font-size: var(--ui-font-size-2xl);' : ''}"
190
190
  >The quick brown fox jumps over the lazy dog</span>
191
191
  <select
192
- class="form-select slot-select"
192
+ class="ui-form-select slot-select"
193
193
  value={slotKey(slot)}
194
194
  onchange={(e) => onSelectChange(e, stack.variable, i)}
195
195
  >