@motion-proto/live-tokens 0.6.2 → 0.8.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.
Files changed (232) hide show
  1. package/README.md +14 -13
  2. package/dist-plugin/index.cjs +854 -226
  3. package/dist-plugin/index.d.cts +2 -1
  4. package/dist-plugin/index.d.ts +2 -1
  5. package/dist-plugin/index.js +852 -225
  6. package/package.json +26 -40
  7. package/src/{styles → app}/site.css +1 -1
  8. package/src/{component-editor → editor/component-editor}/BadgeEditor.svelte +8 -82
  9. package/src/{component-editor → editor/component-editor}/CalloutEditor.svelte +4 -4
  10. package/src/{component-editor → editor/component-editor}/CardEditor.svelte +28 -76
  11. package/src/{component-editor → editor/component-editor}/CollapsibleSectionEditor.svelte +37 -30
  12. package/src/{component-editor → editor/component-editor}/CornerBadgeEditor.svelte +31 -93
  13. package/src/{component-editor → editor/component-editor}/DialogEditor.svelte +60 -57
  14. package/src/editor/component-editor/ImageEditor.svelte +30 -0
  15. package/src/{component-editor → editor/component-editor}/InlineEditActionsEditor.svelte +6 -4
  16. package/src/editor/component-editor/MenuSelectEditor.svelte +160 -0
  17. package/src/{component-editor → editor/component-editor}/NotificationEditor.svelte +67 -38
  18. package/src/{component-editor → editor/component-editor}/ProgressBarEditor.svelte +5 -4
  19. package/src/{component-editor → editor/component-editor}/RadioButtonEditor.svelte +3 -3
  20. package/src/editor/component-editor/SectionDividerEditor.svelte +565 -0
  21. package/src/{component-editor → editor/component-editor}/SegmentedControlEditor.svelte +2 -2
  22. package/src/{component-editor → editor/component-editor}/StandardButtonsEditor.svelte +29 -21
  23. package/src/{component-editor → editor/component-editor}/TabBarEditor.svelte +9 -14
  24. package/src/{component-editor → editor/component-editor}/TableEditor.svelte +9 -18
  25. package/src/{component-editor → editor/component-editor}/TooltipEditor.svelte +11 -47
  26. package/src/editor/component-editor/editors.d.ts +10 -0
  27. package/src/{component-editor → editor/component-editor}/registry.ts +28 -18
  28. package/src/{component-editor → editor/component-editor}/scaffolding/AngleDial.svelte +54 -15
  29. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentEditorBase.svelte +3 -51
  30. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileManager.svelte +151 -424
  31. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileMenu.svelte +18 -170
  32. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentsTab.svelte +2 -2
  33. package/src/{component-editor → editor/component-editor}/scaffolding/CopyFromMenu.svelte +44 -4
  34. package/src/{component-editor → editor/component-editor}/scaffolding/FieldsetWrapper.svelte +1 -1
  35. package/src/{component-editor → editor/component-editor}/scaffolding/LinkageChart.svelte +6 -6
  36. package/src/{component-editor → editor/component-editor}/scaffolding/LinkedBlock.svelte +6 -12
  37. package/src/editor/component-editor/scaffolding/NonStylableConfig.svelte +38 -0
  38. package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
  39. package/src/{component-editor → editor/component-editor}/scaffolding/SaveAsDialog.svelte +66 -12
  40. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +85 -0
  41. package/src/editor/component-editor/scaffolding/ShadowBackdropControls.svelte +132 -0
  42. package/src/editor/component-editor/scaffolding/StateBlock.svelte +345 -0
  43. package/src/{component-editor → editor/component-editor}/scaffolding/TokenLayout.svelte +17 -12
  44. package/src/{component-editor → editor/component-editor}/scaffolding/TypeEditor.svelte +13 -1
  45. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +858 -0
  46. package/src/{component-editor → editor/component-editor}/scaffolding/buildTypeGroupTokens.ts +1 -0
  47. package/src/{component-editor → editor/component-editor}/scaffolding/editorContext.ts +19 -9
  48. package/src/{component-editor → editor/component-editor}/scaffolding/linkedBlock.ts +2 -2
  49. package/src/{component-editor → editor/component-editor}/scaffolding/types.ts +25 -0
  50. package/src/{lib → editor/core/components}/componentConfigKeys.ts +8 -0
  51. package/src/{lib → editor/core/components}/componentConfigService.ts +3 -3
  52. package/src/{lib → editor/core/components}/componentPersist.ts +11 -9
  53. package/src/editor/core/flashStatus.ts +30 -0
  54. package/src/{lib → editor/core/fonts}/fontLoader.ts +2 -2
  55. package/src/{lib → editor/core/fonts}/fontMigration.ts +4 -4
  56. package/src/{lib → editor/core/fonts}/fontParse.ts +1 -1
  57. package/src/editor/core/manifests/manifestService.ts +171 -0
  58. package/src/editor/core/palettes/familySwap.ts +99 -0
  59. package/src/{lib → editor/core/palettes}/paletteDerivation.ts +71 -2
  60. package/src/{lib → editor/core/palettes}/tokenRegistry.ts +9 -6
  61. package/src/editor/core/productionPulse.ts +37 -0
  62. package/src/{lib → editor/core/routing}/router.ts +1 -1
  63. package/src/{lib/files/versionedFileResource.ts → editor/core/storage/files/versionedFileResourceClient.ts} +8 -1
  64. package/src/{lib → editor/core/store}/editorCore.ts +24 -8
  65. package/src/{lib → editor/core/store}/editorPersistence.ts +3 -3
  66. package/src/{lib → editor/core/store}/editorRenderer.ts +2 -2
  67. package/src/{lib → editor/core/store}/editorStore.ts +222 -28
  68. package/src/{lib → editor/core/store}/editorTypes.ts +56 -13
  69. package/src/editor/core/store/gradientSource.ts +192 -0
  70. package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
  71. package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
  72. package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
  73. package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
  74. package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
  75. package/src/{lib → editor/core/themes}/migrations/index.ts +10 -0
  76. package/src/{lib → editor/core/themes}/slices/columns.ts +2 -2
  77. package/src/{lib → editor/core/themes}/slices/components.ts +20 -6
  78. package/src/{lib → editor/core/themes}/slices/fonts.ts +1 -1
  79. package/src/{lib → editor/core/themes}/slices/gradients.ts +89 -14
  80. package/src/{lib → editor/core/themes}/slices/overlays.ts +1 -1
  81. package/src/{lib → editor/core/themes}/slices/palettes.ts +1 -1
  82. package/src/{lib → editor/core/themes}/slices/shadows.ts +3 -3
  83. package/src/{lib → editor/core/themes}/themeInit.ts +8 -8
  84. package/src/{lib → editor/core/themes}/themeService.ts +6 -6
  85. package/src/{lib → editor/core/themes}/themeTypes.ts +67 -8
  86. package/src/editor/index.ts +69 -0
  87. package/src/{lib → editor/overlay}/ColumnsOverlay.svelte +0 -1
  88. package/src/{lib → editor/overlay}/LiveEditorOverlay.svelte +80 -129
  89. package/src/{lib → editor/overlay}/columnsOverlay.ts +2 -2
  90. package/src/{pages → editor/pages}/ComponentEditorPage.svelte +12 -12
  91. package/src/{pages → editor/pages}/Editor.svelte +4 -4
  92. package/src/{pages → editor/pages}/EditorShell.svelte +18 -36
  93. package/src/{styles → editor/styles}/ui-editor.css +43 -22
  94. package/src/{styles → editor/styles}/ui-form-controls.css +23 -24
  95. package/src/{ui → editor/ui}/BezierCurveEditor.svelte +119 -68
  96. package/src/{ui → editor/ui}/ColorEditPanel.svelte +13 -13
  97. package/src/{ui → editor/ui}/EditorViewSwitcher.svelte +7 -6
  98. package/src/editor/ui/FileLoadList.svelte +367 -0
  99. package/src/editor/ui/FilePill.svelte +80 -0
  100. package/src/editor/ui/FontStackEditor.svelte +499 -0
  101. package/src/editor/ui/GradientEditor.svelte +690 -0
  102. package/src/{ui → editor/ui}/GradientStopPicker.svelte +12 -4
  103. package/src/editor/ui/ManifestFileManager.svelte +438 -0
  104. package/src/{ui → editor/ui}/PaletteEditor.svelte +180 -673
  105. package/src/editor/ui/ProjectFontsSection.svelte +638 -0
  106. package/src/{ui → editor/ui}/SurfacesTab.svelte +3 -3
  107. package/src/{ui → editor/ui}/TextTab.svelte +3 -3
  108. package/src/editor/ui/ThemeFileManager.svelte +783 -0
  109. package/src/{ui → editor/ui}/UICopyPopover.svelte +4 -4
  110. package/src/{ui → editor/ui}/UIFontFamilySelector.svelte +6 -7
  111. package/src/{ui → editor/ui}/UIFontSizeSelector.svelte +4 -1
  112. package/src/editor/ui/UIInfoPopover.svelte +243 -0
  113. package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
  114. package/src/{ui → editor/ui}/UILineHeightSelector.svelte +5 -5
  115. package/src/{ui → editor/ui}/UILinkToggle.svelte +2 -2
  116. package/src/{ui → editor/ui}/UIPaddingSelector.svelte +6 -6
  117. package/src/{ui → editor/ui}/UIPaletteSelector.svelte +57 -30
  118. package/src/editor/ui/UIPillButton.svelte +168 -0
  119. package/src/{ui → editor/ui}/UIRadio.svelte +2 -2
  120. package/src/{ui → editor/ui}/UIRelinkConfirmPopover.svelte +4 -4
  121. package/src/editor/ui/UISegmentedControl.svelte +114 -0
  122. package/src/editor/ui/UISquareButton.svelte +172 -0
  123. package/src/{ui → editor/ui}/UITokenSelector.svelte +14 -11
  124. package/src/{ui → editor/ui}/UIVariantSelector.svelte +1 -1
  125. package/src/{ui → editor/ui}/VariablesTab.svelte +46 -17
  126. package/src/{ui → editor/ui}/palette/GradientStopEditor.svelte +13 -13
  127. package/src/{ui → editor/ui}/palette/OverridesPanel.svelte +24 -47
  128. package/src/{ui → editor/ui}/palette/PaletteBase.svelte +11 -8
  129. package/src/{ui → editor/ui}/palette/paletteEditorState.ts +1 -1
  130. package/src/editor/ui/palette/paletteMath.ts +275 -0
  131. package/src/{ui → editor/ui}/sections/ColumnsSection.svelte +137 -18
  132. package/src/{ui → editor/ui}/sections/GradientsSection.svelte +8 -8
  133. package/src/{ui → editor/ui}/sections/OverlaysSection.svelte +18 -18
  134. package/src/{ui → editor/ui}/sections/ShadowsSection.svelte +23 -23
  135. package/src/{ui → editor/ui}/sections/TokenScaleTable.svelte +3 -3
  136. package/src/{components → system/components}/Badge.svelte +0 -36
  137. package/src/{components → system/components}/Button.svelte +2 -2
  138. package/src/{components → system/components}/Card.svelte +34 -60
  139. package/src/{components → system/components}/CollapsibleSection.svelte +25 -2
  140. package/src/{components → system/components}/CornerBadge.svelte +8 -24
  141. package/src/{components → system/components}/Dialog.svelte +1 -1
  142. package/src/system/components/FloatingTokenTags.css +275 -0
  143. package/src/system/components/FloatingTokenTags.svelte +543 -0
  144. package/src/{components → system/components}/InlineEditActions.svelte +6 -4
  145. package/src/system/components/MenuSelect.svelte +229 -0
  146. package/src/{components → system/components}/Notification.svelte +8 -1
  147. package/src/{components → system/components}/ProgressBar.svelte +29 -11
  148. package/src/system/components/SectionDivider.svelte +560 -0
  149. package/src/{components → system/components}/SegmentedControl.svelte +49 -43
  150. package/src/{components → system/components}/TabBar.svelte +81 -65
  151. package/src/{components → system/components}/Table.svelte +17 -3
  152. package/src/{components → system/components}/Tooltip.svelte +6 -4
  153. package/src/system/styles/CONVENTIONS.md +178 -0
  154. package/src/system/styles/fonts.css +20 -0
  155. package/src/system/styles/tokens.css +601 -0
  156. package/src/system/styles/tokens.generated.css +544 -0
  157. package/src/component-editor/ImageEditor.svelte +0 -74
  158. package/src/component-editor/SectionDividerEditor.svelte +0 -265
  159. package/src/component-editor/scaffolding/DividerEditor.svelte +0 -94
  160. package/src/component-editor/scaffolding/GradientCard.svelte +0 -296
  161. package/src/component-editor/scaffolding/NonStylableConfig.svelte +0 -62
  162. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +0 -37
  163. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +0 -61
  164. package/src/component-editor/scaffolding/StateBlock.svelte +0 -132
  165. package/src/component-editor/scaffolding/VariantGroup.svelte +0 -310
  166. package/src/components/SectionDivider.svelte +0 -483
  167. package/src/data/google-fonts.json +0 -75
  168. package/src/lib/index.ts +0 -68
  169. package/src/lib/presetService.ts +0 -214
  170. package/src/lib/productionPulse.ts +0 -32
  171. package/src/styles/fonts.css +0 -30
  172. package/src/styles/tokens.css +0 -1324
  173. package/src/ui/FontStackEditor.svelte +0 -361
  174. package/src/ui/GradientEditor.svelte +0 -470
  175. package/src/ui/PresetFileManager.svelte +0 -1116
  176. package/src/ui/ProjectFontsSection.svelte +0 -645
  177. package/src/ui/ThemeFileManager.svelte +0 -1020
  178. package/src/ui/UnsavedComponentsDialog.svelte +0 -315
  179. /package/src/{component-editor → editor/component-editor}/index.ts +0 -0
  180. /package/src/{component-editor → editor/component-editor}/scaffolding/DemoHeader.svelte +0 -0
  181. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSectionType.ts +0 -0
  182. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSources.ts +0 -0
  183. /package/src/{component-editor → editor/component-editor}/scaffolding/defaultSections.ts +0 -0
  184. /package/src/{component-editor → editor/component-editor}/scaffolding/siblings.ts +0 -0
  185. /package/src/{lib → editor/core}/cssVarSync.ts +0 -0
  186. /package/src/{lib → editor/core/palettes}/oklch.ts +0 -0
  187. /package/src/{lib → editor/core/routing}/navLinkTypes.ts +0 -0
  188. /package/src/{lib → editor/core/routing}/parentRouteStore.ts +0 -0
  189. /package/src/{lib → editor/core/storage}/storage.ts +0 -0
  190. /package/src/{lib → editor/core/store}/editorConfig.ts +0 -0
  191. /package/src/{lib → editor/core/store}/editorConfigStore.ts +0 -0
  192. /package/src/{lib → editor/core/store}/editorKeybindings.ts +0 -0
  193. /package/src/{lib → editor/core/store}/editorViewStore.ts +0 -0
  194. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +0 -0
  195. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +0 -0
  196. /package/src/{lib → editor/core/themes}/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +0 -0
  197. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +0 -0
  198. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +0 -0
  199. /package/src/{lib → editor/core/themes}/migrations/2026-05-10-sectiondivider-gradient-stops.ts +0 -0
  200. /package/src/{lib → editor/core/themes}/migrations/2026-05-13-primary-to-brand.ts +0 -0
  201. /package/src/{lib → editor/core/themes}/parsers/globalRootBlock.ts +0 -0
  202. /package/src/{lib → editor/core/themes}/slices/domainVars.ts +0 -0
  203. /package/src/{lib → editor/overlay}/overlayState.ts +0 -0
  204. /package/src/{pages → editor/pages}/ComponentEditorPage.svelte.d.ts +0 -0
  205. /package/src/{pages → editor/pages}/Editor.svelte.d.ts +0 -0
  206. /package/src/{ui → editor/ui}/Toggle.svelte +0 -0
  207. /package/src/{ui → editor/ui}/UIDialog.svelte +0 -0
  208. /package/src/{ui → editor/ui}/UIFontWeightSelector.svelte +0 -0
  209. /package/src/{ui → editor/ui}/UIOptionItem.svelte +0 -0
  210. /package/src/{ui → editor/ui}/UIOptionList.svelte +0 -0
  211. /package/src/{ui → editor/ui}/UIRadioGroup.svelte +0 -0
  212. /package/src/{lib → editor/ui}/copyPopover.ts +0 -0
  213. /package/src/{ui → editor/ui}/curveEngine.ts +0 -0
  214. /package/src/{ui → editor/ui}/index.ts +0 -0
  215. /package/src/{ui → editor/ui}/keepInViewport.ts +0 -0
  216. /package/src/{ui → editor/ui}/palette/ScaleCurveEditor.svelte +0 -0
  217. /package/src/{lib → editor/ui}/scrollSection.ts +0 -0
  218. /package/src/{ui → editor/ui}/sections/tokenScales.ts +0 -0
  219. /package/src/{ui → editor/ui}/variantScales.ts +0 -0
  220. /package/src/{assets → system/assets}/newspaper.webp +0 -0
  221. /package/src/{assets → system/assets}/offering.webp +0 -0
  222. /package/src/{components → system/components}/Callout.svelte +0 -0
  223. /package/src/{components → system/components}/Image.svelte +0 -0
  224. /package/src/{components → system/components}/RadioButton.svelte +0 -0
  225. /package/src/{components → system/components}/types.ts +0 -0
  226. /package/src/{styles → system/styles}/_padding.scss +0 -0
  227. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
  228. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
  229. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
  230. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
  231. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
  232. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin.woff2 +0 -0
