@motion-proto/live-tokens 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/live-tokens-add-component/SKILL.md +488 -0
- package/README.md +34 -0
- package/dist-plugin/index.cjs +707 -90
- package/dist-plugin/index.d.cts +1 -0
- package/dist-plugin/index.d.ts +1 -0
- package/dist-plugin/index.js +707 -90
- package/package.json +6 -2
- package/src/app/site.css +1 -1
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +34 -27
- package/src/editor/component-editor/DialogEditor.svelte +4 -4
- package/src/editor/component-editor/NotificationEditor.svelte +3 -1
- package/src/editor/component-editor/SectionDividerEditor.svelte +439 -112
- package/src/editor/component-editor/StandardButtonsEditor.svelte +13 -1
- package/src/editor/component-editor/editors.d.ts +10 -0
- package/src/editor/component-editor/index.ts +16 -1
- package/src/editor/component-editor/registry.ts +103 -26
- package/src/editor/component-editor/scaffolding/AngleDial.svelte +52 -13
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +10 -11
- package/src/editor/component-editor/scaffolding/ComponentsTab.svelte +2 -2
- package/src/editor/component-editor/scaffolding/LinkedBlock.svelte +0 -1
- package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
- package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +15 -2
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +103 -15
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +9 -6
- package/src/editor/component-editor/scaffolding/TypeEditor.svelte +13 -1
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +239 -25
- package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -0
- package/src/editor/component-editor/scaffolding/componentSources.ts +3 -3
- package/src/editor/component-editor/scaffolding/defaultSections.ts +15 -10
- package/src/editor/component-editor/scaffolding/types.ts +11 -0
- package/src/editor/core/components/componentConfigKeys.ts +22 -3
- package/src/editor/core/components/componentConfigService.ts +2 -2
- package/src/editor/core/components/componentPersist.ts +7 -5
- package/src/editor/core/manifests/manifestService.ts +58 -3
- package/src/editor/core/palettes/familySwap.ts +99 -0
- package/src/editor/core/palettes/paletteDerivation.ts +69 -0
- package/src/editor/core/palettes/tokenRegistry.ts +4 -1
- package/src/editor/core/store/editorStore.ts +206 -12
- package/src/editor/core/store/editorTypes.ts +55 -12
- package/src/editor/core/store/gradientSource.ts +192 -0
- package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
- package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
- package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
- package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
- package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
- package/src/editor/core/themes/migrations/index.ts +10 -0
- package/src/editor/core/themes/slices/components.ts +27 -4
- package/src/editor/core/themes/slices/gradients.ts +88 -13
- package/src/editor/core/themes/themeInit.ts +2 -2
- package/src/editor/core/themes/themeTypes.ts +56 -1
- package/src/editor/index.ts +10 -1
- package/src/editor/overlay/ColumnsOverlay.svelte +0 -1
- package/src/editor/overlay/LiveEditorOverlay.svelte +1 -4
- package/src/editor/pages/ComponentEditorPage.svelte +53 -3
- package/src/editor/pages/EditorShell.svelte +53 -3
- package/src/editor/styles/ui-editor.css +1 -0
- package/src/editor/styles/ui-form-controls.css +19 -20
- package/src/editor/ui/BezierCurveEditor.svelte +114 -63
- package/src/editor/ui/EditorViewSwitcher.svelte +0 -1
- package/src/editor/ui/FileLoadList.svelte +22 -5
- package/src/editor/ui/FontStackEditor.svelte +214 -76
- package/src/editor/ui/GradientEditor.svelte +435 -215
- package/src/editor/ui/GradientStopPicker.svelte +11 -3
- package/src/editor/ui/ManifestFileManager.svelte +71 -4
- package/src/editor/ui/PaletteEditor.svelte +52 -79
- package/src/editor/ui/ProjectFontsSection.svelte +328 -293
- package/src/editor/ui/ThemeFileManager.svelte +0 -4
- package/src/editor/ui/UIFontFamilySelector.svelte +0 -1
- package/src/editor/ui/UIFontSizeSelector.svelte +3 -0
- package/src/editor/ui/UIInfoPopover.svelte +0 -1
- package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
- package/src/editor/ui/UIPaletteSelector.svelte +31 -4
- package/src/editor/ui/UIPillButton.svelte +33 -3
- package/src/editor/ui/UISegmentedControl.svelte +114 -0
- package/src/editor/ui/UITokenSelector.svelte +4 -1
- package/src/editor/ui/VariablesTab.svelte +41 -35
- package/src/editor/ui/palette/OverridesPanel.svelte +14 -37
- package/src/editor/ui/palette/PaletteBase.svelte +3 -3
- package/src/editor/ui/sections/ColumnsSection.svelte +1 -2
- package/src/editor/ui/sections/GradientsSection.svelte +1 -1
- package/src/editor/ui/sections/OverlaysSection.svelte +1 -1
- package/src/editor/ui/sections/ShadowsSection.svelte +1 -1
- package/src/system/components/Button.svelte +2 -2
- package/src/system/components/Card.svelte +29 -1
- package/src/system/components/CollapsibleSection.svelte +25 -2
- package/src/system/components/Dialog.svelte +24 -4
- package/src/system/components/FloatingTokenTags.css +43 -24
- package/src/system/components/FloatingTokenTags.svelte +88 -137
- package/src/system/components/Notification.svelte +8 -1
- package/src/system/components/SectionDivider.svelte +532 -381
- package/src/system/styles/CONVENTIONS.md +1 -1
- package/src/system/styles/fonts.css +3 -16
- package/src/system/styles/tokens.css +356 -1199
- package/src/system/styles/tokens.generated.css +544 -0
- package/src/editor/component-editor/scaffolding/DividerEditor.svelte +0 -94
- package/src/editor/component-editor/scaffolding/GradientCard.svelte +0 -296
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Theme } from './themeTypes';
|
|
1
|
+
import type { AliasDiskValue, Theme } from './themeTypes';
|
|
2
2
|
import { activeFileName } from '../store/editorConfigStore';
|
|
3
3
|
import { migrateThemeFonts } from '../fonts/fontMigration';
|
|
4
4
|
import { applyFontSources, applyFontStacks } from '../fonts/fontLoader';
|
|
@@ -52,7 +52,7 @@ export async function initializeTheme(): Promise<void> {
|
|
|
52
52
|
if (list && Array.isArray(list.components)) {
|
|
53
53
|
const configs: Record<
|
|
54
54
|
string,
|
|
55
|
-
{ activeFile: string; aliases: Record<string,
|
|
55
|
+
{ activeFile: string; aliases: Record<string, AliasDiskValue>; config?: Record<string, unknown>; schemaVersion?: number }
|
|
56
56
|
> = {};
|
|
57
57
|
await Promise.all(
|
|
58
58
|
list.components.map(async (c) => {
|
|
@@ -27,6 +27,19 @@ export interface PaletteConfig {
|
|
|
27
27
|
gradientStops?: GradientStop[];
|
|
28
28
|
gradientSize?: 'page' | 'window';
|
|
29
29
|
anchorToBase?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Set to true by importers when they overlay `cssVariables[--color-{ns}-*]`
|
|
32
|
+
* without owning the typed-state curves. The storage-layer reconciler uses
|
|
33
|
+
* it as an opt-in switch: snap `baseColor` (or `tintHue`+`tintChroma` for
|
|
34
|
+
* gray palettes) to the imported `--color-{ns}-500` anchor and clear the
|
|
35
|
+
* flag. Editor-authored themes never set this, so the reconciler is a
|
|
36
|
+
* strict no-op for them.
|
|
37
|
+
*
|
|
38
|
+
* Persists on disk for first-load reconciliation. After reconcile strips
|
|
39
|
+
* the palette-derived keys from `cssVariables`, subsequent reconciles find
|
|
40
|
+
* no anchor and become idempotent no-ops regardless of the flag's value.
|
|
41
|
+
*/
|
|
42
|
+
_imported?: boolean;
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
export type FontSourceKind = 'google' | 'typekit' | 'css-url' | 'font-face';
|
|
@@ -92,12 +105,20 @@ export interface ThemeMeta {
|
|
|
92
105
|
isActive: boolean;
|
|
93
106
|
}
|
|
94
107
|
|
|
108
|
+
/** On-disk shape of a single alias entry. Plain strings carry the bulk of
|
|
109
|
+
* aliases (token refs like `--surface-canvas-low` or literal CSS like `4px`);
|
|
110
|
+
* the gradient object shape is the structured payload for component-owned
|
|
111
|
+
* gradients that can't compress to a single string. */
|
|
112
|
+
export type AliasDiskValue =
|
|
113
|
+
| string
|
|
114
|
+
| { kind: 'gradient'; value: { type: 'linear' | 'radial' | 'solid' | 'none'; angle: number; radius?: number; centerX?: number; aspectX?: number; aspectY?: number; stops: { position: number; color: string; opacity?: number }[] } };
|
|
115
|
+
|
|
95
116
|
export interface ComponentConfig {
|
|
96
117
|
name: string;
|
|
97
118
|
component: string;
|
|
98
119
|
createdAt: string;
|
|
99
120
|
updatedAt: string;
|
|
100
|
-
aliases: Record<string,
|
|
121
|
+
aliases: Record<string, AliasDiskValue>;
|
|
101
122
|
config?: Record<string, unknown>;
|
|
102
123
|
/**
|
|
103
124
|
* Server-attached file-name marker. Same role as `Theme._fileName`. Set by
|
|
@@ -139,6 +160,40 @@ export interface Manifest {
|
|
|
139
160
|
_fileName?: string;
|
|
140
161
|
}
|
|
141
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Transport artifact for sharing a manifest with someone else. Self-contained:
|
|
165
|
+
* the bundle inlines the referenced theme and every non-default component
|
|
166
|
+
* config so the receiver doesn't need anything else on disk to apply it.
|
|
167
|
+
*
|
|
168
|
+
* Bundles are *not* stored under `manifests/` — they're transient downloads /
|
|
169
|
+
* uploads. Local manifests stay lightweight pointer files; bundles are the
|
|
170
|
+
* import/export envelope. See temp/manifest-robustness-plan.md §11.
|
|
171
|
+
*
|
|
172
|
+
* `componentConfigs` is keyed by `${component}/${configName}` so a single map
|
|
173
|
+
* carries multiple components. Entries whose manifest value is `"default"`
|
|
174
|
+
* are deliberately omitted — the receiver's local `default.json` is the
|
|
175
|
+
* live-tokens package's canonical default, and shipping the sender's default
|
|
176
|
+
* would risk version-divergence with no clean conflict story.
|
|
177
|
+
*/
|
|
178
|
+
export interface ManifestBundle {
|
|
179
|
+
/** Discriminator for safe identification of bundle JSON files. */
|
|
180
|
+
kind: 'manifest-bundle';
|
|
181
|
+
/** Bumps when the bundle envelope shape changes. Start at 1. */
|
|
182
|
+
schemaVersion: 1;
|
|
183
|
+
/** Sender's `@motion-proto/live-tokens` package version. Receiver can
|
|
184
|
+
* compare to its own to warn about compatibility drift. */
|
|
185
|
+
liveTokensVersion: string;
|
|
186
|
+
/** ISO timestamp of when the bundle was exported. */
|
|
187
|
+
exportedAt: string;
|
|
188
|
+
/** Full pointer-form manifest (same shape as on-disk manifest files). */
|
|
189
|
+
manifest: Manifest;
|
|
190
|
+
/** Full content of the theme that `manifest.theme` references. */
|
|
191
|
+
theme: Theme;
|
|
192
|
+
/** Full content of each non-default component config referenced by
|
|
193
|
+
* `manifest.componentConfigs`, keyed by `${component}/${configName}`. */
|
|
194
|
+
componentConfigs: Record<string, ComponentConfig>;
|
|
195
|
+
}
|
|
196
|
+
|
|
142
197
|
export interface ManifestMeta {
|
|
143
198
|
name: string;
|
|
144
199
|
fileName: string;
|
package/src/editor/index.ts
CHANGED
|
@@ -7,7 +7,13 @@ export { configureEditor, storageKey } from './core/store/editorConfig';
|
|
|
7
7
|
export { activeFileName } from './core/store/editorConfigStore';
|
|
8
8
|
export { init as initRouter, route, navigate } from './core/routing/router';
|
|
9
9
|
export { init as initCssVarSync } from './core/cssVarSync';
|
|
10
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
init as initEditorStore,
|
|
12
|
+
editorState,
|
|
13
|
+
setComponentAlias,
|
|
14
|
+
setComponentConfig,
|
|
15
|
+
registerComponentSchema,
|
|
16
|
+
} from './core/store/editorStore';
|
|
11
17
|
|
|
12
18
|
export { setCssVar, removeCssVar } from './core/cssVarSync';
|
|
13
19
|
|
|
@@ -67,3 +73,6 @@ export { hexToOklch, oklchToHex, gamutClamp } from './core/palettes/oklch';
|
|
|
67
73
|
export type { Oklch } from './core/palettes/oklch';
|
|
68
74
|
|
|
69
75
|
export { initializeTheme } from './core/themes/themeInit';
|
|
76
|
+
|
|
77
|
+
export { registerComponent } from './component-editor/registry';
|
|
78
|
+
export type { RegisterComponentEntry, RegistryEntry, ComponentId } from './component-editor/registry';
|
|
@@ -422,7 +422,7 @@
|
|
|
422
422
|
|
|
423
423
|
.lt-overlay.no-transition,
|
|
424
424
|
.lt-overlay.no-transition .frame-wrap {
|
|
425
|
-
transition: none
|
|
425
|
+
transition: none;
|
|
426
426
|
}
|
|
427
427
|
|
|
428
428
|
.header {
|
|
@@ -451,7 +451,6 @@
|
|
|
451
451
|
font-size: var(--ui-font-size-md, 16px);
|
|
452
452
|
font-weight: var(--ui-font-weight-semibold, 600);
|
|
453
453
|
color: rgba(255, 255, 255, 0.85);
|
|
454
|
-
letter-spacing: 0.02em;
|
|
455
454
|
}
|
|
456
455
|
|
|
457
456
|
.spacer { flex: 1; }
|
|
@@ -460,7 +459,6 @@
|
|
|
460
459
|
font-size: var(--ui-font-size-md, 16px);
|
|
461
460
|
font-weight: var(--ui-font-weight-medium, 500);
|
|
462
461
|
color: rgba(255, 255, 255, 0.4);
|
|
463
|
-
letter-spacing: 0.02em;
|
|
464
462
|
margin-left: var(--ui-space-2, 2px);
|
|
465
463
|
user-select: none;
|
|
466
464
|
}
|
|
@@ -523,7 +521,6 @@
|
|
|
523
521
|
.seg-label {
|
|
524
522
|
font-size: var(--ui-font-size-md, 16px);
|
|
525
523
|
font-weight: var(--ui-font-weight-semibold, 600);
|
|
526
|
-
letter-spacing: 0.02em;
|
|
527
524
|
color: var(--ui-text-primary, #fff);
|
|
528
525
|
}
|
|
529
526
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import ComponentsTab from '../component-editor/scaffolding/ComponentsTab.svelte';
|
|
6
6
|
import ManifestFileManager from '../ui/ManifestFileManager.svelte';
|
|
7
7
|
import { navigate } from '../core/routing/router';
|
|
8
|
-
import {
|
|
8
|
+
import { getComponentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
|
|
9
9
|
import { listComponents } from '../core/components/componentConfigService';
|
|
10
10
|
import { selectedComponent } from '../core/store/editorViewStore';
|
|
11
11
|
import { componentDirty } from '../core/store/editorStore';
|
|
@@ -96,7 +96,9 @@
|
|
|
96
96
|
window.removeEventListener('keydown', handleKeydown);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
const
|
|
99
|
+
const allComponentNavItems = getComponentRegistryEntries().map(({ id, label, icon, origin }) => ({ id, label, icon, origin }));
|
|
100
|
+
const systemNavItems = allComponentNavItems.filter((i) => i.origin === 'system');
|
|
101
|
+
const customNavItems = allComponentNavItems.filter((i) => i.origin === 'custom');
|
|
100
102
|
</script>
|
|
101
103
|
|
|
102
104
|
<!--
|
|
@@ -146,7 +148,7 @@
|
|
|
146
148
|
{/if}
|
|
147
149
|
</div>
|
|
148
150
|
<div class="nav-items">
|
|
149
|
-
{#each
|
|
151
|
+
{#each systemNavItems as item}
|
|
150
152
|
<button
|
|
151
153
|
class="nav-item"
|
|
152
154
|
class:active={$selectedComponent === item.id}
|
|
@@ -162,6 +164,27 @@
|
|
|
162
164
|
{/if}
|
|
163
165
|
</button>
|
|
164
166
|
{/each}
|
|
167
|
+
{#if customNavItems.length > 0}
|
|
168
|
+
<div class="nav-divider">
|
|
169
|
+
<span class="nav-divider-label">Custom</span>
|
|
170
|
+
</div>
|
|
171
|
+
{#each customNavItems as item}
|
|
172
|
+
<button
|
|
173
|
+
class="nav-item"
|
|
174
|
+
class:active={$selectedComponent === item.id}
|
|
175
|
+
class:dirty={$componentDirty[item.id]}
|
|
176
|
+
onmouseenter={(e) => showHint(item.label, e.currentTarget)}
|
|
177
|
+
onmouseleave={hideHint}
|
|
178
|
+
onclick={() => selectComponent(item.id)}
|
|
179
|
+
>
|
|
180
|
+
<i class={item.icon}></i>
|
|
181
|
+
<span class="rail-label">{item.label}</span>
|
|
182
|
+
{#if $componentDirty[item.id]}
|
|
183
|
+
<span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
|
|
184
|
+
{/if}
|
|
185
|
+
</button>
|
|
186
|
+
{/each}
|
|
187
|
+
{/if}
|
|
165
188
|
</div>
|
|
166
189
|
{#if drawerOpen}
|
|
167
190
|
<div class="sidebar-footer">
|
|
@@ -331,6 +354,33 @@
|
|
|
331
354
|
background: black;
|
|
332
355
|
}
|
|
333
356
|
|
|
357
|
+
/* Divider between SYSTEM and CUSTOM groups. The horizontal line uses the
|
|
358
|
+
dimmer border token (sub-element separator), with an uppercase eyebrow
|
|
359
|
+
label that fades out when the rail is collapsed so the line still reads. */
|
|
360
|
+
.nav-divider {
|
|
361
|
+
display: grid;
|
|
362
|
+
grid-template-columns: 48px 1fr;
|
|
363
|
+
align-items: center;
|
|
364
|
+
height: 28px;
|
|
365
|
+
margin-top: var(--ui-space-8);
|
|
366
|
+
border-top: 1px solid var(--ui-border-low);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.nav-divider-label {
|
|
370
|
+
grid-column: 2;
|
|
371
|
+
font-size: var(--ui-font-size-xs);
|
|
372
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
373
|
+
color: var(--ui-text-tertiary);
|
|
374
|
+
text-transform: uppercase;
|
|
375
|
+
letter-spacing: 0.04em;
|
|
376
|
+
opacity: 0;
|
|
377
|
+
transition: opacity 180ms ease;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.components-shell.rail-expanded .nav-divider-label {
|
|
381
|
+
opacity: 1;
|
|
382
|
+
}
|
|
383
|
+
|
|
334
384
|
.sidebar-footer {
|
|
335
385
|
flex-shrink: 0;
|
|
336
386
|
margin-top: auto;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { editorState } from '../core/store/editorStore';
|
|
14
14
|
import { editorView, sidebarCondensed, selectedComponent } from '../core/store/editorViewStore';
|
|
15
15
|
import { componentDirty } from '../core/store/editorStore';
|
|
16
|
-
import {
|
|
16
|
+
import { getComponentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
|
|
17
17
|
import { listComponents } from '../core/components/componentConfigService';
|
|
18
18
|
|
|
19
19
|
const tokenNavItems = [
|
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
{ id: 'utility-tokens', label: 'Utility Tokens', icon: 'fas fa-sliders' }
|
|
30
30
|
];
|
|
31
31
|
|
|
32
|
-
const
|
|
32
|
+
const allComponentNavItems = getComponentRegistryEntries().map(({ id, label, icon, origin }) => ({ id, label, icon, origin }));
|
|
33
|
+
const systemNavItems = allComponentNavItems.filter((i) => i.origin === 'system');
|
|
34
|
+
const customNavItems = allComponentNavItems.filter((i) => i.origin === 'custom');
|
|
33
35
|
|
|
34
36
|
let selectedTokenSection: string | null = $state(null);
|
|
35
37
|
let saveStatus: 'idle' | 'saving' | 'saved' | 'error' = $state('idle');
|
|
@@ -149,7 +151,7 @@
|
|
|
149
151
|
{/if}
|
|
150
152
|
{:else}
|
|
151
153
|
<div class="nav-items">
|
|
152
|
-
{#each
|
|
154
|
+
{#each systemNavItems as item}
|
|
153
155
|
<button
|
|
154
156
|
class="nav-item"
|
|
155
157
|
class:active={$selectedComponent === item.id}
|
|
@@ -165,6 +167,27 @@
|
|
|
165
167
|
{/if}
|
|
166
168
|
</button>
|
|
167
169
|
{/each}
|
|
170
|
+
{#if customNavItems.length > 0}
|
|
171
|
+
<div class="nav-divider">
|
|
172
|
+
<span class="nav-divider-label">Custom</span>
|
|
173
|
+
</div>
|
|
174
|
+
{#each customNavItems as item}
|
|
175
|
+
<button
|
|
176
|
+
class="nav-item"
|
|
177
|
+
class:active={$selectedComponent === item.id}
|
|
178
|
+
class:dirty={$componentDirty[item.id]}
|
|
179
|
+
onmouseenter={(e) => showHint(item.label, e.currentTarget)}
|
|
180
|
+
onmouseleave={hideHint}
|
|
181
|
+
onclick={() => selectComponent(item.id)}
|
|
182
|
+
>
|
|
183
|
+
<i class={item.icon}></i>
|
|
184
|
+
<span class="nav-label">{item.label}</span>
|
|
185
|
+
{#if $componentDirty[item.id]}
|
|
186
|
+
<span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
|
|
187
|
+
{/if}
|
|
188
|
+
</button>
|
|
189
|
+
{/each}
|
|
190
|
+
{/if}
|
|
168
191
|
</div>
|
|
169
192
|
{#if !condensed}
|
|
170
193
|
<div class="sidebar-footer">
|
|
@@ -248,6 +271,33 @@
|
|
|
248
271
|
flex-shrink: 0;
|
|
249
272
|
}
|
|
250
273
|
|
|
274
|
+
/* Divider between SYSTEM and CUSTOM groups. The horizontal line uses the
|
|
275
|
+
dimmer border token (sub-element separator), with an uppercase eyebrow
|
|
276
|
+
label that fades out when the rail is condensed so the line still reads. */
|
|
277
|
+
.nav-divider {
|
|
278
|
+
display: grid;
|
|
279
|
+
grid-template-columns: 48px 1fr;
|
|
280
|
+
align-items: center;
|
|
281
|
+
height: 28px;
|
|
282
|
+
margin-top: var(--ui-space-8);
|
|
283
|
+
border-top: 1px solid var(--ui-border-low);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.nav-divider-label {
|
|
287
|
+
grid-column: 2;
|
|
288
|
+
font-size: var(--ui-font-size-xs);
|
|
289
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
290
|
+
color: var(--ui-text-tertiary);
|
|
291
|
+
text-transform: uppercase;
|
|
292
|
+
letter-spacing: 0.04em;
|
|
293
|
+
opacity: 1;
|
|
294
|
+
transition: opacity 180ms ease;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.layout.condensed .nav-divider-label {
|
|
298
|
+
opacity: 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
251
301
|
.nav-item {
|
|
252
302
|
position: relative;
|
|
253
303
|
display: grid;
|
|
@@ -59,11 +59,12 @@
|
|
|
59
59
|
.ui-form-select {
|
|
60
60
|
/* No vertical padding - let min-height and line-height center text naturally */
|
|
61
61
|
padding: 0 var(--ui-space-16);
|
|
62
|
+
padding-right: var(--ui-space-32);
|
|
62
63
|
min-height: 2.375rem; /* ~38px to match button height */
|
|
63
|
-
background: var(--ui-surface-lowest)
|
|
64
|
-
border: 1px solid var(--ui-border)
|
|
64
|
+
background-color: var(--ui-surface-lowest);
|
|
65
|
+
border: 1px solid var(--ui-border);
|
|
65
66
|
border-radius: var(--ui-radius-md);
|
|
66
|
-
color: var(--ui-text-primary)
|
|
67
|
+
color: var(--ui-text-primary);
|
|
67
68
|
font-family: var(--ui-font-sans);
|
|
68
69
|
font-size: var(--ui-font-size-md);
|
|
69
70
|
font-weight: var(--ui-font-weight-normal);
|
|
@@ -71,28 +72,26 @@
|
|
|
71
72
|
vertical-align: middle;
|
|
72
73
|
cursor: pointer;
|
|
73
74
|
transition: all var(--ui-transition-fast);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
box-sizing: border-box !important;
|
|
75
|
+
overflow: visible;
|
|
76
|
+
box-sizing: border-box;
|
|
77
77
|
/* Reset browser defaults */
|
|
78
78
|
-webkit-appearance: none;
|
|
79
79
|
-moz-appearance: none;
|
|
80
80
|
appearance: none;
|
|
81
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")
|
|
83
|
-
background-repeat: no-repeat
|
|
84
|
-
background-position: right var(--ui-space-12) center
|
|
85
|
-
padding-right: var(--ui-space-32) !important;
|
|
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");
|
|
83
|
+
background-repeat: no-repeat;
|
|
84
|
+
background-position: right var(--ui-space-12) center;
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
.ui-form-select:hover:not(:disabled) {
|
|
89
|
-
background-color: var(--ui-surface-low)
|
|
90
|
-
border-color: var(--ui-border-high)
|
|
88
|
+
background-color: var(--ui-surface-low);
|
|
89
|
+
border-color: var(--ui-border-high);
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
.ui-form-select:focus {
|
|
94
93
|
outline: none;
|
|
95
|
-
border-color: var(--ui-border-higher)
|
|
94
|
+
border-color: var(--ui-border-higher);
|
|
96
95
|
box-shadow: 0 0 0 2px hsla(0, 58%, 50%, 0.2);
|
|
97
96
|
}
|
|
98
97
|
|
|
@@ -102,13 +101,13 @@
|
|
|
102
101
|
}
|
|
103
102
|
|
|
104
103
|
.ui-form-select:active:not(:disabled) {
|
|
105
|
-
background-color: var(--ui-surface)
|
|
104
|
+
background-color: var(--ui-surface);
|
|
106
105
|
}
|
|
107
106
|
|
|
108
107
|
.ui-form-select:disabled {
|
|
109
|
-
background-color: var(--ui-surface-lowest)
|
|
110
|
-
border-color: var(--ui-border-low)
|
|
111
|
-
color: var(--ui-text-disabled)
|
|
108
|
+
background-color: var(--ui-surface-lowest);
|
|
109
|
+
border-color: var(--ui-border-low);
|
|
110
|
+
color: var(--ui-text-disabled);
|
|
112
111
|
cursor: not-allowed;
|
|
113
112
|
}
|
|
114
113
|
|
|
@@ -116,8 +115,8 @@
|
|
|
116
115
|
/* Note: Most option styling is controlled by browser/OS and cannot be fully overridden */
|
|
117
116
|
/* These styles apply where browsers allow (limited support in Chrome/Firefox) */
|
|
118
117
|
.ui-form-select option {
|
|
119
|
-
background-color: var(--ui-surface-lowest)
|
|
120
|
-
color: var(--ui-text-primary)
|
|
118
|
+
background-color: var(--ui-surface-lowest);
|
|
119
|
+
color: var(--ui-text-primary);
|
|
121
120
|
padding: var(--ui-space-8) var(--ui-space-12);
|
|
122
121
|
min-height: 2rem;
|
|
123
122
|
font-size: var(--ui-font-size-md);
|
|
@@ -127,7 +126,7 @@
|
|
|
127
126
|
|
|
128
127
|
/* Disabled options */
|
|
129
128
|
.ui-form-select option:disabled {
|
|
130
|
-
color: var(--ui-text-disabled)
|
|
129
|
+
color: var(--ui-text-disabled);
|
|
131
130
|
}
|
|
132
131
|
|
|
133
132
|
/* Input Field Styling */
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
evalBezier, buildCurvePath, curveTemplates,
|
|
9
9
|
serializeCurve, deserializeCurve,
|
|
10
10
|
} from './curveEngine';
|
|
11
|
+
import UIPillButton from './UIPillButton.svelte';
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
13
14
|
anchors: CurveAnchor[];
|
|
@@ -257,8 +258,31 @@
|
|
|
257
258
|
<div class="curve-panel">
|
|
258
259
|
<div class="curve-panel-header">
|
|
259
260
|
<span class="curve-panel-label">{cfg.label}</span>
|
|
261
|
+
<div class="curve-help">
|
|
262
|
+
<button class="curve-help-badge" type="button" aria-label="Curve editor help">
|
|
263
|
+
<i class="fas fa-circle-info" aria-hidden="true"></i>
|
|
264
|
+
</button>
|
|
265
|
+
<div class="curve-help-popover" role="tooltip">
|
|
266
|
+
<div><strong>Click</strong> path to add a point</div>
|
|
267
|
+
<div><strong>⌥ Click</strong> a point to remove</div>
|
|
268
|
+
<div><strong>Double-click</strong> a point to toggle smooth/corner</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
260
271
|
</div>
|
|
261
|
-
<div class="curve-container"
|
|
272
|
+
<div class="curve-container">
|
|
273
|
+
<div class="curve-chart-overlay">
|
|
274
|
+
<UIPillButton
|
|
275
|
+
size="compact"
|
|
276
|
+
variant={shiftActive ? 'default' : 'outline'}
|
|
277
|
+
title="Vertical offset"
|
|
278
|
+
onclick={() => shiftActive = !shiftActive}
|
|
279
|
+
>
|
|
280
|
+
<svg viewBox="0 0 12 20" class="curve-tool-icon">
|
|
281
|
+
<path d="M6,2 L10,7 L7,7 L7,13 L10,13 L6,18 L2,13 L5,13 L5,7 L2,7 Z" />
|
|
282
|
+
</svg>
|
|
283
|
+
<span>Offset{offset !== 0 ? ` ${offset > 0 ? '+' : ''}${offset}` : ''}</span>
|
|
284
|
+
</UIPillButton>
|
|
285
|
+
</div>
|
|
262
286
|
<svg
|
|
263
287
|
bind:this={svgEl}
|
|
264
288
|
class="curve-svg"
|
|
@@ -383,22 +407,9 @@
|
|
|
383
407
|
</svg>
|
|
384
408
|
</div>
|
|
385
409
|
<div class="curve-toolbar">
|
|
386
|
-
<div class="curve-toolbar-
|
|
387
|
-
<
|
|
388
|
-
|
|
389
|
-
class:active={shiftActive}
|
|
390
|
-
type="button"
|
|
391
|
-
title="Vertical offset"
|
|
392
|
-
onclick={() => shiftActive = !shiftActive}
|
|
393
|
-
>
|
|
394
|
-
<svg viewBox="0 0 12 20" class="curve-tool-icon">
|
|
395
|
-
<path d="M6,2 L10,7 L7,7 L7,13 L10,13 L6,18 L2,13 L5,13 L5,7 L2,7 Z" />
|
|
396
|
-
</svg>
|
|
397
|
-
<span>Offset{offset !== 0 ? ` ${offset > 0 ? '+' : ''}${offset}` : ''}</span>
|
|
398
|
-
</button>
|
|
399
|
-
<span class="curve-hint">&x2325;-click to remove point</span>
|
|
400
|
-
<button class="curve-tool-btn" type="button" title="Copy curve" onclick={copyToClipboard}>Copy</button>
|
|
401
|
-
<button class="curve-tool-btn" type="button" title="Paste curve" onclick={pasteFromClipboard}>Paste</button>
|
|
410
|
+
<div class="curve-toolbar-group">
|
|
411
|
+
<UIPillButton size="compact" variant="outline" title="Copy curve" onclick={copyToClipboard}>Copy</UIPillButton>
|
|
412
|
+
<UIPillButton size="compact" variant="outline" title="Paste curve" onclick={pasteFromClipboard}>Paste</UIPillButton>
|
|
402
413
|
</div>
|
|
403
414
|
<div class="curve-templates">
|
|
404
415
|
{#each curveTemplates as tpl}
|
|
@@ -413,13 +424,12 @@
|
|
|
413
424
|
</svg>
|
|
414
425
|
</button>
|
|
415
426
|
{/each}
|
|
427
|
+
</div>
|
|
428
|
+
<div class="curve-toolbar-group">
|
|
416
429
|
{#if defaultAnchors}
|
|
417
|
-
<
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
title="Reset to default"
|
|
421
|
-
onclick={resetToDefault}
|
|
422
|
-
>Reset</button>
|
|
430
|
+
<UIPillButton size="compact" variant="outline" title="Reset to default" onclick={resetToDefault}>
|
|
431
|
+
Reset
|
|
432
|
+
</UIPillButton>
|
|
423
433
|
{/if}
|
|
424
434
|
</div>
|
|
425
435
|
</div>
|
|
@@ -435,7 +445,7 @@
|
|
|
435
445
|
.curve-panel-header {
|
|
436
446
|
display: flex;
|
|
437
447
|
align-items: center;
|
|
438
|
-
|
|
448
|
+
gap: var(--ui-space-6);
|
|
439
449
|
}
|
|
440
450
|
|
|
441
451
|
.curve-panel-label {
|
|
@@ -445,11 +455,89 @@
|
|
|
445
455
|
}
|
|
446
456
|
|
|
447
457
|
.curve-container {
|
|
458
|
+
position: relative;
|
|
448
459
|
width: 100%;
|
|
449
460
|
height: 250px;
|
|
450
461
|
box-sizing: border-box;
|
|
451
462
|
}
|
|
452
463
|
|
|
464
|
+
.curve-chart-overlay {
|
|
465
|
+
position: absolute;
|
|
466
|
+
inset: var(--ui-space-8);
|
|
467
|
+
display: flex;
|
|
468
|
+
align-items: flex-end;
|
|
469
|
+
pointer-events: none;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.curve-chart-overlay > :global(*) {
|
|
473
|
+
pointer-events: auto;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.curve-help {
|
|
477
|
+
position: relative;
|
|
478
|
+
margin-left: auto;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.curve-help-badge {
|
|
482
|
+
display: inline-flex;
|
|
483
|
+
align-items: center;
|
|
484
|
+
justify-content: center;
|
|
485
|
+
width: 1.25rem;
|
|
486
|
+
height: 1.25rem;
|
|
487
|
+
padding: 0;
|
|
488
|
+
border: none;
|
|
489
|
+
background: transparent;
|
|
490
|
+
color: var(--ui-text-muted);
|
|
491
|
+
cursor: help;
|
|
492
|
+
border-radius: var(--ui-radius-full);
|
|
493
|
+
transition: color var(--ui-transition-fast, 120ms ease);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.curve-help-badge:hover,
|
|
497
|
+
.curve-help-badge:focus-visible {
|
|
498
|
+
color: var(--ui-text-primary);
|
|
499
|
+
outline: none;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.curve-help-badge i {
|
|
503
|
+
font-size: var(--ui-font-size-md);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.curve-help-popover {
|
|
507
|
+
position: absolute;
|
|
508
|
+
top: calc(100% + var(--ui-space-4));
|
|
509
|
+
right: 0;
|
|
510
|
+
display: grid;
|
|
511
|
+
gap: var(--ui-space-4);
|
|
512
|
+
min-width: 14rem;
|
|
513
|
+
padding: var(--ui-space-8) var(--ui-space-12);
|
|
514
|
+
background: var(--ui-surface-highest);
|
|
515
|
+
border: 1px solid var(--ui-border-low);
|
|
516
|
+
border-radius: var(--ui-radius-sm);
|
|
517
|
+
color: var(--ui-text-secondary);
|
|
518
|
+
font-size: var(--ui-font-size-sm);
|
|
519
|
+
line-height: 1.4;
|
|
520
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
521
|
+
opacity: 0;
|
|
522
|
+
transform: translateY(-2px);
|
|
523
|
+
pointer-events: none;
|
|
524
|
+
transition:
|
|
525
|
+
opacity var(--ui-transition-fast, 120ms ease),
|
|
526
|
+
transform var(--ui-transition-fast, 120ms ease);
|
|
527
|
+
z-index: 2;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.curve-help-popover strong {
|
|
531
|
+
color: var(--ui-text-primary);
|
|
532
|
+
font-weight: var(--ui-font-weight-medium);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.curve-help:hover .curve-help-popover,
|
|
536
|
+
.curve-help:focus-within .curve-help-popover {
|
|
537
|
+
opacity: 1;
|
|
538
|
+
transform: translateY(0);
|
|
539
|
+
}
|
|
540
|
+
|
|
453
541
|
.curve-svg {
|
|
454
542
|
width: 100%;
|
|
455
543
|
height: 100%;
|
|
@@ -565,48 +653,17 @@
|
|
|
565
653
|
align-items: center;
|
|
566
654
|
justify-content: space-between;
|
|
567
655
|
flex-wrap: wrap;
|
|
568
|
-
gap: var(--ui-space-
|
|
656
|
+
gap: var(--ui-space-8);
|
|
569
657
|
padding-top: var(--ui-space-2);
|
|
570
658
|
}
|
|
571
659
|
|
|
572
|
-
.curve-toolbar-
|
|
660
|
+
.curve-toolbar-group {
|
|
573
661
|
display: flex;
|
|
574
662
|
align-items: center;
|
|
575
663
|
gap: var(--ui-space-4);
|
|
576
664
|
flex-wrap: wrap;
|
|
577
665
|
}
|
|
578
666
|
|
|
579
|
-
.curve-tool-btn {
|
|
580
|
-
display: flex;
|
|
581
|
-
align-items: center;
|
|
582
|
-
gap: var(--ui-space-4);
|
|
583
|
-
padding: var(--ui-space-2) var(--ui-space-6);
|
|
584
|
-
border: 1px solid var(--ui-border-low);
|
|
585
|
-
border-radius: var(--ui-radius-sm);
|
|
586
|
-
background: var(--ui-surface-lowest);
|
|
587
|
-
cursor: pointer;
|
|
588
|
-
color: var(--ui-text-muted);
|
|
589
|
-
font-size: var(--ui-font-size-md);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
.curve-tool-btn:hover {
|
|
593
|
-
border-color: var(--ui-border-high);
|
|
594
|
-
color: var(--ui-text-secondary);
|
|
595
|
-
background: var(--ui-surface-high);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
.curve-tool-btn.active {
|
|
599
|
-
border-color: var(--ui-border-high);
|
|
600
|
-
background: var(--ui-surface-highest);
|
|
601
|
-
color: var(--ui-text-primary);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
.curve-tool-btn:disabled {
|
|
605
|
-
opacity: 0.35;
|
|
606
|
-
cursor: default;
|
|
607
|
-
pointer-events: none;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
667
|
.curve-tool-icon {
|
|
611
668
|
width: 0.625rem;
|
|
612
669
|
height: 1rem;
|
|
@@ -616,12 +673,6 @@
|
|
|
616
673
|
fill: currentColor;
|
|
617
674
|
}
|
|
618
675
|
|
|
619
|
-
.curve-hint {
|
|
620
|
-
font-size: var(--ui-font-size-md);
|
|
621
|
-
color: var(--ui-text-muted);
|
|
622
|
-
opacity: 0.6;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
676
|
.curve-templates {
|
|
626
677
|
display: flex;
|
|
627
678
|
gap: var(--ui-space-2);
|