@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.
- package/README.md +47 -4
- package/dist-plugin/index.cjs +77 -3
- package/dist-plugin/index.js +77 -3
- package/package.json +10 -5
- package/src/component-editor/DialogEditor.svelte +2 -2
- package/src/component-editor/NotificationEditor.svelte +2 -2
- package/src/component-editor/scaffolding/ComponentFileManager.svelte +4 -1
- package/src/component-editor/scaffolding/SaveAsDialog.svelte +24 -7
- package/src/lib/ColumnsOverlay.svelte +1 -1
- package/src/lib/componentPersist.ts +65 -0
- package/src/lib/presetService.ts +121 -1
- package/src/lib/productionPulse.ts +32 -0
- package/src/pages/ComponentEditorPage.svelte +33 -1
- package/src/pages/Editor.svelte +8 -2
- package/src/pages/EditorShell.svelte +25 -0
- package/src/styles/site.css +138 -0
- package/src/styles/tokens.css +24 -0
- package/src/styles/ui-form-controls.css +186 -0
- package/src/ui/FontStackEditor.svelte +1 -1
- package/src/ui/PresetFileManager.svelte +763 -216
- package/src/ui/ProjectFontsSection.svelte +4 -4
- package/src/ui/ThemeFileManager.svelte +557 -307
- package/src/ui/UnsavedComponentsDialog.svelte +315 -0
- package/src/styles/form-controls.css +0 -188
package/src/lib/presetService.ts
CHANGED
|
@@ -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;
|
package/src/pages/Editor.svelte
CHANGED
|
@@ -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
|
+
}
|
package/src/styles/tokens.css
CHANGED
|
@@ -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
|
>
|