@@ -0,0 +1,543 @@
1
+ <script module lang="ts">
2
+ export type AnchorSide = 'top' | 'right' | 'bottom' | 'left';
3
+
4
+ export type TagControl = 'surface' | 'radius' | 'border-color' | 'border-width' | 'font-family';
5
+
6
+ /**
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.
9
+ */
10
+ export type Anchor =
11
+ | { side: AnchorSide; pos: number }
12
+ | { side: 'inside'; x: number; y: number };
13
+
14
+ export interface FloatingTag {
15
+ icon?: string;
16
+ /** Initial value; typically a CSS-variable name. */
17
+ label: string;
18
+ /** Tag center, in % of the stage. */
19
+ top: number;
20
+ left: number;
21
+ /** Bob delay, seconds. Negative values offset the start. */
22
+ delay?: number;
23
+ /** Static tilt, degrees. */
24
+ rotate?: number;
25
+ anchor: Anchor;
26
+ /** Visual property of the central box this tag drives. */
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';
32
+ }
33
+ </script>
34
+
35
+ <script lang="ts">
36
+ import MenuSelect from './MenuSelect.svelte';
37
+ import { SvelteMap } from 'svelte/reactivity';
38
+ // `.ftt-tag` is hand-rolled (not Badge) so editing badge-* tokens doesn't
39
+ // repaint the playground. The dropdown uses MenuSelect on purpose.
40
+ import './FloatingTokenTags.css';
41
+
42
+ interface Props {
43
+ tags?: FloatingTag[];
44
+ duration?: number;
45
+ distance?: number;
46
+ boxSize?: { w: number; h: number };
47
+ autoplay?: boolean;
48
+ }
49
+
50
+ const controlLabels: Record<TagControl, string> = {
51
+ 'surface': 'Surface color',
52
+ 'radius': 'Corner radius',
53
+ 'border-color': 'Border color',
54
+ 'border-width': 'Border width',
55
+ 'font-family': 'Font family',
56
+ };
57
+
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.
61
+ const valueOptions: Record<TagControl, string[]> = {
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'],
67
+ };
68
+
69
+ const defaultTags: FloatingTag[] = [
70
+ {
71
+ icon: 'fas fa-fill-drip',
72
+ label: '--surface-brand-high',
73
+ top: 15, left: 20, delay: 0, rotate: 3,
74
+ anchor: { side: 'inside', x: 10, y: 40 },
75
+ controls: 'surface',
76
+ placement: 'top',
77
+ pivot: 'right-cap',
78
+ },
79
+ {
80
+ icon: 'fas fa-font',
81
+ label: '--font-display',
82
+ top: 5, left: 45, delay: -0.9, rotate: 2,
83
+ anchor: { side: 'inside', x: 78, y: 28 },
84
+ controls: 'font-family',
85
+ placement: 'top',
86
+ },
87
+ {
88
+ icon: 'fa-solid fa-bezier-curve',
89
+ label: '--radius-2xl',
90
+ top: 23, left: 75, delay: -1.8, rotate: 2,
91
+ anchor: { side: 'top', pos: 100 },
92
+ controls: 'radius',
93
+ placement: 'top',
94
+ },
95
+ {
96
+ icon: 'fas fa-paint-roller',
97
+ label: '--border-brand-strong',
98
+ top: 79, left: 72, delay: -3.6, rotate: -2,
99
+ anchor: { side: 'bottom', pos: 75 },
100
+ controls: 'border-color',
101
+ placement: 'top',
102
+ },
103
+ {
104
+ icon: 'fas fa-grip-lines',
105
+ label: '--border-width-3',
106
+ top: 79, left: 28, delay: -5.2, rotate: -4,
107
+ anchor: { side: 'bottom', pos: 25 },
108
+ controls: 'border-width',
109
+ placement: 'top',
110
+ },
111
+ ];
112
+
113
+ let {
114
+ tags = defaultTags,
115
+ duration = 7,
116
+ distance = 8,
117
+ boxSize = { w: 14, h: 11 },
118
+ autoplay = true,
119
+ }: Props = $props();
120
+
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.
124
+ const defaultLabels = $derived(tags.map(t => t.label));
125
+ let overrides = $state<Record<number, string>>({});
126
+ let boxOverrides = $state<Record<number, string>>({});
127
+ const currentValues = $derived(defaultLabels.map((d, i) => overrides[i] ?? d));
128
+ const boxValues = $derived(defaultLabels.map((d, i) => boxOverrides[i] ?? d));
129
+
130
+ let openIdx = $state<number | null>(null);
131
+ let strobeIdx = $state<number | null>(null);
132
+ let flashingIdx = $state<number | null>(null);
133
+ let bloopActive = $state(false);
134
+
135
+ let dragOverrides = $state<Record<number, { top: number; left: number }>>({});
136
+ let draggingIdx = $state<number | null>(null);
137
+ const DRAG_THRESHOLD_PX = 4;
138
+ const dragStart = { px: 0, py: 0, tagIdx: -1, moved: false };
139
+
140
+ function tagTop(i: number): number { return dragOverrides[i]?.top ?? tags[i].top; }
141
+ function tagLeft(i: number): number { return dragOverrides[i]?.left ?? tags[i].left; }
142
+
143
+ type BallState = { startedAt: number; duration: number; pendingValue: string };
144
+ const ballStates = new SvelteMap<number, BallState>();
145
+ // `$state` so `bind:this={ballEls[i]}` is a reactive binding target.
146
+ const ballEls: HTMLSpanElement[] = $state([]);
147
+
148
+ let stageEl: HTMLDivElement | undefined = $state();
149
+ let boxEl: HTMLDivElement | undefined = $state();
150
+ const tagEls: HTMLSpanElement[] = $state([]);
151
+ const lineEls: SVGLineElement[] = $state([]);
152
+ const knotEls: HTMLSpanElement[] = $state([]);
153
+
154
+ function anchorPoint(
155
+ anchor: Anchor,
156
+ cx = 50,
157
+ cy = 50,
158
+ w = boxSize.w,
159
+ h = boxSize.h,
160
+ ): { x: number; y: number } {
161
+ const halfW = w / 2, halfH = h / 2;
162
+ if (anchor.side === 'inside') {
163
+ return {
164
+ x: cx - halfW + (anchor.x / 100) * w,
165
+ y: cy - halfH + (anchor.y / 100) * h,
166
+ };
167
+ }
168
+ const t = anchor.pos / 100;
169
+ switch (anchor.side) {
170
+ case 'top': return { x: cx - halfW + t * w, y: cy - halfH };
171
+ case 'bottom': return { x: cx - halfW + t * w, y: cy + halfH };
172
+ case 'left': return { x: cx - halfW, y: cy - halfH + t * h };
173
+ case 'right': return { x: cx + halfW, y: cy - halfH + t * h };
174
+ }
175
+ }
176
+
177
+ // Reads `boxValues` (not `currentValues`) so the box repaints at impact.
178
+ function resolveControl(name: TagControl): string | undefined {
179
+ const idx = tags.findIndex(t => t.controls === name);
180
+ return idx >= 0 ? boxValues[idx] : undefined;
181
+ }
182
+ const surfaceVar = $derived(resolveControl('surface'));
183
+ const radiusVar = $derived(resolveControl('radius'));
184
+ const borderColorVar = $derived(resolveControl('border-color'));
185
+ const borderWidthVar = $derived(resolveControl('border-width'));
186
+ const fontFamilyVar = $derived(resolveControl('font-family'));
187
+
188
+ function asVar(name: string | undefined): string | undefined {
189
+ return name ? `var(${name})` : undefined;
190
+ }
191
+
192
+ // Kite strings and energy balls are recomputed each frame from the box's
193
+ // measured rect so intrinsic sizing drives anchor placement.
194
+ function syncFrame() {
195
+ if (!stageEl) return;
196
+ const stageRect = stageEl.getBoundingClientRect();
197
+ if (stageRect.width === 0 || stageRect.height === 0) return;
198
+
199
+ let boxCx = 50, boxCy = 50;
200
+ let boxW = boxSize.w, boxH = boxSize.h;
201
+ if (boxEl) {
202
+ const br = boxEl.getBoundingClientRect();
203
+ if (br.width > 0 && br.height > 0) {
204
+ boxCx = ((br.left + br.width / 2) - stageRect.left) / stageRect.width * 100;
205
+ boxCy = ((br.top + br.height / 2) - stageRect.top) / stageRect.height * 100;
206
+ boxW = (br.width / stageRect.width) * 100;
207
+ boxH = (br.height / stageRect.height) * 100;
208
+ }
209
+ }
210
+
211
+ const now = performance.now();
212
+
213
+ for (let i = 0; i < tags.length; i++) {
214
+ const tagEl = tagEls[i];
215
+ const lineEl = lineEls[i];
216
+ if (!tagEl || !lineEl) continue;
217
+
218
+ // Tag endpoint: center of the pill's rounded cap on the box-facing side.
219
+ const pill = tagEl.querySelector('.ftt-tag') as HTMLElement | null;
220
+ const target = (pill ?? tagEl) as HTMLElement;
221
+ const r = target.getBoundingClientRect();
222
+
223
+ let radius = 0;
224
+ if (pill) {
225
+ const cs = getComputedStyle(pill);
226
+ const raw = parseFloat(cs.borderTopLeftRadius || '0') || 0;
227
+ radius = Math.min(raw, r.height / 2);
228
+ }
229
+
230
+ const onRight = tags[i].left > 50;
231
+ const px = onRight ? r.left + radius : r.right - radius;
232
+ const py = r.top + r.height / 2;
233
+
234
+ const x1 = (px - stageRect.left) / stageRect.width * 100;
235
+ const y1 = (py - stageRect.top) / stageRect.height * 100;
236
+
237
+ lineEl.setAttribute('x1', x1.toFixed(3));
238
+ lineEl.setAttribute('y1', y1.toFixed(3));
239
+
240
+ const a = anchorPoint(tags[i].anchor, boxCx, boxCy, boxW, boxH);
241
+ lineEl.setAttribute('x2', a.x.toFixed(3));
242
+ lineEl.setAttribute('y2', a.y.toFixed(3));
243
+ const knotEl = knotEls[i];
244
+ if (knotEl) {
245
+ knotEl.style.left = `${a.x.toFixed(3)}%`;
246
+ knotEl.style.top = `${a.y.toFixed(3)}%`;
247
+ }
248
+
249
+ const ballEl = ballEls[i];
250
+ const state = ballStates.get(i);
251
+ if (!ballEl) continue;
252
+ if (!state) {
253
+ ballEl.style.opacity = '0';
254
+ continue;
255
+ }
256
+ const elapsed = now - state.startedAt;
257
+ const t = Math.min(1, elapsed / state.duration);
258
+ if (t >= 1) {
259
+ // Commit at impact so the box's appearance changes with the bloop.
260
+ boxOverrides = { ...boxOverrides, [i]: state.pendingValue };
261
+ ballStates.delete(i);
262
+ ballEl.style.opacity = '0';
263
+ triggerBloop();
264
+ continue;
265
+ }
266
+ const x2 = parseFloat(lineEl.getAttribute('x2') || '0');
267
+ const y2 = parseFloat(lineEl.getAttribute('y2') || '0');
268
+ const eased = 1 - Math.pow(1 - t, 3);
269
+ const bx = x1 + (x2 - x1) * eased;
270
+ const by = y1 + (y2 - y1) * eased;
271
+ ballEl.style.left = `${bx.toFixed(3)}%`;
272
+ ballEl.style.top = `${by.toFixed(3)}%`;
273
+ ballEl.style.opacity = '1';
274
+ }
275
+ }
276
+
277
+ $effect(() => {
278
+ let rafId = 0;
279
+ const loop = () => {
280
+ syncFrame();
281
+ rafId = requestAnimationFrame(loop);
282
+ };
283
+ rafId = requestAnimationFrame(loop);
284
+ return () => cancelAnimationFrame(rafId);
285
+ });
286
+
287
+ function pickValue(i: number, value: string) {
288
+ openIdx = null;
289
+ strobeIdx = null;
290
+ overrides = { ...overrides, [i]: value };
291
+ flashingIdx = i;
292
+ window.setTimeout(() => {
293
+ if (flashingIdx === i) flashingIdx = null;
294
+ }, 500);
295
+ ballStates.set(i, { startedAt: performance.now(), duration: 520, pendingValue: value });
296
+ }
297
+
298
+ function triggerBloop() {
299
+ bloopActive = true;
300
+ window.setTimeout(() => { bloopActive = false; }, 480);
301
+ }
302
+
303
+ // Any click pauses auto-cycle for at least this long.
304
+ const USER_HOLD_MS = 4000;
305
+ let lastUserActionAt = 0;
306
+ function noteUserAction() {
307
+ lastUserActionAt = performance.now();
308
+ }
309
+
310
+ function clickTag(i: number) {
311
+ if (!tags[i].controls) return;
312
+ noteUserAction();
313
+ openIdx = openIdx === i ? null : i;
314
+ }
315
+
316
+ function userPick(i: number, value: string) {
317
+ // The matching dropdown item is also rendered disabled; this is a safety net.
318
+ if (currentValues[i] === value) return;
319
+ noteUserAction();
320
+ pickValue(i, value);
321
+ }
322
+
323
+ function onTagPointerDown(i: number, e: PointerEvent) {
324
+ dragStart.px = e.clientX;
325
+ dragStart.py = e.clientY;
326
+ dragStart.tagIdx = i;
327
+ dragStart.moved = false;
328
+ (e.currentTarget as Element).setPointerCapture(e.pointerId);
329
+ }
330
+
331
+ function onTagPointerMove(i: number, e: PointerEvent) {
332
+ if (dragStart.tagIdx !== i || !stageEl) return;
333
+ const dx = e.clientX - dragStart.px;
334
+ const dy = e.clientY - dragStart.py;
335
+ if (!dragStart.moved && Math.hypot(dx, dy) > DRAG_THRESHOLD_PX) {
336
+ dragStart.moved = true;
337
+ draggingIdx = i;
338
+ openIdx = null;
339
+ noteUserAction();
340
+ }
341
+ if (dragStart.moved) {
342
+ const r = stageEl.getBoundingClientRect();
343
+ const x = ((e.clientX - r.left) / r.width) * 100;
344
+ const y = ((e.clientY - r.top) / r.height) * 100;
345
+ dragOverrides = { ...dragOverrides, [i]: { top: y, left: x } };
346
+ }
347
+ }
348
+
349
+ function onTagPointerUp(i: number, e: PointerEvent) {
350
+ if (dragStart.tagIdx !== i) return;
351
+ (e.currentTarget as Element).releasePointerCapture(e.pointerId);
352
+ if (dragStart.moved) {
353
+ const p = dragOverrides[i];
354
+ // eslint-disable-next-line no-console
355
+ console.log(
356
+ `[${i}] ${tags[i].controls ?? '?'}: top: ${p.top.toFixed(1)}, left: ${p.left.toFixed(1)},`,
357
+ );
358
+ } else {
359
+ clickTag(i);
360
+ }
361
+ draggingIdx = null;
362
+ dragStart.tagIdx = -1;
363
+ dragStart.moved = false;
364
+ }
365
+
366
+ let autoAlive = false;
367
+ let lastAutoTagIdx: number | null = null;
368
+ const sleep = (ms: number) => new Promise(r => window.setTimeout(r, ms));
369
+
370
+ const STROBE_STEP_MS = 125;
371
+ const BREATH_MS = 250;
372
+
373
+ async function autoLoop() {
374
+ while (autoAlive) {
375
+ await sleep(2400 + Math.random() * 2400);
376
+ if (!autoAlive) break;
377
+
378
+ // Hold off while the user is interacting; re-check on each fresh click.
379
+ while (autoAlive) {
380
+ const sinceUser = performance.now() - lastUserActionAt;
381
+ if (sinceUser >= USER_HOLD_MS) break;
382
+ await sleep(USER_HOLD_MS - sinceUser);
383
+ }
384
+ if (!autoAlive) break;
385
+
386
+ if (openIdx !== null) continue;
387
+
388
+ // Never the same tag twice in a row.
389
+ const tagCandidates = tags
390
+ .map((_, idx) => idx)
391
+ .filter(idx => idx !== lastAutoTagIdx && tags[idx].controls);
392
+ if (tagCandidates.length === 0) continue;
393
+ const i = tagCandidates[Math.floor(Math.random() * tagCandidates.length)];
394
+ const tag = tags[i];
395
+ const opts = valueOptions[tag.controls!];
396
+
397
+ // Never the same token twice in a row.
398
+ const currentIdx = opts.indexOf(currentValues[i]);
399
+ const candidates = opts
400
+ .map((_, k) => k)
401
+ .filter(k => k !== currentIdx);
402
+ if (candidates.length === 0) continue;
403
+ const finalIdx = candidates[Math.floor(Math.random() * candidates.length)];
404
+
405
+ openIdx = i;
406
+
407
+ // Let the menu sit open before any highlight appears.
408
+ await sleep(BREATH_MS);
409
+ if (!autoAlive || openIdx !== i) continue;
410
+
411
+ // Step from the top down to the chosen item.
412
+ for (let k = 0; k <= finalIdx; k++) {
413
+ if (!autoAlive || openIdx !== i) break;
414
+ strobeIdx = k;
415
+ await sleep(STROBE_STEP_MS);
416
+ }
417
+ if (!autoAlive || openIdx !== i) continue;
418
+
419
+ // Blink twice on the selection.
420
+ for (let blink = 0; blink < 2; blink++) {
421
+ if (!autoAlive || openIdx !== i) break;
422
+ strobeIdx = null;
423
+ await sleep(STROBE_STEP_MS);
424
+ if (!autoAlive || openIdx !== i) break;
425
+ strobeIdx = finalIdx;
426
+ await sleep(STROBE_STEP_MS);
427
+ }
428
+ if (!autoAlive || openIdx !== i) continue;
429
+
430
+ pickValue(i, opts[finalIdx]);
431
+ lastAutoTagIdx = i;
432
+ }
433
+ }
434
+
435
+ $effect(() => {
436
+ if (!autoplay) return;
437
+ autoAlive = true;
438
+ autoLoop();
439
+ return () => { autoAlive = false; };
440
+ });
441
+ </script>
442
+
443
+ <div
444
+ class="ftt-stage"
445
+ bind:this={stageEl}
446
+ style:--ftt-bob-duration="{duration}s"
447
+ style:--ftt-bob-distance="{-distance}px"
448
+ >
449
+ <svg class="ftt-strings" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
450
+ {#each tags as tag, i (i)}
451
+ {@const a = anchorPoint(tag.anchor)}
452
+ <line
453
+ bind:this={lineEls[i]}
454
+ x1={tag.left} y1={tag.top}
455
+ x2={a.x} y2={a.y}
456
+ class="ftt-string"
457
+ class:ftt-live={ballStates.has(i)}
458
+ />
459
+ {/each}
460
+ </svg>
461
+
462
+ <!-- Box is intrinsically sized to content + padding; boxSize is a floor. -->
463
+ <div
464
+ bind:this={boxEl}
465
+ class="ftt-box"
466
+ class:ftt-bloop={bloopActive}
467
+ style:min-width="{boxSize.w}%"
468
+ style:min-height="{boxSize.h}%"
469
+ style:background={asVar(surfaceVar)}
470
+ style:border-radius={asVar(radiusVar)}
471
+ style:border-color={asVar(borderColorVar)}
472
+ style:border-width={asVar(borderWidthVar)}
473
+ >
474
+ <span
475
+ class="ftt-box-label"
476
+ style:font-family={asVar(fontFamilyVar)}
477
+ >I'm a button</span>
478
+ </div>
479
+
480
+ {#each tags as tag, i (i)}
481
+ {@const a = anchorPoint(tag.anchor)}
482
+ <span
483
+ bind:this={knotEls[i]}
484
+ class="ftt-knot"
485
+ style:left="{a.x}%"
486
+ style:top="{a.y}%"
487
+ aria-hidden="true"
488
+ ></span>
489
+ {/each}
490
+
491
+ {#each tags as _t, i (i)}
492
+ <span class="ftt-energy-ball" bind:this={ballEls[i]} aria-hidden="true"></span>
493
+ {/each}
494
+
495
+ {#each tags as tag, i (i)}
496
+ <span
497
+ bind:this={tagEls[i]}
498
+ class="ftt-float"
499
+ class:ftt-flash={flashingIdx === i}
500
+ class:ftt-open={openIdx === i}
501
+ class:ftt-dragging={draggingIdx === i}
502
+ data-placement={tag.placement ?? 'top'}
503
+ data-pivot={tag.pivot ?? 'center'}
504
+ style:top="{tagTop(i)}%"
505
+ style:left="{tagLeft(i)}%"
506
+ style:animation-delay="{tag.delay ?? 0}s"
507
+ style:--ftt-rot="{tag.rotate ?? 0}deg"
508
+ >
509
+ {#if tag.controls}
510
+ <span class="ftt-float-property">{controlLabels[tag.controls]}</span>
511
+ {/if}
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>
541
+ </span>
542
+ {/each}
543
+ </div>
@@ -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
- padding: var(--inlineeditactions-save-default-padding) calc(var(--inlineeditactions-save-default-padding) * 2);
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
- padding: var(--inlineeditactions-save-hover-padding) calc(var(--inlineeditactions-save-hover-padding) * 2);
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
- padding: var(--inlineeditactions-cancel-default-padding) calc(var(--inlineeditactions-cancel-default-padding) * 2);
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
- padding: var(--inlineeditactions-cancel-hover-padding) calc(var(--inlineeditactions-cancel-hover-padding) * 2);
143
+ @include themed-padding(--inlineeditactions-cancel-hover-padding, $h: 2);
142
144
  font-size: var(--inlineeditactions-cancel-hover-icon-size);
143
145
  }
144
146
  }