transitions-refine 0.3.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 +3 -1
- package/demo.html +333 -599
- package/package.json +1 -1
- package/server/relay.mjs +29 -4
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 */
|
|
@@ -251,15 +343,15 @@
|
|
|
251
343
|
|
|
252
344
|
/* ── body / floating cards row ── */
|
|
253
345
|
.tl-body { display: flex; flex: 1; min-height: 0; gap: 0; }
|
|
254
|
-
.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; }
|
|
255
347
|
.tl-inspector { flex: 0 0 280px; padding: 14px 16px 18px; display: flex; flex-direction: column;
|
|
256
348
|
border-left: 1px solid var(--c-line); min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
|
|
257
349
|
.tl-insp-title { font-size: 13px; font-weight: 500; line-height: 18px; color: #171717; margin-bottom: 12px; text-transform: capitalize; }
|
|
258
350
|
.tl-insp-label { font-size: 12px; line-height: 18px; color: #737373; margin: 10px 0 6px; }
|
|
259
351
|
|
|
260
352
|
/* ── tracks / ruler ── */
|
|
261
|
-
.tl-tracks { position: relative; flex: 1; }
|
|
262
|
-
.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); }
|
|
263
355
|
.tl-ruler-spacer { flex: 0 0 150px; border-right: 1px solid var(--c-line); }
|
|
264
356
|
.tl-ruler { position: relative; flex: 1; margin-left: 16px; }
|
|
265
357
|
.tl-ruler .maj { position: absolute; top: 9px; font-size: 12px; line-height: 14px; color: var(--c-ruler); transform: translateX(-50%); white-space: nowrap; }
|
|
@@ -272,22 +364,25 @@
|
|
|
272
364
|
.tl-prop-row.selected { background: #fafafa; }
|
|
273
365
|
.tl-prop-head { flex: 0 0 150px; display: flex; align-items: center; gap: 8px; padding-left: 12px;
|
|
274
366
|
padding-right: 10px; overflow: hidden; border-right: 1px solid var(--c-line); }
|
|
275
|
-
.tl-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
.tl-prop-
|
|
279
|
-
|
|
280
|
-
.tl-prop-row.reordering .tl-prop-grip { color: var(--c-ruler); cursor: grabbing; }
|
|
281
|
-
.tl-prop-label { font-size: 13px; font-weight: 500; line-height: 18px; color: var(--c-text-mut2); white-space: nowrap; overflow: hidden;
|
|
282
|
-
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; }
|
|
283
372
|
.tl-prop-row.selected .tl-prop-label { color: #171717; }
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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); }
|
|
288
380
|
.tl-prop-prop { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
289
|
-
|
|
290
|
-
|
|
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; }
|
|
291
386
|
/* 16px left inset of the plot area (kept in sync with the ruler) */
|
|
292
387
|
.tl-prop-track { position: relative; flex: 1; user-select: none; margin-left: 16px; }
|
|
293
388
|
/* timeline line — capsule (fully rounded), Figma #eeeeef default / #e9e9e9 hover */
|
|
@@ -328,7 +423,8 @@
|
|
|
328
423
|
/* inner slider fill — Figma button/tiny: soft drop shadow + inset ring give the
|
|
329
424
|
"raised block" look (shadow 0 1px 3px @4%, hairline border, bottom 1px). */
|
|
330
425
|
.tl-field-fill { position: absolute; left: 0; top: 0; bottom: 0; min-width: 36px; border-radius: 8px;
|
|
331
|
-
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);
|
|
332
428
|
pointer-events: none; transition: background 0.12s ease, opacity 0.12s ease; z-index: 1; }
|
|
333
429
|
.tl-field.is-dragging .tl-field-fill { background: var(--c-fill-a); }
|
|
334
430
|
.tl-field.is-dragging .tl-field-label { opacity: 0.7; }
|
|
@@ -445,31 +541,9 @@
|
|
|
445
541
|
.tl-seg-btn:hover:not(.is-active) { background: rgba(170,170,170,0.06); color: #17181c; }
|
|
446
542
|
.tl-seg-btn.is-active { background: rgba(170,170,170,0.1); color: #17181c; }
|
|
447
543
|
|
|
448
|
-
/*
|
|
449
|
-
.tl-
|
|
450
|
-
|
|
451
|
-
.tl-preview-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
|
452
|
-
.tl-preview-title { font-size: 11px; font-weight: 600; color: var(--c-text-mut); }
|
|
453
|
-
.tl-preview-btn { display: inline-flex; align-items: center; gap: 5px; height: 22px; padding: 0 8px 0 7px;
|
|
454
|
-
border: none; border-radius: 6px; background: var(--c-field-bg); color: var(--c-text-mut2);
|
|
455
|
-
font: inherit; font-size: 11px; font-weight: 500; cursor: pointer;
|
|
456
|
-
transition: background 0.12s ease, scale 0.12s ease; }
|
|
457
|
-
.tl-preview-btn:hover { background: var(--c-sec-h); }
|
|
458
|
-
.tl-preview-btn:active { scale: 0.96; }
|
|
459
|
-
.tl-preview-track { position: relative; height: 16px; }
|
|
460
|
-
.tl-preview-rail { position: absolute; left: 0; right: 0; top: 50%; height: 2px; transform: translateY(-50%);
|
|
461
|
-
background: var(--c-track); border-radius: 2px; }
|
|
462
|
-
.tl-preview-end { position: absolute; top: 50%; width: 2px; height: 8px; transform: translateY(-50%);
|
|
463
|
-
background: #d0d0d6; border-radius: 2px; }
|
|
464
|
-
.tl-preview-end.left { left: 0; }
|
|
465
|
-
.tl-preview-end.right { right: 0; }
|
|
466
|
-
.tl-preview-dot { position: absolute; left: 0; top: 50%; width: 14px; height: 14px; margin-top: -7px;
|
|
467
|
-
border-radius: 50%; background: var(--c-blue); box-shadow: 0 1px 3px rgba(0,0,0,0.25);
|
|
468
|
-
will-change: transform; }
|
|
469
|
-
|
|
470
|
-
/* menu section header (non-clickable) */
|
|
471
|
-
.tl-menu-group { padding: 9px 10px 4px; font-size: 10.5px; font-weight: 600; letter-spacing: 0.04em;
|
|
472
|
-
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; }
|
|
473
547
|
.tl-menu-group:first-child { padding-top: 4px; }
|
|
474
548
|
|
|
475
549
|
/* spring params box (reuses bounce row layout) */
|
|
@@ -497,10 +571,19 @@
|
|
|
497
571
|
display: flex; align-items: center; justify-content: center;
|
|
498
572
|
background: #fff; border-radius: 12px; box-shadow: var(--card-shadow); }
|
|
499
573
|
|
|
500
|
-
/* ── 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. */
|
|
501
578
|
.tl-menu { background: #fff; border-radius: 12px; padding: 6px; box-shadow: var(--menu-shadow); }
|
|
502
|
-
|
|
503
|
-
|
|
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;
|
|
504
587
|
transition: background 0.1s ease, box-shadow 0.1s ease; }
|
|
505
588
|
.tl-menu-item:hover { background: #f4f4f5; }
|
|
506
589
|
.tl-menu-item:active { background: #ededee; }
|
|
@@ -508,15 +591,14 @@
|
|
|
508
591
|
.tl-menu-item.disabled { color: var(--c-disabled); pointer-events: none; }
|
|
509
592
|
.tl-menu-item-label { flex: 1; min-width: 0; display: flex; align-items: center; }
|
|
510
593
|
.tl-menu-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
511
|
-
.tl-menu-dim { color:
|
|
594
|
+
.tl-menu-dim { color: #979797; }
|
|
512
595
|
.tl-menu-help { color: #c4c4cc; margin-left: 10px; flex: none; cursor: help; }
|
|
513
596
|
.tl-menu-help:hover { color: var(--c-ruler); }
|
|
514
597
|
.tl-menu-help svg { display: block; }
|
|
515
598
|
.tl-menu-check { display: flex; color: var(--c-text-strong); flex: none; }
|
|
516
|
-
.tl-menu-empty { padding: 10px; color: var(--c-disabled); font-size: 13px; }
|
|
517
|
-
.tl-menu-section { padding: 8px
|
|
518
|
-
|
|
519
|
-
.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; }
|
|
520
602
|
|
|
521
603
|
/* ═════ transitions.dev — menu dropdown (verbatim) ═════ */
|
|
522
604
|
.t-dropdown {
|
|
@@ -596,6 +678,7 @@
|
|
|
596
678
|
background: var(--tt-bg);
|
|
597
679
|
color: var(--tt-fg);
|
|
598
680
|
white-space: nowrap;
|
|
681
|
+
z-index: 100;
|
|
599
682
|
box-shadow:
|
|
600
683
|
0 0 0 1px rgba(0, 0, 0, 0.06),
|
|
601
684
|
0 2px 6px 0 rgba(0, 0, 0, 0.05),
|
|
@@ -710,7 +793,8 @@
|
|
|
710
793
|
/* refine panel slides in from the right using transitions.dev panel reveal
|
|
711
794
|
tokens (translate + opacity + cross-blur, per-phase open/close dur/ease). */
|
|
712
795
|
.tl-refine-panel {
|
|
713
|
-
|
|
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;
|
|
714
798
|
width: var(--refine-w, 360px); max-width: 100%; overflow: hidden;
|
|
715
799
|
--panel-translate-x: var(--panel-close-distance);
|
|
716
800
|
--panel-scale-now: var(--panel-scale-close);
|
|
@@ -818,10 +902,7 @@
|
|
|
818
902
|
border: none; border-radius: 60px; cursor: pointer;
|
|
819
903
|
font: inherit; font-size: 13px; font-weight: 500; line-height: 14px; color: #0073e5;
|
|
820
904
|
background: rgba(0,115,229,0.04); text-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
|
821
|
-
box-shadow:
|
|
822
|
-
inset 0 0 0 1px rgba(0,101,208,0.10),
|
|
823
|
-
inset 0 -1px 0 0 rgba(0,0,0,0.06),
|
|
824
|
-
inset 0 0 0 1px rgba(196,196,196,0.10);
|
|
905
|
+
box-shadow: var(--drop-btn), var(--ring-blue);
|
|
825
906
|
--resize-dur: 300ms; --resize-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
826
907
|
transition: width var(--resize-dur) var(--resize-ease),
|
|
827
908
|
height var(--resize-dur) var(--resize-ease),
|
|
@@ -998,12 +1079,6 @@
|
|
|
998
1079
|
// Motion tokens are the verbatim cubic-beziers shipped by the transitions.dev
|
|
999
1080
|
// skill (_root.css); the "Common" set are the standard easings.net curves.
|
|
1000
1081
|
const EASING_LIBRARY = [
|
|
1001
|
-
{ group:"Standard" },
|
|
1002
|
-
{ label:"Linear", value:"linear" },
|
|
1003
|
-
{ label:"Ease", value:"ease" },
|
|
1004
|
-
{ label:"Ease in", value:"ease-in" },
|
|
1005
|
-
{ label:"Ease out", value:"ease-out" },
|
|
1006
|
-
{ label:"Ease in-out", value:"ease-in-out" },
|
|
1007
1082
|
{ group:"Motion tokens (transitions.dev)" },
|
|
1008
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." },
|
|
1009
1084
|
{ label:"Standard close", value:"cubic-bezier(0.4, 0, 0.2, 1)", usage:"Material-style accelerate→decelerate. Used for badge close / calm exits." },
|
|
@@ -1012,25 +1087,6 @@
|
|
|
1012
1087
|
{ label:"Morph open", value:"cubic-bezier(0.34, 1.25, 0.64, 1)", usage:"Plus-to-menu morph open — gentle bouncy expand." },
|
|
1013
1088
|
{ label:"Check bob", value:"cubic-bezier(0.34, 1.35, 0.64, 1)", usage:"Success check bob — playful settle on confirmation." },
|
|
1014
1089
|
{ label:"Big overshoot", value:"cubic-bezier(0.34, 3.85, 0.64, 1)", usage:"Avatar group hover return — aggressive, spring-like overshoot." },
|
|
1015
|
-
{ group:"Common (easings.net)" },
|
|
1016
|
-
{ label:"Sine in", value:"cubic-bezier(0.12, 0, 0.39, 0)" },
|
|
1017
|
-
{ label:"Sine out", value:"cubic-bezier(0.61, 1, 0.88, 1)" },
|
|
1018
|
-
{ label:"Sine in-out", value:"cubic-bezier(0.37, 0, 0.63, 1)" },
|
|
1019
|
-
{ label:"Cubic in", value:"cubic-bezier(0.32, 0, 0.67, 0)" },
|
|
1020
|
-
{ label:"Cubic out", value:"cubic-bezier(0.33, 1, 0.68, 1)" },
|
|
1021
|
-
{ label:"Cubic in-out", value:"cubic-bezier(0.65, 0, 0.35, 1)" },
|
|
1022
|
-
{ label:"Quart in", value:"cubic-bezier(0.5, 0, 0.75, 0)" },
|
|
1023
|
-
{ label:"Quart out", value:"cubic-bezier(0.25, 1, 0.5, 1)" },
|
|
1024
|
-
{ label:"Quart in-out", value:"cubic-bezier(0.76, 0, 0.24, 1)" },
|
|
1025
|
-
{ label:"Expo in", value:"cubic-bezier(0.7, 0, 0.84, 0)" },
|
|
1026
|
-
{ label:"Expo out", value:"cubic-bezier(0.16, 1, 0.3, 1)" },
|
|
1027
|
-
{ label:"Expo in-out", value:"cubic-bezier(0.87, 0, 0.13, 1)" },
|
|
1028
|
-
{ label:"Circ in", value:"cubic-bezier(0.55, 0, 1, 0.45)" },
|
|
1029
|
-
{ label:"Circ out", value:"cubic-bezier(0, 0.55, 0.45, 1)" },
|
|
1030
|
-
{ label:"Circ in-out", value:"cubic-bezier(0.85, 0, 0.15, 1)" },
|
|
1031
|
-
{ label:"Back in", value:"cubic-bezier(0.36, 0, 0.66, -0.56)" },
|
|
1032
|
-
{ label:"Back out", value:"cubic-bezier(0.34, 1.56, 0.64, 1)" },
|
|
1033
|
-
{ label:"Back in-out", value:"cubic-bezier(0.68, -0.6, 0.32, 1.6)" },
|
|
1034
1090
|
{ group:"Custom" },
|
|
1035
1091
|
{ label:"cubic-bezier(\u2026)", value:"__cubic" },
|
|
1036
1092
|
{ label:"custom", value:"__custom" },
|
|
@@ -1193,338 +1249,6 @@
|
|
|
1193
1249
|
}
|
|
1194
1250
|
}
|
|
1195
1251
|
|
|
1196
|
-
// ── preview controller ──
|
|
1197
|
-
// ── transition capture ──
|
|
1198
|
-
// Real-world transitions are usually triggered by interaction (hover, a
|
|
1199
|
-
// click on a *different* button, focus, JS state) — not by clicking the
|
|
1200
|
-
// element that carries the transition. So we observe transitions as they
|
|
1201
|
-
// actually run and remember each property's from/to computed values; the
|
|
1202
|
-
// preview then replays them with no synthetic click required.
|
|
1203
|
-
const _txCapture = new WeakMap(); // Element -> Map<prop,{from,to}>
|
|
1204
|
-
let _txInstalled = false;
|
|
1205
|
-
function _txCamel(p){return p.replace(/-([a-z])/g,(_,c)=>c.toUpperCase());}
|
|
1206
|
-
function _txOnRun(e){
|
|
1207
|
-
const el=e.target, prop=e.propertyName;
|
|
1208
|
-
if(!el||!prop||typeof el.getAnimations!=="function")return;
|
|
1209
|
-
if(el.closest&&el.closest("[data-timeline-panel]"))return;
|
|
1210
|
-
try{
|
|
1211
|
-
const ck=_txCamel(prop);
|
|
1212
|
-
const anims=el.getAnimations();
|
|
1213
|
-
let anim=anims.find(a=>a.transitionProperty===prop);
|
|
1214
|
-
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;}});
|
|
1215
|
-
if(!anim||!anim.effect)return;
|
|
1216
|
-
const kf=anim.effect.getKeyframes();
|
|
1217
|
-
if(!kf||kf.length<2)return;
|
|
1218
|
-
const from=kf[0][ck], to=kf[kf.length-1][ck];
|
|
1219
|
-
if(from==null||to==null)return;
|
|
1220
|
-
let rec=_txCapture.get(el); if(!rec){rec=new Map();_txCapture.set(el,rec);}
|
|
1221
|
-
rec.set(prop,{from:String(from),to:String(to)});
|
|
1222
|
-
}catch{}
|
|
1223
|
-
}
|
|
1224
|
-
function _txInstallCapture(){
|
|
1225
|
-
if(_txInstalled||typeof document==="undefined")return;
|
|
1226
|
-
_txInstalled=true;
|
|
1227
|
-
// capture phase + the bubbling transitionrun event reaches document
|
|
1228
|
-
document.addEventListener("transitionrun",_txOnRun,true);
|
|
1229
|
-
}
|
|
1230
|
-
// from/to recovered from a previously observed real run
|
|
1231
|
-
function _txCaptured(el,et){
|
|
1232
|
-
const rec=_txCapture.get(el);if(!rec)return null;
|
|
1233
|
-
const from={},to={};let n=0;
|
|
1234
|
-
for(const t of et){const c=rec.get(t.property);if(c){from[t.property]=c.from;to[t.property]=c.to;n++;}}
|
|
1235
|
-
return n?{from,to}:null;
|
|
1236
|
-
}
|
|
1237
|
-
// Discover the transition's end-state WITHOUT interaction by reading the
|
|
1238
|
-
// stylesheets: find rules that set a transitioning prop and represent a
|
|
1239
|
-
// *state* of the element (a :hover/:focus pseudo, or a toggled class/attr
|
|
1240
|
-
// like `.modal.open`). from = current computed value, to = the rule's value.
|
|
1241
|
-
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;
|
|
1242
|
-
function _txReduceLast(el,sel){
|
|
1243
|
-
const parts=sel.split(/(\s*[>+~]\s*|\s+)/);
|
|
1244
|
-
let i=parts.length-1;while(i>=0&&/^\s*[>+~]?\s*$/.test(parts[i]))i--;
|
|
1245
|
-
if(i<0)return null;
|
|
1246
|
-
const toks=parts[i].match(/[.#]?[\w-]+|\[[^\]]*\]|::?[\w-]+(?:\([^)]*\))?|\*/g)||[];
|
|
1247
|
-
let dropped=false,hasId=false;
|
|
1248
|
-
const kept=toks.filter(tk=>{
|
|
1249
|
-
if(tk[0]==="."){const ok=el.classList.contains(tk.slice(1));if(ok)hasId=true;else dropped=true;return ok;}
|
|
1250
|
-
if(tk[0]==="["){let ok;try{ok=el.matches(tk);}catch{ok=false;}if(ok)hasId=true;else dropped=true;return ok;}
|
|
1251
|
-
if(tk[0]==="#"){const ok=el.id&&("#"+el.id)===tk;if(ok)hasId=true;else dropped=true;return ok;}
|
|
1252
|
-
if(tk[0]===":"){dropped=true;return false;} // pseudo = the state delta
|
|
1253
|
-
return true; // tag / *
|
|
1254
|
-
});
|
|
1255
|
-
// Only a real *variant of this element*: we must have removed a state token
|
|
1256
|
-
// AND kept a class/id/attr that ties the rule to el. Dropping every class to
|
|
1257
|
-
// land on a bare tag/`*` would falsely match unrelated rules.
|
|
1258
|
-
if(!dropped||!hasId)return null;
|
|
1259
|
-
parts[i]=kept.join("");
|
|
1260
|
-
return parts.join("");
|
|
1261
|
-
}
|
|
1262
|
-
function _txIsStateOf(el,sel){
|
|
1263
|
-
try{if(el.matches(sel))return false;}catch{return false;}
|
|
1264
|
-
const noP=sel.replace(_TX_STATE_PSEUDO,"").trim();
|
|
1265
|
-
if(noP&&noP!==sel){try{if(el.matches(noP))return true;}catch{}}
|
|
1266
|
-
const red=_txReduceLast(el,sel);
|
|
1267
|
-
if(red&&red!==sel){try{if(el.matches(red))return true;}catch{}}
|
|
1268
|
-
return false;
|
|
1269
|
-
}
|
|
1270
|
-
function _txScanRules(rules,el,props,to){
|
|
1271
|
-
for(const rule of rules){
|
|
1272
|
-
// Read style rules directly. NOTE: in browsers with CSS Nesting a plain
|
|
1273
|
-
// CSSStyleRule also has a (usually empty) .cssRules, so test selectorText
|
|
1274
|
-
// first — don't treat every style rule as a grouping rule.
|
|
1275
|
-
const decl=rule.style;
|
|
1276
|
-
if(decl&&rule.selectorText){
|
|
1277
|
-
let sets=false;for(const p of props){if(decl.getPropertyValue(p)){sets=true;break;}}
|
|
1278
|
-
if(sets){
|
|
1279
|
-
for(const sub of rule.selectorText.split(",")){
|
|
1280
|
-
const s=sub.trim();if(!s)continue;
|
|
1281
|
-
if(_txIsStateOf(el,s)){for(const p of props){const v=decl.getPropertyValue(p);if(v&&!(p in to))to[p]=v.trim();}}
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
// Recurse into @media / @supports / @layer and any nested rules.
|
|
1286
|
-
if(rule.cssRules&&rule.cssRules.length){
|
|
1287
|
-
try{if(rule.media&&!window.matchMedia(rule.media.mediaText).matches)continue;}catch{}
|
|
1288
|
-
_txScanRules(rule.cssRules,el,props,to);
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
function _txDiscover(el,et){
|
|
1293
|
-
if(typeof document==="undefined"||!el.matches)return null;
|
|
1294
|
-
const props=et.map(t=>t.property).filter(p=>p&&p!=="all");
|
|
1295
|
-
if(!props.length)return null;
|
|
1296
|
-
const to={};
|
|
1297
|
-
for(const sheet of Array.from(document.styleSheets||[])){
|
|
1298
|
-
let rules;try{rules=sheet.cssRules;}catch{continue;} // cross-origin sheet
|
|
1299
|
-
if(rules)_txScanRules(rules,el,props,to);
|
|
1300
|
-
}
|
|
1301
|
-
const keys=Object.keys(to);if(!keys.length)return null;
|
|
1302
|
-
const cs=getComputedStyle(el);const from={};
|
|
1303
|
-
for(const p of keys)from[p]=cs.getPropertyValue(p)||cs[_txCamel(p)]||"";
|
|
1304
|
-
return {from,to};
|
|
1305
|
-
}
|
|
1306
|
-
// Toggle a phase's state class/attribute (e.g. ".is-open" or "[data-open]")
|
|
1307
|
-
// and read the resulting computed values, so a phase plays its real end-state
|
|
1308
|
-
// even when the source uses a toggled class the DOM isn't currently in.
|
|
1309
|
-
function _txToState(el,et,toState){
|
|
1310
|
-
if(typeof document==="undefined"||!el.matches||!toState)return null;
|
|
1311
|
-
const props=et.map(t=>t.property).filter(p=>p&&p!=="all");
|
|
1312
|
-
if(!props.length)return null;
|
|
1313
|
-
const s=String(toState).trim();
|
|
1314
|
-
let applied=null; // {undo}
|
|
1315
|
-
try{
|
|
1316
|
-
if(s[0]==="."){const c=s.slice(1);if(c&&!el.classList.contains(c)){el.classList.add(c);applied=()=>el.classList.remove(c);}}
|
|
1317
|
-
else if(s[0]==="["){
|
|
1318
|
-
const m=s.match(/^\[\s*([\w-]+)\s*(?:([~|^$*]?=)\s*"?([^"\]]*)"?\s*)?\]$/);
|
|
1319
|
-
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);}}
|
|
1320
|
-
}
|
|
1321
|
-
}catch{}
|
|
1322
|
-
if(!applied)return null; // already in that state, or unparseable
|
|
1323
|
-
const cs=getComputedStyle(el);const to={};
|
|
1324
|
-
for(const p of props)to[p]=cs.getPropertyValue(p)||cs[_txCamel(p)]||"";
|
|
1325
|
-
try{applied();}catch{}
|
|
1326
|
-
const cs0=getComputedStyle(el);const from={};
|
|
1327
|
-
for(const p of props)from[p]=cs0.getPropertyValue(p)||cs0[_txCamel(p)]||"";
|
|
1328
|
-
const keys=Object.keys(to).filter(p=>to[p]!=null&&to[p]!==""&&to[p]!==from[p]);
|
|
1329
|
-
if(!keys.length)return null;
|
|
1330
|
-
const f={},t={};for(const p of keys){f[p]=from[p];t[p]=to[p];}
|
|
1331
|
-
return {from:f,to:t};
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
// Add/remove a single state token (".class" or "[attr=val]"/"[attr]") on an
|
|
1335
|
-
// element. Returns false for tokens we can't toggle (e.g. pseudo ":hover").
|
|
1336
|
-
function _applyStateToken(el,token,add){
|
|
1337
|
-
const s=String(token||"").trim();
|
|
1338
|
-
if(!s)return true;
|
|
1339
|
-
if(s[0]==="."){const c=s.slice(1);if(!c)return false;if(add)el.classList.add(c);else el.classList.remove(c);return true;}
|
|
1340
|
-
if(s[0]==="["){
|
|
1341
|
-
const m=s.match(/^\[\s*([\w-]+)\s*(?:[~|^$*]?=\s*"?([^"\]]*)"?\s*)?\]$/);
|
|
1342
|
-
if(!m)return false;
|
|
1343
|
-
const name=m[1],val=m[2]!=null?m[2]:"";
|
|
1344
|
-
if(add)el.setAttribute(name,val);else el.removeAttribute(name);
|
|
1345
|
-
return true;
|
|
1346
|
-
}
|
|
1347
|
-
return false; // pseudo (:hover/:focus) or unrecognised → not class-toggleable
|
|
1348
|
-
}
|
|
1349
|
-
function _tokenToggleable(token){
|
|
1350
|
-
const s=String(token||"").trim();
|
|
1351
|
-
return !s || s[0]==="."||s[0]==="[";
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
class PreviewController {
|
|
1355
|
-
state="idle"; listeners=new Set(); cleanups=[]; animations=[]; progressListeners=new Set(); _rafId=null; scanner=null; _gen=0;
|
|
1356
|
-
rate=1; loop=false; _current=null;
|
|
1357
|
-
setScanner(s){this.scanner=s;} getState(){return this.state;}
|
|
1358
|
-
setRate(r){this.rate=r;for(const a of this.animations){try{a.playbackRate=r;}catch{}}}
|
|
1359
|
-
setLoop(v){this.loop=v;}
|
|
1360
|
-
subscribe(fn){this.listeners.add(fn);return()=>this.listeners.delete(fn);}
|
|
1361
|
-
onProgress(fn){this.progressListeners.add(fn);return()=>this.progressListeners.delete(fn);}
|
|
1362
|
-
play(entry){this.stop();this._gen++;this._current=entry;
|
|
1363
|
-
if(this.scanner)this.scanner.pause();this._playCss(entry,this._gen);this._setState("playing");}
|
|
1364
|
-
// Resolve a selectable item to a flat list of {el, et, toState} arm targets.
|
|
1365
|
-
// Phase items fan out to their members (selector may match several elements,
|
|
1366
|
-
// e.g. staggered items); flat items use their bound DOM elements.
|
|
1367
|
-
_targets(entry){
|
|
1368
|
-
const out=[];
|
|
1369
|
-
if(entry.kind==="phase"){
|
|
1370
|
-
for(const m of (entry.members||[])){
|
|
1371
|
-
if(!m.selector||!m.lanes||!m.lanes.length)continue;
|
|
1372
|
-
let els=[];try{els=Array.from(document.querySelectorAll(m.selector));}catch{}
|
|
1373
|
-
for(const el of els)out.push({el,et:m.lanes,toState:m.toState});
|
|
1374
|
-
}
|
|
1375
|
-
return out;
|
|
1376
|
-
}
|
|
1377
|
-
if(!entry.bindings||entry.bindings.type!=="css")return out;
|
|
1378
|
-
const et=entry.effectiveTimings||entry.properties.map(p=>({property:p,durationMs:entry.durationMs,delayMs:entry.delayMs,easing:entry.easing}));
|
|
1379
|
-
for(const wr of entry.bindings.elements){const el=wr.deref&&wr.deref();if(el)out.push({el,et,toState:null});}
|
|
1380
|
-
return out;
|
|
1381
|
-
}
|
|
1382
|
-
pause(){if(this.state!=="playing")return;for(const a of this.animations){try{a.pause();}catch{}}this._stopPL();this._setState("paused");}
|
|
1383
|
-
resume(){if(this.state!=="paused")return;for(const a of this.animations){try{a.play();}catch{}}this._startPL();this._setState("playing");}
|
|
1384
|
-
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");}
|
|
1385
|
-
restart(entry){this.stop();requestAnimationFrame(()=>this.play(entry));}
|
|
1386
|
-
playPaused(entry,seekMs){
|
|
1387
|
-
this.stop();this._gen++;const gen=this._gen;
|
|
1388
|
-
this._pendingSeek=seekMs;
|
|
1389
|
-
if(this.scanner)this.scanner.pause();
|
|
1390
|
-
const armed=this._armAll(entry);
|
|
1391
|
-
const els=armed.els;
|
|
1392
|
-
if(!els.length){this._setState("paused");if(seekMs!=null)this._ep(seekMs);return;}
|
|
1393
|
-
this.cleanups.push(armed.restore);
|
|
1394
|
-
this.animations=[];
|
|
1395
|
-
requestAnimationFrame(()=>{if(this._gen!==gen)return;
|
|
1396
|
-
const running=[];for(const el of els){for(const a of el.getAnimations()){a.pause();a.playbackRate=this.rate;running.push(a);}}
|
|
1397
|
-
this.animations=running;
|
|
1398
|
-
const t=this._pendingSeek??seekMs;
|
|
1399
|
-
if(t!=null){for(const a of this.animations){try{a.currentTime=t;}catch{}}this._ep(t);}
|
|
1400
|
-
});
|
|
1401
|
-
this._setState("paused");
|
|
1402
|
-
if(seekMs!=null)this._ep(seekMs);
|
|
1403
|
-
}
|
|
1404
|
-
// Snapshot/restore the toggled state on a root element.
|
|
1405
|
-
_snapshotState(el,toggleTokens){
|
|
1406
|
-
const attrs={},classes={};
|
|
1407
|
-
for(const t of toggleTokens){const s=String(t).trim();
|
|
1408
|
-
if(s[0]===".")classes[s.slice(1)]=el.classList.contains(s.slice(1));
|
|
1409
|
-
else if(s[0]==="["){const m=s.match(/^\[\s*([\w-]+)/);if(m)attrs[m[1]]=el.getAttribute(m[1]);}}
|
|
1410
|
-
return {attrs,classes};
|
|
1411
|
-
}
|
|
1412
|
-
_restoreState(el,snap){
|
|
1413
|
-
for(const c in snap.classes){if(snap.classes[c])el.classList.add(c);else el.classList.remove(c);}
|
|
1414
|
-
for(const n in snap.attrs){const v=snap.attrs[n];if(v==null)el.removeAttribute(n);else el.setAttribute(n,v);}
|
|
1415
|
-
}
|
|
1416
|
-
_setRootState(el,target,toggleTokens){
|
|
1417
|
-
for(const t of toggleTokens)_applyStateToken(el,t,false);
|
|
1418
|
-
if(target)_applyStateToken(el,target,true);
|
|
1419
|
-
}
|
|
1420
|
-
// Arm a whole phase by toggling the REAL driving state on its stateTarget,
|
|
1421
|
-
// so the actual CSS animates every member in the correct direction (open
|
|
1422
|
-
// base→state, close state→base). Edited timings are applied inline on the
|
|
1423
|
-
// member elements so they override the source. Returns {els, restore} or
|
|
1424
|
-
// null to fall back to per-member arming.
|
|
1425
|
-
_armPhase(entry,root){
|
|
1426
|
-
const fromState=entry.fromState??null, toState=entry.toState??null;
|
|
1427
|
-
if(![fromState,toState].every(_tokenToggleable))return null; // pseudo states → legacy
|
|
1428
|
-
const toggleTokens=[fromState,toState].filter(Boolean);
|
|
1429
|
-
if(!toggleTokens.length)return null;
|
|
1430
|
-
const memberEls=[];
|
|
1431
|
-
for(const m of (entry.members||[])){
|
|
1432
|
-
if(!m.selector||!m.lanes||!m.lanes.length)continue;
|
|
1433
|
-
let els=[];try{els=Array.from(document.querySelectorAll(m.selector));}catch{}
|
|
1434
|
-
for(const el of els)memberEls.push({el,lanes:m.lanes});
|
|
1435
|
-
}
|
|
1436
|
-
if(!memberEls.length)return null;
|
|
1437
|
-
const savedRoot=this._snapshotState(root,toggleTokens);
|
|
1438
|
-
const savedStyles=memberEls.map(({el})=>el.style.cssText);
|
|
1439
|
-
// 1. suppress transitions and commit the FROM state
|
|
1440
|
-
for(const {el} of memberEls)el.style.transition="none";
|
|
1441
|
-
this._setRootState(root,fromState,toggleTokens);
|
|
1442
|
-
void root.offsetWidth;for(const {el} of memberEls)void el.offsetWidth;
|
|
1443
|
-
// 2. apply the (possibly edited) per-member timings inline
|
|
1444
|
-
for(const {el,lanes} of memberEls){
|
|
1445
|
-
el.style.transition=lanes.map(t=>`${t.property} ${formatCssTime(t.durationMs)} ${t.easing} ${formatCssTime(t.delayMs)}`).join(", ");
|
|
1446
|
-
}
|
|
1447
|
-
// 3. toggle to the TO state → real CSS transitions fire on every member
|
|
1448
|
-
this._setRootState(root,toState,toggleTokens);
|
|
1449
|
-
const restore=()=>{try{this._restoreState(root,savedRoot);}catch{}
|
|
1450
|
-
memberEls.forEach(({el},i)=>{try{el.style.cssText=savedStyles[i];}catch{}});};
|
|
1451
|
-
return {els:memberEls.map(m=>m.el),restore};
|
|
1452
|
-
}
|
|
1453
|
-
// Resolve an entry to armed element(s) + a restore fn. Phase items with a
|
|
1454
|
-
// toggleable stateTarget use _armPhase; everything else uses per-target arm.
|
|
1455
|
-
_armAll(entry){
|
|
1456
|
-
if(entry&&entry.kind==="phase"&&entry.stateTarget&&typeof document!=="undefined"){
|
|
1457
|
-
let root=null;try{root=document.querySelector(entry.stateTarget);}catch{}
|
|
1458
|
-
if(root){const r=this._armPhase(entry,root);if(r)return r;}
|
|
1459
|
-
}
|
|
1460
|
-
const targets=this._targets(entry);
|
|
1461
|
-
const els=[],restores=[];
|
|
1462
|
-
for(const {el,et,toState} of targets){restores.push(this._arm(el,et,toState));els.push(el);}
|
|
1463
|
-
return {els,restore:()=>{for(const c of restores){try{c();}catch{}}}};
|
|
1464
|
-
}
|
|
1465
|
-
seek(timeMs){
|
|
1466
|
-
if(this.state==="idle")return;
|
|
1467
|
-
if(this.state==="playing"){for(const a of this.animations){try{a.pause();}catch{}}this._stopPL();this._setState("paused");}
|
|
1468
|
-
for(const a of this.animations){try{a.currentTime=timeMs;}catch{}}
|
|
1469
|
-
this._ep(timeMs);
|
|
1470
|
-
this._pendingSeek=timeMs;
|
|
1471
|
-
}
|
|
1472
|
-
_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");}
|
|
1473
|
-
// Arm an element so its transition runs now, without needing the user to
|
|
1474
|
-
// trigger it first. Priority:
|
|
1475
|
-
// 1. a previously observed real run (exact),
|
|
1476
|
-
// 2. end-state discovered from the stylesheets (hover/focus/toggled class),
|
|
1477
|
-
// 3. a synthetic opacity/transform pulse to preview the timing,
|
|
1478
|
-
// 4. click the element (last resort, may have side effects).
|
|
1479
|
-
// Returns a restore fn.
|
|
1480
|
-
_arm(el,et,toState){
|
|
1481
|
-
const saved=el.style.cssText;
|
|
1482
|
-
const tv=et.map(t=>`${t.property} ${formatCssTime(t.durationMs)} ${t.easing} ${formatCssTime(t.delayMs)}`).join(", ");
|
|
1483
|
-
// toState (a phase's driving class/attr) is the most reliable source of
|
|
1484
|
-
// the real end-state; fall back to a captured run, then CSSOM discovery.
|
|
1485
|
-
const states=(toState&&_txToState(el,et,toState))||_txCaptured(el,et)||_txDiscover(el,et);
|
|
1486
|
-
if(states){
|
|
1487
|
-
const props=et.map(t=>t.property).filter(p=>states.from[p]!=null&&states.to[p]!=null);
|
|
1488
|
-
if(props.length){
|
|
1489
|
-
el.style.transition="none";
|
|
1490
|
-
for(const p of props)el.style.setProperty(p,states.from[p]);
|
|
1491
|
-
void el.offsetWidth; // commit the from-state before transitioning
|
|
1492
|
-
el.style.transition=tv;
|
|
1493
|
-
for(const p of props)el.style.setProperty(p,states.to[p]);
|
|
1494
|
-
return ()=>{try{el.style.cssText=saved;}catch{}};
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
const synth=et.filter(t=>t.property==="opacity"||t.property==="transform"||t.property==="all");
|
|
1498
|
-
if(synth.length){
|
|
1499
|
-
const dur=Math.max(...et.map(t=>(t.durationMs||0)+(t.delayMs||0)))||et[0].durationMs||300;
|
|
1500
|
-
const ease=et[0].easing||"ease";
|
|
1501
|
-
const hasOp=synth.some(t=>t.property!=="transform"),hasTr=synth.some(t=>t.property!=="opacity");
|
|
1502
|
-
const k0={},k1={},k2={};
|
|
1503
|
-
if(hasOp){k0.opacity=1;k1.opacity=0.35;k2.opacity=1;}
|
|
1504
|
-
if(hasTr){k0.transform="none";k1.transform="translateY(8px)";k2.transform="none";}
|
|
1505
|
-
try{el.animate([k0,k1,k2],{duration:dur,easing:ease,fill:"none"});return ()=>{};}catch{}
|
|
1506
|
-
}
|
|
1507
|
-
el.style.transition=tv;el.click();
|
|
1508
|
-
return ()=>{try{el.style.cssText=saved;}catch{}};
|
|
1509
|
-
}
|
|
1510
|
-
_playCss(entry,gen){
|
|
1511
|
-
const armed=this._armAll(entry);
|
|
1512
|
-
const els=armed.els;
|
|
1513
|
-
this.animations=[];
|
|
1514
|
-
if(!els.length){this._finish();return;}
|
|
1515
|
-
this.cleanups.push(armed.restore);
|
|
1516
|
-
requestAnimationFrame(()=>{if(this._gen!==gen)return;
|
|
1517
|
-
const running=[];for(const el of els){for(const a of el.getAnimations()){a.playbackRate=this.rate;running.push(a);}}
|
|
1518
|
-
this.animations=running;this._startPL();
|
|
1519
|
-
const onDone=()=>{if(this._gen!==gen||this.state!=="playing")return;if(this.loop&&this._current){this.play(this._current);}else{this._finish();}};
|
|
1520
|
-
if(running.length>0){Promise.allSettled(running.map(a=>a.finished)).then(onDone);}else{onDone();}
|
|
1521
|
-
});}
|
|
1522
|
-
_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);}
|
|
1523
|
-
_stopPL(){if(this._rafId!==null){cancelAnimationFrame(this._rafId);this._rafId=null;}}
|
|
1524
|
-
_ep(p){for(const fn of this.progressListeners)fn(p);}
|
|
1525
|
-
_setState(s){this.state=s;for(const fn of this.listeners)fn(s);}
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
1252
|
// ── scanner ──
|
|
1529
1253
|
class DomScanner {
|
|
1530
1254
|
observer=null;rafId=null;running=false;paused=false;
|
|
@@ -1567,7 +1291,7 @@
|
|
|
1567
1291
|
function usePropOverride(){const{registry,activeId}=useContext(TimelineCtx);return{setPropOverride:useCallback((prop,o)=>{if(activeId)registry.setPropOverride(activeId,prop,o);},[registry,activeId])};}
|
|
1568
1292
|
|
|
1569
1293
|
// ── components ──
|
|
1570
|
-
const LABEL_W =
|
|
1294
|
+
const LABEL_W = 200;
|
|
1571
1295
|
const CLOSE_MS = 150;
|
|
1572
1296
|
const DURATION_TOKENS = [
|
|
1573
1297
|
{label:"Duration-fast", ms:150, usage:"Quick state changes — hovers, toggles, button presses, dropdown & modal close, text swaps."},
|
|
@@ -1605,6 +1329,8 @@
|
|
|
1605
1329
|
};
|
|
1606
1330
|
ICONS.minimize = ICONS.chevron;
|
|
1607
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"/>`};
|
|
1608
1334
|
function Ic({name, size=16}){
|
|
1609
1335
|
const ic = ICONS[name];
|
|
1610
1336
|
if(!ic) return null;
|
|
@@ -1817,7 +1543,10 @@
|
|
|
1817
1543
|
},[phase]);
|
|
1818
1544
|
if(!render)return null;
|
|
1819
1545
|
|
|
1820
|
-
|
|
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]);
|
|
1821
1550
|
const agentMode = mode==="llm";
|
|
1822
1551
|
// null (unknown / still probing) is treated as ready so the panel doesn't
|
|
1823
1552
|
// flash the "unavailable" copy before /health resolves.
|
|
@@ -1854,7 +1583,7 @@
|
|
|
1854
1583
|
h("div",{className:"tl-refine-unavail-text"},error||"The agent reported an error."));
|
|
1855
1584
|
foot = startBtn({label:"Try again"});
|
|
1856
1585
|
} else if(phase==="done"){
|
|
1857
|
-
if(
|
|
1586
|
+
if(visible.length===0){
|
|
1858
1587
|
const emptyMsg = refineType==="replace"
|
|
1859
1588
|
? "I didn't find any transition that would be a good fit as a replacement."
|
|
1860
1589
|
: "Already aligned to the transitions.dev motion tokens. Nothing to refine.";
|
|
@@ -1862,9 +1591,9 @@
|
|
|
1862
1591
|
h("p",{className:"tl-refine-idle-text"},emptyMsg));
|
|
1863
1592
|
foot = startBtn({label:"Scan again"});
|
|
1864
1593
|
} else {
|
|
1865
|
-
body = h(RefineResults,{summary,suggestions,appliedIds,onApply});
|
|
1594
|
+
body = h(RefineResults,{summary:refineType==="replace"?null:summary,suggestions:visible,appliedIds,onApply});
|
|
1866
1595
|
foot = pending.length>1
|
|
1867
|
-
? 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+")")
|
|
1868
1597
|
: startBtn({label:"Scan again"});
|
|
1869
1598
|
}
|
|
1870
1599
|
} else { // idle
|
|
@@ -1889,7 +1618,7 @@
|
|
|
1889
1618
|
h("div",{className:"tl-refine-head"},
|
|
1890
1619
|
h("div",{className:"tl-refine-titles"},
|
|
1891
1620
|
h("h3",null,"Refine"),
|
|
1892
|
-
h("p",null,label
|
|
1621
|
+
h("p",null,label||"Transitions review")),
|
|
1893
1622
|
h("div",{className:"tl-refine-actions"},
|
|
1894
1623
|
h("button",{ref:modeRef,className:cx("tl-refine-mode",modeOpen&&"is-open"),
|
|
1895
1624
|
"aria-haspopup":"menu","aria-expanded":modeOpen?"true":"false",onClick:()=>setModeOpen(v=>!v)},
|
|
@@ -1932,10 +1661,12 @@
|
|
|
1932
1661
|
return out;
|
|
1933
1662
|
}
|
|
1934
1663
|
|
|
1935
|
-
function Header({entries,active,onSelect,onReset,onCopy,copied,snap,setSnap,onMinimize,onRefine,refineActive,onAccept,acceptState,acceptDisabled,acceptError,scanning}){
|
|
1664
|
+
function Header({entries,active,onSelect,onReset,onCopy,copied,snap,setSnap,onMinimize,onRefine,refineActive,onAccept,acceptState,acceptDisabled,acceptError,scanning,onRescan}){
|
|
1936
1665
|
const[pick,setPick]=useState(false);
|
|
1937
1666
|
const[setg,setSetg]=useState(false);
|
|
1938
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
|
|
1939
1670
|
// Split selectable items into agent groups (component → phases) and the
|
|
1940
1671
|
// flat, ungrouped DOM transitions, preserving order.
|
|
1941
1672
|
const groupList=[]; const groupMap=new Map(); const flatItems=[];
|
|
@@ -1946,6 +1677,12 @@
|
|
|
1946
1677
|
g.phases.push(e);
|
|
1947
1678
|
}else flatItems.push(e);
|
|
1948
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;
|
|
1949
1686
|
const phaseItem=(e)=>h(MenuItem,{key:e.id,active:active&&e.id===active.id,onClick:()=>{onSelect(e.id);setPick(false);},
|
|
1950
1687
|
right:active&&e.id===active.id&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},
|
|
1951
1688
|
h("span",null,e.phaseLabel,h("span",{className:"tl-menu-dim"}," "+e.durationMs+"ms")));
|
|
@@ -1959,28 +1696,32 @@
|
|
|
1959
1696
|
:h("span",{className:"tl-dim"},"None"),
|
|
1960
1697
|
h("span",{className:"tl-ghost-chev"},h(Ic,{name:"chevron"}))),
|
|
1961
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);}}})),
|
|
1962
1703
|
entries.length===0
|
|
1963
1704
|
? h("div",{className:"tl-menu-empty"},"No transitions found")
|
|
1964
|
-
:
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
...
|
|
1968
|
-
|
|
1969
|
-
|
|
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))),
|
|
1970
1713
|
h("span",{className:"tl-header-count"},
|
|
1971
1714
|
scanning?"Grouping…":entries.length+" transition"+(entries.length===1?"":"s")+" found"),
|
|
1972
|
-
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"})),
|
|
1973
1716
|
h(Dropdown,{open:setg,onClose:()=>setSetg(false),triggerRef:gearRef,width:210,align:"right"},
|
|
1974
|
-
h(MenuItem,{onClick:()=>setSnap(v=>!v),right:snap&&h("span",{className:"tl-menu-check"},h(Ic,{name:"check"}))},"Snap to grid")
|
|
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")),
|
|
1975
1722
|
h("span",{className:"t-tt-wrap"},
|
|
1976
1723
|
h("button",{className:"tl-sec-btn t-tt-trigger",disabled:!active,onClick:onReset},"Reset"),
|
|
1977
1724
|
h("span",{className:"t-tt tl-tt-below",role:"tooltip"},"Reset values")),
|
|
1978
|
-
h("span",{className:"t-tt-wrap"},
|
|
1979
|
-
h("button",{className:"tl-icon-btn t-tt-trigger",disabled:!active,"aria-label":"Copy values",onClick:onCopy},
|
|
1980
|
-
h("span",{className:"t-icon-swap","data-state":copied?"b":"a"},
|
|
1981
|
-
h("span",{className:"t-icon","data-icon":"a"},h(Ic,{name:"copy"})),
|
|
1982
|
-
h("span",{className:"t-icon","data-icon":"b"},h(Ic,{name:"check"})))),
|
|
1983
|
-
h("span",{className:"t-tt tl-tt-below",role:"tooltip"},copied?"Copied":"Copy values")),
|
|
1984
1725
|
h("span",{className:"t-tt-wrap"},
|
|
1985
1726
|
h("button",{className:cx("tl-accept-btn",acceptState==="saving"&&"is-saving",acceptState==="done"&&"is-done"),
|
|
1986
1727
|
disabled:!active||acceptDisabled||acceptState==="saving"||acceptState==="done",onClick:onAccept,"aria-label":"Accept changes to your code"},
|
|
@@ -2011,7 +1752,7 @@
|
|
|
2011
1752
|
);
|
|
2012
1753
|
}
|
|
2013
1754
|
|
|
2014
|
-
function PropTrack({property, member, delayMs, durationMs, selected,
|
|
1755
|
+
function PropTrack({property, member, delayMs, durationMs, selected, onSelect, onDelayChange, onDurationChange, snap, scaleMs, lockDuration, labelW}){
|
|
2015
1756
|
const trackRef=useRef(null);
|
|
2016
1757
|
const grid = snap ? 25 : 1;
|
|
2017
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]);
|
|
@@ -2038,9 +1779,8 @@
|
|
|
2038
1779
|
},[delayMs,durationMs,pxToMs,grid,scaleMs,onSelect,onDelayChange,onDurationChange,lockDuration]);
|
|
2039
1780
|
|
|
2040
1781
|
const delPct=(delayMs/scaleMs)*100; const durPct=(durationMs/scaleMs)*100;
|
|
2041
|
-
return h("div",{className:cx("tl-prop-row",selected&&"selected",
|
|
1782
|
+
return h("div",{className:cx("tl-prop-row",selected&&"selected",lockDuration&&"is-spring"),onClick:onSelect},
|
|
2042
1783
|
h("div",{className:"tl-prop-head",style:labelW!=null?{flexBasis:labelW+"px"}:undefined},
|
|
2043
|
-
h("span",{className:"tl-prop-grip",title:"Drag to reorder",onMouseDown:onReorder},h(Ic,{name:"dots"})),
|
|
2044
1784
|
h("span",{className:"tl-prop-label"},
|
|
2045
1785
|
member&&h("span",{className:"tl-prop-member"},member),
|
|
2046
1786
|
h("span",{className:"tl-prop-prop"},property))),
|
|
@@ -2129,18 +1869,25 @@
|
|
|
2129
1869
|
}
|
|
2130
1870
|
|
|
2131
1871
|
// shared plot geometry for both the easing curve and the spring curve.
|
|
2132
|
-
//
|
|
2133
|
-
|
|
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 };
|
|
2134
1878
|
|
|
2135
1879
|
function EasingEditor({easing, cubic, spring, durationMs, propKey, apply}){
|
|
2136
1880
|
const [tab, setTab] = useState(spring ? "springs" : "easing");
|
|
2137
1881
|
// when the user selects a different property, reflect that property's mode
|
|
2138
1882
|
useEffect(()=>{ setTab(spring ? "springs" : "easing"); /* eslint-disable-next-line */ }, [propKey]);
|
|
2139
1883
|
|
|
2140
|
-
// remember the last real (non-spring) easing so we can restore
|
|
2141
|
-
// 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).
|
|
2142
1887
|
const lastEasingRef = useRef("ease");
|
|
1888
|
+
const lastEaseDurRef = useRef(durationMs);
|
|
2143
1889
|
useEffect(()=>{ if(!spring && easing && !/^linear\(/.test(easing)) lastEasingRef.current = easing; }, [easing, spring]);
|
|
1890
|
+
useEffect(()=>{ if(!spring && durationMs!=null) lastEaseDurRef.current = durationMs; }, [durationMs, spring]);
|
|
2144
1891
|
|
|
2145
1892
|
const applySpring = useCallback((stiffness,damping,mass,name)=>{
|
|
2146
1893
|
const sim = simulateSpring(stiffness,damping,mass);
|
|
@@ -2153,7 +1900,7 @@
|
|
|
2153
1900
|
if(!spring){ const p=SPRING_PRESETS[0]; applySpring(p.stiffness,p.damping,p.mass,p.label); }
|
|
2154
1901
|
} else {
|
|
2155
1902
|
setTab("easing");
|
|
2156
|
-
if(spring) apply({ spring:null, easing:lastEasingRef.current||"ease" });
|
|
1903
|
+
if(spring) apply({ spring:null, easing:lastEasingRef.current||"ease", durationMs:lastEaseDurRef.current });
|
|
2157
1904
|
}
|
|
2158
1905
|
},[spring,apply,applySpring]);
|
|
2159
1906
|
|
|
@@ -2183,59 +1930,6 @@
|
|
|
2183
1930
|
h("div",{className:"t-page",ref:springPageRef,"data-page-id":"2","aria-hidden":tab!=="springs"},
|
|
2184
1931
|
h(SpringTab,{spring,applySpring})),
|
|
2185
1932
|
),
|
|
2186
|
-
h(PositionPreview,{easing, durationMs}),
|
|
2187
|
-
);
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
// Animated position preview (à la easing.dev): a marker travels left→right
|
|
2191
|
-
// using the live timing function + duration, pauses, returns, and loops.
|
|
2192
|
-
// Works for any timing function — cubic-bezier keywords or a spring's linear().
|
|
2193
|
-
function PositionPreview({easing, durationMs}){
|
|
2194
|
-
const trackRef = useRef(null);
|
|
2195
|
-
const dotRef = useRef(null);
|
|
2196
|
-
const [playing, setPlaying] = useState(false);
|
|
2197
|
-
const safeEasing = (!easing || easing.trim()==="") ? "linear" : easing;
|
|
2198
|
-
const dur = Math.max(120, durationMs || 0);
|
|
2199
|
-
|
|
2200
|
-
useEffect(()=>{
|
|
2201
|
-
if(!playing) return;
|
|
2202
|
-
const dot = dotRef.current, track = trackRef.current;
|
|
2203
|
-
if(!dot || !track) return;
|
|
2204
|
-
let cancelled = false, anim = null, timer = null, atRight = false;
|
|
2205
|
-
const GAP = 480, DOT = 14;
|
|
2206
|
-
const travel = ()=> Math.max(0, track.clientWidth - DOT - 8);
|
|
2207
|
-
const step = ()=>{
|
|
2208
|
-
if(cancelled) return;
|
|
2209
|
-
const from = atRight ? travel() : 0;
|
|
2210
|
-
const to = atRight ? 0 : travel();
|
|
2211
|
-
try {
|
|
2212
|
-
anim = dot.animate(
|
|
2213
|
-
[{transform:`translateX(${from}px)`},{transform:`translateX(${to}px)`}],
|
|
2214
|
-
{duration:dur, easing:safeEasing, fill:"forwards"});
|
|
2215
|
-
} catch {
|
|
2216
|
-
anim = dot.animate(
|
|
2217
|
-
[{transform:`translateX(${from}px)`},{transform:`translateX(${to}px)`}],
|
|
2218
|
-
{duration:dur, easing:"linear", fill:"forwards"});
|
|
2219
|
-
}
|
|
2220
|
-
anim.onfinish = ()=>{ if(cancelled) return; atRight = !atRight; timer = setTimeout(step, GAP); };
|
|
2221
|
-
};
|
|
2222
|
-
step();
|
|
2223
|
-
return ()=>{ cancelled = true; if(anim){try{anim.cancel();}catch{}} if(timer)clearTimeout(timer); };
|
|
2224
|
-
},[playing, safeEasing, dur]);
|
|
2225
|
-
|
|
2226
|
-
return h("div",{className:"tl-preview"},
|
|
2227
|
-
h("div",{className:"tl-preview-head"},
|
|
2228
|
-
h("span",{className:"tl-preview-title"},"Position Preview"),
|
|
2229
|
-
h("button",{className:"tl-preview-btn",onClick:()=>setPlaying(p=>!p)},
|
|
2230
|
-
h(Ic,{name:playing?"pause":"play",size:11}),
|
|
2231
|
-
h("span",null,playing?"Pause":"Play")),
|
|
2232
|
-
),
|
|
2233
|
-
h("div",{className:"tl-preview-track",ref:trackRef},
|
|
2234
|
-
h("span",{className:"tl-preview-rail"}),
|
|
2235
|
-
h("span",{className:"tl-preview-end left"}),
|
|
2236
|
-
h("span",{className:"tl-preview-end right"}),
|
|
2237
|
-
h("span",{className:"tl-preview-dot",ref:dotRef}),
|
|
2238
|
-
),
|
|
2239
1933
|
);
|
|
2240
1934
|
}
|
|
2241
1935
|
|
|
@@ -2423,11 +2117,9 @@
|
|
|
2423
2117
|
}
|
|
2424
2118
|
|
|
2425
2119
|
function Tracks({et, selLane, setSelLane, onPropChange, snap, scaleMs}){
|
|
2426
|
-
const ROW_H = 48;
|
|
2427
2120
|
const rowsRef = useRef(null);
|
|
2428
2121
|
const tracksRef = useRef(null);
|
|
2429
2122
|
const [order, setOrder] = useState([]);
|
|
2430
|
-
const [dragLane, setDragLane] = useState(null);
|
|
2431
2123
|
// Width of the label column (shared by the ruler spacer + every prop-row
|
|
2432
2124
|
// head). Draggable via the divider; the bars column is flex:1 so it just
|
|
2433
2125
|
// takes the remaining space and the bars stay proportional to scaleMs.
|
|
@@ -2457,27 +2149,6 @@
|
|
|
2457
2149
|
});
|
|
2458
2150
|
},[laneKey]);
|
|
2459
2151
|
const orderedRows = order.map(p=>et.find(x=>x.laneId===p)).filter(Boolean);
|
|
2460
|
-
const startReorder = useCallback((laneId,e)=>{
|
|
2461
|
-
e.preventDefault(); e.stopPropagation();
|
|
2462
|
-
setSelLane(laneId);
|
|
2463
|
-
setDragLane(laneId);
|
|
2464
|
-
const top = rowsRef.current ? rowsRef.current.getBoundingClientRect().top : 0;
|
|
2465
|
-
const onMove = e2=>{
|
|
2466
|
-
const idx = Math.floor((e2.clientY - top) / ROW_H);
|
|
2467
|
-
setOrder(prev=>{
|
|
2468
|
-
const cur = prev.indexOf(laneId);
|
|
2469
|
-
if(cur<0) return prev;
|
|
2470
|
-
const target = Math.max(0, Math.min(prev.length-1, idx));
|
|
2471
|
-
if(target===cur) return prev;
|
|
2472
|
-
const next = prev.slice();
|
|
2473
|
-
next.splice(cur,1);
|
|
2474
|
-
next.splice(target,0,laneId);
|
|
2475
|
-
return next;
|
|
2476
|
-
});
|
|
2477
|
-
};
|
|
2478
|
-
const onUp = ()=>{ setDragLane(null); window.removeEventListener("mousemove",onMove); window.removeEventListener("mouseup",onUp); };
|
|
2479
|
-
window.addEventListener("mousemove",onMove); window.addEventListener("mouseup",onUp);
|
|
2480
|
-
},[setSelLane]);
|
|
2481
2152
|
const majorStep = scaleMs <= 2000 ? 500 : scaleMs <= 10000 ? 1000 : scaleMs <= 25000 ? 5000 : 10000;
|
|
2482
2153
|
const minorStep = majorStep / 4;
|
|
2483
2154
|
const ruler=[];
|
|
@@ -2497,9 +2168,8 @@
|
|
|
2497
2168
|
...orderedRows.map(row=>h(PropTrack,{
|
|
2498
2169
|
key:row.laneId, property:row.property, member:row.member,
|
|
2499
2170
|
delayMs:row.delayMs, durationMs:row.durationMs, lockDuration:!!row.spring,
|
|
2500
|
-
selected:row.laneId===selLane,
|
|
2171
|
+
selected:row.laneId===selLane, snap, scaleMs, labelW,
|
|
2501
2172
|
onSelect:()=>setSelLane(row.laneId),
|
|
2502
|
-
onReorder:e=>startReorder(row.laneId,e),
|
|
2503
2173
|
onDelayChange:ms=>onPropChange(row.laneId,{delayMs:ms}),
|
|
2504
2174
|
onDurationChange:ms=>onPropChange(row.laneId,{durationMs:ms}),
|
|
2505
2175
|
}))),
|
|
@@ -2609,8 +2279,10 @@
|
|
|
2609
2279
|
const et=active.effectiveTimings||[];
|
|
2610
2280
|
const timings=et.map(t=>({property:t.property,durationMs:t.durationMs,delayMs:t.delayMs,easing:t.easing}));
|
|
2611
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.
|
|
2612
2284
|
const{id}=await relayCreateJob({transitionId:active.id,label:active.label,selector,
|
|
2613
|
-
phase:active.phase||null,group:active.groupLabel||null,timings,mode,refineType});
|
|
2285
|
+
phase:active.phase||null,group:active.groupLabel||null,timings,mode,refineType:"both"});
|
|
2614
2286
|
setRefineJobId(id);
|
|
2615
2287
|
}catch(e){
|
|
2616
2288
|
setRefinePhase("error");
|
|
@@ -2618,7 +2290,9 @@
|
|
|
2618
2290
|
}
|
|
2619
2291
|
},[active,refineMode,refineType,refreshHealth]);
|
|
2620
2292
|
const changeRefineMode=useCallback((mode)=>{setRefineMode(mode);setRefinePhase("idle");refreshHealth();},[refreshHealth]);
|
|
2621
|
-
|
|
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);},[]);
|
|
2622
2296
|
// poll the relay while a job is running
|
|
2623
2297
|
useEffect(()=>{
|
|
2624
2298
|
if(!refineJobId||refinePhase!=="scanning")return;
|
|
@@ -2769,47 +2443,81 @@
|
|
|
2769
2443
|
},[active]);
|
|
2770
2444
|
// reset Accept feedback when switching transitions
|
|
2771
2445
|
useEffect(()=>{setAcceptState("idle");setAcceptError(null);},[active&&active.id]);
|
|
2772
|
-
//
|
|
2773
|
-
//
|
|
2774
|
-
//
|
|
2775
|
-
//
|
|
2776
|
-
//
|
|
2777
|
-
//
|
|
2778
|
-
//
|
|
2779
|
-
|
|
2780
|
-
//
|
|
2781
|
-
//
|
|
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).
|
|
2782
2515
|
useEffect(()=>{
|
|
2783
2516
|
if(didGroupScanRef.current)return;
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
if(flat0.length)break;
|
|
2789
|
-
await new Promise(r=>setTimeout(r,300));
|
|
2790
|
-
}
|
|
2791
|
-
if(stopped||didGroupScanRef.current)return;
|
|
2792
|
-
didGroupScanRef.current=true;
|
|
2793
|
-
setGroupScanState("scanning");
|
|
2794
|
-
const flat=registry.getAll().filter(e=>e.kind!=="phase");
|
|
2795
|
-
const raw=flat.map(e=>({label:e.label,selector:e.bindings&&e.bindings.selector,
|
|
2796
|
-
properties:e.properties,
|
|
2797
|
-
timings:(e.baseLanes||[]).map(l=>({property:l.property,durationMs:l.durationMs,delayMs:l.delayMs,easing:l.easing}))}));
|
|
2798
|
-
try{
|
|
2799
|
-
const{id}=await relayCreateJob({kind:"scan",url:location.href,raw});
|
|
2800
|
-
for(let i=0;i<520&&!stopped;i++){
|
|
2801
|
-
await new Promise(r=>setTimeout(r,500));
|
|
2802
|
-
const job=await relayGetJob(id);
|
|
2803
|
-
if(stopped)return;
|
|
2804
|
-
if(job.status==="done"){const groups=(job.result&&job.result.groups)||[];if(groups.length)registry.setGroups(groups);setGroupScanState("done");return;}
|
|
2805
|
-
if(job.status==="error"){setGroupScanState("error");return;}
|
|
2806
|
-
}
|
|
2807
|
-
if(!stopped)setGroupScanState("error");
|
|
2808
|
-
}catch(e){ if(!stopped)setGroupScanState("error"); /* relay down → stay flat */ }
|
|
2809
|
-
};
|
|
2810
|
-
run();
|
|
2811
|
-
return ()=>{stopped=true;};
|
|
2812
|
-
},[registry]);
|
|
2517
|
+
didGroupScanRef.current=true;
|
|
2518
|
+
runGroupScan();
|
|
2519
|
+
return ()=>{scanTokenRef.current++;};
|
|
2520
|
+
},[runGroupScan]);
|
|
2813
2521
|
|
|
2814
2522
|
// whole-component open/close uses the transitions.dev panel reveal:
|
|
2815
2523
|
// keep the panel mounted while it animates, flip data-open on the next
|
|
@@ -2860,7 +2568,7 @@
|
|
|
2860
2568
|
h(Header,{entries,active,onSelect:setActiveId,onReset:resetOverrides,onCopy:copyValues,copied,
|
|
2861
2569
|
snap,setSnap,onMinimize:()=>setMinimized(true),onRefine:openRefine,refineActive:refineOpen,
|
|
2862
2570
|
onAccept,acceptState,acceptDisabled:computeChanges(active).length===0,acceptError,
|
|
2863
|
-
scanning:groupScanState==="scanning"}),
|
|
2571
|
+
scanning:groupScanState==="scanning",onRescan:rescanTransitions}),
|
|
2864
2572
|
active
|
|
2865
2573
|
?h(Body,{entry:active,onPropChange:(prop,o)=>setPropOverride(prop,o),snap})
|
|
2866
2574
|
:h("div",{className:"tl-empty"},"Select a transition to inspect and edit it")),
|
|
@@ -2877,10 +2585,36 @@
|
|
|
2877
2585
|
}
|
|
2878
2586
|
|
|
2879
2587
|
// ── demo boxes ──
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
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
|
+
}
|
|
2884
2618
|
|
|
2885
2619
|
// ── live controls for the whole-panel reveal transition (demo harness) ──
|
|
2886
2620
|
// transitions.dev motion tokens (values lifted verbatim from the skill's _root.css)
|
|
@@ -2991,8 +2725,8 @@
|
|
|
2991
2725
|
const showControls=(()=>{try{return new URLSearchParams(location.search).has("controls");}catch(e){return false;}})();
|
|
2992
2726
|
return h(TimelineCtx.Provider,{value:ctx},
|
|
2993
2727
|
showControls&&h(PanelControls),
|
|
2994
|
-
h("div",{ref:rootRef,className:"demo-root"},h("div",{className:"demo-header"},h("h1",null,"Timeline Inspector \u2014 Demo"),h("p",null,"
|
|
2995
|
-
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))),
|
|
2996
2730
|
h(TimelinePanel));
|
|
2997
2731
|
}
|
|
2998
2732
|
|