@motion-proto/live-tokens 0.56.1 → 0.57.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-pair-fonts/SKILL.md +2 -2
- package/CHANGELOG.md +124 -0
- package/README.md +1 -1
- package/bin/cli.mjs +2 -1
- package/dist-plugin/adjust/index.cjs +1 -0
- package/dist-plugin/adjust/index.d.cts +2 -2
- package/dist-plugin/adjust/index.d.ts +2 -2
- package/dist-plugin/adjust/index.js +1 -1
- package/dist-plugin/{chunk-TKZBVIW5.js → chunk-E5QYON4L.js} +59 -2
- package/dist-plugin/{chunk-PB2JTK2H.js → chunk-LW4SR7AZ.js} +39 -6
- package/dist-plugin/{chunk-YR6GPXW2.js → chunk-XWXIMTWZ.js} +0 -5
- package/dist-plugin/{chunk-D3ZVKOR4.js → chunk-Y5CNFSSV.js} +1 -0
- package/dist-plugin/{dataPaths-DBN0RPuT.d.cts → dataPaths-CRfD1LdA.d.cts} +3 -0
- package/dist-plugin/{dataPaths-DBN0RPuT.d.ts → dataPaths-CRfD1LdA.d.ts} +3 -0
- package/dist-plugin/fontPairing/index.cjs +4 -2
- package/dist-plugin/fontPairing/index.d.cts +3 -3
- package/dist-plugin/fontPairing/index.d.ts +3 -3
- package/dist-plugin/fontPairing/index.js +4 -3
- package/dist-plugin/generateColorsAndType/index.cjs +22 -9
- package/dist-plugin/generateColorsAndType/index.d.cts +2 -2
- package/dist-plugin/generateColorsAndType/index.d.ts +2 -2
- package/dist-plugin/generateColorsAndType/index.js +8 -5
- package/dist-plugin/index.cjs +158 -9
- package/dist-plugin/index.d.cts +1 -1
- package/dist-plugin/index.d.ts +1 -1
- package/dist-plugin/index.js +71 -4
- package/dist-plugin/migrateData/index.cjs +1 -0
- package/dist-plugin/migrateData/index.d.cts +1 -1
- package/dist-plugin/migrateData/index.d.ts +1 -1
- package/dist-plugin/migrateData/index.js +1 -1
- package/dist-plugin/{themeTypes-BAqtv4XO.d.cts → themeTypes-DSV3Zisf.d.cts} +12 -1
- package/dist-plugin/{themeTypes-BAqtv4XO.d.ts → themeTypes-DSV3Zisf.d.ts} +12 -1
- package/dist-plugin/tokensCssMigrations/index.cjs +58 -1
- package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
- package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
- package/dist-plugin/tokensCssMigrations/index.js +3 -3
- package/package.json +3 -1
- package/src/editor/core/fonts/applyFontPairing.ts +3 -2
- package/src/editor/core/fonts/fontLoader.ts +1 -0
- package/src/editor/core/fonts/fontMigration.ts +31 -1
- package/src/editor/core/palettes/paletteDerivation.ts +9 -2
- package/src/editor/core/sketch/maskField.ts +381 -0
- package/src/editor/core/sketch/sketchLayer.ts +1028 -0
- package/src/editor/core/sketch/sketchPresetService.ts +65 -0
- package/src/editor/core/sketch/sketchPresets.ts +328 -0
- package/src/editor/core/sketch/sketchStore.ts +190 -0
- package/src/editor/core/store/editorPersistence.ts +28 -12
- package/src/editor/core/store/editorTypes.ts +4 -1
- package/src/editor/core/store/editorViewStore.ts +2 -2
- package/src/editor/core/store/gradientSource.ts +15 -1
- package/src/editor/core/themes/parsers/gradient.ts +62 -4
- package/src/editor/core/themes/slices/gradients.ts +75 -14
- package/src/editor/core/themes/themeTypes.ts +6 -1
- package/src/editor/docs/Docs.svelte +4 -3
- package/src/editor/docs/chapters.ts +1 -0
- package/src/editor/docs/content/01-overview.md +2 -1
- package/src/editor/docs/content/editing-tokens.md +5 -1
- package/src/editor/docs/content/sketch-mode.md +89 -0
- package/src/editor/docs/content.generated.ts +3 -2
- package/src/editor/overlay/LiveEditorOverlay.svelte +15 -0
- package/src/editor/pages/EditorShell.svelte +43 -0
- package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
- package/src/editor/ui/FontStackEditor.svelte +3 -0
- package/src/editor/ui/GradientEditor.svelte +38 -1
- package/src/editor/ui/UIReveal.svelte +7 -1
- package/src/editor/ui/UISegmentedControl.svelte +4 -8
- package/src/editor/ui/sections/GradientsSection.svelte +47 -1
- package/src/editor/ui/sketch/SketchDial.svelte +124 -0
- package/src/editor/ui/sketch/SketchPreview.svelte +119 -0
- package/src/editor/ui/sketch/SketchRange.svelte +185 -0
- package/src/editor/ui/sketch/SketchTab.svelte +1263 -0
- package/src/live-tokens/data/colors-and-type/autumn.json +17 -0
- package/src/live-tokens/data/colors-and-type/default.json +17 -0
- package/src/live-tokens/data/colors-and-type/halloween.json +17 -0
- package/src/live-tokens/data/colors-and-type/midnight-study.json +17 -0
- package/src/live-tokens/data/colors-and-type/ocean.json +17 -0
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +17 -0
- package/src/live-tokens/data/colors-and-type/sketches.json +2520 -0
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +17 -0
- package/src/live-tokens/data/colors-and-type/sunset.json +17 -0
- package/src/live-tokens/data/themes/autumn.json +17 -0
- package/src/live-tokens/data/themes/halloween.json +17 -0
- package/src/live-tokens/data/themes/midnight-study.json +17 -0
- package/src/live-tokens/data/themes/ocean.json +17 -0
- package/src/live-tokens/data/themes/royal-velvet.json +17 -0
- package/src/live-tokens/data/themes/sketches.json +3984 -0
- package/src/live-tokens/data/themes/spring-meadow.json +17 -0
- package/src/live-tokens/data/themes/sunset.json +17 -0
- package/src/live-tokens/data/tokens.generated.css +1 -0
- package/src/system/styles/tokens.css +15 -2
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import ColorsTab from '../ui/colors/ColorsTab.svelte';
|
|
7
7
|
import EditorViewSwitcher from '../ui/EditorViewSwitcher.svelte';
|
|
8
8
|
import ComponentsTab from '../component-editor/scaffolding/ComponentsTab.svelte';
|
|
9
|
+
import SketchTab from '../ui/sketch/SketchTab.svelte';
|
|
9
10
|
import ThemePanel from '../ui/ThemePanel.svelte';
|
|
10
11
|
import { scrollSectionIntoView } from '../ui/scrollSection';
|
|
11
12
|
import { editorView, sidebarCondensed, selectedComponent } from '../core/store/editorViewStore';
|
|
@@ -31,11 +32,22 @@
|
|
|
31
32
|
{ id: 'colors-story', label: 'Story', icon: 'fas fa-layer-group' }
|
|
32
33
|
];
|
|
33
34
|
|
|
35
|
+
const sketchNavItems = [
|
|
36
|
+
{ id: 'sketch-border', label: 'Border', icon: 'fas fa-pen-nib' },
|
|
37
|
+
{ id: 'sketch-fill', label: 'Fill', icon: 'fas fa-fill-drip' },
|
|
38
|
+
{ id: 'sketch-shape', label: 'Shape', icon: 'fas fa-draw-polygon' },
|
|
39
|
+
{ id: 'sketch-icons', label: 'Icons', icon: 'fas fa-shapes' },
|
|
40
|
+
{ id: 'sketch-noise', label: 'Noise', icon: 'fas fa-wave-square' },
|
|
41
|
+
{ id: 'sketch-preview', label: 'Preview', icon: 'fas fa-eye' }
|
|
42
|
+
];
|
|
43
|
+
|
|
34
44
|
const allComponentNavItems = getComponentRegistryEntries().map(({ id, label, icon, origin }) => ({ id, label, icon, origin }));
|
|
35
45
|
const systemNavItems = allComponentNavItems.filter((i) => i.origin === 'system');
|
|
36
46
|
const customNavItems = allComponentNavItems.filter((i) => i.origin === 'custom');
|
|
37
47
|
|
|
38
48
|
let selectedTokenSection: string | null = $state(null);
|
|
49
|
+
/** Rebuilt per click so a panel can react to the same target twice. */
|
|
50
|
+
let sectionJump: { id: string } | null = $state(null);
|
|
39
51
|
|
|
40
52
|
// 'auto' = open by default. Auto-condensing on shellWidth caused a bounce
|
|
41
53
|
// as the overlay panel grew past the threshold mid-animation.
|
|
@@ -70,10 +82,19 @@
|
|
|
70
82
|
|
|
71
83
|
function scrollToSection(sectionId: string) {
|
|
72
84
|
selectedTokenSection = sectionId;
|
|
85
|
+
sectionJump = { id: sectionId };
|
|
73
86
|
const el = document.getElementById(sectionId);
|
|
74
87
|
if (el) scrollSectionIntoView(el);
|
|
75
88
|
}
|
|
76
89
|
|
|
90
|
+
/** The Sketch panel is an accordion, so it opens the target and scrolls once
|
|
91
|
+
the reveal has settled. Scrolling from here would read offsetTop before
|
|
92
|
+
the other sections have collapsed. */
|
|
93
|
+
function jumpToSketchSection(sectionId: string) {
|
|
94
|
+
selectedTokenSection = sectionId;
|
|
95
|
+
sectionJump = { id: sectionId };
|
|
96
|
+
}
|
|
97
|
+
|
|
77
98
|
function selectComponent(id: string) {
|
|
78
99
|
selectedComponent.set(id);
|
|
79
100
|
hideHint();
|
|
@@ -129,6 +150,26 @@
|
|
|
129
150
|
<ThemePanel />
|
|
130
151
|
</div>
|
|
131
152
|
{/if}
|
|
153
|
+
{:else if $editorView === 'sketch'}
|
|
154
|
+
<div class="nav-items">
|
|
155
|
+
{#each sketchNavItems as item (item.id)}
|
|
156
|
+
<button
|
|
157
|
+
class="nav-item"
|
|
158
|
+
class:active={selectedTokenSection === item.id}
|
|
159
|
+
onmouseenter={(e) => showHint(item.label, e.currentTarget)}
|
|
160
|
+
onmouseleave={hideHint}
|
|
161
|
+
onclick={() => jumpToSketchSection(item.id)}
|
|
162
|
+
>
|
|
163
|
+
<i class={item.icon}></i>
|
|
164
|
+
<span class="nav-label">{item.label}</span>
|
|
165
|
+
</button>
|
|
166
|
+
{/each}
|
|
167
|
+
</div>
|
|
168
|
+
{#if !condensed}
|
|
169
|
+
<div class="sidebar-footer">
|
|
170
|
+
<ThemePanel />
|
|
171
|
+
</div>
|
|
172
|
+
{/if}
|
|
132
173
|
{:else if $editorView === 'colors'}
|
|
133
174
|
<div class="nav-items">
|
|
134
175
|
{#each colorsNavItems as item (item.id)}
|
|
@@ -202,6 +243,8 @@
|
|
|
202
243
|
<VariablesTab />
|
|
203
244
|
{:else if $editorView === 'colors'}
|
|
204
245
|
<ColorsTab />
|
|
246
|
+
{:else if $editorView === 'sketch'}
|
|
247
|
+
<SketchTab {sectionJump} />
|
|
205
248
|
{:else}
|
|
206
249
|
<ComponentsTab selectedComponent={$selectedComponent} />
|
|
207
250
|
{/if}
|
|
@@ -18,19 +18,21 @@
|
|
|
18
18
|
let componentsDisabled = $derived($parentRoute === DEFAULT_COMPONENTS_PATH);
|
|
19
19
|
let colorsDisabled = $derived($parentRoute === DEFAULT_COLORS_PATH);
|
|
20
20
|
|
|
21
|
-
// Editing flow order: Tokens → Colors → Components. The condensed rail cycles
|
|
21
|
+
// Editing flow order: Tokens → Colors → Components → Sketch. The condensed rail cycles
|
|
22
22
|
// through these; a view is skipped while it's disabled (already on that
|
|
23
23
|
// page) so the cycle never lands on a dead view.
|
|
24
|
-
const CYCLE: readonly EditorView[] = ['tokens', 'colors', 'components'];
|
|
24
|
+
const CYCLE: readonly EditorView[] = ['tokens', 'colors', 'components', 'sketch'];
|
|
25
25
|
const ICONS: Record<EditorView, string> = {
|
|
26
26
|
tokens: 'fa-sliders',
|
|
27
27
|
colors: 'fa-palette',
|
|
28
28
|
components: 'fa-cubes',
|
|
29
|
+
sketch: 'fa-pen-nib',
|
|
29
30
|
};
|
|
30
31
|
const LABELS: Record<EditorView, string> = {
|
|
31
32
|
tokens: 'Tokens',
|
|
32
33
|
colors: 'Colors',
|
|
33
34
|
components: 'Components',
|
|
35
|
+
sketch: 'Sketch',
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
function set(v: EditorView) {
|
|
@@ -103,6 +105,17 @@
|
|
|
103
105
|
<span class="radio" aria-hidden="true"></span>
|
|
104
106
|
<span>Components</span>
|
|
105
107
|
</button>
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
role="tab"
|
|
111
|
+
class="seg-btn"
|
|
112
|
+
class:active={$editorView === 'sketch'}
|
|
113
|
+
aria-selected={$editorView === 'sketch'}
|
|
114
|
+
onclick={() => set('sketch')}
|
|
115
|
+
>
|
|
116
|
+
<span class="radio" aria-hidden="true"></span>
|
|
117
|
+
<span>Sketch</span>
|
|
118
|
+
</button>
|
|
106
119
|
</div>
|
|
107
120
|
</div>
|
|
108
121
|
{/if}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
'--font-sans',
|
|
26
26
|
'--font-serif',
|
|
27
27
|
'--font-mono',
|
|
28
|
+
'--font-editorial',
|
|
28
29
|
];
|
|
29
30
|
|
|
30
31
|
// Each stack's terminal fallback (bottom slot). It's locked to system or
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
'--font-sans': 'sans-serif',
|
|
36
37
|
'--font-serif': 'serif',
|
|
37
38
|
'--font-mono': 'monospace',
|
|
39
|
+
'--font-editorial': 'sans-serif',
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
// The single matching System UI preset paired with each stack — the only two
|
|
@@ -44,6 +46,7 @@
|
|
|
44
46
|
'--font-sans': 'system-ui-sans',
|
|
45
47
|
'--font-serif': 'system-ui-serif',
|
|
46
48
|
'--font-mono': 'system-ui-mono',
|
|
49
|
+
'--font-editorial': 'system-ui-sans',
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
let fontSourcesList = $derived($editorState.fonts.sources);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { tick, onMount } from 'svelte';
|
|
7
7
|
import { get } from 'svelte/store';
|
|
8
8
|
import type { GradientType, GradientTokenStop } from '../core/store/editorTypes';
|
|
9
|
+
import { LINEAR_DIRECTIONS, type LinearDirection } from '../core/themes/parsers/gradient';
|
|
9
10
|
import {
|
|
10
11
|
colorsAndTypeGradientSource,
|
|
11
12
|
snapshotGradient,
|
|
@@ -87,6 +88,11 @@
|
|
|
87
88
|
gradientSource.setAngle(detail.value);
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
function onDirectionChange(e: Event) {
|
|
92
|
+
const v = (e.currentTarget as HTMLSelectElement).value;
|
|
93
|
+
gradientSource.setDirection(v === '' ? undefined : (v as LinearDirection));
|
|
94
|
+
}
|
|
95
|
+
|
|
90
96
|
function onAspectChange(detail: { x: number; y: number }) {
|
|
91
97
|
gradientSource.setAspect(detail);
|
|
92
98
|
}
|
|
@@ -375,7 +381,26 @@
|
|
|
375
381
|
</div>
|
|
376
382
|
{:else if gradient.type === 'linear'}
|
|
377
383
|
<div class="ribbon-pad ribbon-pad-linear">
|
|
378
|
-
|
|
384
|
+
<!-- A direction keyword overrides the dial: CSS angles it off the box's
|
|
385
|
+
own diagonal, so the dial has no single value to show. -->
|
|
386
|
+
<AngleDial
|
|
387
|
+
value={gradient.angle}
|
|
388
|
+
size={64}
|
|
389
|
+
orientation="vertical"
|
|
390
|
+
label=""
|
|
391
|
+
onchange={onAngleChange}
|
|
392
|
+
/>
|
|
393
|
+
<select
|
|
394
|
+
class="direction-select"
|
|
395
|
+
aria-label="Gradient direction"
|
|
396
|
+
value={gradient.direction ?? ''}
|
|
397
|
+
onchange={onDirectionChange}
|
|
398
|
+
>
|
|
399
|
+
<option value="">{gradient.angle}deg</option>
|
|
400
|
+
{#each LINEAR_DIRECTIONS as d (d)}
|
|
401
|
+
<option value={d}>{d}</option>
|
|
402
|
+
{/each}
|
|
403
|
+
</select>
|
|
379
404
|
</div>
|
|
380
405
|
{/if}
|
|
381
406
|
|
|
@@ -440,6 +465,18 @@
|
|
|
440
465
|
{/if}
|
|
441
466
|
|
|
442
467
|
<style>
|
|
468
|
+
.direction-select {
|
|
469
|
+
margin-top: var(--ui-space-2, 8px);
|
|
470
|
+
max-width: 8.5rem;
|
|
471
|
+
font: inherit;
|
|
472
|
+
font-size: 0.75rem;
|
|
473
|
+
color: var(--ui-gray-1000, #eee);
|
|
474
|
+
background: var(--ui-gray-300, #212121);
|
|
475
|
+
border: 1px solid var(--ui-gray-450, #3a3a3a);
|
|
476
|
+
border-radius: 4px;
|
|
477
|
+
padding: 2px 4px;
|
|
478
|
+
}
|
|
479
|
+
|
|
443
480
|
/* Header labels share grid tracks with the ribbon + pad below them. */
|
|
444
481
|
.gradient-editor {
|
|
445
482
|
display: grid;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
/** Shared so a caller that must wait for the pane to settle — scrolling to a
|
|
3
|
+
section it just opened, say — does not hard-code the number. */
|
|
4
|
+
export const REVEAL_MS = 260;
|
|
5
|
+
</script>
|
|
6
|
+
|
|
1
7
|
<script lang="ts">
|
|
2
8
|
import type { Snippet } from 'svelte';
|
|
3
9
|
import { slide } from 'svelte/transition';
|
|
@@ -22,7 +28,7 @@
|
|
|
22
28
|
children: Snippet;
|
|
23
29
|
}
|
|
24
30
|
|
|
25
|
-
let { open, style = undefined, duration =
|
|
31
|
+
let { open, style = undefined, duration = REVEAL_MS, children }: Props = $props();
|
|
26
32
|
|
|
27
33
|
const reduceMotion = typeof window !== 'undefined'
|
|
28
34
|
&& window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
align-items: center;
|
|
66
66
|
gap: var(--ui-space-6, 6px);
|
|
67
67
|
padding: var(--ui-space-4, 4px) var(--ui-space-12, 12px);
|
|
68
|
-
color: var(--ui-text-
|
|
68
|
+
color: var(--ui-text-primary, #fff);
|
|
69
69
|
font-family: inherit;
|
|
70
70
|
font-size: var(--ui-font-size-sm, 14px);
|
|
71
71
|
font-weight: var(--ui-font-weight-medium, 500);
|
|
@@ -92,12 +92,12 @@
|
|
|
92
92
|
|
|
93
93
|
.ui-seg-item:hover:not(.active) {
|
|
94
94
|
background: rgba(255, 255, 255, 0.06);
|
|
95
|
-
color: var(--ui-text-primary, #fff);
|
|
96
95
|
}
|
|
97
96
|
|
|
97
|
+
/* Every label sits at full brightness, so the pill fill is the only thing
|
|
98
|
+
carrying selection. It has to stay clearly lighter than a hover. */
|
|
98
99
|
.ui-seg-item.active {
|
|
99
|
-
background: linear-gradient(180deg, rgba(255, 255, 255, 0.
|
|
100
|
-
color: var(--ui-text-primary, #fff);
|
|
100
|
+
background: linear-gradient(180deg, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0.3) 100%);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
.ui-seg-item:has(input:focus-visible) {
|
|
@@ -106,9 +106,5 @@
|
|
|
106
106
|
|
|
107
107
|
.ui-seg-item i {
|
|
108
108
|
font-size: var(--ui-font-size-xs, 12px);
|
|
109
|
-
color: rgba(255, 255, 255, 0.65);
|
|
110
|
-
}
|
|
111
|
-
.ui-seg-item.active i {
|
|
112
|
-
color: rgba(255, 255, 255, 0.85);
|
|
113
109
|
}
|
|
114
110
|
</style>
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { editorState } from '../../core/store/editorStore';
|
|
11
11
|
import GradientEditor from '../GradientEditor.svelte';
|
|
12
|
+
import UIPillButton from '../UIPillButton.svelte';
|
|
13
|
+
import {
|
|
14
|
+
addGradientToken,
|
|
15
|
+
removeGradientToken,
|
|
16
|
+
nextGradientSlot,
|
|
17
|
+
} from '../../core/themes/slices/gradients';
|
|
12
18
|
|
|
13
19
|
interface Props {
|
|
14
20
|
copiedVar?: string | null;
|
|
@@ -20,11 +26,37 @@
|
|
|
20
26
|
function copy(v: string) { oncopy?.(v); }
|
|
21
27
|
|
|
22
28
|
let editingGradient: string | null = $state(null);
|
|
29
|
+
|
|
30
|
+
/** A new slot seeds from the library's last entry, so it lands as a visible
|
|
31
|
+
* gradient the user edits rather than an invisible transparent one. */
|
|
32
|
+
function addGradient() {
|
|
33
|
+
const tokens = $editorState.gradients.tokens;
|
|
34
|
+
const seed = tokens[tokens.length - 1];
|
|
35
|
+
const variable = nextGradientSlot(tokens);
|
|
36
|
+
addGradientToken({
|
|
37
|
+
variable,
|
|
38
|
+
type: 'linear',
|
|
39
|
+
angle: seed?.angle ?? 90,
|
|
40
|
+
...(seed?.direction ? { direction: seed.direction } : {}),
|
|
41
|
+
stops: seed
|
|
42
|
+
? seed.stops.map((st) => ({ ...st }))
|
|
43
|
+
: [
|
|
44
|
+
{ position: 0, color: '--color-brand-500' },
|
|
45
|
+
{ position: 100, color: '--color-accent-500' },
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
editingGradient = variable;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function removeGradient(variable: string) {
|
|
52
|
+
if (editingGradient === variable) editingGradient = null;
|
|
53
|
+
removeGradientToken(variable);
|
|
54
|
+
}
|
|
23
55
|
</script>
|
|
24
56
|
|
|
25
57
|
<section class="section" id="gradients">
|
|
26
58
|
<h2 class="section-title">Gradients</h2>
|
|
27
|
-
<p class="editor-intro">Each stop references a color token, so palette edits flow through. Add or remove stops; switch between linear and radial.</p>
|
|
59
|
+
<p class="editor-intro">Each stop references a color token, so palette edits flow through. Add or remove stops; switch between linear and radial; point a gradient at an angle or at a corner.</p>
|
|
28
60
|
<div class="gradients-grid">
|
|
29
61
|
{#each $editorState.gradients.tokens as token (token.variable)}
|
|
30
62
|
{@const isEditing = editingGradient === token.variable}
|
|
@@ -36,6 +68,13 @@
|
|
|
36
68
|
<button class="token-variable copyable" class:copied={copiedVar === token.variable} onclick={() => copy(token.variable)}>{copiedVar === token.variable ? 'copied!' : token.variable}</button>
|
|
37
69
|
{#if !isEditing}
|
|
38
70
|
<button class="gradient-edit-btn" onclick={() => editingGradient = token.variable}>Edit</button>
|
|
71
|
+
{#if $editorState.gradients.tokens.length > 1}
|
|
72
|
+
<button
|
|
73
|
+
class="gradient-edit-btn"
|
|
74
|
+
aria-label={`Remove ${token.variable}`}
|
|
75
|
+
onclick={() => removeGradient(token.variable)}
|
|
76
|
+
>Remove</button>
|
|
77
|
+
{/if}
|
|
39
78
|
{/if}
|
|
40
79
|
</div>
|
|
41
80
|
{#if isEditing}
|
|
@@ -50,9 +89,16 @@
|
|
|
50
89
|
</div>
|
|
51
90
|
{/each}
|
|
52
91
|
</div>
|
|
92
|
+
<div class="gradients-actions">
|
|
93
|
+
<UIPillButton onclick={addGradient}>Add gradient</UIPillButton>
|
|
94
|
+
</div>
|
|
53
95
|
</section>
|
|
54
96
|
|
|
55
97
|
<style>
|
|
98
|
+
.gradients-actions {
|
|
99
|
+
display: flex;
|
|
100
|
+
}
|
|
101
|
+
|
|
56
102
|
.section {
|
|
57
103
|
display: flex;
|
|
58
104
|
flex-direction: column;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
label: string;
|
|
4
|
+
value: number;
|
|
5
|
+
min: number;
|
|
6
|
+
max: number;
|
|
7
|
+
step: number;
|
|
8
|
+
readout: string;
|
|
9
|
+
hint: string;
|
|
10
|
+
/** Captions under the two ends of the track, for a dial with no natural unit. */
|
|
11
|
+
ends?: [string, string];
|
|
12
|
+
onchange: (value: number) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { label, value, min, max, step, readout, hint, ends, onchange }: Props = $props();
|
|
16
|
+
|
|
17
|
+
let filled = $derived(`${((value - min) / (max - min)) * 100}%`);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<label class="dial" data-hint={hint} style:--filled={filled}>
|
|
21
|
+
<span class="dial-label">{label}</span>
|
|
22
|
+
<span class="dial-value">{readout}</span>
|
|
23
|
+
<input
|
|
24
|
+
type="range"
|
|
25
|
+
{min}
|
|
26
|
+
{max}
|
|
27
|
+
{step}
|
|
28
|
+
{value}
|
|
29
|
+
oninput={(e) => onchange(+e.currentTarget.value)}
|
|
30
|
+
/>
|
|
31
|
+
{#if ends}
|
|
32
|
+
<span class="ends"><span>{ends[0]}</span><span>{ends[1]}</span></span>
|
|
33
|
+
{/if}
|
|
34
|
+
</label>
|
|
35
|
+
|
|
36
|
+
<style>
|
|
37
|
+
.dial {
|
|
38
|
+
position: relative;
|
|
39
|
+
display: grid;
|
|
40
|
+
grid-template-columns: 1fr auto;
|
|
41
|
+
gap: 0 var(--ui-space-12);
|
|
42
|
+
align-items: baseline;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.dial-label {
|
|
46
|
+
font-size: var(--ui-font-size-md);
|
|
47
|
+
color: var(--ui-text-primary);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.dial-value {
|
|
51
|
+
font-size: var(--ui-font-size-md);
|
|
52
|
+
font-family: var(--ui-font-mono);
|
|
53
|
+
color: var(--ui-text-primary);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Painted rather than left to accent-color: the browser's own range chrome
|
|
57
|
+
renders the track and thumb too dark to read against --ui-surface-low. */
|
|
58
|
+
input[type='range'] {
|
|
59
|
+
grid-column: 1 / -1;
|
|
60
|
+
width: 100%;
|
|
61
|
+
margin-top: var(--ui-space-2);
|
|
62
|
+
height: var(--ui-space-16);
|
|
63
|
+
-webkit-appearance: none;
|
|
64
|
+
appearance: none;
|
|
65
|
+
background:
|
|
66
|
+
linear-gradient(
|
|
67
|
+
to right,
|
|
68
|
+
var(--ui-text-primary) var(--filled),
|
|
69
|
+
var(--ui-border-high) var(--filled)
|
|
70
|
+
)
|
|
71
|
+
center / 100% 4px no-repeat;
|
|
72
|
+
cursor: ew-resize;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
input[type='range']::-webkit-slider-thumb {
|
|
76
|
+
-webkit-appearance: none;
|
|
77
|
+
width: var(--ui-space-16);
|
|
78
|
+
height: var(--ui-space-16);
|
|
79
|
+
border-radius: var(--ui-radius-full);
|
|
80
|
+
background: var(--ui-text-primary);
|
|
81
|
+
box-shadow: var(--ui-shadow-sm);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
input[type='range']::-moz-range-thumb {
|
|
85
|
+
width: var(--ui-space-16);
|
|
86
|
+
height: var(--ui-space-16);
|
|
87
|
+
border: none;
|
|
88
|
+
border-radius: var(--ui-radius-full);
|
|
89
|
+
background: var(--ui-text-primary);
|
|
90
|
+
box-shadow: var(--ui-shadow-sm);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.ends {
|
|
94
|
+
grid-column: 1 / -1;
|
|
95
|
+
display: flex;
|
|
96
|
+
justify-content: space-between;
|
|
97
|
+
font-size: var(--ui-font-size-xs);
|
|
98
|
+
color: var(--ui-text-tertiary);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
input[type='range']:focus-visible {
|
|
102
|
+
outline: 2px solid var(--ui-highlight);
|
|
103
|
+
outline-offset: 4px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Absolutely positioned so a hint never reflows the stack of dials under it. */
|
|
107
|
+
.dial:hover::after {
|
|
108
|
+
content: attr(data-hint);
|
|
109
|
+
position: absolute;
|
|
110
|
+
top: calc(100% + var(--ui-space-2));
|
|
111
|
+
left: 0;
|
|
112
|
+
right: 0;
|
|
113
|
+
z-index: 2;
|
|
114
|
+
padding: var(--ui-space-8) var(--ui-space-10);
|
|
115
|
+
background: var(--ui-surface-high);
|
|
116
|
+
border: 1px solid var(--ui-border-low);
|
|
117
|
+
border-radius: var(--ui-radius-md);
|
|
118
|
+
box-shadow: var(--ui-shadow-md);
|
|
119
|
+
color: var(--ui-text-secondary);
|
|
120
|
+
font-size: var(--ui-font-size-sm);
|
|
121
|
+
line-height: var(--ui-line-height-relaxed);
|
|
122
|
+
pointer-events: none;
|
|
123
|
+
}
|
|
124
|
+
</style>
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from '../../../system/components/Button.svelte';
|
|
3
|
+
import Card from '../../../system/components/Card.svelte';
|
|
4
|
+
import Input from '../../../system/components/Input.svelte';
|
|
5
|
+
import Badge from '../../../system/components/Badge.svelte';
|
|
6
|
+
import Callout from '../../../system/components/Callout.svelte';
|
|
7
|
+
import Toggle from '../../../system/components/Toggle.svelte';
|
|
8
|
+
import { setSketchScope } from '../../core/sketch/sketchLayer';
|
|
9
|
+
import type { SketchSettings } from '../../core/sketch/sketchPresets';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
settings: SketchSettings;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { settings, enabled }: Props = $props();
|
|
17
|
+
|
|
18
|
+
let stage = $state<HTMLDivElement | undefined>(undefined);
|
|
19
|
+
|
|
20
|
+
// The scope attribute goes on this container rather than the editor document
|
|
21
|
+
// root, so the editor's own chrome never picks the effect up.
|
|
22
|
+
$effect(() => {
|
|
23
|
+
setSketchScope(stage ?? null, enabled ? settings : null);
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<div class="stage" bind:this={stage}>
|
|
28
|
+
<section class="item">
|
|
29
|
+
<h4>Buttons</h4>
|
|
30
|
+
<div class="cluster">
|
|
31
|
+
<Button variant="primary">Primary</Button>
|
|
32
|
+
<Button variant="secondary">Secondary</Button>
|
|
33
|
+
<Button variant="outline">Outline</Button>
|
|
34
|
+
<Button variant="danger">Danger</Button>
|
|
35
|
+
</div>
|
|
36
|
+
</section>
|
|
37
|
+
|
|
38
|
+
<section class="item">
|
|
39
|
+
<h4>Cards</h4>
|
|
40
|
+
<div class="cluster">
|
|
41
|
+
<Card title="Rough draft">
|
|
42
|
+
<p>Fill and outline displace on different noise seeds, so they disagree at the edges.</p>
|
|
43
|
+
</Card>
|
|
44
|
+
<Card title="Second card">
|
|
45
|
+
<p>Same size and same preset. A different seed keeps the silhouette from repeating.</p>
|
|
46
|
+
</Card>
|
|
47
|
+
</div>
|
|
48
|
+
</section>
|
|
49
|
+
|
|
50
|
+
<section class="item">
|
|
51
|
+
<h4>Badges</h4>
|
|
52
|
+
<div class="cluster">
|
|
53
|
+
<Badge variant="info">Info</Badge>
|
|
54
|
+
<Badge variant="success">Success</Badge>
|
|
55
|
+
<Badge variant="warning">Warning</Badge>
|
|
56
|
+
<Badge variant="danger">Danger</Badge>
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<section class="item">
|
|
61
|
+
<h4>Form</h4>
|
|
62
|
+
<div class="cluster stack">
|
|
63
|
+
<Input label="Name" placeholder="Ada Lovelace" hint="Helper text stays crisp" />
|
|
64
|
+
<Toggle label="Enabled" checked />
|
|
65
|
+
</div>
|
|
66
|
+
</section>
|
|
67
|
+
|
|
68
|
+
<section class="item">
|
|
69
|
+
<h4>Callouts</h4>
|
|
70
|
+
<div class="cluster stack">
|
|
71
|
+
<Callout variant="info" label="Note">Body text is never filtered, so it stays fully legible.</Callout>
|
|
72
|
+
<Callout variant="warning" label="Careful">Only the fill and outline layers move.</Callout>
|
|
73
|
+
</div>
|
|
74
|
+
</section>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<style>
|
|
78
|
+
.stage {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
gap: var(--ui-space-24);
|
|
82
|
+
padding: var(--ui-space-24);
|
|
83
|
+
background: var(--surface-neutral-lowest);
|
|
84
|
+
border: 1px solid var(--ui-border-low);
|
|
85
|
+
border-radius: var(--ui-radius-md);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.item {
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
gap: var(--ui-space-10);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
h4 {
|
|
95
|
+
margin: 0;
|
|
96
|
+
font-family: var(--ui-font-sans);
|
|
97
|
+
font-size: var(--ui-font-size-xs);
|
|
98
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
99
|
+
letter-spacing: 0.08em;
|
|
100
|
+
text-transform: uppercase;
|
|
101
|
+
color: var(--ui-text-muted);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.cluster {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-wrap: wrap;
|
|
107
|
+
gap: var(--ui-space-16);
|
|
108
|
+
align-items: flex-start;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.cluster.stack {
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
max-width: 28rem;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.cluster :global(.card) {
|
|
117
|
+
max-width: 20rem;
|
|
118
|
+
}
|
|
119
|
+
</style>
|