@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,15 +1,11 @@
|
|
|
1
1
|
<script module lang="ts">
|
|
2
2
|
export type AnchorSide = 'top' | 'right' | 'bottom' | 'left';
|
|
3
3
|
|
|
4
|
-
/** Which visual property of the central box this tag drives. */
|
|
5
4
|
export type TagControl = 'surface' | 'radius' | 'border-color' | 'border-width' | 'font-family';
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
|
-
* Where the kite string ties to the
|
|
9
|
-
*
|
|
10
|
-
* A corner is just `pos: 0` or `pos: 100` on an adjacent edge.
|
|
11
|
-
* - Inside anchor: a point on the box surface, expressed as % of the
|
|
12
|
-
* box's own footprint (x: 0=left edge, 100=right edge).
|
|
7
|
+
* Where the kite string ties to the box. `inside` anchors are % of the
|
|
8
|
+
* box's footprint; edge anchors are `pos` 0..100 along the named side.
|
|
13
9
|
*/
|
|
14
10
|
export type Anchor =
|
|
15
11
|
| { side: AnchorSide; pos: number }
|
|
@@ -17,32 +13,30 @@
|
|
|
17
13
|
|
|
18
14
|
export interface FloatingTag {
|
|
19
15
|
icon?: string;
|
|
20
|
-
/**
|
|
16
|
+
/** Initial value; typically a CSS-variable name. */
|
|
21
17
|
label: string;
|
|
22
18
|
/** Tag center, in % of the stage. */
|
|
23
19
|
top: number;
|
|
24
20
|
left: number;
|
|
25
21
|
/** Bob delay, seconds. Negative values offset the start. */
|
|
26
22
|
delay?: number;
|
|
27
|
-
/** Static tilt
|
|
23
|
+
/** Static tilt, degrees. */
|
|
28
24
|
rotate?: number;
|
|
29
|
-
/** Where this tag's string ties to the central box. */
|
|
30
25
|
anchor: Anchor;
|
|
31
|
-
/**
|
|
26
|
+
/** Visual property of the central box this tag drives. */
|
|
32
27
|
controls?: TagControl;
|
|
28
|
+
/** `top` chip sits above the box and opens down; `bottom` mirrors. */
|
|
29
|
+
placement?: 'top' | 'bottom';
|
|
30
|
+
/** `right-cap` pivots the tilt around the chip's right cap. */
|
|
31
|
+
pivot?: 'center' | 'right-cap';
|
|
33
32
|
}
|
|
34
33
|
</script>
|
|
35
34
|
|
|
36
35
|
<script lang="ts">
|
|
37
36
|
import MenuSelect from './MenuSelect.svelte';
|
|
38
37
|
import { SvelteMap } from 'svelte/reactivity';
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// with a self-contained `.ftt-tag` element (not the Badge component) so the
|
|
42
|
-
// demo stays visually stable while the user edits badge-* tokens. The one
|
|
43
|
-
// exception is the dropdown panel — it renders through MenuSelect on
|
|
44
|
-
// purpose, so editing the menuselect-* tokens reshapes the in-flight UI.
|
|
45
|
-
// See FloatingTokenTags.css.
|
|
38
|
+
// `.ftt-tag` is hand-rolled (not Badge) so editing badge-* tokens doesn't
|
|
39
|
+
// repaint the playground. The dropdown uses MenuSelect on purpose.
|
|
46
40
|
import './FloatingTokenTags.css';
|
|
47
41
|
|
|
48
42
|
interface Props {
|
|
@@ -50,7 +44,6 @@
|
|
|
50
44
|
duration?: number;
|
|
51
45
|
distance?: number;
|
|
52
46
|
boxSize?: { w: number; h: number };
|
|
53
|
-
/** Auto-cycle through values when idle. */
|
|
54
47
|
autoplay?: boolean;
|
|
55
48
|
}
|
|
56
49
|
|
|
@@ -62,58 +55,58 @@
|
|
|
62
55
|
'font-family': 'Font family',
|
|
63
56
|
};
|
|
64
57
|
|
|
65
|
-
//
|
|
66
|
-
//
|
|
58
|
+
// Surfaces use the `-high` step and borders the `-strong` step so the box
|
|
59
|
+
// reads as the figure against the dark canvas. Picks span the hue wheel;
|
|
60
|
+
// `special` is too close to the background to pull weight here.
|
|
67
61
|
const valueOptions: Record<TagControl, string[]> = {
|
|
68
|
-
'surface': ['--surface-brand-
|
|
69
|
-
'radius': ['--radius-none', '--radius-lg', '--radius-2xl',
|
|
70
|
-
'border-color': ['--border-brand',
|
|
71
|
-
'border-width': ['--border-width-1', '--border-width-2', '--border-width-3',
|
|
72
|
-
'font-family': ['--font-display', '--font-sans', '--font-serif',
|
|
62
|
+
'surface': ['--surface-brand-high', '--surface-accent-high', '--surface-success-high', '--surface-info-high'],
|
|
63
|
+
'radius': ['--radius-none', '--radius-lg', '--radius-2xl', '--radius-full'],
|
|
64
|
+
'border-color': ['--border-brand-strong', '--border-accent-strong','--border-success-strong','--border-info-strong'],
|
|
65
|
+
'border-width': ['--border-width-1', '--border-width-2', '--border-width-3', '--border-width-5'],
|
|
66
|
+
'font-family': ['--font-display', '--font-sans', '--font-serif', '--font-mono'],
|
|
73
67
|
};
|
|
74
68
|
|
|
75
69
|
const defaultTags: FloatingTag[] = [
|
|
76
|
-
// Layout: surface + border-color flank the box at mid-height (far sides);
|
|
77
|
-
// font + corner-radius sit above near the box; border-width sits below
|
|
78
|
-
// centred. Roughly mirrors the user's sketch.
|
|
79
|
-
// Tags spread outward (factor 1.25 from box centre) so the cluster fills
|
|
80
|
-
// the available stage. Kite anchors are computed each frame against the
|
|
81
|
-
// box's *measured* rect, so the central element can size itself like a
|
|
82
|
-
// normal div (intrinsic to content + padding) and the strings still land.
|
|
83
70
|
{
|
|
84
71
|
icon: 'fas fa-fill-drip',
|
|
85
|
-
label: '--surface-brand-
|
|
86
|
-
top:
|
|
87
|
-
anchor: { side: 'inside', x: 10, y:
|
|
72
|
+
label: '--surface-brand-high',
|
|
73
|
+
top: 15, left: 20, delay: 0, rotate: 3,
|
|
74
|
+
anchor: { side: 'inside', x: 10, y: 40 },
|
|
88
75
|
controls: 'surface',
|
|
76
|
+
placement: 'top',
|
|
77
|
+
pivot: 'right-cap',
|
|
89
78
|
},
|
|
90
79
|
{
|
|
91
80
|
icon: 'fas fa-font',
|
|
92
81
|
label: '--font-display',
|
|
93
|
-
top:
|
|
94
|
-
anchor: { side: 'inside', x:
|
|
82
|
+
top: 5, left: 45, delay: -0.9, rotate: 2,
|
|
83
|
+
anchor: { side: 'inside', x: 78, y: 28 },
|
|
95
84
|
controls: 'font-family',
|
|
85
|
+
placement: 'top',
|
|
96
86
|
},
|
|
97
87
|
{
|
|
98
88
|
icon: 'fa-solid fa-bezier-curve',
|
|
99
89
|
label: '--radius-2xl',
|
|
100
|
-
top:
|
|
90
|
+
top: 23, left: 75, delay: -1.8, rotate: 2,
|
|
101
91
|
anchor: { side: 'top', pos: 100 },
|
|
102
92
|
controls: 'radius',
|
|
93
|
+
placement: 'top',
|
|
103
94
|
},
|
|
104
95
|
{
|
|
105
96
|
icon: 'fas fa-paint-roller',
|
|
106
|
-
label: '--border-brand',
|
|
107
|
-
top:
|
|
108
|
-
anchor: { side: '
|
|
97
|
+
label: '--border-brand-strong',
|
|
98
|
+
top: 79, left: 72, delay: -3.6, rotate: -2,
|
|
99
|
+
anchor: { side: 'bottom', pos: 75 },
|
|
109
100
|
controls: 'border-color',
|
|
101
|
+
placement: 'top',
|
|
110
102
|
},
|
|
111
103
|
{
|
|
112
104
|
icon: 'fas fa-grip-lines',
|
|
113
105
|
label: '--border-width-3',
|
|
114
|
-
top:
|
|
115
|
-
anchor: { side: 'bottom', pos:
|
|
106
|
+
top: 79, left: 28, delay: -5.2, rotate: -4,
|
|
107
|
+
anchor: { side: 'bottom', pos: 25 },
|
|
116
108
|
controls: 'border-width',
|
|
109
|
+
placement: 'top',
|
|
117
110
|
},
|
|
118
111
|
];
|
|
119
112
|
|
|
@@ -125,13 +118,9 @@
|
|
|
125
118
|
autoplay = true,
|
|
126
119
|
}: Props = $props();
|
|
127
120
|
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
// without losing user picks, and avoids capturing only the initial `tags`.
|
|
132
|
-
// Two override layers, deliberately desynchronised: `overrides` drives the
|
|
133
|
-
// floating tag's badge label (commits at selection); `boxOverrides` drives
|
|
134
|
-
// the central component's style (commits only when the energy ball lands).
|
|
121
|
+
// Two override layers, deliberately desynchronised: `overrides` commits at
|
|
122
|
+
// selection (drives the tag label); `boxOverrides` commits at impact
|
|
123
|
+
// (drives the box's style) so the box swaps in sync with the bloop.
|
|
135
124
|
const defaultLabels = $derived(tags.map(t => t.label));
|
|
136
125
|
let overrides = $state<Record<number, string>>({});
|
|
137
126
|
let boxOverrides = $state<Record<number, string>>({});
|
|
@@ -143,8 +132,6 @@
|
|
|
143
132
|
let flashingIdx = $state<number | null>(null);
|
|
144
133
|
let bloopActive = $state(false);
|
|
145
134
|
|
|
146
|
-
// Drag state — per-tag overrides for {top, left} in stage % space. Click vs
|
|
147
|
-
// drag is distinguished by a small pixel threshold on pointer movement.
|
|
148
135
|
let dragOverrides = $state<Record<number, { top: number; left: number }>>({});
|
|
149
136
|
let draggingIdx = $state<number | null>(null);
|
|
150
137
|
const DRAG_THRESHOLD_PX = 4;
|
|
@@ -153,28 +140,17 @@
|
|
|
153
140
|
function tagTop(i: number): number { return dragOverrides[i]?.top ?? tags[i].top; }
|
|
154
141
|
function tagLeft(i: number): number { return dragOverrides[i]?.left ?? tags[i].left; }
|
|
155
142
|
|
|
156
|
-
// Energy balls are imperative — keyed by tag index. Updated each rAF tick.
|
|
157
|
-
// SvelteMap so the template can react to in-flight state (line glow).
|
|
158
|
-
// `pendingValue` is the token swap that commits on impact, not on launch —
|
|
159
|
-
// so the box visibly changes at the moment of the bloop.
|
|
160
143
|
type BallState = { startedAt: number; duration: number; pendingValue: string };
|
|
161
144
|
const ballStates = new SvelteMap<number, BallState>();
|
|
162
|
-
//
|
|
163
|
-
// `bind:this={ballEls[i]}` etc. a reactive binding target. They're only
|
|
164
|
-
// read imperatively from the rAF loop, so there's no extra reactivity cost.
|
|
145
|
+
// `$state` so `bind:this={ballEls[i]}` is a reactive binding target.
|
|
165
146
|
const ballEls: HTMLSpanElement[] = $state([]);
|
|
166
147
|
|
|
167
|
-
// --- Element refs --------------------------------------------------------
|
|
168
148
|
let stageEl: HTMLDivElement | undefined = $state();
|
|
169
149
|
let boxEl: HTMLDivElement | undefined = $state();
|
|
170
150
|
const tagEls: HTMLSpanElement[] = $state([]);
|
|
171
151
|
const lineEls: SVGLineElement[] = $state([]);
|
|
172
152
|
const knotEls: HTMLSpanElement[] = $state([]);
|
|
173
153
|
|
|
174
|
-
// --- Anchor math ---------------------------------------------------------
|
|
175
|
-
// `cx`/`cy`/`w`/`h` are the box's centre and dimensions in stage-% space.
|
|
176
|
-
// Defaults fall back to `boxSize` for the first paint (before syncFrame
|
|
177
|
-
// measures the actual box). Runtime calls in syncFrame pass the live rect.
|
|
178
154
|
function anchorPoint(
|
|
179
155
|
anchor: Anchor,
|
|
180
156
|
cx = 50,
|
|
@@ -198,9 +174,7 @@
|
|
|
198
174
|
}
|
|
199
175
|
}
|
|
200
176
|
|
|
201
|
-
//
|
|
202
|
-
// `currentValues`) so the central component only repaints once the ball
|
|
203
|
-
// commits its payload at impact.
|
|
177
|
+
// Reads `boxValues` (not `currentValues`) so the box repaints at impact.
|
|
204
178
|
function resolveControl(name: TagControl): string | undefined {
|
|
205
179
|
const idx = tags.findIndex(t => t.controls === name);
|
|
206
180
|
return idx >= 0 ? boxValues[idx] : undefined;
|
|
@@ -215,14 +189,13 @@
|
|
|
215
189
|
return name ? `var(${name})` : undefined;
|
|
216
190
|
}
|
|
217
191
|
|
|
218
|
-
//
|
|
192
|
+
// Kite strings and energy balls are recomputed each frame from the box's
|
|
193
|
+
// measured rect so intrinsic sizing drives anchor placement.
|
|
219
194
|
function syncFrame() {
|
|
220
195
|
if (!stageEl) return;
|
|
221
196
|
const stageRect = stageEl.getBoundingClientRect();
|
|
222
197
|
if (stageRect.width === 0 || stageRect.height === 0) return;
|
|
223
198
|
|
|
224
|
-
// Box geometry in stage-% space — measured from the live rect so that
|
|
225
|
-
// intrinsic sizing (content + padding) drives anchor placement.
|
|
226
199
|
let boxCx = 50, boxCy = 50;
|
|
227
200
|
let boxW = boxSize.w, boxH = boxSize.h;
|
|
228
201
|
if (boxEl) {
|
|
@@ -242,8 +215,7 @@
|
|
|
242
215
|
const lineEl = lineEls[i];
|
|
243
216
|
if (!tagEl || !lineEl) continue;
|
|
244
217
|
|
|
245
|
-
// Tag
|
|
246
|
-
// side closest to the central component.
|
|
218
|
+
// Tag endpoint: center of the pill's rounded cap on the box-facing side.
|
|
247
219
|
const pill = tagEl.querySelector('.ftt-tag') as HTMLElement | null;
|
|
248
220
|
const target = (pill ?? tagEl) as HTMLElement;
|
|
249
221
|
const r = target.getBoundingClientRect();
|
|
@@ -265,7 +237,6 @@
|
|
|
265
237
|
lineEl.setAttribute('x1', x1.toFixed(3));
|
|
266
238
|
lineEl.setAttribute('y1', y1.toFixed(3));
|
|
267
239
|
|
|
268
|
-
// Box-side endpoint + knot — recomputed from live box geometry.
|
|
269
240
|
const a = anchorPoint(tags[i].anchor, boxCx, boxCy, boxW, boxH);
|
|
270
241
|
lineEl.setAttribute('x2', a.x.toFixed(3));
|
|
271
242
|
lineEl.setAttribute('y2', a.y.toFixed(3));
|
|
@@ -275,7 +246,6 @@
|
|
|
275
246
|
knotEl.style.top = `${a.y.toFixed(3)}%`;
|
|
276
247
|
}
|
|
277
248
|
|
|
278
|
-
// Energy ball traveling tag → box along the same line.
|
|
279
249
|
const ballEl = ballEls[i];
|
|
280
250
|
const state = ballStates.get(i);
|
|
281
251
|
if (!ballEl) continue;
|
|
@@ -286,9 +256,7 @@
|
|
|
286
256
|
const elapsed = now - state.startedAt;
|
|
287
257
|
const t = Math.min(1, elapsed / state.duration);
|
|
288
258
|
if (t >= 1) {
|
|
289
|
-
// Commit
|
|
290
|
-
// sync with the bloop (the "pops up and grows larger" beat). Until
|
|
291
|
-
// this point the box keeps rendering the previous value.
|
|
259
|
+
// Commit at impact so the box's appearance changes with the bloop.
|
|
292
260
|
boxOverrides = { ...boxOverrides, [i]: state.pendingValue };
|
|
293
261
|
ballStates.delete(i);
|
|
294
262
|
ballEl.style.opacity = '0';
|
|
@@ -297,7 +265,7 @@
|
|
|
297
265
|
}
|
|
298
266
|
const x2 = parseFloat(lineEl.getAttribute('x2') || '0');
|
|
299
267
|
const y2 = parseFloat(lineEl.getAttribute('y2') || '0');
|
|
300
|
-
const eased = 1 - Math.pow(1 - t, 3);
|
|
268
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
301
269
|
const bx = x1 + (x2 - x1) * eased;
|
|
302
270
|
const by = y1 + (y2 - y1) * eased;
|
|
303
271
|
ballEl.style.left = `${bx.toFixed(3)}%`;
|
|
@@ -316,13 +284,9 @@
|
|
|
316
284
|
return () => cancelAnimationFrame(rafId);
|
|
317
285
|
});
|
|
318
286
|
|
|
319
|
-
// --- Selection / fire sequence ------------------------------------------
|
|
320
287
|
function pickValue(i: number, value: string) {
|
|
321
288
|
openIdx = null;
|
|
322
289
|
strobeIdx = null;
|
|
323
|
-
// Commit the tag's badge label immediately so the user gets a "you picked
|
|
324
|
-
// this" confirmation. The central box waits — its commit is carried by
|
|
325
|
-
// the energy ball and lands at impact.
|
|
326
290
|
overrides = { ...overrides, [i]: value };
|
|
327
291
|
flashingIdx = i;
|
|
328
292
|
window.setTimeout(() => {
|
|
@@ -336,8 +300,7 @@
|
|
|
336
300
|
window.setTimeout(() => { bloopActive = false; }, 480);
|
|
337
301
|
}
|
|
338
302
|
|
|
339
|
-
//
|
|
340
|
-
// USER_HOLD_MS so the user can play without being interrupted.
|
|
303
|
+
// Any click pauses auto-cycle for at least this long.
|
|
341
304
|
const USER_HOLD_MS = 4000;
|
|
342
305
|
let lastUserActionAt = 0;
|
|
343
306
|
function noteUserAction() {
|
|
@@ -351,14 +314,12 @@
|
|
|
351
314
|
}
|
|
352
315
|
|
|
353
316
|
function userPick(i: number, value: string) {
|
|
354
|
-
// The
|
|
355
|
-
// rendered disabled, this guard is a safety net.
|
|
317
|
+
// The matching dropdown item is also rendered disabled; this is a safety net.
|
|
356
318
|
if (currentValues[i] === value) return;
|
|
357
319
|
noteUserAction();
|
|
358
320
|
pickValue(i, value);
|
|
359
321
|
}
|
|
360
322
|
|
|
361
|
-
// --- Drag handlers ------------------------------------------------------
|
|
362
323
|
function onTagPointerDown(i: number, e: PointerEvent) {
|
|
363
324
|
dragStart.px = e.clientX;
|
|
364
325
|
dragStart.py = e.clientY;
|
|
@@ -375,7 +336,7 @@
|
|
|
375
336
|
dragStart.moved = true;
|
|
376
337
|
draggingIdx = i;
|
|
377
338
|
openIdx = null;
|
|
378
|
-
noteUserAction();
|
|
339
|
+
noteUserAction();
|
|
379
340
|
}
|
|
380
341
|
if (dragStart.moved) {
|
|
381
342
|
const r = stageEl.getBoundingClientRect();
|
|
@@ -402,7 +363,6 @@
|
|
|
402
363
|
dragStart.moved = false;
|
|
403
364
|
}
|
|
404
365
|
|
|
405
|
-
// --- Auto-cycle ----------------------------------------------------------
|
|
406
366
|
let autoAlive = false;
|
|
407
367
|
let lastAutoTagIdx: number | null = null;
|
|
408
368
|
const sleep = (ms: number) => new Promise(r => window.setTimeout(r, ms));
|
|
@@ -415,8 +375,7 @@
|
|
|
415
375
|
await sleep(2400 + Math.random() * 2400);
|
|
416
376
|
if (!autoAlive) break;
|
|
417
377
|
|
|
418
|
-
// Hold off
|
|
419
|
-
// partial sleep so a fresh click resets the wait.
|
|
378
|
+
// Hold off while the user is interacting; re-check on each fresh click.
|
|
420
379
|
while (autoAlive) {
|
|
421
380
|
const sinceUser = performance.now() - lastUserActionAt;
|
|
422
381
|
if (sinceUser >= USER_HOLD_MS) break;
|
|
@@ -424,7 +383,7 @@
|
|
|
424
383
|
}
|
|
425
384
|
if (!autoAlive) break;
|
|
426
385
|
|
|
427
|
-
if (openIdx !== null) continue;
|
|
386
|
+
if (openIdx !== null) continue;
|
|
428
387
|
|
|
429
388
|
// Never the same tag twice in a row.
|
|
430
389
|
const tagCandidates = tags
|
|
@@ -435,8 +394,7 @@
|
|
|
435
394
|
const tag = tags[i];
|
|
436
395
|
const opts = valueOptions[tag.controls!];
|
|
437
396
|
|
|
438
|
-
// Never the same token twice in a row
|
|
439
|
-
// value from the candidate set.
|
|
397
|
+
// Never the same token twice in a row.
|
|
440
398
|
const currentIdx = opts.indexOf(currentValues[i]);
|
|
441
399
|
const candidates = opts
|
|
442
400
|
.map((_, k) => k)
|
|
@@ -446,13 +404,11 @@
|
|
|
446
404
|
|
|
447
405
|
openIdx = i;
|
|
448
406
|
|
|
449
|
-
//
|
|
407
|
+
// Let the menu sit open before any highlight appears.
|
|
450
408
|
await sleep(BREATH_MS);
|
|
451
409
|
if (!autoAlive || openIdx !== i) continue;
|
|
452
410
|
|
|
453
|
-
// Step from the top down to the chosen item
|
|
454
|
-
// Landing position is wherever finalIdx lands — could be the first item
|
|
455
|
-
// (no stepping), could be the fourth.
|
|
411
|
+
// Step from the top down to the chosen item.
|
|
456
412
|
for (let k = 0; k <= finalIdx; k++) {
|
|
457
413
|
if (!autoAlive || openIdx !== i) break;
|
|
458
414
|
strobeIdx = k;
|
|
@@ -460,7 +416,7 @@
|
|
|
460
416
|
}
|
|
461
417
|
if (!autoAlive || openIdx !== i) continue;
|
|
462
418
|
|
|
463
|
-
// Blink twice on the selection
|
|
419
|
+
// Blink twice on the selection.
|
|
464
420
|
for (let blink = 0; blink < 2; blink++) {
|
|
465
421
|
if (!autoAlive || openIdx !== i) break;
|
|
466
422
|
strobeIdx = null;
|
|
@@ -490,7 +446,6 @@
|
|
|
490
446
|
style:--ftt-bob-duration="{duration}s"
|
|
491
447
|
style:--ftt-bob-distance="{-distance}px"
|
|
492
448
|
>
|
|
493
|
-
<!-- Kite strings -->
|
|
494
449
|
<svg class="ftt-strings" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
|
|
495
450
|
{#each tags as tag, i (i)}
|
|
496
451
|
{@const a = anchorPoint(tag.anchor)}
|
|
@@ -504,17 +459,14 @@
|
|
|
504
459
|
{/each}
|
|
505
460
|
</svg>
|
|
506
461
|
|
|
507
|
-
<!--
|
|
508
|
-
intrinsically: width/height grow to fit content + padding. boxSize
|
|
509
|
-
is a baseline (min-width/min-height) so a short label can't shrink
|
|
510
|
-
the box past a sensible footprint. -->
|
|
462
|
+
<!-- Box is intrinsically sized to content + padding; boxSize is a floor. -->
|
|
511
463
|
<div
|
|
512
464
|
bind:this={boxEl}
|
|
513
465
|
class="ftt-box"
|
|
514
466
|
class:ftt-bloop={bloopActive}
|
|
515
467
|
style:min-width="{boxSize.w}%"
|
|
516
468
|
style:min-height="{boxSize.h}%"
|
|
517
|
-
style:background={
|
|
469
|
+
style:background={asVar(surfaceVar)}
|
|
518
470
|
style:border-radius={asVar(radiusVar)}
|
|
519
471
|
style:border-color={asVar(borderColorVar)}
|
|
520
472
|
style:border-width={asVar(borderWidthVar)}
|
|
@@ -525,8 +477,6 @@
|
|
|
525
477
|
>I'm a button</span>
|
|
526
478
|
</div>
|
|
527
479
|
|
|
528
|
-
<!-- Anchor knots on the box. Initial position uses anchorPoint() defaults;
|
|
529
|
-
syncFrame imperatively updates each frame from the box's live rect. -->
|
|
530
480
|
{#each tags as tag, i (i)}
|
|
531
481
|
{@const a = anchorPoint(tag.anchor)}
|
|
532
482
|
<span
|
|
@@ -538,12 +488,10 @@
|
|
|
538
488
|
></span>
|
|
539
489
|
{/each}
|
|
540
490
|
|
|
541
|
-
<!-- Energy balls — positioned each frame by syncFrame() while in flight. -->
|
|
542
491
|
{#each tags as _t, i (i)}
|
|
543
492
|
<span class="ftt-energy-ball" bind:this={ballEls[i]} aria-hidden="true"></span>
|
|
544
493
|
{/each}
|
|
545
494
|
|
|
546
|
-
<!-- Floating tags. -->
|
|
547
495
|
{#each tags as tag, i (i)}
|
|
548
496
|
<span
|
|
549
497
|
bind:this={tagEls[i]}
|
|
@@ -551,6 +499,8 @@
|
|
|
551
499
|
class:ftt-flash={flashingIdx === i}
|
|
552
500
|
class:ftt-open={openIdx === i}
|
|
553
501
|
class:ftt-dragging={draggingIdx === i}
|
|
502
|
+
data-placement={tag.placement ?? 'top'}
|
|
503
|
+
data-pivot={tag.pivot ?? 'center'}
|
|
554
504
|
style:top="{tagTop(i)}%"
|
|
555
505
|
style:left="{tagLeft(i)}%"
|
|
556
506
|
style:animation-delay="{tag.delay ?? 0}s"
|
|
@@ -559,34 +509,35 @@
|
|
|
559
509
|
{#if tag.controls}
|
|
560
510
|
<span class="ftt-float-property">{controlLabels[tag.controls]}</span>
|
|
561
511
|
{/if}
|
|
562
|
-
<
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
{
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
<
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
512
|
+
<span class="ftt-chip-host">
|
|
513
|
+
<button
|
|
514
|
+
type="button"
|
|
515
|
+
class="ftt-tag-trigger"
|
|
516
|
+
onpointerdown={(e) => onTagPointerDown(i, e)}
|
|
517
|
+
onpointermove={(e) => onTagPointerMove(i, e)}
|
|
518
|
+
onpointerup={(e) => onTagPointerUp(i, e)}
|
|
519
|
+
aria-haspopup="listbox"
|
|
520
|
+
aria-expanded={openIdx === i}
|
|
521
|
+
>
|
|
522
|
+
<span class="ftt-tag">
|
|
523
|
+
{#if tag.icon}<span class="ftt-tag-icon"><i class={tag.icon}></i></span>{/if}
|
|
524
|
+
<span class="ftt-tag-label">{currentValues[i]}</span>
|
|
525
|
+
</span>
|
|
526
|
+
</button>
|
|
527
|
+
|
|
528
|
+
{#if openIdx === i && tag.controls}
|
|
529
|
+
{@const opts = valueOptions[tag.controls]}
|
|
530
|
+
{@const strobeValue = strobeIdx !== null ? opts[strobeIdx] : null}
|
|
531
|
+
<div class="ftt-dropdown-wrap">
|
|
532
|
+
<MenuSelect
|
|
533
|
+
items={opts.map((opt) => ({ value: opt, label: opt }))}
|
|
534
|
+
value={currentValues[i]}
|
|
535
|
+
forceHoverValue={strobeValue}
|
|
536
|
+
onchange={(v) => userPick(i, v)}
|
|
537
|
+
/>
|
|
538
|
+
</div>
|
|
539
|
+
{/if}
|
|
540
|
+
</span>
|
|
589
541
|
</span>
|
|
590
542
|
{/each}
|
|
591
543
|
</div>
|
|
592
|
-
|
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
:global(:root) {
|
|
114
114
|
/* Info */
|
|
115
115
|
--notification-info-surface: var(--surface-info);
|
|
116
|
+
--notification-info-action-surface: var(--surface-neutral-lowest);
|
|
116
117
|
--notification-info-border: var(--border-info);
|
|
117
118
|
--notification-info-border-width: var(--border-width-1);
|
|
118
119
|
--notification-info-radius: var(--radius-md);
|
|
@@ -132,6 +133,7 @@
|
|
|
132
133
|
|
|
133
134
|
/* Success */
|
|
134
135
|
--notification-success-surface: var(--surface-success);
|
|
136
|
+
--notification-success-action-surface: var(--surface-neutral-lowest);
|
|
135
137
|
--notification-success-border: var(--border-success);
|
|
136
138
|
--notification-success-border-width: var(--border-width-1);
|
|
137
139
|
--notification-success-radius: var(--radius-md);
|
|
@@ -151,6 +153,7 @@
|
|
|
151
153
|
|
|
152
154
|
/* Warning */
|
|
153
155
|
--notification-warning-surface: var(--surface-warning);
|
|
156
|
+
--notification-warning-action-surface: var(--surface-neutral-lowest);
|
|
154
157
|
--notification-warning-border: var(--border-warning);
|
|
155
158
|
--notification-warning-border-width: var(--border-width-1);
|
|
156
159
|
--notification-warning-radius: var(--radius-md);
|
|
@@ -170,6 +173,7 @@
|
|
|
170
173
|
|
|
171
174
|
/* Danger */
|
|
172
175
|
--notification-danger-surface: var(--surface-danger);
|
|
176
|
+
--notification-danger-action-surface: var(--surface-neutral-lowest);
|
|
173
177
|
--notification-danger-border: var(--border-danger);
|
|
174
178
|
--notification-danger-border-width: var(--border-width-1);
|
|
175
179
|
--notification-danger-radius: var(--radius-md);
|
|
@@ -240,6 +244,10 @@
|
|
|
240
244
|
font-weight: var(--notification-#{$variant}-title-font-weight);
|
|
241
245
|
line-height: var(--notification-#{$variant}-title-line-height);
|
|
242
246
|
}
|
|
247
|
+
|
|
248
|
+
.action-button-backdrop {
|
|
249
|
+
background: var(--notification-#{$variant}-action-surface);
|
|
250
|
+
}
|
|
243
251
|
}
|
|
244
252
|
}
|
|
245
253
|
}
|
|
@@ -270,7 +278,6 @@
|
|
|
270
278
|
|
|
271
279
|
.action-button-backdrop {
|
|
272
280
|
flex-shrink: 0;
|
|
273
|
-
background: var(--surface-neutral-lowest);
|
|
274
281
|
border-radius: var(--radius-md);
|
|
275
282
|
}
|
|
276
283
|
|