transitions-refine 0.1.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/.agents/skills/refine-live/SKILL.md +205 -0
- package/.agents/skills/transitions-dev/01-card-resize.md +53 -0
- package/.agents/skills/transitions-dev/02-number-pop-in.md +119 -0
- package/.agents/skills/transitions-dev/03-notification-badge.md +110 -0
- package/.agents/skills/transitions-dev/04-text-states-swap.md +97 -0
- package/.agents/skills/transitions-dev/05-menu-dropdown.md +105 -0
- package/.agents/skills/transitions-dev/06-modal.md +94 -0
- package/.agents/skills/transitions-dev/07-panel-reveal.md +81 -0
- package/.agents/skills/transitions-dev/08-page-side-by-side.md +100 -0
- package/.agents/skills/transitions-dev/09-icon-swap.md +78 -0
- package/.agents/skills/transitions-dev/10-success-check.md +169 -0
- package/.agents/skills/transitions-dev/11-avatar-group-hover.md +200 -0
- package/.agents/skills/transitions-dev/12-error-state-shake.md +202 -0
- package/.agents/skills/transitions-dev/13-input-clear-dissolve.md +276 -0
- package/.agents/skills/transitions-dev/14-skeleton-reveal.md +149 -0
- package/.agents/skills/transitions-dev/15-shimmer-text.md +95 -0
- package/.agents/skills/transitions-dev/16-tabs-sliding.md +146 -0
- package/.agents/skills/transitions-dev/17-tooltip.md +103 -0
- package/.agents/skills/transitions-dev/18-texts-reveal.md +110 -0
- package/.agents/skills/transitions-dev/19-card-tilt.md +170 -0
- package/.agents/skills/transitions-dev/20-plus-menu-morph.md +167 -0
- package/.agents/skills/transitions-dev/21-accordion.md +124 -0
- package/.agents/skills/transitions-dev/SKILL.md +225 -0
- package/.agents/skills/transitions-dev/_root.css +204 -0
- package/README.md +89 -0
- package/bin/cli.mjs +264 -0
- package/demo.html +2531 -0
- package/package.json +37 -0
- package/server/inject.mjs +116 -0
- package/server/motion-tokens.mjs +106 -0
- package/server/refine-agent.mjs +86 -0
- package/server/relay.mjs +421 -0
package/demo.html
ADDED
|
@@ -0,0 +1,2531 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Timeline Inspector — Demo</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
10
|
+
<style>
|
|
11
|
+
/* @inject-skip-start (demo-only global resets — excluded from the injected build) */
|
|
12
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
13
|
+
/* @inject-skip-end */
|
|
14
|
+
|
|
15
|
+
:root {
|
|
16
|
+
/* theme tokens (exact Figma — Logram design system) */
|
|
17
|
+
--c-text: #17181c; --c-text-strong: #1b1b1b; --c-text-mut: #585858;
|
|
18
|
+
--c-text-mut2: #696969; --c-text-faint: #979797; --c-count: #9b9b9b;
|
|
19
|
+
--c-ruler: #8b9099; --c-disabled: #9294a6; --c-label: #5e6073;
|
|
20
|
+
--c-blue: #0071e2; --c-blue-pressed: #0073e5; --c-blue-bg: rgba(0,117,237,0.06); --c-blue-bg-h: rgba(0,117,237,0.1);
|
|
21
|
+
--c-blue-bg-a: rgba(0,117,237,0.14); --c-blue-ring: rgba(0,102,215,0.1);
|
|
22
|
+
/* secondary button (grey 170 @ alpha) */
|
|
23
|
+
--c-sec: rgba(170,170,170,0.1); --c-sec-h: rgba(170,170,170,0.2); --c-sec-a: rgba(170,170,170,0.28);
|
|
24
|
+
/* ghost button hover/pressed */
|
|
25
|
+
--c-ghost-h: #f7f7f7; --c-ghost-a: #f0f0f0;
|
|
26
|
+
/* play/pause circle */
|
|
27
|
+
--c-circle: #ffffff; --c-circle-h: #f9f9f9; --c-circle-a: #f4f4f4;
|
|
28
|
+
/* fields + tracks */
|
|
29
|
+
--c-field-bg: rgba(238,238,239,0.51); --c-fill: rgba(219,219,219,0.4); --c-fill-a: rgba(219,219,219,0.6);
|
|
30
|
+
--c-track: #eeeeef; --c-track-h: #e9e9e9; --c-thumb: #767676;
|
|
31
|
+
--c-line: #ededf0; --c-hairline: rgba(0,0,0,0.04); --c-hairline2: rgba(0,0,0,0.06);
|
|
32
|
+
/* shadow recipes (exact Figma) */
|
|
33
|
+
--ring: inset 0 0 0 1px rgba(0,0,0,0.06), inset 0 -1px 0 0 rgba(0,0,0,0.06), inset 0 0 0 1px rgba(196,196,196,0.1);
|
|
34
|
+
--ring-blue: inset 0 0 0 1px rgba(0,102,215,0.1), inset 0 -1px 0 0 rgba(0,0,0,0.06), inset 0 0 0 1px rgba(196,196,196,0.1);
|
|
35
|
+
--ring-fill: inset 0 0 0 1px rgba(0,0,0,0.02), inset 0 -1px 0 0 rgba(0,0,0,0.06), inset 0 0 0 1px rgba(196,196,196,0.1);
|
|
36
|
+
--drop: 0 1px 3px 0 rgba(0,0,0,0.04);
|
|
37
|
+
--drop-line: 0 1px 3px 0 rgba(0,0,0,0.08);
|
|
38
|
+
--shadow-border: 0 0 0 1px rgba(0,0,0,0.06), 0 1px 2px -1px rgba(0,0,0,0.06), 0 2px 4px 0 rgba(0,0,0,0.04);
|
|
39
|
+
--shadow-border-hover: 0 0 0 1px rgba(0,0,0,0.08), 0 1px 2px -1px rgba(0,0,0,0.08), 0 2px 4px 0 rgba(0,0,0,0.06);
|
|
40
|
+
--panel-shadow: 0 10px 40px rgba(0,0,0,0.05), 0 0 0 1px rgba(0,0,0,0.06), 0 1px 1px rgba(0,0,0,0.06), 0 2px 6px rgba(0,0,0,0.05);
|
|
41
|
+
--menu-shadow: 0 0 0 1px rgba(0,0,0,0.06), 0 2px 6px rgba(0,0,0,0.05), 0 4px 42px rgba(0,0,0,0.06);
|
|
42
|
+
--card-shadow: 0 0 0 1px rgba(0,0,0,0.05), 0 1px 2px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06);
|
|
43
|
+
|
|
44
|
+
/* transitions.dev — menu dropdown */
|
|
45
|
+
--dropdown-open-dur: 250ms; --dropdown-close-dur: 150ms;
|
|
46
|
+
--dropdown-pre-scale: 0.97; --dropdown-closing-scale: 0.99;
|
|
47
|
+
--dropdown-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
48
|
+
/* transitions.dev — icon swap */
|
|
49
|
+
--icon-swap-dur: 200ms; --icon-swap-blur: 2px; --icon-swap-start-scale: 0.25; --icon-swap-ease: ease-in-out;
|
|
50
|
+
/* transitions.dev — tooltip */
|
|
51
|
+
--tt-in-dur: 150ms; --tt-out-dur: 50ms; --tt-scale: 0.98; --tt-delay: 80ms;
|
|
52
|
+
--tt-in-ease: ease-out; --tt-out-ease: ease-out; --tt-bg: #ffffff; --tt-fg: #2f2f2f;
|
|
53
|
+
/* transitions.dev — easing motion tokens (verbatim from skill _root.css) */
|
|
54
|
+
--panel-ease: cubic-bezier(0.22, 1, 0.36, 1); /* shared standard ease-out */
|
|
55
|
+
--morph-ease: cubic-bezier(0.34, 1.25, 0.64, 1);
|
|
56
|
+
--check-ease-bob: cubic-bezier(0.34, 1.35, 0.64, 1);
|
|
57
|
+
--badge-pop-ease: cubic-bezier(0.34, 1.36, 0.64, 1);
|
|
58
|
+
--digit-ease: cubic-bezier(0.34, 1.45, 0.64, 1);
|
|
59
|
+
--avatar-ease-out: cubic-bezier(0.34, 3.85, 0.64, 1);
|
|
60
|
+
--badge-close-ease: cubic-bezier(0.4, 0, 0.2, 1);
|
|
61
|
+
--text-swap-ease: ease-in-out;
|
|
62
|
+
--shimmer-ease: linear;
|
|
63
|
+
/* transitions.dev — panel reveal (per-phase distance/ease/scale) */
|
|
64
|
+
--panel-open-dur: 400ms; --panel-close-dur: 250ms;
|
|
65
|
+
/* refine panel close runs a touch longer than the main panel close */
|
|
66
|
+
--refine-close-dur: 350ms;
|
|
67
|
+
--panel-open-distance: 100px; --panel-close-distance: 10px;
|
|
68
|
+
--panel-open-ease: var(--panel-ease); --panel-close-ease: var(--panel-ease);
|
|
69
|
+
--panel-scale: 1; --panel-scale-close: 1;
|
|
70
|
+
--panel-blur: 2px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* @inject-skip-start (demo-only page + sample-box styles — excluded from the injected build) */
|
|
74
|
+
body {
|
|
75
|
+
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
|
76
|
+
background: #fafafa;
|
|
77
|
+
color: var(--c-text);
|
|
78
|
+
min-height: 100vh;
|
|
79
|
+
-webkit-font-smoothing: antialiased;
|
|
80
|
+
-moz-osx-font-smoothing: grayscale;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.demo-root { max-width: 900px; margin: 0 auto; padding: 48px 24px 120px; }
|
|
84
|
+
.demo-header { margin-bottom: 40px; }
|
|
85
|
+
.demo-header h1 { font-size: 26px; font-weight: 700; letter-spacing: -0.5px; color: #171717; }
|
|
86
|
+
.demo-header p { color: var(--c-ruler); font-size: 14px; margin-top: 6px; }
|
|
87
|
+
.demo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 20px; }
|
|
88
|
+
.card { padding: 24px; background: #fff; border-radius: 14px; box-shadow: 0 0 0 1px rgba(0,0,0,0.05), 0 2px 6px rgba(0,0,0,0.04); }
|
|
89
|
+
.card h2 { font-size: 15px; font-weight: 600; margin-bottom: 4px; color: #171717; }
|
|
90
|
+
.card p { font-size: 12px; color: var(--c-ruler); margin-bottom: 16px; line-height: 1.5; }
|
|
91
|
+
|
|
92
|
+
.box-resize { width: 80px; height: 80px; background: #ef6c6c; border-radius: 8px; cursor: pointer;
|
|
93
|
+
transition: width 0.4s ease-out, height 0.4s ease-out, background 0.4s ease-out, border-radius 0.3s ease; }
|
|
94
|
+
.box-resize.expanded { width: 180px; height: 140px; background: #6c8eef; border-radius: 24px; }
|
|
95
|
+
|
|
96
|
+
.box-opacity { width: 100px; height: 80px; background: #38c172; border-radius: 10px; cursor: pointer;
|
|
97
|
+
transition: opacity 0.6s ease-in-out, transform 0.6s ease-in-out; }
|
|
98
|
+
.box-opacity.faded { opacity: 0.15; transform: scale(0.85); }
|
|
99
|
+
|
|
100
|
+
.box-slide { width: 60px; height: 60px; background: #ef9c6c; border-radius: 50%; cursor: pointer;
|
|
101
|
+
transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.5s ease; }
|
|
102
|
+
.box-slide.moved { transform: translateX(160px); background: #c96cef; }
|
|
103
|
+
|
|
104
|
+
.box-color { width: 100%; height: 60px; border-radius: 10px; cursor: pointer;
|
|
105
|
+
background: linear-gradient(135deg, #e9e9ef, #d6d6e0);
|
|
106
|
+
transition: background 0.8s ease, box-shadow 0.8s ease; }
|
|
107
|
+
.box-color.lit { background: linear-gradient(135deg, #6c8eef, #ef6c9c);
|
|
108
|
+
box-shadow: 0 0 30px rgba(108, 142, 239, 0.4); }
|
|
109
|
+
/* @inject-skip-end */
|
|
110
|
+
|
|
111
|
+
/* ───────────────────── timeline panel ───────────────────── */
|
|
112
|
+
/* unified panel container — single rounded surface, no gaps (matches Figma) */
|
|
113
|
+
[data-timeline-panel] {
|
|
114
|
+
position: fixed; left: 0; right: 0; bottom: 0; z-index: 99999;
|
|
115
|
+
display: flex; flex-direction: column; gap: 0;
|
|
116
|
+
padding: 12px; color: var(--c-text);
|
|
117
|
+
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
|
118
|
+
font-size: 13px; background: transparent; pointer-events: none;
|
|
119
|
+
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
120
|
+
}
|
|
121
|
+
[data-timeline-panel] > * { pointer-events: auto; }
|
|
122
|
+
.tl-resize-handle {
|
|
123
|
+
position: absolute; top: 0; left: 0; right: 0; height: 14px; z-index: 12;
|
|
124
|
+
cursor: ns-resize; touch-action: none;
|
|
125
|
+
}
|
|
126
|
+
.tl-resize-handle::after {
|
|
127
|
+
content: ""; position: absolute; left: 50%; top: 6px; transform: translateX(-50%);
|
|
128
|
+
width: 40px; height: 3px; border-radius: 2px; background: #dcdce2; pointer-events: none;
|
|
129
|
+
transition: background 0.15s;
|
|
130
|
+
}
|
|
131
|
+
.tl-resize-handle:hover::after, .tl-resize-handle.dragging::after { background: var(--c-blue); }
|
|
132
|
+
.tl-panel-body { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: row; gap: 0; pointer-events: auto;
|
|
133
|
+
background: #fff; border-radius: 12px; box-shadow: var(--card-shadow); overflow: hidden; }
|
|
134
|
+
.tl-panel-body > * { pointer-events: auto; }
|
|
135
|
+
/* timeline content column — full width; the refine panel overlays it without reflow */
|
|
136
|
+
.tl-panel-main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 0; overflow: hidden; }
|
|
137
|
+
.tl-pill { position: fixed; bottom: 16px; right: 16px; z-index: 99999;
|
|
138
|
+
height: 40px; background: #fff; border-radius: 36px;
|
|
139
|
+
padding: 6px 10px 6px 16px;
|
|
140
|
+
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
|
141
|
+
cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px;
|
|
142
|
+
box-shadow: var(--drop), var(--ring); transition: background 0.15s, scale 0.12s ease;
|
|
143
|
+
animation: tl-pill-in 220ms cubic-bezier(0.22, 1, 0.36, 1) both; }
|
|
144
|
+
@keyframes tl-pill-in { from { opacity: 0; transform: translateY(6px) scale(0.96); }
|
|
145
|
+
to { opacity: 1; transform: none; } }
|
|
146
|
+
.tl-pill:hover { background: #f9f9f9; }
|
|
147
|
+
.tl-pill:active { background: #f9f9f9; scale: 0.96; }
|
|
148
|
+
.tl-pill-label { font-size: 13px; font-weight: 500; line-height: 14px; color: var(--c-text); white-space: nowrap; }
|
|
149
|
+
.tl-pill-count { display: flex; align-items: center; justify-content: center;
|
|
150
|
+
height: 20px; min-width: 20px; padding: 0 6px; border-radius: 999px;
|
|
151
|
+
background: #060606; color: #fff; font-size: 11px; font-weight: 600; line-height: 16px;
|
|
152
|
+
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.2), inset 0 0 0 1px rgba(0,0,0,0.06), inset 0 -1px 0 0 rgba(0,0,0,0.06); }
|
|
153
|
+
/* single-digit count → perfect circle (no side padding so width == height) */
|
|
154
|
+
.tl-pill-count.is-single { width: 20px; padding: 0; }
|
|
155
|
+
|
|
156
|
+
/* ── header row ── */
|
|
157
|
+
.tl-header { flex: none; display: flex; align-items: center; gap: 8px; padding: 10px 14px;
|
|
158
|
+
border-bottom: 1px solid var(--c-line); }
|
|
159
|
+
.tl-header-label { font-size: 13px; font-weight: 500; line-height: 18px; color: var(--c-text-mut2); white-space: nowrap; }
|
|
160
|
+
.tl-header-count { margin-left: auto; font-size: 13px; line-height: 18px; color: var(--c-count); white-space: nowrap; }
|
|
161
|
+
|
|
162
|
+
/* ── buttons ── */
|
|
163
|
+
/* topbar buttons share a fully rounded 60px corner radius */
|
|
164
|
+
.tl-header .tl-ghost-btn,
|
|
165
|
+
.tl-header .tl-icon-btn,
|
|
166
|
+
.tl-header .tl-sec-btn,
|
|
167
|
+
.tl-header .tl-refine-btn { border-radius: 60px; }
|
|
168
|
+
|
|
169
|
+
/* ghost button: transparent, grey on hover/pressed (Figma #f7f7f7) */
|
|
170
|
+
.tl-ghost-btn { position: relative; display: inline-flex; align-items: center; gap: 6px; height: 36px;
|
|
171
|
+
padding: 0 10px 0 12px; border: none; background: transparent; border-radius: 8px;
|
|
172
|
+
font: inherit; font-size: 13px; font-weight: 500; line-height: 16px; color: var(--c-text-strong); cursor: pointer;
|
|
173
|
+
transition: background 0.12s ease, scale 0.12s ease; white-space: nowrap; }
|
|
174
|
+
.tl-ghost-btn:hover:not(:disabled), .tl-ghost-btn.is-active { background: var(--c-ghost-h); }
|
|
175
|
+
.tl-ghost-btn:active:not(:disabled) { background: var(--c-ghost-a); scale: 0.97; }
|
|
176
|
+
.tl-ghost-btn:disabled { opacity: 0.6; cursor: default; }
|
|
177
|
+
.tl-ghost-btn .tl-dim { color: var(--c-text-faint); font-weight: 500; }
|
|
178
|
+
.tl-ghost-chev { display: flex; color: var(--c-ruler); }
|
|
179
|
+
|
|
180
|
+
/* icon button: flat secondary fill, no drop shadow (matches design-system secondary) */
|
|
181
|
+
.tl-icon-btn { position: relative; display: inline-flex; align-items: center; justify-content: center; flex: none;
|
|
182
|
+
width: 36px; height: 36px; border: none; border-radius: 8px; background: var(--c-sec);
|
|
183
|
+
color: var(--c-text-mut2); cursor: pointer; transition: background 0.12s ease, color 0.12s ease, scale 0.12s ease; }
|
|
184
|
+
.tl-icon-btn:hover:not(:disabled), .tl-icon-btn.is-active { background: var(--c-sec-h); }
|
|
185
|
+
.tl-icon-btn:active:not(:disabled) { background: var(--c-sec-a); scale: 0.96; }
|
|
186
|
+
.tl-icon-btn:disabled { opacity: 0.5; cursor: default; }
|
|
187
|
+
.tl-icon-btn.ghost { background: transparent; }
|
|
188
|
+
.tl-icon-btn.ghost:hover:not(:disabled) { background: var(--c-sec); }
|
|
189
|
+
|
|
190
|
+
/* secondary button (Reset): flat grey fill, no shadow */
|
|
191
|
+
.tl-sec-btn { display: inline-flex; align-items: center; height: 36px; padding: 0 16px; border: none;
|
|
192
|
+
border-radius: 8px; background: var(--c-sec); color: var(--c-text); font: inherit; font-size: 13px;
|
|
193
|
+
font-weight: 500; cursor: pointer; transition: background 0.12s ease, scale 0.12s ease; white-space: nowrap; }
|
|
194
|
+
.tl-sec-btn:hover:not(:disabled) { background: var(--c-sec-h); }
|
|
195
|
+
.tl-sec-btn:active:not(:disabled) { background: var(--c-sec-a); scale: 0.96; }
|
|
196
|
+
.tl-sec-btn:disabled { opacity: 0.5; cursor: default; }
|
|
197
|
+
|
|
198
|
+
/* blue button (Refine) — Figma Frame 427319678: layered fill + inset ring + drop shadow */
|
|
199
|
+
.tl-refine-btn { position: relative; isolation: isolate;
|
|
200
|
+
display: inline-flex; align-items: center; gap: 8px; height: 36px; padding: 0 12px;
|
|
201
|
+
border: none; border-radius: 8px; background: transparent; color: var(--c-blue);
|
|
202
|
+
box-shadow: var(--drop);
|
|
203
|
+
box-shadow: 0 1px 3px 0 color(display-p3 0 0 0 / 0.04);
|
|
204
|
+
font: inherit; font-size: 13px; font-weight: 500; line-height: 14px;
|
|
205
|
+
cursor: pointer; white-space: nowrap; transition: color 0.12s ease; }
|
|
206
|
+
.tl-refine-btn::before { content: ""; position: absolute; inset: 0; border-radius: inherit;
|
|
207
|
+
background: var(--c-blue-bg);
|
|
208
|
+
background: color(display-p3 0 0.451 0.898 / 0.06);
|
|
209
|
+
transition: background 0.12s ease; z-index: 0; }
|
|
210
|
+
.tl-refine-btn::after { content: ""; position: absolute; inset: 0; border-radius: inherit;
|
|
211
|
+
box-shadow: var(--ring-blue);
|
|
212
|
+
box-shadow: inset 0 0 0 1px color(display-p3 0 0.3942 0.8155 / 0.10), inset 0 -1px 0 0 color(display-p3 0 0 0 / 0.06), inset 0 0 0 1px color(display-p3 0.7674 0.7674 0.7674 / 0.10);
|
|
213
|
+
pointer-events: none; z-index: 2; }
|
|
214
|
+
.tl-refine-btn > * { position: relative; z-index: 1; }
|
|
215
|
+
/* Figma: wand icon stroke is #0073E5, distinct from the #0071e2 label */
|
|
216
|
+
.tl-refine-btn > svg { color: var(--c-blue-pressed); }
|
|
217
|
+
.tl-refine-btn:hover:not(:disabled)::before { background: var(--c-blue-bg-h); background: color(display-p3 0 0.451 0.898 / 0.10); }
|
|
218
|
+
.tl-refine-btn:active:not(:disabled) { color: var(--c-blue-pressed); }
|
|
219
|
+
.tl-refine-btn:active:not(:disabled)::before { background: var(--c-blue-bg-h); background: color(display-p3 0 0.451 0.898 / 0.10); }
|
|
220
|
+
.tl-refine-btn:disabled { cursor: default; color: var(--c-blue); }
|
|
221
|
+
.tl-refine-btn:disabled::before { background: var(--c-blue-bg); background: color(display-p3 0 0.451 0.898 / 0.06); }
|
|
222
|
+
.tl-refine-btn:disabled span { color: var(--c-blue-pressed); opacity: 0.5; }
|
|
223
|
+
/* tiny 1px sparks flying out along each of the wand's spark rays on hover */
|
|
224
|
+
.tl-refine-sparks { position: absolute; left: 23px; top: 15px; width: 0; height: 0; z-index: 3; pointer-events: none; }
|
|
225
|
+
.tl-refine-sparks i { position: absolute; left: 0; top: 0; width: 1px; height: 1px; border-radius: 50%;
|
|
226
|
+
background: var(--c-blue); opacity: 0; }
|
|
227
|
+
@keyframes tl-refine-spark {
|
|
228
|
+
0% { opacity: 0; transform: translate(var(--ox, 0), var(--oy, 0)) scale(0.4); }
|
|
229
|
+
20% { opacity: 1; transform: translate(calc(var(--ox, 0) + var(--sx, 0) * 0.2), calc(var(--oy, 0) + var(--sy, -12px) * 0.2)) scale(1); }
|
|
230
|
+
100% { opacity: 0; transform: translate(calc(var(--ox, 0) + var(--sx, 0)), calc(var(--oy, 0) + var(--sy, -12px))) scale(0.6); }
|
|
231
|
+
}
|
|
232
|
+
.tl-refine-btn:hover:not(:disabled) .tl-refine-sparks i {
|
|
233
|
+
animation: tl-refine-spark var(--sd, 900ms) ease-out infinite;
|
|
234
|
+
animation-delay: var(--sdelay, 0ms); }
|
|
235
|
+
@media (prefers-reduced-motion: reduce) {
|
|
236
|
+
.tl-refine-btn:hover:not(:disabled) .tl-refine-sparks i { animation: none; } }
|
|
237
|
+
|
|
238
|
+
/* ── transport row ── */
|
|
239
|
+
.tl-transport { display: flex; align-items: center; gap: 8px; padding: 10px 16px; border-bottom: 1px solid var(--c-line); }
|
|
240
|
+
.tl-transport-left { flex: 1; display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
241
|
+
.tl-transport-center { flex: 0 0 auto; display: flex; justify-content: center; }
|
|
242
|
+
.tl-transport-right { flex: 1; display: flex; align-items: center; justify-content: flex-end; gap: 12px; }
|
|
243
|
+
.tl-transport .tl-ghost-btn,
|
|
244
|
+
.tl-transport .tl-icon-btn,
|
|
245
|
+
.tl-transport .tl-play-btn { border-radius: 60px; }
|
|
246
|
+
.tl-timecode { font-variant-numeric: tabular-nums; font-size: 13px; font-weight: 500; color: var(--c-text); min-width: 70px; }
|
|
247
|
+
.tl-speed { font-variant-numeric: tabular-nums; }
|
|
248
|
+
.tl-zoom { position: relative; width: 72px; height: 15px; flex: none; }
|
|
249
|
+
/* slider — Logram design system (node 13064:2539): 15px frame, track at y=5, 15px knob at y=0 */
|
|
250
|
+
.tl-zoom-track { position: absolute; left: 0; right: 0; top: 5px; height: 5px; pointer-events: none;
|
|
251
|
+
background: rgba(115,115,115,0.1); border: 1px solid rgba(0,0,0,0.08); border-radius: 7px; box-sizing: border-box; }
|
|
252
|
+
.tl-zoom input[type="range"] { position: relative; z-index: 1; width: 100%; height: 15px; margin: 0;
|
|
253
|
+
-webkit-appearance: none; appearance: none; background: transparent; outline: none; cursor: pointer; }
|
|
254
|
+
.tl-zoom input[type="range"]::-webkit-slider-runnable-track { height: 15px; background: transparent; border: none; }
|
|
255
|
+
.tl-zoom input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none;
|
|
256
|
+
width: 15px; height: 15px; margin-top: 0; border-radius: 50%; background: #fff; cursor: pointer;
|
|
257
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.08), inset 0 0 0 1px rgba(126,126,126,0.1), inset 0 -1px 0 rgba(0,0,0,0.1); }
|
|
258
|
+
.tl-zoom input[type="range"]::-moz-range-track { height: 15px; background: transparent; border: none; }
|
|
259
|
+
.tl-zoom input[type="range"]::-moz-range-thumb { width: 15px; height: 15px; border: none; border-radius: 50%;
|
|
260
|
+
background: #fff; cursor: pointer;
|
|
261
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.08), inset 0 0 0 1px rgba(126,126,126,0.1), inset 0 -1px 0 rgba(0,0,0,0.1); }
|
|
262
|
+
/* play/pause circle — Figma: drop shadow + inset ring overlay */
|
|
263
|
+
.tl-play-btn { position: relative; isolation: isolate; overflow: hidden;
|
|
264
|
+
display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px;
|
|
265
|
+
border: none; border-radius: 50%; background: var(--c-circle); color: #17181c;
|
|
266
|
+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.04); cursor: pointer;
|
|
267
|
+
transition: background 0.12s ease, scale 0.12s ease; }
|
|
268
|
+
.tl-play-btn::after { content: ""; position: absolute; inset: 0; border-radius: inherit;
|
|
269
|
+
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06), inset 0 -1px 0 0 rgba(0, 0, 0, 0.06),
|
|
270
|
+
inset 0 0 0 1px rgba(196, 196, 196, 0.10); pointer-events: none; }
|
|
271
|
+
.tl-play-btn > * { position: relative; z-index: 1; }
|
|
272
|
+
.tl-play-btn:hover:not(:disabled) { background: var(--c-circle-h); }
|
|
273
|
+
.tl-play-btn:active:not(:disabled) { background: var(--c-circle-a); scale: 0.96; }
|
|
274
|
+
.tl-play-btn:disabled { opacity: 0.5; cursor: default; }
|
|
275
|
+
/* center both icon layers in the swap cell so the differently-sized play (10×12)
|
|
276
|
+
and pause (16²) svgs share one center; nudge ONLY the play triangle right for
|
|
277
|
+
optical centering (skill: play-button triangles). left/position (not transform)
|
|
278
|
+
so it doesn't clash with the icon-swap scale animation; pause stays dead-center. */
|
|
279
|
+
.tl-play-btn .t-icon { place-self: center; }
|
|
280
|
+
.tl-play-btn .t-icon[data-icon="a"] { position: relative; left: 1px; }
|
|
281
|
+
|
|
282
|
+
/* ── body / floating cards row ── */
|
|
283
|
+
.tl-body { display: flex; flex: 1; min-height: 0; gap: 0; }
|
|
284
|
+
.tl-main { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
285
|
+
.tl-inspector { flex: 0 0 280px; padding: 14px 16px 18px; display: flex; flex-direction: column;
|
|
286
|
+
border-left: 1px solid var(--c-line); min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
|
|
287
|
+
.tl-insp-title { font-size: 13px; font-weight: 500; line-height: 18px; color: #171717; margin-bottom: 12px; text-transform: capitalize; }
|
|
288
|
+
.tl-insp-label { font-size: 12px; line-height: 18px; color: #737373; margin: 10px 0 6px; }
|
|
289
|
+
|
|
290
|
+
/* ── tracks / ruler ── */
|
|
291
|
+
.tl-tracks { position: relative; flex: 1; }
|
|
292
|
+
.tl-ruler-row { display: flex; height: 30px; border-bottom: 1px solid var(--c-line); }
|
|
293
|
+
.tl-ruler-spacer { flex: 0 0 150px; border-right: 1px solid var(--c-line); }
|
|
294
|
+
.tl-ruler { position: relative; flex: 1; margin-left: 16px; }
|
|
295
|
+
.tl-ruler .maj { position: absolute; top: 9px; font-size: 12px; line-height: 14px; color: var(--c-ruler); transform: translateX(-50%); white-space: nowrap; }
|
|
296
|
+
.tl-ruler .maj.first { transform: none; left: 4px !important; }
|
|
297
|
+
.tl-ruler .maj.last { transform: translateX(-100%); margin-left: -4px; }
|
|
298
|
+
.tl-ruler .tick { position: absolute; top: 14px; width: 2px; height: 2px; border-radius: 50%;
|
|
299
|
+
background: #cdced4; transform: translateX(-50%); }
|
|
300
|
+
.tl-prop-row { display: flex; align-items: stretch; height: 48px; border-bottom: 1px solid var(--c-line); cursor: pointer; }
|
|
301
|
+
.tl-prop-row:hover { background: #fcfcfd; }
|
|
302
|
+
.tl-prop-row.selected { background: #fafafa; }
|
|
303
|
+
.tl-prop-head { flex: 0 0 150px; display: flex; align-items: center; gap: 8px; padding-left: 12px;
|
|
304
|
+
padding-right: 10px; overflow: hidden; border-right: 1px solid var(--c-line); }
|
|
305
|
+
.tl-prop-grip { display: flex; color: #cccdd4; flex: none; cursor: grab; }
|
|
306
|
+
.tl-prop-grip:active { cursor: grabbing; }
|
|
307
|
+
.tl-rows { position: relative; }
|
|
308
|
+
.tl-prop-row.reordering { background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.06), 0 6px 16px rgba(0,0,0,0.10);
|
|
309
|
+
position: relative; z-index: 5; cursor: grabbing; }
|
|
310
|
+
.tl-prop-row.reordering .tl-prop-grip { color: var(--c-ruler); cursor: grabbing; }
|
|
311
|
+
.tl-prop-label { font-size: 13px; font-weight: 500; line-height: 18px; color: var(--c-text-mut2); white-space: nowrap; overflow: hidden;
|
|
312
|
+
text-overflow: ellipsis; text-transform: capitalize; }
|
|
313
|
+
.tl-prop-row.selected .tl-prop-label { color: #171717; }
|
|
314
|
+
/* 16px left inset of the plot area (kept in sync with ruler/playhead/scrub) */
|
|
315
|
+
.tl-prop-track { position: relative; flex: 1; user-select: none; margin-left: 16px; }
|
|
316
|
+
/* timeline line — capsule (fully rounded), Figma #eeeeef default / #e9e9e9 hover */
|
|
317
|
+
.tl-bar { position: absolute; top: 50%; height: 24px; transform: translateY(-50%); background: var(--c-track);
|
|
318
|
+
border-radius: 8px; min-width: 24px; cursor: grab;
|
|
319
|
+
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.08), inset 0 0 0 1px rgba(0,0,0,0.06), inset 0 -1px 0 0 rgba(0,0,0,0.1), inset 0 0 0 1px rgba(196,196,196,0.1);
|
|
320
|
+
transition: background 0.12s ease; }
|
|
321
|
+
.tl-prop-row:hover .tl-bar, .tl-prop-row.selected .tl-bar { background: var(--c-track-h); }
|
|
322
|
+
.tl-bar:active { cursor: grabbing; }
|
|
323
|
+
/* resize handles — Figma node 13044:2625: 2×14px, #767676 @60%, centered, ~5px edge inset */
|
|
324
|
+
.tl-bar-handle { position: absolute; top: 50%; transform: translateY(-50%); width: 2px; height: 14px; border-radius: 20px;
|
|
325
|
+
background: var(--c-thumb); opacity: 0; transition: opacity 0.12s ease; pointer-events: none; }
|
|
326
|
+
.tl-bar-handle.left { left: 5px; }
|
|
327
|
+
.tl-bar-handle.right { right: 5px; }
|
|
328
|
+
.tl-prop-row:hover .tl-bar-handle { opacity: 0.6; }
|
|
329
|
+
.tl-bar:active .tl-bar-handle,
|
|
330
|
+
.tl-bar:has(.tl-bar-grip:active) .tl-bar-handle { opacity: 1; }
|
|
331
|
+
.tl-bar-grip { position: absolute; top: 0; bottom: 0; width: 10px; cursor: ew-resize; z-index: 2; }
|
|
332
|
+
.tl-bar-grip.left { left: -5px; } .tl-bar-grip.right { right: -5px; }
|
|
333
|
+
|
|
334
|
+
.tl-playhead-layer { position: absolute; top: 14px; bottom: 0; left: 166px; right: 0;
|
|
335
|
+
pointer-events: none; z-index: 4; }
|
|
336
|
+
.tl-playhead { position: absolute; top: 0; bottom: 0; width: 2px; background: #1A7AFF; transform: translateX(-1px); }
|
|
337
|
+
.tl-playhead-head { position: absolute; top: -6px; left: 50%; transform: translateX(-50%); width: 16.666px; height: 24px;
|
|
338
|
+
overflow: visible; filter: drop-shadow(0 1px 3px rgba(0,0,0,.25)); }
|
|
339
|
+
.tl-scrub-zone { position: absolute; top: 0; height: 30px; left: 166px; right: 0;
|
|
340
|
+
z-index: 6; cursor: ew-resize; }
|
|
341
|
+
|
|
342
|
+
/* ── value field (slider + input) — exact Figma "Value slider and input" ── */
|
|
343
|
+
.tl-field-wrap { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
|
344
|
+
.tl-field { position: relative; flex: 1; min-width: 0; height: 36px; border-radius: 8px;
|
|
345
|
+
background: var(--c-field-bg); transition: box-shadow 0.12s ease; }
|
|
346
|
+
/* focused: dual inset ring (Figma Focused state) */
|
|
347
|
+
.tl-field.is-editing { box-shadow: inset 0 0 0 1px rgba(0,0,0,0.14), inset 0 0 0 0.5px rgba(255,255,255,0.06); }
|
|
348
|
+
/* inner slider fill — Figma button/tiny: soft drop shadow + inset ring give the
|
|
349
|
+
"raised block" look (shadow 0 1px 3px @4%, hairline border, bottom 1px). */
|
|
350
|
+
.tl-field-fill { position: absolute; left: 0; top: 0; bottom: 0; min-width: 36px; border-radius: 8px;
|
|
351
|
+
background: var(--c-fill); box-shadow: var(--drop), var(--ring-fill);
|
|
352
|
+
pointer-events: none; transition: background 0.12s ease, opacity 0.12s ease; z-index: 1; }
|
|
353
|
+
.tl-field.is-dragging .tl-field-fill { background: var(--c-fill-a); }
|
|
354
|
+
.tl-field.is-dragging .tl-field-label { opacity: 0.7; }
|
|
355
|
+
.tl-field.is-editing .tl-field-fill { opacity: 0; }
|
|
356
|
+
.tl-field-track { position: absolute; inset: 0; cursor: ew-resize; z-index: 2; }
|
|
357
|
+
.tl-field-label { position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
|
|
358
|
+
font-size: 13px; font-weight: 500; color: var(--c-text-mut); pointer-events: none; z-index: 3; }
|
|
359
|
+
.tl-field-value { position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
|
|
360
|
+
font-size: 13px; color: var(--c-text); font-weight: 500; cursor: text; z-index: 3;
|
|
361
|
+
font-variant-numeric: tabular-nums; }
|
|
362
|
+
.tl-field-input { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); width: 64px;
|
|
363
|
+
text-align: right; border: none; background: transparent; font: inherit; font-size: 13px; font-weight: 500;
|
|
364
|
+
color: var(--c-text); outline: none; z-index: 4; font-variant-numeric: tabular-nums; }
|
|
365
|
+
/* thumb at the right edge of the fill (Figma #767676 2×20px, inset 8px, hover 60% / drag 100%) */
|
|
366
|
+
.tl-field-thumb { position: absolute; right: 8px; top: 8px; height: 20px; width: 2px;
|
|
367
|
+
background: var(--c-thumb); border-radius: 20px; opacity: 0; pointer-events: none;
|
|
368
|
+
transition: opacity 0.12s ease; z-index: 3; }
|
|
369
|
+
.tl-field:hover .tl-field-thumb { opacity: 0.6; }
|
|
370
|
+
.tl-field.is-dragging .tl-field-thumb { opacity: 1; }
|
|
371
|
+
.tl-field.is-editing .tl-field-thumb { opacity: 0; }
|
|
372
|
+
.tl-field-chevron { flex: none; width: 36px; height: 36px; border-radius: 8px; display: flex;
|
|
373
|
+
align-items: center; justify-content: center; border: none; background: var(--c-field-bg);
|
|
374
|
+
color: var(--c-ruler); cursor: pointer; transition: background 0.12s ease, scale 0.12s ease; }
|
|
375
|
+
.tl-field-chevron:hover { background: var(--c-sec-h); }
|
|
376
|
+
.tl-field-chevron:active { scale: 0.96; }
|
|
377
|
+
|
|
378
|
+
/* ── easing editor ── */
|
|
379
|
+
.tl-ease { display: flex; flex-direction: column; gap: 8px; }
|
|
380
|
+
|
|
381
|
+
/* tab content page-slide (transitions.dev · 08-page-side-by-side) */
|
|
382
|
+
.tl-ease-pages.t-page-slide {
|
|
383
|
+
position: relative;
|
|
384
|
+
--page-slide-dur: 250ms;
|
|
385
|
+
--page-fade-dur: 250ms;
|
|
386
|
+
--page-slide-distance: 8px;
|
|
387
|
+
--page-blur: 3px;
|
|
388
|
+
--page-stagger: 0ms;
|
|
389
|
+
--page-exit-enabled: 1;
|
|
390
|
+
--page-slide-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
391
|
+
--page-fade-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
392
|
+
transition: height var(--page-slide-dur) var(--page-slide-ease);
|
|
393
|
+
overflow: hidden;
|
|
394
|
+
}
|
|
395
|
+
.tl-ease-pages .t-page[data-page-id="1"] { --t-page-from-x: calc(var(--page-slide-distance) * -1); }
|
|
396
|
+
.tl-ease-pages .t-page[data-page-id="2"] { --t-page-from-x: var(--page-slide-distance); }
|
|
397
|
+
.tl-ease-pages .t-page {
|
|
398
|
+
position: absolute; top: 0; left: 0; right: 0;
|
|
399
|
+
opacity: 0; pointer-events: none;
|
|
400
|
+
transform: translateX(calc(var(--t-page-from-x, 0px) * var(--page-exit-enabled)));
|
|
401
|
+
filter: blur(calc(var(--page-blur) * var(--page-exit-enabled)));
|
|
402
|
+
transition:
|
|
403
|
+
opacity var(--page-fade-dur) var(--page-fade-ease),
|
|
404
|
+
transform var(--page-slide-dur) var(--page-slide-ease),
|
|
405
|
+
filter var(--page-slide-dur) var(--page-slide-ease);
|
|
406
|
+
will-change: opacity, transform, filter;
|
|
407
|
+
}
|
|
408
|
+
.tl-ease-pages[data-page="1"] .t-page[data-page-id="1"],
|
|
409
|
+
.tl-ease-pages[data-page="2"] .t-page[data-page-id="2"] {
|
|
410
|
+
opacity: 1; pointer-events: auto; transform: translateX(0); filter: blur(0);
|
|
411
|
+
transition-delay: var(--page-stagger);
|
|
412
|
+
}
|
|
413
|
+
@media (prefers-reduced-motion: reduce) {
|
|
414
|
+
.tl-ease-pages.t-page-slide,
|
|
415
|
+
.tl-ease-pages .t-page { transition: none !important; }
|
|
416
|
+
}
|
|
417
|
+
/* easing select trigger (custom dropdown) */
|
|
418
|
+
/* select trigger — Logram design system (node 13064:2418): flat #f7f7f7 fill */
|
|
419
|
+
.tl-select { display: flex; align-items: center; justify-content: space-between; width: 100%; height: 36px;
|
|
420
|
+
border: none; border-radius: 8px; background: #f7f7f7; padding: 6px 10px 6px 12px;
|
|
421
|
+
font: inherit; font-size: 13px; font-weight: 500; line-height: 16px; color: #1b1b1b; cursor: pointer; text-align: left;
|
|
422
|
+
transition: background 0.12s ease; }
|
|
423
|
+
.tl-select:hover { background: rgba(170,170,170,0.2); }
|
|
424
|
+
.tl-select.is-open { background: rgba(170,170,170,0.2); }
|
|
425
|
+
.tl-select:disabled { opacity: 0.5; cursor: default; }
|
|
426
|
+
.tl-select-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
427
|
+
.tl-select-chev { display: flex; color: var(--c-ruler); flex: none; }
|
|
428
|
+
/* easing curve — exact Figma (node 13044:2506): #f6f6f7, ~6px radius, subtle inset border */
|
|
429
|
+
.tl-curve { background: #f6f6f7; border-radius: 6px; box-shadow: inset 0 0 0 1px rgba(0,0,0,0.04);
|
|
430
|
+
padding: 0; position: relative; overflow: hidden; }
|
|
431
|
+
.tl-curve svg { display: block; cursor: crosshair; width: 100%; height: auto; }
|
|
432
|
+
.tl-curve-handle { cursor: grab; }
|
|
433
|
+
.tl-curve-handle:active { cursor: grabbing; }
|
|
434
|
+
.tl-cubic-row { display: flex; gap: 4px; }
|
|
435
|
+
/* number field — Logram design system (node 13064:2431): 32px, #f7f7f7 fill,
|
|
436
|
+
Inter Medium 13/16, gray hover overlay, inset 1px focus ring */
|
|
437
|
+
.tl-cubic-row input { flex: 1; width: 0; height: 32px; border: none; border-radius: 8px; background: #f7f7f7;
|
|
438
|
+
box-sizing: border-box; padding: 8px 0;
|
|
439
|
+
text-align: center; font: inherit; font-size: 13px; font-weight: 500; line-height: 16px; color: #1b1b1b;
|
|
440
|
+
outline: none; box-shadow: inset 0 0 0 0 rgba(0,0,0,0.14); transition: box-shadow 0.12s ease, background 0.12s ease; }
|
|
441
|
+
.tl-cubic-row input::placeholder { color: #979797; }
|
|
442
|
+
.tl-cubic-row input:hover { background: linear-gradient(rgba(170,170,170,0.2),rgba(170,170,170,0.2)), #f7f7f7; }
|
|
443
|
+
.tl-cubic-row input:focus { box-shadow: inset 0 0 0 1px rgba(0,0,0,0.14); }
|
|
444
|
+
.tl-custom-input { height: 30px; border: none; border-radius: 6px; background: var(--c-field-bg); padding: 0 8px;
|
|
445
|
+
font: inherit; font-size: 12px; font-family: monospace; color: var(--c-text); outline: none;
|
|
446
|
+
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.04); }
|
|
447
|
+
.tl-bounce { background: #fff; border-radius: 8px; box-shadow: inset 0 0 0 1px var(--c-hairline);
|
|
448
|
+
padding: 8px 10px; display: flex; flex-direction: column; gap: 6px; }
|
|
449
|
+
.tl-bounce-title { font-size: 11px; color: var(--c-text-mut); font-weight: 600; }
|
|
450
|
+
.tl-bounce-row { display: flex; align-items: center; gap: 8px; }
|
|
451
|
+
.tl-bounce-row label { font-size: 11px; color: #737373; width: 54px; flex: none; }
|
|
452
|
+
.tl-bounce-row input[type="range"] { flex: 1; height: 4px; -webkit-appearance: none; appearance: none;
|
|
453
|
+
background: #e3e3e8; border-radius: 2px; outline: none; }
|
|
454
|
+
.tl-bounce-row input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none;
|
|
455
|
+
width: 12px; height: 12px; border-radius: 50%; background: var(--c-blue); cursor: pointer; }
|
|
456
|
+
.tl-bounce-val { font-size: 11px; color: var(--c-blue); width: 34px; text-align: right; }
|
|
457
|
+
.tl-bounce select { height: 26px; border: none; border-radius: 6px; background: var(--c-field-bg);
|
|
458
|
+
font: inherit; font-size: 11px; color: var(--c-text); outline: none; box-shadow: inset 0 0 0 1px rgba(0,0,0,0.04); flex: 1; }
|
|
459
|
+
|
|
460
|
+
/* ── Easing / Springs tabs — Logram design system (node 13064:2552): trackless toggle pills ── */
|
|
461
|
+
.tl-seg { display: flex; align-items: center; gap: 4px; margin-top: 12px; }
|
|
462
|
+
.tl-seg-btn { height: 32px; padding: 6px 12px; border: none; border-radius: 8px; background: transparent;
|
|
463
|
+
font: inherit; font-size: 13px; font-weight: 500; line-height: 14px; color: #676767; cursor: pointer;
|
|
464
|
+
transition: background 0.14s ease, color 0.14s ease; }
|
|
465
|
+
.tl-seg-btn:hover:not(.is-active) { background: rgba(170,170,170,0.06); color: #17181c; }
|
|
466
|
+
.tl-seg-btn.is-active { background: rgba(170,170,170,0.1); color: #17181c; }
|
|
467
|
+
|
|
468
|
+
/* position preview (animated marker à la easing.dev) */
|
|
469
|
+
.tl-preview { margin-top: 8px; background: #fff; border-radius: 8px; box-shadow: inset 0 0 0 1px var(--c-hairline);
|
|
470
|
+
padding: 8px 12px 14px; }
|
|
471
|
+
.tl-preview-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
|
472
|
+
.tl-preview-title { font-size: 11px; font-weight: 600; color: var(--c-text-mut); }
|
|
473
|
+
.tl-preview-btn { display: inline-flex; align-items: center; gap: 5px; height: 22px; padding: 0 8px 0 7px;
|
|
474
|
+
border: none; border-radius: 6px; background: var(--c-field-bg); color: var(--c-text-mut2);
|
|
475
|
+
font: inherit; font-size: 11px; font-weight: 500; cursor: pointer;
|
|
476
|
+
transition: background 0.12s ease, scale 0.12s ease; }
|
|
477
|
+
.tl-preview-btn:hover { background: var(--c-sec-h); }
|
|
478
|
+
.tl-preview-btn:active { scale: 0.96; }
|
|
479
|
+
.tl-preview-track { position: relative; height: 16px; }
|
|
480
|
+
.tl-preview-rail { position: absolute; left: 0; right: 0; top: 50%; height: 2px; transform: translateY(-50%);
|
|
481
|
+
background: var(--c-track); border-radius: 2px; }
|
|
482
|
+
.tl-preview-end { position: absolute; top: 50%; width: 2px; height: 8px; transform: translateY(-50%);
|
|
483
|
+
background: #d0d0d6; border-radius: 2px; }
|
|
484
|
+
.tl-preview-end.left { left: 0; }
|
|
485
|
+
.tl-preview-end.right { right: 0; }
|
|
486
|
+
.tl-preview-dot { position: absolute; left: 0; top: 50%; width: 14px; height: 14px; margin-top: -7px;
|
|
487
|
+
border-radius: 50%; background: var(--c-blue); box-shadow: 0 1px 3px rgba(0,0,0,0.25);
|
|
488
|
+
will-change: transform; }
|
|
489
|
+
|
|
490
|
+
/* menu section header (non-clickable) */
|
|
491
|
+
.tl-menu-group { padding: 9px 10px 4px; font-size: 10.5px; font-weight: 600; letter-spacing: 0.04em;
|
|
492
|
+
text-transform: uppercase; color: var(--c-text-faint); pointer-events: none; }
|
|
493
|
+
.tl-menu-group:first-child { padding-top: 4px; }
|
|
494
|
+
|
|
495
|
+
/* spring params box (reuses bounce row layout) */
|
|
496
|
+
.tl-spring-dur { display: flex; align-items: baseline; gap: 6px; font-size: 11px; color: var(--c-text-mut);
|
|
497
|
+
padding-top: 2px; border-top: 1px solid var(--c-hairline); margin-top: 2px; }
|
|
498
|
+
.tl-spring-dur b { color: var(--c-text); font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
499
|
+
.tl-spring-dur .tl-spring-dur-hint { color: var(--c-text-faint); }
|
|
500
|
+
|
|
501
|
+
/* read-only (spring-driven) value field */
|
|
502
|
+
.tl-field-wrap.is-locked { position: relative; }
|
|
503
|
+
.tl-field.is-readonly { cursor: default; }
|
|
504
|
+
.tl-field.is-readonly .tl-field-track { cursor: default; }
|
|
505
|
+
.tl-field.is-readonly .tl-field-fill { opacity: 0.7; }
|
|
506
|
+
.tl-field.is-readonly .tl-field-value { color: var(--c-text-mut); }
|
|
507
|
+
.tl-field-lock { position: absolute; right: 11px; top: 50%; transform: translateY(-50%);
|
|
508
|
+
display: flex; color: var(--c-text-faint); pointer-events: none; z-index: 4; }
|
|
509
|
+
.tl-field-wrap.is-locked .t-tt { left: 0; transform: translate(0, 4px) scale(var(--tt-scale));
|
|
510
|
+
transform-origin: 0 0; white-space: normal; width: 244px; text-align: left; font-size: 12px;
|
|
511
|
+
line-height: 1.45; padding: 9px 11px; }
|
|
512
|
+
.tl-field-wrap.is-locked:hover .t-tt { opacity: 1; transform: translate(0, 0) scale(1);
|
|
513
|
+
transition-duration: var(--tt-in-dur); transition-timing-function: var(--tt-in-ease);
|
|
514
|
+
transition-delay: var(--tt-delay); pointer-events: none; }
|
|
515
|
+
|
|
516
|
+
.tl-empty { flex: 1; padding: 40px 16px; color: var(--c-disabled); font-size: 13px; text-align: center;
|
|
517
|
+
display: flex; align-items: center; justify-content: center;
|
|
518
|
+
background: #fff; border-radius: 12px; box-shadow: var(--card-shadow); }
|
|
519
|
+
|
|
520
|
+
/* ── shared dropdown surface ── */
|
|
521
|
+
.tl-menu { background: #fff; border-radius: 12px; padding: 6px; box-shadow: var(--menu-shadow); }
|
|
522
|
+
.tl-menu-item { display: flex; align-items: center; gap: 10px; min-height: 32px; padding: 6px 10px;
|
|
523
|
+
border-radius: 7px; font-size: 13px; color: var(--c-text-strong); cursor: pointer;
|
|
524
|
+
transition: background 0.1s ease, box-shadow 0.1s ease; }
|
|
525
|
+
.tl-menu-item:hover { background: #f4f4f5; }
|
|
526
|
+
.tl-menu-item:active { background: #ededee; }
|
|
527
|
+
.tl-menu-item:focus-visible { outline: none; background: #fff; box-shadow: inset 0 0 0 1px rgba(0,115,229,0.4); }
|
|
528
|
+
.tl-menu-item.disabled { color: var(--c-disabled); pointer-events: none; }
|
|
529
|
+
.tl-menu-item-label { flex: 1; min-width: 0; display: flex; align-items: center; }
|
|
530
|
+
.tl-menu-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
531
|
+
.tl-menu-dim { color: var(--c-text-faint); }
|
|
532
|
+
.tl-menu-help { color: #c4c4cc; margin-left: 10px; flex: none; cursor: help; }
|
|
533
|
+
.tl-menu-help:hover { color: var(--c-ruler); }
|
|
534
|
+
.tl-menu-help svg { display: block; }
|
|
535
|
+
.tl-menu-check { display: flex; color: var(--c-text-strong); flex: none; }
|
|
536
|
+
.tl-menu-empty { padding: 10px; color: var(--c-disabled); font-size: 13px; }
|
|
537
|
+
|
|
538
|
+
/* ═════ transitions.dev — menu dropdown (verbatim) ═════ */
|
|
539
|
+
.t-dropdown {
|
|
540
|
+
transform-origin: top left;
|
|
541
|
+
transform: scale(var(--dropdown-pre-scale));
|
|
542
|
+
opacity: 0;
|
|
543
|
+
pointer-events: none;
|
|
544
|
+
transition:
|
|
545
|
+
transform var(--dropdown-open-dur) var(--dropdown-ease),
|
|
546
|
+
opacity var(--dropdown-open-dur) var(--dropdown-ease);
|
|
547
|
+
will-change: transform, opacity;
|
|
548
|
+
}
|
|
549
|
+
.t-dropdown[data-origin="top-right"] { transform-origin: top right; }
|
|
550
|
+
.t-dropdown[data-origin="top-center"] { transform-origin: top center; }
|
|
551
|
+
.t-dropdown[data-origin="bottom-left"] { transform-origin: bottom left; }
|
|
552
|
+
.t-dropdown[data-origin="bottom-center"] { transform-origin: bottom center; }
|
|
553
|
+
.t-dropdown[data-origin="bottom-right"] { transform-origin: bottom right; }
|
|
554
|
+
.t-dropdown.is-open {
|
|
555
|
+
transform: scale(1);
|
|
556
|
+
opacity: 1;
|
|
557
|
+
pointer-events: auto;
|
|
558
|
+
}
|
|
559
|
+
.t-dropdown.is-closing {
|
|
560
|
+
transform: scale(var(--dropdown-closing-scale));
|
|
561
|
+
opacity: 0;
|
|
562
|
+
pointer-events: none;
|
|
563
|
+
transition:
|
|
564
|
+
transform var(--dropdown-close-dur) var(--dropdown-ease),
|
|
565
|
+
opacity var(--dropdown-close-dur) var(--dropdown-ease);
|
|
566
|
+
}
|
|
567
|
+
@media (prefers-reduced-motion: reduce) {
|
|
568
|
+
.t-dropdown { transition: none !important; }
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/* ═════ transitions.dev — icon swap (verbatim) ═════ */
|
|
572
|
+
.t-icon-swap {
|
|
573
|
+
position: relative;
|
|
574
|
+
display: inline-grid;
|
|
575
|
+
}
|
|
576
|
+
.t-icon-swap .t-icon {
|
|
577
|
+
grid-area: 1 / 1;
|
|
578
|
+
transition:
|
|
579
|
+
opacity var(--icon-swap-dur) var(--icon-swap-ease),
|
|
580
|
+
filter var(--icon-swap-dur) var(--icon-swap-ease),
|
|
581
|
+
transform var(--icon-swap-dur) var(--icon-swap-ease);
|
|
582
|
+
will-change: opacity, filter, transform;
|
|
583
|
+
}
|
|
584
|
+
.t-icon-swap[data-state="a"] .t-icon[data-icon="a"],
|
|
585
|
+
.t-icon-swap[data-state="b"] .t-icon[data-icon="b"] {
|
|
586
|
+
opacity: 1;
|
|
587
|
+
filter: blur(0);
|
|
588
|
+
transform: scale(1);
|
|
589
|
+
}
|
|
590
|
+
.t-icon-swap[data-state="a"] .t-icon[data-icon="b"],
|
|
591
|
+
.t-icon-swap[data-state="b"] .t-icon[data-icon="a"] {
|
|
592
|
+
opacity: 0;
|
|
593
|
+
filter: blur(var(--icon-swap-blur));
|
|
594
|
+
transform: scale(var(--icon-swap-start-scale));
|
|
595
|
+
}
|
|
596
|
+
@media (prefers-reduced-motion: reduce) {
|
|
597
|
+
.t-icon-swap .t-icon { transition: none !important; }
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/* ═════ transitions.dev — tooltip (verbatim) ═════ */
|
|
601
|
+
.t-tt-wrap {
|
|
602
|
+
position: relative;
|
|
603
|
+
display: inline-block;
|
|
604
|
+
}
|
|
605
|
+
.t-tt {
|
|
606
|
+
position: absolute;
|
|
607
|
+
bottom: calc(100% + 8px);
|
|
608
|
+
left: 50%;
|
|
609
|
+
transform: translate(-50%, 0) scale(var(--tt-scale));
|
|
610
|
+
transform-origin: 50% 100%;
|
|
611
|
+
padding: 8px 12px;
|
|
612
|
+
border-radius: 8px;
|
|
613
|
+
background: var(--tt-bg);
|
|
614
|
+
color: var(--tt-fg);
|
|
615
|
+
white-space: nowrap;
|
|
616
|
+
box-shadow:
|
|
617
|
+
0 0 0 1px rgba(0, 0, 0, 0.06),
|
|
618
|
+
0 2px 6px 0 rgba(0, 0, 0, 0.05),
|
|
619
|
+
0 4px 42px 0 rgba(0, 0, 0, 0.06);
|
|
620
|
+
opacity: 0;
|
|
621
|
+
pointer-events: none;
|
|
622
|
+
transition:
|
|
623
|
+
opacity var(--tt-out-dur) var(--tt-out-ease),
|
|
624
|
+
transform var(--tt-out-dur) var(--tt-out-ease);
|
|
625
|
+
}
|
|
626
|
+
.t-tt-wrap:hover .t-tt,
|
|
627
|
+
.t-tt-trigger:focus-visible + .t-tt {
|
|
628
|
+
opacity: 1;
|
|
629
|
+
transform: translate(-50%, 0) scale(1);
|
|
630
|
+
transition-duration: var(--tt-in-dur);
|
|
631
|
+
transition-timing-function: var(--tt-in-ease);
|
|
632
|
+
transition-delay: var(--tt-delay);
|
|
633
|
+
}
|
|
634
|
+
@media (prefers-reduced-motion: reduce) {
|
|
635
|
+
.t-tt { transition: none !important; }
|
|
636
|
+
}
|
|
637
|
+
/* below-variant for triggers near the panel top edge */
|
|
638
|
+
.t-tt.tl-tt-below { bottom: auto; top: calc(100% + 8px); transform-origin: 50% 0; font-size: 12px; }
|
|
639
|
+
/* usage-variant: right-anchored, multi-line copy for token help icons */
|
|
640
|
+
.t-tt.tl-tt-usage { left: auto; right: -6px; width: 224px; white-space: normal; text-align: left;
|
|
641
|
+
font-size: 12px; line-height: 1.45; padding: 9px 11px; transform-origin: 100% 100%;
|
|
642
|
+
text-wrap: balance; transform: translate(0, 4px) scale(var(--tt-scale)); }
|
|
643
|
+
.t-tt-wrap:hover .t-tt.tl-tt-usage { transform: translate(0, 0) scale(1); }
|
|
644
|
+
|
|
645
|
+
/* ═════ transitions.dev — panel reveal (per-phase distance/ease/scale) ═════ */
|
|
646
|
+
/* base = closed / close-end state: resolved travel + scale come from the
|
|
647
|
+
phase rules below; default to the close tokens, transition on close. */
|
|
648
|
+
.t-panel-slide {
|
|
649
|
+
--panel-translate-y: var(--panel-close-distance);
|
|
650
|
+
--panel-scale-now: var(--panel-scale-close);
|
|
651
|
+
transform: translateY(var(--panel-translate-y)) scale(var(--panel-scale-now));
|
|
652
|
+
opacity: 0;
|
|
653
|
+
filter: blur(var(--panel-blur));
|
|
654
|
+
pointer-events: none;
|
|
655
|
+
transition:
|
|
656
|
+
transform var(--panel-close-dur) var(--panel-close-ease),
|
|
657
|
+
opacity var(--panel-close-dur) var(--panel-close-ease),
|
|
658
|
+
filter var(--panel-close-dur) var(--panel-close-ease);
|
|
659
|
+
will-change: transform, opacity, filter;
|
|
660
|
+
}
|
|
661
|
+
/* opening starts from the OPEN distance/scale, closing ends at the CLOSE
|
|
662
|
+
distance/scale — both resolved on the shared closed state. */
|
|
663
|
+
.t-panel-slide[data-phase="opening"] {
|
|
664
|
+
--panel-translate-y: var(--panel-open-distance);
|
|
665
|
+
--panel-scale-now: var(--panel-scale);
|
|
666
|
+
}
|
|
667
|
+
.t-panel-slide[data-phase="closing"] {
|
|
668
|
+
--panel-translate-y: var(--panel-close-distance);
|
|
669
|
+
--panel-scale-now: var(--panel-scale-close);
|
|
670
|
+
}
|
|
671
|
+
.t-panel-slide[data-open="true"] {
|
|
672
|
+
transform: translateY(0) scale(1);
|
|
673
|
+
opacity: 1;
|
|
674
|
+
filter: blur(0);
|
|
675
|
+
pointer-events: auto;
|
|
676
|
+
transition:
|
|
677
|
+
transform var(--panel-open-dur) var(--panel-open-ease),
|
|
678
|
+
opacity var(--panel-open-dur) var(--panel-open-ease),
|
|
679
|
+
filter var(--panel-open-dur) var(--panel-open-ease);
|
|
680
|
+
}
|
|
681
|
+
@media (prefers-reduced-motion: reduce) {
|
|
682
|
+
.t-panel-slide { transition: none !important; }
|
|
683
|
+
}
|
|
684
|
+
/* whole-component reveal (pill → panel): slide up from the bottom dock.
|
|
685
|
+
keep the transparent padding area click-through even when open. */
|
|
686
|
+
[data-timeline-panel].t-panel-slide { pointer-events: none; }
|
|
687
|
+
[data-timeline-panel].t-panel-slide[data-open="true"] { pointer-events: none; }
|
|
688
|
+
|
|
689
|
+
/* @inject-skip-start (demo-only transition tweak controls — excluded from the injected build) */
|
|
690
|
+
/* ═════ live panel-transition tweak controls (demo harness, not part of the panel) ═════ */
|
|
691
|
+
.panel-controls {
|
|
692
|
+
position: fixed; top: 16px; right: 16px; z-index: 60;
|
|
693
|
+
width: 286px; background: #fff; border-radius: 12px;
|
|
694
|
+
box-shadow: var(--card-shadow); padding: 13px 14px 14px; font-size: 12px;
|
|
695
|
+
color: var(--c-text); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
696
|
+
}
|
|
697
|
+
.panel-controls h3 { font-size: 12px; font-weight: 600; letter-spacing: -0.2px; color: #171717;
|
|
698
|
+
margin-bottom: 11px; display: flex; align-items: center; gap: 6px; }
|
|
699
|
+
.panel-controls h3 .pc-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--c-blue); }
|
|
700
|
+
.pc-group { font-size: 10px; font-weight: 600; letter-spacing: 0.4px; text-transform: uppercase;
|
|
701
|
+
color: var(--c-text-mut); margin: 12px 0 7px; padding-top: 9px; border-top: 1px solid var(--c-line); }
|
|
702
|
+
.pc-group:first-of-type { margin-top: 4px; }
|
|
703
|
+
.pc-row { display: flex; align-items: center; gap: 9px; margin-bottom: 9px; }
|
|
704
|
+
.pc-label { flex: 0 0 76px; color: var(--c-text-mut); }
|
|
705
|
+
.pc-row input[type=range] { flex: 1; min-width: 0; accent-color: var(--c-blue); }
|
|
706
|
+
.pc-val { flex: 0 0 54px; text-align: right; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
707
|
+
font-size: 11px; color: #171717; }
|
|
708
|
+
.pc-select-row { display: flex; align-items: center; gap: 9px; margin-bottom: 9px; }
|
|
709
|
+
.pc-select-row select { flex: 1; min-width: 0; font: inherit; font-size: 12px; padding: 4px 6px;
|
|
710
|
+
border-radius: 6px; border: 1px solid var(--c-line); background: #fff; color: #171717; }
|
|
711
|
+
.pc-bz { display: flex; gap: 6px; margin: 0 0 11px; }
|
|
712
|
+
.pc-bz input { width: 100%; min-width: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
713
|
+
font-size: 11px; padding: 4px 4px; border-radius: 6px; border: 1px solid var(--c-line);
|
|
714
|
+
text-align: center; color: #171717; }
|
|
715
|
+
.pc-btns { display: flex; gap: 8px; margin-top: 3px; }
|
|
716
|
+
/* @inject-skip-end */
|
|
717
|
+
/* .pc-btn* are shared with the refine panel's "Apply all" footer — keep in the build */
|
|
718
|
+
.pc-btn { flex: 1; font: inherit; font-size: 12px; font-weight: 500; padding: 7px 8px;
|
|
719
|
+
border-radius: 7px; border: 1px solid var(--c-line); background: #fff; color: #171717; cursor: pointer; }
|
|
720
|
+
.pc-btn:hover { background: var(--c-ghost-h); }
|
|
721
|
+
.pc-btn.primary { background: #171717; color: #fff; border-color: #171717; }
|
|
722
|
+
.pc-btn.primary:hover { background: #000; }
|
|
723
|
+
|
|
724
|
+
.tl-refine-btn.is-active { color: var(--c-blue-pressed); }
|
|
725
|
+
.tl-refine-btn.is-active::before { background: color(display-p3 0 0.451 0.898 / 0.12); }
|
|
726
|
+
|
|
727
|
+
/* refine panel slides in from the right using transitions.dev panel reveal
|
|
728
|
+
tokens (translate + opacity + cross-blur, per-phase open/close dur/ease). */
|
|
729
|
+
.tl-refine-panel {
|
|
730
|
+
position: absolute; top: 0; right: 0; bottom: 0; z-index: 6;
|
|
731
|
+
width: var(--refine-w, 360px); max-width: 100%; overflow: hidden;
|
|
732
|
+
--panel-translate-x: var(--panel-close-distance);
|
|
733
|
+
--panel-scale-now: var(--panel-scale-close);
|
|
734
|
+
transform: translateX(calc(100% + var(--panel-translate-x))) scale(var(--panel-scale-now));
|
|
735
|
+
opacity: 0;
|
|
736
|
+
filter: blur(var(--panel-blur));
|
|
737
|
+
pointer-events: none;
|
|
738
|
+
transition:
|
|
739
|
+
transform var(--refine-close-dur) var(--panel-close-ease),
|
|
740
|
+
opacity var(--refine-close-dur) var(--panel-close-ease),
|
|
741
|
+
filter var(--refine-close-dur) var(--panel-close-ease);
|
|
742
|
+
will-change: transform, opacity, filter;
|
|
743
|
+
}
|
|
744
|
+
.tl-refine-panel[data-phase="opening"] {
|
|
745
|
+
--panel-translate-x: var(--panel-open-distance);
|
|
746
|
+
--panel-scale-now: var(--panel-scale);
|
|
747
|
+
}
|
|
748
|
+
.tl-refine-panel[data-phase="closing"] {
|
|
749
|
+
--panel-translate-x: var(--panel-close-distance);
|
|
750
|
+
--panel-scale-now: var(--panel-scale-close);
|
|
751
|
+
}
|
|
752
|
+
.tl-refine-panel[data-open="true"] {
|
|
753
|
+
transform: translateX(0) scale(1);
|
|
754
|
+
opacity: 1;
|
|
755
|
+
filter: blur(0);
|
|
756
|
+
pointer-events: auto;
|
|
757
|
+
transition:
|
|
758
|
+
transform var(--panel-open-dur) var(--panel-open-ease),
|
|
759
|
+
opacity var(--panel-open-dur) var(--panel-open-ease),
|
|
760
|
+
filter var(--panel-open-dur) var(--panel-open-ease);
|
|
761
|
+
}
|
|
762
|
+
.tl-refine-inner {
|
|
763
|
+
width: var(--refine-w, 360px); height: 100%; box-sizing: border-box;
|
|
764
|
+
display: flex; flex-direction: column;
|
|
765
|
+
background: #fff; color: var(--c-text);
|
|
766
|
+
border-left: 1px solid #f0f0f0;
|
|
767
|
+
box-shadow: -10px 0 30px rgba(0,0,0,0.05);
|
|
768
|
+
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
|
769
|
+
}
|
|
770
|
+
/* header: title block (left) + Agent/Deterministic mode dropdown (right) */
|
|
771
|
+
/* match the timeline top bar exactly: 10px/14px padding, centered, 57px tall (36px row + 20 + 1px border) */
|
|
772
|
+
.tl-refine-head { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between;
|
|
773
|
+
gap: 8px; box-sizing: border-box; padding: 10px 14px;
|
|
774
|
+
border-bottom: 1px solid #f0f0f0; }
|
|
775
|
+
.tl-refine-titles { min-width: 0; }
|
|
776
|
+
.tl-refine-titles h3 { font-size: 13px; font-weight: 500; line-height: 14px; color: #17181c; }
|
|
777
|
+
.tl-refine-titles p { margin-top: 2px; font-size: 12px; font-weight: 400; line-height: 14px; color: #676767;
|
|
778
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 190px; }
|
|
779
|
+
.tl-refine-actions { flex: none; display: flex; align-items: center; gap: 2px; }
|
|
780
|
+
.tl-refine-mode { display: flex; align-items: center; gap: 6px; height: 36px;
|
|
781
|
+
padding: 0 10px 0 12px; border: none; background: transparent; border-radius: 60px; cursor: pointer;
|
|
782
|
+
font: inherit; font-size: 13px; font-weight: 500; line-height: 16px; color: #1b1b1b;
|
|
783
|
+
transition: background 0.14s ease; }
|
|
784
|
+
.tl-refine-mode:hover, .tl-refine-mode.is-open { background: rgba(170,170,170,0.10); }
|
|
785
|
+
.tl-refine-mode svg { color: var(--c-ruler); }
|
|
786
|
+
/* close button — Logram design system (node 13065:2266): 36px ghost icon button */
|
|
787
|
+
.tl-refine-close { flex: none; display: inline-flex; align-items: center; justify-content: center;
|
|
788
|
+
width: 36px; height: 36px; border: none; background: transparent; border-radius: 60px; cursor: pointer;
|
|
789
|
+
color: #676767; transition: background 0.14s ease, color 0.14s ease; }
|
|
790
|
+
.tl-refine-close:hover { background: rgba(170,170,170,0.10); color: #17181c; }
|
|
791
|
+
/* refine-type tabs (Small refinements / Replace transition) */
|
|
792
|
+
.tl-refine-tabs { flex: 0 0 auto; display: flex; align-items: center; gap: 5px; padding: 17px 16px 0; }
|
|
793
|
+
.tl-refine-tab { height: 32px; padding: 6px 12px; border: none; background: transparent; cursor: pointer;
|
|
794
|
+
border-radius: 8px; font: inherit; font-size: 13px; font-weight: 500; line-height: 14px; color: #676767;
|
|
795
|
+
transition: background 0.14s ease, color 0.14s ease; }
|
|
796
|
+
.tl-refine-tab:hover:not([aria-selected="true"]) { color: #17181c; }
|
|
797
|
+
.tl-refine-tab[aria-selected="true"] { background: rgba(170,170,170,0.10); color: #17181c; }
|
|
798
|
+
.tl-refine-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column;
|
|
799
|
+
padding: 0 16px; }
|
|
800
|
+
/* idle + unavailable share a vertically-centred column */
|
|
801
|
+
.tl-refine-center { flex: 1; display: flex; flex-direction: column; align-items: center;
|
|
802
|
+
justify-content: center; gap: 12px; text-align: center; padding: 16px 0; }
|
|
803
|
+
.tl-refine-idle-text { max-width: 282px; font-size: 13px; font-weight: 400; line-height: 21px; color: #676767; text-wrap: balance; }
|
|
804
|
+
.tl-refine-unavail-title { font-size: 13px; font-weight: 400; line-height: 14px; color: #17181c; }
|
|
805
|
+
.tl-refine-unavail-text { max-width: 232px; font-size: 13px; font-weight: 400; line-height: 21px; color: #676767; text-wrap: balance; }
|
|
806
|
+
code.tl-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
|
|
807
|
+
color: #17181c; background: #f0f0f0; border-radius: 5px; padding: 1px 5px; }
|
|
808
|
+
/* results state (suggestions list) */
|
|
809
|
+
.tl-refine-results { display: flex; flex-direction: column; gap: 10px; padding: 14px 0 20px; }
|
|
810
|
+
/* results entrance — transitions.dev texts reveal (18): staggered blurred rise */
|
|
811
|
+
.tl-refine-results.t-stagger {
|
|
812
|
+
--stagger-dur: 500ms; --stagger-distance: 12px; --stagger-stagger: 40ms;
|
|
813
|
+
--stagger-blur: 3px; --stagger-ease: cubic-bezier(0.22, 1, 0.36, 1); }
|
|
814
|
+
.tl-refine-results .t-stagger-line {
|
|
815
|
+
opacity: 0; transform: translateY(var(--stagger-distance)); filter: blur(var(--stagger-blur));
|
|
816
|
+
transition: opacity var(--stagger-dur) var(--stagger-ease),
|
|
817
|
+
transform var(--stagger-dur) var(--stagger-ease),
|
|
818
|
+
filter var(--stagger-dur) var(--stagger-ease);
|
|
819
|
+
will-change: transform, opacity, filter; }
|
|
820
|
+
.tl-refine-results.is-shown .t-stagger-line {
|
|
821
|
+
opacity: 1; transform: translateY(0); filter: blur(0); }
|
|
822
|
+
/* foot holds either the Start-scanning button or the in-progress status bar */
|
|
823
|
+
.tl-refine-foot { flex: 0 0 auto; padding: 12px 20px 26px; display: flex; justify-content: center; }
|
|
824
|
+
/* card resize (01-card-resize.md): tween width/height on a layout change */
|
|
825
|
+
.t-resize { transition: width var(--resize-dur) var(--resize-ease),
|
|
826
|
+
height var(--resize-dur) var(--resize-ease); will-change: width, height;
|
|
827
|
+
--resize-dur: 300ms; --resize-ease: cubic-bezier(0.22, 1, 0.36, 1); }
|
|
828
|
+
/* The Start-scanning control IS the loading rectangle — it morphs between the
|
|
829
|
+
two states with card resize (width/height) plus matching radius/bg/shadow,
|
|
830
|
+
so clicking it grows the pill into the status box instead of swapping. */
|
|
831
|
+
.tl-scan-morph { position: relative; isolation: isolate; box-sizing: border-box;
|
|
832
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
833
|
+
margin-bottom: 20px;
|
|
834
|
+
width: var(--scan-idle-w, 122px); height: 35px; padding: 0 16px;
|
|
835
|
+
border: none; border-radius: 60px; cursor: pointer;
|
|
836
|
+
font: inherit; font-size: 13px; font-weight: 500; line-height: 14px; color: #0073e5;
|
|
837
|
+
background: rgba(0,115,229,0.04); text-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
|
838
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.04),
|
|
839
|
+
inset 0 0 0 1px rgba(0,101,208,0.10),
|
|
840
|
+
inset 0 -1px 0 0 rgba(0,0,0,0.06),
|
|
841
|
+
inset 0 0 0 1px rgba(196,196,196,0.10);
|
|
842
|
+
--resize-dur: 300ms; --resize-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
843
|
+
transition: width var(--resize-dur) var(--resize-ease),
|
|
844
|
+
height var(--resize-dur) var(--resize-ease),
|
|
845
|
+
border-radius var(--resize-dur) var(--resize-ease),
|
|
846
|
+
background var(--resize-dur) var(--resize-ease),
|
|
847
|
+
color var(--resize-dur) var(--resize-ease),
|
|
848
|
+
box-shadow var(--resize-dur) var(--resize-ease); }
|
|
849
|
+
/* explicit refine-footer spacing for both idle + scanning states */
|
|
850
|
+
.tl-refine-foot .tl-scan-morph { margin-bottom: 20px; }
|
|
851
|
+
.tl-scan-morph:hover:not(:disabled) { background: rgba(0,115,229,0.08); }
|
|
852
|
+
.tl-scan-morph:active:not(.is-scanning):not(:disabled) { scale: 0.98; }
|
|
853
|
+
.tl-scan-morph:disabled:not(.is-scanning) { cursor: default; opacity: 0.45; }
|
|
854
|
+
.tl-scan-morph.is-scanning { width: 100%; height: 49px; border-radius: 60px;
|
|
855
|
+
padding: 8px 16px; cursor: default; color: #676767; background: #fff;
|
|
856
|
+
text-shadow: none; box-shadow: inset 0 0 0 1px #f0f0f0; }
|
|
857
|
+
/* Two faces share one grid cell and cross-blur (icon swap, 09) between the
|
|
858
|
+
label and the icon+status; whichever is hidden fades out with blur+scale.
|
|
859
|
+
overflow:hidden + nowrap keep the status copy on a single line while the
|
|
860
|
+
box is still narrow mid-morph. */
|
|
861
|
+
.tl-scan-content { position: relative; z-index: 1; display: grid; width: 100%; height: 100%;
|
|
862
|
+
min-width: 0; overflow: hidden; }
|
|
863
|
+
.tl-scan-face { grid-area: 1 / 1; display: flex; align-items: center; gap: 8px;
|
|
864
|
+
min-width: 0; white-space: nowrap;
|
|
865
|
+
--icon-swap-dur: 250ms; --icon-swap-blur: 2px; --icon-swap-ease: ease-in-out;
|
|
866
|
+
transition: opacity var(--icon-swap-dur) var(--icon-swap-ease),
|
|
867
|
+
filter var(--icon-swap-dur) var(--icon-swap-ease),
|
|
868
|
+
transform var(--icon-swap-dur) var(--icon-swap-ease);
|
|
869
|
+
will-change: opacity, filter, transform; }
|
|
870
|
+
.tl-scan-face-label { justify-content: center; }
|
|
871
|
+
.tl-scan-face-status { justify-content: flex-start; }
|
|
872
|
+
/* idle: label shown, status blurred out */
|
|
873
|
+
.tl-scan-face-label { opacity: 1; filter: blur(0); transform: scale(1); }
|
|
874
|
+
.tl-scan-face-status { opacity: 0; filter: blur(var(--icon-swap-blur)); transform: scale(0.98); }
|
|
875
|
+
/* scanning: cross-blur to the status face */
|
|
876
|
+
.tl-scan-morph.is-scanning .tl-scan-face-label { opacity: 0; filter: blur(var(--icon-swap-blur)); transform: scale(0.98); }
|
|
877
|
+
.tl-scan-morph.is-scanning .tl-scan-face-status { opacity: 1; filter: blur(0); transform: scale(1); }
|
|
878
|
+
.tl-scan-label { white-space: nowrap; }
|
|
879
|
+
/* The BorderBeam library injects its own stylesheet that forces
|
|
880
|
+
position:relative on its [data-beam] container (and it lands later in the
|
|
881
|
+
DOM, so it would win at equal specificity). Raise specificity here so the
|
|
882
|
+
beam stays an absolute overlay filling the button instead of collapsing
|
|
883
|
+
into flow as a flex sibling (which hid the beam and skewed the width). */
|
|
884
|
+
.tl-scan-morph .tl-scan-beam { position: absolute; inset: 0; z-index: 0;
|
|
885
|
+
border-radius: 60px; pointer-events: none;
|
|
886
|
+
animation: tl-fade-in 360ms var(--resize-ease) both; }
|
|
887
|
+
.tl-scan-beam-fill { display: block; width: 100%; height: 100%; border-radius: 10px; }
|
|
888
|
+
@keyframes tl-fade-in { from { opacity: 0; } to { opacity: 1; } }
|
|
889
|
+
.tl-scan-ic { flex: 0 0 auto; width: 13px; height: 15px; color: #c4c4c4;
|
|
890
|
+
animation: tl-scan-spin 1.8s linear infinite; }
|
|
891
|
+
@keyframes tl-scan-spin { to { transform: rotate(360deg); } }
|
|
892
|
+
/* loading status text — transitions.dev shimmer text (15) + text states swap (04) */
|
|
893
|
+
.tl-refine-status-text {
|
|
894
|
+
--shimmer-dur: 2000ms; --shimmer-base: #9a9a9a; --shimmer-highlight: #17181c;
|
|
895
|
+
--shimmer-band: 400%; --shimmer-ease: linear;
|
|
896
|
+
--text-swap-dur: 300ms; --text-swap-translate-y: 4px; --text-swap-blur: 2px; --text-swap-ease: ease-in-out; }
|
|
897
|
+
.t-shimmer { position: relative; display: inline-block; color: var(--shimmer-base); }
|
|
898
|
+
.t-shimmer::before {
|
|
899
|
+
content: attr(data-text); position: absolute; inset: 0; pointer-events: none;
|
|
900
|
+
background-image: linear-gradient(90deg, transparent 0%, transparent 40%,
|
|
901
|
+
var(--shimmer-highlight) 50%, transparent 60%, transparent 100%);
|
|
902
|
+
background-size: var(--shimmer-band) 100%; background-repeat: no-repeat;
|
|
903
|
+
-webkit-background-clip: text; background-clip: text;
|
|
904
|
+
color: transparent; -webkit-text-fill-color: transparent;
|
|
905
|
+
animation: t-shimmer var(--shimmer-dur) var(--shimmer-ease) infinite; }
|
|
906
|
+
@keyframes t-shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0% 0; } }
|
|
907
|
+
.t-text-swap { display: inline-block; transform: translateY(0); filter: blur(0); opacity: 1;
|
|
908
|
+
transition: transform var(--text-swap-dur) var(--text-swap-ease),
|
|
909
|
+
filter var(--text-swap-dur) var(--text-swap-ease),
|
|
910
|
+
opacity var(--text-swap-dur) var(--text-swap-ease);
|
|
911
|
+
will-change: transform, filter, opacity; }
|
|
912
|
+
.t-text-swap.is-exit { transform: translateY(calc(var(--text-swap-translate-y) * -1));
|
|
913
|
+
filter: blur(var(--text-swap-blur)); opacity: 0; }
|
|
914
|
+
.t-text-swap.is-enter-start { transform: translateY(var(--text-swap-translate-y));
|
|
915
|
+
filter: blur(var(--text-swap-blur)); opacity: 0; transition: none; }
|
|
916
|
+
/* mode dropdown rows (Agent / Deterministic) */
|
|
917
|
+
.tl-mode-row { display: flex; align-items: center; gap: 12px; width: 100%; padding: 8px 12px;
|
|
918
|
+
border: none; background: transparent; border-radius: 8px; cursor: pointer; text-align: left;
|
|
919
|
+
font: inherit; transition: background 0.12s ease; }
|
|
920
|
+
.tl-mode-row:hover { background: rgba(170,170,170,0.08); }
|
|
921
|
+
.tl-mode-row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
922
|
+
.tl-mode-row-title { font-size: 14px; font-weight: 500; line-height: 16px; color: #1b1b1b; }
|
|
923
|
+
.tl-mode-row-desc { font-size: 12px; font-weight: 400; line-height: 18px; color: #676767; text-wrap: balance; }
|
|
924
|
+
.tl-mode-row-check { flex: 0 0 auto; width: 16px; height: 16px; display: flex; color: #17181c; }
|
|
925
|
+
@keyframes tl-fade-up { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
|
926
|
+
|
|
927
|
+
/* suggestion cards */
|
|
928
|
+
.tl-refine-summary { font-size: 12px; color: var(--c-text-mut); margin-bottom: 2px; }
|
|
929
|
+
.tl-sug { border: 1px solid var(--c-line); border-radius: 10px; padding: 12px 13px;
|
|
930
|
+
display: flex; flex-direction: column; gap: 8px; background: #fff;
|
|
931
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.03); }
|
|
932
|
+
.tl-sug-top { display: flex; align-items: center; gap: 8px; }
|
|
933
|
+
.tl-sug-kind { font-size: 10px; font-weight: 600; letter-spacing: 0.4px; text-transform: uppercase;
|
|
934
|
+
color: var(--c-blue); background: color(display-p3 0 0.451 0.898 / 0.08); padding: 2px 7px; border-radius: 5px; }
|
|
935
|
+
.tl-sug-prop { font-size: 12px; font-weight: 600; color: #171717; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
936
|
+
.tl-sug-title { font-size: 13px; font-weight: 600; color: #171717; }
|
|
937
|
+
.tl-sug-delta { font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
938
|
+
display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
|
|
939
|
+
.tl-sug-from { color: var(--c-text-faint); text-decoration: line-through; }
|
|
940
|
+
.tl-sug-arrow { color: var(--c-text-faint); }
|
|
941
|
+
.tl-sug-to { color: var(--c-blue); font-weight: 600; }
|
|
942
|
+
.tl-sug-reason { font-size: 12px; color: var(--c-text-mut); line-height: 1.45; text-wrap: balance; }
|
|
943
|
+
.tl-sug-apply { align-self: flex-start; font: inherit; font-size: 12px; font-weight: 500;
|
|
944
|
+
padding: 6px 12px; border-radius: 7px; border: none; cursor: pointer;
|
|
945
|
+
background: #171717; color: #fff; transition: background 0.12s ease, scale 0.12s ease; }
|
|
946
|
+
.tl-sug-apply:hover { background: #000; }
|
|
947
|
+
.tl-sug-apply:active { scale: 0.97; }
|
|
948
|
+
.tl-sug-apply.is-applied { background: color(display-p3 0 0.451 0.898 / 0.10); color: var(--c-blue);
|
|
949
|
+
cursor: default; display: flex; align-items: center; gap: 6px; }
|
|
950
|
+
.tl-refine-empty { font-size: 13px; color: var(--c-text-mut); padding: 18px 4px; text-align: center; line-height: 1.5; text-wrap: balance; }
|
|
951
|
+
.tl-refine-error { font-size: 13px; color: #c0392b; padding: 14px; line-height: 1.5;
|
|
952
|
+
background: rgba(192,57,43,0.06); border-radius: 10px; }
|
|
953
|
+
.tl-refine-error code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
|
|
954
|
+
.tl-refine-foot { padding: 12px 20px; display: flex; gap: 8px; }
|
|
955
|
+
@media (prefers-reduced-motion: reduce) {
|
|
956
|
+
.tl-refine-panel { transition: none; }
|
|
957
|
+
.tl-scan-ic { animation: none; }
|
|
958
|
+
.tl-refine-status-text, .tl-sug { animation: none; }
|
|
959
|
+
.t-shimmer::before { animation: none !important; }
|
|
960
|
+
.t-text-swap { transition: none !important; }
|
|
961
|
+
.tl-refine-results .t-stagger-line { transition: none !important; }
|
|
962
|
+
.t-resize, .tl-scan-morph { transition: none !important; }
|
|
963
|
+
.tl-scan-face { transition: none !important; }
|
|
964
|
+
.tl-scan-beam { animation: none !important; }
|
|
965
|
+
.tl-pill { animation: none !important; }
|
|
966
|
+
}
|
|
967
|
+
</style>
|
|
968
|
+
</head>
|
|
969
|
+
<body>
|
|
970
|
+
<div id="root"></div>
|
|
971
|
+
<script type="importmap">
|
|
972
|
+
{ "imports": {
|
|
973
|
+
"react": "https://esm.sh/react@19?dev",
|
|
974
|
+
"react-dom": "https://esm.sh/react-dom@19?dev",
|
|
975
|
+
"react-dom/client": "https://esm.sh/react-dom@19/client?dev",
|
|
976
|
+
"react/jsx-runtime": "https://esm.sh/react@19/jsx-runtime?dev",
|
|
977
|
+
"border-beam": "https://esm.sh/border-beam@1.2.0?external=react,react-dom"
|
|
978
|
+
} }
|
|
979
|
+
</script>
|
|
980
|
+
<script type="module">
|
|
981
|
+
import React from "react";
|
|
982
|
+
import { createRoot } from "react-dom/client";
|
|
983
|
+
import { createPortal } from "react-dom";
|
|
984
|
+
import { BorderBeam } from "border-beam";
|
|
985
|
+
const { createElement: h, useState, useEffect, useLayoutEffect, useRef, useMemo,
|
|
986
|
+
useCallback, useSyncExternalStore, createContext, useContext } = React;
|
|
987
|
+
|
|
988
|
+
// ── helpers ──
|
|
989
|
+
function parseCssTime(v) { const t=v.trim(); if(t.endsWith("ms")) return parseFloat(t); if(t.endsWith("s")) return parseFloat(t)*1000; const n=parseFloat(t); return isNaN(n)?0:n; }
|
|
990
|
+
function formatCssTime(ms) { if(ms>=1000&&ms%1000===0) return `${ms/1000}s`; if(ms>=100) return `${ms/1000}s`; return `${ms}ms`; }
|
|
991
|
+
function zipTransitionLists(props,durs,dels,eass) { if(!props.length)return[]; return props.map((p,i)=>({ property:p.trim(), durationMs:parseCssTime(durs[i%durs.length]??"0s"), delayMs:parseCssTime(dels[i%dels.length]??"0s"), easing:(eass[i%eass.length]??"ease").trim() })); }
|
|
992
|
+
function splitCssValues(str) {
|
|
993
|
+
const parts=[]; let cur="", depth=0;
|
|
994
|
+
for(let i=0;i<str.length;i++){const c=str[i];if(c==="(")depth++;else if(c===")")depth--;if(c===","&&depth===0){parts.push(cur);cur="";}else{cur+=c;}}
|
|
995
|
+
if(cur)parts.push(cur); return parts;
|
|
996
|
+
}
|
|
997
|
+
function transitionSignature(props,dur,del,ease) { return [...props].sort().join(",")+"|"+dur+"|"+del+"|"+ease; }
|
|
998
|
+
|
|
999
|
+
const EASING_PRESETS = [
|
|
1000
|
+
{ keyword:"linear", cubic:[0,0,1,1] }, { keyword:"ease", cubic:[0.25,0.1,0.25,1] },
|
|
1001
|
+
{ keyword:"ease-in", cubic:[0.42,0,1,1] }, { keyword:"ease-out", cubic:[0,0,0.58,1] },
|
|
1002
|
+
{ keyword:"ease-in-out", cubic:[0.42,0,0.58,1] },
|
|
1003
|
+
];
|
|
1004
|
+
const kwMap = new Map(EASING_PRESETS.map(e=>[e.keyword,e]));
|
|
1005
|
+
function easingToCubic(v) { const p=kwMap.get(v); if(p)return p.cubic; const m=v.match(/cubic-bezier\(\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*\)/); if(m)return[parseFloat(m[1]),parseFloat(m[2]),parseFloat(m[3]),parseFloat(m[4])]; return null; }
|
|
1006
|
+
|
|
1007
|
+
// Easing dropdown library. `group` rows are non-selectable section headers.
|
|
1008
|
+
// Motion tokens are the verbatim cubic-beziers shipped by the transitions.dev
|
|
1009
|
+
// skill (_root.css); the "Common" set are the standard easings.net curves.
|
|
1010
|
+
const EASING_LIBRARY = [
|
|
1011
|
+
{ group:"Standard" },
|
|
1012
|
+
{ label:"Linear", value:"linear" },
|
|
1013
|
+
{ label:"Ease", value:"ease" },
|
|
1014
|
+
{ label:"Ease in", value:"ease-in" },
|
|
1015
|
+
{ label:"Ease out", value:"ease-out" },
|
|
1016
|
+
{ label:"Ease in-out", value:"ease-in-out" },
|
|
1017
|
+
{ group:"Motion tokens (transitions.dev)" },
|
|
1018
|
+
{ label:"Smooth out", value:"cubic-bezier(0.22, 1, 0.36, 1)", usage:"The transitions.dev default — dropdown, modal, panel, tabs, page slides, accordion." },
|
|
1019
|
+
{ label:"Standard close", value:"cubic-bezier(0.4, 0, 0.2, 1)", usage:"Material-style accelerate→decelerate. Used for badge close / calm exits." },
|
|
1020
|
+
{ label:"Pop overshoot", value:"cubic-bezier(0.34, 1.36, 0.64, 1)", usage:"Notification badge pop-in — a small overshoot past the target." },
|
|
1021
|
+
{ label:"Digit pop", value:"cubic-bezier(0.34, 1.45, 0.64, 1)", usage:"Number pop-in — slightly springier overshoot for counting digits." },
|
|
1022
|
+
{ label:"Morph open", value:"cubic-bezier(0.34, 1.25, 0.64, 1)", usage:"Plus-to-menu morph open — gentle bouncy expand." },
|
|
1023
|
+
{ label:"Check bob", value:"cubic-bezier(0.34, 1.35, 0.64, 1)", usage:"Success check bob — playful settle on confirmation." },
|
|
1024
|
+
{ label:"Big overshoot", value:"cubic-bezier(0.34, 3.85, 0.64, 1)", usage:"Avatar group hover return — aggressive, spring-like overshoot." },
|
|
1025
|
+
{ group:"Common (easings.net)" },
|
|
1026
|
+
{ label:"Sine in", value:"cubic-bezier(0.12, 0, 0.39, 0)" },
|
|
1027
|
+
{ label:"Sine out", value:"cubic-bezier(0.61, 1, 0.88, 1)" },
|
|
1028
|
+
{ label:"Sine in-out", value:"cubic-bezier(0.37, 0, 0.63, 1)" },
|
|
1029
|
+
{ label:"Cubic in", value:"cubic-bezier(0.32, 0, 0.67, 0)" },
|
|
1030
|
+
{ label:"Cubic out", value:"cubic-bezier(0.33, 1, 0.68, 1)" },
|
|
1031
|
+
{ label:"Cubic in-out", value:"cubic-bezier(0.65, 0, 0.35, 1)" },
|
|
1032
|
+
{ label:"Quart in", value:"cubic-bezier(0.5, 0, 0.75, 0)" },
|
|
1033
|
+
{ label:"Quart out", value:"cubic-bezier(0.25, 1, 0.5, 1)" },
|
|
1034
|
+
{ label:"Quart in-out", value:"cubic-bezier(0.76, 0, 0.24, 1)" },
|
|
1035
|
+
{ label:"Expo in", value:"cubic-bezier(0.7, 0, 0.84, 0)" },
|
|
1036
|
+
{ label:"Expo out", value:"cubic-bezier(0.16, 1, 0.3, 1)" },
|
|
1037
|
+
{ label:"Expo in-out", value:"cubic-bezier(0.87, 0, 0.13, 1)" },
|
|
1038
|
+
{ label:"Circ in", value:"cubic-bezier(0.55, 0, 1, 0.45)" },
|
|
1039
|
+
{ label:"Circ out", value:"cubic-bezier(0, 0.55, 0.45, 1)" },
|
|
1040
|
+
{ label:"Circ in-out", value:"cubic-bezier(0.85, 0, 0.15, 1)" },
|
|
1041
|
+
{ label:"Back in", value:"cubic-bezier(0.36, 0, 0.66, -0.56)" },
|
|
1042
|
+
{ label:"Back out", value:"cubic-bezier(0.34, 1.56, 0.64, 1)" },
|
|
1043
|
+
{ label:"Back in-out", value:"cubic-bezier(0.68, -0.6, 0.32, 1.6)" },
|
|
1044
|
+
{ group:"Custom" },
|
|
1045
|
+
{ label:"cubic-bezier(\u2026)", value:"__cubic" },
|
|
1046
|
+
{ label:"custom", value:"__custom" },
|
|
1047
|
+
];
|
|
1048
|
+
const normEase = s => (s||"").replace(/\s+/g,"");
|
|
1049
|
+
const EASING_LABEL = new Map(EASING_LIBRARY.filter(o=>o.value&&o.value[0]!=="_").map(o=>[normEase(o.value),o.label]));
|
|
1050
|
+
function easingLabel(v){ return EASING_LABEL.get(normEase(v)) || null; }
|
|
1051
|
+
|
|
1052
|
+
// Most-common predefined springs. tension/friction follow react-spring's
|
|
1053
|
+
// naming (tension≈stiffness, friction≈damping, mass=1) so the presets match
|
|
1054
|
+
// what designers already know from react-spring / Framer / SwiftUI.
|
|
1055
|
+
const SPRING_PRESETS = [
|
|
1056
|
+
{ label:"Default", stiffness:170, damping:26, mass:1, usage:"Balanced, barely-there overshoot. react-spring's default." },
|
|
1057
|
+
{ label:"Gentle", stiffness:120, damping:14, mass:1, usage:"Soft and relaxed with a light overshoot." },
|
|
1058
|
+
{ label:"Wobbly", stiffness:180, damping:12, mass:1, usage:"Loose and playful — pronounced multi-bounce." },
|
|
1059
|
+
{ label:"Stiff", stiffness:210, damping:20, mass:1, usage:"Quick and tight with minimal bounce." },
|
|
1060
|
+
{ label:"Slow", stiffness:280, damping:60, mass:1, usage:"Heavy and deliberate, no overshoot." },
|
|
1061
|
+
{ label:"Molasses", stiffness:280, damping:120, mass:1, usage:"Very heavy and slow — fully damped." },
|
|
1062
|
+
{ label:"Bouncy", stiffness:200, damping:10, mass:1, usage:"High-energy, several decaying bounces." },
|
|
1063
|
+
{ label:"Snappy", stiffness:300, damping:24, mass:1, usage:"Fast response, crisp settle." },
|
|
1064
|
+
{ label:"Smooth", stiffness:240, damping:32, mass:1, usage:"Critically damped — fast, zero overshoot." },
|
|
1065
|
+
];
|
|
1066
|
+
function matchSpringPreset(p){
|
|
1067
|
+
if(!p) return null;
|
|
1068
|
+
const m = SPRING_PRESETS.find(s=>s.stiffness===p.stiffness&&s.damping===p.damping&&s.mass===p.mass);
|
|
1069
|
+
return m ? m.label : "Custom";
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Numerically integrate a damped harmonic oscillator animating 0→1. The settle
|
|
1073
|
+
// time (and therefore the CSS duration) EMERGES from stiffness/damping/mass —
|
|
1074
|
+
// it is never set by hand. We then sample the motion into a CSS linear() easing
|
|
1075
|
+
// so a real spring curve (overshoot + decaying oscillation) plays in the browser.
|
|
1076
|
+
const _springCache = new Map();
|
|
1077
|
+
function simulateSpring(stiffness, damping, mass){
|
|
1078
|
+
const k = Math.max(1, stiffness), c = Math.max(0, damping), m = Math.max(0.1, mass);
|
|
1079
|
+
const key = k+"|"+c+"|"+m;
|
|
1080
|
+
if(_springCache.has(key)) return _springCache.get(key);
|
|
1081
|
+
const dt = 1/360, maxT = 6, rest = 0.0015, target = 1;
|
|
1082
|
+
let x = 0, v = 0, lastUnsettled = 0;
|
|
1083
|
+
const pos = [];
|
|
1084
|
+
for(let t=0; t<=maxT+dt; t+=dt){
|
|
1085
|
+
pos.push(x);
|
|
1086
|
+
const a = (-k*(x-target) - c*v)/m;
|
|
1087
|
+
v += a*dt; x += v*dt;
|
|
1088
|
+
if(Math.abs(x-target)>rest || Math.abs(v)>rest) lastUnsettled = t;
|
|
1089
|
+
}
|
|
1090
|
+
const settleT = Math.min(maxT, Math.max(0.08, lastUnsettled + dt*2));
|
|
1091
|
+
const N = 60, values = [];
|
|
1092
|
+
for(let i=0;i<=N;i++){
|
|
1093
|
+
const fi = ((i/N)*settleT)/dt;
|
|
1094
|
+
const i0 = Math.floor(fi), i1 = Math.min(pos.length-1, i0+1), f = fi-i0;
|
|
1095
|
+
values.push(pos[i0]*(1-f) + pos[i1]*f);
|
|
1096
|
+
}
|
|
1097
|
+
values[0] = 0; values[N] = 1;
|
|
1098
|
+
const css = "linear(" + values.map(val=>Math.round(val*10000)/10000).join(", ") + ")";
|
|
1099
|
+
const out = { durationMs: Math.round(settleT*1000), css, values };
|
|
1100
|
+
_springCache.set(key, out);
|
|
1101
|
+
return out;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// ── registry (per-property overrides) ──
|
|
1105
|
+
class TransitionRegistry {
|
|
1106
|
+
entries=new Map(); listeners=new Set(); propOverrides=new Map(); snapshot=[]; effectiveCache=new Map();
|
|
1107
|
+
register(e){ this.entries.set(e.id,e); this._notify(); }
|
|
1108
|
+
unregister(id){ this.entries.delete(id); this.propOverrides.delete(id); this._notify(); }
|
|
1109
|
+
replaceAll(list){ this.entries.clear(); for(const e of list) this.entries.set(e.id,e); this._notify(); }
|
|
1110
|
+
get(id){ return this.entries.get(id); }
|
|
1111
|
+
getAll(){ return this.snapshot; }
|
|
1112
|
+
getEffective(id){ return this.effectiveCache.get(id); }
|
|
1113
|
+
setPropOverride(id, prop, o){
|
|
1114
|
+
const m = this.propOverrides.get(id) || {};
|
|
1115
|
+
m[prop] = { ...m[prop], ...o };
|
|
1116
|
+
this.propOverrides.set(id, m);
|
|
1117
|
+
this._notify();
|
|
1118
|
+
}
|
|
1119
|
+
clearOverride(id){ this.propOverrides.delete(id); this._notify(); }
|
|
1120
|
+
getPropOverrides(id){ return this.propOverrides.get(id); }
|
|
1121
|
+
subscribe(fn){ this.listeners.add(fn); return ()=>this.listeners.delete(fn); }
|
|
1122
|
+
_notify(){
|
|
1123
|
+
this.snapshot=Array.from(this.entries.values());
|
|
1124
|
+
this.effectiveCache.clear();
|
|
1125
|
+
for(const[id,entry]of this.entries){
|
|
1126
|
+
const po = this.propOverrides.get(id) || {};
|
|
1127
|
+
const timings = (entry.propertyTimings || entry.properties.map(p=>({property:p,durationMs:entry.durationMs,delayMs:entry.delayMs,easing:entry.easing})));
|
|
1128
|
+
const effTimings = timings.map(t => {
|
|
1129
|
+
const o = po[t.property];
|
|
1130
|
+
return { property:t.property, durationMs:o?.durationMs??t.durationMs, delayMs:o?.delayMs??t.delayMs, easing:o?.easing??t.easing, spring:o?.spring??t.spring };
|
|
1131
|
+
});
|
|
1132
|
+
this.effectiveCache.set(id, { ...entry, effectiveTimings: effTimings });
|
|
1133
|
+
}
|
|
1134
|
+
for(const fn of this.listeners) fn(this.snapshot);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// ── preview controller ──
|
|
1139
|
+
class PreviewController {
|
|
1140
|
+
state="idle"; listeners=new Set(); cleanups=[]; animations=[]; progressListeners=new Set(); _rafId=null; scanner=null; _gen=0;
|
|
1141
|
+
rate=1; loop=false; _current=null;
|
|
1142
|
+
setScanner(s){this.scanner=s;} getState(){return this.state;}
|
|
1143
|
+
setRate(r){this.rate=r;for(const a of this.animations){try{a.playbackRate=r;}catch{}}}
|
|
1144
|
+
setLoop(v){this.loop=v;}
|
|
1145
|
+
subscribe(fn){this.listeners.add(fn);return()=>this.listeners.delete(fn);}
|
|
1146
|
+
onProgress(fn){this.progressListeners.add(fn);return()=>this.progressListeners.delete(fn);}
|
|
1147
|
+
play(entry){this.stop();this._gen++;this._current=entry;
|
|
1148
|
+
if(this.scanner)this.scanner.pause();if(entry.bindings.type==="css")this._playCss(entry,this._gen);this._setState("playing");}
|
|
1149
|
+
pause(){if(this.state!=="playing")return;for(const a of this.animations){try{a.pause();}catch{}}this._stopPL();this._setState("paused");}
|
|
1150
|
+
resume(){if(this.state!=="paused")return;for(const a of this.animations){try{a.play();}catch{}}this._startPL();this._setState("playing");}
|
|
1151
|
+
stop(){this._stopPL();for(const a of this.animations){try{a.cancel();}catch{}}this.animations=[];for(const c of this.cleanups)c();this.cleanups=[];this._ep(0);if(this.scanner)this.scanner.unpause();this._setState("idle");}
|
|
1152
|
+
restart(entry){this.stop();requestAnimationFrame(()=>this.play(entry));}
|
|
1153
|
+
playPaused(entry,seekMs){
|
|
1154
|
+
this.stop();this._gen++;const gen=this._gen;
|
|
1155
|
+
this._pendingSeek=seekMs;
|
|
1156
|
+
if(this.scanner)this.scanner.pause();
|
|
1157
|
+
if(entry.bindings.type!=="css")return;
|
|
1158
|
+
this.animations=[];
|
|
1159
|
+
const et=entry.effectiveTimings||entry.properties.map(p=>({property:p,durationMs:entry.durationMs,delayMs:entry.delayMs,easing:entry.easing}));
|
|
1160
|
+
for(const wr of entry.bindings.elements){const el=wr.deref();if(!el)continue;const saved=el.style.transition;
|
|
1161
|
+
const tv=et.map(t=>`${t.property} ${formatCssTime(t.durationMs)} ${t.easing} ${formatCssTime(t.delayMs)}`).join(", ");
|
|
1162
|
+
el.style.transition=tv;el.click();
|
|
1163
|
+
requestAnimationFrame(()=>{if(this._gen!==gen)return;const running=el.getAnimations();
|
|
1164
|
+
for(const a of running){a.pause();a.playbackRate=this.rate;this.animations.push(a);}
|
|
1165
|
+
const t=this._pendingSeek??seekMs;
|
|
1166
|
+
if(t!=null){for(const a of this.animations){try{a.currentTime=t;}catch{}}this._ep(t);}
|
|
1167
|
+
});
|
|
1168
|
+
this.cleanups.push(()=>{el.style.transition=saved;});}
|
|
1169
|
+
this._setState("paused");
|
|
1170
|
+
if(seekMs!=null)this._ep(seekMs);
|
|
1171
|
+
}
|
|
1172
|
+
seek(timeMs){
|
|
1173
|
+
if(this.state==="idle")return;
|
|
1174
|
+
if(this.state==="playing"){for(const a of this.animations){try{a.pause();}catch{}}this._stopPL();this._setState("paused");}
|
|
1175
|
+
for(const a of this.animations){try{a.currentTime=timeMs;}catch{}}
|
|
1176
|
+
this._ep(timeMs);
|
|
1177
|
+
this._pendingSeek=timeMs;
|
|
1178
|
+
}
|
|
1179
|
+
_finish(){this._stopPL();if(this.animations.length>0){let end=0;for(const a of this.animations){const t=a.effect?.getTiming();const e=(t?.delay??0)+(Number(t?.duration)||0);if(e>end)end=e;}this._ep(end);}this.animations=[];for(const c of this.cleanups)c();this.cleanups=[];if(this.scanner)this.scanner.unpause();this._setState("idle");}
|
|
1180
|
+
_playCss(entry,gen){if(entry.bindings.type!=="css")return;this.animations=[];
|
|
1181
|
+
const et = entry.effectiveTimings || entry.properties.map(p=>({property:p,durationMs:entry.durationMs,delayMs:entry.delayMs,easing:entry.easing}));
|
|
1182
|
+
for(const wr of entry.bindings.elements){const el=wr.deref();if(!el)continue;const saved=el.style.transition;
|
|
1183
|
+
const tv=et.map(t=>`${t.property} ${formatCssTime(t.durationMs)} ${t.easing} ${formatCssTime(t.delayMs)}`).join(", ");
|
|
1184
|
+
el.style.transition=tv;el.click();
|
|
1185
|
+
requestAnimationFrame(()=>{if(this._gen!==gen)return;const running=el.getAnimations();for(const a of running){a.playbackRate=this.rate;this.animations.push(a);}this._startPL();
|
|
1186
|
+
if(running.length>0){Promise.allSettled(running.map(a=>a.finished)).then(()=>{if(this._gen!==gen||this.state!=="playing")return;if(this.loop&&this._current){this.play(this._current);}else{this._finish();}});}else{this._finish();}});
|
|
1187
|
+
this.cleanups.push(()=>{el.style.transition=saved;});}}
|
|
1188
|
+
_startPL(){this._stopPL();const tick=()=>{if(this.animations.length>0){let cur=0;for(const a of this.animations){const c=Number(a.currentTime)||0;if(c>cur)cur=c;}this._ep(cur);}this._rafId=requestAnimationFrame(tick);};this._rafId=requestAnimationFrame(tick);}
|
|
1189
|
+
_stopPL(){if(this._rafId!==null){cancelAnimationFrame(this._rafId);this._rafId=null;}}
|
|
1190
|
+
_ep(p){for(const fn of this.progressListeners)fn(p);}
|
|
1191
|
+
_setState(s){this.state=s;for(const fn of this.listeners)fn(s);}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// ── scanner ──
|
|
1195
|
+
class DomScanner {
|
|
1196
|
+
observer=null;rafId=null;running=false;paused=false;
|
|
1197
|
+
constructor(root,reg){this.root=root;this.registry=reg;}
|
|
1198
|
+
pause(){this.paused=true;} unpause(){this.paused=false;this._sched();}
|
|
1199
|
+
start(){if(this.running)return;this.running=true;this.scan();this.observer=new MutationObserver(()=>this._sched());this.observer.observe(this.root,{childList:true,subtree:true,attributes:true,attributeFilter:["class","style"]});}
|
|
1200
|
+
stop(){this.running=false;this.observer?.disconnect();this.observer=null;if(this.rafId!==null){cancelAnimationFrame(this.rafId);this.rafId=null;}}
|
|
1201
|
+
_sched(){if(this.rafId!==null)return;this.rafId=requestAnimationFrame(()=>{this.rafId=null;if(this.running&&!this.paused)this.scan();});}
|
|
1202
|
+
scan(){const seen=new Map();const w=document.createTreeWalker(this.root,NodeFilter.SHOW_ELEMENT);let n=w.currentNode;while(n){if(n instanceof HTMLElement)this._proc(n,seen);n=w.nextNode();}this.registry.replaceAll(Array.from(seen.values()));}
|
|
1203
|
+
_proc(el,seen){
|
|
1204
|
+
if(el.closest("[data-timeline-panel]"))return;
|
|
1205
|
+
const s=getComputedStyle(el); const rp=s.transitionProperty;
|
|
1206
|
+
if(!rp||rp==="none"||rp==="all")return;
|
|
1207
|
+
const props=rp.split(",").map(p=>p.trim());
|
|
1208
|
+
const durs=(s.transitionDuration||"0s").split(",");
|
|
1209
|
+
const dels=(s.transitionDelay||"0s").split(",");
|
|
1210
|
+
const eass=splitCssValues(s.transitionTimingFunction||"ease");
|
|
1211
|
+
const z=zipTransitionLists(props,durs,dels,eass);
|
|
1212
|
+
if(!z.length||z.every(x=>x.durationMs===0&&x.delayMs===0))return;
|
|
1213
|
+
const pDur=z[0].durationMs,pDel=z[0].delayMs,pEase=z[0].easing;
|
|
1214
|
+
const allP=z.map(x=>x.property);
|
|
1215
|
+
const sig=transitionSignature(allP,pDur,pDel,pEase);
|
|
1216
|
+
if(seen.has(sig)){const ex=seen.get(sig);if(ex.bindings.type==="css")ex.bindings.elements.push(new WeakRef(el));return;}
|
|
1217
|
+
seen.set(sig,{ id:"css-"+sig, label:this._lbl(el), durationMs:pDur, delayMs:pDel, easing:pEase,
|
|
1218
|
+
properties:allP, propertyTimings:z, source:"css",
|
|
1219
|
+
bindings:{type:"css",elements:[new WeakRef(el)],selector:this._sel(el)} });
|
|
1220
|
+
}
|
|
1221
|
+
_lbl(el){return el.getAttribute("data-testid")||el.getAttribute("aria-label")||(el.id?"#"+el.id:null)||(typeof el.className==="string"&&el.className.trim()?el.tagName.toLowerCase()+"."+el.className.trim().split(/\s+/).slice(0,2).join("."):el.tagName.toLowerCase());}
|
|
1222
|
+
_sel(el){if(el.id)return"#"+el.id;const p=[];let c=el,d=0;while(c&&d<3){let s=c.tagName.toLowerCase();if(c.id){p.unshift("#"+c.id);break;}if(c.className&&typeof c.className==="string"){const cls=c.className.trim().split(/\s+/)[0];if(cls)s+="."+cls;}p.unshift(s);c=c.parentElement;d++;}return p.join(" > ");}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// ── hooks ──
|
|
1226
|
+
const TimelineCtx = createContext(null);
|
|
1227
|
+
function useReg(){const{registry}=useContext(TimelineCtx);return useSyncExternalStore(useCallback(cb=>registry.subscribe(cb),[registry]),useCallback(()=>registry.getAll(),[registry]),useCallback(()=>registry.getAll(),[registry]));}
|
|
1228
|
+
function useActive(){const{activeId,setActiveId,registry}=useContext(TimelineCtx);const active=useSyncExternalStore(useCallback(cb=>registry.subscribe(cb),[registry]),useCallback(()=>activeId?registry.getEffective(activeId):undefined,[registry,activeId]),useCallback(()=>activeId?registry.getEffective(activeId):undefined,[registry,activeId]));return{active,setActiveId};}
|
|
1229
|
+
function usePlayback(){const{preview,registry,activeId}=useContext(TimelineCtx);const state=useSyncExternalStore(useCallback(cb=>preview.subscribe(cb),[preview]),useCallback(()=>preview.getState(),[preview]),useCallback(()=>preview.getState(),[preview]));
|
|
1230
|
+
return{state,play:useCallback(()=>{if(!activeId)return;const e=registry.getEffective(activeId);if(e)preview.play(e);},[preview,registry,activeId]),pause:useCallback(()=>preview.pause(),[preview]),resume:useCallback(()=>preview.resume(),[preview]),restart:useCallback(()=>{if(!activeId)return;const e=registry.getEffective(activeId);if(e)preview.restart(e);},[preview,registry,activeId]),stop:useCallback(()=>preview.stop(),[preview])};}
|
|
1231
|
+
function usePropOverride(){const{registry,activeId}=useContext(TimelineCtx);return{setPropOverride:useCallback((prop,o)=>{if(activeId)registry.setPropOverride(activeId,prop,o);},[registry,activeId])};}
|
|
1232
|
+
|
|
1233
|
+
// ── components ──
|
|
1234
|
+
const BASE_SCALE_MS = 5000;
|
|
1235
|
+
const ZOOM_MIN = 25;
|
|
1236
|
+
const ZOOM_MAX = 400;
|
|
1237
|
+
const ZOOM_DEFAULT = 100;
|
|
1238
|
+
const scaleFromZoom = zoom => Math.round(BASE_SCALE_MS * 100 / zoom);
|
|
1239
|
+
const LABEL_W = 150;
|
|
1240
|
+
const CLOSE_MS = 150;
|
|
1241
|
+
const SPEEDS = [0.25, 0.5, 1, 2];
|
|
1242
|
+
const DURATION_TOKENS = [
|
|
1243
|
+
{label:"Duration-fast", ms:150, usage:"Quick state changes — hovers, toggles, button presses, dropdown & modal close, text swaps."},
|
|
1244
|
+
{label:"Duration-medium", ms:250, usage:"Standard UI motion — icon swaps, dropdown & modal open, sliding tabs, page slides."},
|
|
1245
|
+
{label:"Duration-slow", ms:400, usage:"Larger surfaces — panel open, skeleton reveals, input clear."},
|
|
1246
|
+
{label:"Duration-very-slow", ms:600, usage:"Emphasis & page-level moments — hero animations, big choreographed transitions."},
|
|
1247
|
+
];
|
|
1248
|
+
const DELAY_TOKENS = [
|
|
1249
|
+
{label:"None", ms:0, usage:"No delay — motion begins immediately."},
|
|
1250
|
+
{label:"Short", ms:50, usage:"Slight offset to sequence closely related elements."},
|
|
1251
|
+
{label:"Medium", ms:150, usage:"Stagger between grouped elements for a cascading feel."},
|
|
1252
|
+
{label:"Long", ms:300, usage:"Pronounced wait — use sparingly to draw attention."},
|
|
1253
|
+
];
|
|
1254
|
+
function cx(...a){ return a.filter(Boolean).join(" "); }
|
|
1255
|
+
function fmtTimecode(ms){ const m=Math.floor(ms/60000); const s=Math.floor((ms%60000)/1000); const c=Math.floor((ms%1000)/10); const p=n=>String(n).padStart(2,"0"); return p(m)+":"+p(s)+":"+p(c); }
|
|
1256
|
+
function fmtSpeed(s){ return s+"x"; }
|
|
1257
|
+
|
|
1258
|
+
// ── icons ──
|
|
1259
|
+
// Exact SVGs exported from the Logram ❖ Design System Figma file (no recreations).
|
|
1260
|
+
// Source icons are exported with currentColor so they inherit the button's color.
|
|
1261
|
+
// Timeline-design icons are 16px-native; library icons (pause/stop/check/help) are
|
|
1262
|
+
// 24px-native, so their stroke weight is normalized to read ~1.6px at toolbar size.
|
|
1263
|
+
const ICONS = {
|
|
1264
|
+
play: {vb:"0 0 10 12", svg:`<path d="M9.49453 6.92883L1.54117 11.8508C0.866207 12.2681 0 11.7628 0 10.9216V1.07765C0 0.237764 0.864957 -0.268832 1.54117 0.149776L9.49453 5.07175C9.64807 5.16524 9.7757 5.30037 9.86447 5.46344C9.95324 5.62651 10 5.81172 10 6.00029C10 6.18885 9.95324 6.37407 9.86447 6.53714C9.7757 6.70021 9.64807 6.83534 9.49453 6.92883Z" fill="currentColor"/>`},
|
|
1265
|
+
pause: {vb:"0 0 24 24", svg:`<path d="M5.75 3C4.7835 3 4 3.7835 4 4.75V19.25C4 20.2165 4.7835 21 5.75 21H8.25C9.2165 21 10 20.2165 10 19.25V4.75C10 3.7835 9.2165 3 8.25 3H5.75Z" fill="currentColor"/><path d="M15.75 3C14.7835 3 14 3.7835 14 4.75V19.25C14 20.2165 14.7835 21 15.75 21H18.25C19.2165 21 20 20.2165 20 19.25V4.75C20 3.7835 19.2165 3 18.25 3H15.75Z" fill="currentColor"/>`},
|
|
1266
|
+
stop: {vb:"0 0 24 24", svg:`<path fill-rule="evenodd" clip-rule="evenodd" d="M5.32378 3C5.3325 3 5.34124 3 5.35 3L18.6762 3C18.9337 2.99998 19.1702 2.99997 19.3679 3.01612C19.581 3.03353 19.8142 3.07339 20.0445 3.19074C20.3738 3.35852 20.6415 3.62624 20.8093 3.95552C20.9266 4.18583 20.9665 4.419 20.9839 4.63213C21 4.82981 21 5.06629 21 5.32377V18.6762C21 18.9337 21 19.1702 20.9839 19.3679C20.9665 19.581 20.9266 19.8142 20.8093 20.0445C20.6415 20.3738 20.3738 20.6415 20.0445 20.8093C19.8142 20.9266 19.581 20.9665 19.3679 20.9839C19.1702 21 18.9337 21 18.6762 21H5.32377C5.06629 21 4.82981 21 4.63213 20.9839C4.419 20.9665 4.18583 20.9266 3.95552 20.8093C3.62624 20.6415 3.35852 20.3738 3.19074 20.0445C3.07339 19.8142 3.03353 19.581 3.01612 19.3679C2.99997 19.1702 2.99998 18.9337 3 18.6762L3 5.35C3 5.34124 3 5.3325 3 5.32379C2.99998 5.0663 2.99997 4.82982 3.01612 4.63213C3.03353 4.419 3.07339 4.18583 3.19074 3.95552C3.35852 3.62624 3.62624 3.35852 3.95552 3.19074C4.18583 3.07339 4.419 3.03353 4.63213 3.01612C4.82982 2.99997 5.0663 2.99998 5.32378 3Z" fill="currentColor"/>`},
|
|
1267
|
+
restart: {vb:"0 0 16 16", svg:`<path d="M1.33333 6.66667C1.33333 6.66667 2.66999 4.84548 3.75589 3.75883C4.84179 2.67218 6.3424 2 8 2C11.3137 2 14 4.68629 14 8C14 11.3137 11.3137 14 8 14C5.2646 14 2.95674 12.1695 2.23451 9.66667M5.33333 6.66667H1.33333V2.66667" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`},
|
|
1268
|
+
copy: {vb:"0 0 16 16", svg:`<path d="M5.3335 5.33325V3.46659C5.3335 2.71985 5.3335 2.34648 5.47882 2.06126C5.60665 1.81038 5.81063 1.60641 6.06151 1.47858C6.34672 1.33325 6.72009 1.33325 7.46683 1.33325H12.5335C13.2802 1.33325 13.6536 1.33325 13.9388 1.47858C14.1897 1.60641 14.3937 1.81038 14.5215 2.06126C14.6668 2.34648 14.6668 2.71985 14.6668 3.46659V8.53325C14.6668 9.27999 14.6668 9.65336 14.5215 9.93857C14.3937 10.1895 14.1897 10.3934 13.9388 10.5213C13.6536 10.6666 13.2802 10.6666 12.5335 10.6666H10.6668M3.46683 14.6666H8.5335C9.28023 14.6666 9.6536 14.6666 9.93882 14.5213C10.1897 14.3934 10.3937 14.1895 10.5215 13.9386C10.6668 13.6534 10.6668 13.28 10.6668 12.5333V7.46658C10.6668 6.71985 10.6668 6.34648 10.5215 6.06126C10.3937 5.81038 10.1897 5.60641 9.93882 5.47858C9.6536 5.33325 9.28023 5.33325 8.5335 5.33325H3.46683C2.72009 5.33325 2.34672 5.33325 2.06151 5.47858C1.81063 5.60641 1.60665 5.81038 1.47882 6.06126C1.3335 6.34648 1.3335 6.71985 1.3335 7.46658V12.5333C1.3335 13.28 1.3335 13.6534 1.47882 13.9386C1.60665 14.1895 1.81063 14.3934 2.06151 14.5213C2.34672 14.6666 2.72009 14.6666 3.46683 14.6666Z" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>`},
|
|
1269
|
+
check: {vb:"0 0 16 16", svg:`<path d="M3 7.88889L5.76923 11L12 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`},
|
|
1270
|
+
chevron: {vb:"0 0 16 16", svg:`<path fill-rule="evenodd" clip-rule="evenodd" d="M4.46967 6.46967C4.76256 6.17678 5.23744 6.17678 5.53033 6.46967L8 8.93934L10.4697 6.46967C10.7626 6.17678 11.2374 6.17678 11.5303 6.46967C11.8232 6.76256 11.8232 7.23744 11.5303 7.53033L8.53033 10.5303C8.23744 10.8232 7.76256 10.8232 7.46967 10.5303L4.46967 7.53033C4.17678 7.23744 4.17678 6.76256 4.46967 6.46967Z" fill="currentColor"/>`},
|
|
1271
|
+
gear: {vb:"0 0 16 16", svg:`<path d="M8.00016 9.99992C9.10473 9.99992 10.0002 9.10449 10.0002 7.99992C10.0002 6.89535 9.10473 5.99992 8.00016 5.99992C6.89559 5.99992 6.00016 6.89535 6.00016 7.99992C6.00016 9.10449 6.89559 9.99992 8.00016 9.99992Z" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><path d="M12.485 9.8181C12.4043 10.0009 12.3803 10.2037 12.4159 10.4003C12.4516 10.5969 12.5453 10.7783 12.685 10.9211L12.7214 10.9575C12.8341 11.0701 12.9235 11.2038 12.9845 11.3509C13.0455 11.498 13.0769 11.6558 13.0769 11.8151C13.0769 11.9744 13.0455 12.1321 12.9845 12.2792C12.9235 12.4264 12.8341 12.5601 12.7214 12.6726C12.6088 12.7853 12.4751 12.8747 12.328 12.9357C12.1808 12.9967 12.0231 13.0281 11.8638 13.0281C11.7045 13.0281 11.5468 12.9967 11.3996 12.9357C11.2525 12.8747 11.1188 12.7853 11.0062 12.6726L10.9699 12.6363C10.827 12.4966 10.6456 12.4028 10.449 12.3672C10.2524 12.3315 10.0496 12.3556 9.86683 12.4363C9.68757 12.5131 9.5347 12.6407 9.42702 12.8033C9.31933 12.9659 9.26155 13.1564 9.26077 13.3514V13.4545C9.26077 13.7759 9.13306 14.0842 8.90575 14.3116C8.67843 14.5389 8.37012 14.6666 8.04865 14.6666C7.72717 14.6666 7.41887 14.5389 7.19155 14.3116C6.96423 14.0842 6.83653 13.7759 6.83653 13.4545V13.3999C6.83183 13.1993 6.7669 13.0048 6.65017 12.8416C6.53344 12.6783 6.37031 12.554 6.18198 12.4848C5.99918 12.4041 5.79641 12.38 5.59981 12.4157C5.4032 12.4513 5.22179 12.545 5.07895 12.6848L5.04259 12.7211C4.93001 12.8338 4.79633 12.9232 4.64918 12.9842C4.50203 13.0452 4.3443 13.0766 4.18501 13.0766C4.02572 13.0766 3.86799 13.0452 3.72084 12.9842C3.57369 12.9232 3.44001 12.8338 3.32744 12.7211C3.21474 12.6086 3.12533 12.4749 3.06433 12.3277C3.00333 12.1806 2.97194 12.0228 2.97194 11.8636C2.97194 11.7043 3.00333 11.5465 3.06433 11.3994C3.12533 11.2522 3.21474 11.1186 3.32744 11.006L3.3638 10.9696C3.50352 10.8268 3.59724 10.6454 3.63289 10.4488C3.66854 10.2522 3.64447 10.0494 3.5638 9.86658C3.48697 9.68733 3.35941 9.53445 3.19681 9.42677C3.03421 9.31909 2.84367 9.2613 2.64865 9.26052H2.54562C2.22414 9.26052 1.91583 9.13282 1.68852 8.9055C1.4612 8.67819 1.3335 8.36988 1.3335 8.0484C1.3335 7.72693 1.4612 7.41862 1.68852 7.1913C1.91583 6.96399 2.22414 6.83628 2.54562 6.83628H2.60016C2.80077 6.83159 2.99532 6.76666 3.15853 6.64992C3.32173 6.53319 3.44605 6.37006 3.51531 6.18174C3.59599 5.99894 3.62006 5.79616 3.58441 5.59956C3.54876 5.40296 3.45503 5.22154 3.31531 5.07871L3.27895 5.04234C3.16625 4.92977 3.07685 4.79609 3.01585 4.64894C2.95485 4.50179 2.92345 4.34406 2.92345 4.18477C2.92345 4.02548 2.95485 3.86775 3.01585 3.7206C3.07685 3.57345 3.16625 3.43976 3.27895 3.32719C3.39152 3.21449 3.52521 3.12509 3.67236 3.06409C3.81951 3.00309 3.97723 2.97169 4.13653 2.97169C4.29582 2.97169 4.45355 3.00309 4.6007 3.06409C4.74785 3.12509 4.88153 3.21449 4.9941 3.32719L5.03047 3.36355C5.1733 3.50327 5.35472 3.597 5.55132 3.63265C5.74792 3.6683 5.9507 3.64423 6.1335 3.56355H6.18198C6.36123 3.48673 6.51411 3.35916 6.62179 3.19656C6.72948 3.03396 6.78726 2.84343 6.78804 2.6484V2.54537C6.78804 2.2239 6.91575 1.91559 7.14306 1.68827C7.37038 1.46096 7.67869 1.33325 8.00016 1.33325C8.32164 1.33325 8.62994 1.46096 8.85726 1.68827C9.08458 1.91559 9.21228 2.2239 9.21228 2.54537V2.59992C9.21306 2.79494 9.27085 2.98548 9.37853 3.14808C9.48621 3.31068 9.63909 3.43824 9.81834 3.51507C10.0011 3.59575 10.2039 3.61981 10.4005 3.58416C10.5971 3.54852 10.7785 3.45479 10.9214 3.31507L10.9577 3.27871C11.0703 3.16601 11.204 3.0766 11.3511 3.0156C11.4983 2.9546 11.656 2.92321 11.8153 2.92321C11.9746 2.92321 12.1323 2.9546 12.2795 3.0156C12.4266 3.0766 12.5603 3.16601 12.6729 3.27871C12.7856 3.39128 12.875 3.52496 12.936 3.67211C12.997 3.81926 13.0284 3.97699 13.0284 4.13628C13.0284 4.29557 12.997 4.4533 12.936 4.60045C12.875 4.7476 12.7856 4.88128 12.6729 4.99386L12.6365 5.03022C12.4968 5.17306 12.4031 5.35447 12.3674 5.55108C12.3318 5.74768 12.3558 5.95045 12.4365 6.13325V6.18174C12.5134 6.36099 12.6409 6.51387 12.8035 6.62155C12.9661 6.72923 13.1567 6.78702 13.3517 6.7878H13.4547C13.7762 6.7878 14.0845 6.9155 14.3118 7.14282C14.5391 7.37014 14.6668 7.67844 14.6668 7.99992C14.6668 8.32139 14.5391 8.6297 14.3118 8.85702C14.0845 9.08433 13.7762 9.21204 13.4547 9.21204H13.4002C13.2051 9.21282 13.0146 9.2706 12.852 9.37829C12.6894 9.48597 12.5618 9.63885 12.485 9.8181Z" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>`},
|
|
1272
|
+
wand: {vb:"0 0 16 16", svg:`<path d="M8.66657 9.33325L6.66657 7.33325M10.0068 2.33325V1.33325M12.633 3.37369L13.3401 2.66659M12.633 8.66658L13.3401 9.37369M7.34013 3.37369L6.63303 2.66659M13.6735 5.99992H14.6735M4.08748 13.9123L10.2457 7.75417C10.5097 7.49015 10.6417 7.35815 10.6911 7.20593C10.7346 7.07203 10.7346 6.9278 10.6911 6.79391C10.6417 6.64169 10.5097 6.50968 10.2457 6.24567L9.75415 5.75417C9.49013 5.49015 9.35813 5.35815 9.20591 5.30869C9.07201 5.26518 8.92778 5.26518 8.79389 5.30869C8.64167 5.35815 8.50966 5.49015 8.24565 5.75417L2.08748 11.9123C1.82347 12.1763 1.69146 12.3084 1.642 12.4606C1.5985 12.5945 1.5985 12.7387 1.642 12.8726C1.69146 13.0248 1.82347 13.1568 2.08748 13.4208L2.57899 13.9123C2.843 14.1763 2.975 14.3084 3.12722 14.3578C3.26112 14.4013 3.40535 14.4013 3.53924 14.3578C3.69146 14.3084 3.82347 14.1763 4.08748 13.9123Z" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>`},
|
|
1273
|
+
dots: {vb:"0 0 16 16", svg:`<path d="M10.3333 4.00008C10.7015 4.00008 11 3.7016 11 3.33341C11 2.96522 10.7015 2.66675 10.3333 2.66675C9.96514 2.66675 9.66667 2.96522 9.66667 3.33341C9.66667 3.7016 9.96514 4.00008 10.3333 4.00008Z" fill="currentColor"/><path d="M10.3333 8.66675C10.7015 8.66675 11 8.36827 11 8.00008C11 7.63189 10.7015 7.33342 10.3333 7.33342C9.96514 7.33342 9.66667 7.63189 9.66667 8.00008C9.66667 8.36827 9.96514 8.66675 10.3333 8.66675Z" fill="currentColor"/><path d="M10.3333 13.3334C10.7015 13.3334 11 13.0349 11 12.6667C11 12.2986 10.7015 12.0001 10.3333 12.0001C9.96514 12.0001 9.66667 12.2986 9.66667 12.6667C9.66667 13.0349 9.96514 13.3334 10.3333 13.3334Z" fill="currentColor"/><path d="M5.66667 4.00008C6.03486 4.00008 6.33333 3.7016 6.33333 3.33341C6.33333 2.96522 6.03486 2.66675 5.66667 2.66675C5.29848 2.66675 5 2.96522 5 3.33341C5 3.7016 5.29848 4.00008 5.66667 4.00008Z" fill="currentColor"/><path d="M5.66667 8.66675C6.03486 8.66675 6.33333 8.36827 6.33333 8.00008C6.33333 7.63189 6.03486 7.33342 5.66667 7.33342C5.29848 7.33342 5 7.63189 5 8.00008C5 8.36827 5.29848 8.66675 5.66667 8.66675Z" fill="currentColor"/><path d="M5.66667 13.3334C6.03486 13.3334 6.33333 13.0349 6.33333 12.6667C6.33333 12.2986 6.03486 12.0001 5.66667 12.0001C5.29848 12.0001 5 12.2986 5 12.6667C5 13.0349 5.29848 13.3334 5.66667 13.3334Z" fill="currentColor"/>`},
|
|
1274
|
+
help: {vb:"0 0 24 24", svg:`<path d="M9.75 9.25C9.75 8.42157 10.4216 7.75 11.25 7.75H12.4587C13.448 7.75 14.25 8.552 14.25 9.54132C14.25 10.1402 13.9507 10.6996 13.4523 11.0318L12.8906 11.4063C12.3342 11.7772 12 12.4017 12 13.0704V13.25M12 16V15.99M21.25 12C21.25 17.1086 17.1086 21.25 12 21.25C6.89137 21.25 2.75 17.1086 2.75 12C2.75 6.89137 6.89137 2.75 12 2.75C17.1086 2.75 21.25 6.89137 21.25 12ZM12.25 16C12.25 16.1381 12.1381 16.25 12 16.25C11.8619 16.25 11.75 16.1381 11.75 16C11.75 15.8619 11.8619 15.75 12 15.75C12.1381 15.75 12.25 15.8619 12.25 16Z" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>`},
|
|
1275
|
+
lock: {vb:"0 0 16 16", svg:`<path d="M4.5 7V5C4.5 3.067 6.067 1.5 8 1.5C9.933 1.5 11.5 3.067 11.5 5V7M4.3 14.5H11.7C12.2601 14.5 12.5401 14.5 12.754 14.391C12.9422 14.2951 13.0951 14.1422 13.191 13.954C13.3 13.7401 13.3 13.4601 13.3 12.9V8.6C13.3 8.03995 13.3 7.75992 13.191 7.54601C13.0951 7.35785 12.9422 7.20487 12.754 7.10899C12.5401 7 12.2601 7 11.7 7H4.3C3.73995 7 3.45992 7 3.24601 7.10899C3.05785 7.20487 2.90487 7.35785 2.80899 7.54601C2.7 7.75992 2.7 8.03995 2.7 8.6V12.9C2.7 13.4601 2.7 13.7401 2.80899 13.954C2.90487 14.1422 3.05785 14.2951 3.24601 14.391C3.45992 14.5 3.73995 14.5 4.3 14.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`},
|
|
1276
|
+
scan: {vb:"0 0 13 15", svg:`<path fill-rule="evenodd" clip-rule="evenodd" d="M6.76556 1.2204C6.60067 1.12222 6.39933 1.12222 6.23444 1.2204L4.94588 1.98765C4.68515 2.1429 4.35488 2.04501 4.20822 1.76901C4.06156 1.49301 4.15403 1.14341 4.41477 0.988161L5.70333 0.220909C6.198 -0.0736362 6.802 -0.0736362 7.29667 0.220909L8.58523 0.988161C8.84597 1.14341 8.93844 1.49301 8.79178 1.76901C8.64512 2.04501 8.31485 2.1429 8.05412 1.98765L6.76556 1.2204ZM2.94914 2.51871C3.0958 2.79471 3.00333 3.14431 2.74259 3.29956L1.35944 4.12314C1.18888 4.2247 1.08333 4.41574 1.08333 4.62289L1.08333 6.16211C1.08333 6.47878 0.840821 6.73549 0.541667 6.73549C0.242513 6.73549 4.30478e-07 6.47878 4.73526e-07 6.16211L6.02669e-07 4.62289C6.45717e-07 4.00144 0.316652 3.42832 0.828326 3.12365L2.21148 2.30007C2.47221 2.14482 2.80248 2.24271 2.94914 2.51871ZM10.0509 2.51871C10.1975 2.24271 10.5278 2.14482 10.7885 2.30007L12.1717 3.12365C12.6833 3.42832 13 4.00144 13 4.62288V6.16211C13 6.47878 12.7575 6.73549 12.4583 6.73549C12.1592 6.73549 11.9167 6.47878 11.9167 6.16211V4.62288C11.9167 4.41574 11.8111 4.2247 11.6406 4.12314L10.2574 3.29956C9.99667 3.14431 9.9042 2.79471 10.0509 2.51871ZM12.4583 8.26451C12.7575 8.26451 13 8.52122 13 8.83789V10.3771C13 10.9986 12.6833 11.5717 12.1717 11.8763L10.7885 12.6999C10.5278 12.8552 10.1975 12.7573 10.0509 12.4813C9.9042 12.2053 9.99668 11.8557 10.2574 11.7004L11.6406 10.8769C11.8111 10.7753 11.9167 10.5843 11.9167 10.3771V8.83789C11.9167 8.52122 12.1592 8.26451 12.4583 8.26451ZM0.541667 8.26451C0.840821 8.26451 1.08333 8.52122 1.08333 8.83789L1.08333 10.3771C1.08333 10.5843 1.18888 10.7753 1.35944 10.8769L2.74259 11.7004C3.00332 11.8557 3.0958 12.2053 2.94913 12.4813C2.80247 12.7573 2.47221 12.8552 2.21147 12.6999L0.828326 11.8763C0.31665 11.5717 -4.30478e-08 10.9986 0 10.3771L1.29143e-07 8.83789C1.72191e-07 8.52122 0.242513 8.26451 0.541667 8.26451ZM4.20821 13.231C4.35488 12.955 4.68514 12.8571 4.94588 13.0123L6.23444 13.7796C6.39933 13.8778 6.60067 13.8778 6.76556 13.7796L8.05412 13.0123C8.31486 12.8571 8.64512 12.955 8.79179 13.231C8.93845 13.507 8.84598 13.8566 8.58524 14.0118L7.29667 14.7791C6.802 15.0736 6.198 15.0736 5.70333 14.7791L4.41476 14.0118C4.15402 13.8566 4.06155 13.507 4.20821 13.231Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M4.62357 6.35324C4.77315 6.07899 5.10443 5.98503 5.3635 6.14337L6.49997 6.83792L7.63643 6.14337C7.8955 5.98503 8.22678 6.07899 8.37636 6.35324C8.52594 6.62748 8.43717 6.97816 8.1781 7.13649L7.04164 7.83104V9.22014C7.04164 9.53681 6.79912 9.79353 6.49997 9.79353C6.20082 9.79353 5.9583 9.53681 5.9583 9.22014V7.83104L4.82184 7.13649C4.56276 6.97816 4.474 6.62748 4.62357 6.35324Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M6.49998 0.213281C6.79913 0.213281 7.04164 0.469993 7.04164 0.786663V3.10408C7.04164 3.42075 6.79913 3.67746 6.49998 3.67746C6.20082 3.67746 5.95831 3.42075 5.95831 3.10408V0.786663C5.95831 0.469993 6.20082 0.213281 6.49998 0.213281ZM0.349927 3.83504C0.499504 3.56079 0.830782 3.46683 1.08986 3.62516L3.02114 4.80548C3.28022 4.96381 3.36898 5.31449 3.21941 5.58873C3.06983 5.86297 2.73855 5.95694 2.47948 5.7986L0.548191 4.61829C0.289116 4.45995 0.20035 4.10928 0.349927 3.83504ZM12.65 3.83504C12.7996 4.10928 12.7108 4.45995 12.4518 4.61829L10.5205 5.7986C10.2614 5.95694 9.93012 5.86297 9.78055 5.58873C9.63097 5.31449 9.71974 4.96381 9.97881 4.80548L11.9101 3.62516C12.1692 3.46683 12.5004 3.56079 12.65 3.83504ZM3.24873 9.39335C3.3983 9.6676 3.30954 10.0183 3.05046 10.1766L1.08986 11.3748C0.830782 11.5332 0.499504 11.4392 0.349927 11.165C0.20035 10.8907 0.289115 10.54 0.548191 10.3817L2.50879 9.18348C2.76787 9.02515 3.09915 9.11911 3.24873 9.39335ZM9.75123 9.39335C9.90081 9.11911 10.2321 9.02515 10.4912 9.18348L12.4518 10.3817C12.7108 10.54 12.7996 10.8907 12.65 11.165C12.5004 11.4392 12.1692 11.5332 11.9101 11.3748L9.94949 10.1766C9.69042 10.0183 9.60165 9.6676 9.75123 9.39335ZM6.49998 11.2867C6.79913 11.2867 7.04164 11.5434 7.04164 11.8601V14.2133C7.04164 14.53 6.79913 14.7867 6.49998 14.7867C6.20082 14.7867 5.95831 14.53 5.95831 14.2133V11.8601C5.95831 11.5434 6.20082 11.2867 6.49998 11.2867Z" fill="currentColor"/>`}
|
|
1277
|
+
};
|
|
1278
|
+
ICONS.minimize = ICONS.chevron;
|
|
1279
|
+
ICONS.close = {vb:"0 0 16 16", svg:`<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`};
|
|
1280
|
+
function Ic({name, size=16}){
|
|
1281
|
+
const ic = ICONS[name];
|
|
1282
|
+
if(!ic) return null;
|
|
1283
|
+
// preserve the icon's intrinsic aspect ratio (e.g. play is 10×12, not square)
|
|
1284
|
+
const vb = ic.vb.split(/[ ,]+/).map(Number);
|
|
1285
|
+
const vw = vb[2] || size, vh = vb[3] || size, m = Math.max(vw, vh);
|
|
1286
|
+
const w = +(size * vw / m).toFixed(2), ht = +(size * vh / m).toFixed(2);
|
|
1287
|
+
return h("svg",{width:w,height:ht,viewBox:ic.vb,fill:"none",xmlns:"http://www.w3.org/2000/svg",
|
|
1288
|
+
style:{display:"block"},dangerouslySetInnerHTML:{__html:ic.svg}});
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function usePreviewTime(){
|
|
1292
|
+
const{preview}=useContext(TimelineCtx);
|
|
1293
|
+
const[ms,setMs]=useState(0);
|
|
1294
|
+
useEffect(()=>preview.onProgress(setMs),[preview]);
|
|
1295
|
+
return ms;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// portaled, origin-aware dropdown surface (transitions.dev menu-dropdown)
|
|
1299
|
+
function Dropdown({open,onClose,triggerRef,width,align,children}){
|
|
1300
|
+
const ref=useRef(null);
|
|
1301
|
+
const[render,setRender]=useState(open);
|
|
1302
|
+
const[shown,setShown]=useState(false); // .is-open
|
|
1303
|
+
const[closing,setClosing]=useState(false); // .is-closing
|
|
1304
|
+
const[pos,setPos]=useState(null);
|
|
1305
|
+
// mount on open; keep mounted through the close animation, then unmount
|
|
1306
|
+
useEffect(()=>{
|
|
1307
|
+
if(open){ setRender(true); setClosing(false); }
|
|
1308
|
+
else if(render){
|
|
1309
|
+
setShown(false); setClosing(true);
|
|
1310
|
+
const t=setTimeout(()=>{ setRender(false); setClosing(false); },CLOSE_MS);
|
|
1311
|
+
return()=>clearTimeout(t);
|
|
1312
|
+
}
|
|
1313
|
+
},[open,render]);
|
|
1314
|
+
// measure trigger + viewport to position the surface (origin-aware)
|
|
1315
|
+
useLayoutEffect(()=>{
|
|
1316
|
+
if(!render||!triggerRef.current)return;
|
|
1317
|
+
const r=triggerRef.current.getBoundingClientRect();
|
|
1318
|
+
const PAD=8, GAP=6, MIN_H=140, PREF_H=280;
|
|
1319
|
+
const vw=window.innerWidth;
|
|
1320
|
+
const vh=window.innerHeight;
|
|
1321
|
+
const w=Math.min(width||r.width,Math.max(120,vw-(PAD*2)));
|
|
1322
|
+
const below=Math.max(0,vh-r.bottom-GAP-PAD);
|
|
1323
|
+
const above=Math.max(0,r.top-GAP-PAD);
|
|
1324
|
+
const openUp=(below<PREF_H)&&above>below;
|
|
1325
|
+
const maxH=Math.max(MIN_H,Math.floor(openUp?above:below));
|
|
1326
|
+
const right=align==="right";
|
|
1327
|
+
const style={position:"fixed",width:w,maxHeight:maxH,overflowY:"auto",overscrollBehavior:"contain",zIndex:100000};
|
|
1328
|
+
const rawLeft=right?(r.right-w):r.left;
|
|
1329
|
+
style.left=Math.min(Math.max(PAD,rawLeft),Math.max(PAD,vw-PAD-w));
|
|
1330
|
+
if(openUp)style.bottom=(vh-r.top)+6; else style.top=r.bottom+6;
|
|
1331
|
+
setPos({style,origin:(openUp?"bottom":"top")+"-"+(right?"right":"left")});
|
|
1332
|
+
},[render,open,align,width,triggerRef]);
|
|
1333
|
+
// play the enter transition: once the surface is mounted + positioned in
|
|
1334
|
+
// its closed state, force a reflow to commit that "from" style, then add
|
|
1335
|
+
// .is-open. Forcing the reflow (instead of relying on rAF, which throttles)
|
|
1336
|
+
// guarantees the transitions.dev scale/opacity transition runs.
|
|
1337
|
+
useLayoutEffect(()=>{
|
|
1338
|
+
if(render&&pos&&open&&!shown&&!closing&&ref.current){
|
|
1339
|
+
void ref.current.offsetWidth;
|
|
1340
|
+
setShown(true);
|
|
1341
|
+
}
|
|
1342
|
+
},[render,pos,open,shown,closing]);
|
|
1343
|
+
useEffect(()=>{
|
|
1344
|
+
if(!open)return;
|
|
1345
|
+
const handler=e=>{
|
|
1346
|
+
if(ref.current&&ref.current.contains(e.target))return;
|
|
1347
|
+
if(triggerRef.current&&triggerRef.current.contains(e.target))return;
|
|
1348
|
+
onClose();
|
|
1349
|
+
};
|
|
1350
|
+
document.addEventListener("mousedown",handler);
|
|
1351
|
+
return()=>document.removeEventListener("mousedown",handler);
|
|
1352
|
+
},[open,onClose,triggerRef]);
|
|
1353
|
+
if(!render)return null;
|
|
1354
|
+
// portal into the React root container (not document.body) so React's
|
|
1355
|
+
// event delegation still receives clicks on menu items.
|
|
1356
|
+
const target=document.getElementById("root")||document.body;
|
|
1357
|
+
// Before pos is measured, mount off-screen at opacity 0 (closed state) so
|
|
1358
|
+
// the element paints its pre-open frame and the open transition can run.
|
|
1359
|
+
const style=pos?pos.style:{position:"fixed",left:-9999,top:0,width:(width||200),zIndex:100000};
|
|
1360
|
+
const origin=pos?pos.origin:"top-left";
|
|
1361
|
+
const cls=closing?"is-closing":shown?"is-open":"";
|
|
1362
|
+
return createPortal(
|
|
1363
|
+
h("div",{ref,className:cx("t-dropdown","tl-menu",cls),"data-origin":origin,style},children),
|
|
1364
|
+
target);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function MenuItem({active,disabled,onClick,right,children}){
|
|
1368
|
+
return h("div",{className:cx("tl-menu-item",disabled&&"disabled"),onClick:disabled?undefined:onClick},
|
|
1369
|
+
h("span",{className:"tl-menu-item-label"},children),
|
|
1370
|
+
right);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// ── refine relay client ──
|
|
1374
|
+
// Talks to the local relay (server/relay.mjs). The browser posts a job and
|
|
1375
|
+
// polls; the relay answers each job with one agent run (one-shot).
|
|
1376
|
+
const RELAY_URL = (window.REFINE_RELAY_URL) || "http://localhost:7331";
|
|
1377
|
+
async function relayCreateJob(request){
|
|
1378
|
+
const r = await fetch(RELAY_URL+"/jobs",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({request})});
|
|
1379
|
+
if(!r.ok) throw new Error("relay POST /jobs failed ("+r.status+")");
|
|
1380
|
+
return r.json(); // { id, status }
|
|
1381
|
+
}
|
|
1382
|
+
async function relayGetJob(id){
|
|
1383
|
+
const r = await fetch(RELAY_URL+"/jobs/"+id);
|
|
1384
|
+
if(!r.ok) throw new Error("relay GET /jobs/:id failed ("+r.status+")");
|
|
1385
|
+
return r.json();
|
|
1386
|
+
}
|
|
1387
|
+
async function relayHealth(){
|
|
1388
|
+
const r = await fetch(RELAY_URL+"/health");
|
|
1389
|
+
if(!r.ok) throw new Error("relay /health failed ("+r.status+")");
|
|
1390
|
+
return r.json(); // { ok, auto, llmAvailable, jobs }
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const REFINE_MODES=[
|
|
1394
|
+
{key:"llm",label:"Agent",desc:"Suggested edits from the agent using the Transitions.dev skill."},
|
|
1395
|
+
{key:"deterministic",label:"Deterministic",desc:"Suggests edits only mathematically, with no credit usage."},
|
|
1396
|
+
];
|
|
1397
|
+
const REFINE_TYPES=[
|
|
1398
|
+
{key:"small",label:"Small refinements",desc:"The agent will scan the transition and suggest small refinements by adjusting motion tokens using the Transitions.dev skill."},
|
|
1399
|
+
{key:"replace",label:"Replace transition",desc:"The agent will scan the transitions and suggest replacements from the Transitions.dev library and skill."},
|
|
1400
|
+
];
|
|
1401
|
+
const REFINE_STATUS=["Scanning the selected transition","Reading the Transitions.dev skill","Suggesting edits"];
|
|
1402
|
+
// Loading status copy: transitions.dev shimmer text (15) with a text states
|
|
1403
|
+
// swap (04) between messages — old line exits up + blurs, new enters from below.
|
|
1404
|
+
// JS owns textContent/data-text so React never clobbers the staged swap.
|
|
1405
|
+
function ShimmerSwapText({text}){
|
|
1406
|
+
const ref=useRef(null);
|
|
1407
|
+
const cur=useRef(undefined);
|
|
1408
|
+
const setEl=useCallback((node)=>{
|
|
1409
|
+
ref.current=node;
|
|
1410
|
+
if(node&&cur.current===undefined){node.textContent=text;node.setAttribute("data-text",text);cur.current=text;}
|
|
1411
|
+
},[]);
|
|
1412
|
+
useEffect(()=>{
|
|
1413
|
+
const el=ref.current;if(!el)return;
|
|
1414
|
+
if(cur.current===text)return;
|
|
1415
|
+
const dur=parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--text-swap-dur"))||150;
|
|
1416
|
+
el.classList.add("is-exit");
|
|
1417
|
+
const t=setTimeout(()=>{
|
|
1418
|
+
el.textContent=text;el.setAttribute("data-text",text);
|
|
1419
|
+
el.classList.remove("is-exit");el.classList.add("is-enter-start");
|
|
1420
|
+
void el.offsetHeight; // reflow so the new line animates back to rest
|
|
1421
|
+
el.classList.remove("is-enter-start");
|
|
1422
|
+
cur.current=text;
|
|
1423
|
+
},dur);
|
|
1424
|
+
return()=>clearTimeout(t);
|
|
1425
|
+
},[text]);
|
|
1426
|
+
return h("span",{ref:setEl,className:"tl-refine-status-text t-shimmer t-text-swap"});
|
|
1427
|
+
}
|
|
1428
|
+
// Results entrance — transitions.dev texts reveal (18): the summary and each
|
|
1429
|
+
// suggestion card are stagger lines that rise + unblur in sequence on mount.
|
|
1430
|
+
function RefineResults({summary,suggestions,appliedIds,onApply}){
|
|
1431
|
+
const ref=useRef(null);
|
|
1432
|
+
useEffect(()=>{
|
|
1433
|
+
const el=ref.current;if(!el)return;
|
|
1434
|
+
el.classList.remove("is-shown");
|
|
1435
|
+
void el.offsetHeight; // reflow from the start state before playing
|
|
1436
|
+
el.classList.add("is-shown");
|
|
1437
|
+
},[]);
|
|
1438
|
+
const delay=(i)=>({transitionDelay:"calc(var(--stagger-stagger) * "+i+")"});
|
|
1439
|
+
let i=0;
|
|
1440
|
+
return h("div",{ref,className:"tl-refine-results t-stagger"},
|
|
1441
|
+
summary&&h("div",{className:"tl-refine-summary t-stagger-line",style:delay(i++)},summary),
|
|
1442
|
+
suggestions.map(s=>{
|
|
1443
|
+
const applied=!!appliedIds[s.id];
|
|
1444
|
+
return h("div",{className:"tl-sug t-stagger-line",key:s.id,style:delay(i++)},
|
|
1445
|
+
h("div",{className:"tl-sug-top"},
|
|
1446
|
+
h("span",{className:"tl-sug-kind"},s.kind||"tweak"),
|
|
1447
|
+
s.property&&h("span",{className:"tl-sug-prop"},s.property)),
|
|
1448
|
+
h("div",{className:"tl-sug-title"},s.title),
|
|
1449
|
+
h("div",{className:"tl-sug-delta"},
|
|
1450
|
+
s.from&&h("span",{className:"tl-sug-from"},s.from),
|
|
1451
|
+
s.from&&h("span",{className:"tl-sug-arrow"},"\u2192"),
|
|
1452
|
+
h("span",{className:"tl-sug-to"},s.to)),
|
|
1453
|
+
s.reason&&h("div",{className:"tl-sug-reason"},s.reason),
|
|
1454
|
+
applied
|
|
1455
|
+
? h("button",{className:"tl-sug-apply is-applied",disabled:true},h(Ic,{name:"check",size:13}),"Applied")
|
|
1456
|
+
: h("button",{className:"tl-sug-apply",onClick:()=>onApply(s)},"Apply"));
|
|
1457
|
+
}));
|
|
1458
|
+
}
|
|
1459
|
+
function RefinePanel({open,onClose,phase,label,refineType,onType,suggestions,summary,error,appliedIds,onApply,onApplyAll,mode,onMode,llmAvailable,cliInstalled,onStart}){
|
|
1460
|
+
// mount-on-open; keep mounted through the panel-reveal slide-out, then unmount
|
|
1461
|
+
const[render,setRender]=useState(open);
|
|
1462
|
+
const[panelOpen,setPanelOpen]=useState(false);
|
|
1463
|
+
const[slidePhase,setSlidePhase]=useState("opening");
|
|
1464
|
+
const[modeOpen,setModeOpen]=useState(false);
|
|
1465
|
+
const[statusIx,setStatusIx]=useState(0);
|
|
1466
|
+
const modeRef=useRef(null);
|
|
1467
|
+
useEffect(()=>{
|
|
1468
|
+
let raf, to;
|
|
1469
|
+
if(open){
|
|
1470
|
+
setRender(true);
|
|
1471
|
+
setSlidePhase("opening");
|
|
1472
|
+
raf=requestAnimationFrame(()=>{raf=requestAnimationFrame(()=>setPanelOpen(true));});
|
|
1473
|
+
return()=>{if(raf)cancelAnimationFrame(raf);};
|
|
1474
|
+
}
|
|
1475
|
+
if(render){
|
|
1476
|
+
setSlidePhase("closing");
|
|
1477
|
+
setPanelOpen(false);
|
|
1478
|
+
const closeMs=parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--refine-close-dur"))||350;
|
|
1479
|
+
to=setTimeout(()=>setRender(false),closeMs+80);
|
|
1480
|
+
return()=>clearTimeout(to);
|
|
1481
|
+
}
|
|
1482
|
+
},[open,render]);
|
|
1483
|
+
// close on Escape — dismiss the mode dropdown first if it's open
|
|
1484
|
+
useEffect(()=>{
|
|
1485
|
+
if(!open)return;
|
|
1486
|
+
const onKey=e=>{if(e.key!=="Escape")return;if(modeOpen){setModeOpen(false);return;}onClose();};
|
|
1487
|
+
window.addEventListener("keydown",onKey);
|
|
1488
|
+
return()=>window.removeEventListener("keydown",onKey);
|
|
1489
|
+
},[open,onClose,modeOpen]);
|
|
1490
|
+
// cycle the in-progress status copy while scanning (matches the design's
|
|
1491
|
+
// "Scanning… → Reading the Transitions.dev skill → Suggesting edits").
|
|
1492
|
+
useEffect(()=>{
|
|
1493
|
+
if(phase!=="scanning"){setStatusIx(0);return;}
|
|
1494
|
+
const id=setInterval(()=>setStatusIx(i=>(i+1)%REFINE_STATUS.length),2800);
|
|
1495
|
+
return()=>clearInterval(id);
|
|
1496
|
+
},[phase]);
|
|
1497
|
+
if(!render)return null;
|
|
1498
|
+
|
|
1499
|
+
const pending = suggestions.filter(s=>!appliedIds[s.id]);
|
|
1500
|
+
const agentMode = mode==="llm";
|
|
1501
|
+
// null (unknown / still probing) is treated as ready so the panel doesn't
|
|
1502
|
+
// flash the "unavailable" copy before /health resolves.
|
|
1503
|
+
const agentReady = !agentMode || llmAvailable!==false;
|
|
1504
|
+
const typeDesc = (REFINE_TYPES.find(t=>t.key===refineType)||REFINE_TYPES[0]).desc;
|
|
1505
|
+
const modeLabel = (REFINE_MODES.find(m=>m.key===mode)||REFINE_MODES[0]).label;
|
|
1506
|
+
// One persistent control for the whole foot: in idle/done/error it's the
|
|
1507
|
+
// pill button; while scanning it carries `.is-scanning` and the same DOM
|
|
1508
|
+
// node morphs (card resize) into the loading rectangle with the border-beam.
|
|
1509
|
+
const scanning = phase==="scanning";
|
|
1510
|
+
const startBtn=(o)=>{o=o||{};
|
|
1511
|
+
// Both faces stay mounted and stacked; the morph cross-blurs (icon swap)
|
|
1512
|
+
// between the label and the icon+status instead of swapping the subtree.
|
|
1513
|
+
return h("button",{className:cx("tl-scan-morph","t-resize",scanning&&"is-scanning"),
|
|
1514
|
+
disabled:(!!o.disabled)||scanning,"aria-busy":scanning?"true":undefined,
|
|
1515
|
+
onClick:(o.disabled||scanning)?undefined:onStart},
|
|
1516
|
+
scanning&&h(BorderBeam,{size:"md",colorVariant:"ocean",theme:"light",borderRadius:10,duration:2.6,saturation:2,brightness:1.6,className:"tl-scan-beam"},
|
|
1517
|
+
h("span",{className:"tl-scan-beam-fill"})),
|
|
1518
|
+
h("span",{className:"tl-scan-content"},
|
|
1519
|
+
h("span",{className:"tl-scan-face tl-scan-face-label","aria-hidden":scanning?"true":undefined},
|
|
1520
|
+
h("span",{className:"tl-scan-label"},o.label||"Start scanning")),
|
|
1521
|
+
h("span",{className:"tl-scan-face tl-scan-face-status","aria-hidden":scanning?undefined:"true"},
|
|
1522
|
+
h("span",{className:"tl-scan-ic","aria-hidden":"true"},h(Ic,{name:"scan",size:15})),
|
|
1523
|
+
h(ShimmerSwapText,{text:REFINE_STATUS[statusIx]}))));
|
|
1524
|
+
};
|
|
1525
|
+
|
|
1526
|
+
let body, foot;
|
|
1527
|
+
if(phase==="scanning"){
|
|
1528
|
+
body = h("div",{className:"tl-refine-center"});
|
|
1529
|
+
foot = startBtn();
|
|
1530
|
+
} else if(phase==="error"){
|
|
1531
|
+
body = h("div",{className:"tl-refine-center"},
|
|
1532
|
+
h("div",{className:"tl-refine-unavail-title"},"Something went wrong."),
|
|
1533
|
+
h("div",{className:"tl-refine-unavail-text"},error||"The agent reported an error."));
|
|
1534
|
+
foot = startBtn({label:"Try again"});
|
|
1535
|
+
} else if(phase==="done"){
|
|
1536
|
+
if(suggestions.length===0){
|
|
1537
|
+
const emptyMsg = refineType==="replace"
|
|
1538
|
+
? "I didn't find any transition that would be a good fit as a replacement."
|
|
1539
|
+
: "Already aligned to the transitions.dev motion tokens. Nothing to refine.";
|
|
1540
|
+
body = h("div",{className:"tl-refine-center"},
|
|
1541
|
+
h("p",{className:"tl-refine-idle-text"},emptyMsg));
|
|
1542
|
+
foot = startBtn({label:"Scan again"});
|
|
1543
|
+
} else {
|
|
1544
|
+
body = h(RefineResults,{summary,suggestions,appliedIds,onApply});
|
|
1545
|
+
foot = pending.length>1
|
|
1546
|
+
? h("button",{className:"pc-btn primary",style:{flex:1},onClick:onApplyAll},"Apply all ("+pending.length+")")
|
|
1547
|
+
: startBtn({label:"Scan again"});
|
|
1548
|
+
}
|
|
1549
|
+
} else { // idle
|
|
1550
|
+
if(!agentReady){
|
|
1551
|
+
const cliMissing = cliInstalled===false;
|
|
1552
|
+
body = h("div",{className:"tl-refine-center"},
|
|
1553
|
+
h("div",{className:"tl-refine-unavail-title"},"Agent unavailable."),
|
|
1554
|
+
cliMissing
|
|
1555
|
+
? h("p",{className:"tl-refine-unavail-text"},"Install Cursor CLI by ",h("code",{className:"tl-code"},"/refine live")," command in your agent to enable live functionality.")
|
|
1556
|
+
: h("p",{className:"tl-refine-unavail-text"},"Run the ",h("code",{className:"tl-code"},"/refine live")," command in your agent to enable live functionality."));
|
|
1557
|
+
foot = startBtn({disabled:true});
|
|
1558
|
+
} else {
|
|
1559
|
+
body = h("div",{className:"tl-refine-center"},
|
|
1560
|
+
h("p",{className:"tl-refine-idle-text"},typeDesc));
|
|
1561
|
+
foot = startBtn({});
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
return h("div",{className:"tl-refine-panel","data-open":panelOpen?"true":"false","data-phase":slidePhase,
|
|
1566
|
+
role:"dialog","aria-label":"Refine transition"},
|
|
1567
|
+
h("div",{className:"tl-refine-inner"},
|
|
1568
|
+
h("div",{className:"tl-refine-head"},
|
|
1569
|
+
h("div",{className:"tl-refine-titles"},
|
|
1570
|
+
h("h3",null,"Refine"),
|
|
1571
|
+
h("p",null,label?("Tuning "+label):"Transitions review")),
|
|
1572
|
+
h("div",{className:"tl-refine-actions"},
|
|
1573
|
+
h("button",{ref:modeRef,className:cx("tl-refine-mode",modeOpen&&"is-open"),
|
|
1574
|
+
"aria-haspopup":"menu","aria-expanded":modeOpen?"true":"false",onClick:()=>setModeOpen(v=>!v)},
|
|
1575
|
+
h("span",null,modeLabel),
|
|
1576
|
+
h(Ic,{name:"chevron",size:16})),
|
|
1577
|
+
h("button",{className:"tl-refine-close","aria-label":"Close refine panel",onClick:onClose},
|
|
1578
|
+
h(Ic,{name:"close",size:16})))),
|
|
1579
|
+
h(Dropdown,{open:modeOpen,onClose:()=>setModeOpen(false),triggerRef:modeRef,width:276,align:"right"},
|
|
1580
|
+
REFINE_MODES.map(m=>h("button",{key:m.key,className:"tl-mode-row",
|
|
1581
|
+
onClick:()=>{if(m.key!==mode)onMode(m.key);setModeOpen(false);}},
|
|
1582
|
+
h("div",{className:"tl-mode-row-main"},
|
|
1583
|
+
h("div",{className:"tl-mode-row-title"},m.label),
|
|
1584
|
+
h("div",{className:"tl-mode-row-desc"},m.desc)),
|
|
1585
|
+
(mode===m.key)&&h("span",{className:"tl-mode-row-check"},h(Ic,{name:"check",size:16}))))),
|
|
1586
|
+
h("div",{className:"tl-refine-tabs",role:"tablist","aria-label":"Refinement type"},
|
|
1587
|
+
REFINE_TYPES.map(t=>h("button",{key:t.key,className:"tl-refine-tab",role:"tab",
|
|
1588
|
+
"aria-selected":refineType===t.key?"true":"false",
|
|
1589
|
+
onClick:()=>{if(t.key!==refineType)onType(t.key);}},t.label))),
|
|
1590
|
+
h("div",{className:"tl-refine-body"},body),
|
|
1591
|
+
h("div",{className:"tl-refine-foot"},foot)));
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function Header({entries,active,onSelect,onReset,onCopy,copied,loop,setLoop,snap,setSnap,onMinimize,onRefine,refineActive}){
|
|
1595
|
+
const[pick,setPick]=useState(false);
|
|
1596
|
+
const[setg,setSetg]=useState(false);
|
|
1597
|
+
const pickRef=useRef(null), gearRef=useRef(null);
|
|
1598
|
+
return h("div",{className:"tl-header"},
|
|
1599
|
+
h("span",{className:"tl-header-label"},"Selected"),
|
|
1600
|
+
h("button",{ref:pickRef,className:cx("tl-ghost-btn",pick&&"is-active"),disabled:!active,onClick:()=>setPick(v=>!v)},
|
|
1601
|
+
active?h(React.Fragment,null,h("span",null,active.label),h("span",{className:"tl-dim"}," "+active.durationMs+"ms"))
|
|
1602
|
+
:h("span",{className:"tl-dim"},"None"),
|
|
1603
|
+
h("span",{className:"tl-ghost-chev"},h(Ic,{name:"chevron"}))),
|
|
1604
|
+
h(Dropdown,{open:pick,onClose:()=>setPick(false),triggerRef:pickRef,width:Math.max(240,(pickRef.current&&pickRef.current.offsetWidth)||240),align:"left"},
|
|
1605
|
+
entries.length===0
|
|
1606
|
+
? h("div",{className:"tl-menu-empty"},"No transitions found")
|
|
1607
|
+
: entries.map(e=>h(MenuItem,{key:e.id,active:active&&e.id===active.id,onClick:()=>{onSelect(e.id);setPick(false);},
|
|
1608
|
+
right:active&&e.id===active.id&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},
|
|
1609
|
+
h("span",null,e.label,h("span",{className:"tl-menu-dim"}," "+e.durationMs+"ms")))) ),
|
|
1610
|
+
h("span",{className:"tl-header-count"},entries.length+" transition"+(entries.length===1?"":"s")+" found"),
|
|
1611
|
+
h("button",{ref:gearRef,className:cx("tl-icon-btn",setg&&"is-active"),title:"Settings",onClick:()=>setSetg(v=>!v)},h(Ic,{name:"gear"})),
|
|
1612
|
+
h(Dropdown,{open:setg,onClose:()=>setSetg(false),triggerRef:gearRef,width:210,align:"right"},
|
|
1613
|
+
h(MenuItem,{onClick:()=>setLoop(v=>!v),right:loop&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},"Loop playback"),
|
|
1614
|
+
h(MenuItem,{onClick:()=>setSnap(v=>!v),right:snap&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},"Snap to grid")),
|
|
1615
|
+
h("span",{className:"t-tt-wrap"},
|
|
1616
|
+
h("button",{className:"tl-sec-btn t-tt-trigger",disabled:!active,onClick:onReset},"Reset"),
|
|
1617
|
+
h("span",{className:"t-tt tl-tt-below",role:"tooltip"},"Reset values")),
|
|
1618
|
+
h("span",{className:"t-tt-wrap"},
|
|
1619
|
+
h("button",{className:"tl-icon-btn t-tt-trigger",disabled:!active,"aria-label":"Copy values",onClick:onCopy},
|
|
1620
|
+
h("span",{className:"t-icon-swap","data-state":copied?"b":"a"},
|
|
1621
|
+
h("span",{className:"t-icon","data-icon":"a"},h(Ic,{name:"copy"})),
|
|
1622
|
+
h("span",{className:"t-icon","data-icon":"b"},h(Ic,{name:"check"})))),
|
|
1623
|
+
h("span",{className:"t-tt tl-tt-below",role:"tooltip"},copied?"Copied":"Copy values")),
|
|
1624
|
+
h("button",{className:cx("tl-refine-btn",refineActive&&"is-active"),disabled:!active,onClick:onRefine},
|
|
1625
|
+
h(Ic,{name:"wand"}),
|
|
1626
|
+
h("span",{className:"tl-refine-sparks","aria-hidden":"true"},
|
|
1627
|
+
h("i",{style:{"--ox":"-1px","--oy":"-3px","--sx":"0px","--sy":"-11px","--sd":"900ms","--sdelay":"0ms"}}),
|
|
1628
|
+
h("i",{style:{"--ox":"2px","--oy":"-2px","--sx":"9px","--sy":"-9px","--sd":"1000ms","--sdelay":"120ms"}}),
|
|
1629
|
+
h("i",{style:{"--ox":"2px","--oy":"5px","--sx":"9px","--sy":"9px","--sd":"950ms","--sdelay":"240ms"}}),
|
|
1630
|
+
h("i",{style:{"--ox":"-5px","--oy":"-2px","--sx":"-9px","--sy":"-9px","--sd":"1000ms","--sdelay":"360ms"}}),
|
|
1631
|
+
h("i",{style:{"--ox":"3px","--oy":"1px","--sx":"12px","--sy":"1px","--sd":"920ms","--sdelay":"480ms"}}),
|
|
1632
|
+
h("i",{style:{"--ox":"-1px","--oy":"-3px","--sx":"0px","--sy":"-11px","--sd":"950ms","--sdelay":"520ms"}}),
|
|
1633
|
+
h("i",{style:{"--ox":"2px","--oy":"-2px","--sx":"9px","--sy":"-9px","--sd":"900ms","--sdelay":"650ms"}}),
|
|
1634
|
+
h("i",{style:{"--ox":"2px","--oy":"5px","--sx":"9px","--sy":"9px","--sd":"1000ms","--sdelay":"760ms"}}),
|
|
1635
|
+
h("i",{style:{"--ox":"-5px","--oy":"-2px","--sx":"-9px","--sy":"-9px","--sd":"930ms","--sdelay":"180ms"}}),
|
|
1636
|
+
h("i",{style:{"--ox":"3px","--oy":"1px","--sx":"12px","--sy":"1px","--sd":"980ms","--sdelay":"300ms"}})),
|
|
1637
|
+
h("span",null,"Refine")),
|
|
1638
|
+
h("button",{className:"tl-icon-btn ghost",title:"Minimize",onClick:onMinimize},h(Ic,{name:"minimize"})),
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
function PropTrack({property, delayMs, durationMs, selected, dragging, onSelect, onReorder, onDelayChange, onDurationChange, snap, scaleMs, lockDuration}){
|
|
1643
|
+
const trackRef=useRef(null);
|
|
1644
|
+
const grid = snap ? 25 : 1;
|
|
1645
|
+
const pxToMs=useCallback(px=>{if(!trackRef.current)return 0;const w=trackRef.current.getBoundingClientRect().width;return Math.round((px/w)*scaleMs/grid)*grid;},[grid,scaleMs]);
|
|
1646
|
+
|
|
1647
|
+
const startDrag=useCallback((mode,e)=>{
|
|
1648
|
+
e.preventDefault(); e.stopPropagation();
|
|
1649
|
+
onSelect();
|
|
1650
|
+
// a spring controls its own duration, so its bar can be moved (delay) but not resized
|
|
1651
|
+
if(lockDuration && mode!=="move") return;
|
|
1652
|
+
const startX=e.clientX; const sd=delayMs; const sdur=durationMs;
|
|
1653
|
+
const onMove=e2=>{
|
|
1654
|
+
const dx=e2.clientX-startX; const dMs=pxToMs(dx);
|
|
1655
|
+
if(mode==="move"){
|
|
1656
|
+
onDelayChange(Math.max(0,Math.min(sd+dMs,scaleMs-sdur)));
|
|
1657
|
+
} else if(mode==="left"){
|
|
1658
|
+
const rawDel=sd+dMs; const clampedDel=Math.max(0,Math.min(rawDel,sd+sdur-grid));
|
|
1659
|
+
onDelayChange(clampedDel); onDurationChange(sdur-(clampedDel-sd));
|
|
1660
|
+
} else {
|
|
1661
|
+
onDurationChange(Math.max(grid,Math.min(sdur+dMs,scaleMs-sd)));
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
const onUp=()=>{window.removeEventListener("mousemove",onMove);window.removeEventListener("mouseup",onUp);};
|
|
1665
|
+
window.addEventListener("mousemove",onMove);window.addEventListener("mouseup",onUp);
|
|
1666
|
+
},[delayMs,durationMs,pxToMs,grid,scaleMs,onSelect,onDelayChange,onDurationChange,lockDuration]);
|
|
1667
|
+
|
|
1668
|
+
const delPct=(delayMs/scaleMs)*100; const durPct=(durationMs/scaleMs)*100;
|
|
1669
|
+
return h("div",{className:cx("tl-prop-row",selected&&"selected",dragging&&"reordering",lockDuration&&"is-spring"),onClick:onSelect},
|
|
1670
|
+
h("div",{className:"tl-prop-head"},
|
|
1671
|
+
h("span",{className:"tl-prop-grip",title:"Drag to reorder",onMouseDown:onReorder},h(Ic,{name:"dots"})),
|
|
1672
|
+
h("span",{className:"tl-prop-label"},property)),
|
|
1673
|
+
h("div",{className:"tl-prop-track",ref:trackRef},
|
|
1674
|
+
h("div",{className:"tl-bar",style:{left:delPct+"%",width:durPct+"%"},onMouseDown:e=>startDrag("move",e),
|
|
1675
|
+
title:lockDuration?"Spring duration is derived \u2014 drag to move, resize is locked":undefined},
|
|
1676
|
+
h("span",{className:"tl-bar-handle left"}),
|
|
1677
|
+
h("span",{className:"tl-bar-handle right"}),
|
|
1678
|
+
!lockDuration&&h("div",{className:"tl-bar-grip left",onMouseDown:e=>startDrag("left",e)}),
|
|
1679
|
+
!lockDuration&&h("div",{className:"tl-bar-grip right",onMouseDown:e=>startDrag("right",e)}),
|
|
1680
|
+
),
|
|
1681
|
+
),
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
function ValueField({label, value, min, max, step, tokens, onChange, snap, readOnly, readOnlyHint}){
|
|
1686
|
+
const fieldRef = useRef(null);
|
|
1687
|
+
const trackRef = useRef(null);
|
|
1688
|
+
const inputRef = useRef(null);
|
|
1689
|
+
const[editing,setEditing]=useState(false);
|
|
1690
|
+
const[draft,setDraft]=useState("");
|
|
1691
|
+
const[dragging,setDragging]=useState(false);
|
|
1692
|
+
const[menu,setMenu]=useState(false);
|
|
1693
|
+
const pct = Math.min(Math.max((value - min) / (max - min), 0), 1) * 100;
|
|
1694
|
+
const grid = snap ? step : Math.max(1, Math.round(step/5));
|
|
1695
|
+
const selectAll = useCallback(e=>{
|
|
1696
|
+
const el = e && e.target ? e.target : inputRef.current;
|
|
1697
|
+
if(!el) return;
|
|
1698
|
+
// defer one tick so selection wins over browser caret placement
|
|
1699
|
+
requestAnimationFrame(()=>el.select());
|
|
1700
|
+
},[]);
|
|
1701
|
+
|
|
1702
|
+
const setFromX = useCallback(clientX => {
|
|
1703
|
+
if(!trackRef.current) return;
|
|
1704
|
+
const rect = trackRef.current.getBoundingClientRect();
|
|
1705
|
+
const ratio = Math.max(0, Math.min((clientX - rect.left) / rect.width, 1));
|
|
1706
|
+
const raw = min + ratio * (max - min);
|
|
1707
|
+
const snapped = Math.round(raw / grid) * grid;
|
|
1708
|
+
onChange(Math.max(min, Math.min(max, snapped)));
|
|
1709
|
+
},[min,max,grid,onChange]);
|
|
1710
|
+
|
|
1711
|
+
const startDrag = useCallback(e=>{
|
|
1712
|
+
if(editing || readOnly) return;
|
|
1713
|
+
e.preventDefault();
|
|
1714
|
+
setDragging(true);
|
|
1715
|
+
setFromX(e.clientX);
|
|
1716
|
+
const onMove = e2 => setFromX(e2.clientX);
|
|
1717
|
+
const onUp = () => { setDragging(false); window.removeEventListener("mousemove",onMove); window.removeEventListener("mouseup",onUp); };
|
|
1718
|
+
window.addEventListener("mousemove",onMove);
|
|
1719
|
+
window.addEventListener("mouseup",onUp);
|
|
1720
|
+
},[setFromX,editing]);
|
|
1721
|
+
|
|
1722
|
+
const beginEdit = useCallback(e=>{ e.stopPropagation(); if(readOnly) return; setDraft(String(value)); setEditing(true); },[value,readOnly]);
|
|
1723
|
+
const commit = useCallback(()=>{ const n=parseFloat(draft); if(!isNaN(n)) onChange(Math.max(min,Math.min(max,Math.round(n)))); setEditing(false); },[draft,min,max,onChange]);
|
|
1724
|
+
|
|
1725
|
+
const chevRef=useRef(null);
|
|
1726
|
+
const wrapRef=useRef(null);
|
|
1727
|
+
return h("div",{ref:wrapRef,className:cx("tl-field-wrap",readOnly&&"is-locked")},
|
|
1728
|
+
h("div",{ref:fieldRef,className:cx("tl-field",dragging&&"is-dragging",editing&&"is-editing",readOnly&&"is-readonly")},
|
|
1729
|
+
h("div",{className:"tl-field-fill",style:{width:pct+"%"}},
|
|
1730
|
+
!readOnly&&h("div",{className:"tl-field-thumb"}),
|
|
1731
|
+
),
|
|
1732
|
+
h("div",{ref:trackRef,className:"tl-field-track",onMouseDown:startDrag}),
|
|
1733
|
+
h("span",{className:"tl-field-label"},label),
|
|
1734
|
+
editing
|
|
1735
|
+
? h("input",{ref:inputRef,className:"tl-field-input",autoFocus:true,value:draft,
|
|
1736
|
+
onChange:e=>setDraft(e.target.value),onBlur:commit,
|
|
1737
|
+
onFocus:selectAll,
|
|
1738
|
+
onKeyDown:e=>{if(e.key==="Enter")commit();if(e.key==="Escape")setEditing(false);}})
|
|
1739
|
+
: h("span",{className:cx("tl-field-value",readOnly&&"is-readonly"),style:readOnly?{right:"30px"}:null,
|
|
1740
|
+
onMouseDown:e=>e.stopPropagation(),onClick:beginEdit},value),
|
|
1741
|
+
readOnly&&h("span",{className:"tl-field-lock"},h(Ic,{name:"lock",size:13})),
|
|
1742
|
+
),
|
|
1743
|
+
!readOnly&&h("button",{ref:chevRef,className:"tl-field-chevron",onClick:()=>setMenu(v=>!v)},h(Ic,{name:"chevron"})),
|
|
1744
|
+
!readOnly&&h(Dropdown,{open:menu,onClose:()=>setMenu(false),triggerRef:chevRef,
|
|
1745
|
+
width:(wrapRef.current&&wrapRef.current.offsetWidth)||220,align:"right"},
|
|
1746
|
+
(tokens||[]).map(tk=>h(MenuItem,{key:tk.label,active:value===tk.ms,onClick:()=>{onChange(tk.ms);setMenu(false);},
|
|
1747
|
+
right:value===tk.ms&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},
|
|
1748
|
+
h("span",{className:"tl-menu-text"},tk.label,h("span",{className:"tl-menu-dim"}," "+tk.ms+"ms")),
|
|
1749
|
+
tk.usage&&h("span",{className:"t-tt-wrap tl-menu-help",
|
|
1750
|
+
onClick:e=>e.stopPropagation(),onMouseDown:e=>e.stopPropagation()},
|
|
1751
|
+
h(Ic,{name:"help",size:13}),
|
|
1752
|
+
h("span",{className:"t-tt tl-tt-usage",role:"tooltip"},tk.usage)))) ),
|
|
1753
|
+
readOnly&&readOnlyHint&&h("span",{className:"t-tt",role:"tooltip"},readOnlyHint),
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// shared plot geometry for both the easing curve and the spring curve.
|
|
1758
|
+
// (Figma easing frame node 13044:2506: 269×162 with generous H margins + tight V margins.)
|
|
1759
|
+
const CURVE = { VBW:269, VBH:162, PAD_X:60, PAD_Y:16 };
|
|
1760
|
+
|
|
1761
|
+
function EasingEditor({easing, cubic, spring, durationMs, propKey, apply}){
|
|
1762
|
+
const [tab, setTab] = useState(spring ? "springs" : "easing");
|
|
1763
|
+
// when the user selects a different property, reflect that property's mode
|
|
1764
|
+
useEffect(()=>{ setTab(spring ? "springs" : "easing"); /* eslint-disable-next-line */ }, [propKey]);
|
|
1765
|
+
|
|
1766
|
+
// remember the last real (non-spring) easing so we can restore it when
|
|
1767
|
+
// switching back from the Springs tab.
|
|
1768
|
+
const lastEasingRef = useRef("ease");
|
|
1769
|
+
useEffect(()=>{ if(!spring && easing && !/^linear\(/.test(easing)) lastEasingRef.current = easing; }, [easing, spring]);
|
|
1770
|
+
|
|
1771
|
+
const applySpring = useCallback((stiffness,damping,mass,name)=>{
|
|
1772
|
+
const sim = simulateSpring(stiffness,damping,mass);
|
|
1773
|
+
apply({ spring:{stiffness,damping,mass,name}, easing:sim.css, durationMs:sim.durationMs });
|
|
1774
|
+
},[apply]);
|
|
1775
|
+
|
|
1776
|
+
const selectTab = useCallback(t=>{
|
|
1777
|
+
if(t==="springs"){
|
|
1778
|
+
setTab("springs");
|
|
1779
|
+
if(!spring){ const p=SPRING_PRESETS[0]; applySpring(p.stiffness,p.damping,p.mass,p.label); }
|
|
1780
|
+
} else {
|
|
1781
|
+
setTab("easing");
|
|
1782
|
+
if(spring) apply({ spring:null, easing:lastEasingRef.current||"ease" });
|
|
1783
|
+
}
|
|
1784
|
+
},[spring,apply,applySpring]);
|
|
1785
|
+
|
|
1786
|
+
// page-slide between the two tabs (transitions.dev · 08-page-side-by-side).
|
|
1787
|
+
// both pages stay mounted so they can cross-slide; the container height
|
|
1788
|
+
// follows the active page so it resizes smoothly while sliding.
|
|
1789
|
+
const slideRef = useRef(null);
|
|
1790
|
+
const easePageRef = useRef(null);
|
|
1791
|
+
const springPageRef = useRef(null);
|
|
1792
|
+
const page = tab==="springs" ? "2" : "1";
|
|
1793
|
+
useLayoutEffect(()=>{
|
|
1794
|
+
const slide = slideRef.current; if(!slide) return;
|
|
1795
|
+
const active = tab==="springs" ? springPageRef.current : easePageRef.current;
|
|
1796
|
+
if(active) slide.style.height = active.offsetHeight + "px";
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
return h("div",{className:"tl-ease"},
|
|
1800
|
+
h("div",{className:"tl-seg",role:"tablist"},
|
|
1801
|
+
h("button",{className:cx("tl-seg-btn",tab==="easing"&&"is-active"),role:"tab",
|
|
1802
|
+
"aria-selected":tab==="easing",onClick:()=>selectTab("easing")},"Easing"),
|
|
1803
|
+
h("button",{className:cx("tl-seg-btn",tab==="springs"&&"is-active"),role:"tab",
|
|
1804
|
+
"aria-selected":tab==="springs",onClick:()=>selectTab("springs")},"Springs"),
|
|
1805
|
+
),
|
|
1806
|
+
h("div",{className:"tl-ease-pages t-page-slide",ref:slideRef,"data-page":page},
|
|
1807
|
+
h("div",{className:"t-page",ref:easePageRef,"data-page-id":"1","aria-hidden":tab!=="easing"},
|
|
1808
|
+
h(EasingTab,{easing,cubic,setEasing:v=>apply({easing:v, spring:null})})),
|
|
1809
|
+
h("div",{className:"t-page",ref:springPageRef,"data-page-id":"2","aria-hidden":tab!=="springs"},
|
|
1810
|
+
h(SpringTab,{spring,applySpring})),
|
|
1811
|
+
),
|
|
1812
|
+
h(PositionPreview,{easing, durationMs}),
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// Animated position preview (à la easing.dev): a marker travels left→right
|
|
1817
|
+
// using the live timing function + duration, pauses, returns, and loops.
|
|
1818
|
+
// Works for any timing function — cubic-bezier keywords or a spring's linear().
|
|
1819
|
+
function PositionPreview({easing, durationMs}){
|
|
1820
|
+
const trackRef = useRef(null);
|
|
1821
|
+
const dotRef = useRef(null);
|
|
1822
|
+
const [playing, setPlaying] = useState(false);
|
|
1823
|
+
const safeEasing = (!easing || easing.trim()==="") ? "linear" : easing;
|
|
1824
|
+
const dur = Math.max(120, durationMs || 0);
|
|
1825
|
+
|
|
1826
|
+
useEffect(()=>{
|
|
1827
|
+
if(!playing) return;
|
|
1828
|
+
const dot = dotRef.current, track = trackRef.current;
|
|
1829
|
+
if(!dot || !track) return;
|
|
1830
|
+
let cancelled = false, anim = null, timer = null, atRight = false;
|
|
1831
|
+
const GAP = 480, DOT = 14;
|
|
1832
|
+
const travel = ()=> Math.max(0, track.clientWidth - DOT - 8);
|
|
1833
|
+
const step = ()=>{
|
|
1834
|
+
if(cancelled) return;
|
|
1835
|
+
const from = atRight ? travel() : 0;
|
|
1836
|
+
const to = atRight ? 0 : travel();
|
|
1837
|
+
try {
|
|
1838
|
+
anim = dot.animate(
|
|
1839
|
+
[{transform:`translateX(${from}px)`},{transform:`translateX(${to}px)`}],
|
|
1840
|
+
{duration:dur, easing:safeEasing, fill:"forwards"});
|
|
1841
|
+
} catch {
|
|
1842
|
+
anim = dot.animate(
|
|
1843
|
+
[{transform:`translateX(${from}px)`},{transform:`translateX(${to}px)`}],
|
|
1844
|
+
{duration:dur, easing:"linear", fill:"forwards"});
|
|
1845
|
+
}
|
|
1846
|
+
anim.onfinish = ()=>{ if(cancelled) return; atRight = !atRight; timer = setTimeout(step, GAP); };
|
|
1847
|
+
};
|
|
1848
|
+
step();
|
|
1849
|
+
return ()=>{ cancelled = true; if(anim){try{anim.cancel();}catch{}} if(timer)clearTimeout(timer); };
|
|
1850
|
+
},[playing, safeEasing, dur]);
|
|
1851
|
+
|
|
1852
|
+
return h("div",{className:"tl-preview"},
|
|
1853
|
+
h("div",{className:"tl-preview-head"},
|
|
1854
|
+
h("span",{className:"tl-preview-title"},"Position Preview"),
|
|
1855
|
+
h("button",{className:"tl-preview-btn",onClick:()=>setPlaying(p=>!p)},
|
|
1856
|
+
h(Ic,{name:playing?"pause":"play",size:11}),
|
|
1857
|
+
h("span",null,playing?"Pause":"Play")),
|
|
1858
|
+
),
|
|
1859
|
+
h("div",{className:"tl-preview-track",ref:trackRef},
|
|
1860
|
+
h("span",{className:"tl-preview-rail"}),
|
|
1861
|
+
h("span",{className:"tl-preview-end left"}),
|
|
1862
|
+
h("span",{className:"tl-preview-end right"}),
|
|
1863
|
+
h("span",{className:"tl-preview-dot",ref:dotRef}),
|
|
1864
|
+
),
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
function EasingTab({easing, cubic, setEasing}){
|
|
1869
|
+
const isPreset = EASING_PRESETS.some(p=>p.keyword===easing);
|
|
1870
|
+
const isCubic = !isPreset && /^cubic-bezier\(/.test(easing);
|
|
1871
|
+
const isLibrary = EASING_LABEL.has(normEase(easing));
|
|
1872
|
+
const mode = isLibrary ? "__lib" : isPreset ? easing : isCubic ? "__cubic" : "__custom";
|
|
1873
|
+
const cb = cubic || [0.25,0.1,0.25,1];
|
|
1874
|
+
const svgRef = useRef(null);
|
|
1875
|
+
const easeRef = useRef(null);
|
|
1876
|
+
const [easeOpen, setEaseOpen] = useState(false);
|
|
1877
|
+
const selLabel = easingLabel(easing) || (isCubic ? "cubic-bezier(\u2026)" : easing ? easing : "custom");
|
|
1878
|
+
const pickEase = useCallback(v=>{
|
|
1879
|
+
if(v==="__cubic") setEasing(`cubic-bezier(${cb.join(", ")})`);
|
|
1880
|
+
else if(v==="__custom") setEasing("");
|
|
1881
|
+
else setEasing(v);
|
|
1882
|
+
setEaseOpen(false);
|
|
1883
|
+
},[cb,setEasing]);
|
|
1884
|
+
|
|
1885
|
+
const setCubicVal = useCallback((i,v)=>{
|
|
1886
|
+
const next = [...cb]; next[i]=Math.round(parseFloat(v)*100)/100;
|
|
1887
|
+
if(isNaN(next[i])) return;
|
|
1888
|
+
setEasing(`cubic-bezier(${next.join(", ")})`);
|
|
1889
|
+
},[cb,setEasing]);
|
|
1890
|
+
|
|
1891
|
+
const { VBW, VBH, PAD_X, PAD_Y } = CURVE;
|
|
1892
|
+
const plotW = VBW - 2*PAD_X, plotH = VBH - 2*PAD_Y;
|
|
1893
|
+
const originX = PAD_X, originY = VBH - PAD_Y, endX = VBW - PAD_X, endY = PAD_Y;
|
|
1894
|
+
const fx = f => PAD_X + f*plotW;
|
|
1895
|
+
const fy = v => (VBH - PAD_Y) - v*plotH;
|
|
1896
|
+
|
|
1897
|
+
const startHandleDrag = useCallback((pointIdx, e)=>{
|
|
1898
|
+
e.preventDefault(); e.stopPropagation();
|
|
1899
|
+
const svg = svgRef.current;
|
|
1900
|
+
if(!svg) return;
|
|
1901
|
+
const calc = (clientX, clientY) => {
|
|
1902
|
+
const rect = svg.getBoundingClientRect();
|
|
1903
|
+
const vbx = (clientX - rect.left) / rect.width * VBW;
|
|
1904
|
+
const vby = (clientY - rect.top) / rect.height * VBH;
|
|
1905
|
+
let x = (vbx - PAD_X) / plotW;
|
|
1906
|
+
let y = ((VBH - PAD_Y) - vby) / plotH;
|
|
1907
|
+
x = Math.round(Math.max(0, Math.min(1, x)) * 100) / 100;
|
|
1908
|
+
y = Math.round(Math.max(-0.5, Math.min(1.5, y)) * 100) / 100;
|
|
1909
|
+
const next = [...cb];
|
|
1910
|
+
next[pointIdx * 2] = x;
|
|
1911
|
+
next[pointIdx * 2 + 1] = y;
|
|
1912
|
+
setEasing(`cubic-bezier(${next.join(", ")})`);
|
|
1913
|
+
};
|
|
1914
|
+
const onMove = e2 => calc(e2.clientX, e2.clientY);
|
|
1915
|
+
const onUp = () => { window.removeEventListener("mousemove",onMove); window.removeEventListener("mouseup",onUp); };
|
|
1916
|
+
window.addEventListener("mousemove",onMove);
|
|
1917
|
+
window.addEventListener("mouseup",onUp);
|
|
1918
|
+
},[cb,setEasing,plotW,plotH,VBW,VBH,PAD_X,PAD_Y]);
|
|
1919
|
+
|
|
1920
|
+
const p1x = fx(cb[0]), p1y = fy(cb[1]);
|
|
1921
|
+
const p2x = fx(cb[2]), p2y = fy(cb[3]);
|
|
1922
|
+
|
|
1923
|
+
return h(React.Fragment,null,
|
|
1924
|
+
h("button",{ref:easeRef,className:cx("tl-select",easeOpen&&"is-open"),onClick:()=>setEaseOpen(v=>!v)},
|
|
1925
|
+
h("span",{className:"tl-select-label"},selLabel),
|
|
1926
|
+
h("span",{className:"tl-select-chev"},h(Ic,{name:"chevron"}))),
|
|
1927
|
+
h(Dropdown,{open:easeOpen,onClose:()=>setEaseOpen(false),triggerRef:easeRef,
|
|
1928
|
+
width:Math.max(248,(easeRef.current&&easeRef.current.offsetWidth)||248),align:"left"},
|
|
1929
|
+
EASING_LIBRARY.map((o,i)=> o.group
|
|
1930
|
+
? h("div",{key:"g"+i,className:"tl-menu-group"},o.group)
|
|
1931
|
+
: h(MenuItem,{key:o.value,active:normEase(o.value)===normEase(easing)||(o.value===mode),
|
|
1932
|
+
onClick:()=>pickEase(o.value),
|
|
1933
|
+
right:(normEase(o.value)===normEase(easing))&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},
|
|
1934
|
+
h("span",{className:"tl-menu-text"},o.label),
|
|
1935
|
+
o.usage&&h("span",{className:"t-tt-wrap tl-menu-help",
|
|
1936
|
+
onClick:e=>e.stopPropagation(),onMouseDown:e=>e.stopPropagation()},
|
|
1937
|
+
h(Ic,{name:"help",size:13}),
|
|
1938
|
+
h("span",{className:"t-tt tl-tt-usage",role:"tooltip"},o.usage))))),
|
|
1939
|
+
h("div",{className:"tl-cubic-row"},
|
|
1940
|
+
...[0,1,2,3].map(i=>h("input",{key:i,type:"number",step:0.05,min:i%2===0?0:undefined,max:i%2===0?1:undefined,
|
|
1941
|
+
value:cb[i],onChange:e=>setCubicVal(i,e.target.value)})),
|
|
1942
|
+
),
|
|
1943
|
+
mode==="__custom" && h("input",{className:"tl-custom-input",value:easing,
|
|
1944
|
+
placeholder:"e.g. steps(4, end)",
|
|
1945
|
+
onChange:e=>setEasing(e.target.value)}),
|
|
1946
|
+
h("div",{className:"tl-curve"},
|
|
1947
|
+
h("svg",{ref:svgRef,viewBox:`0 0 ${VBW} ${VBH}`},
|
|
1948
|
+
h("defs",null,
|
|
1949
|
+
h("pattern",{id:"tl-curve-dots",width:14,height:14,patternUnits:"userSpaceOnUse",patternTransform:"translate(7 7)"},
|
|
1950
|
+
h("circle",{cx:0,cy:0,r:1,fill:"rgba(0,0,0,0.10)"}))),
|
|
1951
|
+
h("rect",{x:0,y:0,width:VBW,height:VBH,fill:"url(#tl-curve-dots)",style:{pointerEvents:"none"}}),
|
|
1952
|
+
h("path",{d:`M ${originX} ${originY} C ${p1x} ${p1y}, ${p2x} ${p2y}, ${endX} ${endY}`,
|
|
1953
|
+
fill:"none",stroke:"#181a1e",strokeWidth:2,strokeLinecap:"round"}),
|
|
1954
|
+
h("line",{x1:p1x,y1:p1y,x2:originX,y2:originY,stroke:"#1A7AFF",strokeWidth:2,strokeLinecap:"round",style:{pointerEvents:"none"}}),
|
|
1955
|
+
h("line",{x1:p2x,y1:p2y,x2:endX,y2:endY,stroke:"#1A7AFF",strokeWidth:2,strokeLinecap:"round",style:{pointerEvents:"none"}}),
|
|
1956
|
+
h("circle",{cx:p1x,cy:p1y,r:5.5,fill:"#1A7AFF",style:{pointerEvents:"none",filter:"drop-shadow(0 1px 3px rgba(0,0,0,.25))"}}),
|
|
1957
|
+
h("circle",{cx:p2x,cy:p2y,r:5.5,fill:"#1A7AFF",style:{pointerEvents:"none",filter:"drop-shadow(0 1px 3px rgba(0,0,0,.25))"}}),
|
|
1958
|
+
h("circle",{className:"tl-curve-handle",cx:p1x,cy:p1y,r:9,fill:"transparent",
|
|
1959
|
+
onMouseDown:e=>startHandleDrag(0,e)}),
|
|
1960
|
+
h("circle",{className:"tl-curve-handle",cx:p2x,cy:p2y,r:9,fill:"transparent",
|
|
1961
|
+
onMouseDown:e=>startHandleDrag(1,e)}),
|
|
1962
|
+
),
|
|
1963
|
+
),
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function SpringTab({spring, applySpring}){
|
|
1968
|
+
const sp = spring || { ...SPRING_PRESETS[0] };
|
|
1969
|
+
const stiffness = sp.stiffness, damping = sp.damping, mass = sp.mass ?? 1;
|
|
1970
|
+
const presetName = matchSpringPreset({stiffness,damping,mass});
|
|
1971
|
+
const spRef = useRef(null);
|
|
1972
|
+
const [spOpen, setSpOpen] = useState(false);
|
|
1973
|
+
|
|
1974
|
+
const sim = useMemo(()=>simulateSpring(stiffness,damping,mass),[stiffness,damping,mass]);
|
|
1975
|
+
const overshoots = useMemo(()=>sim.values.some(v=>v>1.001),[sim]);
|
|
1976
|
+
|
|
1977
|
+
const setParam = useCallback((key,val)=>{
|
|
1978
|
+
const next = { stiffness, damping, mass };
|
|
1979
|
+
next[key] = val;
|
|
1980
|
+
applySpring(next.stiffness, next.damping, next.mass, matchSpringPreset(next));
|
|
1981
|
+
},[stiffness,damping,mass,applySpring]);
|
|
1982
|
+
|
|
1983
|
+
// spring curve — auto-fit vertically so overshoot above 1 stays in frame.
|
|
1984
|
+
const { VBW, VBH, PAD_X, PAD_Y } = CURVE;
|
|
1985
|
+
const plotW = VBW - 2*PAD_X, plotH = VBH - 2*PAD_Y;
|
|
1986
|
+
const vals = sim.values;
|
|
1987
|
+
const hi = Math.max(1, ...vals), lo = Math.min(0, ...vals);
|
|
1988
|
+
const range = (hi - lo) || 1;
|
|
1989
|
+
const sx = i => PAD_X + (i/(vals.length-1))*plotW;
|
|
1990
|
+
const sy = v => (VBH - PAD_Y) - ((v - lo)/range)*plotH;
|
|
1991
|
+
const yOne = sy(1), yZero = sy(0);
|
|
1992
|
+
const curveD = vals.map((v,i)=>`${i===0?"M":"L"} ${sx(i).toFixed(2)} ${sy(v).toFixed(2)}`).join(" ");
|
|
1993
|
+
|
|
1994
|
+
return h(React.Fragment,null,
|
|
1995
|
+
h("button",{ref:spRef,className:cx("tl-select",spOpen&&"is-open"),onClick:()=>setSpOpen(v=>!v)},
|
|
1996
|
+
h("span",{className:"tl-select-label"},presetName==="Custom"?"Custom spring":presetName),
|
|
1997
|
+
h("span",{className:"tl-select-chev"},h(Ic,{name:"chevron"}))),
|
|
1998
|
+
h(Dropdown,{open:spOpen,onClose:()=>setSpOpen(false),triggerRef:spRef,
|
|
1999
|
+
width:Math.max(248,(spRef.current&&spRef.current.offsetWidth)||248),align:"left"},
|
|
2000
|
+
SPRING_PRESETS.map(p=>h(MenuItem,{key:p.label,active:p.label===presetName,
|
|
2001
|
+
onClick:()=>{applySpring(p.stiffness,p.damping,p.mass,p.label);setSpOpen(false);},
|
|
2002
|
+
right:p.label===presetName&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},
|
|
2003
|
+
h("span",{className:"tl-menu-text"},p.label),
|
|
2004
|
+
p.usage&&h("span",{className:"t-tt-wrap tl-menu-help",
|
|
2005
|
+
onClick:e=>e.stopPropagation(),onMouseDown:e=>e.stopPropagation()},
|
|
2006
|
+
h(Ic,{name:"help",size:13}),
|
|
2007
|
+
h("span",{className:"t-tt tl-tt-usage",role:"tooltip"},p.usage))))),
|
|
2008
|
+
h("div",{className:"tl-bounce"},
|
|
2009
|
+
h("div",{className:"tl-bounce-title"},"Spring physics"),
|
|
2010
|
+
h("div",{className:"tl-bounce-row"},
|
|
2011
|
+
h("label",null,"Stiffness"),
|
|
2012
|
+
h("input",{type:"range",min:20,max:400,step:5,value:stiffness,
|
|
2013
|
+
onChange:e=>setParam("stiffness",parseFloat(e.target.value))}),
|
|
2014
|
+
h("span",{className:"tl-bounce-val"},Math.round(stiffness)),
|
|
2015
|
+
),
|
|
2016
|
+
h("div",{className:"tl-bounce-row"},
|
|
2017
|
+
h("label",null,"Damping"),
|
|
2018
|
+
h("input",{type:"range",min:1,max:120,step:1,value:damping,
|
|
2019
|
+
onChange:e=>setParam("damping",parseFloat(e.target.value))}),
|
|
2020
|
+
h("span",{className:"tl-bounce-val"},Math.round(damping)),
|
|
2021
|
+
),
|
|
2022
|
+
h("div",{className:"tl-bounce-row"},
|
|
2023
|
+
h("label",null,"Mass"),
|
|
2024
|
+
h("input",{type:"range",min:0.2,max:5,step:0.1,value:mass,
|
|
2025
|
+
onChange:e=>setParam("mass",parseFloat(e.target.value))}),
|
|
2026
|
+
h("span",{className:"tl-bounce-val"},mass.toFixed(1)),
|
|
2027
|
+
),
|
|
2028
|
+
h("div",{className:"tl-spring-dur"},
|
|
2029
|
+
h("span",null,"Duration"),
|
|
2030
|
+
h("b",null,"~"+sim.durationMs+"ms"),
|
|
2031
|
+
h("span",{className:"tl-spring-dur-hint"},"· derived"+(overshoots?" · overshoots":"")),
|
|
2032
|
+
),
|
|
2033
|
+
),
|
|
2034
|
+
h("div",{className:"tl-curve"},
|
|
2035
|
+
h("svg",{viewBox:`0 0 ${VBW} ${VBH}`},
|
|
2036
|
+
h("defs",null,
|
|
2037
|
+
h("pattern",{id:"tl-spring-dots",width:14,height:14,patternUnits:"userSpaceOnUse",patternTransform:"translate(7 7)"},
|
|
2038
|
+
h("circle",{cx:0,cy:0,r:1,fill:"rgba(0,0,0,0.10)"}))),
|
|
2039
|
+
h("rect",{x:0,y:0,width:VBW,height:VBH,fill:"url(#tl-spring-dots)",style:{pointerEvents:"none"}}),
|
|
2040
|
+
// target (value = 1) and baseline (value = 0) reference lines
|
|
2041
|
+
h("line",{x1:PAD_X,y1:yOne,x2:VBW-PAD_X,y2:yOne,stroke:"#1A7AFF",strokeWidth:1,
|
|
2042
|
+
strokeDasharray:"3 4",style:{opacity:0.5,pointerEvents:"none"}}),
|
|
2043
|
+
h("line",{x1:PAD_X,y1:yZero,x2:VBW-PAD_X,y2:yZero,stroke:"rgba(0,0,0,0.18)",strokeWidth:1,
|
|
2044
|
+
strokeDasharray:"3 4",style:{pointerEvents:"none"}}),
|
|
2045
|
+
h("path",{d:curveD,fill:"none",stroke:"#181a1e",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),
|
|
2046
|
+
),
|
|
2047
|
+
),
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
function ScrubZone({scaleMs}){
|
|
2052
|
+
const { preview, registry, activeId } = useContext(TimelineCtx);
|
|
2053
|
+
const areaRef = useRef(null);
|
|
2054
|
+
const startedRef = useRef(false);
|
|
2055
|
+
|
|
2056
|
+
const pxToMs = useCallback(clientX=>{
|
|
2057
|
+
if(!areaRef.current) return 0;
|
|
2058
|
+
const rect = areaRef.current.getBoundingClientRect();
|
|
2059
|
+
const ratio = Math.max(0, Math.min((clientX - rect.left) / rect.width, 1));
|
|
2060
|
+
return ratio * scaleMs;
|
|
2061
|
+
},[scaleMs]);
|
|
2062
|
+
|
|
2063
|
+
const doSeek = useCallback(ms=>{
|
|
2064
|
+
if(preview.getState()==="idle" && !startedRef.current){
|
|
2065
|
+
if(!activeId) return;
|
|
2066
|
+
const entry = registry.getEffective(activeId);
|
|
2067
|
+
if(!entry) return;
|
|
2068
|
+
startedRef.current = true;
|
|
2069
|
+
preview.playPaused(entry, ms);
|
|
2070
|
+
} else {
|
|
2071
|
+
preview.seek(ms);
|
|
2072
|
+
}
|
|
2073
|
+
},[preview,registry,activeId]);
|
|
2074
|
+
|
|
2075
|
+
const startScrub = useCallback(e=>{
|
|
2076
|
+
e.preventDefault();
|
|
2077
|
+
startedRef.current = false;
|
|
2078
|
+
doSeek(pxToMs(e.clientX));
|
|
2079
|
+
const onMove = e2 => doSeek(pxToMs(e2.clientX));
|
|
2080
|
+
const onUp = () => { startedRef.current = false; window.removeEventListener("mousemove",onMove); window.removeEventListener("mouseup",onUp); };
|
|
2081
|
+
window.addEventListener("mousemove",onMove);
|
|
2082
|
+
window.addEventListener("mouseup",onUp);
|
|
2083
|
+
},[doSeek,pxToMs]);
|
|
2084
|
+
|
|
2085
|
+
return h("div",{className:"tl-scrub-zone",ref:areaRef,onMouseDown:startScrub});
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function Tracks({et, selProp, setSelProp, onPropChange, snap, scaleMs}){
|
|
2089
|
+
const t = usePreviewTime();
|
|
2090
|
+
const ratio = Math.min(t / scaleMs, 1);
|
|
2091
|
+
const ROW_H = 48;
|
|
2092
|
+
const rowsRef = useRef(null);
|
|
2093
|
+
const [order, setOrder] = useState([]);
|
|
2094
|
+
const [dragProp, setDragProp] = useState(null);
|
|
2095
|
+
const propKey = et.map(x=>x.property).join("|");
|
|
2096
|
+
useEffect(()=>{
|
|
2097
|
+
const props = et.map(x=>x.property);
|
|
2098
|
+
setOrder(prev=>{
|
|
2099
|
+
const kept = prev.filter(p=>props.includes(p));
|
|
2100
|
+
const added = props.filter(p=>!kept.includes(p));
|
|
2101
|
+
return [...kept, ...added];
|
|
2102
|
+
});
|
|
2103
|
+
},[propKey]);
|
|
2104
|
+
const orderedRows = order.map(p=>et.find(x=>x.property===p)).filter(Boolean);
|
|
2105
|
+
const startReorder = useCallback((property,e)=>{
|
|
2106
|
+
e.preventDefault(); e.stopPropagation();
|
|
2107
|
+
setSelProp(property);
|
|
2108
|
+
setDragProp(property);
|
|
2109
|
+
const top = rowsRef.current ? rowsRef.current.getBoundingClientRect().top : 0;
|
|
2110
|
+
const onMove = e2=>{
|
|
2111
|
+
const idx = Math.floor((e2.clientY - top) / ROW_H);
|
|
2112
|
+
setOrder(prev=>{
|
|
2113
|
+
const cur = prev.indexOf(property);
|
|
2114
|
+
if(cur<0) return prev;
|
|
2115
|
+
const target = Math.max(0, Math.min(prev.length-1, idx));
|
|
2116
|
+
if(target===cur) return prev;
|
|
2117
|
+
const next = prev.slice();
|
|
2118
|
+
next.splice(cur,1);
|
|
2119
|
+
next.splice(target,0,property);
|
|
2120
|
+
return next;
|
|
2121
|
+
});
|
|
2122
|
+
};
|
|
2123
|
+
const onUp = ()=>{ setDragProp(null); window.removeEventListener("mousemove",onMove); window.removeEventListener("mouseup",onUp); };
|
|
2124
|
+
window.addEventListener("mousemove",onMove); window.addEventListener("mouseup",onUp);
|
|
2125
|
+
},[setSelProp]);
|
|
2126
|
+
const majorStep = scaleMs <= 10000 ? 1000 : scaleMs <= 25000 ? 5000 : 10000;
|
|
2127
|
+
const minorStep = majorStep / 4;
|
|
2128
|
+
const ruler=[];
|
|
2129
|
+
for(let ms=0; ms<=scaleMs; ms+=majorStep){
|
|
2130
|
+
const left=(ms/scaleMs)*100;
|
|
2131
|
+
ruler.push(h("span",{key:"l"+ms,className:cx("maj",ms===0&&"first",ms===scaleMs&&"last"),style:{left:left+"%"}},(ms/1000)+"s"));
|
|
2132
|
+
}
|
|
2133
|
+
for(let ms=minorStep; ms<scaleMs; ms+=minorStep){
|
|
2134
|
+
if(ms%majorStep===0) continue;
|
|
2135
|
+
ruler.push(h("span",{key:"t"+ms,className:"tick",style:{left:((ms/scaleMs)*100)+"%"}}));
|
|
2136
|
+
}
|
|
2137
|
+
return h("div",{className:"tl-tracks"},
|
|
2138
|
+
h("div",{className:"tl-ruler-row"},
|
|
2139
|
+
h("div",{className:"tl-ruler-spacer"}),
|
|
2140
|
+
h("div",{className:"tl-ruler"},...ruler)),
|
|
2141
|
+
h("div",{className:"tl-rows",ref:rowsRef},
|
|
2142
|
+
...orderedRows.map(row=>h(PropTrack,{
|
|
2143
|
+
key:row.property, property:row.property,
|
|
2144
|
+
delayMs:row.delayMs, durationMs:row.durationMs, lockDuration:!!row.spring,
|
|
2145
|
+
selected:row.property===selProp, dragging:row.property===dragProp, snap, scaleMs,
|
|
2146
|
+
onSelect:()=>setSelProp(row.property),
|
|
2147
|
+
onReorder:e=>startReorder(row.property,e),
|
|
2148
|
+
onDelayChange:ms=>onPropChange(row.property,{delayMs:ms}),
|
|
2149
|
+
onDurationChange:ms=>onPropChange(row.property,{durationMs:ms}),
|
|
2150
|
+
}))),
|
|
2151
|
+
h(ScrubZone,{scaleMs}),
|
|
2152
|
+
h("div",{className:"tl-playhead-layer"},
|
|
2153
|
+
h("div",{className:"tl-playhead",style:{left:(ratio*100)+"%"}},
|
|
2154
|
+
h("svg",{className:"tl-playhead-head",viewBox:"0 0 16.666 24",width:"16.666",height:"24",fill:"none"},
|
|
2155
|
+
h("path",{d:"M13.666 7.333C13.666 11.609 9.333 15.666 9.333 19.942L9.333 24L7.333 24L7.333 19.942C7.333 15.666 3 11.609 3 7.333C3 4.387 5.387 2 8.333 2C11.279 2 13.666 4.387 13.666 7.333Z",fill:"#1A7AFF"}))),
|
|
2156
|
+
),
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
function Inspector({entry, selProp, onPropChange, snap}){
|
|
2161
|
+
const et = entry.effectiveTimings || [];
|
|
2162
|
+
const sel = et.find(t=>t.property===selProp) || et[0];
|
|
2163
|
+
if(!sel) return h("div",{className:"tl-inspector"});
|
|
2164
|
+
const cubic = easingToCubic(sel.easing);
|
|
2165
|
+
const isSpring = !!sel.spring;
|
|
2166
|
+
return h("div",{className:"tl-inspector"},
|
|
2167
|
+
h("div",{className:"tl-insp-title"},sel.property),
|
|
2168
|
+
h(ValueField,{label:"Duration",value:sel.durationMs,min:0,max:5000,step:25,tokens:DURATION_TOKENS,snap,
|
|
2169
|
+
readOnly:isSpring,
|
|
2170
|
+
readOnlyHint:"Duration is set by the spring. A spring's settle time is derived from its stiffness, damping and mass \u2014 so it can't be edited directly. Switch to the Easing tab to set a fixed duration.",
|
|
2171
|
+
onChange:v=>onPropChange(sel.property,{durationMs:v})}),
|
|
2172
|
+
h(ValueField,{label:"Delay",value:sel.delayMs,min:0,max:5000,step:25,tokens:DELAY_TOKENS,snap,
|
|
2173
|
+
onChange:v=>onPropChange(sel.property,{delayMs:v})}),
|
|
2174
|
+
h(EasingEditor,{easing:sel.easing, cubic, spring:sel.spring, durationMs:sel.durationMs, propKey:sel.property,
|
|
2175
|
+
apply:partial=>onPropChange(sel.property,partial)}),
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
function Body({entry, onPropChange, state, play, pause, resume, restart, stop, speed, setSpeed, snap}){
|
|
2180
|
+
const et = entry.effectiveTimings || [];
|
|
2181
|
+
const [selProp, setSelProp] = useState(et[0]?.property ?? null);
|
|
2182
|
+
const [zoom, setZoom] = useState(ZOOM_DEFAULT);
|
|
2183
|
+
const scaleMs = scaleFromZoom(zoom);
|
|
2184
|
+
useEffect(()=>{
|
|
2185
|
+
if(et.length && !et.find(t=>t.property===selProp)) setSelProp(et[0]?.property);
|
|
2186
|
+
},[et,selProp]);
|
|
2187
|
+
|
|
2188
|
+
return h("div",{className:"tl-body"},
|
|
2189
|
+
h("div",{className:"tl-main"},
|
|
2190
|
+
h(Transport,{state,disabled:!entry,onPlay:play,onPause:pause,onResume:resume,onRestart:restart,onStop:stop,speed,setSpeed,zoom,setZoom}),
|
|
2191
|
+
h(Tracks,{et,selProp,setSelProp,onPropChange,snap,scaleMs}),
|
|
2192
|
+
),
|
|
2193
|
+
h(Inspector,{entry,selProp,onPropChange,snap}),
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
function Transport({state,disabled,onPlay,onPause,onResume,onRestart,onStop,speed,setSpeed,zoom,setZoom}){
|
|
2198
|
+
const t = usePreviewTime();
|
|
2199
|
+
const spRef = useRef(null);
|
|
2200
|
+
const [sp,setSp] = useState(false);
|
|
2201
|
+
const playing = state==="playing";
|
|
2202
|
+
const onMain = () => playing ? onPause() : state==="paused" ? onResume() : onPlay();
|
|
2203
|
+
return h("div",{className:"tl-transport"},
|
|
2204
|
+
h("div",{className:"tl-transport-left"},
|
|
2205
|
+
h("span",{className:"tl-timecode"}, fmtTimecode(t)),
|
|
2206
|
+
h("button",{ref:spRef,className:cx("tl-ghost-btn","tl-speed",sp&&"is-active"),onClick:()=>setSp(v=>!v)},
|
|
2207
|
+
fmtSpeed(speed), h("span",{className:"tl-ghost-chev"},h(Ic,{name:"chevron"}))),
|
|
2208
|
+
h(Dropdown,{open:sp,onClose:()=>setSp(false),triggerRef:spRef,width:120,align:"left"},
|
|
2209
|
+
SPEEDS.map(s=>h(MenuItem,{key:s,active:s===speed,onClick:()=>{setSpeed(s);setSp(false);},
|
|
2210
|
+
right:s===speed&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},fmtSpeed(s)))) ),
|
|
2211
|
+
h("div",{className:"tl-transport-center"},
|
|
2212
|
+
h("button",{className:"tl-play-btn",disabled,onClick:onMain,title:playing?"Pause":"Play"},
|
|
2213
|
+
h("span",{className:"t-icon-swap","data-state":playing?"b":"a"},
|
|
2214
|
+
h("span",{className:"t-icon","data-icon":"a"},h(Ic,{name:"play",size:12})),
|
|
2215
|
+
h("span",{className:"t-icon","data-icon":"b"},h(Ic,{name:"pause",size:16}))))),
|
|
2216
|
+
h("div",{className:"tl-transport-right"},
|
|
2217
|
+
h("div",{className:"tl-zoom",title:"Timeline zoom"},
|
|
2218
|
+
h("div",{className:"tl-zoom-track","aria-hidden":true}),
|
|
2219
|
+
h("input",{type:"range",min:ZOOM_MIN,max:ZOOM_MAX,value:zoom,
|
|
2220
|
+
onChange:e=>setZoom(Number(e.target.value))})),
|
|
2221
|
+
h("span",{className:"t-tt-wrap"},
|
|
2222
|
+
h("button",{className:"tl-icon-btn ghost t-tt-trigger",disabled,"aria-label":"Replay",onClick:onRestart},h(Ic,{name:"restart"})),
|
|
2223
|
+
h("span",{className:"t-tt tl-tt-below",role:"tooltip"},"Replay")),
|
|
2224
|
+
),
|
|
2225
|
+
);
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
function TimelinePanel(){
|
|
2229
|
+
const entries=useReg(); const{active,setActiveId}=useActive();
|
|
2230
|
+
const{state,play,pause,resume,restart,stop}=usePlayback(); const{setPropOverride}=usePropOverride();
|
|
2231
|
+
const{registry,preview}=useContext(TimelineCtx);
|
|
2232
|
+
const[copied,setCopied]=useState(false);
|
|
2233
|
+
const[minimized,setMinimized]=useState(false);
|
|
2234
|
+
const[panelHeight,setPanelHeight]=useState(440);
|
|
2235
|
+
const[resizing,setResizing]=useState(false);
|
|
2236
|
+
const[speed,setSpeed]=useState(1);
|
|
2237
|
+
const[loop,setLoop]=useState(false);
|
|
2238
|
+
const[snap,setSnap]=useState(true);
|
|
2239
|
+
// ── refine ──
|
|
2240
|
+
const[refineOpen,setRefineOpen]=useState(false);
|
|
2241
|
+
const[refinePhase,setRefinePhase]=useState("idle"); // idle | scanning | done | error
|
|
2242
|
+
const[refineJobId,setRefineJobId]=useState(null);
|
|
2243
|
+
const[refineLog,setRefineLog]=useState([]);
|
|
2244
|
+
const[refineSuggestions,setRefineSuggestions]=useState([]);
|
|
2245
|
+
const[refineSummary,setRefineSummary]=useState(null);
|
|
2246
|
+
const[refineError,setRefineError]=useState(null);
|
|
2247
|
+
const[refineLabel,setRefineLabel]=useState(null);
|
|
2248
|
+
const[appliedIds,setAppliedIds]=useState({});
|
|
2249
|
+
const[refineMode,setRefineMode]=useState("llm"); // llm (Agent) | deterministic
|
|
2250
|
+
const[refineType,setRefineType]=useState("small"); // small | replace
|
|
2251
|
+
const[llmAvailable,setLlmAvailable]=useState(null); // null=unknown, true/false from /health
|
|
2252
|
+
const[cliInstalled,setCliInstalled]=useState(null); // null=unknown, true/false from /health
|
|
2253
|
+
// probe the relay so the idle panel can show the right availability copy
|
|
2254
|
+
const refreshHealth=useCallback(async()=>{
|
|
2255
|
+
try{const j=await relayHealth();setLlmAvailable(!!j.llmAvailable);
|
|
2256
|
+
setCliInstalled(j.cliInstalled==null?null:!!j.cliInstalled);return j;}
|
|
2257
|
+
catch{setLlmAvailable(false);setCliInstalled(false);return null;}
|
|
2258
|
+
},[]);
|
|
2259
|
+
// the Refine button toggles the panel; opening drops into the idle state
|
|
2260
|
+
// (description + Start scanning) rather than auto-scanning.
|
|
2261
|
+
const openRefine=useCallback(()=>{
|
|
2262
|
+
if(!active)return;
|
|
2263
|
+
if(refineOpen){setRefineOpen(false);return;}
|
|
2264
|
+
setRefineOpen(true);setRefinePhase("idle");setRefineLog([]);
|
|
2265
|
+
setRefineSuggestions([]);setRefineSummary(null);setRefineError(null);
|
|
2266
|
+
setAppliedIds({});setRefineLabel(active.label);setRefineJobId(null);
|
|
2267
|
+
refreshHealth();
|
|
2268
|
+
},[active,refineOpen,refreshHealth]);
|
|
2269
|
+
// "Start scanning" resolves availability first, then posts the job.
|
|
2270
|
+
const startScan=useCallback(async()=>{
|
|
2271
|
+
if(!active)return;
|
|
2272
|
+
setRefinePhase("scanning");setRefineLog([]);
|
|
2273
|
+
setRefineSuggestions([]);setRefineSummary(null);setRefineError(null);
|
|
2274
|
+
setAppliedIds({});setRefineLabel(active.label);setRefineJobId(null);
|
|
2275
|
+
const j=await refreshHealth();
|
|
2276
|
+
const avail=j?!!j.llmAvailable:false;
|
|
2277
|
+
const mode=(refineMode==="llm"&&!avail)?"deterministic":refineMode;
|
|
2278
|
+
if(mode!==refineMode)setRefineMode(mode);
|
|
2279
|
+
try{
|
|
2280
|
+
const timings=(active.effectiveTimings||[]).map(t=>({property:t.property,durationMs:t.durationMs,delayMs:t.delayMs,easing:t.easing}));
|
|
2281
|
+
const{id}=await relayCreateJob({transitionId:active.id,label:active.label,selector:active.bindings&&active.bindings.selector,timings,mode,refineType});
|
|
2282
|
+
setRefineJobId(id);
|
|
2283
|
+
}catch(e){
|
|
2284
|
+
setRefinePhase("error");
|
|
2285
|
+
setRefineError(h(React.Fragment,null,"Couldn't reach the refine relay. Start it with ",h("code",{className:"tl-code"},"npx transitions-refine live"),"."));
|
|
2286
|
+
}
|
|
2287
|
+
},[active,refineMode,refineType,refreshHealth]);
|
|
2288
|
+
const changeRefineMode=useCallback((mode)=>{setRefineMode(mode);setRefinePhase("idle");refreshHealth();},[refreshHealth]);
|
|
2289
|
+
const changeRefineType=useCallback((t)=>{setRefineType(t);setRefinePhase("idle");},[]);
|
|
2290
|
+
// poll the relay while a job is running
|
|
2291
|
+
useEffect(()=>{
|
|
2292
|
+
if(!refineJobId||refinePhase!=="scanning")return;
|
|
2293
|
+
let live=true,to=null;
|
|
2294
|
+
const tick=async()=>{
|
|
2295
|
+
try{
|
|
2296
|
+
const job=await relayGetJob(refineJobId);
|
|
2297
|
+
if(!live)return;
|
|
2298
|
+
if(Array.isArray(job.statusLog))setRefineLog(job.statusLog);
|
|
2299
|
+
if(job.status==="done"){setRefineSuggestions((job.result&&job.result.suggestions)||[]);setRefineSummary(job.result&&job.result.summary);setRefinePhase("done");return;}
|
|
2300
|
+
if(job.status==="error"){setRefineError(job.error||"The agent reported an error.");setRefinePhase("error");return;}
|
|
2301
|
+
to=setTimeout(tick,500);
|
|
2302
|
+
}catch(e){if(live){setRefineError("Lost connection to the relay.");setRefinePhase("error");}}
|
|
2303
|
+
};
|
|
2304
|
+
to=setTimeout(tick,400);
|
|
2305
|
+
return()=>{live=false;if(to)clearTimeout(to);};
|
|
2306
|
+
},[refineJobId,refinePhase]);
|
|
2307
|
+
const applySuggestion=useCallback((s)=>{
|
|
2308
|
+
const p=s.patch||{};if(!p.property)return;
|
|
2309
|
+
const o={};
|
|
2310
|
+
if(p.durationMs!=null)o.durationMs=p.durationMs;
|
|
2311
|
+
if(p.delayMs!=null)o.delayMs=p.delayMs;
|
|
2312
|
+
if(p.easing!=null)o.easing=p.easing;
|
|
2313
|
+
setPropOverride(p.property,o);
|
|
2314
|
+
setAppliedIds(prev=>({...prev,[s.id]:true}));
|
|
2315
|
+
},[setPropOverride]);
|
|
2316
|
+
const applyAllSuggestions=useCallback(()=>{
|
|
2317
|
+
for(const s of refineSuggestions){if(!appliedIds[s.id])applySuggestion(s);}
|
|
2318
|
+
},[refineSuggestions,appliedIds,applySuggestion]);
|
|
2319
|
+
const panelMinH=200;
|
|
2320
|
+
const panelMaxH=useCallback(()=>Math.round(window.innerHeight*0.92),[]);
|
|
2321
|
+
useEffect(()=>{if(!active&&entries.length>0)setActiveId(entries[0].id);},[active,entries,setActiveId]);
|
|
2322
|
+
useEffect(()=>{preview.setRate(speed);},[preview,speed]);
|
|
2323
|
+
useEffect(()=>{preview.setLoop(loop);},[preview,loop]);
|
|
2324
|
+
const startResize=useCallback(e=>{
|
|
2325
|
+
e.preventDefault();
|
|
2326
|
+
const startY=e.clientY; const startH=panelHeight;
|
|
2327
|
+
setResizing(true);
|
|
2328
|
+
document.body.style.cursor="ns-resize";
|
|
2329
|
+
document.body.style.userSelect="none";
|
|
2330
|
+
const onMove=e2=>setPanelHeight(Math.max(panelMinH,Math.min(panelMaxH(),startH+(startY-e2.clientY))));
|
|
2331
|
+
const onUp=()=>{setResizing(false);document.body.style.cursor="";document.body.style.userSelect="";window.removeEventListener("mousemove",onMove);window.removeEventListener("mouseup",onUp);};
|
|
2332
|
+
window.addEventListener("mousemove",onMove);window.addEventListener("mouseup",onUp);
|
|
2333
|
+
},[panelHeight,panelMaxH]);
|
|
2334
|
+
const copyValues=useCallback(()=>{
|
|
2335
|
+
if(!active)return;
|
|
2336
|
+
const et=active.effectiveTimings||[];
|
|
2337
|
+
const css=et.map(t=>
|
|
2338
|
+
`${t.property} ${formatCssTime(t.durationMs)} ${t.easing} ${formatCssTime(t.delayMs)}`
|
|
2339
|
+
).join(",\n ");
|
|
2340
|
+
navigator.clipboard.writeText("transition: "+css+";").then(()=>{setCopied(true);setTimeout(()=>setCopied(false),1500);});
|
|
2341
|
+
},[active]);
|
|
2342
|
+
const resetOverrides=useCallback(()=>{if(active)registry.clearOverride(active.id);},[registry,active]);
|
|
2343
|
+
|
|
2344
|
+
// whole-component open/close uses the transitions.dev panel reveal:
|
|
2345
|
+
// keep the panel mounted while it animates, flip data-open on the next
|
|
2346
|
+
// frame so the closed → open transition runs, and delay unmount on close.
|
|
2347
|
+
const[render,setRender]=useState(!minimized);
|
|
2348
|
+
const[panelOpen,setPanelOpen]=useState(false);
|
|
2349
|
+
// phase picks which closed-state distance/scale the shared closed render
|
|
2350
|
+
// resolves to: "opening" → open-distance/open-scale start point, "closing"
|
|
2351
|
+
// → close-distance/close-scale end point. First mount opens, so start here.
|
|
2352
|
+
const[phase,setPhase]=useState("opening");
|
|
2353
|
+
useEffect(()=>{
|
|
2354
|
+
let raf, to;
|
|
2355
|
+
if(!minimized){
|
|
2356
|
+
setRender(true);
|
|
2357
|
+
setPhase("opening");
|
|
2358
|
+
raf=requestAnimationFrame(()=>{raf=requestAnimationFrame(()=>setPanelOpen(true));});
|
|
2359
|
+
return()=>{if(raf)cancelAnimationFrame(raf);};
|
|
2360
|
+
}
|
|
2361
|
+
setPhase("closing");
|
|
2362
|
+
setPanelOpen(false);
|
|
2363
|
+
// unmount after the slide-out finishes; read the live close-dur so long
|
|
2364
|
+
// tweak values from the controls don't clip the animation. +80ms buffer.
|
|
2365
|
+
const closeMs=parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--panel-close-dur"))||350;
|
|
2366
|
+
to=setTimeout(()=>setRender(false),closeMs+80);
|
|
2367
|
+
return()=>clearTimeout(to);
|
|
2368
|
+
},[minimized]);
|
|
2369
|
+
|
|
2370
|
+
// demo controls (top-right) drive open/close so tweaks can be previewed
|
|
2371
|
+
// without hunting for the Minimize button. "replay" closes then re-opens.
|
|
2372
|
+
useEffect(()=>{
|
|
2373
|
+
const onToggle=(e)=>{
|
|
2374
|
+
const action=e.detail&&e.detail.action;
|
|
2375
|
+
if(action==="toggle"){setMinimized(v=>!v);return;}
|
|
2376
|
+
setMinimized(true);
|
|
2377
|
+
const closeMs=parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--panel-close-dur"))||350;
|
|
2378
|
+
setTimeout(()=>setMinimized(false),closeMs+120);
|
|
2379
|
+
};
|
|
2380
|
+
window.addEventListener("tl-toggle-panel",onToggle);
|
|
2381
|
+
return()=>window.removeEventListener("tl-toggle-panel",onToggle);
|
|
2382
|
+
},[]);
|
|
2383
|
+
|
|
2384
|
+
return h(React.Fragment,null,
|
|
2385
|
+
render&&h("div",{className:"t-panel-slide","data-timeline-panel":true,
|
|
2386
|
+
"data-open":panelOpen?"true":"false","data-phase":phase,style:{height:panelHeight+"px"}},
|
|
2387
|
+
h("div",{className:cx("tl-resize-handle",resizing&&"dragging"),onMouseDown:startResize,title:"Drag to resize"}),
|
|
2388
|
+
h("div",{className:"tl-panel-body"},
|
|
2389
|
+
h("div",{className:"tl-panel-main"},
|
|
2390
|
+
h(Header,{entries,active,onSelect:setActiveId,onReset:resetOverrides,onCopy:copyValues,copied,
|
|
2391
|
+
loop,setLoop,snap,setSnap,onMinimize:()=>setMinimized(true),onRefine:openRefine,refineActive:refineOpen}),
|
|
2392
|
+
active
|
|
2393
|
+
?h(Body,{entry:active,onPropChange:(prop,o)=>setPropOverride(prop,o),
|
|
2394
|
+
state,play,pause,resume,restart,stop,speed,setSpeed,snap})
|
|
2395
|
+
:h("div",{className:"tl-empty"},"Select a transition to inspect and edit it")),
|
|
2396
|
+
h(RefinePanel,{open:refineOpen,onClose:()=>setRefineOpen(false),phase:refinePhase,label:refineLabel,
|
|
2397
|
+
refineType,onType:changeRefineType,suggestions:refineSuggestions,summary:refineSummary,error:refineError,
|
|
2398
|
+
appliedIds,onApply:applySuggestion,onApplyAll:applyAllSuggestions,
|
|
2399
|
+
mode:refineMode,onMode:changeRefineMode,llmAvailable,cliInstalled,onStart:startScan}),
|
|
2400
|
+
),
|
|
2401
|
+
),
|
|
2402
|
+
minimized&&h("div",{className:"tl-pill",onClick:()=>setMinimized(false)},
|
|
2403
|
+
h("span",{className:"tl-pill-label"},"Transitions"),
|
|
2404
|
+
h("span",{className:cx("tl-pill-count",String(entries.length).length===1&&"is-single")},entries.length)),
|
|
2405
|
+
);
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
// ── demo boxes ──
|
|
2409
|
+
function BoxResize(){const[on,set]=useState(false);return h("div",{className:"card"},h("h2",null,"Resize + Color"),h("p",null,"Click to toggle. Transitions: width, height, background, border-radius."),h("div",{"data-testid":"resize-box",className:"box-resize"+(on?" expanded":""),onClick:()=>set(v=>!v)}));}
|
|
2410
|
+
function BoxOpacity(){const[on,set]=useState(false);return h("div",{className:"card"},h("h2",null,"Opacity + Scale"),h("p",null,"Click to fade. Uses ease-in-out at 600ms."),h("div",{"data-testid":"opacity-box",className:"box-opacity"+(on?" faded":""),onClick:()=>set(v=>!v)}));}
|
|
2411
|
+
function BoxSlide(){const[on,set]=useState(false);return h("div",{className:"card"},h("h2",null,"Slide + Bounce"),h("p",null,"Click to slide. Custom cubic-bezier with overshoot."),h("div",{"data-testid":"slide-box",className:"box-slide"+(on?" moved":""),onClick:()=>set(v=>!v)}));}
|
|
2412
|
+
function BoxColor(){const[on,set]=useState(false);return h("div",{className:"card"},h("h2",null,"Gradient Glow"),h("p",null,"Click to light up. 800ms background + box-shadow."),h("div",{"data-testid":"glow-box",className:"box-color"+(on?" lit":""),onClick:()=>set(v=>!v)}));}
|
|
2413
|
+
|
|
2414
|
+
// ── live controls for the whole-panel reveal transition (demo harness) ──
|
|
2415
|
+
// transitions.dev motion tokens (values lifted verbatim from the skill's _root.css)
|
|
2416
|
+
// transitions.dev easing motion tokens. `token` is the actual skill CSS var
|
|
2417
|
+
// applied to the panel (so the control writes e.g. var(--morph-ease), not a
|
|
2418
|
+
// copied literal); `value` is the resolved curve, used only to match a live
|
|
2419
|
+
// :root value back onto a preset on init.
|
|
2420
|
+
const EASE_PRESETS = [
|
|
2421
|
+
{key:"panel", label:"panel reveal", token:"var(--panel-ease)", value:"cubic-bezier(0.22, 1, 0.36, 1)", usage:"--panel-ease — the standard ease-out shared across most transitions.dev transitions."},
|
|
2422
|
+
{key:"morph", label:"morph open", token:"var(--morph-ease)", value:"cubic-bezier(0.34, 1.25, 0.64, 1)", usage:"--morph-ease — plus → menu morph, a gentle settle past the target."},
|
|
2423
|
+
{key:"check-bob", label:"check bob", token:"var(--check-ease-bob)", value:"cubic-bezier(0.34, 1.35, 0.64, 1)", usage:"--check-ease-bob — success check Y-bob overshoot."},
|
|
2424
|
+
{key:"badge-pop", label:"badge pop", token:"var(--badge-pop-ease)", value:"cubic-bezier(0.34, 1.36, 0.64, 1)", usage:"--badge-pop-ease — notification badge dot pop-in."},
|
|
2425
|
+
{key:"digit", label:"digit pop", token:"var(--digit-ease)", value:"cubic-bezier(0.34, 1.45, 0.64, 1)", usage:"--digit-ease — number pop-in, a springier overshoot."},
|
|
2426
|
+
{key:"avatar", label:"avatar spring", token:"var(--avatar-ease-out)", value:"cubic-bezier(0.34, 3.85, 0.64, 1)", usage:"--avatar-ease-out — avatar group hover return, aggressive spring."},
|
|
2427
|
+
{key:"badge-close", label:"badge close", token:"var(--badge-close-ease)", value:"cubic-bezier(0.4, 0, 0.2, 1)", usage:"--badge-close-ease — accelerate-decelerate close."},
|
|
2428
|
+
{key:"text-swap", label:"text swap", token:"var(--text-swap-ease)", value:"ease-in-out", usage:"--text-swap-ease — in-place text states swap."},
|
|
2429
|
+
{key:"tooltip", label:"tooltip", token:"var(--tt-in-ease)", value:"ease-out", usage:"--tt-in-ease — tooltip open/close."},
|
|
2430
|
+
{key:"shimmer", label:"shimmer", token:"var(--shimmer-ease)", value:"linear", usage:"--shimmer-ease — shimmer text sweep."},
|
|
2431
|
+
{key:"custom", label:"custom cubic-bezier", token:null, value:null, usage:"Hand-tuned cubic-bezier control points."},
|
|
2432
|
+
];
|
|
2433
|
+
const readRootVar=(name)=>getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
2434
|
+
const numFrom=(s,fb)=>{const n=parseFloat(s);return Number.isFinite(n)?n:fb;};
|
|
2435
|
+
const stripWs=(s)=>(s||"").replace(/\s+/g,"");
|
|
2436
|
+
// map a live ease value back onto a preset key (or "custom") + its cubic-bezier
|
|
2437
|
+
const easeKeyFor=(val)=>{const v=stripWs(val);const p=EASE_PRESETS.find(p=>(p.value&&stripWs(p.value)===v)||(p.token&&stripWs(p.token)===v));return p?p.key:"custom";};
|
|
2438
|
+
const bzFor=(val)=>{const m=(val||"").match(/cubic-bezier\(([^)]+)\)/);if(m){const a=m[1].split(",").map(Number);if(a.length===4&&a.every(Number.isFinite))return a;}return[0.22,1,0.36,1];};
|
|
2439
|
+
|
|
2440
|
+
function PcSlider({label,unit,value,min,max,step,onChange}){
|
|
2441
|
+
return h("label",{className:"pc-row"},
|
|
2442
|
+
h("span",{className:"pc-label"},label),
|
|
2443
|
+
h("input",{type:"range",min,max,step:step||1,value,onChange:e=>onChange(Number(e.target.value))}),
|
|
2444
|
+
h("span",{className:"pc-val"},value+unit));
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
function PcEase({label,easeKey,bz,onKey,onBz}){
|
|
2448
|
+
return h(React.Fragment,null,
|
|
2449
|
+
h("div",{className:"pc-select-row"},
|
|
2450
|
+
h("span",{className:"pc-label"},label),
|
|
2451
|
+
h("select",{value:easeKey,onChange:e=>onKey(e.target.value)},
|
|
2452
|
+
EASE_PRESETS.map(p=>h("option",{key:p.key,value:p.key,title:p.usage},p.label)))),
|
|
2453
|
+
easeKey==="custom"&&h("div",{className:"pc-bz"},
|
|
2454
|
+
bz.map((v,i)=>h("input",{key:i,type:"number",step:0.01,value:v,
|
|
2455
|
+
onChange:e=>onBz(i,Number(e.target.value))}))));
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
function PanelControls(){
|
|
2459
|
+
const[openDur,setOpenDur] =useState(()=>numFrom(readRootVar("--panel-open-dur"),400));
|
|
2460
|
+
const[closeDur,setCloseDur] =useState(()=>numFrom(readRootVar("--panel-close-dur"),350));
|
|
2461
|
+
const[openDist,setOpenDist] =useState(()=>numFrom(readRootVar("--panel-open-distance"),100));
|
|
2462
|
+
const[closeDist,setCloseDist] =useState(()=>numFrom(readRootVar("--panel-close-distance"),100));
|
|
2463
|
+
const[openScale,setOpenScale] =useState(()=>numFrom(readRootVar("--panel-scale"),0.96));
|
|
2464
|
+
const[closeScale,setCloseScale]=useState(()=>numFrom(readRootVar("--panel-scale-close"),0.96));
|
|
2465
|
+
const[blur,setBlur] =useState(()=>numFrom(readRootVar("--panel-blur"),2));
|
|
2466
|
+
const[openEaseKey,setOpenEaseKey] =useState(()=>easeKeyFor(readRootVar("--panel-open-ease")));
|
|
2467
|
+
const[openBz,setOpenBz] =useState(()=>bzFor(readRootVar("--panel-open-ease")));
|
|
2468
|
+
const[closeEaseKey,setCloseEaseKey] =useState(()=>easeKeyFor(readRootVar("--panel-close-ease")));
|
|
2469
|
+
const[closeBz,setCloseBz] =useState(()=>bzFor(readRootVar("--panel-close-ease")));
|
|
2470
|
+
|
|
2471
|
+
useEffect(()=>{document.documentElement.style.setProperty("--panel-open-dur",openDur+"ms");},[openDur]);
|
|
2472
|
+
useEffect(()=>{document.documentElement.style.setProperty("--panel-close-dur",closeDur+"ms");},[closeDur]);
|
|
2473
|
+
useEffect(()=>{document.documentElement.style.setProperty("--panel-open-distance",openDist+"px");},[openDist]);
|
|
2474
|
+
useEffect(()=>{document.documentElement.style.setProperty("--panel-close-distance",closeDist+"px");},[closeDist]);
|
|
2475
|
+
useEffect(()=>{document.documentElement.style.setProperty("--panel-scale",String(openScale));},[openScale]);
|
|
2476
|
+
useEffect(()=>{document.documentElement.style.setProperty("--panel-scale-close",String(closeScale));},[closeScale]);
|
|
2477
|
+
useEffect(()=>{document.documentElement.style.setProperty("--panel-blur",blur+"px");},[blur]);
|
|
2478
|
+
useEffect(()=>{
|
|
2479
|
+
const preset=EASE_PRESETS.find(p=>p.key===openEaseKey);
|
|
2480
|
+
const val=openEaseKey==="custom"?`cubic-bezier(${openBz.join(", ")})`:(preset&&(preset.token||preset.value));
|
|
2481
|
+
if(val)document.documentElement.style.setProperty("--panel-open-ease",val);
|
|
2482
|
+
},[openEaseKey,openBz]);
|
|
2483
|
+
useEffect(()=>{
|
|
2484
|
+
const preset=EASE_PRESETS.find(p=>p.key===closeEaseKey);
|
|
2485
|
+
const val=closeEaseKey==="custom"?`cubic-bezier(${closeBz.join(", ")})`:(preset&&(preset.token||preset.value));
|
|
2486
|
+
if(val)document.documentElement.style.setProperty("--panel-close-ease",val);
|
|
2487
|
+
},[closeEaseKey,closeBz]);
|
|
2488
|
+
|
|
2489
|
+
const setOpenBzAt=(i,v)=>setOpenBz(prev=>{const next=prev.slice();next[i]=v;return next;});
|
|
2490
|
+
const setCloseBzAt=(i,v)=>setCloseBz(prev=>{const next=prev.slice();next[i]=v;return next;});
|
|
2491
|
+
const fire=(action)=>window.dispatchEvent(new CustomEvent("tl-toggle-panel",{detail:{action}}));
|
|
2492
|
+
|
|
2493
|
+
return h("div",{className:"panel-controls"},
|
|
2494
|
+
h("h3",null,h("span",{className:"pc-dot"}),"Panel transition"),
|
|
2495
|
+
h("div",{className:"pc-group"},"Open"),
|
|
2496
|
+
h(PcSlider,{label:"open dur",unit:"ms",value:openDur,min:0,max:1200,step:10,onChange:setOpenDur}),
|
|
2497
|
+
h(PcSlider,{label:"distance",unit:"px",value:openDist,min:0,max:400,step:1,onChange:setOpenDist}),
|
|
2498
|
+
h(PcSlider,{label:"scale",unit:"",value:openScale,min:0.80,max:1.00,step:0.01,onChange:setOpenScale}),
|
|
2499
|
+
h(PcEase,{label:"ease",easeKey:openEaseKey,bz:openBz,onKey:setOpenEaseKey,onBz:setOpenBzAt}),
|
|
2500
|
+
h("div",{className:"pc-group"},"Close"),
|
|
2501
|
+
h(PcSlider,{label:"close dur",unit:"ms",value:closeDur,min:0,max:1200,step:10,onChange:setCloseDur}),
|
|
2502
|
+
h(PcSlider,{label:"distance",unit:"px",value:closeDist,min:0,max:400,step:1,onChange:setCloseDist}),
|
|
2503
|
+
h(PcSlider,{label:"scale",unit:"",value:closeScale,min:0.80,max:1.00,step:0.01,onChange:setCloseScale}),
|
|
2504
|
+
h(PcEase,{label:"ease",easeKey:closeEaseKey,bz:closeBz,onKey:setCloseEaseKey,onBz:setCloseBzAt}),
|
|
2505
|
+
h("div",{className:"pc-group"},"Shared"),
|
|
2506
|
+
h(PcSlider,{label:"blur",unit:"px",value:blur,min:0,max:12,step:0.5,onChange:setBlur}),
|
|
2507
|
+
h("div",{className:"pc-btns"},
|
|
2508
|
+
h("button",{className:"pc-btn",onClick:()=>fire("toggle")},"Toggle panel"),
|
|
2509
|
+
h("button",{className:"pc-btn primary",onClick:()=>fire("replay")},"Replay open")),
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
function App(){
|
|
2514
|
+
const rootRef=useRef(null);const registry=useMemo(()=>new TransitionRegistry(),[]);const preview=useMemo(()=>new PreviewController(),[]);const[activeId,setActiveId]=useState(null);
|
|
2515
|
+
useEffect(()=>{const root=rootRef.current??document.body;const scanner=new DomScanner(root,registry);preview.setScanner(scanner);scanner.start();return()=>{scanner.stop();preview.setScanner(null);};},[registry,preview]);
|
|
2516
|
+
const ctx=useMemo(()=>({registry,preview,activeId,setActiveId}),[registry,preview,activeId]);
|
|
2517
|
+
// Demo-only tweak controls: hidden by default. Append ?controls to the URL
|
|
2518
|
+
// to show them for testing. (This whole block is below the inject CUT_MARKER,
|
|
2519
|
+
// so it never ships in the injected build.)
|
|
2520
|
+
const showControls=(()=>{try{return new URLSearchParams(location.search).has("controls");}catch(e){return false;}})();
|
|
2521
|
+
return h(TimelineCtx.Provider,{value:ctx},
|
|
2522
|
+
showControls&&h(PanelControls),
|
|
2523
|
+
h("div",{ref:rootRef,className:"demo-root"},h("div",{className:"demo-header"},h("h1",null,"Timeline Inspector \u2014 Demo"),h("p",null,"CSS transitions are automatically detected. Select one below, drag the bars to edit timing, then Play.")),
|
|
2524
|
+
h("div",{className:"demo-grid"},h(BoxResize),h(BoxOpacity),h(BoxSlide),h(BoxColor))),
|
|
2525
|
+
h(TimelinePanel));
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
createRoot(document.getElementById("root")).render(h(App));
|
|
2529
|
+
</script>
|
|
2530
|
+
</body>
|
|
2531
|
+
</html>
|