transitions-refine 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/demo.html +451 -672
- package/package.json +1 -1
- package/server/inject.mjs +3 -5
- package/server/relay.mjs +39 -6
package/demo.html
CHANGED
|
@@ -31,9 +31,14 @@
|
|
|
31
31
|
--c-line: #ededf0; --c-hairline: rgba(0,0,0,0.04); --c-hairline2: rgba(0,0,0,0.06);
|
|
32
32
|
/* shadow recipes (exact Figma) */
|
|
33
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
|
-
|
|
34
|
+
/* blue button inset ring (exact Figma node 580:753) */
|
|
35
|
+
--ring-blue: inset 0 0 0 1px rgba(0,101,208,0.1), inset 0 -1px 0 0 rgba(3,66,142,0.15);
|
|
35
36
|
--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
37
|
--drop: 0 1px 3px 0 rgba(0,0,0,0.04);
|
|
38
|
+
/* topbar button drop shadow — exact Figma node 580:753 (navy-tinted) */
|
|
39
|
+
--drop-btn: 0 1px 3px 0 rgba(4,41,117,0.08);
|
|
40
|
+
/* secondary/raised white button (Reset, Settings, Copy, Accept) — exact spec */
|
|
41
|
+
--shadow-btn: 0 1px 3px 0 rgba(0,0,0,0.04), inset 0 0 0 1px rgba(0,0,0,0.06), inset 0 -1px 0 0 rgba(0,0,0,0.10), inset 0 0 0 1px rgba(196,196,196,0.10);
|
|
37
42
|
--drop-line: 0 1px 3px 0 rgba(0,0,0,0.08);
|
|
38
43
|
--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
44
|
--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);
|
|
@@ -53,6 +58,16 @@
|
|
|
53
58
|
/* transitions.dev — easing motion tokens (verbatim from skill _root.css) */
|
|
54
59
|
--panel-ease: cubic-bezier(0.22, 1, 0.36, 1); /* shared standard ease-out */
|
|
55
60
|
--morph-ease: cubic-bezier(0.34, 1.25, 0.64, 1);
|
|
61
|
+
--morph-open-dur: 350ms;
|
|
62
|
+
--morph-close-dur: 250ms;
|
|
63
|
+
--morph-close-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
64
|
+
--morph-r-closed: 40px;
|
|
65
|
+
--morph-r-open: 20px;
|
|
66
|
+
--morph-fade-dur: 200ms;
|
|
67
|
+
--morph-slide: 40px;
|
|
68
|
+
--morph-rotate: 45deg;
|
|
69
|
+
--morph-scale: 0.97;
|
|
70
|
+
--morph-blur: 2px;
|
|
56
71
|
--check-ease-bob: cubic-bezier(0.34, 1.35, 0.64, 1);
|
|
57
72
|
--badge-pop-ease: cubic-bezier(0.34, 1.36, 0.64, 1);
|
|
58
73
|
--digit-ease: cubic-bezier(0.34, 1.45, 0.64, 1);
|
|
@@ -89,23 +104,93 @@
|
|
|
89
104
|
.card h2 { font-size: 15px; font-weight: 600; margin-bottom: 4px; color: #171717; }
|
|
90
105
|
.card p { font-size: 12px; color: var(--c-ruler); margin-bottom: 16px; line-height: 1.5; }
|
|
91
106
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
.box-resize.expanded { width: 180px; height: 140px; background: #6c8eef; border-radius: 24px; }
|
|
107
|
+
/* Anchor sized to the OPEN footprint so the box grows up-and-left out of the corner. */
|
|
108
|
+
.morph-stage { position: relative; width: 183px; height: 172px; margin: 8px auto 0; }
|
|
95
109
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
.
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
/* Plus → menu morph — verbatim from transitions.dev skill (20-plus-menu-morph.md).
|
|
111
|
+
Closed: a small circular button. Open: a rounded panel.
|
|
112
|
+
Width/height/border-radius animate; the open state uses a
|
|
113
|
+
bouncier ease than the close. */
|
|
114
|
+
.t-morph {
|
|
115
|
+
position: absolute;
|
|
116
|
+
inset: auto 0 0 auto;
|
|
117
|
+
width: 40px;
|
|
118
|
+
height: 40px;
|
|
119
|
+
border-radius: var(--morph-r-closed);
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
background: #fff;
|
|
122
|
+
box-shadow: var(--card-shadow);
|
|
123
|
+
transition:
|
|
124
|
+
width var(--morph-close-dur) var(--morph-close-ease),
|
|
125
|
+
height var(--morph-close-dur) var(--morph-close-ease),
|
|
126
|
+
border-radius var(--morph-close-dur) var(--morph-close-ease);
|
|
127
|
+
}
|
|
128
|
+
.t-morph[data-open="true"] {
|
|
129
|
+
width: 183px;
|
|
130
|
+
height: 172px;
|
|
131
|
+
border-radius: var(--morph-r-open);
|
|
132
|
+
transition:
|
|
133
|
+
width 400ms var(--morph-ease),
|
|
134
|
+
height var(--morph-open-dur) var(--morph-ease),
|
|
135
|
+
border-radius var(--morph-open-dur) var(--morph-ease);
|
|
136
|
+
}
|
|
137
|
+
/* Plus fades + slides out and the icon rotates into an ×. */
|
|
138
|
+
.t-morph-plus {
|
|
139
|
+
position: absolute;
|
|
140
|
+
inset: auto 0 0 auto;
|
|
141
|
+
width: 40px; height: 40px;
|
|
142
|
+
display: grid; place-items: center;
|
|
143
|
+
border: 0; background: transparent; cursor: pointer;
|
|
144
|
+
color: var(--c-text);
|
|
145
|
+
transition:
|
|
146
|
+
opacity var(--morph-fade-dur) var(--morph-close-ease),
|
|
147
|
+
transform var(--morph-open-dur) var(--morph-close-ease),
|
|
148
|
+
filter var(--morph-fade-dur) var(--morph-close-ease);
|
|
149
|
+
}
|
|
150
|
+
.t-morph-plus svg {
|
|
151
|
+
transition: transform var(--morph-open-dur) var(--morph-close-ease);
|
|
152
|
+
}
|
|
153
|
+
.t-morph[data-open="true"] .t-morph-plus {
|
|
154
|
+
opacity: 0;
|
|
155
|
+
transform: translateX(calc(-1 * var(--morph-slide)));
|
|
156
|
+
filter: blur(var(--morph-blur));
|
|
157
|
+
pointer-events: none;
|
|
158
|
+
}
|
|
159
|
+
.t-morph[data-open="true"] .t-morph-plus svg {
|
|
160
|
+
transform: scale(var(--morph-scale)) rotate(var(--morph-rotate));
|
|
161
|
+
}
|
|
162
|
+
/* Menu starts slid in + scaled + blurred; reveals on open. */
|
|
163
|
+
.t-morph-menu {
|
|
164
|
+
position: absolute;
|
|
165
|
+
inset: 0;
|
|
166
|
+
opacity: 0;
|
|
167
|
+
transform: translateX(var(--morph-slide)) scale(var(--morph-scale));
|
|
168
|
+
filter: blur(var(--morph-blur));
|
|
169
|
+
pointer-events: none;
|
|
170
|
+
transition:
|
|
171
|
+
opacity var(--morph-fade-dur) var(--morph-close-ease),
|
|
172
|
+
transform var(--morph-open-dur) var(--morph-close-ease),
|
|
173
|
+
filter var(--morph-fade-dur) var(--morph-close-ease);
|
|
174
|
+
}
|
|
175
|
+
.t-morph[data-open="true"] .t-morph-menu {
|
|
176
|
+
opacity: 1;
|
|
177
|
+
transform: translateX(0) scale(1);
|
|
178
|
+
filter: blur(0);
|
|
179
|
+
pointer-events: auto;
|
|
180
|
+
}
|
|
181
|
+
/* menu contents */
|
|
182
|
+
.t-morph-menu { padding: 12px; display: flex; flex-direction: column; }
|
|
183
|
+
.t-morph-menu .morph-title { font-size: 12px; font-weight: 600; color: var(--c-text-mut);
|
|
184
|
+
padding: 4px 8px 8px; }
|
|
185
|
+
.t-morph-menu button.morph-item { display: flex; align-items: center; gap: 10px;
|
|
186
|
+
width: 100%; padding: 9px 8px; border: 0; background: transparent; border-radius: 8px;
|
|
187
|
+
font: inherit; font-size: 13px; color: var(--c-text); text-align: left; cursor: pointer;
|
|
188
|
+
transition: background 120ms ease; }
|
|
189
|
+
.t-morph-menu button.morph-item:hover { background: var(--c-ghost-h); }
|
|
103
190
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
.box-color.lit { background: linear-gradient(135deg, #6c8eef, #ef6c9c);
|
|
108
|
-
box-shadow: 0 0 30px rgba(108, 142, 239, 0.4); }
|
|
191
|
+
@media (prefers-reduced-motion: reduce) {
|
|
192
|
+
.t-morph, .t-morph-plus, .t-morph-menu { transition: none !important; }
|
|
193
|
+
}
|
|
109
194
|
/* @inject-skip-end */
|
|
110
195
|
|
|
111
196
|
/* ───────────────────── timeline panel ───────────────────── */
|
|
@@ -154,7 +239,7 @@
|
|
|
154
239
|
.tl-pill-count.is-single { width: 20px; padding: 0; }
|
|
155
240
|
|
|
156
241
|
/* ── header row ── */
|
|
157
|
-
.tl-header { flex: none; display: flex; align-items: center; gap: 8px; padding:
|
|
242
|
+
.tl-header { flex: none; position: relative; z-index: 40; display: flex; align-items: center; gap: 8px; padding: 7.5px 14px;
|
|
158
243
|
border-bottom: 1px solid var(--c-line); }
|
|
159
244
|
.tl-header-label { font-size: 13px; font-weight: 500; line-height: 18px; color: var(--c-text-mut2); white-space: nowrap; }
|
|
160
245
|
.tl-header-count { margin-left: auto; font-size: 13px; line-height: 18px; color: var(--c-count); white-space: nowrap; }
|
|
@@ -166,10 +251,9 @@
|
|
|
166
251
|
.tl-header .tl-sec-btn,
|
|
167
252
|
.tl-header .tl-accept-btn,
|
|
168
253
|
.tl-header .tl-refine-btn { border-radius: 60px; }
|
|
169
|
-
.tl-accept-btn { display: inline-flex; align-items: center; gap:
|
|
170
|
-
border: none; cursor: pointer; font-weight: 500; font-size: 13px; line-height: 14px; color: #17181C;
|
|
171
|
-
background: #fff; box-shadow:
|
|
172
|
-
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.10);
|
|
254
|
+
.tl-accept-btn { display: inline-flex; align-items: center; gap: 6px; height: 36px; padding: 0 14px;
|
|
255
|
+
border: none; cursor: pointer; font: inherit; font-weight: 500; font-size: 13px; line-height: 14px; color: #17181C;
|
|
256
|
+
background: #fff; box-shadow: var(--shadow-btn);
|
|
173
257
|
transition: background 140ms ease, scale 140ms ease, opacity 140ms ease; }
|
|
174
258
|
.tl-accept-btn > svg { width: 16px; height: 16px; color: #17181C; flex: none; }
|
|
175
259
|
.tl-accept-btn:hover:not(:disabled) { background: #f9f9f9; }
|
|
@@ -191,30 +275,38 @@
|
|
|
191
275
|
.tl-ghost-btn .tl-dim { color: var(--c-text-faint); font-weight: 500; }
|
|
192
276
|
.tl-ghost-chev { display: flex; color: var(--c-ruler); }
|
|
193
277
|
|
|
194
|
-
/* icon button:
|
|
278
|
+
/* icon button: secondary fill, raised with the shared Figma button shadow */
|
|
195
279
|
.tl-icon-btn { position: relative; display: inline-flex; align-items: center; justify-content: center; flex: none;
|
|
196
280
|
width: 36px; height: 36px; border: none; border-radius: 8px; background: var(--c-sec);
|
|
281
|
+
box-shadow: var(--shadow-btn);
|
|
197
282
|
color: var(--c-text-mut2); cursor: pointer; transition: background 0.12s ease, color 0.12s ease, scale 0.12s ease; }
|
|
198
283
|
.tl-icon-btn:hover:not(:disabled), .tl-icon-btn.is-active { background: var(--c-sec-h); }
|
|
199
284
|
.tl-icon-btn:active:not(:disabled) { background: var(--c-sec-a); scale: 0.96; }
|
|
200
285
|
.tl-icon-btn:disabled { opacity: 0.5; cursor: default; }
|
|
201
|
-
.tl-icon-btn.ghost { background: transparent; }
|
|
286
|
+
.tl-icon-btn.ghost { background: transparent; box-shadow: none; }
|
|
202
287
|
.tl-icon-btn.ghost:hover:not(:disabled) { background: var(--c-sec); }
|
|
203
|
-
|
|
204
|
-
|
|
288
|
+
/* header Settings + Copy use the same white styling as the Reset button */
|
|
289
|
+
.tl-header .tl-icon-btn:not(.ghost) { background: #fff; }
|
|
290
|
+
.tl-header .tl-icon-btn:not(.ghost):hover:not(:disabled),
|
|
291
|
+
.tl-header .tl-icon-btn:not(.ghost).is-active { background: #f9f9f9; }
|
|
292
|
+
.tl-header .tl-icon-btn:not(.ghost):active:not(:disabled) { background: #f9f9f9; scale: 0.96; }
|
|
293
|
+
|
|
294
|
+
/* secondary button (Reset): Figma default is white, raised with the shared
|
|
295
|
+
button shadow; hover/pressed darken to #f9f9f9 (matches Accept states) */
|
|
205
296
|
.tl-sec-btn { display: inline-flex; align-items: center; height: 36px; padding: 0 16px; border: none;
|
|
206
|
-
border-radius: 8px; background:
|
|
297
|
+
border-radius: 8px; background: #fff; box-shadow: var(--shadow-btn);
|
|
298
|
+
color: var(--c-text); font: inherit; font-size: 13px;
|
|
207
299
|
font-weight: 500; cursor: pointer; transition: background 0.12s ease, scale 0.12s ease; white-space: nowrap; }
|
|
208
|
-
.tl-sec-btn:hover:not(:disabled) { background:
|
|
209
|
-
.tl-sec-btn:active:not(:disabled) { background:
|
|
300
|
+
.tl-sec-btn:hover:not(:disabled) { background: #f9f9f9; }
|
|
301
|
+
.tl-sec-btn:active:not(:disabled) { background: #f9f9f9; scale: 0.96; }
|
|
210
302
|
.tl-sec-btn:disabled { opacity: 0.5; cursor: default; }
|
|
211
303
|
|
|
212
304
|
/* blue button (Refine) — Figma Frame 427319678: layered fill + inset ring + drop shadow */
|
|
213
305
|
.tl-refine-btn { position: relative; isolation: isolate;
|
|
214
306
|
display: inline-flex; align-items: center; gap: 8px; height: 36px; padding: 0 12px;
|
|
215
307
|
border: none; border-radius: 8px; background: transparent; color: var(--c-blue);
|
|
216
|
-
box-shadow: var(--drop);
|
|
217
|
-
box-shadow: 0 1px 3px 0 color(display-p3 0 0 0 / 0.
|
|
308
|
+
box-shadow: var(--drop-btn);
|
|
309
|
+
box-shadow: 0 1px 3px 0 color(display-p3 0.0157 0.1608 0.4588 / 0.08);
|
|
218
310
|
font: inherit; font-size: 13px; font-weight: 500; line-height: 14px;
|
|
219
311
|
cursor: pointer; white-space: nowrap; transition: color 0.12s ease; }
|
|
220
312
|
.tl-refine-btn::before { content: ""; position: absolute; inset: 0; border-radius: inherit;
|
|
@@ -223,7 +315,7 @@
|
|
|
223
315
|
transition: background 0.12s ease; z-index: 0; }
|
|
224
316
|
.tl-refine-btn::after { content: ""; position: absolute; inset: 0; border-radius: inherit;
|
|
225
317
|
box-shadow: var(--ring-blue);
|
|
226
|
-
box-shadow: inset 0 0 0 1px color(display-p3 0 0.
|
|
318
|
+
box-shadow: inset 0 0 0 1px color(display-p3 0 0.3961 0.8157 / 0.10), inset 0 -1px 0 0 color(display-p3 0.0118 0.2588 0.5569 / 0.15);
|
|
227
319
|
pointer-events: none; z-index: 2; }
|
|
228
320
|
.tl-refine-btn > * { position: relative; z-index: 1; }
|
|
229
321
|
/* Figma: wand icon stroke is #0073E5, distinct from the #0071e2 label */
|
|
@@ -249,61 +341,17 @@
|
|
|
249
341
|
@media (prefers-reduced-motion: reduce) {
|
|
250
342
|
.tl-refine-btn:hover:not(:disabled) .tl-refine-sparks i { animation: none; } }
|
|
251
343
|
|
|
252
|
-
/* ── transport row ── */
|
|
253
|
-
.tl-transport { display: flex; align-items: center; gap: 8px; padding: 10px 16px; border-bottom: 1px solid var(--c-line); }
|
|
254
|
-
.tl-transport-left { flex: 1; display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
255
|
-
.tl-transport-center { flex: 0 0 auto; display: flex; justify-content: center; }
|
|
256
|
-
.tl-transport-right { flex: 1; display: flex; align-items: center; justify-content: flex-end; gap: 12px; }
|
|
257
|
-
.tl-transport .tl-ghost-btn,
|
|
258
|
-
.tl-transport .tl-icon-btn,
|
|
259
|
-
.tl-transport .tl-play-btn { border-radius: 60px; }
|
|
260
|
-
.tl-timecode { font-variant-numeric: tabular-nums; font-size: 13px; font-weight: 500; color: var(--c-text); min-width: 70px; }
|
|
261
|
-
.tl-speed { font-variant-numeric: tabular-nums; }
|
|
262
|
-
.tl-zoom { position: relative; width: 72px; height: 15px; flex: none; }
|
|
263
|
-
/* slider — Logram design system (node 13064:2539): 15px frame, track at y=5, 15px knob at y=0 */
|
|
264
|
-
.tl-zoom-track { position: absolute; left: 0; right: 0; top: 5px; height: 5px; pointer-events: none;
|
|
265
|
-
background: rgba(115,115,115,0.1); border: 1px solid rgba(0,0,0,0.08); border-radius: 7px; box-sizing: border-box; }
|
|
266
|
-
.tl-zoom input[type="range"] { position: relative; z-index: 1; width: 100%; height: 15px; margin: 0;
|
|
267
|
-
-webkit-appearance: none; appearance: none; background: transparent; outline: none; cursor: pointer; }
|
|
268
|
-
.tl-zoom input[type="range"]::-webkit-slider-runnable-track { height: 15px; background: transparent; border: none; }
|
|
269
|
-
.tl-zoom input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none;
|
|
270
|
-
width: 15px; height: 15px; margin-top: 0; border-radius: 50%; background: #fff; cursor: pointer;
|
|
271
|
-
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); }
|
|
272
|
-
.tl-zoom input[type="range"]::-moz-range-track { height: 15px; background: transparent; border: none; }
|
|
273
|
-
.tl-zoom input[type="range"]::-moz-range-thumb { width: 15px; height: 15px; border: none; border-radius: 50%;
|
|
274
|
-
background: #fff; cursor: pointer;
|
|
275
|
-
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); }
|
|
276
|
-
/* play/pause circle — Figma: drop shadow + inset ring overlay */
|
|
277
|
-
.tl-play-btn { position: relative; isolation: isolate; overflow: hidden;
|
|
278
|
-
display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px;
|
|
279
|
-
border: none; border-radius: 50%; background: var(--c-circle); color: #17181c;
|
|
280
|
-
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.04); cursor: pointer;
|
|
281
|
-
transition: background 0.12s ease, scale 0.12s ease; }
|
|
282
|
-
.tl-play-btn::after { content: ""; position: absolute; inset: 0; border-radius: inherit;
|
|
283
|
-
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06), inset 0 -1px 0 0 rgba(0, 0, 0, 0.06),
|
|
284
|
-
inset 0 0 0 1px rgba(196, 196, 196, 0.10); pointer-events: none; }
|
|
285
|
-
.tl-play-btn > * { position: relative; z-index: 1; }
|
|
286
|
-
.tl-play-btn:hover:not(:disabled) { background: var(--c-circle-h); }
|
|
287
|
-
.tl-play-btn:active:not(:disabled) { background: var(--c-circle-a); scale: 0.96; }
|
|
288
|
-
.tl-play-btn:disabled { opacity: 0.5; cursor: default; }
|
|
289
|
-
/* center both icon layers in the swap cell so the differently-sized play (10×12)
|
|
290
|
-
and pause (16²) svgs share one center; nudge ONLY the play triangle right for
|
|
291
|
-
optical centering (skill: play-button triangles). left/position (not transform)
|
|
292
|
-
so it doesn't clash with the icon-swap scale animation; pause stays dead-center. */
|
|
293
|
-
.tl-play-btn .t-icon { place-self: center; }
|
|
294
|
-
.tl-play-btn .t-icon[data-icon="a"] { position: relative; left: 1px; }
|
|
295
|
-
|
|
296
344
|
/* ── body / floating cards row ── */
|
|
297
345
|
.tl-body { display: flex; flex: 1; min-height: 0; gap: 0; }
|
|
298
|
-
.tl-main { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
346
|
+
.tl-main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
299
347
|
.tl-inspector { flex: 0 0 280px; padding: 14px 16px 18px; display: flex; flex-direction: column;
|
|
300
348
|
border-left: 1px solid var(--c-line); min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
|
|
301
349
|
.tl-insp-title { font-size: 13px; font-weight: 500; line-height: 18px; color: #171717; margin-bottom: 12px; text-transform: capitalize; }
|
|
302
350
|
.tl-insp-label { font-size: 12px; line-height: 18px; color: #737373; margin: 10px 0 6px; }
|
|
303
351
|
|
|
304
352
|
/* ── tracks / ruler ── */
|
|
305
|
-
.tl-tracks { position: relative; flex: 1; }
|
|
306
|
-
.tl-ruler-row { display: flex; height: 30px; border-bottom: 1px solid var(--c-line); }
|
|
353
|
+
.tl-tracks { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
354
|
+
.tl-ruler-row { flex: none; display: flex; height: 30px; border-bottom: 1px solid var(--c-line); }
|
|
307
355
|
.tl-ruler-spacer { flex: 0 0 150px; border-right: 1px solid var(--c-line); }
|
|
308
356
|
.tl-ruler { position: relative; flex: 1; margin-left: 16px; }
|
|
309
357
|
.tl-ruler .maj { position: absolute; top: 9px; font-size: 12px; line-height: 14px; color: var(--c-ruler); transform: translateX(-50%); white-space: nowrap; }
|
|
@@ -316,23 +364,26 @@
|
|
|
316
364
|
.tl-prop-row.selected { background: #fafafa; }
|
|
317
365
|
.tl-prop-head { flex: 0 0 150px; display: flex; align-items: center; gap: 8px; padding-left: 12px;
|
|
318
366
|
padding-right: 10px; overflow: hidden; border-right: 1px solid var(--c-line); }
|
|
319
|
-
.tl-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
.tl-prop-
|
|
323
|
-
|
|
324
|
-
.tl-prop-row.reordering .tl-prop-grip { color: var(--c-ruler); cursor: grabbing; }
|
|
325
|
-
.tl-prop-label { font-size: 13px; font-weight: 500; line-height: 18px; color: var(--c-text-mut2); white-space: nowrap; overflow: hidden;
|
|
326
|
-
text-overflow: ellipsis; text-transform: capitalize; display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
367
|
+
.tl-rows { position: relative; flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
|
|
368
|
+
/* no overflow:hidden here — the member chip's outset ring would get clipped
|
|
369
|
+
top/bottom; the property name's ellipsis is handled by .tl-prop-prop. */
|
|
370
|
+
.tl-prop-label { font-size: 13px; font-weight: 500; line-height: 18px; color: var(--c-text-mut2); white-space: nowrap;
|
|
371
|
+
text-transform: capitalize; display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
327
372
|
.tl-prop-row.selected .tl-prop-label { color: #171717; }
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
373
|
+
/* member chip — Figma node 580:3567: 63×20 white pill, 1px hairline ring, Inter Regular 11px #696969 (text centered, 1px above/below) */
|
|
374
|
+
.tl-prop-member { flex: none; box-sizing: border-box; display: inline-block; height: 20px; vertical-align: middle;
|
|
375
|
+
font-size: 11px; font-weight: 400; line-height: 20px; padding: 0 6px; border-radius: 50px;
|
|
376
|
+
background: #fff; box-shadow: 0 0 0 1px rgba(0,0,0,0.08); color: #696969; text-transform: none; max-width: 96px;
|
|
377
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
378
|
+
/* selection must NOT restyle the chip — single Figma style in every row state */
|
|
379
|
+
.tl-prop-row.selected .tl-prop-member { background: #fff; color: #696969; box-shadow: 0 0 0 1px rgba(0,0,0,0.08); }
|
|
332
380
|
.tl-prop-prop { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
381
|
+
/* inspector member chip — mirrors right-column chip (Figma 580:3567) */
|
|
382
|
+
.tl-insp-member { box-sizing: border-box; display: inline-block; height: 20px; vertical-align: middle; margin-right: 6px;
|
|
383
|
+
font-size: 11px; font-weight: 400; line-height: 20px; padding: 0 6px; border-radius: 50px;
|
|
384
|
+
background: #fff; box-shadow: 0 0 0 1px rgba(0,0,0,0.08); color: #696969; text-transform: none;
|
|
385
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
386
|
+
/* 16px left inset of the plot area (kept in sync with the ruler) */
|
|
336
387
|
.tl-prop-track { position: relative; flex: 1; user-select: none; margin-left: 16px; }
|
|
337
388
|
/* timeline line — capsule (fully rounded), Figma #eeeeef default / #e9e9e9 hover */
|
|
338
389
|
.tl-bar { position: absolute; top: 50%; height: 24px; transform: translateY(-50%); background: var(--c-track);
|
|
@@ -352,13 +403,16 @@
|
|
|
352
403
|
.tl-bar-grip { position: absolute; top: 0; bottom: 0; width: 10px; cursor: ew-resize; z-index: 2; }
|
|
353
404
|
.tl-bar-grip.left { left: -5px; } .tl-bar-grip.right { right: -5px; }
|
|
354
405
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
.tl-
|
|
359
|
-
|
|
360
|
-
.tl-
|
|
361
|
-
|
|
406
|
+
/* draggable divider between the label column and the bars column. The hit
|
|
407
|
+
area is wide for easy grabbing; the visible line is thin and only accents
|
|
408
|
+
on hover / while dragging (matches the panel's 0.12s hover house style). */
|
|
409
|
+
.tl-col-resizer { position: absolute; top: 0; bottom: 0; width: 11px; transform: translateX(-50%);
|
|
410
|
+
cursor: col-resize; z-index: 6; display: flex; justify-content: center; }
|
|
411
|
+
.tl-col-resizer-line { width: 1px; height: 100%; background: transparent;
|
|
412
|
+
transition: background 0.12s ease, width 0.12s ease; }
|
|
413
|
+
.tl-col-resizer:hover .tl-col-resizer-line,
|
|
414
|
+
.tl-col-resizer.dragging .tl-col-resizer-line { width: 2px; background: #1A7AFF; }
|
|
415
|
+
@media (prefers-reduced-motion: reduce) { .tl-col-resizer-line { transition: none; } }
|
|
362
416
|
|
|
363
417
|
/* ── value field (slider + input) — exact Figma "Value slider and input" ── */
|
|
364
418
|
.tl-field-wrap { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
|
@@ -369,7 +423,8 @@
|
|
|
369
423
|
/* inner slider fill — Figma button/tiny: soft drop shadow + inset ring give the
|
|
370
424
|
"raised block" look (shadow 0 1px 3px @4%, hairline border, bottom 1px). */
|
|
371
425
|
.tl-field-fill { position: absolute; left: 0; top: 0; bottom: 0; min-width: 36px; border-radius: 8px;
|
|
372
|
-
background: var(--c-fill);
|
|
426
|
+
background: var(--c-fill);
|
|
427
|
+
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.02), inset 0 -1px 0 0 rgba(0,0,0,0.08), inset 0 0 0 1px rgba(191,191,191,0.10), 0 1px 3px 0 rgba(0,0,0,0.06);
|
|
373
428
|
pointer-events: none; transition: background 0.12s ease, opacity 0.12s ease; z-index: 1; }
|
|
374
429
|
.tl-field.is-dragging .tl-field-fill { background: var(--c-fill-a); }
|
|
375
430
|
.tl-field.is-dragging .tl-field-label { opacity: 0.7; }
|
|
@@ -486,31 +541,9 @@
|
|
|
486
541
|
.tl-seg-btn:hover:not(.is-active) { background: rgba(170,170,170,0.06); color: #17181c; }
|
|
487
542
|
.tl-seg-btn.is-active { background: rgba(170,170,170,0.1); color: #17181c; }
|
|
488
543
|
|
|
489
|
-
/*
|
|
490
|
-
.tl-
|
|
491
|
-
|
|
492
|
-
.tl-preview-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
|
493
|
-
.tl-preview-title { font-size: 11px; font-weight: 600; color: var(--c-text-mut); }
|
|
494
|
-
.tl-preview-btn { display: inline-flex; align-items: center; gap: 5px; height: 22px; padding: 0 8px 0 7px;
|
|
495
|
-
border: none; border-radius: 6px; background: var(--c-field-bg); color: var(--c-text-mut2);
|
|
496
|
-
font: inherit; font-size: 11px; font-weight: 500; cursor: pointer;
|
|
497
|
-
transition: background 0.12s ease, scale 0.12s ease; }
|
|
498
|
-
.tl-preview-btn:hover { background: var(--c-sec-h); }
|
|
499
|
-
.tl-preview-btn:active { scale: 0.96; }
|
|
500
|
-
.tl-preview-track { position: relative; height: 16px; }
|
|
501
|
-
.tl-preview-rail { position: absolute; left: 0; right: 0; top: 50%; height: 2px; transform: translateY(-50%);
|
|
502
|
-
background: var(--c-track); border-radius: 2px; }
|
|
503
|
-
.tl-preview-end { position: absolute; top: 50%; width: 2px; height: 8px; transform: translateY(-50%);
|
|
504
|
-
background: #d0d0d6; border-radius: 2px; }
|
|
505
|
-
.tl-preview-end.left { left: 0; }
|
|
506
|
-
.tl-preview-end.right { right: 0; }
|
|
507
|
-
.tl-preview-dot { position: absolute; left: 0; top: 50%; width: 14px; height: 14px; margin-top: -7px;
|
|
508
|
-
border-radius: 50%; background: var(--c-blue); box-shadow: 0 1px 3px rgba(0,0,0,0.25);
|
|
509
|
-
will-change: transform; }
|
|
510
|
-
|
|
511
|
-
/* menu section header (non-clickable) */
|
|
512
|
-
.tl-menu-group { padding: 9px 10px 4px; font-size: 10.5px; font-weight: 600; letter-spacing: 0.04em;
|
|
513
|
-
text-transform: uppercase; color: var(--c-text-faint); pointer-events: none; }
|
|
544
|
+
/* menu section header (non-clickable) — Figma node 580:1696 dropdown */
|
|
545
|
+
.tl-menu-group { padding: 12px 8px 8px; font-size: 11px; font-weight: 400; line-height: 14px;
|
|
546
|
+
color: #8b8b8b; pointer-events: none; }
|
|
514
547
|
.tl-menu-group:first-child { padding-top: 4px; }
|
|
515
548
|
|
|
516
549
|
/* spring params box (reuses bounce row layout) */
|
|
@@ -538,10 +571,19 @@
|
|
|
538
571
|
display: flex; align-items: center; justify-content: center;
|
|
539
572
|
background: #fff; border-radius: 12px; box-shadow: var(--card-shadow); }
|
|
540
573
|
|
|
541
|
-
/* ── shared dropdown surface ──
|
|
574
|
+
/* ── shared dropdown surface (Figma node 580:1696) ──
|
|
575
|
+
container: rounded-12 p-8 + menu shadow; rows: h-40 rounded-8 px-12,
|
|
576
|
+
Inter Medium 13/16 #1b1b1b label with #979797 suffix; section headers:
|
|
577
|
+
Inter Regular 11/14 #8b8b8b, pt-12 pb-8 pl-12, no divider. */
|
|
542
578
|
.tl-menu { background: #fff; border-radius: 12px; padding: 6px; box-shadow: var(--menu-shadow); }
|
|
543
|
-
|
|
544
|
-
|
|
579
|
+
/* full-bleed divider: negative side margins cancel the .tl-menu 6px padding so
|
|
580
|
+
the line spans the whole dropdown width; no top gap. */
|
|
581
|
+
.tl-menu-search { margin: 0 -6px 6px; padding: 0 6px 6px; border-bottom: 1px solid var(--c-line); }
|
|
582
|
+
.tl-menu-search-input { width: 100%; height: 32px; box-sizing: border-box; border: none; outline: none;
|
|
583
|
+
background: transparent; font: inherit; font-size: 13px; line-height: 16px; color: #1b1b1b; padding: 0 8px; }
|
|
584
|
+
.tl-menu-search-input::placeholder { color: #999; }
|
|
585
|
+
.tl-menu-item { display: flex; align-items: center; gap: 8px; min-height: 36px; padding: 0 8px;
|
|
586
|
+
border-radius: 8px; font-size: 13px; font-weight: 500; line-height: 16px; color: #1b1b1b; cursor: pointer;
|
|
545
587
|
transition: background 0.1s ease, box-shadow 0.1s ease; }
|
|
546
588
|
.tl-menu-item:hover { background: #f4f4f5; }
|
|
547
589
|
.tl-menu-item:active { background: #ededee; }
|
|
@@ -549,15 +591,14 @@
|
|
|
549
591
|
.tl-menu-item.disabled { color: var(--c-disabled); pointer-events: none; }
|
|
550
592
|
.tl-menu-item-label { flex: 1; min-width: 0; display: flex; align-items: center; }
|
|
551
593
|
.tl-menu-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
552
|
-
.tl-menu-dim { color:
|
|
594
|
+
.tl-menu-dim { color: #979797; }
|
|
553
595
|
.tl-menu-help { color: #c4c4cc; margin-left: 10px; flex: none; cursor: help; }
|
|
554
596
|
.tl-menu-help:hover { color: var(--c-ruler); }
|
|
555
597
|
.tl-menu-help svg { display: block; }
|
|
556
598
|
.tl-menu-check { display: flex; color: var(--c-text-strong); flex: none; }
|
|
557
|
-
.tl-menu-empty { padding: 10px; color: var(--c-disabled); font-size: 13px; }
|
|
558
|
-
.tl-menu-section { padding: 8px
|
|
559
|
-
|
|
560
|
-
.tl-menu-section:not(:first-child) { margin-top: 2px; border-top: 1px solid var(--c-border, rgba(0,0,0,0.06)); padding-top: 8px; }
|
|
599
|
+
.tl-menu-empty { padding: 10px 8px; color: var(--c-disabled); font-size: 13px; }
|
|
600
|
+
.tl-menu-section { padding: 12px 8px 8px; font-size: 11px; font-weight: 400; line-height: 14px;
|
|
601
|
+
color: #8b8b8b; }
|
|
561
602
|
|
|
562
603
|
/* ═════ transitions.dev — menu dropdown (verbatim) ═════ */
|
|
563
604
|
.t-dropdown {
|
|
@@ -637,6 +678,7 @@
|
|
|
637
678
|
background: var(--tt-bg);
|
|
638
679
|
color: var(--tt-fg);
|
|
639
680
|
white-space: nowrap;
|
|
681
|
+
z-index: 100;
|
|
640
682
|
box-shadow:
|
|
641
683
|
0 0 0 1px rgba(0, 0, 0, 0.06),
|
|
642
684
|
0 2px 6px 0 rgba(0, 0, 0, 0.05),
|
|
@@ -751,7 +793,8 @@
|
|
|
751
793
|
/* refine panel slides in from the right using transitions.dev panel reveal
|
|
752
794
|
tokens (translate + opacity + cross-blur, per-phase open/close dur/ease). */
|
|
753
795
|
.tl-refine-panel {
|
|
754
|
-
|
|
796
|
+
/* must sit above the header (z-index:40) so it overlays the whole panel */
|
|
797
|
+
position: absolute; top: 0; right: 0; bottom: 0; z-index: 50;
|
|
755
798
|
width: var(--refine-w, 360px); max-width: 100%; overflow: hidden;
|
|
756
799
|
--panel-translate-x: var(--panel-close-distance);
|
|
757
800
|
--panel-scale-now: var(--panel-scale-close);
|
|
@@ -859,10 +902,7 @@
|
|
|
859
902
|
border: none; border-radius: 60px; cursor: pointer;
|
|
860
903
|
font: inherit; font-size: 13px; font-weight: 500; line-height: 14px; color: #0073e5;
|
|
861
904
|
background: rgba(0,115,229,0.04); text-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
|
862
|
-
box-shadow:
|
|
863
|
-
inset 0 0 0 1px rgba(0,101,208,0.10),
|
|
864
|
-
inset 0 -1px 0 0 rgba(0,0,0,0.06),
|
|
865
|
-
inset 0 0 0 1px rgba(196,196,196,0.10);
|
|
905
|
+
box-shadow: var(--drop-btn), var(--ring-blue);
|
|
866
906
|
--resize-dur: 300ms; --resize-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
867
907
|
transition: width var(--resize-dur) var(--resize-ease),
|
|
868
908
|
height var(--resize-dur) var(--resize-ease),
|
|
@@ -1019,6 +1059,13 @@
|
|
|
1019
1059
|
if(cur)parts.push(cur); return parts;
|
|
1020
1060
|
}
|
|
1021
1061
|
function transitionSignature(props,dur,del,ease) { return [...props].sort().join(",")+"|"+dur+"|"+del+"|"+ease; }
|
|
1062
|
+
// Serialise a set of effective lanes back into a CSS `transition` shorthand.
|
|
1063
|
+
function transitionLanesToCss(lanes){ return (lanes||[]).map(t=>`${t.property} ${formatCssTime(t.durationMs)} ${t.easing} ${formatCssTime(t.delayMs)}`).join(", "); }
|
|
1064
|
+
// Elements we've live-applied edited timings to. Maps element → its ORIGINAL
|
|
1065
|
+
// source lanes ({z}), so the DomScanner keeps the entry pinned to the source
|
|
1066
|
+
// signature/timings even while our inline `transition` overrides the DOM —
|
|
1067
|
+
// otherwise the timing-based id would churn and orphan the user's edits.
|
|
1068
|
+
const _TX_LIVE_BASE = new WeakMap();
|
|
1022
1069
|
|
|
1023
1070
|
const EASING_PRESETS = [
|
|
1024
1071
|
{ keyword:"linear", cubic:[0,0,1,1] }, { keyword:"ease", cubic:[0.25,0.1,0.25,1] },
|
|
@@ -1032,12 +1079,6 @@
|
|
|
1032
1079
|
// Motion tokens are the verbatim cubic-beziers shipped by the transitions.dev
|
|
1033
1080
|
// skill (_root.css); the "Common" set are the standard easings.net curves.
|
|
1034
1081
|
const EASING_LIBRARY = [
|
|
1035
|
-
{ group:"Standard" },
|
|
1036
|
-
{ label:"Linear", value:"linear" },
|
|
1037
|
-
{ label:"Ease", value:"ease" },
|
|
1038
|
-
{ label:"Ease in", value:"ease-in" },
|
|
1039
|
-
{ label:"Ease out", value:"ease-out" },
|
|
1040
|
-
{ label:"Ease in-out", value:"ease-in-out" },
|
|
1041
1082
|
{ group:"Motion tokens (transitions.dev)" },
|
|
1042
1083
|
{ label:"Smooth out", value:"cubic-bezier(0.22, 1, 0.36, 1)", usage:"The transitions.dev default — dropdown, modal, panel, tabs, page slides, accordion." },
|
|
1043
1084
|
{ label:"Standard close", value:"cubic-bezier(0.4, 0, 0.2, 1)", usage:"Material-style accelerate→decelerate. Used for badge close / calm exits." },
|
|
@@ -1046,25 +1087,6 @@
|
|
|
1046
1087
|
{ label:"Morph open", value:"cubic-bezier(0.34, 1.25, 0.64, 1)", usage:"Plus-to-menu morph open — gentle bouncy expand." },
|
|
1047
1088
|
{ label:"Check bob", value:"cubic-bezier(0.34, 1.35, 0.64, 1)", usage:"Success check bob — playful settle on confirmation." },
|
|
1048
1089
|
{ label:"Big overshoot", value:"cubic-bezier(0.34, 3.85, 0.64, 1)", usage:"Avatar group hover return — aggressive, spring-like overshoot." },
|
|
1049
|
-
{ group:"Common (easings.net)" },
|
|
1050
|
-
{ label:"Sine in", value:"cubic-bezier(0.12, 0, 0.39, 0)" },
|
|
1051
|
-
{ label:"Sine out", value:"cubic-bezier(0.61, 1, 0.88, 1)" },
|
|
1052
|
-
{ label:"Sine in-out", value:"cubic-bezier(0.37, 0, 0.63, 1)" },
|
|
1053
|
-
{ label:"Cubic in", value:"cubic-bezier(0.32, 0, 0.67, 0)" },
|
|
1054
|
-
{ label:"Cubic out", value:"cubic-bezier(0.33, 1, 0.68, 1)" },
|
|
1055
|
-
{ label:"Cubic in-out", value:"cubic-bezier(0.65, 0, 0.35, 1)" },
|
|
1056
|
-
{ label:"Quart in", value:"cubic-bezier(0.5, 0, 0.75, 0)" },
|
|
1057
|
-
{ label:"Quart out", value:"cubic-bezier(0.25, 1, 0.5, 1)" },
|
|
1058
|
-
{ label:"Quart in-out", value:"cubic-bezier(0.76, 0, 0.24, 1)" },
|
|
1059
|
-
{ label:"Expo in", value:"cubic-bezier(0.7, 0, 0.84, 0)" },
|
|
1060
|
-
{ label:"Expo out", value:"cubic-bezier(0.16, 1, 0.3, 1)" },
|
|
1061
|
-
{ label:"Expo in-out", value:"cubic-bezier(0.87, 0, 0.13, 1)" },
|
|
1062
|
-
{ label:"Circ in", value:"cubic-bezier(0.55, 0, 1, 0.45)" },
|
|
1063
|
-
{ label:"Circ out", value:"cubic-bezier(0, 0.55, 0.45, 1)" },
|
|
1064
|
-
{ label:"Circ in-out", value:"cubic-bezier(0.85, 0, 0.15, 1)" },
|
|
1065
|
-
{ label:"Back in", value:"cubic-bezier(0.36, 0, 0.66, -0.56)" },
|
|
1066
|
-
{ label:"Back out", value:"cubic-bezier(0.34, 1.56, 0.64, 1)" },
|
|
1067
|
-
{ label:"Back in-out", value:"cubic-bezier(0.68, -0.6, 0.32, 1.6)" },
|
|
1068
1090
|
{ group:"Custom" },
|
|
1069
1091
|
{ label:"cubic-bezier(\u2026)", value:"__cubic" },
|
|
1070
1092
|
{ label:"custom", value:"__custom" },
|
|
@@ -1205,6 +1227,7 @@
|
|
|
1205
1227
|
});
|
|
1206
1228
|
const item={id,kind:"phase",groupId:g.id,groupLabel:g.label||g.id,component:g.component||null,
|
|
1207
1229
|
phase:ph.phase||null,phaseLabel:ph.label||ph.phase||"Phase",
|
|
1230
|
+
stateTarget:ph.stateTarget||null,fromState:(ph.fromState??null),toState:(ph.toState??null),
|
|
1208
1231
|
label:(g.label||g.id)+" · "+(ph.label||ph.phase||"Phase"),durationMs:repDur,
|
|
1209
1232
|
members,effectiveTimings:effTimings,baseLanes};
|
|
1210
1233
|
this.effectiveCache.set(id,item);
|
|
@@ -1226,263 +1249,12 @@
|
|
|
1226
1249
|
}
|
|
1227
1250
|
}
|
|
1228
1251
|
|
|
1229
|
-
// ── preview controller ──
|
|
1230
|
-
// ── transition capture ──
|
|
1231
|
-
// Real-world transitions are usually triggered by interaction (hover, a
|
|
1232
|
-
// click on a *different* button, focus, JS state) — not by clicking the
|
|
1233
|
-
// element that carries the transition. So we observe transitions as they
|
|
1234
|
-
// actually run and remember each property's from/to computed values; the
|
|
1235
|
-
// preview then replays them with no synthetic click required.
|
|
1236
|
-
const _txCapture = new WeakMap(); // Element -> Map<prop,{from,to}>
|
|
1237
|
-
let _txInstalled = false;
|
|
1238
|
-
function _txCamel(p){return p.replace(/-([a-z])/g,(_,c)=>c.toUpperCase());}
|
|
1239
|
-
function _txOnRun(e){
|
|
1240
|
-
const el=e.target, prop=e.propertyName;
|
|
1241
|
-
if(!el||!prop||typeof el.getAnimations!=="function")return;
|
|
1242
|
-
if(el.closest&&el.closest("[data-timeline-panel]"))return;
|
|
1243
|
-
try{
|
|
1244
|
-
const ck=_txCamel(prop);
|
|
1245
|
-
const anims=el.getAnimations();
|
|
1246
|
-
let anim=anims.find(a=>a.transitionProperty===prop);
|
|
1247
|
-
if(!anim)anim=anims.find(a=>{try{const k=a.effect&&a.effect.getKeyframes();return k&&k.length&&(ck in k[0]||ck in k[k.length-1]);}catch{return false;}});
|
|
1248
|
-
if(!anim||!anim.effect)return;
|
|
1249
|
-
const kf=anim.effect.getKeyframes();
|
|
1250
|
-
if(!kf||kf.length<2)return;
|
|
1251
|
-
const from=kf[0][ck], to=kf[kf.length-1][ck];
|
|
1252
|
-
if(from==null||to==null)return;
|
|
1253
|
-
let rec=_txCapture.get(el); if(!rec){rec=new Map();_txCapture.set(el,rec);}
|
|
1254
|
-
rec.set(prop,{from:String(from),to:String(to)});
|
|
1255
|
-
}catch{}
|
|
1256
|
-
}
|
|
1257
|
-
function _txInstallCapture(){
|
|
1258
|
-
if(_txInstalled||typeof document==="undefined")return;
|
|
1259
|
-
_txInstalled=true;
|
|
1260
|
-
// capture phase + the bubbling transitionrun event reaches document
|
|
1261
|
-
document.addEventListener("transitionrun",_txOnRun,true);
|
|
1262
|
-
}
|
|
1263
|
-
// from/to recovered from a previously observed real run
|
|
1264
|
-
function _txCaptured(el,et){
|
|
1265
|
-
const rec=_txCapture.get(el);if(!rec)return null;
|
|
1266
|
-
const from={},to={};let n=0;
|
|
1267
|
-
for(const t of et){const c=rec.get(t.property);if(c){from[t.property]=c.from;to[t.property]=c.to;n++;}}
|
|
1268
|
-
return n?{from,to}:null;
|
|
1269
|
-
}
|
|
1270
|
-
// Discover the transition's end-state WITHOUT interaction by reading the
|
|
1271
|
-
// stylesheets: find rules that set a transitioning prop and represent a
|
|
1272
|
-
// *state* of the element (a :hover/:focus pseudo, or a toggled class/attr
|
|
1273
|
-
// like `.modal.open`). from = current computed value, to = the rule's value.
|
|
1274
|
-
const _TX_STATE_PSEUDO=/:{1,2}(hover|focus|focus-visible|focus-within|active|checked|enabled|disabled|target|visited|link|valid|invalid|placeholder-shown|default)\b(\([^)]*\))?/g;
|
|
1275
|
-
function _txReduceLast(el,sel){
|
|
1276
|
-
const parts=sel.split(/(\s*[>+~]\s*|\s+)/);
|
|
1277
|
-
let i=parts.length-1;while(i>=0&&/^\s*[>+~]?\s*$/.test(parts[i]))i--;
|
|
1278
|
-
if(i<0)return null;
|
|
1279
|
-
const toks=parts[i].match(/[.#]?[\w-]+|\[[^\]]*\]|::?[\w-]+(?:\([^)]*\))?|\*/g)||[];
|
|
1280
|
-
let dropped=false,hasId=false;
|
|
1281
|
-
const kept=toks.filter(tk=>{
|
|
1282
|
-
if(tk[0]==="."){const ok=el.classList.contains(tk.slice(1));if(ok)hasId=true;else dropped=true;return ok;}
|
|
1283
|
-
if(tk[0]==="["){let ok;try{ok=el.matches(tk);}catch{ok=false;}if(ok)hasId=true;else dropped=true;return ok;}
|
|
1284
|
-
if(tk[0]==="#"){const ok=el.id&&("#"+el.id)===tk;if(ok)hasId=true;else dropped=true;return ok;}
|
|
1285
|
-
if(tk[0]===":"){dropped=true;return false;} // pseudo = the state delta
|
|
1286
|
-
return true; // tag / *
|
|
1287
|
-
});
|
|
1288
|
-
// Only a real *variant of this element*: we must have removed a state token
|
|
1289
|
-
// AND kept a class/id/attr that ties the rule to el. Dropping every class to
|
|
1290
|
-
// land on a bare tag/`*` would falsely match unrelated rules.
|
|
1291
|
-
if(!dropped||!hasId)return null;
|
|
1292
|
-
parts[i]=kept.join("");
|
|
1293
|
-
return parts.join("");
|
|
1294
|
-
}
|
|
1295
|
-
function _txIsStateOf(el,sel){
|
|
1296
|
-
try{if(el.matches(sel))return false;}catch{return false;}
|
|
1297
|
-
const noP=sel.replace(_TX_STATE_PSEUDO,"").trim();
|
|
1298
|
-
if(noP&&noP!==sel){try{if(el.matches(noP))return true;}catch{}}
|
|
1299
|
-
const red=_txReduceLast(el,sel);
|
|
1300
|
-
if(red&&red!==sel){try{if(el.matches(red))return true;}catch{}}
|
|
1301
|
-
return false;
|
|
1302
|
-
}
|
|
1303
|
-
function _txScanRules(rules,el,props,to){
|
|
1304
|
-
for(const rule of rules){
|
|
1305
|
-
// Read style rules directly. NOTE: in browsers with CSS Nesting a plain
|
|
1306
|
-
// CSSStyleRule also has a (usually empty) .cssRules, so test selectorText
|
|
1307
|
-
// first — don't treat every style rule as a grouping rule.
|
|
1308
|
-
const decl=rule.style;
|
|
1309
|
-
if(decl&&rule.selectorText){
|
|
1310
|
-
let sets=false;for(const p of props){if(decl.getPropertyValue(p)){sets=true;break;}}
|
|
1311
|
-
if(sets){
|
|
1312
|
-
for(const sub of rule.selectorText.split(",")){
|
|
1313
|
-
const s=sub.trim();if(!s)continue;
|
|
1314
|
-
if(_txIsStateOf(el,s)){for(const p of props){const v=decl.getPropertyValue(p);if(v&&!(p in to))to[p]=v.trim();}}
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
// Recurse into @media / @supports / @layer and any nested rules.
|
|
1319
|
-
if(rule.cssRules&&rule.cssRules.length){
|
|
1320
|
-
try{if(rule.media&&!window.matchMedia(rule.media.mediaText).matches)continue;}catch{}
|
|
1321
|
-
_txScanRules(rule.cssRules,el,props,to);
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
function _txDiscover(el,et){
|
|
1326
|
-
if(typeof document==="undefined"||!el.matches)return null;
|
|
1327
|
-
const props=et.map(t=>t.property).filter(p=>p&&p!=="all");
|
|
1328
|
-
if(!props.length)return null;
|
|
1329
|
-
const to={};
|
|
1330
|
-
for(const sheet of Array.from(document.styleSheets||[])){
|
|
1331
|
-
let rules;try{rules=sheet.cssRules;}catch{continue;} // cross-origin sheet
|
|
1332
|
-
if(rules)_txScanRules(rules,el,props,to);
|
|
1333
|
-
}
|
|
1334
|
-
const keys=Object.keys(to);if(!keys.length)return null;
|
|
1335
|
-
const cs=getComputedStyle(el);const from={};
|
|
1336
|
-
for(const p of keys)from[p]=cs.getPropertyValue(p)||cs[_txCamel(p)]||"";
|
|
1337
|
-
return {from,to};
|
|
1338
|
-
}
|
|
1339
|
-
// Toggle a phase's state class/attribute (e.g. ".is-open" or "[data-open]")
|
|
1340
|
-
// and read the resulting computed values, so a phase plays its real end-state
|
|
1341
|
-
// even when the source uses a toggled class the DOM isn't currently in.
|
|
1342
|
-
function _txToState(el,et,toState){
|
|
1343
|
-
if(typeof document==="undefined"||!el.matches||!toState)return null;
|
|
1344
|
-
const props=et.map(t=>t.property).filter(p=>p&&p!=="all");
|
|
1345
|
-
if(!props.length)return null;
|
|
1346
|
-
const s=String(toState).trim();
|
|
1347
|
-
let applied=null; // {undo}
|
|
1348
|
-
try{
|
|
1349
|
-
if(s[0]==="."){const c=s.slice(1);if(c&&!el.classList.contains(c)){el.classList.add(c);applied=()=>el.classList.remove(c);}}
|
|
1350
|
-
else if(s[0]==="["){
|
|
1351
|
-
const m=s.match(/^\[\s*([\w-]+)\s*(?:([~|^$*]?=)\s*"?([^"\]]*)"?\s*)?\]$/);
|
|
1352
|
-
if(m){const name=m[1],val=m[3]!=null?m[3]:"";if(el.getAttribute(name)!==val){el.setAttribute(name,val);applied=()=>el.removeAttribute(name);}}
|
|
1353
|
-
}
|
|
1354
|
-
}catch{}
|
|
1355
|
-
if(!applied)return null; // already in that state, or unparseable
|
|
1356
|
-
const cs=getComputedStyle(el);const to={};
|
|
1357
|
-
for(const p of props)to[p]=cs.getPropertyValue(p)||cs[_txCamel(p)]||"";
|
|
1358
|
-
try{applied();}catch{}
|
|
1359
|
-
const cs0=getComputedStyle(el);const from={};
|
|
1360
|
-
for(const p of props)from[p]=cs0.getPropertyValue(p)||cs0[_txCamel(p)]||"";
|
|
1361
|
-
const keys=Object.keys(to).filter(p=>to[p]!=null&&to[p]!==""&&to[p]!==from[p]);
|
|
1362
|
-
if(!keys.length)return null;
|
|
1363
|
-
const f={},t={};for(const p of keys){f[p]=from[p];t[p]=to[p];}
|
|
1364
|
-
return {from:f,to:t};
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
class PreviewController {
|
|
1368
|
-
state="idle"; listeners=new Set(); cleanups=[]; animations=[]; progressListeners=new Set(); _rafId=null; scanner=null; _gen=0;
|
|
1369
|
-
rate=1; loop=false; _current=null;
|
|
1370
|
-
setScanner(s){this.scanner=s;} getState(){return this.state;}
|
|
1371
|
-
setRate(r){this.rate=r;for(const a of this.animations){try{a.playbackRate=r;}catch{}}}
|
|
1372
|
-
setLoop(v){this.loop=v;}
|
|
1373
|
-
subscribe(fn){this.listeners.add(fn);return()=>this.listeners.delete(fn);}
|
|
1374
|
-
onProgress(fn){this.progressListeners.add(fn);return()=>this.progressListeners.delete(fn);}
|
|
1375
|
-
play(entry){this.stop();this._gen++;this._current=entry;
|
|
1376
|
-
if(this.scanner)this.scanner.pause();this._playCss(entry,this._gen);this._setState("playing");}
|
|
1377
|
-
// Resolve a selectable item to a flat list of {el, et, toState} arm targets.
|
|
1378
|
-
// Phase items fan out to their members (selector may match several elements,
|
|
1379
|
-
// e.g. staggered items); flat items use their bound DOM elements.
|
|
1380
|
-
_targets(entry){
|
|
1381
|
-
const out=[];
|
|
1382
|
-
if(entry.kind==="phase"){
|
|
1383
|
-
for(const m of (entry.members||[])){
|
|
1384
|
-
if(!m.selector||!m.lanes||!m.lanes.length)continue;
|
|
1385
|
-
let els=[];try{els=Array.from(document.querySelectorAll(m.selector));}catch{}
|
|
1386
|
-
for(const el of els)out.push({el,et:m.lanes,toState:m.toState});
|
|
1387
|
-
}
|
|
1388
|
-
return out;
|
|
1389
|
-
}
|
|
1390
|
-
if(!entry.bindings||entry.bindings.type!=="css")return out;
|
|
1391
|
-
const et=entry.effectiveTimings||entry.properties.map(p=>({property:p,durationMs:entry.durationMs,delayMs:entry.delayMs,easing:entry.easing}));
|
|
1392
|
-
for(const wr of entry.bindings.elements){const el=wr.deref&&wr.deref();if(el)out.push({el,et,toState:null});}
|
|
1393
|
-
return out;
|
|
1394
|
-
}
|
|
1395
|
-
pause(){if(this.state!=="playing")return;for(const a of this.animations){try{a.pause();}catch{}}this._stopPL();this._setState("paused");}
|
|
1396
|
-
resume(){if(this.state!=="paused")return;for(const a of this.animations){try{a.play();}catch{}}this._startPL();this._setState("playing");}
|
|
1397
|
-
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");}
|
|
1398
|
-
restart(entry){this.stop();requestAnimationFrame(()=>this.play(entry));}
|
|
1399
|
-
playPaused(entry,seekMs){
|
|
1400
|
-
this.stop();this._gen++;const gen=this._gen;
|
|
1401
|
-
this._pendingSeek=seekMs;
|
|
1402
|
-
if(this.scanner)this.scanner.pause();
|
|
1403
|
-
const targets=this._targets(entry);
|
|
1404
|
-
if(!targets.length)return;
|
|
1405
|
-
this.animations=[];
|
|
1406
|
-
for(const {el,et,toState} of targets){
|
|
1407
|
-
const restore=this._arm(el,et,toState);
|
|
1408
|
-
requestAnimationFrame(()=>{if(this._gen!==gen)return;const running=el.getAnimations();
|
|
1409
|
-
for(const a of running){a.pause();a.playbackRate=this.rate;this.animations.push(a);}
|
|
1410
|
-
const t=this._pendingSeek??seekMs;
|
|
1411
|
-
if(t!=null){for(const a of this.animations){try{a.currentTime=t;}catch{}}this._ep(t);}
|
|
1412
|
-
});
|
|
1413
|
-
this.cleanups.push(restore);}
|
|
1414
|
-
this._setState("paused");
|
|
1415
|
-
if(seekMs!=null)this._ep(seekMs);
|
|
1416
|
-
}
|
|
1417
|
-
seek(timeMs){
|
|
1418
|
-
if(this.state==="idle")return;
|
|
1419
|
-
if(this.state==="playing"){for(const a of this.animations){try{a.pause();}catch{}}this._stopPL();this._setState("paused");}
|
|
1420
|
-
for(const a of this.animations){try{a.currentTime=timeMs;}catch{}}
|
|
1421
|
-
this._ep(timeMs);
|
|
1422
|
-
this._pendingSeek=timeMs;
|
|
1423
|
-
}
|
|
1424
|
-
_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");}
|
|
1425
|
-
// Arm an element so its transition runs now, without needing the user to
|
|
1426
|
-
// trigger it first. Priority:
|
|
1427
|
-
// 1. a previously observed real run (exact),
|
|
1428
|
-
// 2. end-state discovered from the stylesheets (hover/focus/toggled class),
|
|
1429
|
-
// 3. a synthetic opacity/transform pulse to preview the timing,
|
|
1430
|
-
// 4. click the element (last resort, may have side effects).
|
|
1431
|
-
// Returns a restore fn.
|
|
1432
|
-
_arm(el,et,toState){
|
|
1433
|
-
const saved=el.style.cssText;
|
|
1434
|
-
const tv=et.map(t=>`${t.property} ${formatCssTime(t.durationMs)} ${t.easing} ${formatCssTime(t.delayMs)}`).join(", ");
|
|
1435
|
-
// toState (a phase's driving class/attr) is the most reliable source of
|
|
1436
|
-
// the real end-state; fall back to a captured run, then CSSOM discovery.
|
|
1437
|
-
const states=(toState&&_txToState(el,et,toState))||_txCaptured(el,et)||_txDiscover(el,et);
|
|
1438
|
-
if(states){
|
|
1439
|
-
const props=et.map(t=>t.property).filter(p=>states.from[p]!=null&&states.to[p]!=null);
|
|
1440
|
-
if(props.length){
|
|
1441
|
-
el.style.transition="none";
|
|
1442
|
-
for(const p of props)el.style.setProperty(p,states.from[p]);
|
|
1443
|
-
void el.offsetWidth; // commit the from-state before transitioning
|
|
1444
|
-
el.style.transition=tv;
|
|
1445
|
-
for(const p of props)el.style.setProperty(p,states.to[p]);
|
|
1446
|
-
return ()=>{try{el.style.cssText=saved;}catch{}};
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
const synth=et.filter(t=>t.property==="opacity"||t.property==="transform"||t.property==="all");
|
|
1450
|
-
if(synth.length){
|
|
1451
|
-
const dur=Math.max(...et.map(t=>(t.durationMs||0)+(t.delayMs||0)))||et[0].durationMs||300;
|
|
1452
|
-
const ease=et[0].easing||"ease";
|
|
1453
|
-
const hasOp=synth.some(t=>t.property!=="transform"),hasTr=synth.some(t=>t.property!=="opacity");
|
|
1454
|
-
const k0={},k1={},k2={};
|
|
1455
|
-
if(hasOp){k0.opacity=1;k1.opacity=0.35;k2.opacity=1;}
|
|
1456
|
-
if(hasTr){k0.transform="none";k1.transform="translateY(8px)";k2.transform="none";}
|
|
1457
|
-
try{el.animate([k0,k1,k2],{duration:dur,easing:ease,fill:"none"});return ()=>{};}catch{}
|
|
1458
|
-
}
|
|
1459
|
-
el.style.transition=tv;el.click();
|
|
1460
|
-
return ()=>{try{el.style.cssText=saved;}catch{}};
|
|
1461
|
-
}
|
|
1462
|
-
_playCss(entry,gen){
|
|
1463
|
-
const targets=this._targets(entry);
|
|
1464
|
-
this.animations=[];
|
|
1465
|
-
if(!targets.length){this._finish();return;}
|
|
1466
|
-
let pending=targets.length;
|
|
1467
|
-
for(const {el,et,toState} of targets){
|
|
1468
|
-
const restore=this._arm(el,et,toState);
|
|
1469
|
-
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();
|
|
1470
|
-
// resolve the whole group when every target's animations have settled
|
|
1471
|
-
const onDone=()=>{if(this._gen!==gen||this.state!=="playing")return;pending--;if(pending>0)return;if(this.loop&&this._current){this.play(this._current);}else{this._finish();}};
|
|
1472
|
-
if(running.length>0){Promise.allSettled(running.map(a=>a.finished)).then(onDone);}else{onDone();}});
|
|
1473
|
-
this.cleanups.push(restore);}}
|
|
1474
|
-
_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);}
|
|
1475
|
-
_stopPL(){if(this._rafId!==null){cancelAnimationFrame(this._rafId);this._rafId=null;}}
|
|
1476
|
-
_ep(p){for(const fn of this.progressListeners)fn(p);}
|
|
1477
|
-
_setState(s){this.state=s;for(const fn of this.listeners)fn(s);}
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
1252
|
// ── scanner ──
|
|
1481
1253
|
class DomScanner {
|
|
1482
1254
|
observer=null;rafId=null;running=false;paused=false;
|
|
1483
1255
|
constructor(root,reg){this.root=root;this.registry=reg;}
|
|
1484
1256
|
pause(){this.paused=true;} unpause(){this.paused=false;this._sched();}
|
|
1485
|
-
start(){if(this.running)return;this.running=true;
|
|
1257
|
+
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"]});}
|
|
1486
1258
|
stop(){this.running=false;this.observer?.disconnect();this.observer=null;if(this.rafId!==null){cancelAnimationFrame(this.rafId);this.rafId=null;}}
|
|
1487
1259
|
_sched(){if(this.rafId!==null)return;this.rafId=requestAnimationFrame(()=>{this.rafId=null;if(this.running&&!this.paused)this.scan();});}
|
|
1488
1260
|
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()));}
|
|
@@ -1494,7 +1266,11 @@
|
|
|
1494
1266
|
const durs=(s.transitionDuration||"0s").split(",");
|
|
1495
1267
|
const dels=(s.transitionDelay||"0s").split(",");
|
|
1496
1268
|
const eass=splitCssValues(s.transitionTimingFunction||"ease");
|
|
1497
|
-
|
|
1269
|
+
let z=zipTransitionLists(props,durs,dels,eass);
|
|
1270
|
+
// If we've live-applied edited timings here, keep the entry pinned to the
|
|
1271
|
+
// ORIGINAL source lanes so its id/overrides stay stable across rescans.
|
|
1272
|
+
const _lb=_TX_LIVE_BASE.get(el);
|
|
1273
|
+
if(_lb){ if(_lb.z&&_lb.z.length) z=_lb.z; else return; }
|
|
1498
1274
|
if(!z.length||z.every(x=>x.durationMs===0&&x.delayMs===0))return;
|
|
1499
1275
|
const pDur=z[0].durationMs,pDel=z[0].delayMs,pEase=z[0].easing;
|
|
1500
1276
|
const allP=z.map(x=>x.property);
|
|
@@ -1512,19 +1288,11 @@
|
|
|
1512
1288
|
const TimelineCtx = createContext(null);
|
|
1513
1289
|
function useReg(){const{registry}=useContext(TimelineCtx);return useSyncExternalStore(useCallback(cb=>registry.subscribe(cb),[registry]),useCallback(()=>registry.getAll(),[registry]),useCallback(()=>registry.getAll(),[registry]));}
|
|
1514
1290
|
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};}
|
|
1515
|
-
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]));
|
|
1516
|
-
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])};}
|
|
1517
1291
|
function usePropOverride(){const{registry,activeId}=useContext(TimelineCtx);return{setPropOverride:useCallback((prop,o)=>{if(activeId)registry.setPropOverride(activeId,prop,o);},[registry,activeId])};}
|
|
1518
1292
|
|
|
1519
1293
|
// ── components ──
|
|
1520
|
-
const
|
|
1521
|
-
const ZOOM_MIN = 25;
|
|
1522
|
-
const ZOOM_MAX = 400;
|
|
1523
|
-
const ZOOM_DEFAULT = 100;
|
|
1524
|
-
const scaleFromZoom = zoom => Math.round(BASE_SCALE_MS * 100 / zoom);
|
|
1525
|
-
const LABEL_W = 150;
|
|
1294
|
+
const LABEL_W = 200;
|
|
1526
1295
|
const CLOSE_MS = 150;
|
|
1527
|
-
const SPEEDS = [0.25, 0.5, 1, 2];
|
|
1528
1296
|
const DURATION_TOKENS = [
|
|
1529
1297
|
{label:"Duration-fast", ms:150, usage:"Quick state changes — hovers, toggles, button presses, dropdown & modal close, text swaps."},
|
|
1530
1298
|
{label:"Duration-medium", ms:250, usage:"Standard UI motion — icon swaps, dropdown & modal open, sliding tabs, page slides."},
|
|
@@ -1538,8 +1306,6 @@
|
|
|
1538
1306
|
{label:"Long", ms:300, usage:"Pronounced wait — use sparingly to draw attention."},
|
|
1539
1307
|
];
|
|
1540
1308
|
function cx(...a){ return a.filter(Boolean).join(" "); }
|
|
1541
|
-
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); }
|
|
1542
|
-
function fmtSpeed(s){ return s+"x"; }
|
|
1543
1309
|
|
|
1544
1310
|
// ── icons ──
|
|
1545
1311
|
// Exact SVGs exported from the Logram ❖ Design System Figma file (no recreations).
|
|
@@ -1563,6 +1329,8 @@
|
|
|
1563
1329
|
};
|
|
1564
1330
|
ICONS.minimize = ICONS.chevron;
|
|
1565
1331
|
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"/>`};
|
|
1332
|
+
// dots-vertical (Figma node 580:4819) — vertical "⋮" overflow icon
|
|
1333
|
+
ICONS.dotsv = {vb:"0 0 16 16", svg:`<path fill-rule="evenodd" clip-rule="evenodd" d="M6.66667 8C6.66667 7.26362 7.26362 6.66667 8 6.66667C8.73638 6.66667 9.33333 7.26362 9.33333 8C9.33333 8.73638 8.73638 9.33333 8 9.33333C7.26362 9.33333 6.66667 8.73638 6.66667 8Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M6.66667 3.33333C6.66667 2.59695 7.26362 2 8 2C8.73638 2 9.33333 2.59695 9.33333 3.33333C9.33333 4.06971 8.73638 4.66667 8 4.66667C7.26362 4.66667 6.66667 4.06971 6.66667 3.33333Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M6.66667 12.6667C6.66667 11.9303 7.26362 11.3333 8 11.3333C8.73638 11.3333 9.33333 11.9303 9.33333 12.6667C9.33333 13.403 8.73638 14 8 14C7.26362 14 6.66667 13.403 6.66667 12.6667Z" fill="currentColor"/>`};
|
|
1566
1334
|
function Ic({name, size=16}){
|
|
1567
1335
|
const ic = ICONS[name];
|
|
1568
1336
|
if(!ic) return null;
|
|
@@ -1574,13 +1342,6 @@
|
|
|
1574
1342
|
style:{display:"block"},dangerouslySetInnerHTML:{__html:ic.svg}});
|
|
1575
1343
|
}
|
|
1576
1344
|
|
|
1577
|
-
function usePreviewTime(){
|
|
1578
|
-
const{preview}=useContext(TimelineCtx);
|
|
1579
|
-
const[ms,setMs]=useState(0);
|
|
1580
|
-
useEffect(()=>preview.onProgress(setMs),[preview]);
|
|
1581
|
-
return ms;
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
1345
|
// portaled, origin-aware dropdown surface (transitions.dev menu-dropdown)
|
|
1585
1346
|
function Dropdown({open,onClose,triggerRef,width,align,children}){
|
|
1586
1347
|
const ref=useRef(null);
|
|
@@ -1782,7 +1543,10 @@
|
|
|
1782
1543
|
},[phase]);
|
|
1783
1544
|
if(!render)return null;
|
|
1784
1545
|
|
|
1785
|
-
|
|
1546
|
+
// one scan returns both kinds; each tab shows only its own:
|
|
1547
|
+
// Replace transition → kind "replace"; Small refinements → token tweaks.
|
|
1548
|
+
const visible = suggestions.filter(s=> refineType==="replace" ? s.kind==="replace" : s.kind!=="replace");
|
|
1549
|
+
const pending = visible.filter(s=>!appliedIds[s.id]);
|
|
1786
1550
|
const agentMode = mode==="llm";
|
|
1787
1551
|
// null (unknown / still probing) is treated as ready so the panel doesn't
|
|
1788
1552
|
// flash the "unavailable" copy before /health resolves.
|
|
@@ -1819,7 +1583,7 @@
|
|
|
1819
1583
|
h("div",{className:"tl-refine-unavail-text"},error||"The agent reported an error."));
|
|
1820
1584
|
foot = startBtn({label:"Try again"});
|
|
1821
1585
|
} else if(phase==="done"){
|
|
1822
|
-
if(
|
|
1586
|
+
if(visible.length===0){
|
|
1823
1587
|
const emptyMsg = refineType==="replace"
|
|
1824
1588
|
? "I didn't find any transition that would be a good fit as a replacement."
|
|
1825
1589
|
: "Already aligned to the transitions.dev motion tokens. Nothing to refine.";
|
|
@@ -1827,9 +1591,9 @@
|
|
|
1827
1591
|
h("p",{className:"tl-refine-idle-text"},emptyMsg));
|
|
1828
1592
|
foot = startBtn({label:"Scan again"});
|
|
1829
1593
|
} else {
|
|
1830
|
-
body = h(RefineResults,{summary,suggestions,appliedIds,onApply});
|
|
1594
|
+
body = h(RefineResults,{summary:refineType==="replace"?null:summary,suggestions:visible,appliedIds,onApply});
|
|
1831
1595
|
foot = pending.length>1
|
|
1832
|
-
? h("button",{className:"pc-btn primary",style:{flex:1},onClick:
|
|
1596
|
+
? h("button",{className:"pc-btn primary",style:{flex:1},onClick:()=>pending.forEach(onApply)},"Apply all ("+pending.length+")")
|
|
1833
1597
|
: startBtn({label:"Scan again"});
|
|
1834
1598
|
}
|
|
1835
1599
|
} else { // idle
|
|
@@ -1854,7 +1618,7 @@
|
|
|
1854
1618
|
h("div",{className:"tl-refine-head"},
|
|
1855
1619
|
h("div",{className:"tl-refine-titles"},
|
|
1856
1620
|
h("h3",null,"Refine"),
|
|
1857
|
-
h("p",null,label
|
|
1621
|
+
h("p",null,label||"Transitions review")),
|
|
1858
1622
|
h("div",{className:"tl-refine-actions"},
|
|
1859
1623
|
h("button",{ref:modeRef,className:cx("tl-refine-mode",modeOpen&&"is-open"),
|
|
1860
1624
|
"aria-haspopup":"menu","aria-expanded":modeOpen?"true":"false",onClick:()=>setModeOpen(v=>!v)},
|
|
@@ -1897,10 +1661,12 @@
|
|
|
1897
1661
|
return out;
|
|
1898
1662
|
}
|
|
1899
1663
|
|
|
1900
|
-
function Header({entries,active,onSelect,onReset,onCopy,copied,
|
|
1664
|
+
function Header({entries,active,onSelect,onReset,onCopy,copied,snap,setSnap,onMinimize,onRefine,refineActive,onAccept,acceptState,acceptDisabled,acceptError,scanning,onRescan}){
|
|
1901
1665
|
const[pick,setPick]=useState(false);
|
|
1902
1666
|
const[setg,setSetg]=useState(false);
|
|
1903
1667
|
const pickRef=useRef(null), gearRef=useRef(null);
|
|
1668
|
+
const[q,setQ]=useState("");
|
|
1669
|
+
useEffect(()=>{if(!pick)setQ("");},[pick]); // reset search when the picker closes
|
|
1904
1670
|
// Split selectable items into agent groups (component → phases) and the
|
|
1905
1671
|
// flat, ungrouped DOM transitions, preserving order.
|
|
1906
1672
|
const groupList=[]; const groupMap=new Map(); const flatItems=[];
|
|
@@ -1911,6 +1677,12 @@
|
|
|
1911
1677
|
g.phases.push(e);
|
|
1912
1678
|
}else flatItems.push(e);
|
|
1913
1679
|
}
|
|
1680
|
+
// live filter for the search input (matches phase/group/flat labels)
|
|
1681
|
+
const ql=q.trim().toLowerCase();
|
|
1682
|
+
const fGroups=groupList.map(g=>({...g,phases:g.phases.filter(e=>!ql||
|
|
1683
|
+
((e.phaseLabel||"")+" "+(e.groupLabel||"")+" "+(e.label||"")).toLowerCase().includes(ql))})).filter(g=>g.phases.length);
|
|
1684
|
+
const fFlat=flatItems.filter(e=>!ql||(e.label||"").toLowerCase().includes(ql));
|
|
1685
|
+
const noMatches=!!ql&&fGroups.length===0&&fFlat.length===0;
|
|
1914
1686
|
const phaseItem=(e)=>h(MenuItem,{key:e.id,active:active&&e.id===active.id,onClick:()=>{onSelect(e.id);setPick(false);},
|
|
1915
1687
|
right:active&&e.id===active.id&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},
|
|
1916
1688
|
h("span",null,e.phaseLabel,h("span",{className:"tl-menu-dim"}," "+e.durationMs+"ms")));
|
|
@@ -1924,29 +1696,32 @@
|
|
|
1924
1696
|
:h("span",{className:"tl-dim"},"None"),
|
|
1925
1697
|
h("span",{className:"tl-ghost-chev"},h(Ic,{name:"chevron"}))),
|
|
1926
1698
|
h(Dropdown,{open:pick,onClose:()=>setPick(false),triggerRef:pickRef,width:Math.max(240,(pickRef.current&&pickRef.current.offsetWidth)||240),align:"left"},
|
|
1699
|
+
h("div",{className:"tl-menu-search"},
|
|
1700
|
+
h("input",{className:"tl-menu-search-input",type:"text",placeholder:"Search",value:q,autoFocus:true,
|
|
1701
|
+
onChange:e=>setQ(e.target.value),
|
|
1702
|
+
onKeyDown:e=>{e.stopPropagation();if(e.key==="Escape"){if(q)setQ("");else setPick(false);}}})),
|
|
1927
1703
|
entries.length===0
|
|
1928
1704
|
? h("div",{className:"tl-menu-empty"},"No transitions found")
|
|
1929
|
-
:
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
...
|
|
1933
|
-
|
|
1934
|
-
|
|
1705
|
+
: noMatches
|
|
1706
|
+
? h("div",{className:"tl-menu-empty"},"No matches")
|
|
1707
|
+
: h(React.Fragment,null,
|
|
1708
|
+
...fGroups.map(g=>h(React.Fragment,{key:g.id},
|
|
1709
|
+
h("div",{className:"tl-menu-section"},g.label),
|
|
1710
|
+
...g.phases.map(phaseItem))),
|
|
1711
|
+
fFlat.length>0&&fGroups.length>0&&h("div",{className:"tl-menu-section"},"Ungrouped"),
|
|
1712
|
+
...fFlat.map(flatItem))),
|
|
1935
1713
|
h("span",{className:"tl-header-count"},
|
|
1936
1714
|
scanning?"Grouping…":entries.length+" transition"+(entries.length===1?"":"s")+" found"),
|
|
1937
|
-
h("button",{ref:gearRef,className:cx("tl-icon-btn",setg&&"is-active"),title:"Settings",onClick:()=>setSetg(v=>!v)},h(Ic,{name:"
|
|
1715
|
+
h("button",{ref:gearRef,className:cx("tl-icon-btn",setg&&"is-active"),title:"Settings",onClick:()=>setSetg(v=>!v)},h(Ic,{name:"dotsv"})),
|
|
1938
1716
|
h(Dropdown,{open:setg,onClose:()=>setSetg(false),triggerRef:gearRef,width:210,align:"right"},
|
|
1939
|
-
h(MenuItem,{onClick:()=>
|
|
1940
|
-
h(MenuItem,{onClick:()=>
|
|
1717
|
+
h(MenuItem,{onClick:()=>setSnap(v=>!v),right:snap&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},"Snap to grid"),
|
|
1718
|
+
h(MenuItem,{disabled:!active,onClick:()=>{onCopy&&onCopy();setSetg(false);},
|
|
1719
|
+
right:copied&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},copied?"Copied":"Copy values"),
|
|
1720
|
+
h(MenuItem,{disabled:scanning,onClick:()=>{setSetg(false);onRescan&&onRescan();}},
|
|
1721
|
+
scanning?"Rescanning…":"Rescan transitions")),
|
|
1941
1722
|
h("span",{className:"t-tt-wrap"},
|
|
1942
1723
|
h("button",{className:"tl-sec-btn t-tt-trigger",disabled:!active,onClick:onReset},"Reset"),
|
|
1943
1724
|
h("span",{className:"t-tt tl-tt-below",role:"tooltip"},"Reset values")),
|
|
1944
|
-
h("span",{className:"t-tt-wrap"},
|
|
1945
|
-
h("button",{className:"tl-icon-btn t-tt-trigger",disabled:!active,"aria-label":"Copy values",onClick:onCopy},
|
|
1946
|
-
h("span",{className:"t-icon-swap","data-state":copied?"b":"a"},
|
|
1947
|
-
h("span",{className:"t-icon","data-icon":"a"},h(Ic,{name:"copy"})),
|
|
1948
|
-
h("span",{className:"t-icon","data-icon":"b"},h(Ic,{name:"check"})))),
|
|
1949
|
-
h("span",{className:"t-tt tl-tt-below",role:"tooltip"},copied?"Copied":"Copy values")),
|
|
1950
1725
|
h("span",{className:"t-tt-wrap"},
|
|
1951
1726
|
h("button",{className:cx("tl-accept-btn",acceptState==="saving"&&"is-saving",acceptState==="done"&&"is-done"),
|
|
1952
1727
|
disabled:!active||acceptDisabled||acceptState==="saving"||acceptState==="done",onClick:onAccept,"aria-label":"Accept changes to your code"},
|
|
@@ -1977,7 +1752,7 @@
|
|
|
1977
1752
|
);
|
|
1978
1753
|
}
|
|
1979
1754
|
|
|
1980
|
-
function PropTrack({property, member, delayMs, durationMs, selected,
|
|
1755
|
+
function PropTrack({property, member, delayMs, durationMs, selected, onSelect, onDelayChange, onDurationChange, snap, scaleMs, lockDuration, labelW}){
|
|
1981
1756
|
const trackRef=useRef(null);
|
|
1982
1757
|
const grid = snap ? 25 : 1;
|
|
1983
1758
|
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]);
|
|
@@ -2004,9 +1779,8 @@
|
|
|
2004
1779
|
},[delayMs,durationMs,pxToMs,grid,scaleMs,onSelect,onDelayChange,onDurationChange,lockDuration]);
|
|
2005
1780
|
|
|
2006
1781
|
const delPct=(delayMs/scaleMs)*100; const durPct=(durationMs/scaleMs)*100;
|
|
2007
|
-
return h("div",{className:cx("tl-prop-row",selected&&"selected",
|
|
2008
|
-
h("div",{className:"tl-prop-head"},
|
|
2009
|
-
h("span",{className:"tl-prop-grip",title:"Drag to reorder",onMouseDown:onReorder},h(Ic,{name:"dots"})),
|
|
1782
|
+
return h("div",{className:cx("tl-prop-row",selected&&"selected",lockDuration&&"is-spring"),onClick:onSelect},
|
|
1783
|
+
h("div",{className:"tl-prop-head",style:labelW!=null?{flexBasis:labelW+"px"}:undefined},
|
|
2010
1784
|
h("span",{className:"tl-prop-label"},
|
|
2011
1785
|
member&&h("span",{className:"tl-prop-member"},member),
|
|
2012
1786
|
h("span",{className:"tl-prop-prop"},property))),
|
|
@@ -2095,18 +1869,25 @@
|
|
|
2095
1869
|
}
|
|
2096
1870
|
|
|
2097
1871
|
// shared plot geometry for both the easing curve and the spring curve.
|
|
2098
|
-
//
|
|
2099
|
-
|
|
1872
|
+
// Compact, landscape box: small horizontal padding (bezier x is clamped 0..1
|
|
1873
|
+
// so there is no horizontal overshoot to reserve room for) makes the curve
|
|
1874
|
+
// fill the width, while PAD_Y keeps the 0..1 band vertically centered with
|
|
1875
|
+
// equal headroom above/below so overshoot/undershoot handles (clamped to the
|
|
1876
|
+
// -0.5..1.5 range) stay visible instead of being clipped at the edges.
|
|
1877
|
+
const CURVE = { VBW:269, VBH:150, PAD_X:30, PAD_Y:42 };
|
|
2100
1878
|
|
|
2101
1879
|
function EasingEditor({easing, cubic, spring, durationMs, propKey, apply}){
|
|
2102
1880
|
const [tab, setTab] = useState(spring ? "springs" : "easing");
|
|
2103
1881
|
// when the user selects a different property, reflect that property's mode
|
|
2104
1882
|
useEffect(()=>{ setTab(spring ? "springs" : "easing"); /* eslint-disable-next-line */ }, [propKey]);
|
|
2105
1883
|
|
|
2106
|
-
// remember the last real (non-spring) easing so we can restore
|
|
2107
|
-
// switching back from the Springs tab
|
|
1884
|
+
// remember the last real (non-spring) easing + duration so we can restore
|
|
1885
|
+
// them when switching back from the Springs tab (the spring overwrites the
|
|
1886
|
+
// duration with its derived settle time).
|
|
2108
1887
|
const lastEasingRef = useRef("ease");
|
|
1888
|
+
const lastEaseDurRef = useRef(durationMs);
|
|
2109
1889
|
useEffect(()=>{ if(!spring && easing && !/^linear\(/.test(easing)) lastEasingRef.current = easing; }, [easing, spring]);
|
|
1890
|
+
useEffect(()=>{ if(!spring && durationMs!=null) lastEaseDurRef.current = durationMs; }, [durationMs, spring]);
|
|
2110
1891
|
|
|
2111
1892
|
const applySpring = useCallback((stiffness,damping,mass,name)=>{
|
|
2112
1893
|
const sim = simulateSpring(stiffness,damping,mass);
|
|
@@ -2119,7 +1900,7 @@
|
|
|
2119
1900
|
if(!spring){ const p=SPRING_PRESETS[0]; applySpring(p.stiffness,p.damping,p.mass,p.label); }
|
|
2120
1901
|
} else {
|
|
2121
1902
|
setTab("easing");
|
|
2122
|
-
if(spring) apply({ spring:null, easing:lastEasingRef.current||"ease" });
|
|
1903
|
+
if(spring) apply({ spring:null, easing:lastEasingRef.current||"ease", durationMs:lastEaseDurRef.current });
|
|
2123
1904
|
}
|
|
2124
1905
|
},[spring,apply,applySpring]);
|
|
2125
1906
|
|
|
@@ -2149,59 +1930,6 @@
|
|
|
2149
1930
|
h("div",{className:"t-page",ref:springPageRef,"data-page-id":"2","aria-hidden":tab!=="springs"},
|
|
2150
1931
|
h(SpringTab,{spring,applySpring})),
|
|
2151
1932
|
),
|
|
2152
|
-
h(PositionPreview,{easing, durationMs}),
|
|
2153
|
-
);
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
// Animated position preview (à la easing.dev): a marker travels left→right
|
|
2157
|
-
// using the live timing function + duration, pauses, returns, and loops.
|
|
2158
|
-
// Works for any timing function — cubic-bezier keywords or a spring's linear().
|
|
2159
|
-
function PositionPreview({easing, durationMs}){
|
|
2160
|
-
const trackRef = useRef(null);
|
|
2161
|
-
const dotRef = useRef(null);
|
|
2162
|
-
const [playing, setPlaying] = useState(false);
|
|
2163
|
-
const safeEasing = (!easing || easing.trim()==="") ? "linear" : easing;
|
|
2164
|
-
const dur = Math.max(120, durationMs || 0);
|
|
2165
|
-
|
|
2166
|
-
useEffect(()=>{
|
|
2167
|
-
if(!playing) return;
|
|
2168
|
-
const dot = dotRef.current, track = trackRef.current;
|
|
2169
|
-
if(!dot || !track) return;
|
|
2170
|
-
let cancelled = false, anim = null, timer = null, atRight = false;
|
|
2171
|
-
const GAP = 480, DOT = 14;
|
|
2172
|
-
const travel = ()=> Math.max(0, track.clientWidth - DOT - 8);
|
|
2173
|
-
const step = ()=>{
|
|
2174
|
-
if(cancelled) return;
|
|
2175
|
-
const from = atRight ? travel() : 0;
|
|
2176
|
-
const to = atRight ? 0 : travel();
|
|
2177
|
-
try {
|
|
2178
|
-
anim = dot.animate(
|
|
2179
|
-
[{transform:`translateX(${from}px)`},{transform:`translateX(${to}px)`}],
|
|
2180
|
-
{duration:dur, easing:safeEasing, fill:"forwards"});
|
|
2181
|
-
} catch {
|
|
2182
|
-
anim = dot.animate(
|
|
2183
|
-
[{transform:`translateX(${from}px)`},{transform:`translateX(${to}px)`}],
|
|
2184
|
-
{duration:dur, easing:"linear", fill:"forwards"});
|
|
2185
|
-
}
|
|
2186
|
-
anim.onfinish = ()=>{ if(cancelled) return; atRight = !atRight; timer = setTimeout(step, GAP); };
|
|
2187
|
-
};
|
|
2188
|
-
step();
|
|
2189
|
-
return ()=>{ cancelled = true; if(anim){try{anim.cancel();}catch{}} if(timer)clearTimeout(timer); };
|
|
2190
|
-
},[playing, safeEasing, dur]);
|
|
2191
|
-
|
|
2192
|
-
return h("div",{className:"tl-preview"},
|
|
2193
|
-
h("div",{className:"tl-preview-head"},
|
|
2194
|
-
h("span",{className:"tl-preview-title"},"Position Preview"),
|
|
2195
|
-
h("button",{className:"tl-preview-btn",onClick:()=>setPlaying(p=>!p)},
|
|
2196
|
-
h(Ic,{name:playing?"pause":"play",size:11}),
|
|
2197
|
-
h("span",null,playing?"Pause":"Play")),
|
|
2198
|
-
),
|
|
2199
|
-
h("div",{className:"tl-preview-track",ref:trackRef},
|
|
2200
|
-
h("span",{className:"tl-preview-rail"}),
|
|
2201
|
-
h("span",{className:"tl-preview-end left"}),
|
|
2202
|
-
h("span",{className:"tl-preview-end right"}),
|
|
2203
|
-
h("span",{className:"tl-preview-dot",ref:dotRef}),
|
|
2204
|
-
),
|
|
2205
1933
|
);
|
|
2206
1934
|
}
|
|
2207
1935
|
|
|
@@ -2388,50 +2116,29 @@
|
|
|
2388
2116
|
);
|
|
2389
2117
|
}
|
|
2390
2118
|
|
|
2391
|
-
function ScrubZone({scaleMs}){
|
|
2392
|
-
const { preview, registry, activeId } = useContext(TimelineCtx);
|
|
2393
|
-
const areaRef = useRef(null);
|
|
2394
|
-
const startedRef = useRef(false);
|
|
2395
|
-
|
|
2396
|
-
const pxToMs = useCallback(clientX=>{
|
|
2397
|
-
if(!areaRef.current) return 0;
|
|
2398
|
-
const rect = areaRef.current.getBoundingClientRect();
|
|
2399
|
-
const ratio = Math.max(0, Math.min((clientX - rect.left) / rect.width, 1));
|
|
2400
|
-
return ratio * scaleMs;
|
|
2401
|
-
},[scaleMs]);
|
|
2402
|
-
|
|
2403
|
-
const doSeek = useCallback(ms=>{
|
|
2404
|
-
if(preview.getState()==="idle" && !startedRef.current){
|
|
2405
|
-
if(!activeId) return;
|
|
2406
|
-
const entry = registry.getEffective(activeId);
|
|
2407
|
-
if(!entry) return;
|
|
2408
|
-
startedRef.current = true;
|
|
2409
|
-
preview.playPaused(entry, ms);
|
|
2410
|
-
} else {
|
|
2411
|
-
preview.seek(ms);
|
|
2412
|
-
}
|
|
2413
|
-
},[preview,registry,activeId]);
|
|
2414
|
-
|
|
2415
|
-
const startScrub = useCallback(e=>{
|
|
2416
|
-
e.preventDefault();
|
|
2417
|
-
startedRef.current = false;
|
|
2418
|
-
doSeek(pxToMs(e.clientX));
|
|
2419
|
-
const onMove = e2 => doSeek(pxToMs(e2.clientX));
|
|
2420
|
-
const onUp = () => { startedRef.current = false; window.removeEventListener("mousemove",onMove); window.removeEventListener("mouseup",onUp); };
|
|
2421
|
-
window.addEventListener("mousemove",onMove);
|
|
2422
|
-
window.addEventListener("mouseup",onUp);
|
|
2423
|
-
},[doSeek,pxToMs]);
|
|
2424
|
-
|
|
2425
|
-
return h("div",{className:"tl-scrub-zone",ref:areaRef,onMouseDown:startScrub});
|
|
2426
|
-
}
|
|
2427
|
-
|
|
2428
2119
|
function Tracks({et, selLane, setSelLane, onPropChange, snap, scaleMs}){
|
|
2429
|
-
const t = usePreviewTime();
|
|
2430
|
-
const ratio = Math.min(t / scaleMs, 1);
|
|
2431
|
-
const ROW_H = 48;
|
|
2432
2120
|
const rowsRef = useRef(null);
|
|
2121
|
+
const tracksRef = useRef(null);
|
|
2433
2122
|
const [order, setOrder] = useState([]);
|
|
2434
|
-
|
|
2123
|
+
// Width of the label column (shared by the ruler spacer + every prop-row
|
|
2124
|
+
// head). Draggable via the divider; the bars column is flex:1 so it just
|
|
2125
|
+
// takes the remaining space and the bars stay proportional to scaleMs.
|
|
2126
|
+
const LABEL_MIN = 96, LABEL_TRACK_MIN = 160;
|
|
2127
|
+
const [labelW, setLabelW] = useState(LABEL_W);
|
|
2128
|
+
const [colDragging, setColDragging] = useState(false);
|
|
2129
|
+
const startColResize = useCallback(e=>{
|
|
2130
|
+
e.preventDefault(); e.stopPropagation();
|
|
2131
|
+
const startX = e.clientX, startW = labelW;
|
|
2132
|
+
const maxW = tracksRef.current
|
|
2133
|
+
? Math.max(LABEL_MIN, tracksRef.current.getBoundingClientRect().width - LABEL_TRACK_MIN)
|
|
2134
|
+
: 360;
|
|
2135
|
+
setColDragging(true);
|
|
2136
|
+
document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none";
|
|
2137
|
+
const onMove = e2 => setLabelW(Math.max(LABEL_MIN, Math.min(maxW, startW + (e2.clientX - startX))));
|
|
2138
|
+
const onUp = () => { setColDragging(false); document.body.style.cursor=""; document.body.style.userSelect="";
|
|
2139
|
+
window.removeEventListener("mousemove",onMove); window.removeEventListener("mouseup",onUp); };
|
|
2140
|
+
window.addEventListener("mousemove",onMove); window.addEventListener("mouseup",onUp);
|
|
2141
|
+
},[labelW]);
|
|
2435
2142
|
const laneKey = et.map(x=>x.laneId).join("|");
|
|
2436
2143
|
useEffect(()=>{
|
|
2437
2144
|
const ids = et.map(x=>x.laneId);
|
|
@@ -2442,28 +2149,7 @@
|
|
|
2442
2149
|
});
|
|
2443
2150
|
},[laneKey]);
|
|
2444
2151
|
const orderedRows = order.map(p=>et.find(x=>x.laneId===p)).filter(Boolean);
|
|
2445
|
-
const
|
|
2446
|
-
e.preventDefault(); e.stopPropagation();
|
|
2447
|
-
setSelLane(laneId);
|
|
2448
|
-
setDragLane(laneId);
|
|
2449
|
-
const top = rowsRef.current ? rowsRef.current.getBoundingClientRect().top : 0;
|
|
2450
|
-
const onMove = e2=>{
|
|
2451
|
-
const idx = Math.floor((e2.clientY - top) / ROW_H);
|
|
2452
|
-
setOrder(prev=>{
|
|
2453
|
-
const cur = prev.indexOf(laneId);
|
|
2454
|
-
if(cur<0) return prev;
|
|
2455
|
-
const target = Math.max(0, Math.min(prev.length-1, idx));
|
|
2456
|
-
if(target===cur) return prev;
|
|
2457
|
-
const next = prev.slice();
|
|
2458
|
-
next.splice(cur,1);
|
|
2459
|
-
next.splice(target,0,laneId);
|
|
2460
|
-
return next;
|
|
2461
|
-
});
|
|
2462
|
-
};
|
|
2463
|
-
const onUp = ()=>{ setDragLane(null); window.removeEventListener("mousemove",onMove); window.removeEventListener("mouseup",onUp); };
|
|
2464
|
-
window.addEventListener("mousemove",onMove); window.addEventListener("mouseup",onUp);
|
|
2465
|
-
},[setSelLane]);
|
|
2466
|
-
const majorStep = scaleMs <= 10000 ? 1000 : scaleMs <= 25000 ? 5000 : 10000;
|
|
2152
|
+
const majorStep = scaleMs <= 2000 ? 500 : scaleMs <= 10000 ? 1000 : scaleMs <= 25000 ? 5000 : 10000;
|
|
2467
2153
|
const minorStep = majorStep / 4;
|
|
2468
2154
|
const ruler=[];
|
|
2469
2155
|
for(let ms=0; ms<=scaleMs; ms+=majorStep){
|
|
@@ -2474,26 +2160,22 @@
|
|
|
2474
2160
|
if(ms%majorStep===0) continue;
|
|
2475
2161
|
ruler.push(h("span",{key:"t"+ms,className:"tick",style:{left:((ms/scaleMs)*100)+"%"}}));
|
|
2476
2162
|
}
|
|
2477
|
-
return h("div",{className:"tl-tracks"},
|
|
2163
|
+
return h("div",{className:"tl-tracks",ref:tracksRef},
|
|
2478
2164
|
h("div",{className:"tl-ruler-row"},
|
|
2479
|
-
h("div",{className:"tl-ruler-spacer"}),
|
|
2165
|
+
h("div",{className:"tl-ruler-spacer",style:{flexBasis:labelW+"px"}}),
|
|
2480
2166
|
h("div",{className:"tl-ruler"},...ruler)),
|
|
2481
2167
|
h("div",{className:"tl-rows",ref:rowsRef},
|
|
2482
2168
|
...orderedRows.map(row=>h(PropTrack,{
|
|
2483
2169
|
key:row.laneId, property:row.property, member:row.member,
|
|
2484
2170
|
delayMs:row.delayMs, durationMs:row.durationMs, lockDuration:!!row.spring,
|
|
2485
|
-
selected:row.laneId===selLane,
|
|
2171
|
+
selected:row.laneId===selLane, snap, scaleMs, labelW,
|
|
2486
2172
|
onSelect:()=>setSelLane(row.laneId),
|
|
2487
|
-
onReorder:e=>startReorder(row.laneId,e),
|
|
2488
2173
|
onDelayChange:ms=>onPropChange(row.laneId,{delayMs:ms}),
|
|
2489
2174
|
onDurationChange:ms=>onPropChange(row.laneId,{durationMs:ms}),
|
|
2490
2175
|
}))),
|
|
2491
|
-
h(
|
|
2492
|
-
|
|
2493
|
-
h("
|
|
2494
|
-
h("svg",{className:"tl-playhead-head",viewBox:"0 0 16.666 24",width:"16.666",height:"24",fill:"none"},
|
|
2495
|
-
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"}))),
|
|
2496
|
-
),
|
|
2176
|
+
h("div",{className:cx("tl-col-resizer",colDragging&&"dragging"),style:{left:labelW+"px"},
|
|
2177
|
+
onMouseDown:startColResize,title:"Drag to resize columns","aria-label":"Resize label column",role:"separator"},
|
|
2178
|
+
h("span",{className:"tl-col-resizer-line"})),
|
|
2497
2179
|
);
|
|
2498
2180
|
}
|
|
2499
2181
|
|
|
@@ -2518,65 +2200,34 @@
|
|
|
2518
2200
|
);
|
|
2519
2201
|
}
|
|
2520
2202
|
|
|
2521
|
-
function Body({entry, onPropChange,
|
|
2203
|
+
function Body({entry, onPropChange, snap}){
|
|
2522
2204
|
const et = entry.effectiveTimings || [];
|
|
2523
2205
|
const [selLane, setSelLane] = useState(et[0]?.laneId ?? null);
|
|
2524
|
-
|
|
2525
|
-
|
|
2206
|
+
// No playback: the real component is the preview. The track scale just
|
|
2207
|
+
// auto-fits the longest lane (duration+delay) with a little headroom so
|
|
2208
|
+
// the static bars stay readable across short and long transitions.
|
|
2209
|
+
const maxEnd = et.reduce((m,t)=>Math.max(m,(t.durationMs||0)+(t.delayMs||0)),0);
|
|
2210
|
+
const scaleMs = Math.max(500, Math.ceil((maxEnd*1.15)/250)*250);
|
|
2526
2211
|
useEffect(()=>{
|
|
2527
2212
|
if(et.length && !et.find(t=>t.laneId===selLane)) setSelLane(et[0]?.laneId);
|
|
2528
2213
|
},[et,selLane]);
|
|
2529
2214
|
|
|
2530
2215
|
return h("div",{className:"tl-body"},
|
|
2531
2216
|
h("div",{className:"tl-main"},
|
|
2532
|
-
h(Transport,{state,disabled:!entry,onPlay:play,onPause:pause,onResume:resume,onRestart:restart,onStop:stop,speed,setSpeed,zoom,setZoom}),
|
|
2533
2217
|
h(Tracks,{et,selLane,setSelLane,onPropChange,snap,scaleMs}),
|
|
2534
2218
|
),
|
|
2535
2219
|
h(Inspector,{entry,selLane,onPropChange,snap}),
|
|
2536
2220
|
);
|
|
2537
2221
|
}
|
|
2538
2222
|
|
|
2539
|
-
function Transport({state,disabled,onPlay,onPause,onResume,onRestart,onStop,speed,setSpeed,zoom,setZoom}){
|
|
2540
|
-
const t = usePreviewTime();
|
|
2541
|
-
const spRef = useRef(null);
|
|
2542
|
-
const [sp,setSp] = useState(false);
|
|
2543
|
-
const playing = state==="playing";
|
|
2544
|
-
const onMain = () => playing ? onPause() : state==="paused" ? onResume() : onPlay();
|
|
2545
|
-
return h("div",{className:"tl-transport"},
|
|
2546
|
-
h("div",{className:"tl-transport-left"},
|
|
2547
|
-
h("span",{className:"tl-timecode"}, fmtTimecode(t)),
|
|
2548
|
-
h("button",{ref:spRef,className:cx("tl-ghost-btn","tl-speed",sp&&"is-active"),onClick:()=>setSp(v=>!v)},
|
|
2549
|
-
fmtSpeed(speed), h("span",{className:"tl-ghost-chev"},h(Ic,{name:"chevron"}))),
|
|
2550
|
-
h(Dropdown,{open:sp,onClose:()=>setSp(false),triggerRef:spRef,width:120,align:"left"},
|
|
2551
|
-
SPEEDS.map(s=>h(MenuItem,{key:s,active:s===speed,onClick:()=>{setSpeed(s);setSp(false);},
|
|
2552
|
-
right:s===speed&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},fmtSpeed(s)))) ),
|
|
2553
|
-
h("div",{className:"tl-transport-center"},
|
|
2554
|
-
h("button",{className:"tl-play-btn",disabled,onClick:onMain,title:playing?"Pause":"Play"},
|
|
2555
|
-
h("span",{className:"t-icon-swap","data-state":playing?"b":"a"},
|
|
2556
|
-
h("span",{className:"t-icon","data-icon":"a"},h(Ic,{name:"play",size:12})),
|
|
2557
|
-
h("span",{className:"t-icon","data-icon":"b"},h(Ic,{name:"pause",size:16}))))),
|
|
2558
|
-
h("div",{className:"tl-transport-right"},
|
|
2559
|
-
h("div",{className:"tl-zoom",title:"Timeline zoom"},
|
|
2560
|
-
h("div",{className:"tl-zoom-track","aria-hidden":true}),
|
|
2561
|
-
h("input",{type:"range",min:ZOOM_MIN,max:ZOOM_MAX,value:zoom,
|
|
2562
|
-
onChange:e=>setZoom(Number(e.target.value))})),
|
|
2563
|
-
h("span",{className:"t-tt-wrap"},
|
|
2564
|
-
h("button",{className:"tl-icon-btn ghost t-tt-trigger",disabled,"aria-label":"Replay",onClick:onRestart},h(Ic,{name:"restart"})),
|
|
2565
|
-
h("span",{className:"t-tt tl-tt-below",role:"tooltip"},"Replay")),
|
|
2566
|
-
),
|
|
2567
|
-
);
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
2223
|
function TimelinePanel(){
|
|
2571
2224
|
const entries=useReg(); const{active,setActiveId}=useActive();
|
|
2572
|
-
const{
|
|
2573
|
-
const{registry
|
|
2225
|
+
const{setPropOverride}=usePropOverride();
|
|
2226
|
+
const{registry}=useContext(TimelineCtx);
|
|
2574
2227
|
const[copied,setCopied]=useState(false);
|
|
2575
2228
|
const[minimized,setMinimized]=useState(false);
|
|
2576
2229
|
const[panelHeight,setPanelHeight]=useState(440);
|
|
2577
2230
|
const[resizing,setResizing]=useState(false);
|
|
2578
|
-
const[speed,setSpeed]=useState(1);
|
|
2579
|
-
const[loop,setLoop]=useState(false);
|
|
2580
2231
|
const[snap,setSnap]=useState(true);
|
|
2581
2232
|
// ── refine ──
|
|
2582
2233
|
const[refineOpen,setRefineOpen]=useState(false);
|
|
@@ -2628,8 +2279,10 @@
|
|
|
2628
2279
|
const et=active.effectiveTimings||[];
|
|
2629
2280
|
const timings=et.map(t=>({property:t.property,durationMs:t.durationMs,delayMs:t.delayMs,easing:t.easing}));
|
|
2630
2281
|
const selector=(active.bindings&&active.bindings.selector)||(et[0]&&et[0].selector)||null;
|
|
2282
|
+
// always scan for BOTH kinds (token tweaks + a whole-transition replacement)
|
|
2283
|
+
// in one pass; the tabs just filter which kind they show.
|
|
2631
2284
|
const{id}=await relayCreateJob({transitionId:active.id,label:active.label,selector,
|
|
2632
|
-
phase:active.phase||null,group:active.groupLabel||null,timings,mode,refineType});
|
|
2285
|
+
phase:active.phase||null,group:active.groupLabel||null,timings,mode,refineType:"both"});
|
|
2633
2286
|
setRefineJobId(id);
|
|
2634
2287
|
}catch(e){
|
|
2635
2288
|
setRefinePhase("error");
|
|
@@ -2637,7 +2290,9 @@
|
|
|
2637
2290
|
}
|
|
2638
2291
|
},[active,refineMode,refineType,refreshHealth]);
|
|
2639
2292
|
const changeRefineMode=useCallback((mode)=>{setRefineMode(mode);setRefinePhase("idle");refreshHealth();},[refreshHealth]);
|
|
2640
|
-
|
|
2293
|
+
// switching tabs only changes which kind of suggestion is shown — it never
|
|
2294
|
+
// interrupts a scan or discards results, since one scan covers both kinds.
|
|
2295
|
+
const changeRefineType=useCallback((t)=>{setRefineType(t);},[]);
|
|
2641
2296
|
// poll the relay while a job is running
|
|
2642
2297
|
useEffect(()=>{
|
|
2643
2298
|
if(!refineJobId||refinePhase!=="scanning")return;
|
|
@@ -2676,8 +2331,59 @@
|
|
|
2676
2331
|
const panelMinH=200;
|
|
2677
2332
|
const panelMaxH=useCallback(()=>Math.round(window.innerHeight*0.92),[]);
|
|
2678
2333
|
useEffect(()=>{if(!active&&entries.length>0)setActiveId(entries[0].id);},[active,entries,setActiveId]);
|
|
2679
|
-
|
|
2680
|
-
|
|
2334
|
+
// ── live styling ──────────────────────────────────────────────────────
|
|
2335
|
+
// With playback gone, the real component IS the preview. So whenever a
|
|
2336
|
+
// transition has edits, mirror its effective timings onto the live
|
|
2337
|
+
// element(s) as an inline `transition`, and revert when edits are cleared.
|
|
2338
|
+
// Interacting with the component then animates with the edited values.
|
|
2339
|
+
const liveAppliedRef=useRef(new Map()); // el → {prevInline, css}
|
|
2340
|
+
useEffect(()=>{
|
|
2341
|
+
if(typeof document==="undefined")return;
|
|
2342
|
+
const applied=liveAppliedRef.current;
|
|
2343
|
+
const desired=new Map(); // el → css
|
|
2344
|
+
const add=(els,css)=>{ for(const el of els){ if(!el||(el.closest&&el.closest("[data-timeline-panel]")))continue; desired.set(el,css); } };
|
|
2345
|
+
for(const item of entries){
|
|
2346
|
+
const ov=registry.getPropOverrides(item.id);
|
|
2347
|
+
if(!ov||!Object.keys(ov).length)continue; // only edited transitions go live
|
|
2348
|
+
if(item.kind==="phase"){
|
|
2349
|
+
for(const m of (item.members||[])){
|
|
2350
|
+
if(!m.selector||!m.lanes||!m.lanes.length)continue;
|
|
2351
|
+
let els=[];try{els=Array.from(document.querySelectorAll(m.selector));}catch{}
|
|
2352
|
+
add(els,transitionLanesToCss(m.lanes));
|
|
2353
|
+
}
|
|
2354
|
+
}else{
|
|
2355
|
+
const lanes=item.effectiveTimings||[];
|
|
2356
|
+
if(!lanes.length)continue;
|
|
2357
|
+
const els=((item.bindings&&item.bindings.elements)||[]).map(w=>w.deref&&w.deref()).filter(Boolean);
|
|
2358
|
+
add(els,transitionLanesToCss(lanes));
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
// apply new / changed
|
|
2362
|
+
for(const [el,css] of desired){
|
|
2363
|
+
const cur=applied.get(el);
|
|
2364
|
+
if(cur&&cur.css===css)continue;
|
|
2365
|
+
if(!cur){
|
|
2366
|
+
const s=getComputedStyle(el);
|
|
2367
|
+
const z=zipTransitionLists((s.transitionProperty||"").split(","),(s.transitionDuration||"0s").split(","),(s.transitionDelay||"0s").split(","),splitCssValues(s.transitionTimingFunction||"ease"));
|
|
2368
|
+
_TX_LIVE_BASE.set(el,{z});
|
|
2369
|
+
applied.set(el,{prevInline:el.style.transition||"",css});
|
|
2370
|
+
}else applied.set(el,{prevInline:cur.prevInline,css});
|
|
2371
|
+
try{el.style.transition=css;}catch{}
|
|
2372
|
+
}
|
|
2373
|
+
// revert elements that no longer have edits
|
|
2374
|
+
for(const [el,rec] of Array.from(applied)){
|
|
2375
|
+
if(desired.has(el))continue;
|
|
2376
|
+
try{el.style.transition=rec.prevInline;}catch{}
|
|
2377
|
+
_TX_LIVE_BASE.delete(el);
|
|
2378
|
+
applied.delete(el);
|
|
2379
|
+
}
|
|
2380
|
+
},[entries,registry]);
|
|
2381
|
+
// restore everything on unmount
|
|
2382
|
+
useEffect(()=>()=>{
|
|
2383
|
+
const applied=liveAppliedRef.current;
|
|
2384
|
+
for(const [el,rec] of applied){ try{el.style.transition=rec.prevInline;}catch{} _TX_LIVE_BASE.delete(el); }
|
|
2385
|
+
applied.clear();
|
|
2386
|
+
},[]);
|
|
2681
2387
|
const startResize=useCallback(e=>{
|
|
2682
2388
|
e.preventDefault();
|
|
2683
2389
|
const startY=e.clientY; const startH=panelHeight;
|
|
@@ -2737,33 +2443,81 @@
|
|
|
2737
2443
|
},[active]);
|
|
2738
2444
|
// reset Accept feedback when switching transitions
|
|
2739
2445
|
useEffect(()=>{setAcceptState("idle");setAcceptError(null);},[active&&active.id]);
|
|
2740
|
-
//
|
|
2741
|
-
//
|
|
2742
|
-
//
|
|
2446
|
+
// The grouped scan asks the agent to read the source and return Open/Close
|
|
2447
|
+
// phases — an expensive LLM round-trip. So it runs ONLY:
|
|
2448
|
+
// 1. the first time this page is ever opened (nothing cached), or
|
|
2449
|
+
// 2. on demand via Settings → "Rescan transitions".
|
|
2450
|
+
// The result is cached in localStorage by page path. A plain reload
|
|
2451
|
+
// re-hydrates the groups instantly and NEVER touches the agent. Use
|
|
2452
|
+
// "Rescan transitions" after the page's transitions actually change.
|
|
2453
|
+
const GROUP_STORE_KEY="tlGroups:"+location.pathname;
|
|
2454
|
+
// content signature of the flat transitions — so cached groups invalidate
|
|
2455
|
+
// automatically when the page's transitions change (e.g. the demo content
|
|
2456
|
+
// was swapped under the same pathname). Computed from the SETTLED flat set.
|
|
2457
|
+
const flatSig=useCallback((flat)=>flat.map(e=>[e.label,e.bindings&&e.bindings.selector,(e.properties||[]).join(","),e.durationMs,e.delayMs,e.easing].join("|")).sort().join("\u00a7"),[]);
|
|
2458
|
+
const readGroupCache=useCallback(()=>{
|
|
2459
|
+
try{const c=JSON.parse(localStorage.getItem(GROUP_STORE_KEY)||"null");
|
|
2460
|
+
return c&&Array.isArray(c.groups)&&c.groups.length?c:null;}catch{return null;}
|
|
2461
|
+
},[GROUP_STORE_KEY]);
|
|
2462
|
+
// monotonic token so an unmount or a newer rescan supersedes an in-flight poll
|
|
2463
|
+
const scanTokenRef=useRef(0);
|
|
2464
|
+
const runGroupScan=useCallback(async()=>{
|
|
2465
|
+
const token=++scanTokenRef.current;
|
|
2466
|
+
setGroupScanState("scanning");
|
|
2467
|
+
// wait for the flat DOM scan to settle (count stable twice) so the agent
|
|
2468
|
+
// sees every transition, not a partial set discovered progressively.
|
|
2469
|
+
let prev=-1,stable=0;
|
|
2470
|
+
while(scanTokenRef.current===token){
|
|
2471
|
+
const n=registry.getAll().filter(e=>e.kind!=="phase").length;
|
|
2472
|
+
if(n>0&&n===prev){if(++stable>=2)break;}else stable=0;
|
|
2473
|
+
prev=n;
|
|
2474
|
+
await new Promise(r=>setTimeout(r,300));
|
|
2475
|
+
}
|
|
2476
|
+
if(scanTokenRef.current!==token)return;
|
|
2477
|
+
const flat=registry.getAll().filter(e=>e.kind!=="phase");
|
|
2478
|
+
if(!flat.length){setGroupScanState("idle");return;}
|
|
2479
|
+
// valid cache (same content signature) → apply instantly, skip the agent.
|
|
2480
|
+
// A stale cache (different sig, e.g. content changed) falls through to a
|
|
2481
|
+
// fresh scan and is overwritten below.
|
|
2482
|
+
const sig=flatSig(flat);
|
|
2483
|
+
const cached=readGroupCache();
|
|
2484
|
+
if(cached&&cached.sig===sig){registry.setGroups(cached.groups);setGroupScanState("done");return;}
|
|
2485
|
+
setGroupScanState("scanning");
|
|
2486
|
+
const raw=flat.map(e=>({label:e.label,selector:e.bindings&&e.bindings.selector,
|
|
2487
|
+
properties:e.properties,
|
|
2488
|
+
timings:(e.baseLanes||[]).map(l=>({property:l.property,durationMs:l.durationMs,delayMs:l.delayMs,easing:l.easing}))}));
|
|
2489
|
+
try{
|
|
2490
|
+
const{id}=await relayCreateJob({kind:"scan",url:location.href,raw});
|
|
2491
|
+
for(let i=0;i<520;i++){
|
|
2492
|
+
if(scanTokenRef.current!==token)return; // superseded / unmounted
|
|
2493
|
+
await new Promise(r=>setTimeout(r,500));
|
|
2494
|
+
if(scanTokenRef.current!==token)return;
|
|
2495
|
+
const job=await relayGetJob(id);
|
|
2496
|
+
if(job.status==="done"){
|
|
2497
|
+
const groups=(job.result&&job.result.groups)||[];
|
|
2498
|
+
if(groups.length){registry.setGroups(groups);
|
|
2499
|
+
try{localStorage.setItem(GROUP_STORE_KEY,JSON.stringify({groups,sig,ts:Date.now()}));}catch{}}
|
|
2500
|
+
setGroupScanState("done");return;}
|
|
2501
|
+
if(job.status==="error"){setGroupScanState("error");return;}
|
|
2502
|
+
}
|
|
2503
|
+
setGroupScanState("error");
|
|
2504
|
+
}catch(e){ setGroupScanState("error"); /* relay down → stay flat, retry next open */ }
|
|
2505
|
+
},[registry,GROUP_STORE_KEY,flatSig,readGroupCache]);
|
|
2506
|
+
// manual "Rescan transitions" — drop the cache and force a fresh agent scan
|
|
2507
|
+
const rescanTransitions=useCallback(()=>{
|
|
2508
|
+
try{localStorage.removeItem(GROUP_STORE_KEY);}catch{}
|
|
2509
|
+
runGroupScan();
|
|
2510
|
+
},[runGroupScan,GROUP_STORE_KEY]);
|
|
2511
|
+
// On mount, run the group resolver once. It waits for the flat scan to
|
|
2512
|
+
// settle, then either re-applies cached groups (when the content signature
|
|
2513
|
+
// matches — a pure cache read, no agent) or kicks off a fresh agent scan
|
|
2514
|
+
// (first open OR the page's transitions changed since last cache).
|
|
2743
2515
|
useEffect(()=>{
|
|
2744
|
-
if(didGroupScanRef.current
|
|
2516
|
+
if(didGroupScanRef.current)return;
|
|
2745
2517
|
didGroupScanRef.current=true;
|
|
2746
|
-
|
|
2747
|
-
(
|
|
2748
|
-
|
|
2749
|
-
const flat=registry.getAll().filter(e=>e.kind!=="phase");
|
|
2750
|
-
const raw=flat.map(e=>({label:e.label,selector:e.bindings&&e.bindings.selector,
|
|
2751
|
-
properties:e.properties,
|
|
2752
|
-
timings:(e.baseLanes||[]).map(l=>({property:l.property,durationMs:l.durationMs,delayMs:l.delayMs,easing:l.easing}))}));
|
|
2753
|
-
try{
|
|
2754
|
-
const{id}=await relayCreateJob({kind:"scan",url:location.href,raw});
|
|
2755
|
-
for(let i=0;i<240&&!cancelled;i++){
|
|
2756
|
-
await new Promise(r=>setTimeout(r,500));
|
|
2757
|
-
const job=await relayGetJob(id);
|
|
2758
|
-
if(cancelled)return;
|
|
2759
|
-
if(job.status==="done"){const groups=(job.result&&job.result.groups)||[];if(groups.length)registry.setGroups(groups);setGroupScanState("done");return;}
|
|
2760
|
-
if(job.status==="error"){setGroupScanState("error");return;}
|
|
2761
|
-
}
|
|
2762
|
-
if(!cancelled)setGroupScanState("error");
|
|
2763
|
-
}catch(e){ if(!cancelled)setGroupScanState("error"); /* relay down → stay flat */ }
|
|
2764
|
-
})();
|
|
2765
|
-
return ()=>{cancelled=true;};
|
|
2766
|
-
},[entries.length,registry]);
|
|
2518
|
+
runGroupScan();
|
|
2519
|
+
return ()=>{scanTokenRef.current++;};
|
|
2520
|
+
},[runGroupScan]);
|
|
2767
2521
|
|
|
2768
2522
|
// whole-component open/close uses the transitions.dev panel reveal:
|
|
2769
2523
|
// keep the panel mounted while it animates, flip data-open on the next
|
|
@@ -2812,12 +2566,11 @@
|
|
|
2812
2566
|
h("div",{className:"tl-panel-body"},
|
|
2813
2567
|
h("div",{className:"tl-panel-main"},
|
|
2814
2568
|
h(Header,{entries,active,onSelect:setActiveId,onReset:resetOverrides,onCopy:copyValues,copied,
|
|
2815
|
-
|
|
2569
|
+
snap,setSnap,onMinimize:()=>setMinimized(true),onRefine:openRefine,refineActive:refineOpen,
|
|
2816
2570
|
onAccept,acceptState,acceptDisabled:computeChanges(active).length===0,acceptError,
|
|
2817
|
-
scanning:groupScanState==="scanning"}),
|
|
2571
|
+
scanning:groupScanState==="scanning",onRescan:rescanTransitions}),
|
|
2818
2572
|
active
|
|
2819
|
-
?h(Body,{entry:active,onPropChange:(prop,o)=>setPropOverride(prop,o),
|
|
2820
|
-
state,play,pause,resume,restart,stop,speed,setSpeed,snap})
|
|
2573
|
+
?h(Body,{entry:active,onPropChange:(prop,o)=>setPropOverride(prop,o),snap})
|
|
2821
2574
|
:h("div",{className:"tl-empty"},"Select a transition to inspect and edit it")),
|
|
2822
2575
|
h(RefinePanel,{open:refineOpen,onClose:()=>setRefineOpen(false),phase:refinePhase,label:refineLabel,
|
|
2823
2576
|
refineType,onType:changeRefineType,suggestions:refineSuggestions,summary:refineSummary,error:refineError,
|
|
@@ -2832,10 +2585,36 @@
|
|
|
2832
2585
|
}
|
|
2833
2586
|
|
|
2834
2587
|
// ── demo boxes ──
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
function
|
|
2588
|
+
// Plus → menu morph — transitions.dev (20-plus-menu-morph.md). The trigger
|
|
2589
|
+
// button morphs into the panel it opens; CSS owns the morph, JS only flips
|
|
2590
|
+
// data-open and mirrors it to aria-expanded, closing on outside-click / Escape.
|
|
2591
|
+
function BoxMorph(){
|
|
2592
|
+
const[open,setOpen]=useState(false);
|
|
2593
|
+
const ref=useRef(null);
|
|
2594
|
+
useEffect(()=>{
|
|
2595
|
+
const onDoc=e=>{if(ref.current&&!ref.current.contains(e.target))setOpen(false);};
|
|
2596
|
+
const onKey=e=>{if(e.key==="Escape")setOpen(false);};
|
|
2597
|
+
document.addEventListener("click",onDoc);
|
|
2598
|
+
document.addEventListener("keydown",onKey);
|
|
2599
|
+
return()=>{document.removeEventListener("click",onDoc);document.removeEventListener("keydown",onKey);};
|
|
2600
|
+
},[]);
|
|
2601
|
+
const item=(label,icon)=>h("button",{className:"morph-item",type:"button",onClick:()=>setOpen(false)},
|
|
2602
|
+
h(Ic,{name:icon,size:16}),label);
|
|
2603
|
+
return h("div",{className:"card"},
|
|
2604
|
+
h("h2",null,"Plus \u2192 Menu morph"),
|
|
2605
|
+
h("p",null,"Click the + button. The circular trigger morphs into the menu it opens \u2014 width, height & corner radius animate while the plus cross-fades into the panel."),
|
|
2606
|
+
h("div",{className:"morph-stage"},
|
|
2607
|
+
h("div",{className:"t-morph","data-open":String(open),ref},
|
|
2608
|
+
h("div",{className:"t-morph-menu","aria-hidden":!open},
|
|
2609
|
+
h("div",{className:"morph-title"},"Create"),
|
|
2610
|
+
item("New file","copy"),
|
|
2611
|
+
item("Upload","restart"),
|
|
2612
|
+
item("Settings","gear")),
|
|
2613
|
+
h("button",{className:"t-morph-plus",type:"button","aria-expanded":String(open),"aria-label":"Open menu",
|
|
2614
|
+
onClick:e=>{e.stopPropagation();setOpen(v=>!v);}},
|
|
2615
|
+
h("svg",{width:20,height:20,viewBox:"0 0 20 20",fill:"none"},
|
|
2616
|
+
h("path",{d:"M10 4.5v11M4.5 10h11",stroke:"currentColor","strokeWidth":1.8,"strokeLinecap":"round"}))))));
|
|
2617
|
+
}
|
|
2839
2618
|
|
|
2840
2619
|
// ── live controls for the whole-panel reveal transition (demo harness) ──
|
|
2841
2620
|
// transitions.dev motion tokens (values lifted verbatim from the skill's _root.css)
|
|
@@ -2937,17 +2716,17 @@
|
|
|
2937
2716
|
}
|
|
2938
2717
|
|
|
2939
2718
|
function App(){
|
|
2940
|
-
const rootRef=useRef(null);const registry=useMemo(()=>new TransitionRegistry(),[]);const
|
|
2941
|
-
useEffect(()=>{const root=rootRef.current??document.body;const scanner=new DomScanner(root,registry);
|
|
2942
|
-
const ctx=useMemo(()=>({registry,
|
|
2719
|
+
const rootRef=useRef(null);const registry=useMemo(()=>new TransitionRegistry(),[]);const[activeId,setActiveId]=useState(null);
|
|
2720
|
+
useEffect(()=>{const root=rootRef.current??document.body;const scanner=new DomScanner(root,registry);scanner.start();return()=>{scanner.stop();};},[registry]);
|
|
2721
|
+
const ctx=useMemo(()=>({registry,activeId,setActiveId}),[registry,activeId]);
|
|
2943
2722
|
// Demo-only tweak controls: hidden by default. Append ?controls to the URL
|
|
2944
2723
|
// to show them for testing. (This whole block is below the inject CUT_MARKER,
|
|
2945
2724
|
// so it never ships in the injected build.)
|
|
2946
2725
|
const showControls=(()=>{try{return new URLSearchParams(location.search).has("controls");}catch(e){return false;}})();
|
|
2947
2726
|
return h(TimelineCtx.Provider,{value:ctx},
|
|
2948
2727
|
showControls&&h(PanelControls),
|
|
2949
|
-
h("div",{ref:rootRef,className:"demo-root"},h("div",{className:"demo-header"},h("h1",null,"Timeline Inspector \u2014 Demo"),h("p",null,"
|
|
2950
|
-
h("div",{className:"demo-grid"},h(
|
|
2728
|
+
h("div",{ref:rootRef,className:"demo-root"},h("div",{className:"demo-header"},h("h1",null,"Timeline Inspector \u2014 Demo"),h("p",null,"A single transitions.dev element: the plus \u2192 menu morph. Its transitions are auto-detected \u2014 select one below, drag the bars to edit timing, then interact with the component itself to preview the change.")),
|
|
2729
|
+
h("div",{className:"demo-grid"},h(BoxMorph))),
|
|
2951
2730
|
h(TimelinePanel));
|
|
2952
2731
|
}
|
|
2953
2732
|
|