transitions-refine 0.3.9 → 0.3.10
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 +72 -49
- package/demo.html +480 -110
- package/package.json +2 -1
- package/server/relay.mjs +70 -7
package/demo.html
CHANGED
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
--drop-btn: 0 1px 3px 0 rgba(4,41,117,0.08);
|
|
40
40
|
/* secondary/raised white button (Reset, Settings, Copy, Accept) — exact spec */
|
|
41
41
|
--shadow-btn: 0 1px 3px 0 rgba(0,0,0,0.04), inset 0 0 0 1px rgba(0,0,0,0.06), inset 0 -1px 0 0 rgba(0,0,0,0.10), inset 0 0 0 1px rgba(196,196,196,0.10);
|
|
42
|
+
/* inset-only portion of --shadow-btn — for overlay rings that must reproduce
|
|
43
|
+
the Reset button's visible border on top of a separate fill layer */
|
|
44
|
+
--shadow-btn-inset: inset 0 0 0 1px rgba(0,0,0,0.06), inset 0 -1px 0 0 rgba(0,0,0,0.10), inset 0 0 0 1px rgba(196,196,196,0.10);
|
|
42
45
|
--drop-line: 0 1px 3px 0 rgba(0,0,0,0.08);
|
|
43
46
|
--shadow-border: 0 0 0 1px rgba(0,0,0,0.06), 0 1px 2px -1px rgba(0,0,0,0.06), 0 2px 4px 0 rgba(0,0,0,0.04);
|
|
44
47
|
--shadow-border-hover: 0 0 0 1px rgba(0,0,0,0.08), 0 1px 2px -1px rgba(0,0,0,0.08), 0 2px 4px 0 rgba(0,0,0,0.06);
|
|
@@ -327,7 +330,7 @@
|
|
|
327
330
|
border: none; border-radius: 8px; background: transparent; color: var(--c-blue);
|
|
328
331
|
box-shadow: var(--drop-btn);
|
|
329
332
|
font: inherit; font-size: 13px; font-weight: 500; line-height: 14px;
|
|
330
|
-
cursor: pointer; white-space: nowrap; transition: color 0.12s ease; }
|
|
333
|
+
cursor: pointer; white-space: nowrap; transition: color 0.12s ease, scale 0.12s ease; }
|
|
331
334
|
.tl-refine-btn::before { content: ""; position: absolute; inset: 0; border-radius: inherit;
|
|
332
335
|
background: var(--c-blue-bg);
|
|
333
336
|
background: color(display-p3 0 0.451 0.898 / 0.06);
|
|
@@ -339,7 +342,7 @@
|
|
|
339
342
|
/* Figma: wand icon stroke is #0073E5, distinct from the #0071e2 label */
|
|
340
343
|
.tl-refine-btn > svg { color: var(--c-blue-pressed); }
|
|
341
344
|
.tl-refine-btn:hover:not(:disabled)::before { background: var(--c-blue-bg-h); background: color(display-p3 0 0.451 0.898 / 0.10); }
|
|
342
|
-
.tl-refine-btn:active:not(:disabled) { color: var(--c-blue-pressed); }
|
|
345
|
+
.tl-refine-btn:active:not(:disabled) { color: var(--c-blue-pressed); scale: 0.96; }
|
|
343
346
|
.tl-refine-btn:active:not(:disabled)::before { background: var(--c-blue-bg-h); background: color(display-p3 0 0.451 0.898 / 0.10); }
|
|
344
347
|
.tl-refine-btn:disabled { cursor: default; color: var(--c-blue); }
|
|
345
348
|
.tl-refine-btn:disabled::before { background: var(--c-blue-bg); background: color(display-p3 0 0.451 0.898 / 0.06); }
|
|
@@ -412,7 +415,15 @@
|
|
|
412
415
|
.tl-bar { position: absolute; top: 50%; height: 24px; transform: translateY(-50%); background: var(--c-track);
|
|
413
416
|
border-radius: 8px; min-width: 24px; cursor: grab;
|
|
414
417
|
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.08), inset 0 0 0 1px rgba(0,0,0,0.06), inset 0 -1px 0 0 rgba(0,0,0,0.1), inset 0 0 0 1px rgba(196,196,196,0.1);
|
|
415
|
-
|
|
418
|
+
/* card resize (transitions.dev #01): tween left/width when timing changes
|
|
419
|
+
programmatically (Apply, snap, value-field edit). Suppressed mid-drag
|
|
420
|
+
(.is-dragging) so the bar tracks the cursor 1:1. */
|
|
421
|
+
transition: background 0.12s ease,
|
|
422
|
+
left 220ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
423
|
+
width 220ms cubic-bezier(0.22, 1, 0.36, 1); will-change: left, width; }
|
|
424
|
+
.tl-bar.is-dragging { transition: background 0.12s ease; }
|
|
425
|
+
@media (prefers-reduced-motion: reduce) {
|
|
426
|
+
.tl-bar { transition: background 0.12s ease !important; } }
|
|
416
427
|
.tl-prop-row:hover .tl-bar, .tl-prop-row.selected .tl-bar { background: var(--c-track-h); }
|
|
417
428
|
.tl-bar:active { cursor: grabbing; }
|
|
418
429
|
/* resize handles — Figma node 13044:2625: 2×14px, #767676 @60%, centered, ~5px edge inset */
|
|
@@ -450,18 +461,23 @@
|
|
|
450
461
|
.tl-field-fill { position: absolute; left: 0; top: 0; bottom: 0; min-width: 32px; border-radius: 8px;
|
|
451
462
|
background: var(--c-fill);
|
|
452
463
|
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.02), inset 0 -1px 0 0 rgba(0,0,0,0.08), inset 0 0 0 1px rgba(191,191,191,0.10), 0 1px 3px 0 rgba(0,0,0,0.06);
|
|
453
|
-
|
|
454
|
-
|
|
464
|
+
/* card resize (#01): tween width on programmatic changes + release snap;
|
|
465
|
+
suppressed mid-drag so it tracks the cursor 1:1. */
|
|
466
|
+
pointer-events: none; transition: background 0.12s ease, opacity 0.12s ease,
|
|
467
|
+
width 220ms cubic-bezier(0.22, 1, 0.36, 1); z-index: 1; }
|
|
468
|
+
.tl-field.is-dragging .tl-field-fill { background: var(--c-fill-a); transition: background 0.12s ease, opacity 0.12s ease; }
|
|
469
|
+
@media (prefers-reduced-motion: reduce) {
|
|
470
|
+
.tl-field-fill { transition: background 0.12s ease, opacity 0.12s ease !important; } }
|
|
455
471
|
.tl-field.is-dragging .tl-field-label { opacity: 0.7; }
|
|
456
472
|
.tl-field.is-editing .tl-field-fill { opacity: 0; }
|
|
457
473
|
.tl-field-track { position: absolute; inset: 0; cursor: ew-resize; z-index: 2; }
|
|
458
474
|
.tl-field-label { position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
|
|
459
475
|
font-size: 13px; font-weight: 500; color: var(--c-text-mut); pointer-events: none; z-index: 3; }
|
|
460
476
|
.tl-field-value { position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
|
|
461
|
-
font-size: 13px; color: var(--c-text); font-weight:
|
|
477
|
+
font-size: 13px; color: var(--c-text); font-weight: 400; cursor: text; z-index: 3;
|
|
462
478
|
font-variant-numeric: tabular-nums; }
|
|
463
479
|
.tl-field-input { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); width: 64px;
|
|
464
|
-
text-align: right; border: none; background: transparent; font: inherit; font-size: 13px; font-weight:
|
|
480
|
+
text-align: right; border: none; background: transparent; font: inherit; font-size: 13px; font-weight: 400;
|
|
465
481
|
color: var(--c-text); outline: none; z-index: 4; font-variant-numeric: tabular-nums; }
|
|
466
482
|
/* thumb at the right edge of the fill (Figma #767676 2×20px, inset 8px, hover 60% / drag 100%) */
|
|
467
483
|
.tl-field-thumb { position: absolute; right: 8px; top: 7px; height: 18px; width: 2px;
|
|
@@ -520,7 +536,7 @@
|
|
|
520
536
|
/* select trigger — Logram design system (node 13064:2418): flat #f7f7f7 fill */
|
|
521
537
|
.tl-select { display: flex; align-items: center; justify-content: space-between; width: 100%; height: 32px;
|
|
522
538
|
border: none; border-radius: 8px; background: #f7f7f7; padding: 6px 10px 6px 12px;
|
|
523
|
-
font: inherit; font-size: 13px; font-weight:
|
|
539
|
+
font: inherit; font-size: 13px; font-weight: 400; line-height: 16px; color: #1b1b1b; cursor: pointer; text-align: left;
|
|
524
540
|
transition: background 0.12s ease; }
|
|
525
541
|
.tl-select:hover { background: var(--c-sec-h); }
|
|
526
542
|
.tl-select.is-open { background: var(--c-sec-h); }
|
|
@@ -546,7 +562,7 @@
|
|
|
546
562
|
.tl-cubic-cell:focus-within { box-shadow: inset 0 0 0 1px rgba(0,0,0,0.14); }
|
|
547
563
|
.tl-cubic-row input { flex: 1; min-width: 0; width: 100%; height: 100%; border: none; background: transparent;
|
|
548
564
|
box-sizing: border-box; padding: 0; text-align: left;
|
|
549
|
-
font: inherit; font-size: 12px; font-weight:
|
|
565
|
+
font: inherit; font-size: 12px; font-weight: 400; line-height: 16px; color: #1b1b1b; outline: none;
|
|
550
566
|
-moz-appearance: textfield; appearance: textfield; }
|
|
551
567
|
.tl-cubic-row input::-webkit-outer-spin-button, .tl-cubic-row input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; width: 0; }
|
|
552
568
|
.tl-cubic-row input::placeholder { color: #979797; }
|
|
@@ -709,6 +725,12 @@
|
|
|
709
725
|
/* trailing chevron on link rows (Figma 581:2828 — #696969 right chevron) */
|
|
710
726
|
.tl-menu-chevr { display: flex; flex: none; color: #696969; }
|
|
711
727
|
|
|
728
|
+
/* trailing slot on a menu row — current value + chevron (Figma: Theme row
|
|
729
|
+
shows "System" then a right chevron and opens the theme picker sub-page). */
|
|
730
|
+
.tl-menu-trail { display: flex; align-items: center; gap: 4px; }
|
|
731
|
+
.tl-menu-value { font-size: 11px; line-height: 14px; color: #8b8b8b;
|
|
732
|
+
font-variant-numeric: tabular-nums; }
|
|
733
|
+
|
|
712
734
|
/* ── settings dropdown sub-page slider (transitions.dev · 08-page-side-by-side) ──
|
|
713
735
|
One dropdown surface that pages internally: main settings ↔ keyboard
|
|
714
736
|
shortcuts. Both pages stay mounted so they can cross-slide; the surface
|
|
@@ -716,6 +738,10 @@
|
|
|
716
738
|
smoothly instead of jumping. Page 1 exits left, page 2 exits right. */
|
|
717
739
|
.tl-set-slide.t-page-slide {
|
|
718
740
|
position: relative;
|
|
741
|
+
/* bleed past the .tl-menu 6px padding so the clipped (overflow:hidden)
|
|
742
|
+
viewport reaches the menu edges — lets dividers go truly full-width.
|
|
743
|
+
The 6px row inset is restored on .tl-set-page below. */
|
|
744
|
+
margin-left: -6px; margin-right: -6px;
|
|
719
745
|
--page-slide-dur: 250ms; /* Fast — page slide */
|
|
720
746
|
--page-fade-dur: 250ms;
|
|
721
747
|
--page-slide-distance: 8px; /* Base distance */
|
|
@@ -728,9 +754,11 @@
|
|
|
728
754
|
overflow: hidden;
|
|
729
755
|
}
|
|
730
756
|
.tl-set-slide .t-page[data-page-id="1"] { --t-page-from-x: calc(var(--page-slide-distance) * -1); }
|
|
731
|
-
.tl-set-slide .t-page[data-page-id="2"]
|
|
757
|
+
.tl-set-slide .t-page[data-page-id="2"],
|
|
758
|
+
.tl-set-slide .t-page[data-page-id="3"] { --t-page-from-x: var(--page-slide-distance); }
|
|
732
759
|
.tl-set-slide .t-page {
|
|
733
760
|
position: absolute; top: 0; left: 0; right: 0;
|
|
761
|
+
box-sizing: border-box; padding-left: 6px; padding-right: 6px;
|
|
734
762
|
opacity: 0; pointer-events: none;
|
|
735
763
|
transform: translateX(calc(var(--t-page-from-x, 0px) * var(--page-exit-enabled)));
|
|
736
764
|
filter: blur(calc(var(--page-blur) * var(--page-exit-enabled)));
|
|
@@ -741,7 +769,8 @@
|
|
|
741
769
|
will-change: opacity, transform, filter;
|
|
742
770
|
}
|
|
743
771
|
.tl-set-slide[data-page="1"] .t-page[data-page-id="1"],
|
|
744
|
-
.tl-set-slide[data-page="2"] .t-page[data-page-id="2"]
|
|
772
|
+
.tl-set-slide[data-page="2"] .t-page[data-page-id="2"],
|
|
773
|
+
.tl-set-slide[data-page="3"] .t-page[data-page-id="3"] {
|
|
745
774
|
opacity: 1; pointer-events: auto; transform: translateX(0); filter: blur(0);
|
|
746
775
|
transition-delay: var(--page-stagger);
|
|
747
776
|
}
|
|
@@ -887,6 +916,12 @@
|
|
|
887
916
|
transform: translate(0, 0) scale(var(--tt-scale)); }
|
|
888
917
|
.t-tt-wrap:hover .t-tt.tl-tt-msg,
|
|
889
918
|
.t-tt-trigger:focus-visible + .t-tt.tl-tt-msg { transform: translate(0, 0) scale(1); }
|
|
919
|
+
/* right-anchored variant for the panel's rightmost trigger (Minimize) so the
|
|
920
|
+
bubble grows leftward from the right edge and never clips off the panel. */
|
|
921
|
+
.t-tt.tl-tt-right { left: auto; right: 0; transform-origin: 100% 0;
|
|
922
|
+
transform: translate(0, 0) scale(var(--tt-scale)); }
|
|
923
|
+
.t-tt-wrap:hover .t-tt.tl-tt-right,
|
|
924
|
+
.t-tt-trigger:focus-visible + .t-tt.tl-tt-right { transform: translate(0, 0) scale(1); }
|
|
890
925
|
|
|
891
926
|
/* ═════ transitions.dev — panel reveal (per-phase distance/ease/scale) ═════ */
|
|
892
927
|
/* base = closed / close-end state: resolved travel + scale come from the
|
|
@@ -1018,7 +1053,7 @@
|
|
|
1018
1053
|
/* header: title block (left) + Agent/Deterministic mode dropdown (right) */
|
|
1019
1054
|
/* match the timeline top bar exactly: 7.5px/14px padding, centered, 52px tall (36px row + 15 + 1px border) */
|
|
1020
1055
|
.tl-refine-head { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between;
|
|
1021
|
-
gap: 8px; box-sizing: border-box; padding: 7.5px 14px;
|
|
1056
|
+
gap: 8px; box-sizing: border-box; padding: 7.5px 7.5px 7.5px 14px;
|
|
1022
1057
|
border-bottom: 1px solid #f0f0f0; }
|
|
1023
1058
|
.tl-refine-titles { min-width: 0; }
|
|
1024
1059
|
.tl-refine-titles h3 { font-size: 13px; font-weight: 500; line-height: 14px; color: #17181c; }
|
|
@@ -1047,6 +1082,49 @@
|
|
|
1047
1082
|
.tl-refine-tab[aria-selected="true"] { background: rgba(170,170,170,0.10); color: #17181c; }
|
|
1048
1083
|
.tl-refine-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column;
|
|
1049
1084
|
padding: 0 16px; }
|
|
1085
|
+
/* ── refine-type tab content slider (transitions.dev · 08-page-side-by-side) ──
|
|
1086
|
+
Switching the type tabs (Small refinements ↔ Replace transition) slides the
|
|
1087
|
+
body content side-by-side: the outgoing page exits with blur+fade while the
|
|
1088
|
+
incoming page slides in from the opposite edge. Both pages stay mounted so
|
|
1089
|
+
they cross-slide. Page 1 (Small, tab index 0) exits left; page 2 (Replace,
|
|
1090
|
+
tab index 1) exits right — going to Replace brings new content in from the
|
|
1091
|
+
right, going back brings it from the left. Mirrors the settings dropdown's
|
|
1092
|
+
.tl-set-slide tokens exactly. Pages are absolutely stacked and fill the
|
|
1093
|
+
fixed body region, scrolling internally, so no height tween is needed. */
|
|
1094
|
+
.tl-refine-body.tl-refine-pages.t-page-slide {
|
|
1095
|
+
position: relative; overflow: hidden; padding: 0;
|
|
1096
|
+
--page-slide-dur: 250ms; /* Fast — page slide */
|
|
1097
|
+
--page-fade-dur: 250ms;
|
|
1098
|
+
--page-slide-distance: 8px; /* Base distance */
|
|
1099
|
+
--page-blur: 3px; /* Medium blur */
|
|
1100
|
+
--page-stagger: 0ms;
|
|
1101
|
+
--page-exit-enabled: 1;
|
|
1102
|
+
--page-slide-ease: cubic-bezier(0.22, 1, 0.36, 1); /* Smooth ease out */
|
|
1103
|
+
--page-fade-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
1104
|
+
}
|
|
1105
|
+
.tl-refine-pages .t-page[data-page-id="1"] { --t-page-from-x: calc(var(--page-slide-distance) * -1); }
|
|
1106
|
+
.tl-refine-pages .t-page[data-page-id="2"] { --t-page-from-x: var(--page-slide-distance); }
|
|
1107
|
+
.tl-refine-pages .t-page {
|
|
1108
|
+
position: absolute; inset: 0; overflow-y: auto;
|
|
1109
|
+
display: flex; flex-direction: column; box-sizing: border-box; padding: 0 16px;
|
|
1110
|
+
opacity: 0; pointer-events: none;
|
|
1111
|
+
transform: translateX(calc(var(--t-page-from-x, 0px) * var(--page-exit-enabled)));
|
|
1112
|
+
filter: blur(calc(var(--page-blur) * var(--page-exit-enabled)));
|
|
1113
|
+
transition:
|
|
1114
|
+
opacity var(--page-fade-dur) var(--page-fade-ease),
|
|
1115
|
+
transform var(--page-slide-dur) var(--page-slide-ease),
|
|
1116
|
+
filter var(--page-slide-dur) var(--page-slide-ease);
|
|
1117
|
+
will-change: opacity, transform, filter;
|
|
1118
|
+
}
|
|
1119
|
+
.tl-refine-pages[data-page="1"] .t-page[data-page-id="1"],
|
|
1120
|
+
.tl-refine-pages[data-page="2"] .t-page[data-page-id="2"] {
|
|
1121
|
+
opacity: 1; pointer-events: auto; transform: translateX(0); filter: blur(0);
|
|
1122
|
+
transition-delay: var(--page-stagger);
|
|
1123
|
+
}
|
|
1124
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1125
|
+
.tl-refine-pages.t-page-slide,
|
|
1126
|
+
.tl-refine-pages .t-page { transition: none !important; }
|
|
1127
|
+
}
|
|
1050
1128
|
/* idle + unavailable share a vertically-centred column */
|
|
1051
1129
|
.tl-refine-center { flex: 1; display: flex; flex-direction: column; align-items: center;
|
|
1052
1130
|
justify-content: center; gap: 12px; text-align: center; padding: 16px 0; }
|
|
@@ -1092,7 +1170,8 @@
|
|
|
1092
1170
|
height var(--resize-dur) var(--resize-ease),
|
|
1093
1171
|
border-radius var(--resize-dur) var(--resize-ease),
|
|
1094
1172
|
color var(--resize-dur) var(--resize-ease),
|
|
1095
|
-
box-shadow var(--resize-dur) var(--resize-ease)
|
|
1173
|
+
box-shadow var(--resize-dur) var(--resize-ease),
|
|
1174
|
+
scale 0.12s ease; }
|
|
1096
1175
|
.tl-scan-morph::before { content: ""; position: absolute; inset: 0; border-radius: inherit;
|
|
1097
1176
|
background: var(--c-blue-bg);
|
|
1098
1177
|
background: color(display-p3 0 0.451 0.898 / 0.06);
|
|
@@ -1104,7 +1183,7 @@
|
|
|
1104
1183
|
.tl-refine-foot .tl-scan-morph { margin-bottom: 20px; }
|
|
1105
1184
|
.tl-scan-morph:hover:not(:disabled):not(.is-scanning)::before {
|
|
1106
1185
|
background: var(--c-blue-bg-h); background: color(display-p3 0 0.451 0.898 / 0.10); }
|
|
1107
|
-
.tl-scan-morph:active:not(.is-scanning):not(:disabled) { color: var(--c-blue-pressed); }
|
|
1186
|
+
.tl-scan-morph:active:not(.is-scanning):not(:disabled) { color: var(--c-blue-pressed); scale: 0.96; }
|
|
1108
1187
|
.tl-scan-morph:active:not(.is-scanning):not(:disabled)::before {
|
|
1109
1188
|
background: var(--c-blue-bg-h); background: color(display-p3 0 0.451 0.898 / 0.10); }
|
|
1110
1189
|
.tl-scan-morph:disabled:not(.is-scanning) { cursor: default; color: var(--c-blue); }
|
|
@@ -1114,12 +1193,12 @@
|
|
|
1114
1193
|
color: var(--c-blue-pressed); opacity: 0.5; }
|
|
1115
1194
|
.tl-scan-morph.is-scanning { width: 100%; height: 49px; border-radius: 60px;
|
|
1116
1195
|
padding: 8px 16px; cursor: default; color: #676767; background: transparent;
|
|
1117
|
-
box-shadow:
|
|
1196
|
+
box-shadow: var(--shadow-btn); }
|
|
1118
1197
|
.tl-scan-morph.is-scanning::before {
|
|
1119
1198
|
background: #fff;
|
|
1120
1199
|
transition: background var(--resize-dur) var(--resize-ease); }
|
|
1121
1200
|
.tl-scan-morph.is-scanning::after {
|
|
1122
|
-
box-shadow: inset
|
|
1201
|
+
box-shadow: var(--shadow-btn-inset);
|
|
1123
1202
|
transition: box-shadow var(--resize-dur) var(--resize-ease); }
|
|
1124
1203
|
/* Two faces share one grid cell and cross-blur (icon swap, 09) between the
|
|
1125
1204
|
label and the icon+status; whichever is hidden fades out with blur+scale.
|
|
@@ -1242,7 +1321,7 @@
|
|
|
1242
1321
|
font: inherit; transition: background 0.12s ease; }
|
|
1243
1322
|
.tl-mode-row:hover { background: rgba(170,170,170,0.08); }
|
|
1244
1323
|
.tl-mode-row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
1245
|
-
.tl-mode-row-title { font-size:
|
|
1324
|
+
.tl-mode-row-title { font-size: 13px; font-weight: 500; line-height: 16px; color: #1b1b1b; }
|
|
1246
1325
|
.tl-mode-row-desc { font-size: 12px; font-weight: 400; line-height: 18px; color: #676767; text-wrap: balance; }
|
|
1247
1326
|
.tl-mode-row-check { flex: 0 0 auto; width: 16px; height: 16px; display: flex; color: #17181c; }
|
|
1248
1327
|
@keyframes tl-fade-up { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
|
@@ -1289,10 +1368,15 @@
|
|
|
1289
1368
|
transition: background 0.12s ease, scale 0.12s ease, opacity 0.12s ease; }
|
|
1290
1369
|
.tl-pill-btn .tl-dim { color: #696969; }
|
|
1291
1370
|
.tl-pill-btn:hover:not(:disabled) { background: #fafafa; }
|
|
1292
|
-
.tl-pill-btn:active:not(:disabled) { scale: 0.
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
.tl-pill-btn
|
|
1371
|
+
.tl-pill-btn:active:not(:disabled) { scale: 0.96; }
|
|
1372
|
+
/* disabled — flat grey fill matching the active segmented tab, no raised
|
|
1373
|
+
shadow, muted text (instead of a faded white button). */
|
|
1374
|
+
.tl-pill-btn:disabled { cursor: not-allowed; opacity: 1; color: var(--c-disabled);
|
|
1375
|
+
background: rgba(170,170,170,0.1); box-shadow: none; }
|
|
1376
|
+
/* applied state — GRAY, never green (matches Accept icon --c-text #17181c).
|
|
1377
|
+
Flat grey fill matching the active segmented tab, no raised shadow. */
|
|
1378
|
+
.tl-pill-btn.is-applied { color: var(--c-text); cursor: not-allowed; opacity: 1;
|
|
1379
|
+
background: rgba(170,170,170,0.1); box-shadow: none; }
|
|
1296
1380
|
.tl-pill-btn.is-applied svg path { stroke: var(--c-text); }
|
|
1297
1381
|
/* Apply ⟷ Applied: a LEADING check is revealed first and pushes the text to
|
|
1298
1382
|
the right, then the differing SUFFIX cross-blurs. The shared stem "Appl"
|
|
@@ -1758,6 +1842,8 @@
|
|
|
1758
1842
|
// accept checkmark (Figma node 580:9057) — slimmer 1.5px check centered in a
|
|
1759
1843
|
// square 16-frame (the glyph spans ~50% width, like the source icon frame).
|
|
1760
1844
|
ICONS.accept = {vb:"0 0 16 16", svg:`<path d="M4 8.4268L6.46155 11.19223L12 4.97001" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`};
|
|
1845
|
+
// external-link (Figma "link-external-01") — box with an arrow leaving the top-right
|
|
1846
|
+
ICONS.linkout = {vb:"0 0 16 16", svg:`<path d="M9.5 3.5H12.5V6.5M12.5 3.5L7.5 8.5M11.5 9.25V11.5C11.5 12.0523 11.0523 12.5 10.5 12.5H4.5C3.94772 12.5 3.5 12.0523 3.5 11.5V5.5C3.5 4.94772 3.94772 4.5 4.5 4.5H6.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`};
|
|
1761
1847
|
function Ic({name, size=16}){
|
|
1762
1848
|
const ic = ICONS[name];
|
|
1763
1849
|
if(!ic) return null;
|
|
@@ -1860,41 +1946,147 @@
|
|
|
1860
1946
|
// page so the one dropdown surface resizes smoothly between pages. Lives
|
|
1861
1947
|
// inside the Dropdown portal, so its layout effect fires when the surface
|
|
1862
1948
|
// mounts (the parent Header doesn't re-render on the portal's own mount).
|
|
1863
|
-
function SettingsSlide({view,mainPage,shortcutsPage,onBack,onClose}){
|
|
1864
|
-
const slideRef=useRef(null), p1=useRef(null), p2=useRef(null);
|
|
1949
|
+
function SettingsSlide({view,mainPage,shortcutsPage,themePage,onBack,onClose}){
|
|
1950
|
+
const slideRef=useRef(null), p1=useRef(null), p2=useRef(null), p3=useRef(null);
|
|
1951
|
+
const pageNum=view==="shortcuts"?"2":view==="theme"?"3":"1";
|
|
1865
1952
|
useLayoutEffect(()=>{
|
|
1866
1953
|
const slide=slideRef.current; if(!slide) return;
|
|
1867
|
-
const active=view==="shortcuts"?p2.current:p1.current;
|
|
1954
|
+
const active=view==="shortcuts"?p2.current:view==="theme"?p3.current:p1.current;
|
|
1868
1955
|
if(active) slide.style.height=active.offsetHeight+"px";
|
|
1869
1956
|
});
|
|
1870
|
-
// Escape steps back from
|
|
1957
|
+
// Escape steps back from a sub-page first, then closes the dropdown —
|
|
1871
1958
|
// mirrors the picker's nested-dismiss behaviour.
|
|
1872
1959
|
useEffect(()=>{
|
|
1873
1960
|
const onKey=e=>{ if(e.key!=="Escape")return;
|
|
1874
|
-
if(view
|
|
1961
|
+
if(view!=="main"){onBack&&onBack();} else {onClose&&onClose();} };
|
|
1875
1962
|
document.addEventListener("keydown",onKey);
|
|
1876
1963
|
return()=>document.removeEventListener("keydown",onKey);
|
|
1877
1964
|
},[view,onBack,onClose]);
|
|
1878
|
-
return h("div",{ref:slideRef,className:"tl-set-slide t-page-slide","data-page":
|
|
1879
|
-
h("div",{ref:p1,className:"tl-set-page t-page","data-page-id":"1","aria-hidden":view
|
|
1880
|
-
h("div",{ref:p2,className:"tl-set-page t-page","data-page-id":"2","aria-hidden":view!=="shortcuts"},shortcutsPage)
|
|
1965
|
+
return h("div",{ref:slideRef,className:"tl-set-slide t-page-slide","data-page":pageNum},
|
|
1966
|
+
h("div",{ref:p1,className:"tl-set-page t-page","data-page-id":"1","aria-hidden":view!=="main"},mainPage),
|
|
1967
|
+
h("div",{ref:p2,className:"tl-set-page t-page","data-page-id":"2","aria-hidden":view!=="shortcuts"},shortcutsPage),
|
|
1968
|
+
h("div",{ref:p3,className:"tl-set-page t-page","data-page-id":"3","aria-hidden":view!=="theme"},themePage));
|
|
1881
1969
|
}
|
|
1882
1970
|
|
|
1883
1971
|
// ── refine relay client ──
|
|
1884
1972
|
// Talks to the local relay (server/relay.mjs). The browser posts a job and
|
|
1885
1973
|
// polls; the relay answers each job with one agent run (one-shot).
|
|
1886
1974
|
const RELAY_URL = (window.REFINE_RELAY_URL) || "http://localhost:7331";
|
|
1975
|
+
|
|
1976
|
+
// ── demo/mock agent (guarded — additive, OFF by default) ──
|
|
1977
|
+
// When `window.__TX_REFINE_MOCK` is set (the standalone website-demo.html),
|
|
1978
|
+
// the three relay fns below short-circuit to a built-in mock so the panel is
|
|
1979
|
+
// fully interactive with NO relay/LLM/CLI. When the flag is UNSET this whole
|
|
1980
|
+
// block is inert and every fn falls through to the real fetch path unchanged.
|
|
1981
|
+
const TX_MOCK = typeof window!=="undefined" && window.__TX_REFINE_MOCK;
|
|
1982
|
+
const _txMockJobs = new Map(); // id → { request, createdAt }
|
|
1983
|
+
const _txMockId = ()=>"mock-"+Date.now()+"-"+Math.random().toString(36).slice(2,8);
|
|
1984
|
+
const _txMs = (n)=>(n==null?"":Math.round(n)+"ms");
|
|
1985
|
+
const _TX_EASE = "cubic-bezier(0.22, 1, 0.36, 1)"; // transitions.dev standard ease-out
|
|
1986
|
+
// Build a "done" result shaped by the stored request kind (mirrors relay.mjs
|
|
1987
|
+
// parseScanOutput/parseApplyOutput/parseAgentOutput shapes).
|
|
1988
|
+
function _txMockResult(request){
|
|
1989
|
+
const kind = request && request.kind;
|
|
1990
|
+
if(kind==="scan") return { groups: _txMockGroups(request), summary: "Grouped 1 component — Menu dropdown (open + close)." };
|
|
1991
|
+
if(kind==="apply") return { applied: true, summary: "Demo mode — simulated writing the change to your source. (No files touched.)", files: [] };
|
|
1992
|
+
return _txMockSuggestions(request);
|
|
1993
|
+
}
|
|
1994
|
+
// scan → one "Menu dropdown" group with Open + Close phases. Members are
|
|
1995
|
+
// derived from the raw DOM-detected transitions so the timeline shows real
|
|
1996
|
+
// lanes; selectors come straight from the live scan so they resolve in the
|
|
1997
|
+
// DOM. Shape mirrors relay.mjs buildScanPrompt's groups[] (id/label/phases →
|
|
1998
|
+
// phase{id,phase,label,stateTarget,fromState,toState,members[{id,label,
|
|
1999
|
+
// selector,propertyTimings}]}) and what TransitionRegistry.setGroups expects.
|
|
2000
|
+
function _txMockGroups(request){
|
|
2001
|
+
const raw = (request && request.raw) || [];
|
|
2002
|
+
const base = raw.length ? raw : [{label:"Menu", selector:".wd-dd .wd-dd-menu", properties:["opacity","transform"]}];
|
|
2003
|
+
// OPEN phase matches the current DOM → copy the scanned timings verbatim
|
|
2004
|
+
// (so the timeline shows the element's real, possibly off-token values).
|
|
2005
|
+
const open = base.map((e,i)=>({
|
|
2006
|
+
id:"m"+i,
|
|
2007
|
+
label:e.label||("Element "+(i+1)),
|
|
2008
|
+
selector:e.selector||".wd-dd .wd-dd-menu",
|
|
2009
|
+
propertyTimings:((e.timings&&e.timings.length)?e.timings:(e.properties||["opacity","transform"]).map(p=>({property:p,durationMs:280,delayMs:0,easing:"ease"})))
|
|
2010
|
+
.map(t=>({property:t.property, durationMs:t.durationMs!=null?t.durationMs:280, delayMs:t.delayMs||0, easing:t.easing||"ease"})),
|
|
2011
|
+
}));
|
|
2012
|
+
// CLOSE phase lives on a state variant the DOM scan can't see; the recipe's
|
|
2013
|
+
// faster close (the authored 180ms) is recovered here, mirroring how the
|
|
2014
|
+
// real agent reads the opposite-phase timings from cssRules.
|
|
2015
|
+
const close = open.map(m=>({ ...m, propertyTimings:m.propertyTimings.map(t=>({property:t.property, durationMs:180, delayMs:0, easing:"ease"})) }));
|
|
2016
|
+
const phases=[
|
|
2017
|
+
{id:"menu-dropdown:open", phase:"open", label:"Open", stateTarget:".wd-dd", fromState:null, toState:".is-open", members:open},
|
|
2018
|
+
{id:"menu-dropdown:close",phase:"close", label:"Close", stateTarget:".wd-dd", fromState:".is-open", toState:null, members:close},
|
|
2019
|
+
];
|
|
2020
|
+
return [{ id:"menu-dropdown", label:"Menu dropdown", component:null, phases }];
|
|
2021
|
+
}
|
|
2022
|
+
// refine → deterministic, transitions.dev-token-aligned suggestions. patches
|
|
2023
|
+
// derive property/member from request.timings so Apply changes a real lane.
|
|
2024
|
+
// Shape mirrors relay.mjs's suggestion contract (id/kind/property/title/from/
|
|
2025
|
+
// to/patch/reason, + reference on the replace card).
|
|
2026
|
+
function _txMockSuggestions(request){
|
|
2027
|
+
const t = (request && request.timings) || [];
|
|
2028
|
+
const p0 = (t[0]&&t[0].property) || "opacity";
|
|
2029
|
+
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
|
+
const mp = (member, rest)=> member ? {member, ...rest} : rest;
|
|
2035
|
+
const suggestions=[
|
|
2036
|
+
{ id:"mock-dur-fast", kind:"duration", property:p0, member:m0,
|
|
2037
|
+
title:"Duration → Fast", from:from0, to:"250ms",
|
|
2038
|
+
patch:mp(m0,{property:p0, durationMs:250}),
|
|
2039
|
+
reason:"Open motion snaps to the 250ms “medium” token — standard UI motion on transitions.dev." },
|
|
2040
|
+
{ id:"mock-dur-quick", kind:"duration", property:p1, member:m1,
|
|
2041
|
+
title:"Duration → Quick", from:_txMs(t[1]&&t[1].durationMs)||from0, to:"150ms",
|
|
2042
|
+
patch:mp(m1,{property:p1, durationMs:150}),
|
|
2043
|
+
reason:"Closing feels crisper at the 150ms “fast” token — quick state changes." },
|
|
2044
|
+
{ id:"mock-ease-smooth", kind:"easing", property:p0, member:m0,
|
|
2045
|
+
title:"Easing → Smooth ease out", from:fromE, to:_TX_EASE,
|
|
2046
|
+
patch:mp(m0,{property:p0, easing:_TX_EASE}),
|
|
2047
|
+
reason:"transitions.dev standard ease-out — a gentle decelerate to rest." },
|
|
2048
|
+
{ id:"mock-replace-menu", kind:"replace", property:p0, member:m0,
|
|
2049
|
+
title:"Menu dropdown", reference:"05-menu-dropdown.md",
|
|
2050
|
+
patch:mp(m0,{property:"all", durationMs:250, easing:_TX_EASE}),
|
|
2051
|
+
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)." },
|
|
2052
|
+
];
|
|
2053
|
+
return { suggestions, summary:"3 motion-token tweaks and 1 transitions.dev recipe match for the Menu dropdown." };
|
|
2054
|
+
}
|
|
2055
|
+
// statusLog so the panel's shimmer status copy has something to show.
|
|
2056
|
+
function _txMockLog(request, createdAt){
|
|
2057
|
+
const kind = request && request.kind;
|
|
2058
|
+
const head = kind==="scan" ? "Grouping transitions from your source…"
|
|
2059
|
+
: kind==="apply" ? "Writing “"+((request&&request.label)||"transition")+"” to your code…"
|
|
2060
|
+
: "Scanning “"+((request&&request.label)||"transition")+"”…";
|
|
2061
|
+
return [{message:head, at:createdAt}, {message:"Reading the Transitions.dev skill…", at:createdAt+350}];
|
|
2062
|
+
}
|
|
2063
|
+
|
|
1887
2064
|
async function relayCreateJob(request){
|
|
2065
|
+
if(TX_MOCK){
|
|
2066
|
+
const id=_txMockId();
|
|
2067
|
+
_txMockJobs.set(id,{request,createdAt:Date.now()});
|
|
2068
|
+
return { id, status:"pending" };
|
|
2069
|
+
}
|
|
1888
2070
|
const r = await fetch(RELAY_URL+"/jobs",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({request})});
|
|
1889
2071
|
if(!r.ok) throw new Error("relay POST /jobs failed ("+r.status+")");
|
|
1890
2072
|
return r.json(); // { id, status }
|
|
1891
2073
|
}
|
|
1892
2074
|
async function relayGetJob(id){
|
|
2075
|
+
if(TX_MOCK){
|
|
2076
|
+
const rec=_txMockJobs.get(id);
|
|
2077
|
+
if(!rec) return { id, status:"error", statusLog:[], result:null, error:"mock: unknown job" };
|
|
2078
|
+
const elapsed=Date.now()-rec.createdAt;
|
|
2079
|
+
const log=_txMockLog(rec.request, rec.createdAt);
|
|
2080
|
+
// ~800ms simulated work: the first poll(s) see "running", then "done".
|
|
2081
|
+
if(elapsed<800) return { id, status:"running", statusLog:[log[0]], result:null, error:null };
|
|
2082
|
+
return { id, status:"done", statusLog:log, result:_txMockResult(rec.request), error:null };
|
|
2083
|
+
}
|
|
1893
2084
|
const r = await fetch(RELAY_URL+"/jobs/"+id);
|
|
1894
2085
|
if(!r.ok) throw new Error("relay GET /jobs/:id failed ("+r.status+")");
|
|
1895
2086
|
return r.json();
|
|
1896
2087
|
}
|
|
1897
2088
|
async function relayHealth(){
|
|
2089
|
+
if(TX_MOCK) return { ok:true, auto:true, llmAvailable:true, cliInstalled:true, jobs:0 };
|
|
1898
2090
|
const r = await fetch(RELAY_URL+"/health");
|
|
1899
2091
|
if(!r.ok) throw new Error("relay /health failed ("+r.status+")");
|
|
1900
2092
|
return r.json(); // { ok, auto, llmAvailable, jobs }
|
|
@@ -2012,14 +2204,21 @@
|
|
|
2012
2204
|
h("span",{className:"tl-apply-seg tl-apply-y"},"y"),
|
|
2013
2205
|
h("span",{className:"tl-apply-seg tl-apply-ied"},"ied"))));
|
|
2014
2206
|
}
|
|
2015
|
-
function RefineResults({suggestions,appliedIds,onApply,lanes}){
|
|
2207
|
+
function RefineResults({suggestions,appliedIds,onApply,lanes,revealedRef,active=true}){
|
|
2016
2208
|
const ref=useRef(null);
|
|
2017
2209
|
useEffect(()=>{
|
|
2018
2210
|
const el=ref.current;if(!el)return;
|
|
2211
|
+
// Both type tabs keep their results mounted so they can cross-slide
|
|
2212
|
+
// (page-side-by-side). Only the visible page plays the staggered reveal,
|
|
2213
|
+
// and only once per scan; the hidden page just shows its final state so
|
|
2214
|
+
// switching tabs slides finished content in without re-staggering.
|
|
2215
|
+
if(!active){ el.classList.add("is-shown"); return; }
|
|
2216
|
+
if(revealedRef&&revealedRef.current){ el.classList.add("is-shown"); return; }
|
|
2019
2217
|
el.classList.remove("is-shown");
|
|
2020
2218
|
void el.offsetHeight; // reflow from the start state before playing
|
|
2021
2219
|
el.classList.add("is-shown");
|
|
2022
|
-
|
|
2220
|
+
if(revealedRef) revealedRef.current=true;
|
|
2221
|
+
},[active]);
|
|
2023
2222
|
const delay=(i)=>({transitionDelay:"calc(var(--stagger-stagger) * "+i+")"});
|
|
2024
2223
|
// category chip label + color modifier, keyed by the suggestion kind
|
|
2025
2224
|
const CAT={easing:["Easing","is-easing"],duration:["Duration","is-duration"],
|
|
@@ -2067,6 +2266,11 @@
|
|
|
2067
2266
|
const[modeOpen,setModeOpen]=useState(false);
|
|
2068
2267
|
const[statusIx,setStatusIx]=useState(0);
|
|
2069
2268
|
const modeRef=useRef(null);
|
|
2269
|
+
// The results reveal should play once per scan, not on every tab switch.
|
|
2270
|
+
// This persists across RefineResults remounts; reset it when a new scan
|
|
2271
|
+
// starts (or the panel returns to idle) so fresh results animate again.
|
|
2272
|
+
const revealedRef=useRef(false);
|
|
2273
|
+
useEffect(()=>{ if(phase==="scanning"||phase==="idle") revealedRef.current=false; },[phase]);
|
|
2070
2274
|
useEffect(()=>{
|
|
2071
2275
|
let raf, to;
|
|
2072
2276
|
if(open){
|
|
@@ -2107,14 +2311,11 @@
|
|
|
2107
2311
|
// null (unknown / still probing) is treated as ready so the panel doesn't
|
|
2108
2312
|
// flash the "unavailable" copy before /health resolves.
|
|
2109
2313
|
const agentReady = !agentMode || llmAvailable!==false;
|
|
2110
|
-
const typeDesc = (REFINE_TYPES.find(t=>t.key===refineType)||REFINE_TYPES[0]).desc;
|
|
2111
2314
|
const modeLabel = (REFINE_MODES.find(m=>m.key===mode)||REFINE_MODES[0]).label;
|
|
2112
|
-
// Idle empty-state copy is mode-aware
|
|
2113
|
-
// agent-oriented desc; Deterministic has no
|
|
2114
|
-
// math-snap behavior (and that "replace" needs
|
|
2115
|
-
|
|
2116
|
-
? "Deterministic mode can't pick a recipe to replace the transition — switch to Agent mode for replacements."
|
|
2117
|
-
: "Deterministic mode snaps your transition's timing to the nearest Transitions.dev motion tokens — mathematically, with no agent or credits.");
|
|
2315
|
+
// Idle empty-state copy is mode-aware (computed per type in bodyFor):
|
|
2316
|
+
// Agent (llm) keeps the type's agent-oriented desc; Deterministic has no
|
|
2317
|
+
// agent, so it describes the math-snap behavior (and that "replace" needs
|
|
2318
|
+
// Agent mode).
|
|
2118
2319
|
// One persistent control for the whole foot: in idle/done/error it's the
|
|
2119
2320
|
// pill button; while scanning it carries `.is-scanning` and the same DOM
|
|
2120
2321
|
// node morphs (card resize) into the loading rectangle with the border-beam.
|
|
@@ -2135,25 +2336,58 @@
|
|
|
2135
2336
|
h(ShimmerSwapText,{text:REFINE_STATUS[statusIx]}))));
|
|
2136
2337
|
};
|
|
2137
2338
|
|
|
2138
|
-
|
|
2339
|
+
// Body content for a given refine type. Both type tabs render their own
|
|
2340
|
+
// body so they can cross-slide (page-side-by-side). Phase is global, so
|
|
2341
|
+
// only the type-specific bits (filtered suggestions, empty/idle copy)
|
|
2342
|
+
// change per page. `isActive` tells the results list whether to play its
|
|
2343
|
+
// reveal (only the visible page does).
|
|
2344
|
+
const bodyFor=(rt,isActive)=>{
|
|
2345
|
+
const vis = suggestions.filter(s=> rt==="replace" ? s.kind==="replace" : s.kind!=="replace");
|
|
2346
|
+
if(phase==="scanning"){
|
|
2347
|
+
return h("div",{className:"tl-refine-center"});
|
|
2348
|
+
}
|
|
2349
|
+
if(phase==="error"){
|
|
2350
|
+
return h("div",{className:"tl-refine-center"},
|
|
2351
|
+
h("div",{className:"tl-refine-unavail-title"},"Something went wrong."),
|
|
2352
|
+
h("div",{className:"tl-refine-unavail-text"},error||"The agent reported an error."));
|
|
2353
|
+
}
|
|
2354
|
+
if(phase==="done"){
|
|
2355
|
+
if(vis.length===0){
|
|
2356
|
+
const emptyMsg = rt==="replace"
|
|
2357
|
+
? "I didn't find any transition that would be a good fit as a replacement."
|
|
2358
|
+
: "Already aligned to the transitions.dev motion tokens. Nothing to refine.";
|
|
2359
|
+
return h("div",{className:"tl-refine-center"},
|
|
2360
|
+
h("p",{className:"tl-refine-idle-text"},emptyMsg));
|
|
2361
|
+
}
|
|
2362
|
+
return h(RefineResults,{suggestions:vis,appliedIds,onApply,lanes:lanes||[],revealedRef,active:isActive});
|
|
2363
|
+
}
|
|
2364
|
+
// idle
|
|
2365
|
+
if(!agentReady){
|
|
2366
|
+
const cliMissing = cliInstalled===false;
|
|
2367
|
+
return h("div",{className:"tl-refine-center"},
|
|
2368
|
+
h("div",{className:"tl-refine-unavail-title"},"Agent unavailable."),
|
|
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."));
|
|
2372
|
+
}
|
|
2373
|
+
const idleDescR = agentMode ? (REFINE_TYPES.find(t=>t.key===rt)||REFINE_TYPES[0]).desc : (rt==="replace"
|
|
2374
|
+
? "Deterministic mode can't pick a recipe to replace the transition — switch to Agent mode for replacements."
|
|
2375
|
+
: "Deterministic mode snaps your transition's timing to the nearest Transitions.dev motion tokens — mathematically, with no agent or credits.");
|
|
2376
|
+
return h("div",{className:"tl-refine-center"},
|
|
2377
|
+
h("p",{className:"tl-refine-idle-text"},idleDescR));
|
|
2378
|
+
};
|
|
2379
|
+
|
|
2380
|
+
// Foot tracks the active type (it lives below the sliding body, so it
|
|
2381
|
+
// just updates in place on a tab switch).
|
|
2382
|
+
let foot, footBar=false;
|
|
2139
2383
|
if(phase==="scanning"){
|
|
2140
|
-
body = h("div",{className:"tl-refine-center"});
|
|
2141
2384
|
foot = startBtn();
|
|
2142
2385
|
} else if(phase==="error"){
|
|
2143
|
-
body = h("div",{className:"tl-refine-center"},
|
|
2144
|
-
h("div",{className:"tl-refine-unavail-title"},"Something went wrong."),
|
|
2145
|
-
h("div",{className:"tl-refine-unavail-text"},error||"The agent reported an error."));
|
|
2146
2386
|
foot = startBtn({label:"Try again"});
|
|
2147
2387
|
} else if(phase==="done"){
|
|
2148
2388
|
if(visible.length===0){
|
|
2149
|
-
const emptyMsg = refineType==="replace"
|
|
2150
|
-
? "I didn't find any transition that would be a good fit as a replacement."
|
|
2151
|
-
: "Already aligned to the transitions.dev motion tokens. Nothing to refine.";
|
|
2152
|
-
body = h("div",{className:"tl-refine-center"},
|
|
2153
|
-
h("p",{className:"tl-refine-idle-text"},emptyMsg));
|
|
2154
2389
|
foot = startBtn({label:"Scan again"});
|
|
2155
2390
|
} else {
|
|
2156
|
-
body = h(RefineResults,{suggestions:visible,appliedIds,onApply,lanes:lanes||[]});
|
|
2157
2391
|
footBar = true;
|
|
2158
2392
|
foot = h(React.Fragment,null,
|
|
2159
2393
|
h("button",{className:"tl-pill-btn",disabled:pending.length===0,
|
|
@@ -2162,19 +2396,7 @@
|
|
|
2162
2396
|
h("button",{className:"tl-pill-btn",onClick:onStart},"Scan again"));
|
|
2163
2397
|
}
|
|
2164
2398
|
} else { // idle
|
|
2165
|
-
|
|
2166
|
-
const cliMissing = cliInstalled===false;
|
|
2167
|
-
body = h("div",{className:"tl-refine-center"},
|
|
2168
|
-
h("div",{className:"tl-refine-unavail-title"},"Agent unavailable."),
|
|
2169
|
-
cliMissing
|
|
2170
|
-
? 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.")
|
|
2171
|
-
: h("p",{className:"tl-refine-unavail-text"},"Run the ",h("code",{className:"tl-code"},"/refine live")," command in your agent to enable live functionality."));
|
|
2172
|
-
foot = startBtn({disabled:true});
|
|
2173
|
-
} else {
|
|
2174
|
-
body = h("div",{className:"tl-refine-center"},
|
|
2175
|
-
h("p",{className:"tl-refine-idle-text"},idleDesc));
|
|
2176
|
-
foot = startBtn({});
|
|
2177
|
-
}
|
|
2399
|
+
foot = startBtn(agentReady?{}:{disabled:true});
|
|
2178
2400
|
}
|
|
2179
2401
|
|
|
2180
2402
|
return h("div",{className:"tl-refine-panel","data-open":panelOpen?"true":"false","data-phase":slidePhase,
|
|
@@ -2202,7 +2424,11 @@
|
|
|
2202
2424
|
REFINE_TYPES.map(t=>h("button",{key:t.key,className:"tl-refine-tab",role:"tab",
|
|
2203
2425
|
"aria-selected":refineType===t.key?"true":"false",
|
|
2204
2426
|
onClick:()=>{if(t.key!==refineType)onType(t.key);}},t.label))),
|
|
2205
|
-
h("div",{className:"tl-refine-body"},
|
|
2427
|
+
h("div",{className:"tl-refine-body tl-refine-pages t-page-slide","data-page":refineType==="replace"?"2":"1"},
|
|
2428
|
+
h("div",{className:"tl-refine-page t-page","data-page-id":"1","aria-hidden":refineType!=="small"},
|
|
2429
|
+
bodyFor("small",refineType==="small")),
|
|
2430
|
+
h("div",{className:"tl-refine-page t-page","data-page-id":"2","aria-hidden":refineType!=="replace"},
|
|
2431
|
+
bodyFor("replace",refineType==="replace"))),
|
|
2206
2432
|
h("div",{className:cx("tl-refine-foot",footBar&&"is-bar")},foot)));
|
|
2207
2433
|
}
|
|
2208
2434
|
|
|
@@ -2230,6 +2456,9 @@
|
|
|
2230
2456
|
const[setg,setSetg]=useState(false);
|
|
2231
2457
|
// settings dropdown sub-page (main menu ↔ keyboard-shortcuts cheat sheet)
|
|
2232
2458
|
const[settingsView,setSettingsView]=useState("main"); // "main" | "shortcuts"
|
|
2459
|
+
// theme preference — UI only for now (dark theme not wired up yet)
|
|
2460
|
+
const[themePref,setThemePref]=useState("system"); // "light" | "dark" | "system"
|
|
2461
|
+
const THEME_OPTS=[{key:"light",label:"Light"},{key:"dark",label:"Dark"},{key:"system",label:"System"}];
|
|
2233
2462
|
const isMac=typeof navigator!=="undefined"&&/mac/i.test((navigator.userAgentData&&navigator.userAgentData.platform)||navigator.platform||navigator.userAgent||"");
|
|
2234
2463
|
const modKey=isMac?"Cmd":"Ctrl";
|
|
2235
2464
|
// small gray shortcut hint appended inside a tooltip (e.g. "⌘A")
|
|
@@ -2308,23 +2537,38 @@
|
|
|
2308
2537
|
h(SettingsSlide,{view:settingsView,
|
|
2309
2538
|
onBack:()=>setSettingsView("main"),
|
|
2310
2539
|
onClose:()=>{setSetg(false);setSettingsView("main");},
|
|
2311
|
-
// ── main settings page ──
|
|
2540
|
+
// ── main settings page (Figma 580:4723) ──
|
|
2312
2541
|
mainPage:h(React.Fragment,null,
|
|
2313
|
-
h("div",{className:"tl-menu-head"},
|
|
2314
|
-
h("span",null,"Transitions.dev ",h("span",{className:"tl-menu-head-name"},"Refine")),
|
|
2315
|
-
h("span",{className:"tl-menu-head-ver"},REFINE_VERSION)),
|
|
2316
|
-
h("div",{className:"tl-menu-divider"}),
|
|
2317
2542
|
h(MenuItem,{disabled:!active,onClick:()=>{onCopy&&onCopy();setSetg(false);},
|
|
2318
2543
|
left:h("span",{className:"tl-menu-icon"},h(Ic,{name:"copy"})),
|
|
2319
2544
|
right:copied&&h("span",{className:"tl-menu-check"},h(Ic,{name:"accept"}))},copied?"Copied":"Copy values"),
|
|
2320
2545
|
h(MenuItem,{disabled:scanning,onClick:()=>{setSetg(false);onRescan&&onRescan();},
|
|
2321
2546
|
left:h("span",{className:"tl-menu-icon"},h(Ic,{name:"restart"}))},
|
|
2322
2547
|
scanning?"Rescanning…":"Rescan transitions"),
|
|
2548
|
+
h("div",{className:"tl-menu-divider"}),
|
|
2549
|
+
// theme picker row — shows the current value + chevron; opens the sub-page
|
|
2550
|
+
h(MenuItem,{onClick:()=>setSettingsView("theme"),
|
|
2551
|
+
right:h("span",{className:"tl-menu-trail"},
|
|
2552
|
+
h("span",{className:"tl-menu-value"},(THEME_OPTS.find(o=>o.key===themePref)||{}).label),
|
|
2553
|
+
h("span",{className:"tl-menu-chevr"},h(Ic,{name:"chevronr",size:16})))},"Theme"),
|
|
2323
2554
|
h(MenuItem,{onClick:()=>setSettingsView("shortcuts"),
|
|
2324
2555
|
right:h("span",{className:"tl-menu-chevr"},h(Ic,{name:"chevronr",size:16}))},"Keyboard shortcuts"),
|
|
2325
|
-
h("div",{className:"tl-menu-divider"}),
|
|
2326
2556
|
h(MenuItem,{onClick:()=>{setSetg(false);window.open("https://transitions.dev","_blank","noopener");},
|
|
2327
|
-
right:h("span",{className:"tl-menu-chevr"},h(Ic,{name:"
|
|
2557
|
+
right:h("span",{className:"tl-menu-chevr"},h(Ic,{name:"linkout",size:16}))},"Send feedback"),
|
|
2558
|
+
h(MenuItem,{onClick:()=>{setSetg(false);window.open("https://transitions.dev","_blank","noopener");},
|
|
2559
|
+
right:h("span",{className:"tl-menu-chevr"},h(Ic,{name:"linkout",size:16}))},"Learn more"),
|
|
2560
|
+
h("div",{className:"tl-menu-divider"}),
|
|
2561
|
+
h("div",{className:"tl-menu-head"},
|
|
2562
|
+
h("span",null,"Transitions.dev ",h("span",{className:"tl-menu-head-name"},"Refine")),
|
|
2563
|
+
h("span",{className:"tl-menu-head-ver"},REFINE_VERSION))),
|
|
2564
|
+
// ── theme sub-page (Light / Dark / System — System default) ──
|
|
2565
|
+
themePage:h(React.Fragment,null,
|
|
2566
|
+
h("button",{type:"button",className:"tl-set-back",onClick:()=>setSettingsView("main")},
|
|
2567
|
+
h("span",{className:"tl-set-back-ic"},h(Ic,{name:"chevronr",size:16})),
|
|
2568
|
+
h("span",{className:"tl-set-back-title"},"Theme")),
|
|
2569
|
+
h("div",{className:"tl-menu-divider"}),
|
|
2570
|
+
...THEME_OPTS.map(o=>h(MenuItem,{key:o.key,onClick:()=>{setThemePref(o.key);setSettingsView("main");},
|
|
2571
|
+
right:themePref===o.key&&h("span",{className:"tl-menu-check"},h(Ic,{name:"accept"}))},o.label))),
|
|
2328
2572
|
// ── keyboard-shortcuts sub-page ──
|
|
2329
2573
|
shortcutsPage:h(React.Fragment,null,
|
|
2330
2574
|
h("button",{type:"button",className:"tl-set-back",onClick:()=>setSettingsView("main")},
|
|
@@ -2371,35 +2615,54 @@
|
|
|
2371
2615
|
h("span",{className:"t-tt tl-tt-below",role:"tooltip"},"Refine",ttHint("R"))),
|
|
2372
2616
|
h("span",{className:"t-tt-wrap"},
|
|
2373
2617
|
h("button",{className:"tl-icon-btn ghost t-tt-trigger","aria-label":"Minimize",onClick:onMinimize},h(Ic,{name:"minimize"})),
|
|
2374
|
-
h("span",{className:"t-tt tl-tt-below",role:"tooltip"},"Minimize",ttHint("."))),
|
|
2618
|
+
h("span",{className:"t-tt tl-tt-below tl-tt-right",role:"tooltip"},"Minimize",ttHint("."))),
|
|
2375
2619
|
);
|
|
2376
2620
|
}
|
|
2377
2621
|
|
|
2378
2622
|
function PropTrack({property, member, delayMs, durationMs, selected, onSelect, onDelayChange, onDurationChange, snap, scaleMs, lockDuration, labelW}){
|
|
2379
2623
|
const trackRef=useRef(null);
|
|
2380
|
-
const
|
|
2381
|
-
|
|
2624
|
+
const [dragging,setDragging]=useState(false);
|
|
2625
|
+
// 1ms resolution while dragging so the bar moves smoothly; the snap grid
|
|
2626
|
+
// (25ms) is applied only on release when `snap` is on.
|
|
2627
|
+
const SNAP_GRID = 25;
|
|
2628
|
+
const pxToMs=useCallback((px,g)=>{if(!trackRef.current)return 0;const w=trackRef.current.getBoundingClientRect().width;const gg=g||1;return Math.round((px/w)*scaleMs/gg)*gg;},[scaleMs]);
|
|
2382
2629
|
|
|
2383
2630
|
const startDrag=useCallback((mode,e)=>{
|
|
2384
2631
|
e.preventDefault(); e.stopPropagation();
|
|
2385
2632
|
onSelect();
|
|
2386
2633
|
// a spring controls its own duration, so its bar can be moved (delay) but not resized
|
|
2387
2634
|
if(lockDuration && mode!=="move") return;
|
|
2635
|
+
setDragging(true);
|
|
2388
2636
|
const startX=e.clientX; const sd=delayMs; const sdur=durationMs;
|
|
2637
|
+
let lastDel=sd, lastDur=sdur;
|
|
2389
2638
|
const onMove=e2=>{
|
|
2390
|
-
const dx=e2.clientX-startX; const dMs=pxToMs(dx);
|
|
2639
|
+
const dx=e2.clientX-startX; const dMs=pxToMs(dx,1); // smooth: 1ms steps
|
|
2391
2640
|
if(mode==="move"){
|
|
2392
|
-
|
|
2641
|
+
lastDel=Math.max(0,Math.min(sd+dMs,scaleMs-sdur));
|
|
2642
|
+
onDelayChange(lastDel);
|
|
2393
2643
|
} else if(mode==="left"){
|
|
2394
|
-
const rawDel=sd+dMs; const clampedDel=Math.max(0,Math.min(rawDel,sd+sdur-
|
|
2395
|
-
|
|
2644
|
+
const rawDel=sd+dMs; const clampedDel=Math.max(0,Math.min(rawDel,sd+sdur-1));
|
|
2645
|
+
lastDel=clampedDel; lastDur=sdur-(clampedDel-sd);
|
|
2646
|
+
onDelayChange(lastDel); onDurationChange(lastDur);
|
|
2396
2647
|
} else {
|
|
2397
|
-
|
|
2648
|
+
lastDur=Math.max(1,Math.min(sdur+dMs,scaleMs-sd));
|
|
2649
|
+
onDurationChange(lastDur);
|
|
2398
2650
|
}
|
|
2399
2651
|
};
|
|
2400
|
-
const onUp=()=>{
|
|
2652
|
+
const onUp=()=>{
|
|
2653
|
+
setDragging(false);
|
|
2654
|
+
// snap to the grid once, on release (bar tweens there since the
|
|
2655
|
+
// is-dragging transition lock is now removed).
|
|
2656
|
+
if(snap){
|
|
2657
|
+
const g=SNAP_GRID, sn=v=>Math.round(v/g)*g;
|
|
2658
|
+
if(mode==="move"){ onDelayChange(Math.max(0,Math.min(sn(lastDel),scaleMs-sdur))); }
|
|
2659
|
+
else if(mode==="left"){ onDelayChange(Math.max(0,sn(lastDel))); onDurationChange(Math.max(g,sn(lastDur))); }
|
|
2660
|
+
else { onDurationChange(Math.max(g,Math.min(sn(lastDur),scaleMs-sd))); }
|
|
2661
|
+
}
|
|
2662
|
+
window.removeEventListener("mousemove",onMove);window.removeEventListener("mouseup",onUp);
|
|
2663
|
+
};
|
|
2401
2664
|
window.addEventListener("mousemove",onMove);window.addEventListener("mouseup",onUp);
|
|
2402
|
-
},[delayMs,durationMs,pxToMs,
|
|
2665
|
+
},[delayMs,durationMs,pxToMs,snap,scaleMs,onSelect,onDelayChange,onDurationChange,lockDuration]);
|
|
2403
2666
|
|
|
2404
2667
|
const delPct=(delayMs/scaleMs)*100; const durPct=(durationMs/scaleMs)*100;
|
|
2405
2668
|
return h("div",{className:cx("tl-prop-row",selected&&"selected",lockDuration&&"is-spring"),onClick:onSelect},
|
|
@@ -2408,7 +2671,7 @@
|
|
|
2408
2671
|
member&&h("span",{className:"tl-prop-member"},member),
|
|
2409
2672
|
h("span",{className:"tl-prop-prop"},property))),
|
|
2410
2673
|
h("div",{className:"tl-prop-track",ref:trackRef},
|
|
2411
|
-
h("div",{className:"tl-bar",style:{left:delPct+"%",width:durPct+"%"},onMouseDown:e=>startDrag("move",e),
|
|
2674
|
+
h("div",{className:cx("tl-bar",dragging&&"is-dragging"),style:{left:delPct+"%",width:durPct+"%"},onMouseDown:e=>startDrag("move",e),
|
|
2412
2675
|
title:lockDuration?"Spring duration is derived \u2014 drag to move, resize is locked":undefined},
|
|
2413
2676
|
h("span",{className:"tl-bar-handle left"}),
|
|
2414
2677
|
h("span",{className:"tl-bar-handle right"}),
|
|
@@ -2428,7 +2691,8 @@
|
|
|
2428
2691
|
const[dragging,setDragging]=useState(false);
|
|
2429
2692
|
const[menu,setMenu]=useState(false);
|
|
2430
2693
|
const pct = Math.min(Math.max((value - min) / (max - min), 0), 1) * 100;
|
|
2431
|
-
|
|
2694
|
+
// fine grid while dragging (smooth); the snap `step` is applied on release.
|
|
2695
|
+
const dragGrid = Math.max(1, Math.round(step/5));
|
|
2432
2696
|
const selectAll = useCallback(e=>{
|
|
2433
2697
|
const el = e && e.target ? e.target : inputRef.current;
|
|
2434
2698
|
if(!el) return;
|
|
@@ -2436,25 +2700,27 @@
|
|
|
2436
2700
|
requestAnimationFrame(()=>el.select());
|
|
2437
2701
|
},[]);
|
|
2438
2702
|
|
|
2439
|
-
const setFromX = useCallback(clientX => {
|
|
2703
|
+
const setFromX = useCallback((clientX, g) => {
|
|
2440
2704
|
if(!trackRef.current) return;
|
|
2441
2705
|
const rect = trackRef.current.getBoundingClientRect();
|
|
2442
2706
|
const ratio = Math.max(0, Math.min((clientX - rect.left) / rect.width, 1));
|
|
2443
2707
|
const raw = min + ratio * (max - min);
|
|
2444
|
-
const
|
|
2708
|
+
const gg = g || 1;
|
|
2709
|
+
const snapped = Math.round(raw / gg) * gg;
|
|
2445
2710
|
onChange(Math.max(min, Math.min(max, snapped)));
|
|
2446
|
-
},[min,max,
|
|
2711
|
+
},[min,max,onChange]);
|
|
2447
2712
|
|
|
2448
2713
|
const startDrag = useCallback(e=>{
|
|
2449
2714
|
if(editing || readOnly) return;
|
|
2450
2715
|
e.preventDefault();
|
|
2451
2716
|
setDragging(true);
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
const
|
|
2717
|
+
let lastX = e.clientX;
|
|
2718
|
+
setFromX(e.clientX, dragGrid); // smooth fine steps during drag
|
|
2719
|
+
const onMove = e2 => { lastX = e2.clientX; setFromX(e2.clientX, dragGrid); };
|
|
2720
|
+
const onUp = () => { setDragging(false); if(snap) setFromX(lastX, step); window.removeEventListener("mousemove",onMove); window.removeEventListener("mouseup",onUp); };
|
|
2455
2721
|
window.addEventListener("mousemove",onMove);
|
|
2456
2722
|
window.addEventListener("mouseup",onUp);
|
|
2457
|
-
},[setFromX,editing]);
|
|
2723
|
+
},[setFromX,editing,readOnly,snap,step,dragGrid]);
|
|
2458
2724
|
|
|
2459
2725
|
const beginEdit = useCallback(e=>{ e.stopPropagation(); if(readOnly) return; setDraft(String(value)); setEditing(true); },[value,readOnly]);
|
|
2460
2726
|
const commit = useCallback(()=>{ const n=parseFloat(draft); if(!isNaN(n)) onChange(Math.max(min,Math.min(max,Math.round(n)))); setEditing(false); },[draft,min,max,onChange]);
|
|
@@ -3051,35 +3317,76 @@
|
|
|
3051
3317
|
refreshHealth();
|
|
3052
3318
|
},[active,refineOpen,refreshHealth]);
|
|
3053
3319
|
// "Start scanning" resolves availability first, then posts the job.
|
|
3054
|
-
|
|
3320
|
+
// per-(transition,type) result cache so switching tabs reuses a prior scan
|
|
3321
|
+
// instead of re-asking the agent. In llm mode each scan is scoped to the
|
|
3322
|
+
// active tab (Small = token tweaks, Replace = recipe) so the common Small
|
|
3323
|
+
// view skips the recipe selection + reference-file read. Deterministic stays
|
|
3324
|
+
// a single instant "both" pass that feeds both tabs.
|
|
3325
|
+
const refineCacheRef=useRef(new Map()); // `${id}::${type}` -> {suggestions,summary}
|
|
3326
|
+
const refineScanTypeRef=useRef("small"); // which type the in-flight job targets
|
|
3327
|
+
// results are not comparable across transitions — drop the cache when the
|
|
3328
|
+
// target transition changes.
|
|
3329
|
+
useEffect(()=>{refineCacheRef.current.clear();},[active&&active.id]);
|
|
3330
|
+
// recompute the displayed suggestions from whatever types are cached for a
|
|
3331
|
+
// transition (the panel filters by kind per tab, so a merged array is fine).
|
|
3332
|
+
const applyMerged=useCallback((id)=>{
|
|
3333
|
+
const c=refineCacheRef.current;
|
|
3334
|
+
const both=c.get(id+"::both");
|
|
3335
|
+
if(both){setRefineSuggestions(both.suggestions||[]);setRefineSummary(both.summary||null);return;}
|
|
3336
|
+
const small=c.get(id+"::small"), repl=c.get(id+"::replace");
|
|
3337
|
+
setRefineSuggestions([...(small?small.suggestions:[]),...(repl?repl.suggestions:[])]);
|
|
3338
|
+
setRefineSummary((small&&small.summary)||(repl&&repl.summary)||null);
|
|
3339
|
+
},[]);
|
|
3340
|
+
const startScan=useCallback(async(typeArg,opts)=>{
|
|
3055
3341
|
if(!active)return;
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
const avail=
|
|
3342
|
+
opts=opts||{};
|
|
3343
|
+
// Resolve availability from the continuously-polled /health (2s cadence)
|
|
3344
|
+
// rather than a serial probe on the click path. Unknown (null) counts as
|
|
3345
|
+
// available so the first click isn't blocked; a dead relay surfaces via
|
|
3346
|
+
// the POST below (→ error state).
|
|
3347
|
+
const avail=llmAvailable!==false;
|
|
3062
3348
|
const mode=(refineMode==="llm"&&!avail)?"deterministic":refineMode;
|
|
3063
|
-
scanModeRef.current=mode;
|
|
3064
3349
|
if(mode!==refineMode)setRefineMode(mode);
|
|
3350
|
+
scanModeRef.current=mode;
|
|
3351
|
+
// deterministic → one "both" pass (instant, free). llm → scope to the
|
|
3352
|
+
// active tab so Small skips recipe work; Replace asks for it on demand.
|
|
3353
|
+
// NOTE: this is also wired as a button onClick, so typeArg may be a click
|
|
3354
|
+
// event — only honour it when it's an explicit "small"/"replace" string.
|
|
3355
|
+
const reqType=(typeArg==="small"||typeArg==="replace")?typeArg:refineType;
|
|
3356
|
+
const type=mode==="deterministic"?"both":reqType;
|
|
3357
|
+
refineScanTypeRef.current=type;
|
|
3358
|
+
scanStartedAtRef.current=Date.now();
|
|
3359
|
+
setRefinePhase("scanning");setRefineLog([]);setRefineError(null);
|
|
3360
|
+
setRefineLabel(active.label);setRefineJobId(null);
|
|
3361
|
+
if(!opts.keepApplied)setAppliedIds({});
|
|
3362
|
+
// re-scan only this type; keep the other tab's cached result for the merge.
|
|
3363
|
+
refineCacheRef.current.delete(active.id+"::"+type);
|
|
3065
3364
|
try{
|
|
3066
3365
|
const et=active.effectiveTimings||[];
|
|
3067
3366
|
const timings=et.map(t=>({property:t.property,member:t.member||null,durationMs:t.durationMs,delayMs:t.delayMs,easing:t.easing}));
|
|
3068
3367
|
const selector=(active.bindings&&active.bindings.selector)||(et[0]&&et[0].selector)||null;
|
|
3069
|
-
// always scan for BOTH kinds (token tweaks + a whole-transition replacement)
|
|
3070
|
-
// in one pass; the tabs just filter which kind they show.
|
|
3071
3368
|
const{id}=await relayCreateJob({transitionId:active.id,label:active.label,selector,
|
|
3072
|
-
phase:active.phase||null,group:active.groupLabel||null,timings,mode,refineType:
|
|
3369
|
+
phase:active.phase||null,group:active.groupLabel||null,timings,mode,refineType:type});
|
|
3073
3370
|
setRefineJobId(id);
|
|
3074
3371
|
}catch(e){
|
|
3075
3372
|
setRefinePhase("error");
|
|
3076
3373
|
setRefineError(h(React.Fragment,null,"Couldn't reach the refine relay. Start it with ",h("code",{className:"tl-code"},"npx transitions-refine live"),"."));
|
|
3077
3374
|
}
|
|
3078
|
-
},[active,refineMode,refineType,
|
|
3079
|
-
const changeRefineMode=useCallback((mode)=>{setRefineMode(mode);setRefinePhase("idle");refreshHealth();},[refreshHealth]);
|
|
3080
|
-
//
|
|
3081
|
-
//
|
|
3082
|
-
|
|
3375
|
+
},[active,refineMode,refineType,llmAvailable]);
|
|
3376
|
+
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) it
|
|
3378
|
+
// scans the newly-selected type on demand — the trade-off for not scanning
|
|
3379
|
+
// the recipe upfront. Deterministic feeds both tabs from one "both" pass, so
|
|
3380
|
+
// its tab switch is a pure filter. Before any scan (idle/error), tabs just
|
|
3381
|
+
// preview copy and never auto-scan.
|
|
3382
|
+
const changeRefineType=useCallback((t)=>{
|
|
3383
|
+
setRefineType(t);
|
|
3384
|
+
if(!active)return;
|
|
3385
|
+
if(scanModeRef.current==="deterministic")return;
|
|
3386
|
+
if(refinePhase!=="done"&&refinePhase!=="scanning")return;
|
|
3387
|
+
if(refineCacheRef.current.has(active.id+"::"+t)){applyMerged(active.id);setRefinePhase("done");return;}
|
|
3388
|
+
startScan(t,{keepApplied:true});
|
|
3389
|
+
},[active,refinePhase,applyMerged,startScan]);
|
|
3083
3390
|
// poll the relay while a job is running
|
|
3084
3391
|
useEffect(()=>{
|
|
3085
3392
|
if(!refineJobId||refinePhase!=="scanning")return;
|
|
@@ -3090,7 +3397,15 @@
|
|
|
3090
3397
|
if(!live)return;
|
|
3091
3398
|
if(Array.isArray(job.statusLog))setRefineLog(job.statusLog);
|
|
3092
3399
|
if(job.status==="done"){
|
|
3093
|
-
|
|
3400
|
+
try{console.log("[refine] "+refineScanTypeRef.current+" job done in "+(Date.now()-scanStartedAtRef.current)+"ms");}catch{}
|
|
3401
|
+
const finish=()=>{if(!live)return;
|
|
3402
|
+
// cache this scan's result under its type, then show the merged set
|
|
3403
|
+
// (so the other tab's cached suggestions stay visible).
|
|
3404
|
+
const type=refineScanTypeRef.current;
|
|
3405
|
+
const id=active?active.id:"";
|
|
3406
|
+
refineCacheRef.current.set(id+"::"+type,{suggestions:(job.result&&job.result.suggestions)||[],summary:(job.result&&job.result.summary)||null});
|
|
3407
|
+
if(active)applyMerged(active.id);else{setRefineSuggestions((job.result&&job.result.suggestions)||[]);setRefineSummary(job.result&&job.result.summary);}
|
|
3408
|
+
setRefinePhase("done");};
|
|
3094
3409
|
// deterministic mode floors the scanning phase at 2s so it doesn't flash by.
|
|
3095
3410
|
const minMs=scanModeRef.current==="deterministic"?2000:0;
|
|
3096
3411
|
const wait=Math.max(0,minMs-(Date.now()-scanStartedAtRef.current));
|
|
@@ -3098,10 +3413,10 @@
|
|
|
3098
3413
|
return;
|
|
3099
3414
|
}
|
|
3100
3415
|
if(job.status==="error"){setRefineError(job.error||"The agent reported an error.");setRefinePhase("error");return;}
|
|
3101
|
-
to=setTimeout(tick,
|
|
3416
|
+
to=setTimeout(tick,200);
|
|
3102
3417
|
}catch(e){if(live){setRefineError("Lost connection to the relay.");setRefinePhase("error");}}
|
|
3103
3418
|
};
|
|
3104
|
-
to=setTimeout(tick,
|
|
3419
|
+
to=setTimeout(tick,200);
|
|
3105
3420
|
return()=>{live=false;if(to)clearTimeout(to);};
|
|
3106
3421
|
},[refineJobId,refinePhase]);
|
|
3107
3422
|
const applySuggestion=useCallback((s)=>{
|
|
@@ -3305,6 +3620,11 @@
|
|
|
3305
3620
|
if(scanTokenRef.current!==token)return;
|
|
3306
3621
|
const flat=registry.getAll().filter(e=>e.kind!=="phase");
|
|
3307
3622
|
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;}
|
|
3308
3628
|
// valid cache (same content signature) → apply instantly, skip the agent.
|
|
3309
3629
|
// A stale cache (different sig, e.g. content changed) falls through to a
|
|
3310
3630
|
// fresh scan and is overwritten below.
|
|
@@ -3353,7 +3673,12 @@
|
|
|
3353
3673
|
// (signature match, no agent) or kicks off a fresh agent scan. Rescan is
|
|
3354
3674
|
// manual after that.
|
|
3355
3675
|
useEffect(()=>{
|
|
3356
|
-
|
|
3676
|
+
// A host/demo page can hardcode a grouping via window.__TX_SEED_GROUPS so
|
|
3677
|
+
// the panel shows it WITHOUT a live agent (demo.html bakes in the agent's
|
|
3678
|
+
// initial scan this way). Otherwise the agent group-scan stays gated behind
|
|
3679
|
+
// a live agent as usual.
|
|
3680
|
+
const seeded=typeof window!=="undefined"&&Array.isArray(window.__TX_SEED_GROUPS)&&window.__TX_SEED_GROUPS.length;
|
|
3681
|
+
if((!live&&!seeded)||didGroupScanRef.current)return;
|
|
3357
3682
|
didGroupScanRef.current=true;
|
|
3358
3683
|
runGroupScan();
|
|
3359
3684
|
},[live,runGroupScan]);
|
|
@@ -3522,6 +3847,40 @@
|
|
|
3522
3847
|
}
|
|
3523
3848
|
|
|
3524
3849
|
// ── demo boxes ──
|
|
3850
|
+
// Hardcoded grouping — the transitions.dev agent's initial scan of this demo
|
|
3851
|
+
// (run once via the live relay), baked in so the panel groups the morph
|
|
3852
|
+
// instantly with NO relay/agent round-trip. Member labels are cleaned up from
|
|
3853
|
+
// the agent's output ("Morph container"→"Container", "Morph menu"→"Menu panel").
|
|
3854
|
+
// Edit here if the morph's transitions change, or delete window.__TX_SEED_GROUPS
|
|
3855
|
+
// to fall back to a live agent scan. Shape mirrors relay.mjs buildScanPrompt's
|
|
3856
|
+
// groups[] (the same structure TransitionRegistry.setGroups consumes). This is
|
|
3857
|
+
// below the inject CUT_MARKER, so it never ships in the injected/website build.
|
|
3858
|
+
const _mEO="cubic-bezier(0.22, 1, 0.36, 1)"; // smooth ease-out (open)
|
|
3859
|
+
const _mEM="cubic-bezier(0.34, 1.25, 0.64, 1)"; // morph ease (close settle)
|
|
3860
|
+
const _mpt=(property,durationMs,easing)=>({property,durationMs,delayMs:0,easing});
|
|
3861
|
+
const MORPH_SEED_GROUPS=[
|
|
3862
|
+
{id:"morph",label:"Plus \u2192 menu morph",component:null,phases:[
|
|
3863
|
+
{id:"morph:open",phase:"open",label:"Open",stateTarget:".t-morph",fromState:null,toState:'[data-open="true"]',members:[
|
|
3864
|
+
{id:"morph-container",label:"Container",selector:".t-morph",propertyTimings:[
|
|
3865
|
+
_mpt("width",250,_mEO),_mpt("height",250,_mEO),_mpt("border-radius",250,_mEO)]},
|
|
3866
|
+
{id:"morph-plus",label:"Plus button",selector:".t-morph .t-morph-plus",propertyTimings:[
|
|
3867
|
+
_mpt("opacity",200,_mEO),_mpt("transform",350,_mEO),_mpt("filter",200,_mEO)]},
|
|
3868
|
+
{id:"morph-menu",label:"Menu panel",selector:".t-morph .t-morph-menu",propertyTimings:[
|
|
3869
|
+
_mpt("opacity",200,_mEO),_mpt("transform",350,_mEO),_mpt("filter",200,_mEO)]}]},
|
|
3870
|
+
{id:"morph:close",phase:"close",label:"Close",stateTarget:".t-morph",fromState:'[data-open="true"]',toState:null,members:[
|
|
3871
|
+
{id:"morph-container",label:"Container",selector:".t-morph",propertyTimings:[
|
|
3872
|
+
_mpt("width",400,_mEM),_mpt("height",350,_mEM),_mpt("border-radius",350,_mEM)]},
|
|
3873
|
+
{id:"morph-plus",label:"Plus button",selector:".t-morph .t-morph-plus",propertyTimings:[
|
|
3874
|
+
_mpt("opacity",200,_mEO),_mpt("transform",350,_mEO),_mpt("filter",200,_mEO)]},
|
|
3875
|
+
{id:"morph-menu",label:"Menu panel",selector:".t-morph .t-morph-menu",propertyTimings:[
|
|
3876
|
+
_mpt("opacity",200,_mEO),_mpt("transform",350,_mEO),_mpt("filter",200,_mEO)]}]}]},
|
|
3877
|
+
{id:"morph-item",label:"Menu item hover",component:null,phases:[
|
|
3878
|
+
{id:"morph-item:hover",phase:"hover",label:"Hover",stateTarget:".t-morph-menu button.morph-item",fromState:null,toState:":hover",members:[
|
|
3879
|
+
{id:"morph-item-bg",label:"Menu item",selector:".t-morph-menu button.morph-item",propertyTimings:[
|
|
3880
|
+
_mpt("background",120,"ease")]}]}]},
|
|
3881
|
+
];
|
|
3882
|
+
if(typeof window!=="undefined")window.__TX_SEED_GROUPS=MORPH_SEED_GROUPS;
|
|
3883
|
+
|
|
3525
3884
|
// Plus → menu morph — transitions.dev (20-plus-menu-morph.md). The trigger
|
|
3526
3885
|
// button morphs into the panel it opens; CSS owns the morph, JS only flips
|
|
3527
3886
|
// data-open and mirrors it to aria-expanded, closing on outside-click / Escape.
|
|
@@ -3660,9 +4019,20 @@
|
|
|
3660
4019
|
// to show them for testing. (This whole block is below the inject CUT_MARKER,
|
|
3661
4020
|
// so it never ships in the injected build.)
|
|
3662
4021
|
const showControls=(()=>{try{return new URLSearchParams(location.search).has("controls");}catch(e){return false;}})();
|
|
4022
|
+
// TESTING ONLY (not part of the tool): top-right toggle to hide the demo
|
|
4023
|
+
// page content so the panel can be inspected against a blank page.
|
|
4024
|
+
const[demoHidden,setDemoHidden]=useState(false);
|
|
3663
4025
|
return h(TimelineCtx.Provider,{value:ctx},
|
|
3664
4026
|
showControls&&h(PanelControls),
|
|
3665
|
-
h("
|
|
4027
|
+
h("button",{type:"button",onClick:()=>setDemoHidden(v=>!v),
|
|
4028
|
+
title:"Testing only: hide/show demo page content",
|
|
4029
|
+
style:{position:"fixed",top:"12px",right:"12px",zIndex:2147483000,
|
|
4030
|
+
height:"28px",padding:"0 10px",borderRadius:"8px",cursor:"pointer",
|
|
4031
|
+
border:"1px solid rgba(0,0,0,0.12)",background:"#fff",color:"#444",
|
|
4032
|
+
font:"500 12px/1 ui-sans-serif,system-ui,sans-serif",
|
|
4033
|
+
boxShadow:"0 1px 3px rgba(0,0,0,0.12)"}},
|
|
4034
|
+
demoHidden?"Show demo":"Hide demo"),
|
|
4035
|
+
h("div",{ref:rootRef,className:"demo-root",style:{display:demoHidden?"none":undefined}},h("div",{className:"demo-header"},h("h1",null,"Timeline Inspector \u2014 Demo"),h("p",null,"A single transitions.dev element: the plus \u2192 menu morph. Its transitions are auto-detected \u2014 select one below, drag the bars to edit timing, then interact with the component itself to preview the change.")),
|
|
3666
4036
|
h("div",{className:"demo-grid"},h(BoxMorph))),
|
|
3667
4037
|
h(TimelinePanel));
|
|
3668
4038
|
}
|