@motion-proto/live-tokens 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -13
- package/dist-plugin/index.cjs +147 -136
- package/dist-plugin/index.d.cts +1 -1
- package/dist-plugin/index.d.ts +1 -1
- package/dist-plugin/index.js +145 -135
- package/package.json +25 -40
- package/src/{component-editor → editor/component-editor}/BadgeEditor.svelte +8 -82
- package/src/{component-editor → editor/component-editor}/CalloutEditor.svelte +4 -4
- package/src/{component-editor → editor/component-editor}/CardEditor.svelte +28 -76
- package/src/{component-editor → editor/component-editor}/CollapsibleSectionEditor.svelte +3 -3
- package/src/{component-editor → editor/component-editor}/CornerBadgeEditor.svelte +31 -93
- package/src/{component-editor → editor/component-editor}/DialogEditor.svelte +60 -57
- package/src/editor/component-editor/ImageEditor.svelte +30 -0
- package/src/{component-editor → editor/component-editor}/InlineEditActionsEditor.svelte +6 -4
- package/src/editor/component-editor/MenuSelectEditor.svelte +160 -0
- package/src/{component-editor → editor/component-editor}/NotificationEditor.svelte +64 -37
- package/src/{component-editor → editor/component-editor}/ProgressBarEditor.svelte +5 -4
- package/src/{component-editor → editor/component-editor}/RadioButtonEditor.svelte +3 -3
- package/src/{component-editor → editor/component-editor}/SectionDividerEditor.svelte +57 -84
- package/src/{component-editor → editor/component-editor}/SegmentedControlEditor.svelte +2 -2
- package/src/{component-editor → editor/component-editor}/StandardButtonsEditor.svelte +16 -20
- package/src/{component-editor → editor/component-editor}/TabBarEditor.svelte +9 -14
- package/src/{component-editor → editor/component-editor}/TableEditor.svelte +9 -18
- package/src/{component-editor → editor/component-editor}/TooltipEditor.svelte +11 -47
- package/src/{component-editor → editor/component-editor}/registry.ts +28 -18
- package/src/{component-editor → editor/component-editor}/scaffolding/AngleDial.svelte +2 -2
- package/src/{component-editor → editor/component-editor}/scaffolding/ComponentEditorBase.svelte +3 -51
- package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileManager.svelte +144 -416
- package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileMenu.svelte +18 -170
- package/src/{component-editor → editor/component-editor}/scaffolding/ComponentsTab.svelte +2 -2
- package/src/{component-editor → editor/component-editor}/scaffolding/CopyFromMenu.svelte +44 -4
- package/src/{component-editor → editor/component-editor}/scaffolding/DividerEditor.svelte +1 -1
- package/src/{component-editor → editor/component-editor}/scaffolding/FieldsetWrapper.svelte +1 -1
- package/src/{component-editor → editor/component-editor}/scaffolding/GradientCard.svelte +6 -6
- package/src/{component-editor → editor/component-editor}/scaffolding/LinkageChart.svelte +6 -6
- package/src/{component-editor → editor/component-editor}/scaffolding/LinkedBlock.svelte +6 -11
- package/src/editor/component-editor/scaffolding/NonStylableConfig.svelte +38 -0
- package/src/{component-editor → editor/component-editor}/scaffolding/SaveAsDialog.svelte +66 -12
- package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +72 -0
- package/src/editor/component-editor/scaffolding/ShadowBackdropControls.svelte +132 -0
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +257 -0
- package/src/{component-editor → editor/component-editor}/scaffolding/TokenLayout.svelte +9 -7
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +644 -0
- package/src/{component-editor → editor/component-editor}/scaffolding/editorContext.ts +19 -9
- package/src/{component-editor → editor/component-editor}/scaffolding/linkedBlock.ts +2 -2
- package/src/{component-editor → editor/component-editor}/scaffolding/types.ts +14 -0
- package/src/{lib → editor/core/components}/componentConfigService.ts +2 -2
- package/src/{lib → editor/core/components}/componentPersist.ts +5 -5
- package/src/editor/core/flashStatus.ts +30 -0
- package/src/{lib → editor/core/fonts}/fontLoader.ts +2 -2
- package/src/{lib → editor/core/fonts}/fontMigration.ts +4 -4
- package/src/{lib → editor/core/fonts}/fontParse.ts +1 -1
- package/src/editor/core/manifests/manifestService.ts +116 -0
- package/src/{lib → editor/core/palettes}/paletteDerivation.ts +2 -2
- package/src/{lib → editor/core/palettes}/tokenRegistry.ts +5 -5
- package/src/editor/core/productionPulse.ts +37 -0
- package/src/{lib → editor/core/routing}/router.ts +1 -1
- package/src/{lib/files/versionedFileResource.ts → editor/core/storage/files/versionedFileResourceClient.ts} +8 -1
- package/src/{lib → editor/core/store}/editorCore.ts +24 -8
- package/src/{lib → editor/core/store}/editorPersistence.ts +3 -3
- package/src/{lib → editor/core/store}/editorRenderer.ts +2 -2
- package/src/{lib → editor/core/store}/editorStore.ts +17 -17
- package/src/{lib → editor/core/store}/editorTypes.ts +1 -1
- package/src/{lib → editor/core/themes}/slices/columns.ts +2 -2
- package/src/{lib → editor/core/themes}/slices/components.ts +2 -2
- package/src/{lib → editor/core/themes}/slices/fonts.ts +1 -1
- package/src/{lib → editor/core/themes}/slices/gradients.ts +2 -2
- package/src/{lib → editor/core/themes}/slices/overlays.ts +1 -1
- package/src/{lib → editor/core/themes}/slices/palettes.ts +1 -1
- package/src/{lib → editor/core/themes}/slices/shadows.ts +3 -3
- package/src/{lib → editor/core/themes}/themeInit.ts +6 -6
- package/src/{lib → editor/core/themes}/themeService.ts +6 -6
- package/src/{lib → editor/core/themes}/themeTypes.ts +11 -7
- package/src/editor/index.ts +69 -0
- package/src/{lib → editor/overlay}/LiveEditorOverlay.svelte +79 -125
- package/src/{lib → editor/overlay}/columnsOverlay.ts +2 -2
- package/src/{pages → editor/pages}/ComponentEditorPage.svelte +12 -12
- package/src/{pages → editor/pages}/Editor.svelte +4 -4
- package/src/{pages → editor/pages}/EditorShell.svelte +18 -36
- package/src/{styles → editor/styles}/ui-editor.css +41 -21
- package/src/{styles → editor/styles}/ui-form-controls.css +8 -8
- package/src/{ui → editor/ui}/BezierCurveEditor.svelte +8 -8
- package/src/{ui → editor/ui}/ColorEditPanel.svelte +13 -13
- package/src/{ui → editor/ui}/EditorViewSwitcher.svelte +8 -6
- package/src/editor/ui/FileLoadList.svelte +350 -0
- package/src/editor/ui/FilePill.svelte +80 -0
- package/src/{ui → editor/ui}/FontStackEditor.svelte +7 -7
- package/src/{ui → editor/ui}/GradientEditor.svelte +11 -11
- package/src/{ui → editor/ui}/GradientStopPicker.svelte +1 -1
- package/src/editor/ui/ManifestFileManager.svelte +371 -0
- package/src/{ui → editor/ui}/PaletteEditor.svelte +132 -598
- package/src/{ui → editor/ui}/ProjectFontsSection.svelte +102 -144
- package/src/{ui → editor/ui}/SurfacesTab.svelte +3 -3
- package/src/{ui → editor/ui}/TextTab.svelte +3 -3
- package/src/{ui → editor/ui}/ThemeFileManager.svelte +286 -519
- package/src/{ui → editor/ui}/UICopyPopover.svelte +4 -4
- package/src/{ui → editor/ui}/UIFontFamilySelector.svelte +6 -6
- package/src/{ui → editor/ui}/UIFontSizeSelector.svelte +1 -1
- package/src/editor/ui/UIInfoPopover.svelte +244 -0
- package/src/{ui → editor/ui}/UILineHeightSelector.svelte +5 -5
- package/src/{ui → editor/ui}/UILinkToggle.svelte +2 -2
- package/src/{ui → editor/ui}/UIPaddingSelector.svelte +6 -6
- package/src/{ui → editor/ui}/UIPaletteSelector.svelte +26 -26
- package/src/editor/ui/UIPillButton.svelte +138 -0
- package/src/{ui → editor/ui}/UIRadio.svelte +2 -2
- package/src/{ui → editor/ui}/UIRelinkConfirmPopover.svelte +4 -4
- package/src/editor/ui/UISquareButton.svelte +172 -0
- package/src/{ui → editor/ui}/UITokenSelector.svelte +10 -10
- package/src/{ui → editor/ui}/UIVariantSelector.svelte +1 -1
- package/src/{ui → editor/ui}/VariablesTab.svelte +31 -8
- package/src/{ui → editor/ui}/palette/GradientStopEditor.svelte +13 -13
- package/src/{ui → editor/ui}/palette/OverridesPanel.svelte +13 -13
- package/src/{ui → editor/ui}/palette/PaletteBase.svelte +8 -5
- package/src/{ui → editor/ui}/palette/paletteEditorState.ts +1 -1
- package/src/editor/ui/palette/paletteMath.ts +275 -0
- package/src/{ui → editor/ui}/sections/ColumnsSection.svelte +137 -17
- package/src/{ui → editor/ui}/sections/GradientsSection.svelte +7 -7
- package/src/{ui → editor/ui}/sections/OverlaysSection.svelte +17 -17
- package/src/{ui → editor/ui}/sections/ShadowsSection.svelte +22 -22
- package/src/{ui → editor/ui}/sections/TokenScaleTable.svelte +3 -3
- package/src/{components → system/components}/Badge.svelte +0 -36
- package/src/{components → system/components}/Card.svelte +8 -62
- package/src/{components → system/components}/CornerBadge.svelte +8 -24
- package/src/{components → system/components}/Dialog.svelte +1 -1
- package/src/system/components/FloatingTokenTags.css +256 -0
- package/src/system/components/FloatingTokenTags.svelte +592 -0
- package/src/{components → system/components}/InlineEditActions.svelte +6 -4
- package/src/system/components/MenuSelect.svelte +229 -0
- package/src/{components → system/components}/ProgressBar.svelte +29 -11
- package/src/{components → system/components}/SegmentedControl.svelte +49 -43
- package/src/{components → system/components}/TabBar.svelte +81 -65
- package/src/{components → system/components}/Table.svelte +17 -3
- package/src/{components → system/components}/Tooltip.svelte +6 -4
- package/src/system/styles/CONVENTIONS.md +178 -0
- package/src/{styles → system/styles}/fonts.css +6 -3
- package/src/{styles → system/styles}/tokens.css +149 -29
- package/src/component-editor/ImageEditor.svelte +0 -74
- package/src/component-editor/scaffolding/NonStylableConfig.svelte +0 -62
- package/src/component-editor/scaffolding/ShadowBackdrop.svelte +0 -37
- package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +0 -61
- package/src/component-editor/scaffolding/StateBlock.svelte +0 -132
- package/src/component-editor/scaffolding/VariantGroup.svelte +0 -310
- package/src/data/google-fonts.json +0 -75
- package/src/lib/index.ts +0 -68
- package/src/lib/presetService.ts +0 -214
- package/src/lib/productionPulse.ts +0 -32
- package/src/ui/PresetFileManager.svelte +0 -1116
- package/src/ui/UnsavedComponentsDialog.svelte +0 -315
- /package/src/{styles → app}/site.css +0 -0
- /package/src/{component-editor → editor/component-editor}/index.ts +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/DemoHeader.svelte +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/TypeEditor.svelte +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/buildTypeGroupTokens.ts +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/componentSectionType.ts +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/componentSources.ts +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/defaultSections.ts +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/siblings.ts +0 -0
- /package/src/{lib → editor/core/components}/componentConfigKeys.ts +0 -0
- /package/src/{lib → editor/core}/cssVarSync.ts +0 -0
- /package/src/{lib → editor/core/palettes}/oklch.ts +0 -0
- /package/src/{lib → editor/core/routing}/navLinkTypes.ts +0 -0
- /package/src/{lib → editor/core/routing}/parentRouteStore.ts +0 -0
- /package/src/{lib → editor/core/storage}/storage.ts +0 -0
- /package/src/{lib → editor/core/store}/editorConfig.ts +0 -0
- /package/src/{lib → editor/core/store}/editorConfigStore.ts +0 -0
- /package/src/{lib → editor/core/store}/editorKeybindings.ts +0 -0
- /package/src/{lib → editor/core/store}/editorViewStore.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-05-10-sectiondivider-gradient-stops.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-05-13-primary-to-brand.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/index.ts +0 -0
- /package/src/{lib → editor/core/themes}/parsers/globalRootBlock.ts +0 -0
- /package/src/{lib → editor/core/themes}/slices/domainVars.ts +0 -0
- /package/src/{lib → editor/overlay}/ColumnsOverlay.svelte +0 -0
- /package/src/{lib → editor/overlay}/overlayState.ts +0 -0
- /package/src/{pages → editor/pages}/ComponentEditorPage.svelte.d.ts +0 -0
- /package/src/{pages → editor/pages}/Editor.svelte.d.ts +0 -0
- /package/src/{ui → editor/ui}/Toggle.svelte +0 -0
- /package/src/{ui → editor/ui}/UIDialog.svelte +0 -0
- /package/src/{ui → editor/ui}/UIFontWeightSelector.svelte +0 -0
- /package/src/{ui → editor/ui}/UIOptionItem.svelte +0 -0
- /package/src/{ui → editor/ui}/UIOptionList.svelte +0 -0
- /package/src/{ui → editor/ui}/UIRadioGroup.svelte +0 -0
- /package/src/{lib → editor/ui}/copyPopover.ts +0 -0
- /package/src/{ui → editor/ui}/curveEngine.ts +0 -0
- /package/src/{ui → editor/ui}/index.ts +0 -0
- /package/src/{ui → editor/ui}/keepInViewport.ts +0 -0
- /package/src/{ui → editor/ui}/palette/ScaleCurveEditor.svelte +0 -0
- /package/src/{lib → editor/ui}/scrollSection.ts +0 -0
- /package/src/{ui → editor/ui}/sections/tokenScales.ts +0 -0
- /package/src/{ui → editor/ui}/variantScales.ts +0 -0
- /package/src/{assets → system/assets}/newspaper.webp +0 -0
- /package/src/{assets → system/assets}/offering.webp +0 -0
- /package/src/{components → system/components}/Button.svelte +0 -0
- /package/src/{components → system/components}/Callout.svelte +0 -0
- /package/src/{components → system/components}/CollapsibleSection.svelte +0 -0
- /package/src/{components → system/components}/Image.svelte +0 -0
- /package/src/{components → system/components}/Notification.svelte +0 -0
- /package/src/{components → system/components}/RadioButton.svelte +0 -0
- /package/src/{components → system/components}/SectionDivider.svelte +0 -0
- /package/src/{components → system/components}/types.ts +0 -0
- /package/src/{styles → system/styles}/_padding.scss +0 -0
- /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
- /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
- /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
- /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
- /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
- /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin.woff2 +0 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type AnchorSide = 'top' | 'right' | 'bottom' | 'left';
|
|
3
|
+
|
|
4
|
+
/** Which visual property of the central box this tag drives. */
|
|
5
|
+
export type TagControl = 'surface' | 'radius' | 'border-color' | 'border-width' | 'font-family';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Where the kite string ties to the central box.
|
|
9
|
+
* - Edge anchor: a point along one of the four box edges (`pos` 0..100).
|
|
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).
|
|
13
|
+
*/
|
|
14
|
+
export type Anchor =
|
|
15
|
+
| { side: AnchorSide; pos: number }
|
|
16
|
+
| { side: 'inside'; x: number; y: number };
|
|
17
|
+
|
|
18
|
+
export interface FloatingTag {
|
|
19
|
+
icon?: string;
|
|
20
|
+
/** Typically a CSS-variable name. Used as the initial value. */
|
|
21
|
+
label: string;
|
|
22
|
+
/** Tag center, in % of the stage. */
|
|
23
|
+
top: number;
|
|
24
|
+
left: number;
|
|
25
|
+
/** Bob delay, seconds. Negative values offset the start. */
|
|
26
|
+
delay?: number;
|
|
27
|
+
/** Static tilt of the tag, degrees. */
|
|
28
|
+
rotate?: number;
|
|
29
|
+
/** Where this tag's string ties to the central box. */
|
|
30
|
+
anchor: Anchor;
|
|
31
|
+
/** Property of the central box this tag drives (live preview of the token). */
|
|
32
|
+
controls?: TagControl;
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<script lang="ts">
|
|
37
|
+
import MenuSelect from './MenuSelect.svelte';
|
|
38
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
39
|
+
// Playground chrome lives in its own CSS file so live token edits in the
|
|
40
|
+
// editor don't repaint our animation. The floating tag pills are rendered
|
|
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.
|
|
46
|
+
import './FloatingTokenTags.css';
|
|
47
|
+
|
|
48
|
+
interface Props {
|
|
49
|
+
tags?: FloatingTag[];
|
|
50
|
+
duration?: number;
|
|
51
|
+
distance?: number;
|
|
52
|
+
boxSize?: { w: number; h: number };
|
|
53
|
+
/** Auto-cycle through values when idle. */
|
|
54
|
+
autoplay?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const controlLabels: Record<TagControl, string> = {
|
|
58
|
+
'surface': 'Surface color',
|
|
59
|
+
'radius': 'Corner radius',
|
|
60
|
+
'border-color': 'Border color',
|
|
61
|
+
'border-width': 'Border width',
|
|
62
|
+
'font-family': 'Font family',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Four candidate token values per control. Picked to span the visible
|
|
66
|
+
// gamut so cycling produces noticeably different box states.
|
|
67
|
+
const valueOptions: Record<TagControl, string[]> = {
|
|
68
|
+
'surface': ['--surface-brand-low', '--surface-danger-low', '--surface-accent-low', '--surface-special-low'],
|
|
69
|
+
'radius': ['--radius-none', '--radius-lg', '--radius-2xl', '--radius-full'],
|
|
70
|
+
'border-color': ['--border-brand', '--border-danger', '--border-success', '--border-special'],
|
|
71
|
+
'border-width': ['--border-width-1', '--border-width-2', '--border-width-3', '--border-width-5'],
|
|
72
|
+
'font-family': ['--font-display', '--font-sans', '--font-serif', '--font-mono'],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
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
|
+
{
|
|
84
|
+
icon: 'fas fa-fill-drip',
|
|
85
|
+
label: '--surface-brand-low',
|
|
86
|
+
top: 28, left: 18, delay: 0, rotate: -3,
|
|
87
|
+
anchor: { side: 'inside', x: 10, y: 50 },
|
|
88
|
+
controls: 'surface',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
icon: 'fas fa-font',
|
|
92
|
+
label: '--font-display',
|
|
93
|
+
top: 11.1, left: 40.8, delay: -0.9, rotate: 2,
|
|
94
|
+
anchor: { side: 'inside', x: 45, y: 28 },
|
|
95
|
+
controls: 'font-family',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
icon: 'fa-solid fa-bezier-curve',
|
|
99
|
+
label: '--radius-2xl',
|
|
100
|
+
top: 19.8, left: 72.0, delay: -1.8, rotate: 2,
|
|
101
|
+
anchor: { side: 'top', pos: 100 },
|
|
102
|
+
controls: 'radius',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
icon: 'fas fa-paint-roller',
|
|
106
|
+
label: '--border-brand',
|
|
107
|
+
top: 75.5, left: 79.5, delay: -3.6, rotate: -2,
|
|
108
|
+
anchor: { side: 'right', pos: 55 },
|
|
109
|
+
controls: 'border-color',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
icon: 'fas fa-grip-lines',
|
|
113
|
+
label: '--border-width-3',
|
|
114
|
+
top: 78, left: 33, delay: -5.2, rotate: -3,
|
|
115
|
+
anchor: { side: 'bottom', pos: 50 },
|
|
116
|
+
controls: 'border-width',
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
let {
|
|
121
|
+
tags = defaultTags,
|
|
122
|
+
duration = 7,
|
|
123
|
+
distance = 8,
|
|
124
|
+
boxSize = { w: 14, h: 11 },
|
|
125
|
+
autoplay = true,
|
|
126
|
+
}: Props = $props();
|
|
127
|
+
|
|
128
|
+
// --- Per-tag mutable state ----------------------------------------------
|
|
129
|
+
// Tag values are modelled as defaults (derived from the `tags` prop) plus
|
|
130
|
+
// user overrides. This keeps `currentValues` reactive to prop changes
|
|
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).
|
|
135
|
+
const defaultLabels = $derived(tags.map(t => t.label));
|
|
136
|
+
let overrides = $state<Record<number, string>>({});
|
|
137
|
+
let boxOverrides = $state<Record<number, string>>({});
|
|
138
|
+
const currentValues = $derived(defaultLabels.map((d, i) => overrides[i] ?? d));
|
|
139
|
+
const boxValues = $derived(defaultLabels.map((d, i) => boxOverrides[i] ?? d));
|
|
140
|
+
|
|
141
|
+
let openIdx = $state<number | null>(null);
|
|
142
|
+
let strobeIdx = $state<number | null>(null);
|
|
143
|
+
let flashingIdx = $state<number | null>(null);
|
|
144
|
+
let bloopActive = $state(false);
|
|
145
|
+
|
|
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
|
+
let dragOverrides = $state<Record<number, { top: number; left: number }>>({});
|
|
149
|
+
let draggingIdx = $state<number | null>(null);
|
|
150
|
+
const DRAG_THRESHOLD_PX = 4;
|
|
151
|
+
const dragStart = { px: 0, py: 0, tagIdx: -1, moved: false };
|
|
152
|
+
|
|
153
|
+
function tagTop(i: number): number { return dragOverrides[i]?.top ?? tags[i].top; }
|
|
154
|
+
function tagLeft(i: number): number { return dragOverrides[i]?.left ?? tags[i].left; }
|
|
155
|
+
|
|
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
|
+
type BallState = { startedAt: number; duration: number; pendingValue: string };
|
|
161
|
+
const ballStates = new SvelteMap<number, BallState>();
|
|
162
|
+
// Element-ref arrays are `$state` so Svelte 5 considers
|
|
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.
|
|
165
|
+
const ballEls: HTMLSpanElement[] = $state([]);
|
|
166
|
+
|
|
167
|
+
// --- Element refs --------------------------------------------------------
|
|
168
|
+
let stageEl: HTMLDivElement | undefined = $state();
|
|
169
|
+
let boxEl: HTMLDivElement | undefined = $state();
|
|
170
|
+
const tagEls: HTMLSpanElement[] = $state([]);
|
|
171
|
+
const lineEls: SVGLineElement[] = $state([]);
|
|
172
|
+
const knotEls: HTMLSpanElement[] = $state([]);
|
|
173
|
+
|
|
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
|
+
function anchorPoint(
|
|
179
|
+
anchor: Anchor,
|
|
180
|
+
cx = 50,
|
|
181
|
+
cy = 50,
|
|
182
|
+
w = boxSize.w,
|
|
183
|
+
h = boxSize.h,
|
|
184
|
+
): { x: number; y: number } {
|
|
185
|
+
const halfW = w / 2, halfH = h / 2;
|
|
186
|
+
if (anchor.side === 'inside') {
|
|
187
|
+
return {
|
|
188
|
+
x: cx - halfW + (anchor.x / 100) * w,
|
|
189
|
+
y: cy - halfH + (anchor.y / 100) * h,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const t = anchor.pos / 100;
|
|
193
|
+
switch (anchor.side) {
|
|
194
|
+
case 'top': return { x: cx - halfW + t * w, y: cy - halfH };
|
|
195
|
+
case 'bottom': return { x: cx - halfW + t * w, y: cy + halfH };
|
|
196
|
+
case 'left': return { x: cx - halfW, y: cy - halfH + t * h };
|
|
197
|
+
case 'right': return { x: cx + halfW, y: cy - halfH + t * h };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Resolve the active CSS-var name for each control. Reads `boxValues` (not
|
|
202
|
+
// `currentValues`) so the central component only repaints once the ball
|
|
203
|
+
// commits its payload at impact.
|
|
204
|
+
function resolveControl(name: TagControl): string | undefined {
|
|
205
|
+
const idx = tags.findIndex(t => t.controls === name);
|
|
206
|
+
return idx >= 0 ? boxValues[idx] : undefined;
|
|
207
|
+
}
|
|
208
|
+
const surfaceVar = $derived(resolveControl('surface'));
|
|
209
|
+
const radiusVar = $derived(resolveControl('radius'));
|
|
210
|
+
const borderColorVar = $derived(resolveControl('border-color'));
|
|
211
|
+
const borderWidthVar = $derived(resolveControl('border-width'));
|
|
212
|
+
const fontFamilyVar = $derived(resolveControl('font-family'));
|
|
213
|
+
|
|
214
|
+
function asVar(name: string | undefined): string | undefined {
|
|
215
|
+
return name ? `var(${name})` : undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --- Animation loop: kite strings + energy balls ------------------------
|
|
219
|
+
function syncFrame() {
|
|
220
|
+
if (!stageEl) return;
|
|
221
|
+
const stageRect = stageEl.getBoundingClientRect();
|
|
222
|
+
if (stageRect.width === 0 || stageRect.height === 0) return;
|
|
223
|
+
|
|
224
|
+
// Box geometry in stage-% space — measured from the live rect so that
|
|
225
|
+
// intrinsic sizing (content + padding) drives anchor placement.
|
|
226
|
+
let boxCx = 50, boxCy = 50;
|
|
227
|
+
let boxW = boxSize.w, boxH = boxSize.h;
|
|
228
|
+
if (boxEl) {
|
|
229
|
+
const br = boxEl.getBoundingClientRect();
|
|
230
|
+
if (br.width > 0 && br.height > 0) {
|
|
231
|
+
boxCx = ((br.left + br.width / 2) - stageRect.left) / stageRect.width * 100;
|
|
232
|
+
boxCy = ((br.top + br.height / 2) - stageRect.top) / stageRect.height * 100;
|
|
233
|
+
boxW = (br.width / stageRect.width) * 100;
|
|
234
|
+
boxH = (br.height / stageRect.height) * 100;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const now = performance.now();
|
|
239
|
+
|
|
240
|
+
for (let i = 0; i < tags.length; i++) {
|
|
241
|
+
const tagEl = tagEls[i];
|
|
242
|
+
const lineEl = lineEls[i];
|
|
243
|
+
if (!tagEl || !lineEl) continue;
|
|
244
|
+
|
|
245
|
+
// Tag-side endpoint: pill cap (corner-radius circle) center on the
|
|
246
|
+
// side closest to the central component.
|
|
247
|
+
const pill = tagEl.querySelector('.ftt-tag') as HTMLElement | null;
|
|
248
|
+
const target = (pill ?? tagEl) as HTMLElement;
|
|
249
|
+
const r = target.getBoundingClientRect();
|
|
250
|
+
|
|
251
|
+
let radius = 0;
|
|
252
|
+
if (pill) {
|
|
253
|
+
const cs = getComputedStyle(pill);
|
|
254
|
+
const raw = parseFloat(cs.borderTopLeftRadius || '0') || 0;
|
|
255
|
+
radius = Math.min(raw, r.height / 2);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const onRight = tags[i].left > 50;
|
|
259
|
+
const px = onRight ? r.left + radius : r.right - radius;
|
|
260
|
+
const py = r.top + r.height / 2;
|
|
261
|
+
|
|
262
|
+
const x1 = (px - stageRect.left) / stageRect.width * 100;
|
|
263
|
+
const y1 = (py - stageRect.top) / stageRect.height * 100;
|
|
264
|
+
|
|
265
|
+
lineEl.setAttribute('x1', x1.toFixed(3));
|
|
266
|
+
lineEl.setAttribute('y1', y1.toFixed(3));
|
|
267
|
+
|
|
268
|
+
// Box-side endpoint + knot — recomputed from live box geometry.
|
|
269
|
+
const a = anchorPoint(tags[i].anchor, boxCx, boxCy, boxW, boxH);
|
|
270
|
+
lineEl.setAttribute('x2', a.x.toFixed(3));
|
|
271
|
+
lineEl.setAttribute('y2', a.y.toFixed(3));
|
|
272
|
+
const knotEl = knotEls[i];
|
|
273
|
+
if (knotEl) {
|
|
274
|
+
knotEl.style.left = `${a.x.toFixed(3)}%`;
|
|
275
|
+
knotEl.style.top = `${a.y.toFixed(3)}%`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Energy ball traveling tag → box along the same line.
|
|
279
|
+
const ballEl = ballEls[i];
|
|
280
|
+
const state = ballStates.get(i);
|
|
281
|
+
if (!ballEl) continue;
|
|
282
|
+
if (!state) {
|
|
283
|
+
ballEl.style.opacity = '0';
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const elapsed = now - state.startedAt;
|
|
287
|
+
const t = Math.min(1, elapsed / state.duration);
|
|
288
|
+
if (t >= 1) {
|
|
289
|
+
// Commit the box's token swap at impact so its appearance changes in
|
|
290
|
+
// sync with the bloop (the "pops up and grows larger" beat). Until
|
|
291
|
+
// this point the box keeps rendering the previous value.
|
|
292
|
+
boxOverrides = { ...boxOverrides, [i]: state.pendingValue };
|
|
293
|
+
ballStates.delete(i);
|
|
294
|
+
ballEl.style.opacity = '0';
|
|
295
|
+
triggerBloop();
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const x2 = parseFloat(lineEl.getAttribute('x2') || '0');
|
|
299
|
+
const y2 = parseFloat(lineEl.getAttribute('y2') || '0');
|
|
300
|
+
const eased = 1 - Math.pow(1 - t, 3); // ease-out cubic
|
|
301
|
+
const bx = x1 + (x2 - x1) * eased;
|
|
302
|
+
const by = y1 + (y2 - y1) * eased;
|
|
303
|
+
ballEl.style.left = `${bx.toFixed(3)}%`;
|
|
304
|
+
ballEl.style.top = `${by.toFixed(3)}%`;
|
|
305
|
+
ballEl.style.opacity = '1';
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
$effect(() => {
|
|
310
|
+
let rafId = 0;
|
|
311
|
+
const loop = () => {
|
|
312
|
+
syncFrame();
|
|
313
|
+
rafId = requestAnimationFrame(loop);
|
|
314
|
+
};
|
|
315
|
+
rafId = requestAnimationFrame(loop);
|
|
316
|
+
return () => cancelAnimationFrame(rafId);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// --- Selection / fire sequence ------------------------------------------
|
|
320
|
+
function pickValue(i: number, value: string) {
|
|
321
|
+
openIdx = null;
|
|
322
|
+
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
|
+
overrides = { ...overrides, [i]: value };
|
|
327
|
+
flashingIdx = i;
|
|
328
|
+
window.setTimeout(() => {
|
|
329
|
+
if (flashingIdx === i) flashingIdx = null;
|
|
330
|
+
}, 500);
|
|
331
|
+
ballStates.set(i, { startedAt: performance.now(), duration: 520, pendingValue: value });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function triggerBloop() {
|
|
335
|
+
bloopActive = true;
|
|
336
|
+
window.setTimeout(() => { bloopActive = false; }, 480);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// User-interaction cooldown: any click pauses the auto-loop for at least
|
|
340
|
+
// USER_HOLD_MS so the user can play without being interrupted.
|
|
341
|
+
const USER_HOLD_MS = 4000;
|
|
342
|
+
let lastUserActionAt = 0;
|
|
343
|
+
function noteUserAction() {
|
|
344
|
+
lastUserActionAt = performance.now();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function clickTag(i: number) {
|
|
348
|
+
if (!tags[i].controls) return;
|
|
349
|
+
noteUserAction();
|
|
350
|
+
openIdx = openIdx === i ? null : i;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function userPick(i: number, value: string) {
|
|
354
|
+
// The current value can never be re-picked — the dropdown item is also
|
|
355
|
+
// rendered disabled, this guard is a safety net.
|
|
356
|
+
if (currentValues[i] === value) return;
|
|
357
|
+
noteUserAction();
|
|
358
|
+
pickValue(i, value);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// --- Drag handlers ------------------------------------------------------
|
|
362
|
+
function onTagPointerDown(i: number, e: PointerEvent) {
|
|
363
|
+
dragStart.px = e.clientX;
|
|
364
|
+
dragStart.py = e.clientY;
|
|
365
|
+
dragStart.tagIdx = i;
|
|
366
|
+
dragStart.moved = false;
|
|
367
|
+
(e.currentTarget as Element).setPointerCapture(e.pointerId);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function onTagPointerMove(i: number, e: PointerEvent) {
|
|
371
|
+
if (dragStart.tagIdx !== i || !stageEl) return;
|
|
372
|
+
const dx = e.clientX - dragStart.px;
|
|
373
|
+
const dy = e.clientY - dragStart.py;
|
|
374
|
+
if (!dragStart.moved && Math.hypot(dx, dy) > DRAG_THRESHOLD_PX) {
|
|
375
|
+
dragStart.moved = true;
|
|
376
|
+
draggingIdx = i;
|
|
377
|
+
openIdx = null;
|
|
378
|
+
noteUserAction(); // pause auto-cycle while dragging
|
|
379
|
+
}
|
|
380
|
+
if (dragStart.moved) {
|
|
381
|
+
const r = stageEl.getBoundingClientRect();
|
|
382
|
+
const x = ((e.clientX - r.left) / r.width) * 100;
|
|
383
|
+
const y = ((e.clientY - r.top) / r.height) * 100;
|
|
384
|
+
dragOverrides = { ...dragOverrides, [i]: { top: y, left: x } };
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function onTagPointerUp(i: number, e: PointerEvent) {
|
|
389
|
+
if (dragStart.tagIdx !== i) return;
|
|
390
|
+
(e.currentTarget as Element).releasePointerCapture(e.pointerId);
|
|
391
|
+
if (dragStart.moved) {
|
|
392
|
+
const p = dragOverrides[i];
|
|
393
|
+
// eslint-disable-next-line no-console
|
|
394
|
+
console.log(
|
|
395
|
+
`[${i}] ${tags[i].controls ?? '?'}: top: ${p.top.toFixed(1)}, left: ${p.left.toFixed(1)},`,
|
|
396
|
+
);
|
|
397
|
+
} else {
|
|
398
|
+
clickTag(i);
|
|
399
|
+
}
|
|
400
|
+
draggingIdx = null;
|
|
401
|
+
dragStart.tagIdx = -1;
|
|
402
|
+
dragStart.moved = false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// --- Auto-cycle ----------------------------------------------------------
|
|
406
|
+
let autoAlive = false;
|
|
407
|
+
let lastAutoTagIdx: number | null = null;
|
|
408
|
+
const sleep = (ms: number) => new Promise(r => window.setTimeout(r, ms));
|
|
409
|
+
|
|
410
|
+
const STROBE_STEP_MS = 125;
|
|
411
|
+
const BREATH_MS = 250;
|
|
412
|
+
|
|
413
|
+
async function autoLoop() {
|
|
414
|
+
while (autoAlive) {
|
|
415
|
+
await sleep(2400 + Math.random() * 2400);
|
|
416
|
+
if (!autoAlive) break;
|
|
417
|
+
|
|
418
|
+
// Hold off if the user clicked anything recently. Re-check after each
|
|
419
|
+
// partial sleep so a fresh click resets the wait.
|
|
420
|
+
while (autoAlive) {
|
|
421
|
+
const sinceUser = performance.now() - lastUserActionAt;
|
|
422
|
+
if (sinceUser >= USER_HOLD_MS) break;
|
|
423
|
+
await sleep(USER_HOLD_MS - sinceUser);
|
|
424
|
+
}
|
|
425
|
+
if (!autoAlive) break;
|
|
426
|
+
|
|
427
|
+
if (openIdx !== null) continue; // user is interacting
|
|
428
|
+
|
|
429
|
+
// Never the same tag twice in a row.
|
|
430
|
+
const tagCandidates = tags
|
|
431
|
+
.map((_, idx) => idx)
|
|
432
|
+
.filter(idx => idx !== lastAutoTagIdx && tags[idx].controls);
|
|
433
|
+
if (tagCandidates.length === 0) continue;
|
|
434
|
+
const i = tagCandidates[Math.floor(Math.random() * tagCandidates.length)];
|
|
435
|
+
const tag = tags[i];
|
|
436
|
+
const opts = valueOptions[tag.controls!];
|
|
437
|
+
|
|
438
|
+
// Never the same token twice in a row: exclude the currently-active
|
|
439
|
+
// value from the candidate set.
|
|
440
|
+
const currentIdx = opts.indexOf(currentValues[i]);
|
|
441
|
+
const candidates = opts
|
|
442
|
+
.map((_, k) => k)
|
|
443
|
+
.filter(k => k !== currentIdx);
|
|
444
|
+
if (candidates.length === 0) continue;
|
|
445
|
+
const finalIdx = candidates[Math.floor(Math.random() * candidates.length)];
|
|
446
|
+
|
|
447
|
+
openIdx = i;
|
|
448
|
+
|
|
449
|
+
// Breath: open menu, let it sit before any highlight appears.
|
|
450
|
+
await sleep(BREATH_MS);
|
|
451
|
+
if (!autoAlive || openIdx !== i) continue;
|
|
452
|
+
|
|
453
|
+
// Step from the top down to the chosen item, one step per STROBE_STEP_MS.
|
|
454
|
+
// Landing position is wherever finalIdx lands — could be the first item
|
|
455
|
+
// (no stepping), could be the fourth.
|
|
456
|
+
for (let k = 0; k <= finalIdx; k++) {
|
|
457
|
+
if (!autoAlive || openIdx !== i) break;
|
|
458
|
+
strobeIdx = k;
|
|
459
|
+
await sleep(STROBE_STEP_MS);
|
|
460
|
+
}
|
|
461
|
+
if (!autoAlive || openIdx !== i) continue;
|
|
462
|
+
|
|
463
|
+
// Blink twice on the selection (off/on x2).
|
|
464
|
+
for (let blink = 0; blink < 2; blink++) {
|
|
465
|
+
if (!autoAlive || openIdx !== i) break;
|
|
466
|
+
strobeIdx = null;
|
|
467
|
+
await sleep(STROBE_STEP_MS);
|
|
468
|
+
if (!autoAlive || openIdx !== i) break;
|
|
469
|
+
strobeIdx = finalIdx;
|
|
470
|
+
await sleep(STROBE_STEP_MS);
|
|
471
|
+
}
|
|
472
|
+
if (!autoAlive || openIdx !== i) continue;
|
|
473
|
+
|
|
474
|
+
pickValue(i, opts[finalIdx]);
|
|
475
|
+
lastAutoTagIdx = i;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
$effect(() => {
|
|
480
|
+
if (!autoplay) return;
|
|
481
|
+
autoAlive = true;
|
|
482
|
+
autoLoop();
|
|
483
|
+
return () => { autoAlive = false; };
|
|
484
|
+
});
|
|
485
|
+
</script>
|
|
486
|
+
|
|
487
|
+
<div
|
|
488
|
+
class="ftt-stage"
|
|
489
|
+
bind:this={stageEl}
|
|
490
|
+
style:--ftt-bob-duration="{duration}s"
|
|
491
|
+
style:--ftt-bob-distance="{-distance}px"
|
|
492
|
+
>
|
|
493
|
+
<!-- Kite strings -->
|
|
494
|
+
<svg class="ftt-strings" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
|
|
495
|
+
{#each tags as tag, i (i)}
|
|
496
|
+
{@const a = anchorPoint(tag.anchor)}
|
|
497
|
+
<line
|
|
498
|
+
bind:this={lineEls[i]}
|
|
499
|
+
x1={tag.left} y1={tag.top}
|
|
500
|
+
x2={a.x} y2={a.y}
|
|
501
|
+
class="ftt-string"
|
|
502
|
+
class:ftt-live={ballStates.has(i)}
|
|
503
|
+
/>
|
|
504
|
+
{/each}
|
|
505
|
+
</svg>
|
|
506
|
+
|
|
507
|
+
<!-- Central component — driven by the active token of each tag. Sized
|
|
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. -->
|
|
511
|
+
<div
|
|
512
|
+
bind:this={boxEl}
|
|
513
|
+
class="ftt-box"
|
|
514
|
+
class:ftt-bloop={bloopActive}
|
|
515
|
+
style:min-width="{boxSize.w}%"
|
|
516
|
+
style:min-height="{boxSize.h}%"
|
|
517
|
+
style:background={surfaceVar ? `color-mix(in srgb, var(${surfaceVar}) 50%, transparent)` : undefined}
|
|
518
|
+
style:border-radius={asVar(radiusVar)}
|
|
519
|
+
style:border-color={asVar(borderColorVar)}
|
|
520
|
+
style:border-width={asVar(borderWidthVar)}
|
|
521
|
+
>
|
|
522
|
+
<span
|
|
523
|
+
class="ftt-box-label"
|
|
524
|
+
style:font-family={asVar(fontFamilyVar)}
|
|
525
|
+
>I'm a button</span>
|
|
526
|
+
</div>
|
|
527
|
+
|
|
528
|
+
<!-- Anchor knots on the box. Initial position uses anchorPoint() defaults;
|
|
529
|
+
syncFrame imperatively updates each frame from the box's live rect. -->
|
|
530
|
+
{#each tags as tag, i (i)}
|
|
531
|
+
{@const a = anchorPoint(tag.anchor)}
|
|
532
|
+
<span
|
|
533
|
+
bind:this={knotEls[i]}
|
|
534
|
+
class="ftt-knot"
|
|
535
|
+
style:left="{a.x}%"
|
|
536
|
+
style:top="{a.y}%"
|
|
537
|
+
aria-hidden="true"
|
|
538
|
+
></span>
|
|
539
|
+
{/each}
|
|
540
|
+
|
|
541
|
+
<!-- Energy balls — positioned each frame by syncFrame() while in flight. -->
|
|
542
|
+
{#each tags as _t, i (i)}
|
|
543
|
+
<span class="ftt-energy-ball" bind:this={ballEls[i]} aria-hidden="true"></span>
|
|
544
|
+
{/each}
|
|
545
|
+
|
|
546
|
+
<!-- Floating tags. -->
|
|
547
|
+
{#each tags as tag, i (i)}
|
|
548
|
+
<span
|
|
549
|
+
bind:this={tagEls[i]}
|
|
550
|
+
class="ftt-float"
|
|
551
|
+
class:ftt-flash={flashingIdx === i}
|
|
552
|
+
class:ftt-open={openIdx === i}
|
|
553
|
+
class:ftt-dragging={draggingIdx === i}
|
|
554
|
+
style:top="{tagTop(i)}%"
|
|
555
|
+
style:left="{tagLeft(i)}%"
|
|
556
|
+
style:animation-delay="{tag.delay ?? 0}s"
|
|
557
|
+
style:--ftt-rot="{tag.rotate ?? 0}deg"
|
|
558
|
+
>
|
|
559
|
+
{#if tag.controls}
|
|
560
|
+
<span class="ftt-float-property">{controlLabels[tag.controls]}</span>
|
|
561
|
+
{/if}
|
|
562
|
+
<button
|
|
563
|
+
type="button"
|
|
564
|
+
class="ftt-tag-trigger"
|
|
565
|
+
onpointerdown={(e) => onTagPointerDown(i, e)}
|
|
566
|
+
onpointermove={(e) => onTagPointerMove(i, e)}
|
|
567
|
+
onpointerup={(e) => onTagPointerUp(i, e)}
|
|
568
|
+
aria-haspopup="listbox"
|
|
569
|
+
aria-expanded={openIdx === i}
|
|
570
|
+
>
|
|
571
|
+
<span class="ftt-tag">
|
|
572
|
+
{#if tag.icon}<span class="ftt-tag-icon"><i class={tag.icon}></i></span>{/if}
|
|
573
|
+
<span class="ftt-tag-label">{currentValues[i]}</span>
|
|
574
|
+
</span>
|
|
575
|
+
</button>
|
|
576
|
+
|
|
577
|
+
{#if openIdx === i && tag.controls}
|
|
578
|
+
{@const opts = valueOptions[tag.controls]}
|
|
579
|
+
{@const strobeValue = strobeIdx !== null ? opts[strobeIdx] : null}
|
|
580
|
+
<div class="ftt-dropdown-wrap">
|
|
581
|
+
<MenuSelect
|
|
582
|
+
items={opts.map((opt) => ({ value: opt, label: opt }))}
|
|
583
|
+
value={currentValues[i]}
|
|
584
|
+
forceHoverValue={strobeValue}
|
|
585
|
+
onchange={(v) => userPick(i, v)}
|
|
586
|
+
/>
|
|
587
|
+
</div>
|
|
588
|
+
{/if}
|
|
589
|
+
</span>
|
|
590
|
+
{/each}
|
|
591
|
+
</div>
|
|
592
|
+
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
</div>
|
|
48
48
|
|
|
49
49
|
<style lang="scss">
|
|
50
|
+
@use '../styles/padding' as *;
|
|
51
|
+
|
|
50
52
|
:global(:root) {
|
|
51
53
|
/* Save (default) */
|
|
52
54
|
--inlineeditactions-save-default-surface: var(--surface-success-low);
|
|
@@ -110,7 +112,7 @@
|
|
|
110
112
|
color: var(--inlineeditactions-save-default-text);
|
|
111
113
|
border: var(--inlineeditactions-save-default-border-width) solid var(--inlineeditactions-save-default-border);
|
|
112
114
|
border-radius: var(--inlineeditactions-save-default-radius);
|
|
113
|
-
|
|
115
|
+
@include themed-padding(--inlineeditactions-save-default-padding, $h: 2);
|
|
114
116
|
font-size: var(--inlineeditactions-save-default-icon-size);
|
|
115
117
|
|
|
116
118
|
&:hover:not(:disabled),
|
|
@@ -119,7 +121,7 @@
|
|
|
119
121
|
color: var(--inlineeditactions-save-hover-text);
|
|
120
122
|
border: var(--inlineeditactions-save-hover-border-width) solid var(--inlineeditactions-save-hover-border);
|
|
121
123
|
border-radius: var(--inlineeditactions-save-hover-radius);
|
|
122
|
-
|
|
124
|
+
@include themed-padding(--inlineeditactions-save-hover-padding, $h: 2);
|
|
123
125
|
font-size: var(--inlineeditactions-save-hover-icon-size);
|
|
124
126
|
}
|
|
125
127
|
}
|
|
@@ -129,7 +131,7 @@
|
|
|
129
131
|
color: var(--inlineeditactions-cancel-default-text);
|
|
130
132
|
border: var(--inlineeditactions-cancel-default-border-width) solid var(--inlineeditactions-cancel-default-border);
|
|
131
133
|
border-radius: var(--inlineeditactions-cancel-default-radius);
|
|
132
|
-
|
|
134
|
+
@include themed-padding(--inlineeditactions-cancel-default-padding, $h: 2);
|
|
133
135
|
font-size: var(--inlineeditactions-cancel-default-icon-size);
|
|
134
136
|
|
|
135
137
|
&:hover:not(:disabled),
|
|
@@ -138,7 +140,7 @@
|
|
|
138
140
|
color: var(--inlineeditactions-cancel-hover-text);
|
|
139
141
|
border: var(--inlineeditactions-cancel-hover-border-width) solid var(--inlineeditactions-cancel-hover-border);
|
|
140
142
|
border-radius: var(--inlineeditactions-cancel-hover-radius);
|
|
141
|
-
|
|
143
|
+
@include themed-padding(--inlineeditactions-cancel-hover-padding, $h: 2);
|
|
142
144
|
font-size: var(--inlineeditactions-cancel-hover-icon-size);
|
|
143
145
|
}
|
|
144
146
|
}
|