@motion-proto/live-tokens 0.94.2 → 0.95.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-create-page/SKILL.md +7 -3
- package/CHANGELOG.md +108 -0
- package/README.md +6 -6
- package/dist-plugin/{chunk-7XSUHJUH.js → chunk-4J6JPSVS.js} +53 -2
- package/dist-plugin/{chunk-W5VQV32K.js → chunk-GYRULB2X.js} +1 -1
- package/dist-plugin/{chunk-ONYAIXCZ.js → chunk-QQASYPW7.js} +32 -9
- package/dist-plugin/index.cjs +598 -17
- package/dist-plugin/index.d.cts +9 -1
- package/dist-plugin/index.d.ts +9 -1
- package/dist-plugin/index.js +513 -6
- package/dist-plugin/migrateData/index.cjs +32 -9
- package/dist-plugin/migrateData/index.js +2 -2
- package/dist-plugin/setColors/index.cjs +32 -22
- package/dist-plugin/setColors/index.js +2 -15
- package/dist-plugin/setGeometry/index.cjs +32 -9
- package/dist-plugin/setGeometry/index.js +1 -1
- package/dist-plugin/tokensCssMigrations/index.cjs +53 -2
- package/dist-plugin/tokensCssMigrations/index.d.cts +9 -2
- package/dist-plugin/tokensCssMigrations/index.d.ts +9 -2
- package/dist-plugin/tokensCssMigrations/index.js +1 -1
- package/package.json +8 -3
- package/src/demo/ChromeSelect.svelte +111 -0
- package/src/demo/Demo.svelte +39 -5
- package/src/demo/DemoBar.svelte +33 -14
- package/src/demo/FloatingTokenTags.css +33 -4
- package/src/demo/FloatingTokenTags.svelte +81 -68
- package/src/demo/Section.svelte +1 -7
- package/src/demo/SketchSelect.svelte +36 -12
- package/src/demo/SourceLinks.svelte +5 -4
- package/src/demo/TestingLoops.svelte +128 -19
- package/src/demo/ThemeSelect.svelte +34 -11
- package/src/demo/evalResults.ts +7 -7
- package/src/demo/sections/SectionClaude.svelte +20 -13
- package/src/demo/sections/SectionFooter.svelte +4 -5
- package/src/demo/sections/SectionGetStarted.svelte +5 -6
- package/src/demo/sections/SectionHero.svelte +43 -23
- package/src/demo/sections/SectionKit.svelte +141 -45
- package/src/demo/sections/SectionLayers.svelte +3 -3
- package/src/demo/sections/SectionOffering.svelte +8 -23
- package/src/editor/bootstrap.ts +12 -6
- package/src/editor/component-editor/ImageEditor.svelte +1 -1
- package/src/editor/component-editor/ImageLightboxEditor.svelte +3 -4
- package/src/editor/component-editor/registration.ts +36 -0
- package/src/editor/component-editor/registry.ts +5 -12
- package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +11 -2
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +7 -0
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +1 -2
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +17 -2
- package/src/editor/core/store/editorPersistence.ts +6 -1
- package/src/editor/core/store/editorRenderer.ts +1 -4
- package/src/editor/core/store/editorStore.ts +5 -9
- package/src/editor/core/store/editorTypes.ts +2 -2
- package/src/editor/core/themes/migrations/2026-10-15-responsive-columns.ts +40 -0
- package/src/editor/core/themes/migrations/index.ts +2 -0
- package/src/editor/core/themes/slices/columns.ts +112 -42
- package/src/editor/docs/Docs.svelte +25 -22
- package/src/editor/docs/chapters.ts +1 -0
- package/src/editor/docs/content/editing-tokens.md +5 -3
- package/src/editor/docs/content/production-css.md +116 -0
- package/src/editor/docs/content/themes-workflow.md +2 -0
- package/src/editor/docs/content.generated.ts +3 -2
- package/src/editor/index.ts +2 -1
- package/src/editor/overlay/LiveEditorOverlay.svelte +67 -19
- package/src/editor/overlay/LiveTokensRouter.svelte +8 -4
- package/src/editor/pages/Editor.svelte +15 -1
- package/src/editor/pages/EditorShell.svelte +312 -99
- package/src/editor/skill-atlas/SkillAtlas.svelte +111 -24
- package/src/editor/skill-atlas/TreeCanvas.svelte +14 -0
- package/src/editor/skill-atlas/skillSources.generated.ts +1 -1
- package/src/editor/skill-atlas/trees/create-page.ts +24 -24
- package/src/editor/skill-atlas/wireLayout.ts +62 -0
- package/src/editor/ui/BezierCurveEditor.svelte +3 -0
- package/src/editor/ui/FontStackEditor.svelte +2 -2
- package/src/editor/ui/PaletteEditor.svelte +64 -6
- package/src/editor/ui/UIPaletteSelector.svelte +2 -0
- package/src/editor/ui/colors/ColorsTab.svelte +5 -2
- package/src/editor/ui/palette/OverridesPanel.svelte +1 -0
- package/src/editor/ui/palette/ScaleCurveEditor.svelte +127 -18
- package/src/editor/ui/sections/ColumnsSection.svelte +79 -49
- package/src/editor/ui/sections/ShadowsSection.svelte +14 -0
- package/src/editor/ui/sections/WashesSection.svelte +11 -0
- package/src/editor/ui/sketch/SketchTab.svelte +1 -1
- package/src/live-tokens/data/colors-and-type/autumn.json +1 -4
- package/src/live-tokens/data/colors-and-type/default.json +1 -4
- package/src/live-tokens/data/colors-and-type/halloween.json +1 -4
- package/src/live-tokens/data/colors-and-type/midnight-study.json +1 -4
- package/src/live-tokens/data/colors-and-type/ocean.json +1 -4
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +1 -4
- package/src/live-tokens/data/colors-and-type/sketchy.json +1 -4
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +1 -4
- package/src/live-tokens/data/colors-and-type/sunset.json +1 -4
- package/src/live-tokens/data/themes/autumn.json +1 -4
- package/src/live-tokens/data/themes/halloween.json +1 -4
- package/src/live-tokens/data/themes/midnight-study.json +1 -4
- package/src/live-tokens/data/themes/ocean.json +1 -4
- package/src/live-tokens/data/themes/royal-velvet.json +1 -4
- package/src/live-tokens/data/themes/sketchy.json +1 -4
- package/src/live-tokens/data/themes/spring-meadow.json +1 -4
- package/src/live-tokens/data/themes/sunset.json +1 -4
- package/src/live-tokens/data/tokens.generated.css +1 -4
- package/src/system/components/SectionDivider.svelte +4 -0
- package/src/system/styles/CONVENTIONS.md +1 -1
- package/src/system/styles/tokens.css +30 -4
- package/template/src/main.ts +2 -2
- package/template/src/pages/Home.svelte +11 -8
- package/template/vite.config.ts +7 -3
- package/src/system/assets/offering.webp +0 -0
- /package/dist-plugin/{chunk-O6GQ6GBH.js → chunk-A4TBP4MM.js} +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import UIMenuButton from '../editor/ui/UIMenuButton.svelte';
|
|
3
|
+
import UIOptionList from '../editor/ui/UIOptionList.svelte';
|
|
4
|
+
import UIOptionItem from '../editor/ui/UIOptionItem.svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
label: string;
|
|
8
|
+
items: { value: string; label: string }[];
|
|
9
|
+
value: string;
|
|
10
|
+
/** Trigger text when `value` matches no item. Defaults to the label. */
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
onchange: (value: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
label: heading,
|
|
19
|
+
items,
|
|
20
|
+
value,
|
|
21
|
+
placeholder = heading,
|
|
22
|
+
disabled = false,
|
|
23
|
+
error = '',
|
|
24
|
+
onchange,
|
|
25
|
+
}: Props = $props();
|
|
26
|
+
|
|
27
|
+
const selectedLabel = $derived(items.find((item) => item.value === value)?.label ?? placeholder);
|
|
28
|
+
|
|
29
|
+
function select(next: string, close: () => void): void {
|
|
30
|
+
close();
|
|
31
|
+
if (next === value) return;
|
|
32
|
+
onchange(next);
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<!-- LabeledSelect's twin in the editor's --ui-* chrome. It needs an
|
|
37
|
+
.editor-page ancestor, which scopes those tokens and hosts the menu. -->
|
|
38
|
+
<div class="chrome-select" inert={disabled || items.length === 0}>
|
|
39
|
+
<span class="chrome-select-label">{heading}</span>
|
|
40
|
+
|
|
41
|
+
<UIMenuButton header={heading} triggerClass="chrome-select-trigger" menuMinWidth="12rem">
|
|
42
|
+
{#snippet trigger()}
|
|
43
|
+
<span>{selectedLabel}</span>
|
|
44
|
+
<i class="fas fa-chevron-down" aria-hidden="true"></i>
|
|
45
|
+
{/snippet}
|
|
46
|
+
{#snippet children({ close })}
|
|
47
|
+
<UIOptionList>
|
|
48
|
+
{#each items as item (item.value)}
|
|
49
|
+
<UIOptionItem active={item.value === value} onclick={() => select(item.value, close)}>
|
|
50
|
+
{#snippet label()}{item.label}{/snippet}
|
|
51
|
+
</UIOptionItem>
|
|
52
|
+
{/each}
|
|
53
|
+
</UIOptionList>
|
|
54
|
+
{/snippet}
|
|
55
|
+
</UIMenuButton>
|
|
56
|
+
|
|
57
|
+
{#if error}
|
|
58
|
+
<span class="error" aria-live="polite">{error}</span>
|
|
59
|
+
{/if}
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
.chrome-select {
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-wrap: wrap;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: var(--ui-space-8);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.chrome-select[inert] {
|
|
71
|
+
opacity: var(--ui-opacity-disabled);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.chrome-select-label {
|
|
75
|
+
font-size: var(--ui-font-size-xs);
|
|
76
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
77
|
+
color: var(--ui-text-secondary);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.chrome-select :global(.chrome-select-trigger) {
|
|
81
|
+
min-width: 9rem;
|
|
82
|
+
justify-content: space-between;
|
|
83
|
+
gap: var(--ui-space-12);
|
|
84
|
+
padding: var(--ui-space-6) var(--ui-space-10);
|
|
85
|
+
font-size: var(--ui-font-size-xs);
|
|
86
|
+
color: var(--ui-text-primary);
|
|
87
|
+
background: var(--ui-surface-high);
|
|
88
|
+
border: 1px solid var(--ui-border);
|
|
89
|
+
border-radius: var(--ui-radius-md);
|
|
90
|
+
transition:
|
|
91
|
+
background var(--ui-transition-fast),
|
|
92
|
+
border-color var(--ui-transition-fast);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.chrome-select :global(.chrome-select-trigger:hover),
|
|
96
|
+
.chrome-select :global(.chrome-select-trigger[aria-expanded='true']) {
|
|
97
|
+
background: var(--ui-surface-highest);
|
|
98
|
+
border-color: var(--ui-border-higher);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.chrome-select :global(.chrome-select-trigger .fa-chevron-down) {
|
|
102
|
+
font-size: 0.625rem;
|
|
103
|
+
color: var(--ui-text-secondary);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.error {
|
|
107
|
+
max-width: 20rem;
|
|
108
|
+
font-size: var(--ui-font-size-xs);
|
|
109
|
+
color: var(--ui-link-broken);
|
|
110
|
+
}
|
|
111
|
+
</style>
|
package/src/demo/Demo.svelte
CHANGED
|
@@ -36,14 +36,24 @@
|
|
|
36
36
|
</div>
|
|
37
37
|
|
|
38
38
|
<style>
|
|
39
|
+
/* Sections place themselves on these spans, and the full-width ones nest a
|
|
40
|
+
subgrid, so the breakpoints below are the only place a section's columns
|
|
41
|
+
change. minmax(0, 1fr) keeps a wide child from widening its track. */
|
|
39
42
|
.kit {
|
|
43
|
+
--kit-content: 2 / -2;
|
|
44
|
+
--kit-narrow: 3 / -3;
|
|
45
|
+
--kit-section-lead: var(--space-32);
|
|
46
|
+
--kit-quote-pause: var(--space-32);
|
|
47
|
+
--kit-footer-lead: var(--space-32);
|
|
40
48
|
display: grid;
|
|
41
|
-
grid-template-columns: repeat(var(--columns-count), 1fr);
|
|
49
|
+
grid-template-columns: repeat(var(--columns-count), minmax(0, 1fr));
|
|
42
50
|
column-gap: var(--columns-gutter);
|
|
43
51
|
row-gap: var(--space-64);
|
|
52
|
+
box-sizing: content-box;
|
|
44
53
|
max-width: var(--columns-max-width);
|
|
45
54
|
margin: 0 auto;
|
|
46
|
-
padding: var(--space-48)
|
|
55
|
+
padding-block: var(--space-48);
|
|
56
|
+
padding-inline: var(--columns-margin);
|
|
47
57
|
min-height: 100vh;
|
|
48
58
|
}
|
|
49
59
|
|
|
@@ -57,11 +67,21 @@
|
|
|
57
67
|
margin-bottom: 0;
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
/*
|
|
70
|
+
/* Every titled section opens with the same lead-in: row-gap, this margin,
|
|
71
|
+
and the SectionDivider's own top margin. The quote pauses between the
|
|
72
|
+
layers and the panel below it, and the footer sets off from the page. */
|
|
61
73
|
.kit > :global(.kit-section),
|
|
62
74
|
.kit > :global(.layers),
|
|
63
75
|
.kit > :global(.claude) {
|
|
64
|
-
margin-top: var(--
|
|
76
|
+
margin-top: var(--kit-section-lead);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.kit > :global(.offering) {
|
|
80
|
+
margin-block: var(--kit-quote-pause);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.kit > :global(.provenance) {
|
|
84
|
+
margin-top: var(--kit-footer-lead);
|
|
65
85
|
}
|
|
66
86
|
|
|
67
87
|
/* Inline code styling cascades into every section via :global descendants. */
|
|
@@ -75,10 +95,24 @@
|
|
|
75
95
|
color: var(--text-primary);
|
|
76
96
|
}
|
|
77
97
|
|
|
98
|
+
@media (max-width: 1200px) {
|
|
99
|
+
.kit {
|
|
100
|
+
--kit-content: 1 / -1;
|
|
101
|
+
--kit-narrow: 2 / -2;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
@media (max-width: 600px) {
|
|
106
|
+
.kit {
|
|
107
|
+
--kit-narrow: 1 / -1;
|
|
108
|
+
}
|
|
109
|
+
|
|
79
110
|
.kit,
|
|
80
111
|
.kit.has-bar {
|
|
81
|
-
|
|
112
|
+
--kit-section-lead: var(--space-8);
|
|
113
|
+
--kit-quote-pause: var(--space-16);
|
|
114
|
+
--kit-footer-lead: var(--space-16);
|
|
115
|
+
padding-block: var(--space-32);
|
|
82
116
|
row-gap: var(--space-48);
|
|
83
117
|
}
|
|
84
118
|
}
|
package/src/demo/DemoBar.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import '../editor/styles/ui-editor.css';
|
|
2
3
|
import ThemeSelect from './ThemeSelect.svelte';
|
|
3
4
|
import SketchSelect from './SketchSelect.svelte';
|
|
4
5
|
|
|
@@ -11,11 +12,14 @@
|
|
|
11
12
|
} = $props();
|
|
12
13
|
</script>
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
<!-- Editor chrome, like the overlay: the bar paints from --ui-*, so every
|
|
16
|
+
theme leaves it legible, and data-live-tokens-chrome keeps sketch mode off
|
|
17
|
+
it. .editor-page scopes the --ui-* tokens and hosts the selects' menus. -->
|
|
18
|
+
<div class="demo-bar editor-page" data-live-tokens-chrome>
|
|
15
19
|
<div class="bar">
|
|
16
20
|
<div class="pickers">
|
|
17
|
-
<ThemeSelect
|
|
18
|
-
<SketchSelect
|
|
21
|
+
<ThemeSelect chrome onpick={onThemePick} />
|
|
22
|
+
<SketchSelect chrome onpick={onSketchPick} />
|
|
19
23
|
</div>
|
|
20
24
|
</div>
|
|
21
25
|
</div>
|
|
@@ -27,20 +31,22 @@
|
|
|
27
31
|
left: 0;
|
|
28
32
|
right: 0;
|
|
29
33
|
z-index: var(--z-sticky);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
border-bottom:
|
|
34
|
+
--sketch-icon-off: none;
|
|
35
|
+
background: var(--ui-surface-lower);
|
|
36
|
+
border-bottom: 1px solid var(--ui-border-low);
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
/* The bar repeats the demo's .kit grid, so its content lines up with the
|
|
36
40
|
sections beneath it. The right side stays empty for the editor's collapsed
|
|
37
41
|
toggle, which pins top-right. */
|
|
38
42
|
.bar {
|
|
43
|
+
box-sizing: content-box;
|
|
39
44
|
max-width: var(--columns-max-width);
|
|
40
45
|
margin-inline: auto;
|
|
41
|
-
padding: var(--space-12)
|
|
46
|
+
padding-block: var(--space-12);
|
|
47
|
+
padding-inline: var(--columns-margin);
|
|
42
48
|
display: grid;
|
|
43
|
-
grid-template-columns: repeat(var(--columns-count), 1fr);
|
|
49
|
+
grid-template-columns: repeat(var(--columns-count), minmax(0, 1fr));
|
|
44
50
|
column-gap: var(--columns-gutter);
|
|
45
51
|
}
|
|
46
52
|
|
|
@@ -53,23 +59,36 @@
|
|
|
53
59
|
row-gap: var(--space-8);
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
@media (max-width:
|
|
62
|
+
@media (max-width: 1200px) {
|
|
57
63
|
.pickers {
|
|
58
64
|
grid-column: 1 / -1;
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
/* No room beside the editor's toggle on a phone, so the pickers sit in the
|
|
63
|
-
page below it and scroll away
|
|
69
|
+
page below it and scroll away, clearing the toggle only when it rests in
|
|
70
|
+
the top band. */
|
|
64
71
|
@media (max-width: 600px) {
|
|
65
72
|
.demo-bar {
|
|
66
73
|
position: static;
|
|
67
|
-
background: none;
|
|
68
|
-
backdrop-filter: none;
|
|
69
|
-
border-bottom: none;
|
|
70
74
|
}
|
|
71
75
|
.bar {
|
|
72
|
-
padding: var(--space-
|
|
76
|
+
padding-block: max(var(--space-16), calc(var(--lt-pill-clearance, 0px) + var(--space-8))) var(--space-16);
|
|
77
|
+
}
|
|
78
|
+
/* Side by side, each label over a trigger that fills its half. */
|
|
79
|
+
.pickers {
|
|
80
|
+
display: grid;
|
|
81
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
82
|
+
gap: var(--space-12);
|
|
83
|
+
}
|
|
84
|
+
.pickers :global(.chrome-select) {
|
|
85
|
+
flex-direction: column;
|
|
86
|
+
align-items: stretch;
|
|
87
|
+
gap: var(--ui-space-4);
|
|
88
|
+
}
|
|
89
|
+
.pickers :global(.chrome-select-trigger) {
|
|
90
|
+
width: 100%;
|
|
91
|
+
min-width: 0;
|
|
73
92
|
}
|
|
74
93
|
}
|
|
75
94
|
</style>
|
|
@@ -79,8 +79,10 @@
|
|
|
79
79
|
|
|
80
80
|
.ftt-string {
|
|
81
81
|
stroke: var(--ftt-contrast-color);
|
|
82
|
+
fill: none;
|
|
82
83
|
stroke-width: 2;
|
|
83
84
|
stroke-linecap: round;
|
|
85
|
+
stroke-linejoin: round;
|
|
84
86
|
vector-effect: non-scaling-stroke;
|
|
85
87
|
opacity: var(--ftt-string-opacity);
|
|
86
88
|
transition: opacity 150ms ease, stroke 150ms ease;
|
|
@@ -124,8 +126,9 @@
|
|
|
124
126
|
|
|
125
127
|
/* --- Floating tag wrapper ------------------------------------------------- */
|
|
126
128
|
.ftt-float {
|
|
129
|
+
--ftt-shift-x: -50%;
|
|
127
130
|
position: absolute;
|
|
128
|
-
translate: -
|
|
131
|
+
translate: var(--ftt-shift-x) -50%;
|
|
129
132
|
rotate: var(--ftt-rot, 0deg);
|
|
130
133
|
animation: ftt-bob var(--ftt-bob-duration, 7s) ease-in-out infinite;
|
|
131
134
|
z-index: 4;
|
|
@@ -134,7 +137,7 @@
|
|
|
134
137
|
flex-direction: column;
|
|
135
138
|
align-items: center;
|
|
136
139
|
gap: 0.25rem;
|
|
137
|
-
width:
|
|
140
|
+
width: max-content;
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
/* Lift open tag so its dropdown paints over sibling tags. */
|
|
@@ -159,8 +162,8 @@
|
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
@keyframes ftt-bob {
|
|
162
|
-
0%, 100% { translate: -
|
|
163
|
-
50% { translate: -
|
|
165
|
+
0%, 100% { translate: var(--ftt-shift-x) -50%; }
|
|
166
|
+
50% { translate: var(--ftt-shift-x) calc(-50% + var(--ftt-bob-distance, -8px)); }
|
|
164
167
|
}
|
|
165
168
|
|
|
166
169
|
/* --- Property label paired with each tag ---------------------------------- */
|
|
@@ -272,6 +275,32 @@
|
|
|
272
275
|
z-index: 10;
|
|
273
276
|
}
|
|
274
277
|
|
|
278
|
+
/* --- Stacked layout ------------------------------------------------------ */
|
|
279
|
+
/* Narrow stage: tags hang in a column left of the box and the strings elbow
|
|
280
|
+
into its top and bottom edges. `left` positions each tag's left edge. */
|
|
281
|
+
.ftt-stacked .ftt-box {
|
|
282
|
+
left: auto;
|
|
283
|
+
right: 0;
|
|
284
|
+
translate: 0 -50%;
|
|
285
|
+
width: 10rem;
|
|
286
|
+
padding: 0.75rem 1rem;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.ftt-stacked .ftt-float {
|
|
290
|
+
--ftt-shift-x: 0%;
|
|
291
|
+
rotate: 0deg;
|
|
292
|
+
align-items: flex-start;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.ftt-stacked .ftt-float-property {
|
|
296
|
+
padding-left: 0.75rem;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.ftt-stacked .ftt-dropdown-wrap {
|
|
300
|
+
left: 0;
|
|
301
|
+
translate: none;
|
|
302
|
+
}
|
|
303
|
+
|
|
275
304
|
/* --- Reduced motion ------------------------------------------------------- */
|
|
276
305
|
@media (prefers-reduced-motion: reduce) {
|
|
277
306
|
.ftt-float {
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
/** Tag center, in % of the stage. */
|
|
19
19
|
top: number;
|
|
20
20
|
left: number;
|
|
21
|
+
/** Stacked layout for a stage narrower than `NARROW_STAGE_PX`: tags form a
|
|
22
|
+
* column left of the box, `left` is the tag's left edge, and the string
|
|
23
|
+
* elbows into `anchor`. Nest anchors so higher tags land further right. */
|
|
24
|
+
narrow?: { top: number; left: number; anchor: Anchor };
|
|
21
25
|
/** Bob delay, seconds. Negative values offset the start. */
|
|
22
26
|
delay?: number;
|
|
23
27
|
/** Static tilt, degrees. */
|
|
@@ -34,6 +38,7 @@
|
|
|
34
38
|
|
|
35
39
|
<script lang="ts">
|
|
36
40
|
import MenuSelect from '../system/components/MenuSelect.svelte';
|
|
41
|
+
import { untrack } from 'svelte';
|
|
37
42
|
import { SvelteMap } from 'svelte/reactivity';
|
|
38
43
|
import { contrastTokenForBackground } from '../system/backdrop';
|
|
39
44
|
// `.ftt-tag` is hand-rolled (not Badge) so editing badge-* tokens doesn't
|
|
@@ -72,6 +77,7 @@
|
|
|
72
77
|
icon: 'fas fa-fill-drip',
|
|
73
78
|
label: '--surface-brand-high',
|
|
74
79
|
top: 15, left: 20, delay: 0, rotate: 3,
|
|
80
|
+
narrow: { top: 6, left: 0, anchor: { side: 'top', pos: 90 } },
|
|
75
81
|
anchor: { side: 'inside', x: 10, y: 40 },
|
|
76
82
|
controls: 'surface',
|
|
77
83
|
placement: 'top',
|
|
@@ -81,6 +87,7 @@
|
|
|
81
87
|
icon: 'fas fa-font',
|
|
82
88
|
label: '--font-display',
|
|
83
89
|
top: 5, left: 45, delay: -0.9, rotate: 2,
|
|
90
|
+
narrow: { top: 20, left: 5, anchor: { side: 'top', pos: 64 } },
|
|
84
91
|
anchor: { side: 'inside', x: 78, y: 28 },
|
|
85
92
|
controls: 'font-family',
|
|
86
93
|
placement: 'top',
|
|
@@ -89,6 +96,7 @@
|
|
|
89
96
|
icon: 'fa-solid fa-bezier-curve',
|
|
90
97
|
label: '--radius-2xl',
|
|
91
98
|
top: 23, left: 75, delay: -1.8, rotate: 2,
|
|
99
|
+
narrow: { top: 34, left: 10, anchor: { side: 'top', pos: 38 } },
|
|
92
100
|
anchor: { side: 'top', pos: 100 },
|
|
93
101
|
controls: 'radius',
|
|
94
102
|
placement: 'top',
|
|
@@ -97,6 +105,7 @@
|
|
|
97
105
|
icon: 'fas fa-paint-roller',
|
|
98
106
|
label: '--border-brand-strong',
|
|
99
107
|
top: 79, left: 72, delay: -3.6, rotate: -2,
|
|
108
|
+
narrow: { top: 82, left: 8, anchor: { side: 'bottom', pos: 84 } },
|
|
100
109
|
anchor: { side: 'bottom', pos: 75 },
|
|
101
110
|
controls: 'border-color',
|
|
102
111
|
placement: 'top',
|
|
@@ -105,6 +114,7 @@
|
|
|
105
114
|
icon: 'fas fa-grip-lines',
|
|
106
115
|
label: '--border-width-3',
|
|
107
116
|
top: 79, left: 28, delay: -5.2, rotate: -4,
|
|
117
|
+
narrow: { top: 66, left: 3, anchor: { side: 'bottom', pos: 44 } },
|
|
108
118
|
anchor: { side: 'bottom', pos: 25 },
|
|
109
119
|
controls: 'border-width',
|
|
110
120
|
placement: 'top',
|
|
@@ -138,8 +148,17 @@
|
|
|
138
148
|
const DRAG_THRESHOLD_PX = 4;
|
|
139
149
|
const dragStart = { px: 0, py: 0, tagIdx: -1, moved: false };
|
|
140
150
|
|
|
141
|
-
|
|
142
|
-
|
|
151
|
+
const NARROW_STAGE_PX = 480;
|
|
152
|
+
let stageWidth = $state(0);
|
|
153
|
+
const stacked = $derived(
|
|
154
|
+
stageWidth > 0 && stageWidth < NARROW_STAGE_PX && tags.every(t => t.narrow),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
function home(i: number): { top: number; left: number; anchor: Anchor } {
|
|
158
|
+
return (stacked && tags[i].narrow) || tags[i];
|
|
159
|
+
}
|
|
160
|
+
function tagTop(i: number): number { return dragOverrides[i]?.top ?? home(i).top; }
|
|
161
|
+
function tagLeft(i: number): number { return dragOverrides[i]?.left ?? home(i).left; }
|
|
143
162
|
|
|
144
163
|
type BallState = { startedAt: number; duration: number; pendingValue: string };
|
|
145
164
|
const ballStates = new SvelteMap<number, BallState>();
|
|
@@ -147,34 +166,38 @@
|
|
|
147
166
|
const ballEls: HTMLSpanElement[] = $state([]);
|
|
148
167
|
|
|
149
168
|
let stageEl: HTMLDivElement | undefined = $state();
|
|
169
|
+
let svgEl: SVGSVGElement | undefined = $state();
|
|
150
170
|
let boxEl: HTMLDivElement | undefined = $state();
|
|
151
171
|
const tagEls: HTMLSpanElement[] = $state([]);
|
|
152
|
-
const
|
|
172
|
+
const pathEls: SVGPathElement[] = $state([]);
|
|
153
173
|
const knotEls: HTMLSpanElement[] = $state([]);
|
|
154
174
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
cy = 50,
|
|
159
|
-
w = boxSize.w,
|
|
160
|
-
h = boxSize.h,
|
|
161
|
-
): { x: number; y: number } {
|
|
162
|
-
const halfW = w / 2, halfH = h / 2;
|
|
175
|
+
type Rect = { x: number; y: number; w: number; h: number };
|
|
176
|
+
|
|
177
|
+
function anchorPoint(anchor: Anchor, box: Rect): { x: number; y: number } {
|
|
163
178
|
if (anchor.side === 'inside') {
|
|
164
|
-
return {
|
|
165
|
-
x: cx - halfW + (anchor.x / 100) * w,
|
|
166
|
-
y: cy - halfH + (anchor.y / 100) * h,
|
|
167
|
-
};
|
|
179
|
+
return { x: box.x + (anchor.x / 100) * box.w, y: box.y + (anchor.y / 100) * box.h };
|
|
168
180
|
}
|
|
169
181
|
const t = anchor.pos / 100;
|
|
170
182
|
switch (anchor.side) {
|
|
171
|
-
case 'top': return { x:
|
|
172
|
-
case 'bottom': return { x:
|
|
173
|
-
case 'left': return { x:
|
|
174
|
-
case 'right': return { x:
|
|
183
|
+
case 'top': return { x: box.x + t * box.w, y: box.y };
|
|
184
|
+
case 'bottom': return { x: box.x + t * box.w, y: box.y + box.h };
|
|
185
|
+
case 'left': return { x: box.x, y: box.y + t * box.h };
|
|
186
|
+
case 'right': return { x: box.x + box.w, y: box.y + t * box.h };
|
|
175
187
|
}
|
|
176
188
|
}
|
|
177
189
|
|
|
190
|
+
const ELBOW_RADIUS_PX = 14;
|
|
191
|
+
const px = (n: number) => n.toFixed(2);
|
|
192
|
+
|
|
193
|
+
// Runs level out of the tag, then turns through a rounded corner into the box.
|
|
194
|
+
function elbowPath(x1: number, y1: number, x2: number, y2: number): string {
|
|
195
|
+
const sx = Math.sign(x2 - x1), sy = Math.sign(y2 - y1);
|
|
196
|
+
const r = Math.min(ELBOW_RADIUS_PX, Math.abs(x2 - x1), Math.abs(y2 - y1));
|
|
197
|
+
return `M${px(x1)} ${px(y1)} H${px(x2 - sx * r)} `
|
|
198
|
+
+ `Q${px(x2)} ${px(y1)} ${px(x2)} ${px(y1 + sy * r)} V${px(y2)}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
178
201
|
// Reads `boxValues` (not `currentValues`) so the box repaints at impact.
|
|
179
202
|
function resolveControl(name: TagControl): string | undefined {
|
|
180
203
|
const idx = tags.findIndex(t => t.controls === name);
|
|
@@ -216,31 +239,35 @@
|
|
|
216
239
|
|
|
217
240
|
// Kite strings and energy balls are recomputed each frame from the box's
|
|
218
241
|
// measured rect so intrinsic sizing drives anchor placement.
|
|
242
|
+
let lastViewBox = '';
|
|
219
243
|
function syncFrame() {
|
|
220
|
-
if (!stageEl) return;
|
|
244
|
+
if (!stageEl || !svgEl || !boxEl) return;
|
|
221
245
|
syncConnectorContrast();
|
|
222
246
|
syncBoxLabelContrast();
|
|
223
247
|
const stageRect = stageEl.getBoundingClientRect();
|
|
224
248
|
if (stageRect.width === 0 || stageRect.height === 0) return;
|
|
225
249
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
}
|
|
250
|
+
// A pixel viewBox keeps the elbow corners round on any stage aspect.
|
|
251
|
+
const viewBox = `0 0 ${px(stageRect.width)} ${px(stageRect.height)}`;
|
|
252
|
+
if (viewBox !== lastViewBox) {
|
|
253
|
+
svgEl.setAttribute('viewBox', viewBox);
|
|
254
|
+
lastViewBox = viewBox;
|
|
236
255
|
}
|
|
237
256
|
|
|
257
|
+
const br = boxEl.getBoundingClientRect();
|
|
258
|
+
const box: Rect = {
|
|
259
|
+
x: br.left - stageRect.left,
|
|
260
|
+
y: br.top - stageRect.top,
|
|
261
|
+
w: br.width,
|
|
262
|
+
h: br.height,
|
|
263
|
+
};
|
|
264
|
+
|
|
238
265
|
const now = performance.now();
|
|
239
266
|
|
|
240
267
|
for (let i = 0; i < tags.length; i++) {
|
|
241
268
|
const tagEl = tagEls[i];
|
|
242
|
-
const
|
|
243
|
-
if (!tagEl || !
|
|
269
|
+
const pathEl = pathEls[i];
|
|
270
|
+
if (!tagEl || !pathEl) continue;
|
|
244
271
|
|
|
245
272
|
// Tag endpoint: center of the pill's rounded cap on the box-facing side.
|
|
246
273
|
const pill = tagEl.querySelector('.ftt-tag') as HTMLElement | null;
|
|
@@ -254,23 +281,19 @@
|
|
|
254
281
|
radius = Math.min(raw, r.height / 2);
|
|
255
282
|
}
|
|
256
283
|
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
const x1 = (px - stageRect.left) / stageRect.width * 100;
|
|
262
|
-
const y1 = (py - stageRect.top) / stageRect.height * 100;
|
|
263
|
-
|
|
264
|
-
lineEl.setAttribute('x1', x1.toFixed(3));
|
|
265
|
-
lineEl.setAttribute('y1', y1.toFixed(3));
|
|
284
|
+
const capOnRight = stacked || tagLeft(i) <= 50;
|
|
285
|
+
const x1 = (capOnRight ? r.right - radius : r.left + radius) - stageRect.left;
|
|
286
|
+
const y1 = r.top + r.height / 2 - stageRect.top;
|
|
266
287
|
|
|
267
|
-
const a = anchorPoint(
|
|
268
|
-
|
|
269
|
-
|
|
288
|
+
const a = anchorPoint(home(i).anchor, box);
|
|
289
|
+
pathEl.setAttribute(
|
|
290
|
+
'd',
|
|
291
|
+
stacked ? elbowPath(x1, y1, a.x, a.y) : `M${px(x1)} ${px(y1)} L${px(a.x)} ${px(a.y)}`,
|
|
292
|
+
);
|
|
270
293
|
const knotEl = knotEls[i];
|
|
271
294
|
if (knotEl) {
|
|
272
|
-
knotEl.style.left = `${a.x
|
|
273
|
-
knotEl.style.top = `${a.y
|
|
295
|
+
knotEl.style.left = `${px(a.x)}px`;
|
|
296
|
+
knotEl.style.top = `${px(a.y)}px`;
|
|
274
297
|
}
|
|
275
298
|
|
|
276
299
|
const ballEl = ballEls[i];
|
|
@@ -290,13 +313,10 @@
|
|
|
290
313
|
triggerBloop();
|
|
291
314
|
continue;
|
|
292
315
|
}
|
|
293
|
-
const x2 = parseFloat(lineEl.getAttribute('x2') || '0');
|
|
294
|
-
const y2 = parseFloat(lineEl.getAttribute('y2') || '0');
|
|
295
316
|
const eased = 1 - Math.pow(1 - t, 3);
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
ballEl.style.
|
|
299
|
-
ballEl.style.top = `${by.toFixed(3)}%`;
|
|
317
|
+
const p = pathEl.getPointAtLength(pathEl.getTotalLength() * eased);
|
|
318
|
+
ballEl.style.left = `${px(p.x)}px`;
|
|
319
|
+
ballEl.style.top = `${px(p.y)}px`;
|
|
300
320
|
ballEl.style.opacity = '1';
|
|
301
321
|
}
|
|
302
322
|
}
|
|
@@ -307,6 +327,7 @@
|
|
|
307
327
|
syncFrame();
|
|
308
328
|
rafId = requestAnimationFrame(loop);
|
|
309
329
|
};
|
|
330
|
+
untrack(syncFrame);
|
|
310
331
|
rafId = requestAnimationFrame(loop);
|
|
311
332
|
return () => cancelAnimationFrame(rafId);
|
|
312
333
|
});
|
|
@@ -469,17 +490,16 @@
|
|
|
469
490
|
|
|
470
491
|
<div
|
|
471
492
|
class="ftt-stage"
|
|
493
|
+
class:ftt-stacked={stacked}
|
|
472
494
|
bind:this={stageEl}
|
|
495
|
+
bind:clientWidth={stageWidth}
|
|
473
496
|
style:--ftt-bob-duration="{duration}s"
|
|
474
497
|
style:--ftt-bob-distance="{-distance}px"
|
|
475
498
|
>
|
|
476
|
-
<svg class="ftt-strings"
|
|
477
|
-
{#each tags as
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
bind:this={lineEls[i]}
|
|
481
|
-
x1={tag.left} y1={tag.top}
|
|
482
|
-
x2={a.x} y2={a.y}
|
|
499
|
+
<svg class="ftt-strings" bind:this={svgEl} aria-hidden="true">
|
|
500
|
+
{#each tags as _t, i (i)}
|
|
501
|
+
<path
|
|
502
|
+
bind:this={pathEls[i]}
|
|
483
503
|
class="ftt-string"
|
|
484
504
|
class:ftt-live={ballStates.has(i)}
|
|
485
505
|
/>
|
|
@@ -504,15 +524,8 @@
|
|
|
504
524
|
>I'm a button</span>
|
|
505
525
|
</div>
|
|
506
526
|
|
|
507
|
-
{#each tags as
|
|
508
|
-
{
|
|
509
|
-
<span
|
|
510
|
-
bind:this={knotEls[i]}
|
|
511
|
-
class="ftt-knot"
|
|
512
|
-
style:left="{a.x}%"
|
|
513
|
-
style:top="{a.y}%"
|
|
514
|
-
aria-hidden="true"
|
|
515
|
-
></span>
|
|
527
|
+
{#each tags as _t, i (i)}
|
|
528
|
+
<span bind:this={knotEls[i]} class="ftt-knot" aria-hidden="true"></span>
|
|
516
529
|
{/each}
|
|
517
530
|
|
|
518
531
|
{#each tags as _t, i (i)}
|
package/src/demo/Section.svelte
CHANGED
|
@@ -33,14 +33,8 @@
|
|
|
33
33
|
|
|
34
34
|
<style>
|
|
35
35
|
.demo-section {
|
|
36
|
-
grid-column:
|
|
36
|
+
grid-column: var(--kit-content);
|
|
37
37
|
display: flex;
|
|
38
38
|
flex-direction: column;
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
@media (max-width: 960px) {
|
|
42
|
-
.demo-section {
|
|
43
|
-
grid-column: 1 / -1;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
40
|
</style>
|