transitions-refine 0.3.10 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/refine-live/SKILL.md +46 -16
- package/README.md +40 -13
- package/bin/cli.mjs +167 -58
- package/demo.html +832 -145
- package/package.json +1 -1
- package/server/live-refine-loop.mjs +473 -0
- package/server/motion-tokens.mjs +116 -2
- package/server/relay.mjs +130 -13
package/demo.html
CHANGED
|
@@ -91,15 +91,15 @@
|
|
|
91
91
|
/* @inject-skip-start (demo-only page + sample-box styles — excluded from the injected build) */
|
|
92
92
|
body {
|
|
93
93
|
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
|
94
|
-
background: #
|
|
94
|
+
background: #EEEEEE;
|
|
95
95
|
color: var(--c-text);
|
|
96
96
|
min-height: 100vh;
|
|
97
97
|
-webkit-font-smoothing: antialiased;
|
|
98
98
|
-moz-osx-font-smoothing: grayscale;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
.demo-root { max-width: 900px; margin: 0 auto; padding:
|
|
102
|
-
.demo-header { margin-bottom:
|
|
101
|
+
.demo-root { max-width: 900px; margin: 0 auto; padding: 24px 24px 120px; }
|
|
102
|
+
.demo-header { margin-bottom: 18px; }
|
|
103
103
|
.demo-header h1 { font-size: 26px; font-weight: 700; letter-spacing: -0.5px; color: #171717; }
|
|
104
104
|
.demo-header p { color: var(--c-ruler); font-size: 14px; margin-top: 6px; }
|
|
105
105
|
.demo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 20px; }
|
|
@@ -194,6 +194,130 @@
|
|
|
194
194
|
@media (prefers-reduced-motion: reduce) {
|
|
195
195
|
.t-morph, .t-morph-plus, .t-morph-menu { transition: none !important; }
|
|
196
196
|
}
|
|
197
|
+
|
|
198
|
+
/* transitions.dev motion tokens for the extra prototypes (verbatim from
|
|
199
|
+
05-menu-dropdown.md / 06-modal.md). Demo-only — excluded from the build. */
|
|
200
|
+
:root {
|
|
201
|
+
--dropdown-open-dur: 250ms; --dropdown-close-dur: 150ms;
|
|
202
|
+
--dropdown-pre-scale: 0.97; --dropdown-closing-scale: 0.99;
|
|
203
|
+
--dropdown-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
204
|
+
--modal-open-dur: 250ms; --modal-close-dur: 150ms;
|
|
205
|
+
--modal-scale: 0.96; --modal-scale-close: 0.96;
|
|
206
|
+
--modal-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
207
|
+
--modal-overlay-bg: rgba(15, 17, 21, 0.45);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* ── prototype switcher tabs (sliding pill — 16-tabs-sliding.md) ── */
|
|
211
|
+
.proto-tabs {
|
|
212
|
+
position: relative; display: inline-flex; align-items: center; gap: 3px;
|
|
213
|
+
margin-top: 18px; padding: 3px; border-radius: 48px; background: #f1f1f1;
|
|
214
|
+
}
|
|
215
|
+
.proto-tab {
|
|
216
|
+
position: relative; appearance: none; border: 0; background: transparent;
|
|
217
|
+
height: 30px; padding: 4px 14px; border-radius: 48px; cursor: pointer;
|
|
218
|
+
font: 500 13px/1 "Inter", system-ui, sans-serif; color: rgba(15,15,15,0.6);
|
|
219
|
+
z-index: 1; transition: color var(--dropdown-open-dur) var(--dropdown-ease);
|
|
220
|
+
}
|
|
221
|
+
.proto-tab:not(.is-active):hover { color: rgba(15,15,15,0.85); }
|
|
222
|
+
.proto-tab.is-active { color: #0f0f0f; }
|
|
223
|
+
.proto-tabs-pill {
|
|
224
|
+
position: absolute; top: 3px; left: 0; height: 30px; width: 0;
|
|
225
|
+
background: #fff; border-radius: 48px; transform: translateX(0);
|
|
226
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 0 0 1px rgba(0,0,0,0.04);
|
|
227
|
+
transition:
|
|
228
|
+
transform var(--dropdown-open-dur) var(--dropdown-ease),
|
|
229
|
+
width var(--dropdown-open-dur) var(--dropdown-ease);
|
|
230
|
+
will-change: transform, width; z-index: 0; pointer-events: none;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* ── prototype: Menu dropdown (05-menu-dropdown.md) ── */
|
|
234
|
+
.dd-stage { position: relative; min-height: 224px; display: flex; justify-content: center; padding-top: 4px; }
|
|
235
|
+
.dd { position: relative; display: inline-block; }
|
|
236
|
+
.dd-trigger {
|
|
237
|
+
display: inline-flex; align-items: center; gap: 8px; padding: 9px 14px;
|
|
238
|
+
font: 500 14px/1 "Inter", system-ui, sans-serif; color: #1b1b1b;
|
|
239
|
+
background: #fff; border: 0; border-radius: 10px; cursor: pointer;
|
|
240
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.04), inset 0 0 0 1px rgba(0,0,0,0.06), inset 0 -1px 0 rgba(0,0,0,0.10);
|
|
241
|
+
}
|
|
242
|
+
.dd-trigger:hover { background: #f9f9f9; }
|
|
243
|
+
.dd-caret { transition: transform var(--dropdown-open-dur) var(--dropdown-ease); }
|
|
244
|
+
.dd.is-open .dd-caret { transform: rotate(180deg); }
|
|
245
|
+
/* recipe surface */
|
|
246
|
+
.t-dropdown {
|
|
247
|
+
transform-origin: top left; transform: scale(var(--dropdown-pre-scale));
|
|
248
|
+
opacity: 0; pointer-events: none;
|
|
249
|
+
transition:
|
|
250
|
+
transform var(--dropdown-open-dur) var(--dropdown-ease),
|
|
251
|
+
opacity var(--dropdown-open-dur) var(--dropdown-ease);
|
|
252
|
+
will-change: transform, opacity;
|
|
253
|
+
}
|
|
254
|
+
.t-dropdown.is-open { transform: scale(1); opacity: 1; pointer-events: auto; }
|
|
255
|
+
.t-dropdown.is-closing {
|
|
256
|
+
transform: scale(var(--dropdown-closing-scale)); opacity: 0; pointer-events: none;
|
|
257
|
+
transition:
|
|
258
|
+
transform var(--dropdown-close-dur) var(--dropdown-ease),
|
|
259
|
+
opacity var(--dropdown-close-dur) var(--dropdown-ease);
|
|
260
|
+
}
|
|
261
|
+
.dd-menu {
|
|
262
|
+
position: absolute; top: calc(100% + 8px); left: 0; min-width: 216px; padding: 6px;
|
|
263
|
+
background: #fff; border-radius: 12px;
|
|
264
|
+
box-shadow: 0 0 0 1px rgba(0,0,0,0.06), 0 2px 6px rgba(0,0,0,0.05), 0 4px 42px rgba(0,0,0,0.06);
|
|
265
|
+
z-index: 5;
|
|
266
|
+
}
|
|
267
|
+
.dd-title { padding: 6px 10px 4px; font-size: 11px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: #9b9b9b; }
|
|
268
|
+
.dd-item {
|
|
269
|
+
display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 10px;
|
|
270
|
+
font: 500 13px/1 "Inter", system-ui, sans-serif; color: #2f2f2f;
|
|
271
|
+
background: transparent; border: 0; border-radius: 8px; cursor: pointer; text-align: left;
|
|
272
|
+
}
|
|
273
|
+
.dd-item:hover { background: #f4f4f5; }
|
|
274
|
+
|
|
275
|
+
/* ── prototype: Modal (06-modal.md) ── */
|
|
276
|
+
.modal-stage {
|
|
277
|
+
position: relative; height: 240px; border-radius: 12px; overflow: hidden;
|
|
278
|
+
background: #f6f7f9; box-shadow: inset 0 0 0 1px rgba(0,0,0,0.05);
|
|
279
|
+
display: grid; place-items: center;
|
|
280
|
+
}
|
|
281
|
+
.modal-open-btn {
|
|
282
|
+
position: relative; z-index: 0; padding: 9px 16px;
|
|
283
|
+
font: 500 14px/1 "Inter", system-ui, sans-serif; color: #fff;
|
|
284
|
+
background: #171717; border: 0; border-radius: 10px; cursor: pointer;
|
|
285
|
+
}
|
|
286
|
+
.modal-backdrop {
|
|
287
|
+
position: absolute; inset: 0; z-index: 1; background: var(--modal-overlay-bg);
|
|
288
|
+
opacity: 0; pointer-events: none; cursor: pointer;
|
|
289
|
+
transition: opacity var(--modal-open-dur) var(--modal-ease);
|
|
290
|
+
}
|
|
291
|
+
.modal-backdrop.is-open { opacity: 1; pointer-events: auto; }
|
|
292
|
+
.modal-backdrop.is-closing { opacity: 0; transition: opacity var(--modal-close-dur) var(--modal-ease); }
|
|
293
|
+
.t-modal {
|
|
294
|
+
position: relative; z-index: 2; width: 264px; padding: 20px; background: #fff;
|
|
295
|
+
border-radius: 14px; box-shadow: 0 10px 40px rgba(0,0,0,0.18);
|
|
296
|
+
transform-origin: center; transform: scale(var(--modal-scale)); opacity: 0; pointer-events: none;
|
|
297
|
+
transition:
|
|
298
|
+
transform var(--modal-open-dur) var(--modal-ease),
|
|
299
|
+
opacity var(--modal-open-dur) var(--modal-ease);
|
|
300
|
+
will-change: transform, opacity;
|
|
301
|
+
}
|
|
302
|
+
.t-modal.is-open { transform: scale(1); opacity: 1; pointer-events: auto; }
|
|
303
|
+
.t-modal.is-closing {
|
|
304
|
+
transform: scale(var(--modal-scale-close)); opacity: 0; pointer-events: none;
|
|
305
|
+
transition:
|
|
306
|
+
transform var(--modal-close-dur) var(--modal-ease),
|
|
307
|
+
opacity var(--modal-close-dur) var(--modal-ease);
|
|
308
|
+
}
|
|
309
|
+
.modal-card-title { font-size: 15px; font-weight: 600; color: #171717; }
|
|
310
|
+
.modal-card-body { margin-top: 6px; font-size: 13px; color: var(--c-ruler); line-height: 1.5; }
|
|
311
|
+
.modal-actions { margin-top: 18px; display: flex; justify-content: flex-end; gap: 8px; }
|
|
312
|
+
.modal-btn { padding: 8px 13px; font: 500 13px/1 "Inter", system-ui, sans-serif; border: 0; border-radius: 8px; cursor: pointer; }
|
|
313
|
+
.modal-btn.secondary { background: #f1f1f2; color: #2f2f2f; }
|
|
314
|
+
.modal-btn.secondary:hover { background: #e9e9eb; }
|
|
315
|
+
.modal-btn.primary { background: #e5484d; color: #fff; }
|
|
316
|
+
.modal-btn.primary:hover { background: #d93b40; }
|
|
317
|
+
|
|
318
|
+
@media (prefers-reduced-motion: reduce) {
|
|
319
|
+
.proto-tabs-pill, .proto-tab, .dd-caret, .t-dropdown, .modal-backdrop, .t-modal { transition: none !important; }
|
|
320
|
+
}
|
|
197
321
|
/* @inject-skip-end */
|
|
198
322
|
|
|
199
323
|
/* ───────────────────── timeline panel ───────────────────── */
|
|
@@ -262,10 +386,32 @@
|
|
|
262
386
|
.tl-pill-count.is-single { width: 20px; padding: 0; }
|
|
263
387
|
|
|
264
388
|
/* ── header row ── */
|
|
389
|
+
/* container-type lets the children below respond to the header's own width
|
|
390
|
+
(the panel is full-viewport-width, so this tracks the viewport too). */
|
|
265
391
|
.tl-header { flex: none; position: relative; z-index: 40; display: flex; align-items: center; gap: 8px; padding: 7.5px 14px;
|
|
266
|
-
border-bottom: 1px solid var(--c-line); }
|
|
267
|
-
.tl-header-label { font-size: 13px; font-weight: 500; line-height: 18px; color: var(--c-text-mut2); white-space: nowrap; }
|
|
268
|
-
|
|
392
|
+
border-bottom: 1px solid var(--c-line); container-type: inline-size; }
|
|
393
|
+
.tl-header-label { font-size: 13px; font-weight: 500; line-height: 18px; color: var(--c-text-mut2); white-space: nowrap; flex: none; }
|
|
394
|
+
/* count is the FIRST thing to give up space (high shrink) and clips to … */
|
|
395
|
+
.tl-header-count { margin-left: auto; display: inline-flex; align-items: center; font-size: 13px; line-height: 18px; color: var(--c-count); white-space: nowrap;
|
|
396
|
+
min-width: 0; flex: 0 99 auto; overflow: hidden; }
|
|
397
|
+
.tl-count-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
398
|
+
/* the transition picker truncates its name SECOND (lower shrink than count) */
|
|
399
|
+
.tl-header .tl-ghost-btn { min-width: 0; flex: 0 1 auto; }
|
|
400
|
+
.tl-ghost-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
401
|
+
.tl-header .tl-ghost-btn .tl-dim, .tl-header .tl-ghost-chev { flex: none; }
|
|
402
|
+
/* the right-hand cluster never shrinks or gets pushed off — minimize stays put */
|
|
403
|
+
.tl-header .tl-icon-btn, .tl-header .tl-sec-btn,
|
|
404
|
+
.tl-header .tl-accept-btn, .tl-header .tl-refine-btn,
|
|
405
|
+
.tl-header > .t-tt-wrap { flex: none; }
|
|
406
|
+
/* still too tight → drop the "Selected" label, then collapse Accept + Refine
|
|
407
|
+
to circular icon-only buttons (radius is already 60px → perfect circle). */
|
|
408
|
+
@container (max-width: 540px) {
|
|
409
|
+
.tl-header-label { display: none; }
|
|
410
|
+
}
|
|
411
|
+
@container (max-width: 470px) {
|
|
412
|
+
.tl-accept-btn .tl-btn-label, .tl-refine-btn .tl-btn-label { display: none; }
|
|
413
|
+
.tl-accept-btn, .tl-refine-btn { width: 36px; padding: 0; justify-content: center; gap: 0; }
|
|
414
|
+
}
|
|
269
415
|
|
|
270
416
|
/* ── buttons ── */
|
|
271
417
|
/* topbar buttons share a fully rounded 60px corner radius */
|
|
@@ -1289,6 +1435,41 @@
|
|
|
1289
1435
|
.tl-scan-note { cursor: help; color: var(--c-count);
|
|
1290
1436
|
display: inline-flex; align-items: center; gap: 6px; }
|
|
1291
1437
|
.tl-scan-note .tl-dotm { width: 14px; height: 14px; }
|
|
1438
|
+
.tl-scan-error { color: var(--c-warn, #c2410c); }
|
|
1439
|
+
.tl-scan-error svg { flex: none; }
|
|
1440
|
+
/* /refine live chip — warns the chat-loop mode bills credits while idle */
|
|
1441
|
+
.tl-live-wrap { display: inline-flex; }
|
|
1442
|
+
.tl-live-chip { display: inline-flex; align-items: center; gap: 6px;
|
|
1443
|
+
height: 22px; padding: 0 4px 0 8px; border-radius: 999px;
|
|
1444
|
+
font-size: 11px; font-weight: 600; line-height: 1; cursor: help;
|
|
1445
|
+
color: #b45309; background: rgba(217,119,6,0.12);
|
|
1446
|
+
border: 1px solid rgba(217,119,6,0.28);
|
|
1447
|
+
/* mount: token dropdown-open feel — 250ms Fast, scale 0.97, smooth ease out */
|
|
1448
|
+
animation: tl-live-in 250ms cubic-bezier(0.22,1,0.36,1) both; }
|
|
1449
|
+
html[data-theme="dark"] .tl-live-chip { color: #fbbf24;
|
|
1450
|
+
background: rgba(251,191,36,0.13); border-color: rgba(251,191,36,0.3); }
|
|
1451
|
+
.tl-live-chip.is-stopped { color: var(--c-count); background: rgba(127,127,127,0.1);
|
|
1452
|
+
border-color: rgba(127,127,127,0.26); padding: 0 8px; }
|
|
1453
|
+
.tl-live-chip.is-stopped svg { flex: none; }
|
|
1454
|
+
.tl-live-dot { width: 7px; height: 7px; border-radius: 50%;
|
|
1455
|
+
background: currentColor; flex: none;
|
|
1456
|
+
animation: tl-live-pulse 1600ms linear infinite; }
|
|
1457
|
+
.tl-live-stop { all: unset; cursor: pointer; font: inherit; font-size: 10px;
|
|
1458
|
+
font-weight: 700; letter-spacing: 0.02em; text-transform: uppercase;
|
|
1459
|
+
color: inherit; padding: 3px 7px; border-radius: 999px;
|
|
1460
|
+
background: rgba(217,119,6,0.16);
|
|
1461
|
+
transition: background 150ms cubic-bezier(0.22,1,0.36,1); }
|
|
1462
|
+
html[data-theme="dark"] .tl-live-stop { background: rgba(251,191,36,0.18); }
|
|
1463
|
+
.tl-live-stop:hover { background: rgba(217,119,6,0.32); }
|
|
1464
|
+
html[data-theme="dark"] .tl-live-stop:hover { background: rgba(251,191,36,0.34); }
|
|
1465
|
+
.tl-live-stop:focus-visible { outline: 2px solid currentColor; outline-offset: 1px; }
|
|
1466
|
+
@keyframes tl-live-in { from { opacity: 0; transform: scale(0.97); } to { opacity: 1; transform: scale(1); } }
|
|
1467
|
+
@keyframes tl-live-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
1468
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1469
|
+
.tl-live-chip { animation: none; }
|
|
1470
|
+
.tl-live-dot { animation: none; }
|
|
1471
|
+
.tl-live-stop { transition: none; }
|
|
1472
|
+
}
|
|
1292
1473
|
.tl-scan-shimmer { --shimmer-dur: 2000ms; --shimmer-base: var(--c-count); --shimmer-highlight: #17181c;
|
|
1293
1474
|
--shimmer-band: 400%; --shimmer-ease: linear; }
|
|
1294
1475
|
/* loading status text — transitions.dev shimmer text (15) + text states swap (04) */
|
|
@@ -1345,6 +1526,7 @@
|
|
|
1345
1526
|
.tl-sug-cat.is-easing { color: #a830fd; background: rgba(173,68,247,0.06); box-shadow: inset 0 0 0 1px rgba(173,68,247,0.18); }
|
|
1346
1527
|
.tl-sug-cat.is-duration { color: #3097fd; background: rgba(111,183,255,0.08); box-shadow: inset 0 0 0 1px rgba(48,151,253,0.18); }
|
|
1347
1528
|
.tl-sug-cat.is-scale { color: #26953b; background: rgba(38,149,59,0.06); box-shadow: inset 0 0 0 1px rgba(38,149,59,0.18); }
|
|
1529
|
+
.tl-sug-cat.is-blur { color: #0e9aa7; background: rgba(14,154,167,0.07); box-shadow: inset 0 0 0 1px rgba(14,154,167,0.20); }
|
|
1348
1530
|
.tl-sug-cat.is-distance, .tl-sug-cat.is-delay { color: #c35c00; background: rgba(247,152,68,0.06); box-shadow: inset 0 0 0 1px rgba(205,115,36,0.18); }
|
|
1349
1531
|
.tl-sug-cat.is-replace { color: #3097fd; background: rgba(111,183,255,0.08); box-shadow: inset 0 0 0 1px rgba(48,151,253,0.18); }
|
|
1350
1532
|
/* content stack */
|
|
@@ -1358,6 +1540,12 @@
|
|
|
1358
1540
|
.tl-sug-from { color: #7b7b7b; text-decoration: line-through; }
|
|
1359
1541
|
.tl-sug-to { color: #1b1b1b; }
|
|
1360
1542
|
.tl-sug-reason { font-size: 12px; font-weight: 400; line-height: 18px; color: #6d6d6d; }
|
|
1543
|
+
/* "Updates Open + Close" — a recipe swap writes every related phase together */
|
|
1544
|
+
.tl-sug-phases { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 6px;
|
|
1545
|
+
font-size: 11px; font-weight: 500; line-height: 16px; color: #6d6d6d; }
|
|
1546
|
+
.tl-sug-phase { display: inline-flex; align-items: baseline; gap: 4px;
|
|
1547
|
+
padding: 1px 7px; border-radius: 20px; background: rgba(15,15,15,0.05); color: #4a4a4a; }
|
|
1548
|
+
.tl-sug-phase-dur { font-family: "Roboto Mono", ui-monospace, SFMono-Regular, Menlo, monospace; color: #1b1b1b; }
|
|
1361
1549
|
/* shared white pill button (Figma 580:757) — card Apply + bottom-bar actions */
|
|
1362
1550
|
.tl-pill-btn { align-self: flex-start; box-sizing: border-box; display: inline-flex; align-items: center;
|
|
1363
1551
|
justify-content: center; gap: 8px; height: 32px; padding: 6px 12px; border: none; cursor: pointer;
|
|
@@ -1654,7 +1842,10 @@
|
|
|
1654
1842
|
return out;
|
|
1655
1843
|
}
|
|
1656
1844
|
_applyOverrides(baseLanes,po){
|
|
1657
|
-
return baseLanes.map(t=>{const o=po[t.laneId];return {...t,durationMs:o
|
|
1845
|
+
return baseLanes.map(t=>{const o=po[t.laneId]||{};return {...t,durationMs:o.durationMs??t.durationMs,delayMs:o.delayMs??t.delayMs,easing:o.easing??t.easing,spring:o.spring??t.spring,
|
|
1846
|
+
// value overrides (scale/blur) come only from applied refine suggestions;
|
|
1847
|
+
// they carry their own from-value + backing var so Accept + live preview work.
|
|
1848
|
+
scale:o.scale??t.scale,blur:o.blur??t.blur,varName:o.varName??t.varName,scaleFrom:o.scaleFrom??t.scaleFrom,blurFrom:o.blurFrom??t.blurFrom};});
|
|
1658
1849
|
}
|
|
1659
1850
|
_notify(){
|
|
1660
1851
|
this.effectiveCache.clear();
|
|
@@ -1787,6 +1978,56 @@
|
|
|
1787
1978
|
}catch{return [];}
|
|
1788
1979
|
}
|
|
1789
1980
|
|
|
1981
|
+
// Read the non-resting ("pre") VALUE a lane animates from the live stylesheets:
|
|
1982
|
+
// transform-scale (≠1) and filter-blur (≠0), plus the CSS custom property
|
|
1983
|
+
// backing it (var-backed values are the ones we can live-preview). Returns
|
|
1984
|
+
// {scale?|blur?, varName?}. Only meaningful for transform/filter lanes.
|
|
1985
|
+
function readElValueHint(el, property){
|
|
1986
|
+
const out={};
|
|
1987
|
+
try{
|
|
1988
|
+
if(!el||(property!=="transform"&&property!=="filter"))return out;
|
|
1989
|
+
const own=(typeof el.className==="string"?el.className.trim().split(/\s+/):Array.from(el.classList||[])).filter(Boolean);
|
|
1990
|
+
const cs=getComputedStyle(el);
|
|
1991
|
+
const esc=s=>s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");
|
|
1992
|
+
const tokRes=own.map(c=>new RegExp("\\."+esc(c)+"(?![\\w-])"));
|
|
1993
|
+
const matchTok=sel=>!own.length||tokRes.some(re=>re.test(sel));
|
|
1994
|
+
let bestScale=null,bestBlur=null;
|
|
1995
|
+
const consider=txt=>{
|
|
1996
|
+
if(property==="transform"){
|
|
1997
|
+
const tm=txt.match(/transform\s*:\s*([^;}]+)/i);
|
|
1998
|
+
if(tm){const sc=tm[1].match(/scale\(\s*([^),]+)/i);
|
|
1999
|
+
if(sc){const inner=sc[1].trim();const vm=inner.match(/var\(\s*(--[\w-]+)/);
|
|
2000
|
+
const num=vm?parseFloat(cs.getPropertyValue(vm[1])):parseFloat(inner);
|
|
2001
|
+
// pick the value furthest from the resting 1 (the real pre-scale).
|
|
2002
|
+
if(Number.isFinite(num)&&Math.abs(num-1)>1e-4&&(!bestScale||Math.abs(num-1)>Math.abs(bestScale.scale-1)))
|
|
2003
|
+
bestScale={scale:num,...(vm?{varName:vm[1]}:{})};}}
|
|
2004
|
+
}else{
|
|
2005
|
+
const fm=txt.match(/filter\s*:\s*([^;}]+)/i);
|
|
2006
|
+
if(fm){const bl=fm[1].match(/blur\(\s*([^)]+)\)/i);
|
|
2007
|
+
if(bl){const inner=bl[1].trim();const vm=inner.match(/var\(\s*(--[\w-]+)/);
|
|
2008
|
+
const num=vm?parseFloat(cs.getPropertyValue(vm[1])):parseFloat(inner);
|
|
2009
|
+
if(Number.isFinite(num)&&num>1e-4&&(!bestBlur||num>bestBlur.blur))
|
|
2010
|
+
bestBlur={blur:num,...(vm?{varName:vm[1]}:{})};}}
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
const collect=list=>{for(const rule of Array.from(list||[])){
|
|
2014
|
+
if(rule.cssRules&&rule.selectorText===undefined){collect(rule.cssRules);continue;}
|
|
2015
|
+
const sel=rule.selectorText;if(!sel||!matchTok(sel))continue;
|
|
2016
|
+
if(rule.cssText)consider(rule.cssText);}};
|
|
2017
|
+
for(const sheet of Array.from(document.styleSheets||[])){let r;try{r=sheet.cssRules;}catch{continue;}collect(r);}
|
|
2018
|
+
if(bestScale)Object.assign(out,bestScale);
|
|
2019
|
+
if(bestBlur)Object.assign(out,bestBlur);
|
|
2020
|
+
}catch{}
|
|
2021
|
+
return out;
|
|
2022
|
+
}
|
|
2023
|
+
// Same, resolving the element from a CSS selector (used when only a lane's
|
|
2024
|
+
// selector is known, e.g. a grouped phase member).
|
|
2025
|
+
function laneValueHint(selector, property){
|
|
2026
|
+
if(!selector||(property!=="transform"&&property!=="filter"))return {};
|
|
2027
|
+
let el=null;try{el=document.querySelector(selector);}catch{return {};}
|
|
2028
|
+
return el?readElValueHint(el,property):{};
|
|
2029
|
+
}
|
|
2030
|
+
|
|
1790
2031
|
// ── hooks ──
|
|
1791
2032
|
const TimelineCtx = createContext(null);
|
|
1792
2033
|
function useReg(){const{registry}=useContext(TimelineCtx);return useSyncExternalStore(useCallback(cb=>registry.subscribe(cb),[registry]),useCallback(()=>registry.getAll(),[registry]),useCallback(()=>registry.getAll(),[registry]));}
|
|
@@ -2019,38 +2260,96 @@
|
|
|
2019
2260
|
];
|
|
2020
2261
|
return [{ id:"menu-dropdown", label:"Menu dropdown", component:null, phases }];
|
|
2021
2262
|
}
|
|
2022
|
-
//
|
|
2023
|
-
//
|
|
2024
|
-
//
|
|
2025
|
-
//
|
|
2263
|
+
// ── deterministic motion-token engine (mirrors server/motion-tokens.mjs) ──
|
|
2264
|
+
// The website demo answers Refine locally, so it must reach the SAME verdict
|
|
2265
|
+
// the shipped agent would: flag ONLY values that are genuinely off the
|
|
2266
|
+
// transitions.dev tokens (by usage where the skill says so). For the demo
|
|
2267
|
+
// dropdown that means the lone real issue — the 0.8 pre-scale — is the
|
|
2268
|
+
// suggestion, instead of a pile of tweaks for values that are already on-grid.
|
|
2269
|
+
const _TX_DUR_TOKENS = [{v:150,name:"Fast",usage:"quick state changes"},{v:250,name:"Medium",usage:"standard UI motion"},{v:400,name:"Slow",usage:"larger surfaces"}];
|
|
2270
|
+
const _TX_SCALE_TOKENS = [{v:0.96,name:"Large",usage:"modal open / close"},{v:0.97,name:"Medium",usage:"dropdown open"},{v:0.98,name:"Small",usage:"tooltip open"},{v:0.99,name:"Tiny",usage:"dropdown close"}];
|
|
2271
|
+
const _TX_BLUR_TOKENS = [{v:2,name:"Small",usage:"panel reveal, icon swap, text swap"},{v:3,name:"Medium",usage:"page slide, text reveal"},{v:8,name:"Large",usage:"success check open"}];
|
|
2272
|
+
const _TX_TOKEN_EASE = new Set(["cubic-bezier(0.22,1,0.36,1)","ease-in-out","ease-out","linear","cubic-bezier(0.34,1.36,0.64,1)","cubic-bezier(0.34,3.85,0.64,1)"]);
|
|
2273
|
+
const _txLc = s=>String(s||"").toLowerCase();
|
|
2274
|
+
const _txNormEase = s=>String(s||"").replace(/\s+/g,"").toLowerCase();
|
|
2275
|
+
const _txNearest = (arr,val)=>{let best=arr[0],bd=Infinity;for(const t of arr){const d=Math.abs(t.v-val);if(d<bd){bd=d;best=t;}}return{token:best,delta:bd};};
|
|
2276
|
+
const _txScaleUsage = (hint)=>{const h=_txLc(hint);if(!h)return null;const close=/clos/.test(h);
|
|
2277
|
+
if(/\bmodal\b|\bdialog\b|\blightbox\b/.test(h))return _TX_SCALE_TOKENS.find(t=>t.v===0.96);
|
|
2278
|
+
if(/\btooltip\b|\btip\b|\bpopover\b/.test(h))return _TX_SCALE_TOKENS.find(t=>t.v===0.98);
|
|
2279
|
+
if(/\bdropdown\b|drop-down|\bmenu\b|\bselect\b|\bcombobox\b|\bcaret\b/.test(h))return _TX_SCALE_TOKENS.find(t=>t.v===(close?0.99:0.97));
|
|
2280
|
+
return null;};
|
|
2281
|
+
const _txBlurUsage = (hint)=>{const h=_txLc(hint);if(!h)return null;
|
|
2282
|
+
if(/success|check/.test(h))return _TX_BLUR_TOKENS.find(t=>t.v===8);
|
|
2283
|
+
if(/\bpage\b|\bslide\b/.test(h))return _TX_BLUR_TOKENS.find(t=>t.v===3);
|
|
2284
|
+
if(/text/.test(h)&&/reveal/.test(h))return _TX_BLUR_TOKENS.find(t=>t.v===3);
|
|
2285
|
+
if(/panel|icon|swap|skeleton|number|digit|reveal/.test(h))return _TX_BLUR_TOKENS.find(t=>t.v===2);
|
|
2286
|
+
return null;};
|
|
2287
|
+
const _txRefineEasing = (e)=>{const n=_txNormEase(e);if(!n)return false;if(_TX_TOKEN_EASE.has(n))return false;return n==="ease"||n==="ease-in"||n.startsWith("cubic-bezier")||n.startsWith("linear(");};
|
|
2288
|
+
// Token-faithful tweaks for one lane list (mirrors refineTimings()).
|
|
2289
|
+
function _txTokenTweaks(timings, ctx){
|
|
2290
|
+
const out=[]; const c=ctx||{};
|
|
2291
|
+
for(const t of (timings||[])){
|
|
2292
|
+
const prop=t.property||"all";
|
|
2293
|
+
const member=t.member||null;
|
|
2294
|
+
const hint=[c.label,c.selector,c.phase,t.property,t.member].filter(Boolean).join(" ");
|
|
2295
|
+
const mp=(rest)=> member ? {member, ...rest} : rest;
|
|
2296
|
+
if(Number.isFinite(t.durationMs)){const {token,delta}=_txNearest(_TX_DUR_TOKENS,t.durationMs);
|
|
2297
|
+
if(delta>10)out.push({id:"mock-"+prop+"-duration",kind:"duration",property:prop,member,
|
|
2298
|
+
title:"Duration → "+token.name,from:t.durationMs+"ms",to:token.v+"ms",
|
|
2299
|
+
patch:mp({property:prop,durationMs:token.v}),
|
|
2300
|
+
reason:token.name+" ("+token.v+"ms) is the closest motion token — used for "+token.usage+". "+t.durationMs+"ms is off-grid."});}
|
|
2301
|
+
if(_txRefineEasing(t.easing))out.push({id:"mock-"+prop+"-easing",kind:"easing",property:prop,member,
|
|
2302
|
+
title:"Easing → Smooth ease out",from:t.easing,to:_TX_EASE,
|
|
2303
|
+
patch:mp({property:prop,easing:_TX_EASE}),
|
|
2304
|
+
reason:'"'+t.easing+'" is a generic curve. The transitions.dev standard ease-out reads more intentional on opens, closes, slides, and resizes.'});
|
|
2305
|
+
if(Number.isFinite(t.scale)&&Math.abs(t.scale-1)>1e-4){const usage=_txScaleUsage(hint);const {token:near,delta}=_txNearest(_TX_SCALE_TOKENS,t.scale);const token=usage||near;
|
|
2306
|
+
if(token&&(usage||delta>5e-3)&&Math.abs(token.v-t.scale)>1e-4)out.push({id:"mock-"+prop+"-scale",kind:"scale",property:prop,member,
|
|
2307
|
+
title:"Scale → "+token.name,from:String(t.scale),to:String(token.v),
|
|
2308
|
+
patch:mp({property:prop,scale:token.v,...(t.varName?{varName:t.varName}:{})}),
|
|
2309
|
+
reason:token.name+" ("+token.v+") is the transitions.dev scale token for "+token.usage+". "+t.scale+" is "+(usage?"off the usage token":"off-grid")+"."});}
|
|
2310
|
+
if(Number.isFinite(t.blur)&&t.blur>1e-4){const usage=_txBlurUsage(hint);const {token:near,delta}=_txNearest(_TX_BLUR_TOKENS,t.blur);const token=usage||near;
|
|
2311
|
+
if(token&&(usage||delta>0.5)&&Math.abs(token.v-t.blur)>1e-4)out.push({id:"mock-"+prop+"-blur",kind:"blur",property:prop,member,
|
|
2312
|
+
title:"Blur → "+token.name,from:t.blur+"px",to:token.v+"px",
|
|
2313
|
+
patch:mp({property:prop,blur:token.v,...(t.varName?{varName:t.varName}:{})}),
|
|
2314
|
+
reason:token.name+" ("+token.v+"px) is the transitions.dev blur token for "+token.usage+". "+t.blur+"px is "+(usage?"off the usage token":"off-grid")+"."});}
|
|
2315
|
+
}
|
|
2316
|
+
return out;
|
|
2317
|
+
}
|
|
2318
|
+
// refine → token-faithful suggestions (same verdict the shipped agent reaches)
|
|
2319
|
+
// plus the best-fit recipe for the Replace tab. patches derive property/member
|
|
2320
|
+
// from request.timings so Apply changes a real lane. Shape mirrors relay.mjs's
|
|
2321
|
+
// suggestion contract (id/kind/property/title/from/to/patch/reason + reference).
|
|
2026
2322
|
function _txMockSuggestions(request){
|
|
2027
2323
|
const t = (request && request.timings) || [];
|
|
2324
|
+
const ctx = { label:(request&&request.label)||null, selector:(request&&request.selector)||null, phase:(request&&request.phase)||null };
|
|
2028
2325
|
const p0 = (t[0]&&t[0].property) || "opacity";
|
|
2029
2326
|
const m0 = (t[0]&&t[0].member) || null;
|
|
2030
|
-
const p1 = (t[1]&&t[1].property) || p0;
|
|
2031
|
-
const m1 = (t[1]&&t[1].member) || m0;
|
|
2032
|
-
const from0 = _txMs(t[0]&&t[0].durationMs) || "—";
|
|
2033
|
-
const fromE = (t[0]&&t[0].easing) || "ease";
|
|
2034
2327
|
const mp = (member, rest)=> member ? {member, ...rest} : rest;
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2328
|
+
// motion-token tweaks: ONLY off-token values, exactly like the real engine.
|
|
2329
|
+
const tweaks = _txTokenTweaks(t, ctx);
|
|
2330
|
+
// related phases (open + close) → a recipe swap updates them together: one
|
|
2331
|
+
// `patches` entry per phase, open slower than close (250ms / 150ms tokens).
|
|
2332
|
+
const reqPhases = (request && Array.isArray(request.phases)) ? request.phases.filter(p=>p&&p.phase) : [];
|
|
2333
|
+
const replacePatches = reqPhases.length>1
|
|
2334
|
+
? reqPhases.map(p=>({phase:p.phase, property:"all", durationMs:/close/i.test(p.phase)?150:250, easing:_TX_EASE, scale:/close/i.test(p.phase)?0.99:0.97}))
|
|
2335
|
+
: null;
|
|
2336
|
+
const replace = { id:"mock-replace-menu", kind:"replace", property:p0, member:m0,
|
|
2337
|
+
title:"Menu dropdown", reference:"05-menu-dropdown.md",
|
|
2338
|
+
patch:mp(m0,{property:"all", durationMs:250, easing:_TX_EASE, scale:0.97}),
|
|
2339
|
+
...(replacePatches?{patches:replacePatches}:{}),
|
|
2340
|
+
reason:"Best-fit transitions.dev recipe (#05 Menu dropdown): scale 0.97→1 + fade, 250ms open / 150ms close on cubic-bezier(0.22, 1, 0.36, 1)." };
|
|
2341
|
+
const suggestions=[...tweaks, replace];
|
|
2342
|
+
// Scope to the requested refineType so per-tab caches merge cleanly.
|
|
2343
|
+
const rt=(request&&request.refineType)||"both";
|
|
2344
|
+
const scoped=rt==="replace"?suggestions.filter(s=>s.kind==="replace")
|
|
2345
|
+
:rt==="small"?suggestions.filter(s=>s.kind!=="replace")
|
|
2346
|
+
:suggestions;
|
|
2347
|
+
const nTweak=tweaks.length;
|
|
2348
|
+
const tweakWord=nTweak+" motion-token tweak"+(nTweak===1?"":"s");
|
|
2349
|
+
const summary=rt==="replace"?"1 transitions.dev recipe match for the Menu dropdown."
|
|
2350
|
+
:rt==="small"?(nTweak?tweakWord+" for the Menu dropdown.":"The Menu dropdown is already on-grid — no motion-token tweaks needed.")
|
|
2351
|
+
:tweakWord+" and 1 transitions.dev recipe match for the Menu dropdown.";
|
|
2352
|
+
return { suggestions:scoped, summary };
|
|
2054
2353
|
}
|
|
2055
2354
|
// statusLog so the panel's shimmer status copy has something to show.
|
|
2056
2355
|
function _txMockLog(request, createdAt){
|
|
@@ -2064,7 +2363,9 @@
|
|
|
2064
2363
|
async function relayCreateJob(request){
|
|
2065
2364
|
if(TX_MOCK){
|
|
2066
2365
|
const id=_txMockId();
|
|
2067
|
-
|
|
2366
|
+
// Website demo: random 4–6s “agent work” so the scanning loader feels real.
|
|
2367
|
+
const delayMs=4000+Math.floor(Math.random()*2001);
|
|
2368
|
+
_txMockJobs.set(id,{request,createdAt:Date.now(),delayMs});
|
|
2068
2369
|
return { id, status:"pending" };
|
|
2069
2370
|
}
|
|
2070
2371
|
const r = await fetch(RELAY_URL+"/jobs",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({request})});
|
|
@@ -2077,8 +2378,8 @@
|
|
|
2077
2378
|
if(!rec) return { id, status:"error", statusLog:[], result:null, error:"mock: unknown job" };
|
|
2078
2379
|
const elapsed=Date.now()-rec.createdAt;
|
|
2079
2380
|
const log=_txMockLog(rec.request, rec.createdAt);
|
|
2080
|
-
|
|
2081
|
-
if(elapsed<
|
|
2381
|
+
const wait=rec.delayMs!=null?rec.delayMs:800;
|
|
2382
|
+
if(elapsed<wait) return { id, status:"running", statusLog:[log[0]], result:null, error:null };
|
|
2082
2383
|
return { id, status:"done", statusLog:log, result:_txMockResult(rec.request), error:null };
|
|
2083
2384
|
}
|
|
2084
2385
|
const r = await fetch(RELAY_URL+"/jobs/"+id);
|
|
@@ -2086,10 +2387,18 @@
|
|
|
2086
2387
|
return r.json();
|
|
2087
2388
|
}
|
|
2088
2389
|
async function relayHealth(){
|
|
2089
|
-
if(TX_MOCK) return { ok:true, auto:true, llmAvailable:true, cliInstalled:true, jobs:0 };
|
|
2390
|
+
if(TX_MOCK) return { ok:true, auto:true, llmAvailable:true, cliInstalled:true, agentCmd:true, pollerActive:false, jobs:0 };
|
|
2090
2391
|
const r = await fetch(RELAY_URL+"/health");
|
|
2091
2392
|
if(!r.ok) throw new Error("relay /health failed ("+r.status+")");
|
|
2092
|
-
return r.json(); // { ok, auto, llmAvailable, jobs }
|
|
2393
|
+
return r.json(); // { ok, auto, llmAvailable, pollerActive, agentCmd, cliInstalled, jobs }
|
|
2394
|
+
}
|
|
2395
|
+
// Ask the relay to stop the in-chat `/refine live` loop (the credit-burning
|
|
2396
|
+
// poller). No-op for a wired REFINE_AGENT_CMD (it never polls /jobs/next).
|
|
2397
|
+
async function relayStopLive(){
|
|
2398
|
+
if(TX_MOCK) return { ok:true };
|
|
2399
|
+
const r = await fetch(RELAY_URL+"/poller/stop",{method:"POST"});
|
|
2400
|
+
if(!r.ok) throw new Error("relay POST /poller/stop failed ("+r.status+")");
|
|
2401
|
+
return r.json();
|
|
2093
2402
|
}
|
|
2094
2403
|
|
|
2095
2404
|
const REFINE_MODES=[
|
|
@@ -2136,7 +2445,20 @@
|
|
|
2136
2445
|
const matches=lanes.filter(t=>t.property===s.property&&t.member);
|
|
2137
2446
|
if(!matches.length) return null;
|
|
2138
2447
|
const names=[...new Set(matches.map(t=>t.member))];
|
|
2139
|
-
|
|
2448
|
+
if(names.length===1) return names[0];
|
|
2449
|
+
// Ambiguous: several lanes share this property (e.g. a caret that rotates
|
|
2450
|
+
// AND a panel that scales both animate `transform`). For a value-bearing
|
|
2451
|
+
// kind, attribute to the lane that actually carries that value rather than
|
|
2452
|
+
// blindly taking the first — read the live non-resting value per lane.
|
|
2453
|
+
if(s.kind==="scale"||s.kind==="blur"){
|
|
2454
|
+
const key=s.kind;
|
|
2455
|
+
const owner=matches.find(t=>{
|
|
2456
|
+
const v=laneValueHint(t.selector,t.property);
|
|
2457
|
+
return key==="scale"?(v.scale!=null&&Math.abs(v.scale-1)>1e-4):(v.blur!=null&&v.blur>1e-4);
|
|
2458
|
+
});
|
|
2459
|
+
if(owner) return owner.member;
|
|
2460
|
+
}
|
|
2461
|
+
return matches[0].member;
|
|
2140
2462
|
}
|
|
2141
2463
|
// Apply button — "Apply" ⟷ "✓ Applied". On apply, a LEADING gray check is
|
|
2142
2464
|
// revealed first and pushes the text to the right: .tl-apply-check tweens its
|
|
@@ -2222,7 +2544,7 @@
|
|
|
2222
2544
|
const delay=(i)=>({transitionDelay:"calc(var(--stagger-stagger) * "+i+")"});
|
|
2223
2545
|
// category chip label + color modifier, keyed by the suggestion kind
|
|
2224
2546
|
const CAT={easing:["Easing","is-easing"],duration:["Duration","is-duration"],
|
|
2225
|
-
delay:["Delay","is-delay"],scale:["Scale","is-scale"],distance:["Distance","is-distance"],
|
|
2547
|
+
delay:["Delay","is-delay"],scale:["Scale","is-scale"],blur:["Blur","is-blur"],distance:["Distance","is-distance"],
|
|
2226
2548
|
replace:["Transition replacement","is-replace"]};
|
|
2227
2549
|
const cap=(p)=>p&&p!=="all"?p.charAt(0).toUpperCase()+p.slice(1):"";
|
|
2228
2550
|
let i=0;
|
|
@@ -2247,8 +2569,14 @@
|
|
|
2247
2569
|
h("div",{className:"tl-sug-body"},
|
|
2248
2570
|
h("div",{className:"tl-sug-text"},
|
|
2249
2571
|
isReplace
|
|
2250
|
-
? h(
|
|
2251
|
-
|
|
2572
|
+
? h(React.Fragment,null,
|
|
2573
|
+
h("div",{className:"tl-sug-title"},
|
|
2574
|
+
"Current transition",h("br"),"\u2192 "+(s.title||"transition")+" from transitions.dev"),
|
|
2575
|
+
Array.isArray(s.patches)&&s.patches.length>1&&h("div",{className:"tl-sug-phases"},
|
|
2576
|
+
"Updates ",
|
|
2577
|
+
s.patches.map((pp,pi)=>h("span",{className:"tl-sug-phase",key:pi},
|
|
2578
|
+
(cap(pp.phase)||pp.phase||"Phase"),
|
|
2579
|
+
pp.durationMs!=null&&h("span",{className:"tl-sug-phase-dur"},pp.durationMs+"ms")))))
|
|
2252
2580
|
: h(React.Fragment,null,
|
|
2253
2581
|
s.title&&h("div",{className:"tl-sug-title"},s.title),
|
|
2254
2582
|
(s.from||s.to)&&h("div",{className:"tl-sug-delta"},
|
|
@@ -2258,6 +2586,20 @@
|
|
|
2258
2586
|
applyBtn));
|
|
2259
2587
|
}));
|
|
2260
2588
|
}
|
|
2589
|
+
// Shared "Connect with your agent" copy — used by the full-panel gate and by
|
|
2590
|
+
// the Refine area when no live agent is connected, so both read identically.
|
|
2591
|
+
// `cliMissing` swaps the body to an install-first hint.
|
|
2592
|
+
function AgentConnectBody({cliMissing}){
|
|
2593
|
+
return cliMissing
|
|
2594
|
+
? h("p",{className:"tl-gate-text"},"Install an agent CLI, then run ",
|
|
2595
|
+
h("code",{className:"tl-code"},"npx transitions-refine live"),
|
|
2596
|
+
" (recommended) or ",h("code",{className:"tl-code"},"/refine live")," in your editor.")
|
|
2597
|
+
: h("p",{className:"tl-gate-text"},
|
|
2598
|
+
"Run ",h("code",{className:"tl-code"},"npx transitions-refine live"),
|
|
2599
|
+
" in your project to start the relay with a persistent agent — works for hours, no chat loop. Or run ",
|
|
2600
|
+
h("code",{className:"tl-code"},"/refine live"),
|
|
2601
|
+
" in your editor if the relay is already up without an agent wired.");
|
|
2602
|
+
}
|
|
2261
2603
|
function RefinePanel({open,onClose,phase,label,refineType,onType,suggestions,summary,error,appliedIds,onApply,onApplyAll,mode,onMode,llmAvailable,cliInstalled,onStart,lanes}){
|
|
2262
2604
|
// mount-on-open; keep mounted through the panel-reveal slide-out, then unmount
|
|
2263
2605
|
const[render,setRender]=useState(open);
|
|
@@ -2363,12 +2705,12 @@
|
|
|
2363
2705
|
}
|
|
2364
2706
|
// idle
|
|
2365
2707
|
if(!agentReady){
|
|
2366
|
-
|
|
2708
|
+
// Same "Connect with your agent" copy as the full-panel gate, so opening
|
|
2709
|
+
// Refine after a post-scan auto-stop reads identically. Re-running
|
|
2710
|
+
// /refine live makes the agent available and Start scanning works again.
|
|
2367
2711
|
return h("div",{className:"tl-refine-center"},
|
|
2368
|
-
h("div",{className:"tl-refine-unavail-title"},"
|
|
2369
|
-
cliMissing
|
|
2370
|
-
? h("p",{className:"tl-refine-unavail-text"},"Install Cursor CLI by ",h("code",{className:"tl-code"},"/refine live")," command in your agent to enable live functionality.")
|
|
2371
|
-
: h("p",{className:"tl-refine-unavail-text"},"Run the ",h("code",{className:"tl-code"},"/refine live")," command in your agent to enable live functionality."));
|
|
2712
|
+
h("div",{className:"tl-refine-unavail-title"},"Connect with your agent"),
|
|
2713
|
+
h(AgentConnectBody,{cliMissing:cliInstalled===false}));
|
|
2372
2714
|
}
|
|
2373
2715
|
const idleDescR = agentMode ? (REFINE_TYPES.find(t=>t.key===rt)||REFINE_TYPES[0]).desc : (rt==="replace"
|
|
2374
2716
|
? "Deterministic mode can't pick a recipe to replace the transition — switch to Agent mode for replacements."
|
|
@@ -2436,23 +2778,44 @@
|
|
|
2436
2778
|
// originally-scanned values — the set of changes Accept writes to source.
|
|
2437
2779
|
// Each change carries member/selector/phase context so the agent can target
|
|
2438
2780
|
// the right state rule (e.g. .is-open vs .is-closing) for a grouped phase.
|
|
2439
|
-
function computeChanges(active){
|
|
2781
|
+
function computeChanges(active,allEntries){
|
|
2440
2782
|
if(!active)return[];
|
|
2441
|
-
|
|
2442
|
-
|
|
2783
|
+
// Write the active phase plus — for a grouped phase — its sibling phases
|
|
2784
|
+
// (open/close) so a recipe swap applied across both is saved to both. Only
|
|
2785
|
+
// phases that actually have edits contribute changes, so a single-phase tweak
|
|
2786
|
+
// still writes just that phase. Each change carries its own `phase`.
|
|
2787
|
+
let targets=[active];
|
|
2788
|
+
if(active.kind==="phase"&&active.groupId&&Array.isArray(allEntries)){
|
|
2789
|
+
const sibs=allEntries.filter(e=>e.kind==="phase"&&e.groupId===active.groupId&&e.id!==active.id);
|
|
2790
|
+
targets=targets.concat(sibs);
|
|
2791
|
+
}
|
|
2443
2792
|
const out=[];
|
|
2444
|
-
for(const
|
|
2445
|
-
const
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2793
|
+
for(const tgt of targets){
|
|
2794
|
+
const base=tgt.baseLanes||[];
|
|
2795
|
+
const bmap=new Map(base.map(t=>[t.laneId,t]));
|
|
2796
|
+
for(const t of (tgt.effectiveTimings||[])){
|
|
2797
|
+
const b=bmap.get(t.laneId);if(!b)continue;
|
|
2798
|
+
if(t.durationMs!==b.durationMs||t.delayMs!==b.delayMs||(t.easing||"")!==(b.easing||"")){
|
|
2799
|
+
out.push({property:t.property,member:t.member||null,selector:t.selector||null,phase:tgt.phase||null,
|
|
2800
|
+
from:{durationMs:b.durationMs,delayMs:b.delayMs,easing:b.easing},
|
|
2801
|
+
to:{durationMs:t.durationMs,delayMs:t.delayMs,easing:t.easing}});
|
|
2802
|
+
}
|
|
2803
|
+
// value edits (scale/blur) live as overrides on the effective lane and
|
|
2804
|
+
// carry their own from-value + backing var; emit one change each.
|
|
2805
|
+
if(t.scale!=null){
|
|
2806
|
+
out.push({property:t.property,member:t.member||null,selector:t.selector||null,phase:tgt.phase||null,varName:t.varName||null,
|
|
2807
|
+
from:{scale:t.scaleFrom},to:{scale:t.scale}});
|
|
2808
|
+
}
|
|
2809
|
+
if(t.blur!=null){
|
|
2810
|
+
out.push({property:t.property,member:t.member||null,selector:t.selector||null,phase:tgt.phase||null,varName:t.varName||null,
|
|
2811
|
+
from:{blur:t.blurFrom},to:{blur:t.blur}});
|
|
2812
|
+
}
|
|
2450
2813
|
}
|
|
2451
2814
|
}
|
|
2452
2815
|
return out;
|
|
2453
2816
|
}
|
|
2454
2817
|
|
|
2455
|
-
function Header({entries,active,onSelect,onReset,onCopy,copied,snap,setSnap,onMinimize,onRefine,refineActive,onAccept,acceptState,acceptDisabled,acceptError,scanning,onRescan,pick,setPick}){
|
|
2818
|
+
function Header({entries,active,onSelect,onReset,onCopy,copied,snap,setSnap,onMinimize,onRefine,refineActive,onAccept,acceptState,acceptDisabled,acceptError,scanning,scanError,onRescan,pick,setPick,chatLoop,liveStopped,onStopLive}){
|
|
2456
2819
|
const[setg,setSetg]=useState(false);
|
|
2457
2820
|
// settings dropdown sub-page (main menu ↔ keyboard-shortcuts cheat sheet)
|
|
2458
2821
|
const[settingsView,setSettingsView]=useState("main"); // "main" | "shortcuts"
|
|
@@ -2505,7 +2868,7 @@
|
|
|
2505
2868
|
return h("div",{className:"tl-header"},
|
|
2506
2869
|
h("span",{className:"tl-header-label"},"Selected"),
|
|
2507
2870
|
h("button",{ref:pickRef,className:cx("tl-ghost-btn",pick&&"is-active"),disabled:!active,onClick:()=>setPick(v=>!v)},
|
|
2508
|
-
active?h(React.Fragment,null,h("span",
|
|
2871
|
+
active?h(React.Fragment,null,h("span",{className:"tl-ghost-label"},active.label),h("span",{className:"tl-dim"}," "+active.durationMs+"ms"))
|
|
2509
2872
|
:h("span",{className:"tl-dim"},"None"),
|
|
2510
2873
|
h("span",{className:"tl-ghost-chev"},h(Ic,{name:"chevron"}))),
|
|
2511
2874
|
h(Dropdown,{open:pick,onClose:()=>setPick(false),triggerRef:pickRef,width:Math.max(240,(pickRef.current&&pickRef.current.offsetWidth)||240),align:"left"},
|
|
@@ -2521,7 +2884,6 @@
|
|
|
2521
2884
|
...fGroups.map(g=>h(React.Fragment,{key:g.id},
|
|
2522
2885
|
h("div",{className:"tl-menu-section"},g.label),
|
|
2523
2886
|
...g.phases.map(phaseItem))),
|
|
2524
|
-
fFlat.length>0&&fGroups.length>0&&h("div",{className:"tl-menu-section"},"Ungrouped"),
|
|
2525
2887
|
...fFlat.map(flatItem))),
|
|
2526
2888
|
h("span",{className:"tl-header-count"},
|
|
2527
2889
|
scanning
|
|
@@ -2531,7 +2893,33 @@
|
|
|
2531
2893
|
h("span",{className:"t-shimmer tl-scan-shimmer","data-text":"Agent scanning transitions…"},"Agent scanning transitions…")),
|
|
2532
2894
|
h("span",{className:"t-tt tl-tt-below tl-tt-msg",role:"tooltip"},
|
|
2533
2895
|
"Agent is working on understanding your transitions and creating grouping and naming. This usually takes a few seconds to a minute."))
|
|
2534
|
-
:
|
|
2896
|
+
: scanError
|
|
2897
|
+
? h("span",{className:"t-tt-wrap"},
|
|
2898
|
+
h("span",{className:"tl-scan-note tl-scan-error",tabIndex:0},
|
|
2899
|
+
h(Ic,{name:"help",size:13}),
|
|
2900
|
+
h("span",null,"Couldn’t group — agent offline")),
|
|
2901
|
+
h("span",{className:"t-tt tl-tt-below tl-tt-msg",role:"tooltip"},
|
|
2902
|
+
/refine live|REFINE_AGENT_CMD/i.test(scanError)
|
|
2903
|
+
? scanError
|
|
2904
|
+
: scanError+" — grouping needs the agent. Run `npx transitions-refine live` (or `/refine live` in your editor)."))
|
|
2905
|
+
: h("span",{className:"tl-count-text"},entries.length+" transition"+(entries.length===1?"":"s")+" found")),
|
|
2906
|
+
chatLoop
|
|
2907
|
+
? h("span",{className:"t-tt-wrap tl-live-wrap"},
|
|
2908
|
+
h("span",{className:"tl-live-chip",tabIndex:0},
|
|
2909
|
+
h("span",{className:"tl-live-dot"}),
|
|
2910
|
+
h("span",null,"Live"),
|
|
2911
|
+
h("button",{className:"tl-live-stop",title:"Stop /refine live",
|
|
2912
|
+
onClick:(e)=>{e.stopPropagation();onStopLive&&onStopLive();}},"Stop")),
|
|
2913
|
+
h("span",{className:"t-tt tl-tt-below tl-tt-msg",role:"tooltip"},
|
|
2914
|
+
"`/refine live` is polling in your editor chat — it uses agent credits while idle. Stop it when you’re done, or it auto-stops after 10 min idle."))
|
|
2915
|
+
: liveStopped
|
|
2916
|
+
? h("span",{className:"t-tt-wrap tl-live-wrap"},
|
|
2917
|
+
h("span",{className:"tl-live-chip is-stopped",tabIndex:0},
|
|
2918
|
+
h(Ic,{name:"check",size:12}),
|
|
2919
|
+
h("span",null,"Live stopped")),
|
|
2920
|
+
h("span",{className:"t-tt tl-tt-below tl-tt-msg",role:"tooltip"},
|
|
2921
|
+
"The `/refine live` loop was asked to stop and will exit on its next poll. Run `/refine live` again to resume."))
|
|
2922
|
+
: null,
|
|
2535
2923
|
h("button",{ref:gearRef,className:cx("tl-icon-btn",setg&&"is-active"),title:"Settings",onClick:()=>setSetg(v=>!v)},h(Ic,{name:"dotsv"})),
|
|
2536
2924
|
h(Dropdown,{open:setg,onClose:()=>{setSetg(false);setSettingsView("main");},triggerRef:gearRef,width:240,align:"right"},
|
|
2537
2925
|
h(SettingsSlide,{view:settingsView,
|
|
@@ -2591,14 +2979,14 @@
|
|
|
2591
2979
|
acceptState==="saving"
|
|
2592
2980
|
? h("span",{className:"tl-accept-spin","aria-hidden":"true"})
|
|
2593
2981
|
: h(Ic,{name:"accept"}),
|
|
2594
|
-
h("span",
|
|
2982
|
+
h("span",{className:"tl-btn-label"},acceptState==="done"?"Done":"Accept")),
|
|
2595
2983
|
h("span",{className:cx("t-tt","tl-tt-below",acceptState==="error"&&acceptError&&"tl-tt-msg"),role:"tooltip"},
|
|
2596
2984
|
acceptState==="error"&&acceptError?acceptError
|
|
2597
2985
|
:acceptState==="done"?"Saved to your code"
|
|
2598
2986
|
:acceptDisabled?"No changes to save"
|
|
2599
2987
|
:h(React.Fragment,null,"Save changes to your codebase",ttHint("A")))),
|
|
2600
2988
|
h("span",{className:"t-tt-wrap"},
|
|
2601
|
-
h("button",{className:cx("tl-refine-btn t-tt-trigger",refineActive&&"is-active"),disabled:!active,onClick:onRefine},
|
|
2989
|
+
h("button",{className:cx("tl-refine-btn t-tt-trigger",refineActive&&"is-active"),disabled:!active,onClick:onRefine,"aria-label":"Refine"},
|
|
2602
2990
|
h(Ic,{name:"wand"}),
|
|
2603
2991
|
h("span",{className:"tl-refine-sparks","aria-hidden":"true"},
|
|
2604
2992
|
h("i",{style:{"--ox":"-1px","--oy":"-3px","--sx":"0px","--sy":"-11px","--sd":"900ms","--sdelay":"0ms"}}),
|
|
@@ -2611,7 +2999,7 @@
|
|
|
2611
2999
|
h("i",{style:{"--ox":"2px","--oy":"5px","--sx":"9px","--sy":"9px","--sd":"1000ms","--sdelay":"760ms"}}),
|
|
2612
3000
|
h("i",{style:{"--ox":"-5px","--oy":"-2px","--sx":"-9px","--sy":"-9px","--sd":"930ms","--sdelay":"180ms"}}),
|
|
2613
3001
|
h("i",{style:{"--ox":"3px","--oy":"1px","--sx":"12px","--sy":"1px","--sd":"980ms","--sdelay":"300ms"}})),
|
|
2614
|
-
h("span",
|
|
3002
|
+
h("span",{className:"tl-btn-label"},"Refine")),
|
|
2615
3003
|
h("span",{className:"t-tt tl-tt-below",role:"tooltip"},"Refine",ttHint("R"))),
|
|
2616
3004
|
h("span",{className:"t-tt-wrap"},
|
|
2617
3005
|
h("button",{className:"tl-icon-btn ghost t-tt-trigger","aria-label":"Minimize",onClick:onMinimize},h(Ic,{name:"minimize"})),
|
|
@@ -3282,18 +3670,85 @@
|
|
|
3282
3670
|
const[acceptError,setAcceptError]=useState(null);
|
|
3283
3671
|
// ── grouped scan (agent reads source → Open/Close phases) ──
|
|
3284
3672
|
const[groupScanState,setGroupScanState]=useState("idle"); // idle | scanning | done | error
|
|
3673
|
+
// Whether the one-time grouping scan has produced groups. Once true the main
|
|
3674
|
+
// panel stays usable even after a chat-loop agent disconnects (so the panel
|
|
3675
|
+
// doesn't snap back to the gate). Seeded synchronously from a cached grouping
|
|
3676
|
+
// or a host-baked seed so a returning user never flashes the gate. (Key is
|
|
3677
|
+
// inlined to match GROUP_STORE_KEY, which is defined further down.)
|
|
3678
|
+
const[scanned,setScanned]=useState(()=>{
|
|
3679
|
+
try{
|
|
3680
|
+
if(typeof window!=="undefined"&&Array.isArray(window.__TX_SEED_GROUPS)&&window.__TX_SEED_GROUPS.length)return true;
|
|
3681
|
+
const c=JSON.parse(localStorage.getItem("tlGroups:"+location.pathname)||"null");
|
|
3682
|
+
return !!(c&&Array.isArray(c.groups)&&c.groups.length);
|
|
3683
|
+
}catch{return false;}
|
|
3684
|
+
});
|
|
3685
|
+
// last scan failure message (relay surfaces "needs the agent. Run /refine
|
|
3686
|
+
// live …") so a failed Rescan can explain WHY nothing re-grouped instead of
|
|
3687
|
+
// silently showing the same flat count.
|
|
3688
|
+
const[groupScanErr,setGroupScanErr]=useState(null);
|
|
3285
3689
|
const didGroupScanRef=useRef(false);
|
|
3690
|
+
// One failed agent spawn (e.g. cursor-agent's cli-config race on cold start)
|
|
3691
|
+
// shouldn't leave the panel stuck flat until a manual rescan. Allow a couple
|
|
3692
|
+
// of automatic re-tries with backoff before giving up; reset on success and
|
|
3693
|
+
// on manual rescan so each fresh scan gets its own retry budget.
|
|
3694
|
+
const groupScanRetryRef=useRef(0);
|
|
3695
|
+
const GROUP_SCAN_MAX_RETRIES=2;
|
|
3286
3696
|
const[refineLabel,setRefineLabel]=useState(null);
|
|
3287
3697
|
const[appliedIds,setAppliedIds]=useState({});
|
|
3288
3698
|
const[refineMode,setRefineMode]=useState("llm"); // llm (Agent) | deterministic
|
|
3289
3699
|
const[refineType,setRefineType]=useState("small"); // small | replace
|
|
3290
3700
|
const[llmAvailable,setLlmAvailable]=useState(null); // null=unknown, true/false from /health
|
|
3291
3701
|
const[cliInstalled,setCliInstalled]=useState(null); // null=unknown, true/false from /health
|
|
3702
|
+
// chatLoop = LLM served by the in-chat `/refine live` poll loop (pollerActive
|
|
3703
|
+
// and no wired REFINE_AGENT_CMD). This is the mode that bills agent turns
|
|
3704
|
+
// while idle, so the header shows a credit warning + Stop for it.
|
|
3705
|
+
const[chatLoop,setChatLoop]=useState(false);
|
|
3706
|
+
const[liveStopped,setLiveStopped]=useState(false); // user hit Stop → show paused chip
|
|
3707
|
+
// After a Stop, the loop stays pollerActive for its ~120s TTL while it
|
|
3708
|
+
// winds down, so we can't just re-read pollerActive. stopPendingRef tracks
|
|
3709
|
+
// "a stop was requested"; sawPollerGoneRef flips once the poller actually
|
|
3710
|
+
// disappears, so only a poller that returns *after that* counts as a fresh
|
|
3711
|
+
// `/refine live` and flips the chip back to Live. (Refs because
|
|
3712
|
+
// refreshHealth is a []-deps callback and would otherwise read stale state.)
|
|
3713
|
+
const stopPendingRef=useRef(false);
|
|
3714
|
+
const sawPollerGoneRef=useRef(false);
|
|
3715
|
+
// A quiet stop (the after-scan auto-stop) drains the loop without showing the
|
|
3716
|
+
// "Live stopped" chip — it's a normal connect-for-scan, not a user Stop.
|
|
3717
|
+
const quietStopRef=useRef(false);
|
|
3718
|
+
// Latest chatLoop value for callbacks (runGroupScan, autoStopAfterScan) that
|
|
3719
|
+
// are []-deps and would otherwise read a stale value.
|
|
3720
|
+
const chatLoopRef=useRef(false);
|
|
3292
3721
|
// probe the relay so the idle panel can show the right availability copy
|
|
3293
3722
|
const refreshHealth=useCallback(async()=>{
|
|
3294
3723
|
try{const j=await relayHealth();setLlmAvailable(!!j.llmAvailable);
|
|
3295
|
-
setCliInstalled(j.cliInstalled==null?null:!!j.cliInstalled);
|
|
3296
|
-
|
|
3724
|
+
setCliInstalled(j.cliInstalled==null?null:!!j.cliInstalled);
|
|
3725
|
+
const loop=!!j.pollerActive&&!j.agentCmd;
|
|
3726
|
+
if(!stopPendingRef.current){chatLoopRef.current=loop;setChatLoop(loop);setLiveStopped(false);return j;}
|
|
3727
|
+
if(!j.pollerActive)sawPollerGoneRef.current=true;
|
|
3728
|
+
if(loop&&sawPollerGoneRef.current){ // a fresh loop started after the stop
|
|
3729
|
+
stopPendingRef.current=false;quietStopRef.current=false;
|
|
3730
|
+
chatLoopRef.current=true;setChatLoop(true);setLiveStopped(false);
|
|
3731
|
+
}else{chatLoopRef.current=false;setChatLoop(false);setLiveStopped(!quietStopRef.current);} // draining
|
|
3732
|
+
return j;}
|
|
3733
|
+
catch{chatLoopRef.current=false;setLlmAvailable(false);setCliInstalled(false);setChatLoop(false);return null;}
|
|
3734
|
+
},[]);
|
|
3735
|
+
const stopLive=useCallback(async()=>{
|
|
3736
|
+
quietStopRef.current=false;
|
|
3737
|
+
stopPendingRef.current=true;sawPollerGoneRef.current=false;
|
|
3738
|
+
chatLoopRef.current=false;setLiveStopped(true);setChatLoop(false);
|
|
3739
|
+
try{await relayStopLive();}catch{}
|
|
3740
|
+
refreshHealth();
|
|
3741
|
+
},[refreshHealth]);
|
|
3742
|
+
// Quiet auto-stop after the one-time grouping scan: in chat-loop mode the
|
|
3743
|
+
// loop has done its only needed job (the scan), so stop it to avoid idle
|
|
3744
|
+
// credit burn. Drains like a normal stop but shows no chip; the Refine area
|
|
3745
|
+
// then shows "Connect with your agent" until the user re-runs /refine live.
|
|
3746
|
+
const autoStopAfterScan=useCallback(async()=>{
|
|
3747
|
+
if(!chatLoopRef.current)return; // only the in-chat loop burns idle credits
|
|
3748
|
+
quietStopRef.current=true;
|
|
3749
|
+
stopPendingRef.current=true;sawPollerGoneRef.current=false;
|
|
3750
|
+
chatLoopRef.current=false;setChatLoop(false);setLiveStopped(false);
|
|
3751
|
+
try{await relayStopLive();}catch{}
|
|
3297
3752
|
},[]);
|
|
3298
3753
|
// a live agent = the relay reports an agent it can drive (REFINE_AGENT_CMD
|
|
3299
3754
|
// wired OR a `/refine live` loop polling). Until then the panel is gated.
|
|
@@ -3363,30 +3818,45 @@
|
|
|
3363
3818
|
refineCacheRef.current.delete(active.id+"::"+type);
|
|
3364
3819
|
try{
|
|
3365
3820
|
const et=active.effectiveTimings||[];
|
|
3366
|
-
|
|
3821
|
+
// attach the lane's non-resting scale/blur value (+ backing var) so the
|
|
3822
|
+
// agent + deterministic engine can refine transform/filter VALUES too.
|
|
3823
|
+
const withVal=(t)=>{const v=laneValueHint(t.selector,t.property);return {property:t.property,member:t.member||null,durationMs:t.durationMs,delayMs:t.delayMs,easing:t.easing,
|
|
3824
|
+
...(v.scale!=null?{scale:v.scale}:{}),...(v.blur!=null?{blur:v.blur}:{}),...(v.varName?{varName:v.varName}:{})};};
|
|
3825
|
+
const timings=et.map(withVal);
|
|
3367
3826
|
const selector=(active.bindings&&active.bindings.selector)||(et[0]&&et[0].selector)||null;
|
|
3827
|
+
// A recipe swap (replace/both) on a grouped phase should update its related
|
|
3828
|
+
// phases (open + close) together — they're one motion. Send the whole
|
|
3829
|
+
// group's phases so the agent can return a per-phase `patches` array.
|
|
3830
|
+
let phases;
|
|
3831
|
+
if((type==="replace"||type==="both")&&active.kind==="phase"&&active.groupId){
|
|
3832
|
+
const gp=entries.filter(e=>e.kind==="phase"&&e.groupId===active.groupId);
|
|
3833
|
+
if(gp.length>1)phases=gp.map(e=>({phase:e.phase||e.phaseLabel||"phase",label:e.phaseLabel||e.phase||"Phase",
|
|
3834
|
+
timings:(e.effectiveTimings||[]).map(withVal)}));
|
|
3835
|
+
}
|
|
3368
3836
|
const{id}=await relayCreateJob({transitionId:active.id,label:active.label,selector,
|
|
3369
|
-
phase:active.phase||null,group:active.groupLabel||null,timings,mode,refineType:type});
|
|
3837
|
+
phase:active.phase||null,group:active.groupLabel||null,timings,mode,refineType:type,phases});
|
|
3370
3838
|
setRefineJobId(id);
|
|
3371
3839
|
}catch(e){
|
|
3372
3840
|
setRefinePhase("error");
|
|
3373
3841
|
setRefineError(h(React.Fragment,null,"Couldn't reach the refine relay. Start it with ",h("code",{className:"tl-code"},"npx transitions-refine live"),"."));
|
|
3374
3842
|
}
|
|
3375
|
-
},[active,refineMode,refineType,llmAvailable]);
|
|
3843
|
+
},[active,refineMode,refineType,llmAvailable,entries]);
|
|
3376
3844
|
const changeRefineMode=useCallback((mode)=>{setRefineMode(mode);setRefinePhase("idle");refineCacheRef.current.clear();refreshHealth();},[refreshHealth]);
|
|
3377
|
-
// Switching tabs reuses a cached scan when present; otherwise (llm only)
|
|
3378
|
-
//
|
|
3379
|
-
//
|
|
3380
|
-
// its tab switch is a pure filter. Before any scan
|
|
3381
|
-
// preview copy and never auto-scan.
|
|
3845
|
+
// Switching tabs reuses a cached scan when present; otherwise (llm only) the
|
|
3846
|
+
// tab drops back to its idle state and waits for an explicit Start-scanning
|
|
3847
|
+
// click — switching never auto-runs a scan. Deterministic feeds both tabs
|
|
3848
|
+
// from one "both" pass, so its tab switch is a pure filter. Before any scan
|
|
3849
|
+
// (idle/error), tabs just preview copy and never auto-scan.
|
|
3382
3850
|
const changeRefineType=useCallback((t)=>{
|
|
3383
3851
|
setRefineType(t);
|
|
3384
3852
|
if(!active)return;
|
|
3385
3853
|
if(scanModeRef.current==="deterministic")return;
|
|
3386
3854
|
if(refinePhase!=="done"&&refinePhase!=="scanning")return;
|
|
3387
3855
|
if(refineCacheRef.current.has(active.id+"::"+t)){applyMerged(active.id);setRefinePhase("done");return;}
|
|
3388
|
-
|
|
3389
|
-
|
|
3856
|
+
// No cached result for this tab → show its idle scan button and wait for
|
|
3857
|
+
// the user. (A scan in progress is left running so it can finish.)
|
|
3858
|
+
if(refinePhase==="done")setRefinePhase("idle");
|
|
3859
|
+
},[active,refinePhase,applyMerged]);
|
|
3390
3860
|
// poll the relay while a job is running
|
|
3391
3861
|
useEffect(()=>{
|
|
3392
3862
|
if(!refineJobId||refinePhase!=="scanning")return;
|
|
@@ -3420,20 +3890,43 @@
|
|
|
3420
3890
|
return()=>{live=false;if(to)clearTimeout(to);};
|
|
3421
3891
|
},[refineJobId,refinePhase]);
|
|
3422
3892
|
const applySuggestion=useCallback((s)=>{
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
const
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3893
|
+
// A replace suggestion may carry `patches` (one per related phase, e.g.
|
|
3894
|
+
// open + close) — a recipe swap is one motion, so applying it writes BOTH.
|
|
3895
|
+
// Otherwise fall back to the single `patch` on the active phase.
|
|
3896
|
+
const patches=Array.isArray(s.patches)&&s.patches.length
|
|
3897
|
+
? s.patches
|
|
3898
|
+
: (s.patch&&s.patch.property?[{...s.patch,phase:(active&&active.phase)||null}]:[]);
|
|
3899
|
+
if(!patches.length)return;
|
|
3900
|
+
const groupId=active&&active.groupId;
|
|
3901
|
+
for(const p of patches){
|
|
3902
|
+
if(!p.property)continue;
|
|
3903
|
+
const o={};
|
|
3904
|
+
if(p.durationMs!=null)o.durationMs=p.durationMs;
|
|
3905
|
+
if(p.delayMs!=null)o.delayMs=p.delayMs;
|
|
3906
|
+
if(p.easing!=null)o.easing=p.easing;
|
|
3907
|
+
// value edits: keep the new value, the backing var, and the from-value
|
|
3908
|
+
// (parsed off the suggestion) so computeChanges can write source on Accept.
|
|
3909
|
+
if(p.scale!=null){o.scale=p.scale;o.scaleFrom=(s.from!=null?parseFloat(s.from):undefined);}
|
|
3910
|
+
if(p.blur!=null){o.blur=p.blur;o.blurFrom=(s.from!=null?parseFloat(s.from):undefined);}
|
|
3911
|
+
if(p.varName!=null)o.varName=p.varName;
|
|
3912
|
+
// resolve the phase entry this patch targets (its sibling open/close
|
|
3913
|
+
// lives in the same group); default to the active entry.
|
|
3914
|
+
let target=active;
|
|
3915
|
+
if(p.phase&&groupId){
|
|
3916
|
+
const found=entries.find(e=>e.kind==="phase"&&e.groupId===groupId&&e.phase===p.phase);
|
|
3917
|
+
if(found)target=found;
|
|
3918
|
+
}
|
|
3919
|
+
if(!target)continue;
|
|
3920
|
+
// map the property patch onto the matching lane(s) — a property can span
|
|
3921
|
+
// several members in a grouped phase, so apply to each.
|
|
3922
|
+
const et=target.effectiveTimings||[];
|
|
3923
|
+
let lanes=p.property==="all"?et:et.filter(t=>t.property===p.property);
|
|
3924
|
+
if(p.member)lanes=lanes.filter(t=>(t.member===p.member||t.memberId===p.member));
|
|
3925
|
+
const ids=lanes.length?lanes.map(t=>t.laneId):[p.property];
|
|
3926
|
+
for(const id of ids)registry.setPropOverride(target.id,id,o);
|
|
3927
|
+
}
|
|
3435
3928
|
setAppliedIds(prev=>({...prev,[s.id]:true}));
|
|
3436
|
-
},[
|
|
3929
|
+
},[registry,active,entries]);
|
|
3437
3930
|
const applyAllSuggestions=useCallback(()=>{
|
|
3438
3931
|
for(const s of refineSuggestions){if(!appliedIds[s.id])applySuggestion(s);}
|
|
3439
3932
|
},[refineSuggestions,appliedIds,applySuggestion]);
|
|
@@ -3449,8 +3942,13 @@
|
|
|
3449
3942
|
useEffect(()=>{
|
|
3450
3943
|
if(typeof document==="undefined")return;
|
|
3451
3944
|
const applied=liveAppliedRef.current;
|
|
3452
|
-
const desired=new Map(); // el → css
|
|
3453
|
-
|
|
3945
|
+
const desired=new Map(); // el → {css, vars:{name:value}}
|
|
3946
|
+
// collect the var-backed scale/blur overrides on a set of lanes into a
|
|
3947
|
+
// {varName: cssValue} map (scale → unitless, blur → px) for live preview.
|
|
3948
|
+
const lanesToVars=(lanes)=>{const v={};for(const l of (lanes||[])){if(!l.varName)continue;
|
|
3949
|
+
if(l.scale!=null)v[l.varName]=String(l.scale);
|
|
3950
|
+
if(l.blur!=null)v[l.varName]=l.blur+"px";}return v;};
|
|
3951
|
+
const add=(els,css,vars)=>{ for(const el of els){ if(!el||(el.closest&&el.closest("[data-timeline-panel],[data-tl-ui]")))continue; desired.set(el,{css,vars:vars||{}}); } };
|
|
3454
3952
|
for(const item of entries){
|
|
3455
3953
|
const ov=registry.getPropOverrides(item.id);
|
|
3456
3954
|
if(!ov||!Object.keys(ov).length)continue; // only edited transitions go live
|
|
@@ -3458,31 +3956,40 @@
|
|
|
3458
3956
|
for(const m of (item.members||[])){
|
|
3459
3957
|
if(!m.selector||!m.lanes||!m.lanes.length)continue;
|
|
3460
3958
|
let els=[];try{els=Array.from(document.querySelectorAll(m.selector));}catch{}
|
|
3461
|
-
add(els,transitionLanesToCss(m.lanes));
|
|
3959
|
+
add(els,transitionLanesToCss(m.lanes),lanesToVars(m.lanes));
|
|
3462
3960
|
}
|
|
3463
3961
|
}else{
|
|
3464
3962
|
const lanes=item.effectiveTimings||[];
|
|
3465
3963
|
if(!lanes.length)continue;
|
|
3466
3964
|
const els=((item.bindings&&item.bindings.elements)||[]).map(w=>w.deref&&w.deref()).filter(Boolean);
|
|
3467
|
-
add(els,transitionLanesToCss(lanes));
|
|
3965
|
+
add(els,transitionLanesToCss(lanes),lanesToVars(lanes));
|
|
3468
3966
|
}
|
|
3469
3967
|
}
|
|
3968
|
+
const varsKey=v=>Object.keys(v).sort().map(k=>k+":"+v[k]).join(";");
|
|
3470
3969
|
// apply new / changed
|
|
3471
|
-
for(const [el,
|
|
3970
|
+
for(const [el,d] of desired){
|
|
3971
|
+
const css=d.css,vars=d.vars||{},vk=varsKey(vars);
|
|
3472
3972
|
const cur=applied.get(el);
|
|
3473
|
-
if(cur&&cur.css===css)continue;
|
|
3973
|
+
if(cur&&cur.css===css&&cur.vk===vk)continue;
|
|
3474
3974
|
if(!cur){
|
|
3475
3975
|
const s=getComputedStyle(el);
|
|
3476
3976
|
const z=zipTransitionLists((s.transitionProperty||"").split(","),(s.transitionDuration||"0s").split(","),(s.transitionDelay||"0s").split(","),splitCssValues(s.transitionTimingFunction||"ease"));
|
|
3477
3977
|
_TX_LIVE_BASE.set(el,{z});
|
|
3478
|
-
applied.set(el,{prevInline:el.style.transition||"",css});
|
|
3479
|
-
}else
|
|
3978
|
+
applied.set(el,{prevInline:el.style.transition||"",css,vars,vk,prevVars:Object.fromEntries(Object.keys(vars).map(n=>[n,el.style.getPropertyValue(n)]))});
|
|
3979
|
+
}else{
|
|
3980
|
+
// revert vars the new desired set no longer drives before re-applying
|
|
3981
|
+
for(const n of Object.keys(cur.vars||{})){if(!(n in vars)){try{const pv=(cur.prevVars||{})[n];if(pv)el.style.setProperty(n,pv);else el.style.removeProperty(n);}catch{}}}
|
|
3982
|
+
const prevVars={...(cur.prevVars||{})};for(const n of Object.keys(vars)){if(!(n in prevVars))prevVars[n]=el.style.getPropertyValue(n);}
|
|
3983
|
+
applied.set(el,{prevInline:cur.prevInline,css,vars,vk,prevVars});
|
|
3984
|
+
}
|
|
3480
3985
|
try{el.style.transition=css;}catch{}
|
|
3986
|
+
for(const n of Object.keys(vars)){try{el.style.setProperty(n,vars[n]);}catch{}}
|
|
3481
3987
|
}
|
|
3482
3988
|
// revert elements that no longer have edits
|
|
3483
3989
|
for(const [el,rec] of Array.from(applied)){
|
|
3484
3990
|
if(desired.has(el))continue;
|
|
3485
3991
|
try{el.style.transition=rec.prevInline;}catch{}
|
|
3992
|
+
for(const n of Object.keys(rec.vars||{})){try{const pv=(rec.prevVars||{})[n];if(pv)el.style.setProperty(n,pv);else el.style.removeProperty(n);}catch{}}
|
|
3486
3993
|
_TX_LIVE_BASE.delete(el);
|
|
3487
3994
|
applied.delete(el);
|
|
3488
3995
|
}
|
|
@@ -3490,7 +3997,7 @@
|
|
|
3490
3997
|
// restore everything on unmount
|
|
3491
3998
|
useEffect(()=>()=>{
|
|
3492
3999
|
const applied=liveAppliedRef.current;
|
|
3493
|
-
for(const [el,rec] of applied){ try{el.style.transition=rec.prevInline;}catch{} _TX_LIVE_BASE.delete(el); }
|
|
4000
|
+
for(const [el,rec] of applied){ try{el.style.transition=rec.prevInline;}catch{} for(const n of Object.keys(rec.vars||{})){try{const pv=(rec.prevVars||{})[n];if(pv)el.style.setProperty(n,pv);else el.style.removeProperty(n);}catch{}} _TX_LIVE_BASE.delete(el); }
|
|
3494
4001
|
applied.clear();
|
|
3495
4002
|
},[]);
|
|
3496
4003
|
const startResize=useCallback(e=>{
|
|
@@ -3542,11 +4049,20 @@
|
|
|
3542
4049
|
}else legacyCopy();
|
|
3543
4050
|
}catch(e){legacyCopy();}
|
|
3544
4051
|
},[active,showToast]);
|
|
3545
|
-
|
|
4052
|
+
// Reset the selected transition. For a grouped phase, clear ALL of its
|
|
4053
|
+
// related phases (open/close) too — a recipe swap edits them as one motion,
|
|
4054
|
+
// so Reset should revert them as one.
|
|
4055
|
+
const resetOverrides=useCallback(()=>{
|
|
4056
|
+
if(!active)return;
|
|
4057
|
+
registry.clearOverride(active.id);
|
|
4058
|
+
if(active.kind==="phase"&&active.groupId){
|
|
4059
|
+
for(const e of entries)if(e.kind==="phase"&&e.groupId===active.groupId&&e.id!==active.id)registry.clearOverride(e.id);
|
|
4060
|
+
}
|
|
4061
|
+
},[registry,active,entries]);
|
|
3546
4062
|
// Accept → send an "apply" job so the agent writes the edited timings into
|
|
3547
4063
|
// the user's source, then reflect saving / done / error on the button.
|
|
3548
4064
|
const onAccept=useCallback(async()=>{
|
|
3549
|
-
const changes=computeChanges(active);
|
|
4065
|
+
const changes=computeChanges(active,entries);
|
|
3550
4066
|
if(!active||!changes.length)return;
|
|
3551
4067
|
setAcceptState("saving");setAcceptError(null);
|
|
3552
4068
|
try{
|
|
@@ -3571,7 +4087,7 @@
|
|
|
3571
4087
|
setAcceptState("error");
|
|
3572
4088
|
setAcceptError("Couldn't reach the relay. Run: npx transitions-refine live");
|
|
3573
4089
|
}
|
|
3574
|
-
},[active]);
|
|
4090
|
+
},[active,entries]);
|
|
3575
4091
|
// reset Accept feedback when switching transitions
|
|
3576
4092
|
useEffect(()=>{setAcceptState("idle");setAcceptError(null);},[active&&active.id]);
|
|
3577
4093
|
// while the agent is applying changes, surface the sticky loader toast
|
|
@@ -3601,8 +4117,15 @@
|
|
|
3601
4117
|
},[GROUP_STORE_KEY]);
|
|
3602
4118
|
// monotonic token so an unmount or a newer rescan supersedes an in-flight poll
|
|
3603
4119
|
const scanTokenRef=useRef(0);
|
|
3604
|
-
const runGroupScan=useCallback(async()=>{
|
|
4120
|
+
const runGroupScan=useCallback(async(opts)=>{
|
|
4121
|
+
opts=opts||{};
|
|
3605
4122
|
const token=++scanTokenRef.current;
|
|
4123
|
+
// Hardcoded grouping (window.__TX_SEED_GROUPS): apply INSTANTLY on first load
|
|
4124
|
+
// — no relay, no agent, no settle loop. Manual "Rescan transitions" passes
|
|
4125
|
+
// skipSeed so the user gets a real agent scan instead of re-applying the bake-in.
|
|
4126
|
+
const seedNow=!opts.skipSeed&&typeof window!=="undefined"&&Array.isArray(window.__TX_SEED_GROUPS)&&window.__TX_SEED_GROUPS.length?window.__TX_SEED_GROUPS:null;
|
|
4127
|
+
if(seedNow){registry.setGroups(seedNow);groupScanRetryRef.current=0;setScanned(true);setGroupScanState("done");if(!opts.skipSeed)autoStopAfterScan();return;}
|
|
4128
|
+
setGroupScanErr(null);
|
|
3606
4129
|
setGroupScanState("scanning");
|
|
3607
4130
|
// wait for the flat DOM scan to settle (count stable twice) so the agent
|
|
3608
4131
|
// sees every transition, not a partial set discovered progressively.
|
|
@@ -3620,17 +4143,12 @@
|
|
|
3620
4143
|
if(scanTokenRef.current!==token)return;
|
|
3621
4144
|
const flat=registry.getAll().filter(e=>e.kind!=="phase");
|
|
3622
4145
|
if(!flat.length){setGroupScanState("idle");return;}
|
|
3623
|
-
// Hardcoded grouping (window.__TX_SEED_GROUPS): a host/demo page can ship a
|
|
3624
|
-
// pre-computed agent grouping so the panel groups instantly with NO relay
|
|
3625
|
-
// or agent round-trip. demo.html uses this to bake in the agent's scan.
|
|
3626
|
-
const seed=typeof window!=="undefined"&&Array.isArray(window.__TX_SEED_GROUPS)?window.__TX_SEED_GROUPS:null;
|
|
3627
|
-
if(seed&&seed.length){registry.setGroups(seed);setGroupScanState("done");return;}
|
|
3628
4146
|
// valid cache (same content signature) → apply instantly, skip the agent.
|
|
3629
4147
|
// A stale cache (different sig, e.g. content changed) falls through to a
|
|
3630
4148
|
// fresh scan and is overwritten below.
|
|
3631
4149
|
const sig=flatSig(flat);
|
|
3632
4150
|
const cached=readGroupCache();
|
|
3633
|
-
if(cached&&cached.sig===sig){registry.setGroups(cached.groups);setGroupScanState("done");return;}
|
|
4151
|
+
if(cached&&cached.sig===sig){registry.setGroups(cached.groups);groupScanRetryRef.current=0;setScanned(true);setGroupScanState("done");if(!opts.skipSeed)autoStopAfterScan();return;}
|
|
3634
4152
|
setGroupScanState("scanning");
|
|
3635
4153
|
const raw=flat.map(e=>({label:e.label,selector:e.bindings&&e.bindings.selector,
|
|
3636
4154
|
properties:e.properties,
|
|
@@ -3648,14 +4166,30 @@
|
|
|
3648
4166
|
const job=await relayGetJob(id);
|
|
3649
4167
|
if(job.status==="done"){
|
|
3650
4168
|
const groups=(job.result&&job.result.groups)||[];
|
|
3651
|
-
if(groups.length){registry.setGroups(groups);
|
|
4169
|
+
if(groups.length){registry.setGroups(groups);setScanned(true);
|
|
3652
4170
|
try{localStorage.setItem(GROUP_STORE_KEY,JSON.stringify({groups,sig,ts:Date.now()}));}catch{}}
|
|
3653
|
-
setGroupScanState("done");return;}
|
|
3654
|
-
if(job.status==="error"){
|
|
4171
|
+
groupScanRetryRef.current=0;setGroupScanState("done");if(!opts.skipSeed)autoStopAfterScan();return;}
|
|
4172
|
+
if(job.status==="error"){throw new Error(job.error||"scan job errored");}
|
|
4173
|
+
}
|
|
4174
|
+
throw new Error("scan timed out");
|
|
4175
|
+
}catch(e){
|
|
4176
|
+
// Transient failure (a cold-start agent spawn racing on its CLI config,
|
|
4177
|
+
// a relay blip): auto-retry a couple of times with backoff before
|
|
4178
|
+
// surfacing the error, instead of leaving the panel stuck flat.
|
|
4179
|
+
if(scanTokenRef.current!==token)return;
|
|
4180
|
+
if(groupScanRetryRef.current<GROUP_SCAN_MAX_RETRIES){
|
|
4181
|
+
groupScanRetryRef.current++;
|
|
4182
|
+
const backoff=700*groupScanRetryRef.current;
|
|
4183
|
+
setGroupScanState("scanning");
|
|
4184
|
+
setTimeout(()=>{if(scanTokenRef.current===token)runGroupScan(opts);},backoff);
|
|
4185
|
+
return;
|
|
3655
4186
|
}
|
|
4187
|
+
// gave up → stay flat, retry next open/rescan. Keep the relay's reason
|
|
4188
|
+
// (agent unavailable / run /refine live) so the header can surface it.
|
|
4189
|
+
setGroupScanErr(String((e&&e.message)||e||"Scan failed"));
|
|
3656
4190
|
setGroupScanState("error");
|
|
3657
|
-
}
|
|
3658
|
-
},[registry,GROUP_STORE_KEY,flatSig,readGroupCache]);
|
|
4191
|
+
}
|
|
4192
|
+
},[registry,GROUP_STORE_KEY,flatSig,readGroupCache,autoStopAfterScan]);
|
|
3659
4193
|
// manual "Rescan transitions" — drop the cache AND clear the in-memory
|
|
3660
4194
|
// groups first. Once groups claim every DOM transition the snapshot has no
|
|
3661
4195
|
// "flat" entries left, so the settle loop above would otherwise spin forever
|
|
@@ -3663,25 +4197,28 @@
|
|
|
3663
4197
|
// the flat entries the agent needs to re-group.
|
|
3664
4198
|
const rescanTransitions=useCallback(()=>{
|
|
3665
4199
|
try{localStorage.removeItem(GROUP_STORE_KEY);}catch{}
|
|
4200
|
+
groupScanRetryRef.current=0; // fresh manual scan → fresh retry budget
|
|
3666
4201
|
registry.clearGroups();
|
|
3667
|
-
runGroupScan();
|
|
4202
|
+
runGroupScan({skipSeed:true});
|
|
3668
4203
|
},[runGroupScan,GROUP_STORE_KEY,registry]);
|
|
3669
4204
|
// Gate the auto group-scan behind a live agent: the panel stays on the
|
|
3670
|
-
// "
|
|
3671
|
-
// makes the relay report an agent. The first time we see a
|
|
3672
|
-
// the resolver once — it settles the flat scan, then
|
|
3673
|
-
// (signature match, no agent) or kicks off a fresh
|
|
3674
|
-
// manual after that.
|
|
4205
|
+
// "Connect with your agent" screen until `/refine live` (or a wired
|
|
4206
|
+
// REFINE_AGENT_CMD) makes the relay report an agent. The first time we see a
|
|
4207
|
+
// live agent we run the resolver once — it settles the flat scan, then
|
|
4208
|
+
// re-applies cached groups (signature match, no agent) or kicks off a fresh
|
|
4209
|
+
// agent scan. Rescan is manual after that.
|
|
3675
4210
|
useEffect(()=>{
|
|
3676
4211
|
// A host/demo page can hardcode a grouping via window.__TX_SEED_GROUPS so
|
|
3677
4212
|
// the panel shows it WITHOUT a live agent (demo.html bakes in the agent's
|
|
3678
|
-
// initial scan this way).
|
|
3679
|
-
// a live agent
|
|
4213
|
+
// initial scan this way). A prior cached grouping likewise lets a returning
|
|
4214
|
+
// user skip the gate — apply it without a live agent. Otherwise the agent
|
|
4215
|
+
// group-scan stays gated behind a live agent as usual.
|
|
3680
4216
|
const seeded=typeof window!=="undefined"&&Array.isArray(window.__TX_SEED_GROUPS)&&window.__TX_SEED_GROUPS.length;
|
|
3681
|
-
|
|
4217
|
+
const cachedExists=!!readGroupCache();
|
|
4218
|
+
if((!live&&!seeded&&!cachedExists)||didGroupScanRef.current)return;
|
|
3682
4219
|
didGroupScanRef.current=true;
|
|
3683
4220
|
runGroupScan();
|
|
3684
|
-
},[live,runGroupScan]);
|
|
4221
|
+
},[live,runGroupScan,readGroupCache]);
|
|
3685
4222
|
useEffect(()=>()=>{scanTokenRef.current++;},[]);
|
|
3686
4223
|
|
|
3687
4224
|
// whole-component open/close uses the transitions.dev panel reveal:
|
|
@@ -3744,7 +4281,7 @@
|
|
|
3744
4281
|
panelHotRef.current=hoveredRef.current||!!(el&&document.activeElement&&el.contains(document.activeElement));
|
|
3745
4282
|
},[]);
|
|
3746
4283
|
const kbdRef=useRef({});
|
|
3747
|
-
kbdRef.current={entries,active,acceptState,acceptDisabled:computeChanges(active).length===0,minimized,
|
|
4284
|
+
kbdRef.current={entries,active,acceptState,acceptDisabled:computeChanges(active,entries).length===0,minimized,
|
|
3748
4285
|
openRefine,resetOverrides,copyValues,onAccept,setActiveId,setMinimized,setPickOpen};
|
|
3749
4286
|
useEffect(()=>{
|
|
3750
4287
|
const onKey=e=>{
|
|
@@ -3798,9 +4335,12 @@
|
|
|
3798
4335
|
window.addEventListener("keydown",onKey);
|
|
3799
4336
|
return()=>window.removeEventListener("keydown",onKey);
|
|
3800
4337
|
},[recomputeHot]);
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
4338
|
+
// Ready once a live agent is connected OR the one-time grouping scan has
|
|
4339
|
+
// produced groups (so the panel stays usable after a chat-loop agent
|
|
4340
|
+
// auto-stops post-scan). Only an un-scanned, agent-less panel shows the gate.
|
|
4341
|
+
const gate = (live || scanned)
|
|
4342
|
+
? "ready"
|
|
4343
|
+
: (llmAvailable===null ? "loading" : "blocked");
|
|
3804
4344
|
return h(React.Fragment,null,
|
|
3805
4345
|
render&&h("div",{className:"t-panel-slide","data-timeline-panel":true,ref:panelRootRef,
|
|
3806
4346
|
onMouseEnter:()=>{hoveredRef.current=true;},onMouseLeave:()=>{hoveredRef.current=false;},
|
|
@@ -3812,8 +4352,10 @@
|
|
|
3812
4352
|
h("div",{className:"tl-panel-main"},
|
|
3813
4353
|
h(Header,{entries,active,onSelect:setActiveId,onReset:resetOverrides,onCopy:copyValues,copied,
|
|
3814
4354
|
snap,setSnap,onMinimize:()=>setMinimized(true),onRefine:openRefine,refineActive:refineOpen,
|
|
3815
|
-
onAccept,acceptState,acceptDisabled:computeChanges(active).length===0,acceptError,
|
|
3816
|
-
scanning:groupScanState==="scanning",
|
|
4355
|
+
onAccept,acceptState,acceptDisabled:computeChanges(active,entries).length===0,acceptError,
|
|
4356
|
+
scanning:groupScanState==="scanning",scanError:groupScanState==="error"?groupScanErr:null,
|
|
4357
|
+
onRescan:rescanTransitions,pick:pickOpen,setPick:setPickOpen,
|
|
4358
|
+
chatLoop,liveStopped,onStopLive:stopLive}),
|
|
3817
4359
|
active
|
|
3818
4360
|
?h(Body,{entry:active,onPropChange:(prop,o)=>setPropOverride(prop,o),snap})
|
|
3819
4361
|
:h("div",{className:"tl-empty"},"Select a transition to inspect and edit it")),
|
|
@@ -3825,10 +4367,8 @@
|
|
|
3825
4367
|
: h("div",{className:"tl-panel-main"},
|
|
3826
4368
|
gate==="blocked" && h("div",{className:"tl-gate"},
|
|
3827
4369
|
h("div",{className:"tl-gate-col"},
|
|
3828
|
-
h("div",{className:"tl-gate-title"},"
|
|
3829
|
-
h(
|
|
3830
|
-
"Please run the ",h("code",{className:"tl-code"},"/refine live"),
|
|
3831
|
-
" command in your agent to enable live features, such as scanning and refining transitions.")))),
|
|
4370
|
+
h("div",{className:"tl-gate-title"},"Connect with your agent"),
|
|
4371
|
+
h(AgentConnectBody,{cliMissing:cliInstalled===false})))),
|
|
3832
4372
|
toast&&createPortal(
|
|
3833
4373
|
h("div",{className:"tl-toast-wrap","data-tl-ui":"","aria-live":"polite"},
|
|
3834
4374
|
h("div",{className:cx("tl-toast",toast.closing&&"is-closing")},
|
|
@@ -3847,12 +4387,12 @@
|
|
|
3847
4387
|
}
|
|
3848
4388
|
|
|
3849
4389
|
// ── demo boxes ──
|
|
3850
|
-
// Hardcoded
|
|
3851
|
-
//
|
|
3852
|
-
//
|
|
3853
|
-
//
|
|
3854
|
-
// Edit here if
|
|
3855
|
-
//
|
|
4390
|
+
// Hardcoded groupings — the transitions.dev agent's initial scan of each
|
|
4391
|
+
// prototype, baked in so the panel groups instantly with NO relay/agent
|
|
4392
|
+
// round-trip. The active prototype's seed is written to window.__TX_SEED_GROUPS
|
|
4393
|
+
// by App (PROTO_SEEDS map below). Member labels are cleaned up from the agent's
|
|
4394
|
+
// output. Edit here if a prototype's transitions change, or clear the seed to
|
|
4395
|
+
// fall back to a live agent scan. Shape mirrors relay.mjs buildScanPrompt's
|
|
3856
4396
|
// groups[] (the same structure TransitionRegistry.setGroups consumes). This is
|
|
3857
4397
|
// below the inject CUT_MARKER, so it never ships in the injected/website build.
|
|
3858
4398
|
const _mEO="cubic-bezier(0.22, 1, 0.36, 1)"; // smooth ease-out (open)
|
|
@@ -3879,7 +4419,39 @@
|
|
|
3879
4419
|
{id:"morph-item-bg",label:"Menu item",selector:".t-morph-menu button.morph-item",propertyTimings:[
|
|
3880
4420
|
_mpt("background",120,"ease")]}]}]},
|
|
3881
4421
|
];
|
|
3882
|
-
|
|
4422
|
+
// Menu dropdown (05-menu-dropdown.md) — the surface scales+fades from its
|
|
4423
|
+
// trigger; the caret flips. Close runs shorter and dips to a smaller scale.
|
|
4424
|
+
const DROPDOWN_SEED_GROUPS=[
|
|
4425
|
+
{id:"dropdown",label:"Menu dropdown",component:null,phases:[
|
|
4426
|
+
{id:"dropdown:open",phase:"open",label:"Open",stateTarget:".t-dropdown",fromState:null,toState:".is-open",members:[
|
|
4427
|
+
{id:"dd-caret",label:"Caret",selector:".dd-caret",propertyTimings:[
|
|
4428
|
+
_mpt("transform",250,_mEO)]},
|
|
4429
|
+
{id:"dd-menu",label:"Menu",selector:".t-dropdown",propertyTimings:[
|
|
4430
|
+
_mpt("transform",250,_mEO),_mpt("opacity",250,_mEO)]}]},
|
|
4431
|
+
{id:"dropdown:close",phase:"close",label:"Close",stateTarget:".t-dropdown",fromState:".is-open",toState:null,members:[
|
|
4432
|
+
{id:"dd-caret",label:"Caret",selector:".dd-caret",propertyTimings:[
|
|
4433
|
+
_mpt("transform",250,_mEO)]},
|
|
4434
|
+
{id:"dd-menu",label:"Menu",selector:".t-dropdown",propertyTimings:[
|
|
4435
|
+
_mpt("transform",150,_mEO),_mpt("opacity",150,_mEO)]}]}]},
|
|
4436
|
+
];
|
|
4437
|
+
// Modal (06-modal.md) — the dialog scales up from center over a fading
|
|
4438
|
+
// backdrop; close dips back down. Both phases share the standard ease-out.
|
|
4439
|
+
const MODAL_SEED_GROUPS=[
|
|
4440
|
+
{id:"modal",label:"Modal",component:null,phases:[
|
|
4441
|
+
{id:"modal:open",phase:"open",label:"Open",stateTarget:".t-modal",fromState:null,toState:".is-open",members:[
|
|
4442
|
+
{id:"modal-backdrop",label:"Backdrop",selector:".modal-backdrop",propertyTimings:[
|
|
4443
|
+
_mpt("opacity",250,_mEO)]},
|
|
4444
|
+
{id:"modal-dialog",label:"Dialog",selector:".t-modal",propertyTimings:[
|
|
4445
|
+
_mpt("transform",250,_mEO),_mpt("opacity",250,_mEO)]}]},
|
|
4446
|
+
{id:"modal:close",phase:"close",label:"Close",stateTarget:".t-modal",fromState:".is-open",toState:null,members:[
|
|
4447
|
+
{id:"modal-backdrop",label:"Backdrop",selector:".modal-backdrop",propertyTimings:[
|
|
4448
|
+
_mpt("opacity",150,_mEO)]},
|
|
4449
|
+
{id:"modal-dialog",label:"Dialog",selector:".t-modal",propertyTimings:[
|
|
4450
|
+
_mpt("transform",150,_mEO),_mpt("opacity",150,_mEO)]}]}]},
|
|
4451
|
+
];
|
|
4452
|
+
// Active prototype → seed. App writes window.__TX_SEED_GROUPS[proto] before the
|
|
4453
|
+
// (remounted) panel runs its group scan, so each tab groups agent-free.
|
|
4454
|
+
const PROTO_SEEDS={morph:MORPH_SEED_GROUPS,dropdown:DROPDOWN_SEED_GROUPS,modal:MODAL_SEED_GROUPS};
|
|
3883
4455
|
|
|
3884
4456
|
// Plus → menu morph — transitions.dev (20-plus-menu-morph.md). The trigger
|
|
3885
4457
|
// button morphs into the panel it opens; CSS owns the morph, JS only flips
|
|
@@ -3912,6 +4484,100 @@
|
|
|
3912
4484
|
h("path",{d:"M10 4.5v11M4.5 10h11",stroke:"currentColor","strokeWidth":1.8,"strokeLinecap":"round"}))))));
|
|
3913
4485
|
}
|
|
3914
4486
|
|
|
4487
|
+
// Menu dropdown — transitions.dev (05-menu-dropdown.md). The .t-dropdown surface
|
|
4488
|
+
// owns the scale+fade; close swaps .is-open for .is-closing, then drops the class
|
|
4489
|
+
// after --dropdown-close-dur so the closing scale animates before it resets.
|
|
4490
|
+
function BoxDropdown(){
|
|
4491
|
+
const[open,setOpen]=useState(false);
|
|
4492
|
+
const[closing,setClosing]=useState(false);
|
|
4493
|
+
const ref=useRef(null), toRef=useRef(null);
|
|
4494
|
+
const close=useCallback(()=>{
|
|
4495
|
+
setOpen(false);setClosing(true);
|
|
4496
|
+
if(toRef.current)clearTimeout(toRef.current);
|
|
4497
|
+
toRef.current=setTimeout(()=>setClosing(false),150);
|
|
4498
|
+
},[]);
|
|
4499
|
+
const toggle=useCallback(()=>{setOpen(o=>{if(o){close();return false;}setClosing(false);return true;});},[close]);
|
|
4500
|
+
useEffect(()=>{
|
|
4501
|
+
const onDoc=e=>{if(ref.current&&!ref.current.contains(e.target))close();};
|
|
4502
|
+
const onKey=e=>{if(e.key==="Escape")close();};
|
|
4503
|
+
document.addEventListener("click",onDoc);
|
|
4504
|
+
document.addEventListener("keydown",onKey);
|
|
4505
|
+
return()=>{document.removeEventListener("click",onDoc);document.removeEventListener("keydown",onKey);if(toRef.current)clearTimeout(toRef.current);};
|
|
4506
|
+
},[close]);
|
|
4507
|
+
const item=(label,icon)=>h("button",{className:"dd-item",type:"button",onClick:close},
|
|
4508
|
+
h(Ic,{name:icon,size:16}),label);
|
|
4509
|
+
return h("div",{className:"card"},
|
|
4510
|
+
h("h2",null,"Menu dropdown"),
|
|
4511
|
+
h("p",null,"Click Menu. The panel grows from its trigger with a scale + fade and the caret flips. Closing runs shorter and dips to a smaller scale."),
|
|
4512
|
+
h("div",{className:"dd-stage"},
|
|
4513
|
+
h("div",{className:cx("dd",open&&"is-open"),ref},
|
|
4514
|
+
h("button",{className:"dd-trigger",type:"button","aria-haspopup":"menu","aria-expanded":String(open),
|
|
4515
|
+
onClick:e=>{e.stopPropagation();toggle();}},
|
|
4516
|
+
"Menu",
|
|
4517
|
+
h("svg",{className:"dd-caret",width:14,height:14,viewBox:"0 0 16 16",fill:"none","aria-hidden":"true"},
|
|
4518
|
+
h("path",{d:"M4 6l4 4 4-4",stroke:"currentColor","strokeWidth":1.6,"strokeLinecap":"round","strokeLinejoin":"round"}))),
|
|
4519
|
+
h("div",{className:cx("t-dropdown","dd-menu",open&&"is-open",closing&&"is-closing"),"data-origin":"top-left",role:"menu","aria-hidden":String(!open)},
|
|
4520
|
+
h("div",{className:"dd-title"},"Actions"),
|
|
4521
|
+
item("New file","copy"),
|
|
4522
|
+
item("Refresh","restart"),
|
|
4523
|
+
item("Settings","gear")))));
|
|
4524
|
+
}
|
|
4525
|
+
|
|
4526
|
+
// Modal — transitions.dev (06-modal.md). The .t-modal dialog scales up from
|
|
4527
|
+
// center over a fading .modal-backdrop; same close-then-cleanup pattern as the
|
|
4528
|
+
// dropdown. Scoped to the stage (not full-viewport) so it stays in the scan root.
|
|
4529
|
+
function BoxModal(){
|
|
4530
|
+
const[open,setOpen]=useState(false);
|
|
4531
|
+
const[closing,setClosing]=useState(false);
|
|
4532
|
+
const toRef=useRef(null);
|
|
4533
|
+
const close=useCallback(()=>{
|
|
4534
|
+
setOpen(false);setClosing(true);
|
|
4535
|
+
if(toRef.current)clearTimeout(toRef.current);
|
|
4536
|
+
toRef.current=setTimeout(()=>setClosing(false),150);
|
|
4537
|
+
},[]);
|
|
4538
|
+
const openIt=useCallback(()=>{setClosing(false);setOpen(true);},[]);
|
|
4539
|
+
useEffect(()=>{
|
|
4540
|
+
const onKey=e=>{if(e.key==="Escape")close();};
|
|
4541
|
+
document.addEventListener("keydown",onKey);
|
|
4542
|
+
return()=>{document.removeEventListener("keydown",onKey);if(toRef.current)clearTimeout(toRef.current);};
|
|
4543
|
+
},[close]);
|
|
4544
|
+
const st=(base)=>cx(base,open&&"is-open",closing&&"is-closing");
|
|
4545
|
+
return h("div",{className:"card"},
|
|
4546
|
+
h("h2",null,"Modal"),
|
|
4547
|
+
h("p",null,"Open the dialog. It scales up from 0.96 over a fading backdrop; closing dips back down. Click the backdrop or press Escape to dismiss."),
|
|
4548
|
+
h("div",{className:"modal-stage"},
|
|
4549
|
+
h("button",{className:"modal-open-btn",type:"button",onClick:openIt},"Open dialog"),
|
|
4550
|
+
h("div",{className:st("modal-backdrop"),onClick:close,"aria-hidden":"true"}),
|
|
4551
|
+
h("div",{className:st("t-modal"),role:"dialog","aria-modal":"true","aria-hidden":String(!open)},
|
|
4552
|
+
h("div",{className:"modal-card-title"},"Delete project?"),
|
|
4553
|
+
h("div",{className:"modal-card-body"},"This permanently removes the project and all of its files. This action can\u2019t be undone."),
|
|
4554
|
+
h("div",{className:"modal-actions"},
|
|
4555
|
+
h("button",{className:"modal-btn secondary",type:"button",onClick:close},"Cancel"),
|
|
4556
|
+
h("button",{className:"modal-btn primary",type:"button",onClick:close},"Delete")))));
|
|
4557
|
+
}
|
|
4558
|
+
|
|
4559
|
+
// Prototype switcher — sliding-pill tabs (16-tabs-sliding.md). JS measures the
|
|
4560
|
+
// active tab's offsetLeft/Width onto the pill; CSS tweens. First paint + resize
|
|
4561
|
+
// snap without a transition; only a user switch animates.
|
|
4562
|
+
function ProtoTabs({value,onChange,items}){
|
|
4563
|
+
const pillRef=useRef(null), tabRefs=useRef({}), firstRef=useRef(true);
|
|
4564
|
+
const place=useCallback((animate)=>{
|
|
4565
|
+
const t=tabRefs.current[value], pill=pillRef.current;
|
|
4566
|
+
if(!t||!pill)return;
|
|
4567
|
+
if(!animate){const prev=pill.style.transition;pill.style.transition="none";
|
|
4568
|
+
pill.style.transform="translateX("+t.offsetLeft+"px)";pill.style.width=t.offsetWidth+"px";
|
|
4569
|
+
void pill.offsetWidth;pill.style.transition=prev;}
|
|
4570
|
+
else{pill.style.transform="translateX("+t.offsetLeft+"px)";pill.style.width=t.offsetWidth+"px";}
|
|
4571
|
+
},[value]);
|
|
4572
|
+
useLayoutEffect(()=>{place(firstRef.current?false:true);firstRef.current=false;},[value,place]);
|
|
4573
|
+
useEffect(()=>{const onR=()=>place(false);window.addEventListener("resize",onR);return()=>window.removeEventListener("resize",onR);},[place]);
|
|
4574
|
+
return h("div",{className:"proto-tabs",role:"tablist"},
|
|
4575
|
+
h("span",{className:"proto-tabs-pill","aria-hidden":"true",ref:pillRef}),
|
|
4576
|
+
items.map(it=>h("button",{key:it.key,role:"tab",type:"button","aria-selected":String(value===it.key),
|
|
4577
|
+
className:cx("proto-tab",value===it.key&&"is-active"),ref:el=>{tabRefs.current[it.key]=el;},
|
|
4578
|
+
onClick:()=>onChange(it.key)},it.label)));
|
|
4579
|
+
}
|
|
4580
|
+
|
|
3915
4581
|
// ── live controls for the whole-panel reveal transition (demo harness) ──
|
|
3916
4582
|
// transitions.dev motion tokens (values lifted verbatim from the skill's _root.css)
|
|
3917
4583
|
// transitions.dev easing motion tokens. `token` is the actual skill CSS var
|
|
@@ -4011,9 +4677,25 @@
|
|
|
4011
4677
|
);
|
|
4012
4678
|
}
|
|
4013
4679
|
|
|
4680
|
+
// Prototypes the demo can switch between. Each owns a component + a description
|
|
4681
|
+
// + a baked-in seed grouping (PROTO_SEEDS) so the panel groups it agent-free.
|
|
4682
|
+
const PROTOS=[
|
|
4683
|
+
{key:"morph", label:"Morph menu", comp:BoxMorph, desc:"the plus \u2192 menu morph"},
|
|
4684
|
+
{key:"dropdown",label:"Dropdown", comp:BoxDropdown, desc:"an anchored menu dropdown"},
|
|
4685
|
+
{key:"modal", label:"Modal", comp:BoxModal, desc:"a centered modal dialog"},
|
|
4686
|
+
];
|
|
4014
4687
|
function App(){
|
|
4015
|
-
const
|
|
4016
|
-
|
|
4688
|
+
const[proto,setProto]=useState("dropdown");
|
|
4689
|
+
// Fresh registry per prototype so switching tabs starts the scan clean. The
|
|
4690
|
+
// active prototype's baked grouping is published before the (remounted) panel
|
|
4691
|
+
// runs its seeded group scan — see runGroupScan's window.__TX_SEED_GROUPS path.
|
|
4692
|
+
if(typeof window!=="undefined")window.__TX_SEED_GROUPS=PROTO_SEEDS[proto];
|
|
4693
|
+
const rootRef=useRef(null);const scanRef=useRef(null);
|
|
4694
|
+
const registry=useMemo(()=>new TransitionRegistry(),[proto]);
|
|
4695
|
+
const[activeId,setActiveId]=useState(null);
|
|
4696
|
+
// Scan only the prototype stage (not the header), so the switcher tabs' own
|
|
4697
|
+
// pill transition is never picked up as a demo transition.
|
|
4698
|
+
useEffect(()=>{const root=scanRef.current??document.body;const scanner=new DomScanner(root,registry);scanner.start();return()=>{scanner.stop();};},[registry]);
|
|
4017
4699
|
const ctx=useMemo(()=>({registry,activeId,setActiveId}),[registry,activeId]);
|
|
4018
4700
|
// Demo-only tweak controls: hidden by default. Append ?controls to the URL
|
|
4019
4701
|
// to show them for testing. (This whole block is below the inject CUT_MARKER,
|
|
@@ -4022,6 +4704,7 @@
|
|
|
4022
4704
|
// TESTING ONLY (not part of the tool): top-right toggle to hide the demo
|
|
4023
4705
|
// page content so the panel can be inspected against a blank page.
|
|
4024
4706
|
const[demoHidden,setDemoHidden]=useState(false);
|
|
4707
|
+
const active=PROTOS.find(p=>p.key===proto)||PROTOS[0];
|
|
4025
4708
|
return h(TimelineCtx.Provider,{value:ctx},
|
|
4026
4709
|
showControls&&h(PanelControls),
|
|
4027
4710
|
h("button",{type:"button",onClick:()=>setDemoHidden(v=>!v),
|
|
@@ -4032,9 +4715,13 @@
|
|
|
4032
4715
|
font:"500 12px/1 ui-sans-serif,system-ui,sans-serif",
|
|
4033
4716
|
boxShadow:"0 1px 3px rgba(0,0,0,0.12)"}},
|
|
4034
4717
|
demoHidden?"Show demo":"Hide demo"),
|
|
4035
|
-
h("div",{ref:rootRef,className:"demo-root",style:{display:demoHidden?"none":undefined}},
|
|
4036
|
-
h("div",{className:"demo-
|
|
4037
|
-
|
|
4718
|
+
h("div",{ref:rootRef,className:"demo-root",style:{display:demoHidden?"none":undefined}},
|
|
4719
|
+
h("div",{className:"demo-header"},
|
|
4720
|
+
h("h1",null,"Timeline Inspector \u2014 Demo"),
|
|
4721
|
+
h("p",null,"A live transitions.dev component: ",active.desc,". 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."),
|
|
4722
|
+
h(ProtoTabs,{value:proto,onChange:setProto,items:PROTOS})),
|
|
4723
|
+
h("div",{ref:scanRef,className:"demo-grid"},h(active.comp,{key:proto}))),
|
|
4724
|
+
h(TimelinePanel,{key:proto}));
|
|
4038
4725
|
}
|
|
4039
4726
|
|
|
4040
4727
|
createRoot(document.getElementById("root")).render(h(App));
|