transitions-refine 0.3.8 → 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 +834 -191
- 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);
|
|
@@ -244,7 +247,7 @@
|
|
|
244
247
|
padding: 6px 10px 6px 16px;
|
|
245
248
|
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
|
246
249
|
cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px;
|
|
247
|
-
box-shadow: var(--
|
|
250
|
+
box-shadow: var(--shadow-btn); transition: background 0.15s, scale 0.12s ease;
|
|
248
251
|
animation: tl-pill-in 220ms cubic-bezier(0.22, 1, 0.36, 1) both; }
|
|
249
252
|
@keyframes tl-pill-in { from { opacity: 0; transform: translateY(6px) scale(0.96); }
|
|
250
253
|
to { opacity: 1; transform: none; } }
|
|
@@ -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); }
|
|
@@ -369,7 +372,7 @@
|
|
|
369
372
|
.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
373
|
/* full-bleed divider between the Delay field and the Easing/Springs tabs
|
|
371
374
|
(Figma 580:9049 — spans the inspector edge-to-edge past the 16px padding) */
|
|
372
|
-
.tl-inspector-divider { height: 1px; background: var(--c-line); margin:
|
|
375
|
+
.tl-inspector-divider { height: 1px; background: var(--c-line); margin: 4px -16px; }
|
|
373
376
|
.tl-insp-label { font-size: 12px; line-height: 18px; color: #737373; margin: 10px 0 6px; }
|
|
374
377
|
|
|
375
378
|
/* ── tracks / ruler ── */
|
|
@@ -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); }
|
|
@@ -528,9 +544,11 @@
|
|
|
528
544
|
.tl-select-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
529
545
|
.tl-select-chev { display: flex; color: var(--c-ruler); flex: none; }
|
|
530
546
|
/* easing curve — exact Figma (node 580:11158): #f6f6f7, 8px radius, no grid */
|
|
547
|
+
/* overflow:visible so overshoot handles/lines spill past the box while
|
|
548
|
+
dragging instead of being clipped to the rectangle. */
|
|
531
549
|
.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; }
|
|
550
|
+
padding: 0; position: relative; overflow: visible; }
|
|
551
|
+
.tl-curve svg { display: block; cursor: crosshair; width: 100%; height: auto; overflow: visible; }
|
|
534
552
|
.tl-curve-handle { cursor: grab; }
|
|
535
553
|
.tl-curve-handle:active { cursor: grabbing; }
|
|
536
554
|
.tl-cubic-row { display: flex; gap: 4px; }
|
|
@@ -538,13 +556,13 @@
|
|
|
538
556
|
32px #f7f7f7 pill, hover overlay + inset focus ring. */
|
|
539
557
|
.tl-cubic-cell { flex: 1; min-width: 0; display: flex; flex-direction: row; align-items: center;
|
|
540
558
|
gap: 4px; height: 32px; box-sizing: border-box;
|
|
541
|
-
padding: 0 8px; border-radius: 8px; background:
|
|
559
|
+
padding: 0 8px; border-radius: 8px; background: var(--c-field-bg);
|
|
542
560
|
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:
|
|
561
|
+
.tl-cubic-cell:hover { background: var(--c-sec-h); }
|
|
544
562
|
.tl-cubic-cell:focus-within { box-shadow: inset 0 0 0 1px rgba(0,0,0,0.14); }
|
|
545
563
|
.tl-cubic-row input { flex: 1; min-width: 0; width: 100%; height: 100%; border: none; background: transparent;
|
|
546
564
|
box-sizing: border-box; padding: 0; text-align: left;
|
|
547
|
-
font: inherit; font-size: 12px; font-weight:
|
|
565
|
+
font: inherit; font-size: 12px; font-weight: 400; line-height: 16px; color: #1b1b1b; outline: none;
|
|
548
566
|
-moz-appearance: textfield; appearance: textfield; }
|
|
549
567
|
.tl-cubic-row input::-webkit-outer-spin-button, .tl-cubic-row input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; width: 0; }
|
|
550
568
|
.tl-cubic-row input::placeholder { color: #979797; }
|
|
@@ -682,9 +700,10 @@
|
|
|
682
700
|
.tl-menu-item:focus-visible { outline: none; background: #fff; box-shadow: inset 0 0 0 1px rgba(0,115,229,0.4); }
|
|
683
701
|
.tl-menu-item.disabled { color: var(--c-disabled); pointer-events: none; }
|
|
684
702
|
.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-
|
|
703
|
+
.tl-menu-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
704
|
+
.tl-menu-stack { flex: 1; min-width: 0; display: flex; flex-direction: row; align-items: baseline; gap: 6px; }
|
|
705
|
+
.tl-menu-stack .tl-menu-text { flex: 0 1 auto; }
|
|
706
|
+
.tl-menu-sub { flex: 0 0 auto; font-size: 13px; line-height: 18px; }
|
|
688
707
|
.tl-menu-dim { color: #979797; }
|
|
689
708
|
.tl-menu-check { display: flex; color: var(--c-text-strong); flex: none; }
|
|
690
709
|
.tl-menu-empty { padding: 10px 8px; color: var(--c-disabled); font-size: 13px; }
|
|
@@ -706,6 +725,83 @@
|
|
|
706
725
|
/* trailing chevron on link rows (Figma 581:2828 — #696969 right chevron) */
|
|
707
726
|
.tl-menu-chevr { display: flex; flex: none; color: #696969; }
|
|
708
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
|
+
|
|
734
|
+
/* ── settings dropdown sub-page slider (transitions.dev · 08-page-side-by-side) ──
|
|
735
|
+
One dropdown surface that pages internally: main settings ↔ keyboard
|
|
736
|
+
shortcuts. Both pages stay mounted so they can cross-slide; the surface
|
|
737
|
+
height follows the active page (card-resize-style) so it grows/shrinks
|
|
738
|
+
smoothly instead of jumping. Page 1 exits left, page 2 exits right. */
|
|
739
|
+
.tl-set-slide.t-page-slide {
|
|
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;
|
|
745
|
+
--page-slide-dur: 250ms; /* Fast — page slide */
|
|
746
|
+
--page-fade-dur: 250ms;
|
|
747
|
+
--page-slide-distance: 8px; /* Base distance */
|
|
748
|
+
--page-blur: 3px; /* Medium blur */
|
|
749
|
+
--page-stagger: 0ms;
|
|
750
|
+
--page-exit-enabled: 1;
|
|
751
|
+
--page-slide-ease: cubic-bezier(0.22, 1, 0.36, 1); /* Smooth ease out */
|
|
752
|
+
--page-fade-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
753
|
+
transition: height var(--page-slide-dur) var(--page-slide-ease);
|
|
754
|
+
overflow: hidden;
|
|
755
|
+
}
|
|
756
|
+
.tl-set-slide .t-page[data-page-id="1"] { --t-page-from-x: calc(var(--page-slide-distance) * -1); }
|
|
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); }
|
|
759
|
+
.tl-set-slide .t-page {
|
|
760
|
+
position: absolute; top: 0; left: 0; right: 0;
|
|
761
|
+
box-sizing: border-box; padding-left: 6px; padding-right: 6px;
|
|
762
|
+
opacity: 0; pointer-events: none;
|
|
763
|
+
transform: translateX(calc(var(--t-page-from-x, 0px) * var(--page-exit-enabled)));
|
|
764
|
+
filter: blur(calc(var(--page-blur) * var(--page-exit-enabled)));
|
|
765
|
+
transition:
|
|
766
|
+
opacity var(--page-fade-dur) var(--page-fade-ease),
|
|
767
|
+
transform var(--page-slide-dur) var(--page-slide-ease),
|
|
768
|
+
filter var(--page-slide-dur) var(--page-slide-ease);
|
|
769
|
+
will-change: opacity, transform, filter;
|
|
770
|
+
}
|
|
771
|
+
.tl-set-slide[data-page="1"] .t-page[data-page-id="1"],
|
|
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"] {
|
|
774
|
+
opacity: 1; pointer-events: auto; transform: translateX(0); filter: blur(0);
|
|
775
|
+
transition-delay: var(--page-stagger);
|
|
776
|
+
}
|
|
777
|
+
@media (prefers-reduced-motion: reduce) {
|
|
778
|
+
.tl-set-slide.t-page-slide,
|
|
779
|
+
.tl-set-slide .t-page { transition: none !important; }
|
|
780
|
+
}
|
|
781
|
+
/* sub-page back header — mirrors the menu-item row metrics (h-32, rounded-8,
|
|
782
|
+
13/16 label) so the surface feels continuous between pages. */
|
|
783
|
+
.tl-set-back { display: flex; align-items: center; gap: 4px; width: 100%; box-sizing: border-box;
|
|
784
|
+
min-height: 32px; padding: 0 8px 0 4px; border: none; background: transparent; border-radius: 8px;
|
|
785
|
+
font: inherit; font-size: 13px; font-weight: 500; line-height: 16px; color: #1b1b1b;
|
|
786
|
+
cursor: pointer; text-align: left; transition: background 0.1s ease, box-shadow 0.1s ease; }
|
|
787
|
+
.tl-set-back:hover { background: #f4f4f5; }
|
|
788
|
+
.tl-set-back:active { background: #ededee; }
|
|
789
|
+
.tl-set-back:focus-visible { outline: none; background: #fff; box-shadow: inset 0 0 0 1px rgba(0,115,229,0.4); }
|
|
790
|
+
/* reuse the right chevron flipped to point back; the menu chevron color */
|
|
791
|
+
.tl-set-back-ic { display: flex; flex: none; color: #696969; transform: scaleX(-1); }
|
|
792
|
+
.tl-set-back-title { flex: 1; min-width: 0; }
|
|
793
|
+
/* shortcut row — label left, keycap cluster right; same h-32 rhythm as rows */
|
|
794
|
+
.tl-kbd-row { display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
795
|
+
min-height: 32px; padding: 0 8px; font-size: 13px; line-height: 16px; color: #1b1b1b; }
|
|
796
|
+
.tl-kbd-row-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
797
|
+
.tl-kbd-cluster { display: flex; align-items: center; gap: 4px; flex: none; }
|
|
798
|
+
.tl-kbd-plus { color: #b8b8b8; font-size: 11px; line-height: 1; }
|
|
799
|
+
/* keycap — hairline via inset shadow (shadows over borders), tabular monospace */
|
|
800
|
+
.tl-kbd { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px;
|
|
801
|
+
padding: 0 5px; box-sizing: border-box; border-radius: 5px; background: #f4f4f5;
|
|
802
|
+
box-shadow: inset 0 0 0 1px #ececed; font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
|
803
|
+
font-size: 11px; line-height: 1; font-weight: 500; color: #4c4c4c; font-variant-numeric: tabular-nums; }
|
|
804
|
+
|
|
709
805
|
/* ═════ transitions.dev — menu dropdown (verbatim) ═════ */
|
|
710
806
|
.t-dropdown {
|
|
711
807
|
transform-origin: top left;
|
|
@@ -808,6 +904,8 @@
|
|
|
808
904
|
}
|
|
809
905
|
/* below-variant for triggers near the panel top edge */
|
|
810
906
|
.t-tt.tl-tt-below { bottom: auto; top: calc(100% + 8px); transform-origin: 50% 0; font-size: 12px; }
|
|
907
|
+
/* small gray shortcut hint inside a tooltip (matches the cheat-sheet keys) */
|
|
908
|
+
.tl-tt-kbd { margin-left: 8px; opacity: 0.4; }
|
|
811
909
|
/* multi-line message variant (e.g. Accept error) — long text wraps to a
|
|
812
910
|
readable measure instead of one overflowing line. Right-anchored so the
|
|
813
911
|
wide bubble grows leftward and never clips off the panel edge. Mirrors the
|
|
@@ -818,6 +916,12 @@
|
|
|
818
916
|
transform: translate(0, 0) scale(var(--tt-scale)); }
|
|
819
917
|
.t-tt-wrap:hover .t-tt.tl-tt-msg,
|
|
820
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); }
|
|
821
925
|
|
|
822
926
|
/* ═════ transitions.dev — panel reveal (per-phase distance/ease/scale) ═════ */
|
|
823
927
|
/* base = closed / close-end state: resolved travel + scale come from the
|
|
@@ -899,7 +1003,7 @@
|
|
|
899
1003
|
.pc-btn.primary:hover { background: #000; }
|
|
900
1004
|
|
|
901
1005
|
.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); }
|
|
1006
|
+
.tl-refine-btn.is-active::before { background: var(--c-blue-bg-a); background: color(display-p3 0 0.451 0.898 / 0.12); }
|
|
903
1007
|
|
|
904
1008
|
/* refine panel slides in from the right using transitions.dev panel reveal
|
|
905
1009
|
tokens (translate + opacity + cross-blur, per-phase open/close dur/ease). */
|
|
@@ -949,7 +1053,7 @@
|
|
|
949
1053
|
/* header: title block (left) + Agent/Deterministic mode dropdown (right) */
|
|
950
1054
|
/* match the timeline top bar exactly: 7.5px/14px padding, centered, 52px tall (36px row + 15 + 1px border) */
|
|
951
1055
|
.tl-refine-head { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between;
|
|
952
|
-
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;
|
|
953
1057
|
border-bottom: 1px solid #f0f0f0; }
|
|
954
1058
|
.tl-refine-titles { min-width: 0; }
|
|
955
1059
|
.tl-refine-titles h3 { font-size: 13px; font-weight: 500; line-height: 14px; color: #17181c; }
|
|
@@ -967,8 +1071,10 @@
|
|
|
967
1071
|
width: 36px; height: 36px; border: none; background: transparent; border-radius: 60px; cursor: pointer;
|
|
968
1072
|
color: #676767; transition: background 0.14s ease, color 0.14s ease; }
|
|
969
1073
|
.tl-refine-close:hover { background: rgba(170,170,170,0.10); color: #17181c; }
|
|
970
|
-
/* refine-type tabs
|
|
971
|
-
|
|
1074
|
+
/* refine-type tabs — toolbar strip (Figma 587:9258): 48px white bar, hairline
|
|
1075
|
+
bottom divider, segments inset 16px and packed gap:0 like a segmented control */
|
|
1076
|
+
.tl-refine-tabs { flex: 0 0 auto; box-sizing: border-box; display: flex; align-items: center; gap: 0;
|
|
1077
|
+
height: 48px; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; }
|
|
972
1078
|
.tl-refine-tab { height: 32px; padding: 6px 12px; border: none; background: transparent; cursor: pointer;
|
|
973
1079
|
border-radius: 8px; font: inherit; font-size: 13px; font-weight: 500; line-height: 14px; color: #676767;
|
|
974
1080
|
transition: background 0.14s ease, color 0.14s ease; }
|
|
@@ -976,6 +1082,49 @@
|
|
|
976
1082
|
.tl-refine-tab[aria-selected="true"] { background: rgba(170,170,170,0.10); color: #17181c; }
|
|
977
1083
|
.tl-refine-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column;
|
|
978
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
|
+
}
|
|
979
1128
|
/* idle + unavailable share a vertically-centred column */
|
|
980
1129
|
.tl-refine-center { flex: 1; display: flex; flex-direction: column; align-items: center;
|
|
981
1130
|
justify-content: center; gap: 12px; text-align: center; padding: 16px 0; }
|
|
@@ -1021,7 +1170,8 @@
|
|
|
1021
1170
|
height var(--resize-dur) var(--resize-ease),
|
|
1022
1171
|
border-radius var(--resize-dur) var(--resize-ease),
|
|
1023
1172
|
color var(--resize-dur) var(--resize-ease),
|
|
1024
|
-
box-shadow var(--resize-dur) var(--resize-ease)
|
|
1173
|
+
box-shadow var(--resize-dur) var(--resize-ease),
|
|
1174
|
+
scale 0.12s ease; }
|
|
1025
1175
|
.tl-scan-morph::before { content: ""; position: absolute; inset: 0; border-radius: inherit;
|
|
1026
1176
|
background: var(--c-blue-bg);
|
|
1027
1177
|
background: color(display-p3 0 0.451 0.898 / 0.06);
|
|
@@ -1033,7 +1183,7 @@
|
|
|
1033
1183
|
.tl-refine-foot .tl-scan-morph { margin-bottom: 20px; }
|
|
1034
1184
|
.tl-scan-morph:hover:not(:disabled):not(.is-scanning)::before {
|
|
1035
1185
|
background: var(--c-blue-bg-h); background: color(display-p3 0 0.451 0.898 / 0.10); }
|
|
1036
|
-
.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; }
|
|
1037
1187
|
.tl-scan-morph:active:not(.is-scanning):not(:disabled)::before {
|
|
1038
1188
|
background: var(--c-blue-bg-h); background: color(display-p3 0 0.451 0.898 / 0.10); }
|
|
1039
1189
|
.tl-scan-morph:disabled:not(.is-scanning) { cursor: default; color: var(--c-blue); }
|
|
@@ -1043,12 +1193,12 @@
|
|
|
1043
1193
|
color: var(--c-blue-pressed); opacity: 0.5; }
|
|
1044
1194
|
.tl-scan-morph.is-scanning { width: 100%; height: 49px; border-radius: 60px;
|
|
1045
1195
|
padding: 8px 16px; cursor: default; color: #676767; background: transparent;
|
|
1046
|
-
box-shadow:
|
|
1196
|
+
box-shadow: var(--shadow-btn); }
|
|
1047
1197
|
.tl-scan-morph.is-scanning::before {
|
|
1048
1198
|
background: #fff;
|
|
1049
1199
|
transition: background var(--resize-dur) var(--resize-ease); }
|
|
1050
1200
|
.tl-scan-morph.is-scanning::after {
|
|
1051
|
-
box-shadow: inset
|
|
1201
|
+
box-shadow: var(--shadow-btn-inset);
|
|
1052
1202
|
transition: box-shadow var(--resize-dur) var(--resize-ease); }
|
|
1053
1203
|
/* Two faces share one grid cell and cross-blur (icon swap, 09) between the
|
|
1054
1204
|
label and the icon+status; whichever is hidden fades out with blur+scale.
|
|
@@ -1131,10 +1281,16 @@
|
|
|
1131
1281
|
@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
1282
|
@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
1283
|
@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
|
-
|
|
1284
|
+
/* background-scan note in the header count slot — dot-matrix loader + shimmer
|
|
1285
|
+
text (transitions.dev shimmer text 15). Hover shows the tooltip. */
|
|
1286
|
+
/* the tooltip wrap fills the count slot's height; center its content so the
|
|
1287
|
+
loader + text sit on the slot's vertical midline instead of its bottom. */
|
|
1288
|
+
.tl-header-count .t-tt-wrap { display: inline-flex; align-items: center; align-self: center; }
|
|
1136
1289
|
.tl-scan-note { cursor: help; color: var(--c-count);
|
|
1137
|
-
|
|
1290
|
+
display: inline-flex; align-items: center; gap: 6px; }
|
|
1291
|
+
.tl-scan-note .tl-dotm { width: 14px; height: 14px; }
|
|
1292
|
+
.tl-scan-shimmer { --shimmer-dur: 2000ms; --shimmer-base: var(--c-count); --shimmer-highlight: #17181c;
|
|
1293
|
+
--shimmer-band: 400%; --shimmer-ease: linear; }
|
|
1138
1294
|
/* loading status text — transitions.dev shimmer text (15) + text states swap (04) */
|
|
1139
1295
|
.tl-refine-status-text {
|
|
1140
1296
|
--shimmer-dur: 2000ms; --shimmer-base: #9a9a9a; --shimmer-highlight: #17181c;
|
|
@@ -1165,7 +1321,7 @@
|
|
|
1165
1321
|
font: inherit; transition: background 0.12s ease; }
|
|
1166
1322
|
.tl-mode-row:hover { background: rgba(170,170,170,0.08); }
|
|
1167
1323
|
.tl-mode-row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
1168
|
-
.tl-mode-row-title { font-size:
|
|
1324
|
+
.tl-mode-row-title { font-size: 13px; font-weight: 500; line-height: 16px; color: #1b1b1b; }
|
|
1169
1325
|
.tl-mode-row-desc { font-size: 12px; font-weight: 400; line-height: 18px; color: #676767; text-wrap: balance; }
|
|
1170
1326
|
.tl-mode-row-check { flex: 0 0 auto; width: 16px; height: 16px; display: flex; color: #17181c; }
|
|
1171
1327
|
@keyframes tl-fade-up { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
|
@@ -1212,37 +1368,56 @@
|
|
|
1212
1368
|
transition: background 0.12s ease, scale 0.12s ease, opacity 0.12s ease; }
|
|
1213
1369
|
.tl-pill-btn .tl-dim { color: #696969; }
|
|
1214
1370
|
.tl-pill-btn:hover:not(:disabled) { background: #fafafa; }
|
|
1215
|
-
.tl-pill-btn:active:not(:disabled) { scale: 0.
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
.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; }
|
|
1219
1380
|
.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
|
-
|
|
1381
|
+
/* Apply ⟷ Applied: a LEADING check is revealed first and pushes the text to
|
|
1382
|
+
the right, then the differing SUFFIX cross-blurs. The shared stem "Appl"
|
|
1383
|
+
stays put while "y" ⟷ "ied" cross-blur in a slot whose width tweens between
|
|
1384
|
+
the two measured widths. Both the leading check space AND the suffix slot
|
|
1385
|
+
use the SAME resize token (--apply-resize-*: card resize, smooth ease out),
|
|
1386
|
+
so the whole button grows in one synchronized motion. The check lives in its
|
|
1387
|
+
own overflow:hidden box whose width tweens 0 -> (icon + gap), shoving the
|
|
1388
|
+
text rightward — the check itself stroke-draws IN PLACE (no translate). */
|
|
1389
|
+
.tl-apply-swap { position: relative; display: inline-flex; align-items: center;
|
|
1390
|
+
vertical-align: middle; line-height: 14px;
|
|
1391
|
+
--apply-resize-dur: 400ms; --apply-resize-ease: cubic-bezier(0.22, 1, 0.36, 1); }
|
|
1392
|
+
/* leading check slot: collapsed to width 0 at rest; expands to the measured
|
|
1393
|
+
icon+gap width (--chk-w) when applied, using the same token as the suffix
|
|
1394
|
+
resize. overflow:hidden clips the pinned-left check so it reveals in place
|
|
1395
|
+
(left edge fixed) rather than translating. */
|
|
1396
|
+
.tl-apply-check { display: inline-flex; align-items: center; overflow: hidden;
|
|
1397
|
+
flex: 0 0 auto; width: 0;
|
|
1398
|
+
transition: width var(--apply-resize-dur) var(--apply-resize-ease); }
|
|
1399
|
+
.tl-apply-swap[data-state="applied"] .tl-apply-check { width: var(--chk-w, 19px); }
|
|
1400
|
+
.tl-apply-stem { display: inline-block; }
|
|
1401
|
+
.tl-apply-suffix { position: relative; display: inline-block; width: var(--sfx-y, auto);
|
|
1402
|
+
transition: width var(--apply-resize-dur) var(--apply-resize-ease); }
|
|
1403
|
+
.tl-apply-swap[data-state="applied"] .tl-apply-suffix { width: var(--sfx-ied, auto); }
|
|
1404
|
+
.tl-apply-seg { white-space: pre;
|
|
1230
1405
|
transition: opacity 180ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
1231
1406
|
filter 180ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
1232
1407
|
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); }
|
|
1408
|
+
.tl-apply-y { display: inline-block; }
|
|
1409
|
+
.tl-apply-ied { position: absolute; top: 0; left: 0; display: inline-block; }
|
|
1410
|
+
.tl-apply-swap .tl-apply-y { opacity: 1; filter: blur(0); }
|
|
1411
|
+
.tl-apply-swap .tl-apply-ied { opacity: 0; filter: blur(2px); }
|
|
1412
|
+
.tl-apply-swap[data-state="applied"] .tl-apply-y { opacity: 0; filter: blur(2px); }
|
|
1413
|
+
.tl-apply-swap[data-state="applied"] .tl-apply-ied { opacity: 1; filter: blur(0); }
|
|
1240
1414
|
@media (prefers-reduced-motion: reduce) {
|
|
1241
|
-
.tl-apply-
|
|
1415
|
+
.tl-apply-suffix, .tl-apply-seg { transition: none !important; }
|
|
1242
1416
|
}
|
|
1243
1417
|
/* success check (transitions.dev 10-success-check): stroke-draw on the path with
|
|
1244
1418
|
a plain opacity fade — IN PLACE, no rotate / Y-bob / scale, so the check never
|
|
1245
|
-
moves while it draws.
|
|
1419
|
+
moves while it draws. It is the leading element, unclipped by the .tl-apply-check
|
|
1420
|
+
width tween that pushes the text. GRAY stroke. */
|
|
1246
1421
|
.tl-sug-check { display: inline-flex; opacity: 0; will-change: opacity; }
|
|
1247
1422
|
.tl-sug-check svg { display: block; overflow: visible; }
|
|
1248
1423
|
/* stroke-dasharray = path.getTotalLength() of ICONS.accept "M4 8.4268 L6.46155
|
|
@@ -1278,6 +1453,7 @@
|
|
|
1278
1453
|
.tl-refine-results .t-stagger-line { transition: none !important; }
|
|
1279
1454
|
.t-resize, .tl-scan-morph { transition: none !important; }
|
|
1280
1455
|
.tl-pill-btn.tl-apply-btn { transition: background 0.12s ease, scale 0.12s ease, opacity 0.12s ease !important; }
|
|
1456
|
+
.tl-apply-check { transition: none !important; }
|
|
1281
1457
|
.tl-sug-check { animation: none !important; opacity: 1; }
|
|
1282
1458
|
.tl-sug-check svg path { animation: none !important; stroke-dashoffset: 0 !important; }
|
|
1283
1459
|
.tl-scan-face { transition: none !important; }
|
|
@@ -1530,7 +1706,10 @@
|
|
|
1530
1706
|
_sched(){if(this.rafId!==null)return;this.rafId=requestAnimationFrame(()=>{this.rafId=null;if(this.running&&!this.paused)this.scan();});}
|
|
1531
1707
|
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
1708
|
_proc(el,seen){
|
|
1533
|
-
|
|
1709
|
+
// Skip the Refine UI itself: the panel ([data-timeline-panel]) plus any of
|
|
1710
|
+
// our surfaces that portal OUT of it (dropdown menus, toasts) — those carry
|
|
1711
|
+
// [data-tl-ui]. Without this the scanner lists the tool's own transitions.
|
|
1712
|
+
if(el.closest("[data-timeline-panel],[data-tl-ui]"))return;
|
|
1534
1713
|
const s=getComputedStyle(el); const rp=s.transitionProperty;
|
|
1535
1714
|
if(!rp||rp==="none"||rp==="all")return;
|
|
1536
1715
|
const props=rp.split(",").map(p=>p.trim());
|
|
@@ -1663,6 +1842,8 @@
|
|
|
1663
1842
|
// accept checkmark (Figma node 580:9057) — slimmer 1.5px check centered in a
|
|
1664
1843
|
// square 16-frame (the glyph spans ~50% width, like the source icon frame).
|
|
1665
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"/>`};
|
|
1666
1847
|
function Ic({name, size=16}){
|
|
1667
1848
|
const ic = ICONS[name];
|
|
1668
1849
|
if(!ic) return null;
|
|
@@ -1748,7 +1929,7 @@
|
|
|
1748
1929
|
const origin=pos?pos.origin:"top-left";
|
|
1749
1930
|
const cls=closing?"is-closing":shown?"is-open":"";
|
|
1750
1931
|
return createPortal(
|
|
1751
|
-
h("div",{ref,className:cx("t-dropdown","tl-menu",cls),"data-origin":origin,style},children),
|
|
1932
|
+
h("div",{ref,className:cx("t-dropdown","tl-menu",cls),"data-origin":origin,"data-tl-ui":"",style},children),
|
|
1752
1933
|
target);
|
|
1753
1934
|
}
|
|
1754
1935
|
|
|
@@ -1759,21 +1940,153 @@
|
|
|
1759
1940
|
right);
|
|
1760
1941
|
}
|
|
1761
1942
|
|
|
1943
|
+
// Settings dropdown body that pages between the main menu and the keyboard-
|
|
1944
|
+
// shortcuts cheat-sheet (transitions.dev · 08-page-side-by-side). Both pages
|
|
1945
|
+
// stay mounted so they cross-slide; the container height follows the active
|
|
1946
|
+
// page so the one dropdown surface resizes smoothly between pages. Lives
|
|
1947
|
+
// inside the Dropdown portal, so its layout effect fires when the surface
|
|
1948
|
+
// mounts (the parent Header doesn't re-render on the portal's own mount).
|
|
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";
|
|
1952
|
+
useLayoutEffect(()=>{
|
|
1953
|
+
const slide=slideRef.current; if(!slide) return;
|
|
1954
|
+
const active=view==="shortcuts"?p2.current:view==="theme"?p3.current:p1.current;
|
|
1955
|
+
if(active) slide.style.height=active.offsetHeight+"px";
|
|
1956
|
+
});
|
|
1957
|
+
// Escape steps back from a sub-page first, then closes the dropdown —
|
|
1958
|
+
// mirrors the picker's nested-dismiss behaviour.
|
|
1959
|
+
useEffect(()=>{
|
|
1960
|
+
const onKey=e=>{ if(e.key!=="Escape")return;
|
|
1961
|
+
if(view!=="main"){onBack&&onBack();} else {onClose&&onClose();} };
|
|
1962
|
+
document.addEventListener("keydown",onKey);
|
|
1963
|
+
return()=>document.removeEventListener("keydown",onKey);
|
|
1964
|
+
},[view,onBack,onClose]);
|
|
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));
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1762
1971
|
// ── refine relay client ──
|
|
1763
1972
|
// Talks to the local relay (server/relay.mjs). The browser posts a job and
|
|
1764
1973
|
// polls; the relay answers each job with one agent run (one-shot).
|
|
1765
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
|
+
|
|
1766
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
|
+
}
|
|
1767
2070
|
const r = await fetch(RELAY_URL+"/jobs",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({request})});
|
|
1768
2071
|
if(!r.ok) throw new Error("relay POST /jobs failed ("+r.status+")");
|
|
1769
2072
|
return r.json(); // { id, status }
|
|
1770
2073
|
}
|
|
1771
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
|
+
}
|
|
1772
2084
|
const r = await fetch(RELAY_URL+"/jobs/"+id);
|
|
1773
2085
|
if(!r.ok) throw new Error("relay GET /jobs/:id failed ("+r.status+")");
|
|
1774
2086
|
return r.json();
|
|
1775
2087
|
}
|
|
1776
2088
|
async function relayHealth(){
|
|
2089
|
+
if(TX_MOCK) return { ok:true, auto:true, llmAvailable:true, cliInstalled:true, jobs:0 };
|
|
1777
2090
|
const r = await fetch(RELAY_URL+"/health");
|
|
1778
2091
|
if(!r.ok) throw new Error("relay /health failed ("+r.status+")");
|
|
1779
2092
|
return r.json(); // { ok, auto, llmAvailable, jobs }
|
|
@@ -1825,25 +2138,32 @@
|
|
|
1825
2138
|
const names=[...new Set(matches.map(t=>t.member))];
|
|
1826
2139
|
return names.length===1?names[0]:matches[0].member;
|
|
1827
2140
|
}
|
|
1828
|
-
// Apply button — "Apply" ⟷ "Applied"
|
|
1829
|
-
//
|
|
1830
|
-
//
|
|
1831
|
-
//
|
|
1832
|
-
//
|
|
1833
|
-
//
|
|
1834
|
-
//
|
|
2141
|
+
// Apply button — "Apply" ⟷ "✓ Applied". On apply, a LEADING gray check is
|
|
2142
|
+
// revealed first and pushes the text to the right: .tl-apply-check tweens its
|
|
2143
|
+
// width 0 -> (icon + 6px gap) using the SAME resize token as the suffix slot
|
|
2144
|
+
// (--apply-resize-*, smooth ease out), so the check expansion + the "y" -> "ied"
|
|
2145
|
+
// suffix resize grow the button in one synchronized motion. The stem "Appl"
|
|
2146
|
+
// stays put and "y" ⟷ "ied" cross-blur (transitions.dev 04). The check itself
|
|
2147
|
+
// stroke-draws IN PLACE (transitions.dev 10, no rotate/bob/scale/translate) —
|
|
2148
|
+
// it is pinned to the slot's left edge and the overflow clip reveals it, so it
|
|
2149
|
+
// never translates. The same <button> node is reused across states.
|
|
1835
2150
|
function ApplyBtn({applied,onApply}){
|
|
1836
2151
|
const ref=useRef(null);
|
|
1837
2152
|
const prev=useRef(applied);
|
|
1838
|
-
// measure both
|
|
1839
|
-
// so the
|
|
2153
|
+
// measure both suffixes once (after fonts/layout) and pin the two slot widths
|
|
2154
|
+
// so the suffix can tween between "y" and the checked "ied".
|
|
1840
2155
|
useLayoutEffect(()=>{
|
|
1841
2156
|
const btn=ref.current;if(!btn)return;
|
|
1842
2157
|
const sw=btn.querySelector(".tl-apply-swap"); if(!sw) return;
|
|
2158
|
+
const CHK_GAP=6; // space between the leading check and "Applied"
|
|
1843
2159
|
const measure=()=>{
|
|
1844
|
-
const
|
|
1845
|
-
if(
|
|
1846
|
-
if(
|
|
2160
|
+
const y=sw.querySelector(".tl-apply-y"), ied=sw.querySelector(".tl-apply-ied");
|
|
2161
|
+
if(y) sw.style.setProperty("--sfx-y", Math.ceil(y.getBoundingClientRect().width)+"px");
|
|
2162
|
+
if(ied) sw.style.setProperty("--sfx-ied", Math.ceil(ied.getBoundingClientRect().width)+"px");
|
|
2163
|
+
// leading check slot target width = icon natural width + gap; it stays
|
|
2164
|
+
// measurable even while clipped (the inner .tl-sug-check keeps its size).
|
|
2165
|
+
const chk=sw.querySelector(".tl-sug-check");
|
|
2166
|
+
if(chk) sw.style.setProperty("--chk-w", (Math.ceil(chk.getBoundingClientRect().width)+CHK_GAP)+"px");
|
|
1847
2167
|
};
|
|
1848
2168
|
measure();
|
|
1849
2169
|
if(document.fonts&&document.fonts.ready) document.fonts.ready.then(measure).catch(()=>{});
|
|
@@ -1873,23 +2193,32 @@
|
|
|
1873
2193
|
className:cx("tl-pill-btn tl-apply-btn",applied&&"is-applied"),
|
|
1874
2194
|
disabled:applied,onClick:applied?undefined:onApply},
|
|
1875
2195
|
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"},
|
|
2196
|
+
h("span",{className:"tl-apply-check"},
|
|
1878
2197
|
h("span",{className:"tl-sug-check","data-state":"out","aria-hidden":"true"},
|
|
1879
2198
|
h("svg",{width:13,height:13,viewBox:"0 0 16 16",fill:"none",
|
|
1880
2199
|
xmlns:"http://www.w3.org/2000/svg",style:{display:"block"}},
|
|
1881
2200
|
h("path",{d:"M4 8.4268L6.46155 11.19223L12 4.97001",stroke:"currentColor",
|
|
1882
|
-
strokeWidth:1.5,strokeLinecap:"round",strokeLinejoin:"round"}))),
|
|
1883
|
-
|
|
2201
|
+
strokeWidth:1.5,strokeLinecap:"round",strokeLinejoin:"round"})))),
|
|
2202
|
+
h("span",{className:"tl-apply-stem"},"Appl"),
|
|
2203
|
+
h("span",{className:"tl-apply-suffix"},
|
|
2204
|
+
h("span",{className:"tl-apply-seg tl-apply-y"},"y"),
|
|
2205
|
+
h("span",{className:"tl-apply-seg tl-apply-ied"},"ied"))));
|
|
1884
2206
|
}
|
|
1885
|
-
function RefineResults({suggestions,appliedIds,onApply,lanes}){
|
|
2207
|
+
function RefineResults({suggestions,appliedIds,onApply,lanes,revealedRef,active=true}){
|
|
1886
2208
|
const ref=useRef(null);
|
|
1887
2209
|
useEffect(()=>{
|
|
1888
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; }
|
|
1889
2217
|
el.classList.remove("is-shown");
|
|
1890
2218
|
void el.offsetHeight; // reflow from the start state before playing
|
|
1891
2219
|
el.classList.add("is-shown");
|
|
1892
|
-
|
|
2220
|
+
if(revealedRef) revealedRef.current=true;
|
|
2221
|
+
},[active]);
|
|
1893
2222
|
const delay=(i)=>({transitionDelay:"calc(var(--stagger-stagger) * "+i+")"});
|
|
1894
2223
|
// category chip label + color modifier, keyed by the suggestion kind
|
|
1895
2224
|
const CAT={easing:["Easing","is-easing"],duration:["Duration","is-duration"],
|
|
@@ -1923,7 +2252,7 @@
|
|
|
1923
2252
|
: h(React.Fragment,null,
|
|
1924
2253
|
s.title&&h("div",{className:"tl-sug-title"},s.title),
|
|
1925
2254
|
(s.from||s.to)&&h("div",{className:"tl-sug-delta"},
|
|
1926
|
-
|
|
2255
|
+
s.from&&h("span",{className:"tl-sug-from"},s.from),
|
|
1927
2256
|
s.to&&h("span",{className:"tl-sug-to"},s.to))),
|
|
1928
2257
|
s.reason&&h("div",{className:"tl-sug-reason"},s.reason)),
|
|
1929
2258
|
applyBtn));
|
|
@@ -1937,6 +2266,11 @@
|
|
|
1937
2266
|
const[modeOpen,setModeOpen]=useState(false);
|
|
1938
2267
|
const[statusIx,setStatusIx]=useState(0);
|
|
1939
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]);
|
|
1940
2274
|
useEffect(()=>{
|
|
1941
2275
|
let raf, to;
|
|
1942
2276
|
if(open){
|
|
@@ -1977,14 +2311,11 @@
|
|
|
1977
2311
|
// null (unknown / still probing) is treated as ready so the panel doesn't
|
|
1978
2312
|
// flash the "unavailable" copy before /health resolves.
|
|
1979
2313
|
const agentReady = !agentMode || llmAvailable!==false;
|
|
1980
|
-
const typeDesc = (REFINE_TYPES.find(t=>t.key===refineType)||REFINE_TYPES[0]).desc;
|
|
1981
2314
|
const modeLabel = (REFINE_MODES.find(m=>m.key===mode)||REFINE_MODES[0]).label;
|
|
1982
|
-
// Idle empty-state copy is mode-aware
|
|
1983
|
-
// agent-oriented desc; Deterministic has no
|
|
1984
|
-
// math-snap behavior (and that "replace" needs
|
|
1985
|
-
|
|
1986
|
-
? "Deterministic mode can't pick a recipe to replace the transition — switch to Agent mode for replacements."
|
|
1987
|
-
: "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).
|
|
1988
2319
|
// One persistent control for the whole foot: in idle/done/error it's the
|
|
1989
2320
|
// pill button; while scanning it carries `.is-scanning` and the same DOM
|
|
1990
2321
|
// node morphs (card resize) into the loading rectangle with the border-beam.
|
|
@@ -2005,25 +2336,58 @@
|
|
|
2005
2336
|
h(ShimmerSwapText,{text:REFINE_STATUS[statusIx]}))));
|
|
2006
2337
|
};
|
|
2007
2338
|
|
|
2008
|
-
|
|
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;
|
|
2009
2383
|
if(phase==="scanning"){
|
|
2010
|
-
body = h("div",{className:"tl-refine-center"});
|
|
2011
2384
|
foot = startBtn();
|
|
2012
2385
|
} else if(phase==="error"){
|
|
2013
|
-
body = h("div",{className:"tl-refine-center"},
|
|
2014
|
-
h("div",{className:"tl-refine-unavail-title"},"Something went wrong."),
|
|
2015
|
-
h("div",{className:"tl-refine-unavail-text"},error||"The agent reported an error."));
|
|
2016
2386
|
foot = startBtn({label:"Try again"});
|
|
2017
2387
|
} else if(phase==="done"){
|
|
2018
2388
|
if(visible.length===0){
|
|
2019
|
-
const emptyMsg = refineType==="replace"
|
|
2020
|
-
? "I didn't find any transition that would be a good fit as a replacement."
|
|
2021
|
-
: "Already aligned to the transitions.dev motion tokens. Nothing to refine.";
|
|
2022
|
-
body = h("div",{className:"tl-refine-center"},
|
|
2023
|
-
h("p",{className:"tl-refine-idle-text"},emptyMsg));
|
|
2024
2389
|
foot = startBtn({label:"Scan again"});
|
|
2025
2390
|
} else {
|
|
2026
|
-
body = h(RefineResults,{suggestions:visible,appliedIds,onApply,lanes:lanes||[]});
|
|
2027
2391
|
footBar = true;
|
|
2028
2392
|
foot = h(React.Fragment,null,
|
|
2029
2393
|
h("button",{className:"tl-pill-btn",disabled:pending.length===0,
|
|
@@ -2032,19 +2396,7 @@
|
|
|
2032
2396
|
h("button",{className:"tl-pill-btn",onClick:onStart},"Scan again"));
|
|
2033
2397
|
}
|
|
2034
2398
|
} else { // idle
|
|
2035
|
-
|
|
2036
|
-
const cliMissing = cliInstalled===false;
|
|
2037
|
-
body = h("div",{className:"tl-refine-center"},
|
|
2038
|
-
h("div",{className:"tl-refine-unavail-title"},"Agent unavailable."),
|
|
2039
|
-
cliMissing
|
|
2040
|
-
? 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.")
|
|
2041
|
-
: h("p",{className:"tl-refine-unavail-text"},"Run the ",h("code",{className:"tl-code"},"/refine live")," command in your agent to enable live functionality."));
|
|
2042
|
-
foot = startBtn({disabled:true});
|
|
2043
|
-
} else {
|
|
2044
|
-
body = h("div",{className:"tl-refine-center"},
|
|
2045
|
-
h("p",{className:"tl-refine-idle-text"},idleDesc));
|
|
2046
|
-
foot = startBtn({});
|
|
2047
|
-
}
|
|
2399
|
+
foot = startBtn(agentReady?{}:{disabled:true});
|
|
2048
2400
|
}
|
|
2049
2401
|
|
|
2050
2402
|
return h("div",{className:"tl-refine-panel","data-open":panelOpen?"true":"false","data-phase":slidePhase,
|
|
@@ -2072,7 +2424,11 @@
|
|
|
2072
2424
|
REFINE_TYPES.map(t=>h("button",{key:t.key,className:"tl-refine-tab",role:"tab",
|
|
2073
2425
|
"aria-selected":refineType===t.key?"true":"false",
|
|
2074
2426
|
onClick:()=>{if(t.key!==refineType)onType(t.key);}},t.label))),
|
|
2075
|
-
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"))),
|
|
2076
2432
|
h("div",{className:cx("tl-refine-foot",footBar&&"is-bar")},foot)));
|
|
2077
2433
|
}
|
|
2078
2434
|
|
|
@@ -2096,9 +2452,27 @@
|
|
|
2096
2452
|
return out;
|
|
2097
2453
|
}
|
|
2098
2454
|
|
|
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);
|
|
2455
|
+
function Header({entries,active,onSelect,onReset,onCopy,copied,snap,setSnap,onMinimize,onRefine,refineActive,onAccept,acceptState,acceptDisabled,acceptError,scanning,onRescan,pick,setPick}){
|
|
2101
2456
|
const[setg,setSetg]=useState(false);
|
|
2457
|
+
// settings dropdown sub-page (main menu ↔ keyboard-shortcuts cheat sheet)
|
|
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"}];
|
|
2462
|
+
const isMac=typeof navigator!=="undefined"&&/mac/i.test((navigator.userAgentData&&navigator.userAgentData.platform)||navigator.platform||navigator.userAgent||"");
|
|
2463
|
+
const modKey=isMac?"Cmd":"Ctrl";
|
|
2464
|
+
// small gray shortcut hint appended inside a tooltip (e.g. "⌘A")
|
|
2465
|
+
const ttHint=(k)=>h("span",{className:"tl-tt-kbd"},(isMac?"\u2318":"Ctrl ")+k);
|
|
2466
|
+
// ⌫ = Backspace, ↑/↓ = arrows; letters/Enter shown verbatim
|
|
2467
|
+
const SHORTCUTS=[
|
|
2468
|
+
{label:"Accept changes",k:"Enter"},
|
|
2469
|
+
{label:"Reset values",k:"\u232B"},
|
|
2470
|
+
{label:"Copy values",k:"C"},
|
|
2471
|
+
{label:"Toggle Refine panel",k:"E"},
|
|
2472
|
+
{label:"Open transition picker",k:"K"},
|
|
2473
|
+
{label:"Previous transition",k:"\u2191"},
|
|
2474
|
+
{label:"Next transition",k:"\u2193"},
|
|
2475
|
+
];
|
|
2102
2476
|
const pickRef=useRef(null), gearRef=useRef(null);
|
|
2103
2477
|
const[q,setQ]=useState("");
|
|
2104
2478
|
useEffect(()=>{if(!pick)setQ("");},[pick]); // reset search when the picker closes
|
|
@@ -2152,28 +2526,65 @@
|
|
|
2152
2526
|
h("span",{className:"tl-header-count"},
|
|
2153
2527
|
scanning
|
|
2154
2528
|
? h("span",{className:"t-tt-wrap"},
|
|
2155
|
-
h("span",{className:"tl-scan-note",tabIndex:0},
|
|
2529
|
+
h("span",{className:"tl-scan-note",tabIndex:0},
|
|
2530
|
+
h(DotmLoader),
|
|
2531
|
+
h("span",{className:"t-shimmer tl-scan-shimmer","data-text":"Agent scanning transitions…"},"Agent scanning transitions…")),
|
|
2156
2532
|
h("span",{className:"t-tt tl-tt-below tl-tt-msg",role:"tooltip"},
|
|
2157
2533
|
"Agent is working on understanding your transitions and creating grouping and naming. This usually takes a few seconds to a minute."))
|
|
2158
2534
|
: entries.length+" transition"+(entries.length===1?"":"s")+" found"),
|
|
2159
2535
|
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
|
-
|
|
2536
|
+
h(Dropdown,{open:setg,onClose:()=>{setSetg(false);setSettingsView("main");},triggerRef:gearRef,width:240,align:"right"},
|
|
2537
|
+
h(SettingsSlide,{view:settingsView,
|
|
2538
|
+
onBack:()=>setSettingsView("main"),
|
|
2539
|
+
onClose:()=>{setSetg(false);setSettingsView("main");},
|
|
2540
|
+
// ── main settings page (Figma 580:4723) ──
|
|
2541
|
+
mainPage:h(React.Fragment,null,
|
|
2542
|
+
h(MenuItem,{disabled:!active,onClick:()=>{onCopy&&onCopy();setSetg(false);},
|
|
2543
|
+
left:h("span",{className:"tl-menu-icon"},h(Ic,{name:"copy"})),
|
|
2544
|
+
right:copied&&h("span",{className:"tl-menu-check"},h(Ic,{name:"accept"}))},copied?"Copied":"Copy values"),
|
|
2545
|
+
h(MenuItem,{disabled:scanning,onClick:()=>{setSetg(false);onRescan&&onRescan();},
|
|
2546
|
+
left:h("span",{className:"tl-menu-icon"},h(Ic,{name:"restart"}))},
|
|
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"),
|
|
2554
|
+
h(MenuItem,{onClick:()=>setSettingsView("shortcuts"),
|
|
2555
|
+
right:h("span",{className:"tl-menu-chevr"},h(Ic,{name:"chevronr",size:16}))},"Keyboard shortcuts"),
|
|
2556
|
+
h(MenuItem,{onClick:()=>{setSetg(false);window.open("https://transitions.dev","_blank","noopener");},
|
|
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))),
|
|
2572
|
+
// ── keyboard-shortcuts sub-page ──
|
|
2573
|
+
shortcutsPage:h(React.Fragment,null,
|
|
2574
|
+
h("button",{type:"button",className:"tl-set-back",onClick:()=>setSettingsView("main")},
|
|
2575
|
+
h("span",{className:"tl-set-back-ic"},h(Ic,{name:"chevronr",size:16})),
|
|
2576
|
+
h("span",{className:"tl-set-back-title"},"Keyboard shortcuts")),
|
|
2577
|
+
h("div",{className:"tl-menu-divider"}),
|
|
2578
|
+
...SHORTCUTS.map((s,i)=>h("div",{key:i,className:"tl-kbd-row"},
|
|
2579
|
+
h("span",{className:"tl-kbd-row-label"},s.label),
|
|
2580
|
+
h("span",{className:"tl-kbd-cluster"},
|
|
2581
|
+
h("kbd",{className:"tl-kbd"},modKey),
|
|
2582
|
+
h("span",{className:"tl-kbd-plus"},"+"),
|
|
2583
|
+
h("kbd",{className:"tl-kbd"},s.k)))))}),
|
|
2584
|
+
),
|
|
2174
2585
|
h("span",{className:"t-tt-wrap"},
|
|
2175
2586
|
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")),
|
|
2587
|
+
h("span",{className:"t-tt tl-tt-below",role:"tooltip"},"Reset values",ttHint("\u232B"))),
|
|
2177
2588
|
h("span",{className:"t-tt-wrap"},
|
|
2178
2589
|
h("button",{className:cx("tl-accept-btn",acceptState==="saving"&&"is-saving",acceptState==="done"&&"is-done"),
|
|
2179
2590
|
disabled:!active||acceptDisabled||acceptState==="saving"||acceptState==="done",onClick:onAccept,"aria-label":"Accept changes to your code"},
|
|
@@ -2185,8 +2596,9 @@
|
|
|
2185
2596
|
acceptState==="error"&&acceptError?acceptError
|
|
2186
2597
|
:acceptState==="done"?"Saved to your code"
|
|
2187
2598
|
:acceptDisabled?"No changes to save"
|
|
2188
|
-
:"Save changes to your codebase")),
|
|
2189
|
-
h("
|
|
2599
|
+
:h(React.Fragment,null,"Save changes to your codebase",ttHint("A")))),
|
|
2600
|
+
h("span",{className:"t-tt-wrap"},
|
|
2601
|
+
h("button",{className:cx("tl-refine-btn t-tt-trigger",refineActive&&"is-active"),disabled:!active,onClick:onRefine},
|
|
2190
2602
|
h(Ic,{name:"wand"}),
|
|
2191
2603
|
h("span",{className:"tl-refine-sparks","aria-hidden":"true"},
|
|
2192
2604
|
h("i",{style:{"--ox":"-1px","--oy":"-3px","--sx":"0px","--sy":"-11px","--sd":"900ms","--sdelay":"0ms"}}),
|
|
@@ -2200,35 +2612,57 @@
|
|
|
2200
2612
|
h("i",{style:{"--ox":"-5px","--oy":"-2px","--sx":"-9px","--sy":"-9px","--sd":"930ms","--sdelay":"180ms"}}),
|
|
2201
2613
|
h("i",{style:{"--ox":"3px","--oy":"1px","--sx":"12px","--sy":"1px","--sd":"980ms","--sdelay":"300ms"}})),
|
|
2202
2614
|
h("span",null,"Refine")),
|
|
2203
|
-
|
|
2615
|
+
h("span",{className:"t-tt tl-tt-below",role:"tooltip"},"Refine",ttHint("R"))),
|
|
2616
|
+
h("span",{className:"t-tt-wrap"},
|
|
2617
|
+
h("button",{className:"tl-icon-btn ghost t-tt-trigger","aria-label":"Minimize",onClick:onMinimize},h(Ic,{name:"minimize"})),
|
|
2618
|
+
h("span",{className:"t-tt tl-tt-below tl-tt-right",role:"tooltip"},"Minimize",ttHint("."))),
|
|
2204
2619
|
);
|
|
2205
2620
|
}
|
|
2206
2621
|
|
|
2207
2622
|
function PropTrack({property, member, delayMs, durationMs, selected, onSelect, onDelayChange, onDurationChange, snap, scaleMs, lockDuration, labelW}){
|
|
2208
2623
|
const trackRef=useRef(null);
|
|
2209
|
-
const
|
|
2210
|
-
|
|
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]);
|
|
2211
2629
|
|
|
2212
2630
|
const startDrag=useCallback((mode,e)=>{
|
|
2213
2631
|
e.preventDefault(); e.stopPropagation();
|
|
2214
2632
|
onSelect();
|
|
2215
2633
|
// a spring controls its own duration, so its bar can be moved (delay) but not resized
|
|
2216
2634
|
if(lockDuration && mode!=="move") return;
|
|
2635
|
+
setDragging(true);
|
|
2217
2636
|
const startX=e.clientX; const sd=delayMs; const sdur=durationMs;
|
|
2637
|
+
let lastDel=sd, lastDur=sdur;
|
|
2218
2638
|
const onMove=e2=>{
|
|
2219
|
-
const dx=e2.clientX-startX; const dMs=pxToMs(dx);
|
|
2639
|
+
const dx=e2.clientX-startX; const dMs=pxToMs(dx,1); // smooth: 1ms steps
|
|
2220
2640
|
if(mode==="move"){
|
|
2221
|
-
|
|
2641
|
+
lastDel=Math.max(0,Math.min(sd+dMs,scaleMs-sdur));
|
|
2642
|
+
onDelayChange(lastDel);
|
|
2222
2643
|
} else if(mode==="left"){
|
|
2223
|
-
const rawDel=sd+dMs; const clampedDel=Math.max(0,Math.min(rawDel,sd+sdur-
|
|
2224
|
-
|
|
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);
|
|
2225
2647
|
} else {
|
|
2226
|
-
|
|
2648
|
+
lastDur=Math.max(1,Math.min(sdur+dMs,scaleMs-sd));
|
|
2649
|
+
onDurationChange(lastDur);
|
|
2227
2650
|
}
|
|
2228
2651
|
};
|
|
2229
|
-
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
|
+
};
|
|
2230
2664
|
window.addEventListener("mousemove",onMove);window.addEventListener("mouseup",onUp);
|
|
2231
|
-
},[delayMs,durationMs,pxToMs,
|
|
2665
|
+
},[delayMs,durationMs,pxToMs,snap,scaleMs,onSelect,onDelayChange,onDurationChange,lockDuration]);
|
|
2232
2666
|
|
|
2233
2667
|
const delPct=(delayMs/scaleMs)*100; const durPct=(durationMs/scaleMs)*100;
|
|
2234
2668
|
return h("div",{className:cx("tl-prop-row",selected&&"selected",lockDuration&&"is-spring"),onClick:onSelect},
|
|
@@ -2237,7 +2671,7 @@
|
|
|
2237
2671
|
member&&h("span",{className:"tl-prop-member"},member),
|
|
2238
2672
|
h("span",{className:"tl-prop-prop"},property))),
|
|
2239
2673
|
h("div",{className:"tl-prop-track",ref:trackRef},
|
|
2240
|
-
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),
|
|
2241
2675
|
title:lockDuration?"Spring duration is derived \u2014 drag to move, resize is locked":undefined},
|
|
2242
2676
|
h("span",{className:"tl-bar-handle left"}),
|
|
2243
2677
|
h("span",{className:"tl-bar-handle right"}),
|
|
@@ -2257,7 +2691,8 @@
|
|
|
2257
2691
|
const[dragging,setDragging]=useState(false);
|
|
2258
2692
|
const[menu,setMenu]=useState(false);
|
|
2259
2693
|
const pct = Math.min(Math.max((value - min) / (max - min), 0), 1) * 100;
|
|
2260
|
-
|
|
2694
|
+
// fine grid while dragging (smooth); the snap `step` is applied on release.
|
|
2695
|
+
const dragGrid = Math.max(1, Math.round(step/5));
|
|
2261
2696
|
const selectAll = useCallback(e=>{
|
|
2262
2697
|
const el = e && e.target ? e.target : inputRef.current;
|
|
2263
2698
|
if(!el) return;
|
|
@@ -2265,25 +2700,27 @@
|
|
|
2265
2700
|
requestAnimationFrame(()=>el.select());
|
|
2266
2701
|
},[]);
|
|
2267
2702
|
|
|
2268
|
-
const setFromX = useCallback(clientX => {
|
|
2703
|
+
const setFromX = useCallback((clientX, g) => {
|
|
2269
2704
|
if(!trackRef.current) return;
|
|
2270
2705
|
const rect = trackRef.current.getBoundingClientRect();
|
|
2271
2706
|
const ratio = Math.max(0, Math.min((clientX - rect.left) / rect.width, 1));
|
|
2272
2707
|
const raw = min + ratio * (max - min);
|
|
2273
|
-
const
|
|
2708
|
+
const gg = g || 1;
|
|
2709
|
+
const snapped = Math.round(raw / gg) * gg;
|
|
2274
2710
|
onChange(Math.max(min, Math.min(max, snapped)));
|
|
2275
|
-
},[min,max,
|
|
2711
|
+
},[min,max,onChange]);
|
|
2276
2712
|
|
|
2277
2713
|
const startDrag = useCallback(e=>{
|
|
2278
2714
|
if(editing || readOnly) return;
|
|
2279
2715
|
e.preventDefault();
|
|
2280
2716
|
setDragging(true);
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
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); };
|
|
2284
2721
|
window.addEventListener("mousemove",onMove);
|
|
2285
2722
|
window.addEventListener("mouseup",onUp);
|
|
2286
|
-
},[setFromX,editing]);
|
|
2723
|
+
},[setFromX,editing,readOnly,snap,step,dragGrid]);
|
|
2287
2724
|
|
|
2288
2725
|
const beginEdit = useCallback(e=>{ e.stopPropagation(); if(readOnly) return; setDraft(String(value)); setEditing(true); },[value,readOnly]);
|
|
2289
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]);
|
|
@@ -2317,15 +2754,12 @@
|
|
|
2317
2754
|
}
|
|
2318
2755
|
|
|
2319
2756
|
// 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 };
|
|
2757
|
+
// Square plot with equal padding on all sides (standard cubic-bezier graph
|
|
2758
|
+
// ratio, easing.dev): the 0→1 region is centered with identical top/bottom
|
|
2759
|
+
// (and left/right) margins. The box no longer reserves room for overshoot —
|
|
2760
|
+
// overshoot/undershoot handles overflow the box freely (the curve container
|
|
2761
|
+
// is overflow:visible) so dragging is never clamped to the rectangle.
|
|
2762
|
+
const CURVE = { VBW:240, VBH:240, PAD_X:24, PAD_Y:24 };
|
|
2329
2763
|
|
|
2330
2764
|
// Unified "Preview" section (Figma node 580:11158) — collapsible, collapsed by
|
|
2331
2765
|
// default (transitions.dev · 21-accordion). Holds BOTH the timing curve card
|
|
@@ -2828,6 +3262,9 @@
|
|
|
2828
3262
|
const[panelHeight,setPanelHeight]=useState(440);
|
|
2829
3263
|
const[resizing,setResizing]=useState(false);
|
|
2830
3264
|
const[snap,setSnap]=useState(true);
|
|
3265
|
+
// transition picker open state — lifted out of Header so the panel-focused
|
|
3266
|
+
// Cmd/Ctrl+K shortcut can open it.
|
|
3267
|
+
const[pickOpen,setPickOpen]=useState(false);
|
|
2831
3268
|
// ── refine ──
|
|
2832
3269
|
const[refineOpen,setRefineOpen]=useState(false);
|
|
2833
3270
|
const[refinePhase,setRefinePhase]=useState("idle"); // idle | scanning | done | error
|
|
@@ -2836,6 +3273,10 @@
|
|
|
2836
3273
|
const[refineSuggestions,setRefineSuggestions]=useState([]);
|
|
2837
3274
|
const[refineSummary,setRefineSummary]=useState(null);
|
|
2838
3275
|
const[refineError,setRefineError]=useState(null);
|
|
3276
|
+
// deterministic scans answer almost instantly, so enforce a 2s floor on the
|
|
3277
|
+
// "scanning" phase for that mode only (LLM scans stay immediate).
|
|
3278
|
+
const scanStartedAtRef=useRef(0);
|
|
3279
|
+
const scanModeRef=useRef("llm");
|
|
2839
3280
|
// ── accept (write to source) ──
|
|
2840
3281
|
const[acceptState,setAcceptState]=useState("idle"); // idle | saving | done | error
|
|
2841
3282
|
const[acceptError,setAcceptError]=useState(null);
|
|
@@ -2876,33 +3317,76 @@
|
|
|
2876
3317
|
refreshHealth();
|
|
2877
3318
|
},[active,refineOpen,refreshHealth]);
|
|
2878
3319
|
// "Start scanning" resolves availability first, then posts the job.
|
|
2879
|
-
|
|
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)=>{
|
|
2880
3341
|
if(!active)return;
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
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;
|
|
2886
3348
|
const mode=(refineMode==="llm"&&!avail)?"deterministic":refineMode;
|
|
2887
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);
|
|
2888
3364
|
try{
|
|
2889
3365
|
const et=active.effectiveTimings||[];
|
|
2890
3366
|
const timings=et.map(t=>({property:t.property,member:t.member||null,durationMs:t.durationMs,delayMs:t.delayMs,easing:t.easing}));
|
|
2891
3367
|
const selector=(active.bindings&&active.bindings.selector)||(et[0]&&et[0].selector)||null;
|
|
2892
|
-
// always scan for BOTH kinds (token tweaks + a whole-transition replacement)
|
|
2893
|
-
// in one pass; the tabs just filter which kind they show.
|
|
2894
3368
|
const{id}=await relayCreateJob({transitionId:active.id,label:active.label,selector,
|
|
2895
|
-
phase:active.phase||null,group:active.groupLabel||null,timings,mode,refineType:
|
|
3369
|
+
phase:active.phase||null,group:active.groupLabel||null,timings,mode,refineType:type});
|
|
2896
3370
|
setRefineJobId(id);
|
|
2897
3371
|
}catch(e){
|
|
2898
3372
|
setRefinePhase("error");
|
|
2899
3373
|
setRefineError(h(React.Fragment,null,"Couldn't reach the refine relay. Start it with ",h("code",{className:"tl-code"},"npx transitions-refine live"),"."));
|
|
2900
3374
|
}
|
|
2901
|
-
},[active,refineMode,refineType,
|
|
2902
|
-
const changeRefineMode=useCallback((mode)=>{setRefineMode(mode);setRefinePhase("idle");refreshHealth();},[refreshHealth]);
|
|
2903
|
-
//
|
|
2904
|
-
//
|
|
2905
|
-
|
|
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]);
|
|
2906
3390
|
// poll the relay while a job is running
|
|
2907
3391
|
useEffect(()=>{
|
|
2908
3392
|
if(!refineJobId||refinePhase!=="scanning")return;
|
|
@@ -2912,12 +3396,27 @@
|
|
|
2912
3396
|
const job=await relayGetJob(refineJobId);
|
|
2913
3397
|
if(!live)return;
|
|
2914
3398
|
if(Array.isArray(job.statusLog))setRefineLog(job.statusLog);
|
|
2915
|
-
if(job.status==="done"){
|
|
3399
|
+
if(job.status==="done"){
|
|
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");};
|
|
3409
|
+
// deterministic mode floors the scanning phase at 2s so it doesn't flash by.
|
|
3410
|
+
const minMs=scanModeRef.current==="deterministic"?2000:0;
|
|
3411
|
+
const wait=Math.max(0,minMs-(Date.now()-scanStartedAtRef.current));
|
|
3412
|
+
if(wait>0){to=setTimeout(finish,wait);}else{finish();}
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
2916
3415
|
if(job.status==="error"){setRefineError(job.error||"The agent reported an error.");setRefinePhase("error");return;}
|
|
2917
|
-
to=setTimeout(tick,
|
|
3416
|
+
to=setTimeout(tick,200);
|
|
2918
3417
|
}catch(e){if(live){setRefineError("Lost connection to the relay.");setRefinePhase("error");}}
|
|
2919
3418
|
};
|
|
2920
|
-
to=setTimeout(tick,
|
|
3419
|
+
to=setTimeout(tick,200);
|
|
2921
3420
|
return()=>{live=false;if(to)clearTimeout(to);};
|
|
2922
3421
|
},[refineJobId,refinePhase]);
|
|
2923
3422
|
const applySuggestion=useCallback((s)=>{
|
|
@@ -2951,7 +3450,7 @@
|
|
|
2951
3450
|
if(typeof document==="undefined")return;
|
|
2952
3451
|
const applied=liveAppliedRef.current;
|
|
2953
3452
|
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); } };
|
|
3453
|
+
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
3454
|
for(const item of entries){
|
|
2956
3455
|
const ov=registry.getPropOverrides(item.id);
|
|
2957
3456
|
if(!ov||!Object.keys(ov).length)continue; // only edited transitions go live
|
|
@@ -3019,8 +3518,29 @@
|
|
|
3019
3518
|
}else{
|
|
3020
3519
|
text="transition: "+et.map(decl).join(",\n ")+";";
|
|
3021
3520
|
}
|
|
3022
|
-
|
|
3023
|
-
showToast("
|
|
3521
|
+
const onCopied=()=>{setCopied(true);setTimeout(()=>setCopied(false),1500);showToast("Values copied");};
|
|
3522
|
+
const onCopyFail=()=>{showToast("Couldn't copy");};
|
|
3523
|
+
// navigator.clipboard needs a secure context (https/localhost); when the
|
|
3524
|
+
// host page is plain http — or Safari blocks the async API — fall back to
|
|
3525
|
+
// a hidden-textarea execCommand("copy") so copy still works everywhere.
|
|
3526
|
+
const legacyCopy=()=>{
|
|
3527
|
+
try{
|
|
3528
|
+
const ta=document.createElement("textarea");
|
|
3529
|
+
ta.value=text; ta.setAttribute("readonly","");
|
|
3530
|
+
ta.style.position="fixed"; ta.style.top="-9999px"; ta.style.opacity="0";
|
|
3531
|
+
document.body.appendChild(ta);
|
|
3532
|
+
ta.focus(); ta.select();
|
|
3533
|
+
try{ta.setSelectionRange(0,text.length);}catch(e){}
|
|
3534
|
+
const ok=document.execCommand("copy");
|
|
3535
|
+
document.body.removeChild(ta);
|
|
3536
|
+
ok?onCopied():onCopyFail();
|
|
3537
|
+
}catch(e){onCopyFail();}
|
|
3538
|
+
};
|
|
3539
|
+
try{
|
|
3540
|
+
if(navigator.clipboard&&navigator.clipboard.writeText){
|
|
3541
|
+
navigator.clipboard.writeText(text).then(onCopied).catch(legacyCopy);
|
|
3542
|
+
}else legacyCopy();
|
|
3543
|
+
}catch(e){legacyCopy();}
|
|
3024
3544
|
},[active,showToast]);
|
|
3025
3545
|
const resetOverrides=useCallback(()=>{if(active)registry.clearOverride(active.id);},[registry,active]);
|
|
3026
3546
|
// Accept → send an "apply" job so the agent writes the edited timings into
|
|
@@ -3100,6 +3620,11 @@
|
|
|
3100
3620
|
if(scanTokenRef.current!==token)return;
|
|
3101
3621
|
const flat=registry.getAll().filter(e=>e.kind!=="phase");
|
|
3102
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;}
|
|
3103
3628
|
// valid cache (same content signature) → apply instantly, skip the agent.
|
|
3104
3629
|
// A stale cache (different sig, e.g. content changed) falls through to a
|
|
3105
3630
|
// fresh scan and is overwritten below.
|
|
@@ -3148,7 +3673,12 @@
|
|
|
3148
3673
|
// (signature match, no agent) or kicks off a fresh agent scan. Rescan is
|
|
3149
3674
|
// manual after that.
|
|
3150
3675
|
useEffect(()=>{
|
|
3151
|
-
|
|
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;
|
|
3152
3682
|
didGroupScanRef.current=true;
|
|
3153
3683
|
runGroupScan();
|
|
3154
3684
|
},[live,runGroupScan]);
|
|
@@ -3203,27 +3733,93 @@
|
|
|
3203
3733
|
// small "Agent scanning transitions…" note in the header count
|
|
3204
3734
|
// slot. A failed scan just leaves the flat list (Rescan in
|
|
3205
3735
|
// Settings to retry) — it never traps the panel.
|
|
3736
|
+
// ── keyboard shortcuts (panel-focused, Cmd/Ctrl based) ──
|
|
3737
|
+
// Fire only when the panel is focused or hovered so we never hijack the
|
|
3738
|
+
// host app. A latest-values ref keeps the single window listener stable.
|
|
3739
|
+
const panelRootRef=useRef(null);
|
|
3740
|
+
const hoveredRef=useRef(false);
|
|
3741
|
+
const panelHotRef=useRef(false);
|
|
3742
|
+
const recomputeHot=useCallback(()=>{
|
|
3743
|
+
const el=panelRootRef.current;
|
|
3744
|
+
panelHotRef.current=hoveredRef.current||!!(el&&document.activeElement&&el.contains(document.activeElement));
|
|
3745
|
+
},[]);
|
|
3746
|
+
const kbdRef=useRef({});
|
|
3747
|
+
kbdRef.current={entries,active,acceptState,acceptDisabled:computeChanges(active).length===0,minimized,
|
|
3748
|
+
openRefine,resetOverrides,copyValues,onAccept,setActiveId,setMinimized,setPickOpen};
|
|
3749
|
+
useEffect(()=>{
|
|
3750
|
+
const onKey=e=>{
|
|
3751
|
+
const mod=e.metaKey||e.ctrlKey;
|
|
3752
|
+
if(!mod)return;
|
|
3753
|
+
const L=kbdRef.current;
|
|
3754
|
+
// Cmd+. always restores a minimized panel (panel root isn't in the DOM
|
|
3755
|
+
// then, so the hot/input gates can't apply). All other combos require
|
|
3756
|
+
// the panel to be focused/hovered and not typing in a field.
|
|
3757
|
+
const isRestore=e.key==="."&&L.minimized;
|
|
3758
|
+
recomputeHot();
|
|
3759
|
+
if(!isRestore){
|
|
3760
|
+
if(!panelHotRef.current)return;
|
|
3761
|
+
const t=e.target;
|
|
3762
|
+
if(t&&(t.tagName==="INPUT"||t.tagName==="TEXTAREA"||t.isContentEditable))return;
|
|
3763
|
+
}
|
|
3764
|
+
const handle=fn=>{e.preventDefault();e.stopPropagation();fn&&fn();};
|
|
3765
|
+
switch(e.key){
|
|
3766
|
+
case"a":case"A": // Accept changes (same enabled condition as the Accept button)
|
|
3767
|
+
handle(()=>{if(L.active&&!L.acceptDisabled&&L.acceptState!=="saving"&&L.acceptState!=="done")L.onAccept&&L.onAccept();});
|
|
3768
|
+
break;
|
|
3769
|
+
case"Backspace": // Reset values
|
|
3770
|
+
handle(()=>{if(L.active)L.resetOverrides&&L.resetOverrides();});
|
|
3771
|
+
break;
|
|
3772
|
+
case"c":case"C":{ // Copy values — only when no text is selected (native copy wins otherwise)
|
|
3773
|
+
const sel=window.getSelection();
|
|
3774
|
+
if(sel&&!sel.isCollapsed)return;
|
|
3775
|
+
handle(()=>{if(L.active)L.copyValues&&L.copyValues();});
|
|
3776
|
+
break;}
|
|
3777
|
+
case"r":case"R": // Toggle Refine panel
|
|
3778
|
+
handle(()=>{if(L.active)L.openRefine&&L.openRefine();});
|
|
3779
|
+
break;
|
|
3780
|
+
case"k":case"K": // Open transition picker
|
|
3781
|
+
handle(()=>{if(L.active)L.setPickOpen&&L.setPickOpen(true);});
|
|
3782
|
+
break;
|
|
3783
|
+
case"ArrowUp":
|
|
3784
|
+
case"ArrowDown":{ // Previous / next transition
|
|
3785
|
+
const list=L.entries||[];
|
|
3786
|
+
if(!list.length)break;
|
|
3787
|
+
const dir=e.key==="ArrowUp"?-1:1;
|
|
3788
|
+
const cur=L.active?list.findIndex(x=>x.id===L.active.id):-1;
|
|
3789
|
+
let ni=cur<0?(dir>0?0:list.length-1):cur+dir;
|
|
3790
|
+
ni=Math.max(0,Math.min(list.length-1,ni));
|
|
3791
|
+
if(list[ni])handle(()=>L.setActiveId&&L.setActiveId(list[ni].id));
|
|
3792
|
+
break;}
|
|
3793
|
+
case".": // Minimize / restore panel
|
|
3794
|
+
handle(()=>L.setMinimized&&L.setMinimized(v=>!v));
|
|
3795
|
+
break;
|
|
3796
|
+
}
|
|
3797
|
+
};
|
|
3798
|
+
window.addEventListener("keydown",onKey);
|
|
3799
|
+
return()=>window.removeEventListener("keydown",onKey);
|
|
3800
|
+
},[recomputeHot]);
|
|
3206
3801
|
const gate = !live
|
|
3207
3802
|
? (llmAvailable===null ? "loading" : "blocked")
|
|
3208
3803
|
: "ready";
|
|
3209
3804
|
return h(React.Fragment,null,
|
|
3210
|
-
render&&h("div",{className:"t-panel-slide","data-timeline-panel":true,
|
|
3805
|
+
render&&h("div",{className:"t-panel-slide","data-timeline-panel":true,ref:panelRootRef,
|
|
3806
|
+
onMouseEnter:()=>{hoveredRef.current=true;},onMouseLeave:()=>{hoveredRef.current=false;},
|
|
3211
3807
|
"data-open":panelOpen?"true":"false","data-phase":phase,style:{height:panelHeight+"px"}},
|
|
3212
3808
|
h("div",{className:cx("tl-resize-handle",resizing&&"dragging"),onMouseDown:startResize,title:"Drag to resize"}),
|
|
3213
3809
|
h("div",{className:"tl-panel-body"},
|
|
3214
3810
|
gate==="ready"
|
|
3215
3811
|
? h(React.Fragment,null,
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3812
|
+
h("div",{className:"tl-panel-main"},
|
|
3813
|
+
h(Header,{entries,active,onSelect:setActiveId,onReset:resetOverrides,onCopy:copyValues,copied,
|
|
3814
|
+
snap,setSnap,onMinimize:()=>setMinimized(true),onRefine:openRefine,refineActive:refineOpen,
|
|
3815
|
+
onAccept,acceptState,acceptDisabled:computeChanges(active).length===0,acceptError,
|
|
3816
|
+
scanning:groupScanState==="scanning",onRescan:rescanTransitions,pick:pickOpen,setPick:setPickOpen}),
|
|
3817
|
+
active
|
|
3818
|
+
?h(Body,{entry:active,onPropChange:(prop,o)=>setPropOverride(prop,o),snap})
|
|
3819
|
+
:h("div",{className:"tl-empty"},"Select a transition to inspect and edit it")),
|
|
3820
|
+
h(RefinePanel,{open:refineOpen,onClose:()=>setRefineOpen(false),phase:refinePhase,label:refineLabel,
|
|
3821
|
+
refineType,onType:changeRefineType,suggestions:refineSuggestions,summary:refineSummary,error:refineError,
|
|
3822
|
+
appliedIds,onApply:applySuggestion,onApplyAll:applyAllSuggestions,
|
|
3227
3823
|
mode:refineMode,onMode:changeRefineMode,llmAvailable,cliInstalled,onStart:startScan,
|
|
3228
3824
|
lanes:active?.effectiveTimings||[]}))
|
|
3229
3825
|
: h("div",{className:"tl-panel-main"},
|
|
@@ -3234,21 +3830,57 @@
|
|
|
3234
3830
|
"Please run the ",h("code",{className:"tl-code"},"/refine live"),
|
|
3235
3831
|
" command in your agent to enable live features, such as scanning and refining transitions.")))),
|
|
3236
3832
|
toast&&createPortal(
|
|
3237
|
-
h("div",{className:"tl-toast-wrap","aria-live":"polite"},
|
|
3833
|
+
h("div",{className:"tl-toast-wrap","data-tl-ui":"","aria-live":"polite"},
|
|
3238
3834
|
h("div",{className:cx("tl-toast",toast.closing&&"is-closing")},
|
|
3239
3835
|
h("span",{className:"tl-toast-ic"},
|
|
3240
3836
|
toast.loader?h(DotmLoader):h(Ic,{name:"accept",size:14})),
|
|
3241
|
-
|
|
3837
|
+
toast.loader
|
|
3838
|
+
? h("span",{className:"t-shimmer tl-scan-shimmer","data-text":toast.msg},toast.msg)
|
|
3839
|
+
: h("span",null,toast.msg))),
|
|
3242
3840
|
document.body),
|
|
3243
3841
|
),
|
|
3244
3842
|
),
|
|
3245
|
-
minimized&&h("div",{className:"tl-pill",onClick:()=>setMinimized(false)},
|
|
3843
|
+
minimized&&h("div",{className:"tl-pill","data-tl-ui":"",onClick:()=>setMinimized(false)},
|
|
3246
3844
|
h("span",{className:"tl-pill-label"},"Transitions"),
|
|
3247
3845
|
h("span",{className:cx("tl-pill-count",String(entries.length).length===1&&"is-single")},entries.length)),
|
|
3248
3846
|
);
|
|
3249
3847
|
}
|
|
3250
3848
|
|
|
3251
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
|
+
|
|
3252
3884
|
// Plus → menu morph — transitions.dev (20-plus-menu-morph.md). The trigger
|
|
3253
3885
|
// button morphs into the panel it opens; CSS owns the morph, JS only flips
|
|
3254
3886
|
// data-open and mirrors it to aria-expanded, closing on outside-click / Escape.
|
|
@@ -3387,9 +4019,20 @@
|
|
|
3387
4019
|
// to show them for testing. (This whole block is below the inject CUT_MARKER,
|
|
3388
4020
|
// so it never ships in the injected build.)
|
|
3389
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);
|
|
3390
4025
|
return h(TimelineCtx.Provider,{value:ctx},
|
|
3391
4026
|
showControls&&h(PanelControls),
|
|
3392
|
-
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.")),
|
|
3393
4036
|
h("div",{className:"demo-grid"},h(BoxMorph))),
|
|
3394
4037
|
h(TimelinePanel));
|
|
3395
4038
|
}
|