transitions-refine 0.3.8 → 0.3.9
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/demo.html +375 -102
- package/package.json +1 -1
package/demo.html
CHANGED
|
@@ -244,7 +244,7 @@
|
|
|
244
244
|
padding: 6px 10px 6px 16px;
|
|
245
245
|
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
|
246
246
|
cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px;
|
|
247
|
-
box-shadow: var(--
|
|
247
|
+
box-shadow: var(--shadow-btn); transition: background 0.15s, scale 0.12s ease;
|
|
248
248
|
animation: tl-pill-in 220ms cubic-bezier(0.22, 1, 0.36, 1) both; }
|
|
249
249
|
@keyframes tl-pill-in { from { opacity: 0; transform: translateY(6px) scale(0.96); }
|
|
250
250
|
to { opacity: 1; transform: none; } }
|
|
@@ -369,7 +369,7 @@
|
|
|
369
369
|
.tl-insp-title { display: flex; align-items: center; font-size: 13px; font-weight: 500; line-height: 18px; color: #171717; margin-bottom: 16px; text-transform: capitalize; }
|
|
370
370
|
/* full-bleed divider between the Delay field and the Easing/Springs tabs
|
|
371
371
|
(Figma 580:9049 — spans the inspector edge-to-edge past the 16px padding) */
|
|
372
|
-
.tl-inspector-divider { height: 1px; background: var(--c-line); margin:
|
|
372
|
+
.tl-inspector-divider { height: 1px; background: var(--c-line); margin: 4px -16px; }
|
|
373
373
|
.tl-insp-label { font-size: 12px; line-height: 18px; color: #737373; margin: 10px 0 6px; }
|
|
374
374
|
|
|
375
375
|
/* ── tracks / ruler ── */
|
|
@@ -528,9 +528,11 @@
|
|
|
528
528
|
.tl-select-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
529
529
|
.tl-select-chev { display: flex; color: var(--c-ruler); flex: none; }
|
|
530
530
|
/* easing curve — exact Figma (node 580:11158): #f6f6f7, 8px radius, no grid */
|
|
531
|
+
/* overflow:visible so overshoot handles/lines spill past the box while
|
|
532
|
+
dragging instead of being clipped to the rectangle. */
|
|
531
533
|
.tl-curve { background: #f6f6f7; border-radius: 8px;
|
|
532
|
-
padding: 0; position: relative; overflow:
|
|
533
|
-
.tl-curve svg { display: block; cursor: crosshair; width: 100%; height: auto; }
|
|
534
|
+
padding: 0; position: relative; overflow: visible; }
|
|
535
|
+
.tl-curve svg { display: block; cursor: crosshair; width: 100%; height: auto; overflow: visible; }
|
|
534
536
|
.tl-curve-handle { cursor: grab; }
|
|
535
537
|
.tl-curve-handle:active { cursor: grabbing; }
|
|
536
538
|
.tl-cubic-row { display: flex; gap: 4px; }
|
|
@@ -538,9 +540,9 @@
|
|
|
538
540
|
32px #f7f7f7 pill, hover overlay + inset focus ring. */
|
|
539
541
|
.tl-cubic-cell { flex: 1; min-width: 0; display: flex; flex-direction: row; align-items: center;
|
|
540
542
|
gap: 4px; height: 32px; box-sizing: border-box;
|
|
541
|
-
padding: 0 8px; border-radius: 8px; background:
|
|
543
|
+
padding: 0 8px; border-radius: 8px; background: var(--c-field-bg);
|
|
542
544
|
box-shadow: inset 0 0 0 0 rgba(0,0,0,0.14); transition: box-shadow 0.12s ease, background 0.12s ease; }
|
|
543
|
-
.tl-cubic-cell:hover { background:
|
|
545
|
+
.tl-cubic-cell:hover { background: var(--c-sec-h); }
|
|
544
546
|
.tl-cubic-cell:focus-within { box-shadow: inset 0 0 0 1px rgba(0,0,0,0.14); }
|
|
545
547
|
.tl-cubic-row input { flex: 1; min-width: 0; width: 100%; height: 100%; border: none; background: transparent;
|
|
546
548
|
box-sizing: border-box; padding: 0; text-align: left;
|
|
@@ -682,9 +684,10 @@
|
|
|
682
684
|
.tl-menu-item:focus-visible { outline: none; background: #fff; box-shadow: inset 0 0 0 1px rgba(0,115,229,0.4); }
|
|
683
685
|
.tl-menu-item.disabled { color: var(--c-disabled); pointer-events: none; }
|
|
684
686
|
.tl-menu-item-label { flex: 1; min-width: 0; display: flex; align-items: center; }
|
|
685
|
-
.tl-menu-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
686
|
-
.tl-menu-stack { flex: 1; min-width: 0; display: flex; flex-direction:
|
|
687
|
-
.tl-menu-
|
|
687
|
+
.tl-menu-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
688
|
+
.tl-menu-stack { flex: 1; min-width: 0; display: flex; flex-direction: row; align-items: baseline; gap: 6px; }
|
|
689
|
+
.tl-menu-stack .tl-menu-text { flex: 0 1 auto; }
|
|
690
|
+
.tl-menu-sub { flex: 0 0 auto; font-size: 13px; line-height: 18px; }
|
|
688
691
|
.tl-menu-dim { color: #979797; }
|
|
689
692
|
.tl-menu-check { display: flex; color: var(--c-text-strong); flex: none; }
|
|
690
693
|
.tl-menu-empty { padding: 10px 8px; color: var(--c-disabled); font-size: 13px; }
|
|
@@ -706,6 +709,70 @@
|
|
|
706
709
|
/* trailing chevron on link rows (Figma 581:2828 — #696969 right chevron) */
|
|
707
710
|
.tl-menu-chevr { display: flex; flex: none; color: #696969; }
|
|
708
711
|
|
|
712
|
+
/* ── settings dropdown sub-page slider (transitions.dev · 08-page-side-by-side) ──
|
|
713
|
+
One dropdown surface that pages internally: main settings ↔ keyboard
|
|
714
|
+
shortcuts. Both pages stay mounted so they can cross-slide; the surface
|
|
715
|
+
height follows the active page (card-resize-style) so it grows/shrinks
|
|
716
|
+
smoothly instead of jumping. Page 1 exits left, page 2 exits right. */
|
|
717
|
+
.tl-set-slide.t-page-slide {
|
|
718
|
+
position: relative;
|
|
719
|
+
--page-slide-dur: 250ms; /* Fast — page slide */
|
|
720
|
+
--page-fade-dur: 250ms;
|
|
721
|
+
--page-slide-distance: 8px; /* Base distance */
|
|
722
|
+
--page-blur: 3px; /* Medium blur */
|
|
723
|
+
--page-stagger: 0ms;
|
|
724
|
+
--page-exit-enabled: 1;
|
|
725
|
+
--page-slide-ease: cubic-bezier(0.22, 1, 0.36, 1); /* Smooth ease out */
|
|
726
|
+
--page-fade-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
727
|
+
transition: height var(--page-slide-dur) var(--page-slide-ease);
|
|
728
|
+
overflow: hidden;
|
|
729
|
+
}
|
|
730
|
+
.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"] { --t-page-from-x: var(--page-slide-distance); }
|
|
732
|
+
.tl-set-slide .t-page {
|
|
733
|
+
position: absolute; top: 0; left: 0; right: 0;
|
|
734
|
+
opacity: 0; pointer-events: none;
|
|
735
|
+
transform: translateX(calc(var(--t-page-from-x, 0px) * var(--page-exit-enabled)));
|
|
736
|
+
filter: blur(calc(var(--page-blur) * var(--page-exit-enabled)));
|
|
737
|
+
transition:
|
|
738
|
+
opacity var(--page-fade-dur) var(--page-fade-ease),
|
|
739
|
+
transform var(--page-slide-dur) var(--page-slide-ease),
|
|
740
|
+
filter var(--page-slide-dur) var(--page-slide-ease);
|
|
741
|
+
will-change: opacity, transform, filter;
|
|
742
|
+
}
|
|
743
|
+
.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"] {
|
|
745
|
+
opacity: 1; pointer-events: auto; transform: translateX(0); filter: blur(0);
|
|
746
|
+
transition-delay: var(--page-stagger);
|
|
747
|
+
}
|
|
748
|
+
@media (prefers-reduced-motion: reduce) {
|
|
749
|
+
.tl-set-slide.t-page-slide,
|
|
750
|
+
.tl-set-slide .t-page { transition: none !important; }
|
|
751
|
+
}
|
|
752
|
+
/* sub-page back header — mirrors the menu-item row metrics (h-32, rounded-8,
|
|
753
|
+
13/16 label) so the surface feels continuous between pages. */
|
|
754
|
+
.tl-set-back { display: flex; align-items: center; gap: 4px; width: 100%; box-sizing: border-box;
|
|
755
|
+
min-height: 32px; padding: 0 8px 0 4px; border: none; background: transparent; border-radius: 8px;
|
|
756
|
+
font: inherit; font-size: 13px; font-weight: 500; line-height: 16px; color: #1b1b1b;
|
|
757
|
+
cursor: pointer; text-align: left; transition: background 0.1s ease, box-shadow 0.1s ease; }
|
|
758
|
+
.tl-set-back:hover { background: #f4f4f5; }
|
|
759
|
+
.tl-set-back:active { background: #ededee; }
|
|
760
|
+
.tl-set-back:focus-visible { outline: none; background: #fff; box-shadow: inset 0 0 0 1px rgba(0,115,229,0.4); }
|
|
761
|
+
/* reuse the right chevron flipped to point back; the menu chevron color */
|
|
762
|
+
.tl-set-back-ic { display: flex; flex: none; color: #696969; transform: scaleX(-1); }
|
|
763
|
+
.tl-set-back-title { flex: 1; min-width: 0; }
|
|
764
|
+
/* shortcut row — label left, keycap cluster right; same h-32 rhythm as rows */
|
|
765
|
+
.tl-kbd-row { display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
766
|
+
min-height: 32px; padding: 0 8px; font-size: 13px; line-height: 16px; color: #1b1b1b; }
|
|
767
|
+
.tl-kbd-row-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
768
|
+
.tl-kbd-cluster { display: flex; align-items: center; gap: 4px; flex: none; }
|
|
769
|
+
.tl-kbd-plus { color: #b8b8b8; font-size: 11px; line-height: 1; }
|
|
770
|
+
/* keycap — hairline via inset shadow (shadows over borders), tabular monospace */
|
|
771
|
+
.tl-kbd { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px;
|
|
772
|
+
padding: 0 5px; box-sizing: border-box; border-radius: 5px; background: #f4f4f5;
|
|
773
|
+
box-shadow: inset 0 0 0 1px #ececed; font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
|
774
|
+
font-size: 11px; line-height: 1; font-weight: 500; color: #4c4c4c; font-variant-numeric: tabular-nums; }
|
|
775
|
+
|
|
709
776
|
/* ═════ transitions.dev — menu dropdown (verbatim) ═════ */
|
|
710
777
|
.t-dropdown {
|
|
711
778
|
transform-origin: top left;
|
|
@@ -808,6 +875,8 @@
|
|
|
808
875
|
}
|
|
809
876
|
/* below-variant for triggers near the panel top edge */
|
|
810
877
|
.t-tt.tl-tt-below { bottom: auto; top: calc(100% + 8px); transform-origin: 50% 0; font-size: 12px; }
|
|
878
|
+
/* small gray shortcut hint inside a tooltip (matches the cheat-sheet keys) */
|
|
879
|
+
.tl-tt-kbd { margin-left: 8px; opacity: 0.4; }
|
|
811
880
|
/* multi-line message variant (e.g. Accept error) — long text wraps to a
|
|
812
881
|
readable measure instead of one overflowing line. Right-anchored so the
|
|
813
882
|
wide bubble grows leftward and never clips off the panel edge. Mirrors the
|
|
@@ -899,7 +968,7 @@
|
|
|
899
968
|
.pc-btn.primary:hover { background: #000; }
|
|
900
969
|
|
|
901
970
|
.tl-refine-btn.is-active { color: var(--c-blue-pressed); }
|
|
902
|
-
.tl-refine-btn.is-active::before { background: color(display-p3 0 0.451 0.898 / 0.12); }
|
|
971
|
+
.tl-refine-btn.is-active::before { background: var(--c-blue-bg-a); background: color(display-p3 0 0.451 0.898 / 0.12); }
|
|
903
972
|
|
|
904
973
|
/* refine panel slides in from the right using transitions.dev panel reveal
|
|
905
974
|
tokens (translate + opacity + cross-blur, per-phase open/close dur/ease). */
|
|
@@ -967,8 +1036,10 @@
|
|
|
967
1036
|
width: 36px; height: 36px; border: none; background: transparent; border-radius: 60px; cursor: pointer;
|
|
968
1037
|
color: #676767; transition: background 0.14s ease, color 0.14s ease; }
|
|
969
1038
|
.tl-refine-close:hover { background: rgba(170,170,170,0.10); color: #17181c; }
|
|
970
|
-
/* refine-type tabs
|
|
971
|
-
|
|
1039
|
+
/* refine-type tabs — toolbar strip (Figma 587:9258): 48px white bar, hairline
|
|
1040
|
+
bottom divider, segments inset 16px and packed gap:0 like a segmented control */
|
|
1041
|
+
.tl-refine-tabs { flex: 0 0 auto; box-sizing: border-box; display: flex; align-items: center; gap: 0;
|
|
1042
|
+
height: 48px; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; }
|
|
972
1043
|
.tl-refine-tab { height: 32px; padding: 6px 12px; border: none; background: transparent; cursor: pointer;
|
|
973
1044
|
border-radius: 8px; font: inherit; font-size: 13px; font-weight: 500; line-height: 14px; color: #676767;
|
|
974
1045
|
transition: background 0.14s ease, color 0.14s ease; }
|
|
@@ -1131,10 +1202,16 @@
|
|
|
1131
1202
|
@keyframes tl-dotm-e { 0%{opacity:1} 16.667%{opacity:.52} 33.333%{opacity:.08} 50%{opacity:.52} 66.667%{opacity:.08} 83.333%{opacity:.52} 100%{opacity:1} }
|
|
1132
1203
|
@keyframes tl-dotm-f { 0%{opacity:.08} 16.667%{opacity:1} 33.333%{opacity:.52} 50%{opacity:.08} 66.667%{opacity:.52} 83.333%{opacity:1} 100%{opacity:.08} }
|
|
1133
1204
|
@keyframes tl-dotm-g { 0%{opacity:.08} 16.667%{opacity:.52} 33.333%{opacity:.08} 50%{opacity:.08} 66.667%{opacity:.08} 83.333%{opacity:.52} 100%{opacity:.08} }
|
|
1134
|
-
/* background-scan note in the header count slot —
|
|
1135
|
-
|
|
1205
|
+
/* background-scan note in the header count slot — dot-matrix loader + shimmer
|
|
1206
|
+
text (transitions.dev shimmer text 15). Hover shows the tooltip. */
|
|
1207
|
+
/* the tooltip wrap fills the count slot's height; center its content so the
|
|
1208
|
+
loader + text sit on the slot's vertical midline instead of its bottom. */
|
|
1209
|
+
.tl-header-count .t-tt-wrap { display: inline-flex; align-items: center; align-self: center; }
|
|
1136
1210
|
.tl-scan-note { cursor: help; color: var(--c-count);
|
|
1137
|
-
|
|
1211
|
+
display: inline-flex; align-items: center; gap: 6px; }
|
|
1212
|
+
.tl-scan-note .tl-dotm { width: 14px; height: 14px; }
|
|
1213
|
+
.tl-scan-shimmer { --shimmer-dur: 2000ms; --shimmer-base: var(--c-count); --shimmer-highlight: #17181c;
|
|
1214
|
+
--shimmer-band: 400%; --shimmer-ease: linear; }
|
|
1138
1215
|
/* loading status text — transitions.dev shimmer text (15) + text states swap (04) */
|
|
1139
1216
|
.tl-refine-status-text {
|
|
1140
1217
|
--shimmer-dur: 2000ms; --shimmer-base: #9a9a9a; --shimmer-highlight: #17181c;
|
|
@@ -1217,32 +1294,46 @@
|
|
|
1217
1294
|
/* applied state — GRAY, never green (matches Accept icon --c-text #17181c) */
|
|
1218
1295
|
.tl-pill-btn.is-applied { color: var(--c-text); cursor: default; }
|
|
1219
1296
|
.tl-pill-btn.is-applied svg path { stroke: var(--c-text); }
|
|
1220
|
-
/* Apply ⟷ Applied:
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
.tl-apply-swap
|
|
1229
|
-
|
|
1297
|
+
/* Apply ⟷ Applied: a LEADING check is revealed first and pushes the text to
|
|
1298
|
+
the right, then the differing SUFFIX cross-blurs. The shared stem "Appl"
|
|
1299
|
+
stays put while "y" ⟷ "ied" cross-blur in a slot whose width tweens between
|
|
1300
|
+
the two measured widths. Both the leading check space AND the suffix slot
|
|
1301
|
+
use the SAME resize token (--apply-resize-*: card resize, smooth ease out),
|
|
1302
|
+
so the whole button grows in one synchronized motion. The check lives in its
|
|
1303
|
+
own overflow:hidden box whose width tweens 0 -> (icon + gap), shoving the
|
|
1304
|
+
text rightward — the check itself stroke-draws IN PLACE (no translate). */
|
|
1305
|
+
.tl-apply-swap { position: relative; display: inline-flex; align-items: center;
|
|
1306
|
+
vertical-align: middle; line-height: 14px;
|
|
1307
|
+
--apply-resize-dur: 400ms; --apply-resize-ease: cubic-bezier(0.22, 1, 0.36, 1); }
|
|
1308
|
+
/* leading check slot: collapsed to width 0 at rest; expands to the measured
|
|
1309
|
+
icon+gap width (--chk-w) when applied, using the same token as the suffix
|
|
1310
|
+
resize. overflow:hidden clips the pinned-left check so it reveals in place
|
|
1311
|
+
(left edge fixed) rather than translating. */
|
|
1312
|
+
.tl-apply-check { display: inline-flex; align-items: center; overflow: hidden;
|
|
1313
|
+
flex: 0 0 auto; width: 0;
|
|
1314
|
+
transition: width var(--apply-resize-dur) var(--apply-resize-ease); }
|
|
1315
|
+
.tl-apply-swap[data-state="applied"] .tl-apply-check { width: var(--chk-w, 19px); }
|
|
1316
|
+
.tl-apply-stem { display: inline-block; }
|
|
1317
|
+
.tl-apply-suffix { position: relative; display: inline-block; width: var(--sfx-y, auto);
|
|
1318
|
+
transition: width var(--apply-resize-dur) var(--apply-resize-ease); }
|
|
1319
|
+
.tl-apply-swap[data-state="applied"] .tl-apply-suffix { width: var(--sfx-ied, auto); }
|
|
1320
|
+
.tl-apply-seg { white-space: pre;
|
|
1230
1321
|
transition: opacity 180ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
1231
1322
|
filter 180ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
1232
1323
|
will-change: opacity, filter; }
|
|
1233
|
-
.tl-apply-
|
|
1234
|
-
.tl-apply-
|
|
1235
|
-
|
|
1236
|
-
.tl-apply-swap .tl-apply-
|
|
1237
|
-
.tl-apply-swap .tl-apply-
|
|
1238
|
-
.tl-apply-swap[data-state="applied"] .tl-apply-
|
|
1239
|
-
.tl-apply-swap[data-state="applied"] .tl-apply-b { opacity: 1; filter: blur(0); }
|
|
1324
|
+
.tl-apply-y { display: inline-block; }
|
|
1325
|
+
.tl-apply-ied { position: absolute; top: 0; left: 0; display: inline-block; }
|
|
1326
|
+
.tl-apply-swap .tl-apply-y { opacity: 1; filter: blur(0); }
|
|
1327
|
+
.tl-apply-swap .tl-apply-ied { opacity: 0; filter: blur(2px); }
|
|
1328
|
+
.tl-apply-swap[data-state="applied"] .tl-apply-y { opacity: 0; filter: blur(2px); }
|
|
1329
|
+
.tl-apply-swap[data-state="applied"] .tl-apply-ied { opacity: 1; filter: blur(0); }
|
|
1240
1330
|
@media (prefers-reduced-motion: reduce) {
|
|
1241
|
-
.tl-apply-
|
|
1331
|
+
.tl-apply-suffix, .tl-apply-seg { transition: none !important; }
|
|
1242
1332
|
}
|
|
1243
1333
|
/* success check (transitions.dev 10-success-check): stroke-draw on the path with
|
|
1244
1334
|
a plain opacity fade — IN PLACE, no rotate / Y-bob / scale, so the check never
|
|
1245
|
-
moves while it draws.
|
|
1335
|
+
moves while it draws. It is the leading element, unclipped by the .tl-apply-check
|
|
1336
|
+
width tween that pushes the text. GRAY stroke. */
|
|
1246
1337
|
.tl-sug-check { display: inline-flex; opacity: 0; will-change: opacity; }
|
|
1247
1338
|
.tl-sug-check svg { display: block; overflow: visible; }
|
|
1248
1339
|
/* stroke-dasharray = path.getTotalLength() of ICONS.accept "M4 8.4268 L6.46155
|
|
@@ -1278,6 +1369,7 @@
|
|
|
1278
1369
|
.tl-refine-results .t-stagger-line { transition: none !important; }
|
|
1279
1370
|
.t-resize, .tl-scan-morph { transition: none !important; }
|
|
1280
1371
|
.tl-pill-btn.tl-apply-btn { transition: background 0.12s ease, scale 0.12s ease, opacity 0.12s ease !important; }
|
|
1372
|
+
.tl-apply-check { transition: none !important; }
|
|
1281
1373
|
.tl-sug-check { animation: none !important; opacity: 1; }
|
|
1282
1374
|
.tl-sug-check svg path { animation: none !important; stroke-dashoffset: 0 !important; }
|
|
1283
1375
|
.tl-scan-face { transition: none !important; }
|
|
@@ -1530,7 +1622,10 @@
|
|
|
1530
1622
|
_sched(){if(this.rafId!==null)return;this.rafId=requestAnimationFrame(()=>{this.rafId=null;if(this.running&&!this.paused)this.scan();});}
|
|
1531
1623
|
scan(){const seen=new Map();const w=document.createTreeWalker(this.root,NodeFilter.SHOW_ELEMENT);let n=w.currentNode;while(n){if(n instanceof HTMLElement)this._proc(n,seen);n=w.nextNode();}this.registry.replaceAll(Array.from(seen.values()));}
|
|
1532
1624
|
_proc(el,seen){
|
|
1533
|
-
|
|
1625
|
+
// Skip the Refine UI itself: the panel ([data-timeline-panel]) plus any of
|
|
1626
|
+
// our surfaces that portal OUT of it (dropdown menus, toasts) — those carry
|
|
1627
|
+
// [data-tl-ui]. Without this the scanner lists the tool's own transitions.
|
|
1628
|
+
if(el.closest("[data-timeline-panel],[data-tl-ui]"))return;
|
|
1534
1629
|
const s=getComputedStyle(el); const rp=s.transitionProperty;
|
|
1535
1630
|
if(!rp||rp==="none"||rp==="all")return;
|
|
1536
1631
|
const props=rp.split(",").map(p=>p.trim());
|
|
@@ -1748,7 +1843,7 @@
|
|
|
1748
1843
|
const origin=pos?pos.origin:"top-left";
|
|
1749
1844
|
const cls=closing?"is-closing":shown?"is-open":"";
|
|
1750
1845
|
return createPortal(
|
|
1751
|
-
h("div",{ref,className:cx("t-dropdown","tl-menu",cls),"data-origin":origin,style},children),
|
|
1846
|
+
h("div",{ref,className:cx("t-dropdown","tl-menu",cls),"data-origin":origin,"data-tl-ui":"",style},children),
|
|
1752
1847
|
target);
|
|
1753
1848
|
}
|
|
1754
1849
|
|
|
@@ -1759,6 +1854,32 @@
|
|
|
1759
1854
|
right);
|
|
1760
1855
|
}
|
|
1761
1856
|
|
|
1857
|
+
// Settings dropdown body that pages between the main menu and the keyboard-
|
|
1858
|
+
// shortcuts cheat-sheet (transitions.dev · 08-page-side-by-side). Both pages
|
|
1859
|
+
// stay mounted so they cross-slide; the container height follows the active
|
|
1860
|
+
// page so the one dropdown surface resizes smoothly between pages. Lives
|
|
1861
|
+
// inside the Dropdown portal, so its layout effect fires when the surface
|
|
1862
|
+
// 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);
|
|
1865
|
+
useLayoutEffect(()=>{
|
|
1866
|
+
const slide=slideRef.current; if(!slide) return;
|
|
1867
|
+
const active=view==="shortcuts"?p2.current:p1.current;
|
|
1868
|
+
if(active) slide.style.height=active.offsetHeight+"px";
|
|
1869
|
+
});
|
|
1870
|
+
// Escape steps back from the sub-page first, then closes the dropdown —
|
|
1871
|
+
// mirrors the picker's nested-dismiss behaviour.
|
|
1872
|
+
useEffect(()=>{
|
|
1873
|
+
const onKey=e=>{ if(e.key!=="Escape")return;
|
|
1874
|
+
if(view==="shortcuts"){onBack&&onBack();} else {onClose&&onClose();} };
|
|
1875
|
+
document.addEventListener("keydown",onKey);
|
|
1876
|
+
return()=>document.removeEventListener("keydown",onKey);
|
|
1877
|
+
},[view,onBack,onClose]);
|
|
1878
|
+
return h("div",{ref:slideRef,className:"tl-set-slide t-page-slide","data-page":view==="shortcuts"?"2":"1"},
|
|
1879
|
+
h("div",{ref:p1,className:"tl-set-page t-page","data-page-id":"1","aria-hidden":view==="shortcuts"},mainPage),
|
|
1880
|
+
h("div",{ref:p2,className:"tl-set-page t-page","data-page-id":"2","aria-hidden":view!=="shortcuts"},shortcutsPage));
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1762
1883
|
// ── refine relay client ──
|
|
1763
1884
|
// Talks to the local relay (server/relay.mjs). The browser posts a job and
|
|
1764
1885
|
// polls; the relay answers each job with one agent run (one-shot).
|
|
@@ -1825,25 +1946,32 @@
|
|
|
1825
1946
|
const names=[...new Set(matches.map(t=>t.member))];
|
|
1826
1947
|
return names.length===1?names[0]:matches[0].member;
|
|
1827
1948
|
}
|
|
1828
|
-
// Apply button — "Apply" ⟷ "Applied"
|
|
1829
|
-
//
|
|
1830
|
-
//
|
|
1831
|
-
//
|
|
1832
|
-
//
|
|
1833
|
-
//
|
|
1834
|
-
//
|
|
1949
|
+
// Apply button — "Apply" ⟷ "✓ Applied". On apply, a LEADING gray check is
|
|
1950
|
+
// revealed first and pushes the text to the right: .tl-apply-check tweens its
|
|
1951
|
+
// width 0 -> (icon + 6px gap) using the SAME resize token as the suffix slot
|
|
1952
|
+
// (--apply-resize-*, smooth ease out), so the check expansion + the "y" -> "ied"
|
|
1953
|
+
// suffix resize grow the button in one synchronized motion. The stem "Appl"
|
|
1954
|
+
// stays put and "y" ⟷ "ied" cross-blur (transitions.dev 04). The check itself
|
|
1955
|
+
// stroke-draws IN PLACE (transitions.dev 10, no rotate/bob/scale/translate) —
|
|
1956
|
+
// it is pinned to the slot's left edge and the overflow clip reveals it, so it
|
|
1957
|
+
// never translates. The same <button> node is reused across states.
|
|
1835
1958
|
function ApplyBtn({applied,onApply}){
|
|
1836
1959
|
const ref=useRef(null);
|
|
1837
1960
|
const prev=useRef(applied);
|
|
1838
|
-
// measure both
|
|
1839
|
-
// so the
|
|
1961
|
+
// measure both suffixes once (after fonts/layout) and pin the two slot widths
|
|
1962
|
+
// so the suffix can tween between "y" and the checked "ied".
|
|
1840
1963
|
useLayoutEffect(()=>{
|
|
1841
1964
|
const btn=ref.current;if(!btn)return;
|
|
1842
1965
|
const sw=btn.querySelector(".tl-apply-swap"); if(!sw) return;
|
|
1966
|
+
const CHK_GAP=6; // space between the leading check and "Applied"
|
|
1843
1967
|
const measure=()=>{
|
|
1844
|
-
const
|
|
1845
|
-
if(
|
|
1846
|
-
if(
|
|
1968
|
+
const y=sw.querySelector(".tl-apply-y"), ied=sw.querySelector(".tl-apply-ied");
|
|
1969
|
+
if(y) sw.style.setProperty("--sfx-y", Math.ceil(y.getBoundingClientRect().width)+"px");
|
|
1970
|
+
if(ied) sw.style.setProperty("--sfx-ied", Math.ceil(ied.getBoundingClientRect().width)+"px");
|
|
1971
|
+
// leading check slot target width = icon natural width + gap; it stays
|
|
1972
|
+
// measurable even while clipped (the inner .tl-sug-check keeps its size).
|
|
1973
|
+
const chk=sw.querySelector(".tl-sug-check");
|
|
1974
|
+
if(chk) sw.style.setProperty("--chk-w", (Math.ceil(chk.getBoundingClientRect().width)+CHK_GAP)+"px");
|
|
1847
1975
|
};
|
|
1848
1976
|
measure();
|
|
1849
1977
|
if(document.fonts&&document.fonts.ready) document.fonts.ready.then(measure).catch(()=>{});
|
|
@@ -1873,14 +2001,16 @@
|
|
|
1873
2001
|
className:cx("tl-pill-btn tl-apply-btn",applied&&"is-applied"),
|
|
1874
2002
|
disabled:applied,onClick:applied?undefined:onApply},
|
|
1875
2003
|
h("span",{className:"tl-apply-swap","data-state":applied?"applied":"apply"},
|
|
1876
|
-
h("span",{className:"tl-apply-
|
|
1877
|
-
h("span",{className:"tl-apply-label tl-apply-b"},
|
|
2004
|
+
h("span",{className:"tl-apply-check"},
|
|
1878
2005
|
h("span",{className:"tl-sug-check","data-state":"out","aria-hidden":"true"},
|
|
1879
2006
|
h("svg",{width:13,height:13,viewBox:"0 0 16 16",fill:"none",
|
|
1880
2007
|
xmlns:"http://www.w3.org/2000/svg",style:{display:"block"}},
|
|
1881
2008
|
h("path",{d:"M4 8.4268L6.46155 11.19223L12 4.97001",stroke:"currentColor",
|
|
1882
|
-
strokeWidth:1.5,strokeLinecap:"round",strokeLinejoin:"round"}))),
|
|
1883
|
-
|
|
2009
|
+
strokeWidth:1.5,strokeLinecap:"round",strokeLinejoin:"round"})))),
|
|
2010
|
+
h("span",{className:"tl-apply-stem"},"Appl"),
|
|
2011
|
+
h("span",{className:"tl-apply-suffix"},
|
|
2012
|
+
h("span",{className:"tl-apply-seg tl-apply-y"},"y"),
|
|
2013
|
+
h("span",{className:"tl-apply-seg tl-apply-ied"},"ied"))));
|
|
1884
2014
|
}
|
|
1885
2015
|
function RefineResults({suggestions,appliedIds,onApply,lanes}){
|
|
1886
2016
|
const ref=useRef(null);
|
|
@@ -1923,7 +2053,7 @@
|
|
|
1923
2053
|
: h(React.Fragment,null,
|
|
1924
2054
|
s.title&&h("div",{className:"tl-sug-title"},s.title),
|
|
1925
2055
|
(s.from||s.to)&&h("div",{className:"tl-sug-delta"},
|
|
1926
|
-
|
|
2056
|
+
s.from&&h("span",{className:"tl-sug-from"},s.from),
|
|
1927
2057
|
s.to&&h("span",{className:"tl-sug-to"},s.to))),
|
|
1928
2058
|
s.reason&&h("div",{className:"tl-sug-reason"},s.reason)),
|
|
1929
2059
|
applyBtn));
|
|
@@ -2096,9 +2226,24 @@
|
|
|
2096
2226
|
return out;
|
|
2097
2227
|
}
|
|
2098
2228
|
|
|
2099
|
-
function Header({entries,active,onSelect,onReset,onCopy,copied,snap,setSnap,onMinimize,onRefine,refineActive,onAccept,acceptState,acceptDisabled,acceptError,scanning,onRescan}){
|
|
2100
|
-
const[pick,setPick]=useState(false);
|
|
2229
|
+
function Header({entries,active,onSelect,onReset,onCopy,copied,snap,setSnap,onMinimize,onRefine,refineActive,onAccept,acceptState,acceptDisabled,acceptError,scanning,onRescan,pick,setPick}){
|
|
2101
2230
|
const[setg,setSetg]=useState(false);
|
|
2231
|
+
// settings dropdown sub-page (main menu ↔ keyboard-shortcuts cheat sheet)
|
|
2232
|
+
const[settingsView,setSettingsView]=useState("main"); // "main" | "shortcuts"
|
|
2233
|
+
const isMac=typeof navigator!=="undefined"&&/mac/i.test((navigator.userAgentData&&navigator.userAgentData.platform)||navigator.platform||navigator.userAgent||"");
|
|
2234
|
+
const modKey=isMac?"Cmd":"Ctrl";
|
|
2235
|
+
// small gray shortcut hint appended inside a tooltip (e.g. "⌘A")
|
|
2236
|
+
const ttHint=(k)=>h("span",{className:"tl-tt-kbd"},(isMac?"\u2318":"Ctrl ")+k);
|
|
2237
|
+
// ⌫ = Backspace, ↑/↓ = arrows; letters/Enter shown verbatim
|
|
2238
|
+
const SHORTCUTS=[
|
|
2239
|
+
{label:"Accept changes",k:"Enter"},
|
|
2240
|
+
{label:"Reset values",k:"\u232B"},
|
|
2241
|
+
{label:"Copy values",k:"C"},
|
|
2242
|
+
{label:"Toggle Refine panel",k:"E"},
|
|
2243
|
+
{label:"Open transition picker",k:"K"},
|
|
2244
|
+
{label:"Previous transition",k:"\u2191"},
|
|
2245
|
+
{label:"Next transition",k:"\u2193"},
|
|
2246
|
+
];
|
|
2102
2247
|
const pickRef=useRef(null), gearRef=useRef(null);
|
|
2103
2248
|
const[q,setQ]=useState("");
|
|
2104
2249
|
useEffect(()=>{if(!pick)setQ("");},[pick]); // reset search when the picker closes
|
|
@@ -2152,28 +2297,50 @@
|
|
|
2152
2297
|
h("span",{className:"tl-header-count"},
|
|
2153
2298
|
scanning
|
|
2154
2299
|
? h("span",{className:"t-tt-wrap"},
|
|
2155
|
-
h("span",{className:"tl-scan-note",tabIndex:0},
|
|
2300
|
+
h("span",{className:"tl-scan-note",tabIndex:0},
|
|
2301
|
+
h(DotmLoader),
|
|
2302
|
+
h("span",{className:"t-shimmer tl-scan-shimmer","data-text":"Agent scanning transitions…"},"Agent scanning transitions…")),
|
|
2156
2303
|
h("span",{className:"t-tt tl-tt-below tl-tt-msg",role:"tooltip"},
|
|
2157
2304
|
"Agent is working on understanding your transitions and creating grouping and naming. This usually takes a few seconds to a minute."))
|
|
2158
2305
|
: entries.length+" transition"+(entries.length===1?"":"s")+" found"),
|
|
2159
2306
|
h("button",{ref:gearRef,className:cx("tl-icon-btn",setg&&"is-active"),title:"Settings",onClick:()=>setSetg(v=>!v)},h(Ic,{name:"dotsv"})),
|
|
2160
|
-
h(Dropdown,{open:setg,onClose:()=>setSetg(false),triggerRef:gearRef,width:
|
|
2161
|
-
h(
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2307
|
+
h(Dropdown,{open:setg,onClose:()=>{setSetg(false);setSettingsView("main");},triggerRef:gearRef,width:240,align:"right"},
|
|
2308
|
+
h(SettingsSlide,{view:settingsView,
|
|
2309
|
+
onBack:()=>setSettingsView("main"),
|
|
2310
|
+
onClose:()=>{setSetg(false);setSettingsView("main");},
|
|
2311
|
+
// ── main settings page ──
|
|
2312
|
+
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
|
+
h(MenuItem,{disabled:!active,onClick:()=>{onCopy&&onCopy();setSetg(false);},
|
|
2318
|
+
left:h("span",{className:"tl-menu-icon"},h(Ic,{name:"copy"})),
|
|
2319
|
+
right:copied&&h("span",{className:"tl-menu-check"},h(Ic,{name:"accept"}))},copied?"Copied":"Copy values"),
|
|
2320
|
+
h(MenuItem,{disabled:scanning,onClick:()=>{setSetg(false);onRescan&&onRescan();},
|
|
2321
|
+
left:h("span",{className:"tl-menu-icon"},h(Ic,{name:"restart"}))},
|
|
2322
|
+
scanning?"Rescanning…":"Rescan transitions"),
|
|
2323
|
+
h(MenuItem,{onClick:()=>setSettingsView("shortcuts"),
|
|
2324
|
+
right:h("span",{className:"tl-menu-chevr"},h(Ic,{name:"chevronr",size:16}))},"Keyboard shortcuts"),
|
|
2325
|
+
h("div",{className:"tl-menu-divider"}),
|
|
2326
|
+
h(MenuItem,{onClick:()=>{setSetg(false);window.open("https://transitions.dev","_blank","noopener");},
|
|
2327
|
+
right:h("span",{className:"tl-menu-chevr"},h(Ic,{name:"chevronr",size:16}))},"Learn more")),
|
|
2328
|
+
// ── keyboard-shortcuts sub-page ──
|
|
2329
|
+
shortcutsPage:h(React.Fragment,null,
|
|
2330
|
+
h("button",{type:"button",className:"tl-set-back",onClick:()=>setSettingsView("main")},
|
|
2331
|
+
h("span",{className:"tl-set-back-ic"},h(Ic,{name:"chevronr",size:16})),
|
|
2332
|
+
h("span",{className:"tl-set-back-title"},"Keyboard shortcuts")),
|
|
2333
|
+
h("div",{className:"tl-menu-divider"}),
|
|
2334
|
+
...SHORTCUTS.map((s,i)=>h("div",{key:i,className:"tl-kbd-row"},
|
|
2335
|
+
h("span",{className:"tl-kbd-row-label"},s.label),
|
|
2336
|
+
h("span",{className:"tl-kbd-cluster"},
|
|
2337
|
+
h("kbd",{className:"tl-kbd"},modKey),
|
|
2338
|
+
h("span",{className:"tl-kbd-plus"},"+"),
|
|
2339
|
+
h("kbd",{className:"tl-kbd"},s.k)))))}),
|
|
2340
|
+
),
|
|
2174
2341
|
h("span",{className:"t-tt-wrap"},
|
|
2175
2342
|
h("button",{className:"tl-sec-btn t-tt-trigger",disabled:!active,onClick:onReset},"Reset"),
|
|
2176
|
-
h("span",{className:"t-tt tl-tt-below",role:"tooltip"},"Reset values")),
|
|
2343
|
+
h("span",{className:"t-tt tl-tt-below",role:"tooltip"},"Reset values",ttHint("\u232B"))),
|
|
2177
2344
|
h("span",{className:"t-tt-wrap"},
|
|
2178
2345
|
h("button",{className:cx("tl-accept-btn",acceptState==="saving"&&"is-saving",acceptState==="done"&&"is-done"),
|
|
2179
2346
|
disabled:!active||acceptDisabled||acceptState==="saving"||acceptState==="done",onClick:onAccept,"aria-label":"Accept changes to your code"},
|
|
@@ -2185,8 +2352,9 @@
|
|
|
2185
2352
|
acceptState==="error"&&acceptError?acceptError
|
|
2186
2353
|
:acceptState==="done"?"Saved to your code"
|
|
2187
2354
|
:acceptDisabled?"No changes to save"
|
|
2188
|
-
:"Save changes to your codebase")),
|
|
2189
|
-
h("
|
|
2355
|
+
:h(React.Fragment,null,"Save changes to your codebase",ttHint("A")))),
|
|
2356
|
+
h("span",{className:"t-tt-wrap"},
|
|
2357
|
+
h("button",{className:cx("tl-refine-btn t-tt-trigger",refineActive&&"is-active"),disabled:!active,onClick:onRefine},
|
|
2190
2358
|
h(Ic,{name:"wand"}),
|
|
2191
2359
|
h("span",{className:"tl-refine-sparks","aria-hidden":"true"},
|
|
2192
2360
|
h("i",{style:{"--ox":"-1px","--oy":"-3px","--sx":"0px","--sy":"-11px","--sd":"900ms","--sdelay":"0ms"}}),
|
|
@@ -2200,7 +2368,10 @@
|
|
|
2200
2368
|
h("i",{style:{"--ox":"-5px","--oy":"-2px","--sx":"-9px","--sy":"-9px","--sd":"930ms","--sdelay":"180ms"}}),
|
|
2201
2369
|
h("i",{style:{"--ox":"3px","--oy":"1px","--sx":"12px","--sy":"1px","--sd":"980ms","--sdelay":"300ms"}})),
|
|
2202
2370
|
h("span",null,"Refine")),
|
|
2203
|
-
|
|
2371
|
+
h("span",{className:"t-tt tl-tt-below",role:"tooltip"},"Refine",ttHint("R"))),
|
|
2372
|
+
h("span",{className:"t-tt-wrap"},
|
|
2373
|
+
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("."))),
|
|
2204
2375
|
);
|
|
2205
2376
|
}
|
|
2206
2377
|
|
|
@@ -2317,15 +2488,12 @@
|
|
|
2317
2488
|
}
|
|
2318
2489
|
|
|
2319
2490
|
// shared plot geometry for both the easing curve and the spring curve.
|
|
2320
|
-
//
|
|
2321
|
-
//
|
|
2322
|
-
//
|
|
2323
|
-
//
|
|
2324
|
-
//
|
|
2325
|
-
|
|
2326
|
-
// graph ratio (easing.dev): the 0→1 region is a square, centered, with
|
|
2327
|
-
// matching overshoot headroom above 1 and below 0.
|
|
2328
|
-
const CURVE = { VBW:240, VBH:240, PAD_X:48, PAD_Y:48 };
|
|
2491
|
+
// Square plot with equal padding on all sides (standard cubic-bezier graph
|
|
2492
|
+
// ratio, easing.dev): the 0→1 region is centered with identical top/bottom
|
|
2493
|
+
// (and left/right) margins. The box no longer reserves room for overshoot —
|
|
2494
|
+
// overshoot/undershoot handles overflow the box freely (the curve container
|
|
2495
|
+
// is overflow:visible) so dragging is never clamped to the rectangle.
|
|
2496
|
+
const CURVE = { VBW:240, VBH:240, PAD_X:24, PAD_Y:24 };
|
|
2329
2497
|
|
|
2330
2498
|
// Unified "Preview" section (Figma node 580:11158) — collapsible, collapsed by
|
|
2331
2499
|
// default (transitions.dev · 21-accordion). Holds BOTH the timing curve card
|
|
@@ -2828,6 +2996,9 @@
|
|
|
2828
2996
|
const[panelHeight,setPanelHeight]=useState(440);
|
|
2829
2997
|
const[resizing,setResizing]=useState(false);
|
|
2830
2998
|
const[snap,setSnap]=useState(true);
|
|
2999
|
+
// transition picker open state — lifted out of Header so the panel-focused
|
|
3000
|
+
// Cmd/Ctrl+K shortcut can open it.
|
|
3001
|
+
const[pickOpen,setPickOpen]=useState(false);
|
|
2831
3002
|
// ── refine ──
|
|
2832
3003
|
const[refineOpen,setRefineOpen]=useState(false);
|
|
2833
3004
|
const[refinePhase,setRefinePhase]=useState("idle"); // idle | scanning | done | error
|
|
@@ -2836,6 +3007,10 @@
|
|
|
2836
3007
|
const[refineSuggestions,setRefineSuggestions]=useState([]);
|
|
2837
3008
|
const[refineSummary,setRefineSummary]=useState(null);
|
|
2838
3009
|
const[refineError,setRefineError]=useState(null);
|
|
3010
|
+
// deterministic scans answer almost instantly, so enforce a 2s floor on the
|
|
3011
|
+
// "scanning" phase for that mode only (LLM scans stay immediate).
|
|
3012
|
+
const scanStartedAtRef=useRef(0);
|
|
3013
|
+
const scanModeRef=useRef("llm");
|
|
2839
3014
|
// ── accept (write to source) ──
|
|
2840
3015
|
const[acceptState,setAcceptState]=useState("idle"); // idle | saving | done | error
|
|
2841
3016
|
const[acceptError,setAcceptError]=useState(null);
|
|
@@ -2878,12 +3053,14 @@
|
|
|
2878
3053
|
// "Start scanning" resolves availability first, then posts the job.
|
|
2879
3054
|
const startScan=useCallback(async()=>{
|
|
2880
3055
|
if(!active)return;
|
|
3056
|
+
scanStartedAtRef.current=Date.now();
|
|
2881
3057
|
setRefinePhase("scanning");setRefineLog([]);
|
|
2882
3058
|
setRefineSuggestions([]);setRefineSummary(null);setRefineError(null);
|
|
2883
3059
|
setAppliedIds({});setRefineLabel(active.label);setRefineJobId(null);
|
|
2884
3060
|
const j=await refreshHealth();
|
|
2885
3061
|
const avail=j?!!j.llmAvailable:false;
|
|
2886
3062
|
const mode=(refineMode==="llm"&&!avail)?"deterministic":refineMode;
|
|
3063
|
+
scanModeRef.current=mode;
|
|
2887
3064
|
if(mode!==refineMode)setRefineMode(mode);
|
|
2888
3065
|
try{
|
|
2889
3066
|
const et=active.effectiveTimings||[];
|
|
@@ -2912,7 +3089,14 @@
|
|
|
2912
3089
|
const job=await relayGetJob(refineJobId);
|
|
2913
3090
|
if(!live)return;
|
|
2914
3091
|
if(Array.isArray(job.statusLog))setRefineLog(job.statusLog);
|
|
2915
|
-
if(job.status==="done"){
|
|
3092
|
+
if(job.status==="done"){
|
|
3093
|
+
const finish=()=>{if(!live)return;setRefineSuggestions((job.result&&job.result.suggestions)||[]);setRefineSummary(job.result&&job.result.summary);setRefinePhase("done");};
|
|
3094
|
+
// deterministic mode floors the scanning phase at 2s so it doesn't flash by.
|
|
3095
|
+
const minMs=scanModeRef.current==="deterministic"?2000:0;
|
|
3096
|
+
const wait=Math.max(0,minMs-(Date.now()-scanStartedAtRef.current));
|
|
3097
|
+
if(wait>0){to=setTimeout(finish,wait);}else{finish();}
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
2916
3100
|
if(job.status==="error"){setRefineError(job.error||"The agent reported an error.");setRefinePhase("error");return;}
|
|
2917
3101
|
to=setTimeout(tick,500);
|
|
2918
3102
|
}catch(e){if(live){setRefineError("Lost connection to the relay.");setRefinePhase("error");}}
|
|
@@ -2951,7 +3135,7 @@
|
|
|
2951
3135
|
if(typeof document==="undefined")return;
|
|
2952
3136
|
const applied=liveAppliedRef.current;
|
|
2953
3137
|
const desired=new Map(); // el → css
|
|
2954
|
-
const add=(els,css)=>{ for(const el of els){ if(!el||(el.closest&&el.closest("[data-timeline-panel]")))continue; desired.set(el,css); } };
|
|
3138
|
+
const add=(els,css)=>{ for(const el of els){ if(!el||(el.closest&&el.closest("[data-timeline-panel],[data-tl-ui]")))continue; desired.set(el,css); } };
|
|
2955
3139
|
for(const item of entries){
|
|
2956
3140
|
const ov=registry.getPropOverrides(item.id);
|
|
2957
3141
|
if(!ov||!Object.keys(ov).length)continue; // only edited transitions go live
|
|
@@ -3019,8 +3203,29 @@
|
|
|
3019
3203
|
}else{
|
|
3020
3204
|
text="transition: "+et.map(decl).join(",\n ")+";";
|
|
3021
3205
|
}
|
|
3022
|
-
|
|
3023
|
-
showToast("
|
|
3206
|
+
const onCopied=()=>{setCopied(true);setTimeout(()=>setCopied(false),1500);showToast("Values copied");};
|
|
3207
|
+
const onCopyFail=()=>{showToast("Couldn't copy");};
|
|
3208
|
+
// navigator.clipboard needs a secure context (https/localhost); when the
|
|
3209
|
+
// host page is plain http — or Safari blocks the async API — fall back to
|
|
3210
|
+
// a hidden-textarea execCommand("copy") so copy still works everywhere.
|
|
3211
|
+
const legacyCopy=()=>{
|
|
3212
|
+
try{
|
|
3213
|
+
const ta=document.createElement("textarea");
|
|
3214
|
+
ta.value=text; ta.setAttribute("readonly","");
|
|
3215
|
+
ta.style.position="fixed"; ta.style.top="-9999px"; ta.style.opacity="0";
|
|
3216
|
+
document.body.appendChild(ta);
|
|
3217
|
+
ta.focus(); ta.select();
|
|
3218
|
+
try{ta.setSelectionRange(0,text.length);}catch(e){}
|
|
3219
|
+
const ok=document.execCommand("copy");
|
|
3220
|
+
document.body.removeChild(ta);
|
|
3221
|
+
ok?onCopied():onCopyFail();
|
|
3222
|
+
}catch(e){onCopyFail();}
|
|
3223
|
+
};
|
|
3224
|
+
try{
|
|
3225
|
+
if(navigator.clipboard&&navigator.clipboard.writeText){
|
|
3226
|
+
navigator.clipboard.writeText(text).then(onCopied).catch(legacyCopy);
|
|
3227
|
+
}else legacyCopy();
|
|
3228
|
+
}catch(e){legacyCopy();}
|
|
3024
3229
|
},[active,showToast]);
|
|
3025
3230
|
const resetOverrides=useCallback(()=>{if(active)registry.clearOverride(active.id);},[registry,active]);
|
|
3026
3231
|
// Accept → send an "apply" job so the agent writes the edited timings into
|
|
@@ -3203,27 +3408,93 @@
|
|
|
3203
3408
|
// small "Agent scanning transitions…" note in the header count
|
|
3204
3409
|
// slot. A failed scan just leaves the flat list (Rescan in
|
|
3205
3410
|
// Settings to retry) — it never traps the panel.
|
|
3411
|
+
// ── keyboard shortcuts (panel-focused, Cmd/Ctrl based) ──
|
|
3412
|
+
// Fire only when the panel is focused or hovered so we never hijack the
|
|
3413
|
+
// host app. A latest-values ref keeps the single window listener stable.
|
|
3414
|
+
const panelRootRef=useRef(null);
|
|
3415
|
+
const hoveredRef=useRef(false);
|
|
3416
|
+
const panelHotRef=useRef(false);
|
|
3417
|
+
const recomputeHot=useCallback(()=>{
|
|
3418
|
+
const el=panelRootRef.current;
|
|
3419
|
+
panelHotRef.current=hoveredRef.current||!!(el&&document.activeElement&&el.contains(document.activeElement));
|
|
3420
|
+
},[]);
|
|
3421
|
+
const kbdRef=useRef({});
|
|
3422
|
+
kbdRef.current={entries,active,acceptState,acceptDisabled:computeChanges(active).length===0,minimized,
|
|
3423
|
+
openRefine,resetOverrides,copyValues,onAccept,setActiveId,setMinimized,setPickOpen};
|
|
3424
|
+
useEffect(()=>{
|
|
3425
|
+
const onKey=e=>{
|
|
3426
|
+
const mod=e.metaKey||e.ctrlKey;
|
|
3427
|
+
if(!mod)return;
|
|
3428
|
+
const L=kbdRef.current;
|
|
3429
|
+
// Cmd+. always restores a minimized panel (panel root isn't in the DOM
|
|
3430
|
+
// then, so the hot/input gates can't apply). All other combos require
|
|
3431
|
+
// the panel to be focused/hovered and not typing in a field.
|
|
3432
|
+
const isRestore=e.key==="."&&L.minimized;
|
|
3433
|
+
recomputeHot();
|
|
3434
|
+
if(!isRestore){
|
|
3435
|
+
if(!panelHotRef.current)return;
|
|
3436
|
+
const t=e.target;
|
|
3437
|
+
if(t&&(t.tagName==="INPUT"||t.tagName==="TEXTAREA"||t.isContentEditable))return;
|
|
3438
|
+
}
|
|
3439
|
+
const handle=fn=>{e.preventDefault();e.stopPropagation();fn&&fn();};
|
|
3440
|
+
switch(e.key){
|
|
3441
|
+
case"a":case"A": // Accept changes (same enabled condition as the Accept button)
|
|
3442
|
+
handle(()=>{if(L.active&&!L.acceptDisabled&&L.acceptState!=="saving"&&L.acceptState!=="done")L.onAccept&&L.onAccept();});
|
|
3443
|
+
break;
|
|
3444
|
+
case"Backspace": // Reset values
|
|
3445
|
+
handle(()=>{if(L.active)L.resetOverrides&&L.resetOverrides();});
|
|
3446
|
+
break;
|
|
3447
|
+
case"c":case"C":{ // Copy values — only when no text is selected (native copy wins otherwise)
|
|
3448
|
+
const sel=window.getSelection();
|
|
3449
|
+
if(sel&&!sel.isCollapsed)return;
|
|
3450
|
+
handle(()=>{if(L.active)L.copyValues&&L.copyValues();});
|
|
3451
|
+
break;}
|
|
3452
|
+
case"r":case"R": // Toggle Refine panel
|
|
3453
|
+
handle(()=>{if(L.active)L.openRefine&&L.openRefine();});
|
|
3454
|
+
break;
|
|
3455
|
+
case"k":case"K": // Open transition picker
|
|
3456
|
+
handle(()=>{if(L.active)L.setPickOpen&&L.setPickOpen(true);});
|
|
3457
|
+
break;
|
|
3458
|
+
case"ArrowUp":
|
|
3459
|
+
case"ArrowDown":{ // Previous / next transition
|
|
3460
|
+
const list=L.entries||[];
|
|
3461
|
+
if(!list.length)break;
|
|
3462
|
+
const dir=e.key==="ArrowUp"?-1:1;
|
|
3463
|
+
const cur=L.active?list.findIndex(x=>x.id===L.active.id):-1;
|
|
3464
|
+
let ni=cur<0?(dir>0?0:list.length-1):cur+dir;
|
|
3465
|
+
ni=Math.max(0,Math.min(list.length-1,ni));
|
|
3466
|
+
if(list[ni])handle(()=>L.setActiveId&&L.setActiveId(list[ni].id));
|
|
3467
|
+
break;}
|
|
3468
|
+
case".": // Minimize / restore panel
|
|
3469
|
+
handle(()=>L.setMinimized&&L.setMinimized(v=>!v));
|
|
3470
|
+
break;
|
|
3471
|
+
}
|
|
3472
|
+
};
|
|
3473
|
+
window.addEventListener("keydown",onKey);
|
|
3474
|
+
return()=>window.removeEventListener("keydown",onKey);
|
|
3475
|
+
},[recomputeHot]);
|
|
3206
3476
|
const gate = !live
|
|
3207
3477
|
? (llmAvailable===null ? "loading" : "blocked")
|
|
3208
3478
|
: "ready";
|
|
3209
3479
|
return h(React.Fragment,null,
|
|
3210
|
-
render&&h("div",{className:"t-panel-slide","data-timeline-panel":true,
|
|
3480
|
+
render&&h("div",{className:"t-panel-slide","data-timeline-panel":true,ref:panelRootRef,
|
|
3481
|
+
onMouseEnter:()=>{hoveredRef.current=true;},onMouseLeave:()=>{hoveredRef.current=false;},
|
|
3211
3482
|
"data-open":panelOpen?"true":"false","data-phase":phase,style:{height:panelHeight+"px"}},
|
|
3212
3483
|
h("div",{className:cx("tl-resize-handle",resizing&&"dragging"),onMouseDown:startResize,title:"Drag to resize"}),
|
|
3213
3484
|
h("div",{className:"tl-panel-body"},
|
|
3214
3485
|
gate==="ready"
|
|
3215
3486
|
? h(React.Fragment,null,
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3487
|
+
h("div",{className:"tl-panel-main"},
|
|
3488
|
+
h(Header,{entries,active,onSelect:setActiveId,onReset:resetOverrides,onCopy:copyValues,copied,
|
|
3489
|
+
snap,setSnap,onMinimize:()=>setMinimized(true),onRefine:openRefine,refineActive:refineOpen,
|
|
3490
|
+
onAccept,acceptState,acceptDisabled:computeChanges(active).length===0,acceptError,
|
|
3491
|
+
scanning:groupScanState==="scanning",onRescan:rescanTransitions,pick:pickOpen,setPick:setPickOpen}),
|
|
3492
|
+
active
|
|
3493
|
+
?h(Body,{entry:active,onPropChange:(prop,o)=>setPropOverride(prop,o),snap})
|
|
3494
|
+
:h("div",{className:"tl-empty"},"Select a transition to inspect and edit it")),
|
|
3495
|
+
h(RefinePanel,{open:refineOpen,onClose:()=>setRefineOpen(false),phase:refinePhase,label:refineLabel,
|
|
3496
|
+
refineType,onType:changeRefineType,suggestions:refineSuggestions,summary:refineSummary,error:refineError,
|
|
3497
|
+
appliedIds,onApply:applySuggestion,onApplyAll:applyAllSuggestions,
|
|
3227
3498
|
mode:refineMode,onMode:changeRefineMode,llmAvailable,cliInstalled,onStart:startScan,
|
|
3228
3499
|
lanes:active?.effectiveTimings||[]}))
|
|
3229
3500
|
: h("div",{className:"tl-panel-main"},
|
|
@@ -3234,15 +3505,17 @@
|
|
|
3234
3505
|
"Please run the ",h("code",{className:"tl-code"},"/refine live"),
|
|
3235
3506
|
" command in your agent to enable live features, such as scanning and refining transitions.")))),
|
|
3236
3507
|
toast&&createPortal(
|
|
3237
|
-
h("div",{className:"tl-toast-wrap","aria-live":"polite"},
|
|
3508
|
+
h("div",{className:"tl-toast-wrap","data-tl-ui":"","aria-live":"polite"},
|
|
3238
3509
|
h("div",{className:cx("tl-toast",toast.closing&&"is-closing")},
|
|
3239
3510
|
h("span",{className:"tl-toast-ic"},
|
|
3240
3511
|
toast.loader?h(DotmLoader):h(Ic,{name:"accept",size:14})),
|
|
3241
|
-
|
|
3512
|
+
toast.loader
|
|
3513
|
+
? h("span",{className:"t-shimmer tl-scan-shimmer","data-text":toast.msg},toast.msg)
|
|
3514
|
+
: h("span",null,toast.msg))),
|
|
3242
3515
|
document.body),
|
|
3243
3516
|
),
|
|
3244
3517
|
),
|
|
3245
|
-
minimized&&h("div",{className:"tl-pill",onClick:()=>setMinimized(false)},
|
|
3518
|
+
minimized&&h("div",{className:"tl-pill","data-tl-ui":"",onClick:()=>setMinimized(false)},
|
|
3246
3519
|
h("span",{className:"tl-pill-label"},"Transitions"),
|
|
3247
3520
|
h("span",{className:cx("tl-pill-count",String(entries.length).length===1&&"is-single")},entries.length)),
|
|
3248
3521
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "transitions-refine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "Live, agent-driven Refine panel for CSS/Motion transitions — injects a timeline + Refine UI and runs transitions.dev suggestions via your coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|