transitions-refine 0.3.6 → 0.3.8
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 +36 -9
- package/demo.html +217 -125
- package/package.json +1 -1
- package/server/relay.mjs +27 -10
|
@@ -197,27 +197,54 @@ separately. You fix that by reading the source. The request looks like:
|
|
|
197
197
|
"raw": [
|
|
198
198
|
{ "label": "div.dropdown-panel", "selector": ".dropdown-panel",
|
|
199
199
|
"properties": ["opacity","transform"],
|
|
200
|
-
"timings": [{ "property": "opacity", "durationMs": 200, "delayMs": 0, "easing": "ease-out" }]
|
|
200
|
+
"timings": [{ "property": "opacity", "durationMs": 200, "delayMs": 0, "easing": "ease-out" }],
|
|
201
|
+
"cssRules": [
|
|
202
|
+
".dropdown .dropdown-panel { opacity: 0; transition: opacity 200ms ease-out 0ms, transform 200ms cubic-bezier(0.22, 1, 0.36, 1) 0ms; }",
|
|
203
|
+
".dropdown.is-open .dropdown-panel { opacity: 1; transform: translateY(0); }",
|
|
204
|
+
".dropdown.is-closing .dropdown-panel { transition: opacity 150ms ease-in 0ms; opacity: 0; }"
|
|
205
|
+
] }
|
|
201
206
|
]
|
|
202
207
|
}
|
|
203
208
|
}
|
|
204
209
|
```
|
|
205
210
|
|
|
211
|
+
**Be fast.** The `raw.timings` are already accurate for each element's *current*
|
|
212
|
+
on-screen state — treat them as ground truth and reuse them verbatim. Most `raw`
|
|
213
|
+
entries also carry **`cssRules`**: the CSS rules harvested live from the page
|
|
214
|
+
(CSSOM) that drive that element across *all* states (base + open + close), with
|
|
215
|
+
`var()` already resolved to concrete values.
|
|
216
|
+
|
|
217
|
+
**Fast path — prefer `cssRules` over the filesystem.** When an entry has
|
|
218
|
+
`cssRules`, they are authoritative and contain everything you need: the opposite
|
|
219
|
+
phase's timings live on a state-variant selector inside them (e.g.
|
|
220
|
+
`.dd.is-closing .dd-panel`, `.modal[data-closing] .dialog`), and the toggled
|
|
221
|
+
state is visible in those selectors. Derive grouping, phases, toggled state, and
|
|
222
|
+
opposite-phase timings **directly from `cssRules` + `timings`** — do **not**
|
|
223
|
+
glob/grep/read files for any element whose `cssRules` is non-empty; it only
|
|
224
|
+
wastes time. Only fall back to reading source for entries with an empty/missing
|
|
225
|
+
`cssRules` (CORS-locked sheets, styled-components, Tailwind, etc.), and even then
|
|
226
|
+
read the minimum.
|
|
227
|
+
|
|
206
228
|
Do this:
|
|
207
229
|
|
|
208
230
|
1. **Identify each animated component** the raw entries belong to (dropdown,
|
|
209
|
-
modal, tooltip, accordion, drawer, toast…).
|
|
210
|
-
|
|
211
|
-
Tailwind, inline styles,
|
|
231
|
+
modal, tooltip, accordion, drawer, toast…). The selectors/labels usually make
|
|
232
|
+
this obvious — only read source (plain CSS / CSS Modules,
|
|
233
|
+
styled-components/emotion, Tailwind, inline styles, Motion/Framer variants)
|
|
234
|
+
when the grouping is genuinely unclear.
|
|
212
235
|
2. **Split each component into phases** — usually `open` and `close` (a hover-only
|
|
213
|
-
component can be a single phase).
|
|
214
|
-
|
|
215
|
-
|
|
236
|
+
component can be a single phase). The phase matching the current DOM reuses the
|
|
237
|
+
provided timings; the *opposite* phase often lives on a different selector
|
|
238
|
+
(`.is-open` vs `.is-closing`) with different timings — take it from the entry's
|
|
239
|
+
`cssRules` (or, only if it has none, read source). Report **both** even though
|
|
240
|
+
only one is in the DOM right now.
|
|
216
241
|
3. **List each phase's members** — the elements that animate in that phase. Give
|
|
217
242
|
each a stable `id`, a human `label`, a live-resolvable CSS `selector`, an
|
|
218
243
|
optional `toState` hint (the class/attribute that drives the phase, e.g.
|
|
219
|
-
`.is-open`), and its
|
|
220
|
-
|
|
244
|
+
`.is-open`), and its `propertyTimings`. For the current-state phase, **copy the
|
|
245
|
+
provided `raw.timings` verbatim**; for the opposite phase, **quote the real
|
|
246
|
+
timings from the entry's `cssRules`** (already var()-resolved) — or from source
|
|
247
|
+
if it has none — **never invent.**
|
|
221
248
|
4. **Post the groups** (this completes the job):
|
|
222
249
|
|
|
223
250
|
```bash
|
package/demo.html
CHANGED
|
@@ -440,7 +440,9 @@
|
|
|
440
440
|
/* ── value field (slider + input) — exact Figma "Value slider and input" ── */
|
|
441
441
|
.tl-field-wrap { display: flex; align-items: center; gap: 6px; margin-bottom: 12px; }
|
|
442
442
|
.tl-field { position: relative; flex: 1; min-width: 0; height: 32px; border-radius: 8px;
|
|
443
|
-
background: var(--c-field-bg); transition: box-shadow 0.12s ease; }
|
|
443
|
+
background: var(--c-field-bg); transition: box-shadow 0.12s ease, background 0.12s ease; }
|
|
444
|
+
/* match the chevron stepper's hover wash so the whole field reads as one control */
|
|
445
|
+
.tl-field:hover:not(.is-editing) { background: var(--c-sec-h); }
|
|
444
446
|
/* focused: dual inset ring (Figma Focused state) */
|
|
445
447
|
.tl-field.is-editing { box-shadow: inset 0 0 0 1px rgba(0,0,0,0.14), inset 0 0 0 0.5px rgba(255,255,255,0.06); }
|
|
446
448
|
/* inner slider fill — Figma button/tiny: soft drop shadow + inset ring give the
|
|
@@ -520,8 +522,8 @@
|
|
|
520
522
|
border: none; border-radius: 8px; background: #f7f7f7; padding: 6px 10px 6px 12px;
|
|
521
523
|
font: inherit; font-size: 13px; font-weight: 500; line-height: 16px; color: #1b1b1b; cursor: pointer; text-align: left;
|
|
522
524
|
transition: background 0.12s ease; }
|
|
523
|
-
.tl-select:hover { background:
|
|
524
|
-
.tl-select.is-open { background:
|
|
525
|
+
.tl-select:hover { background: var(--c-sec-h); }
|
|
526
|
+
.tl-select.is-open { background: var(--c-sec-h); }
|
|
525
527
|
.tl-select:disabled { opacity: 0.5; cursor: default; }
|
|
526
528
|
.tl-select-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
527
529
|
.tl-select-chev { display: flex; color: var(--c-ruler); flex: none; }
|
|
@@ -590,6 +592,27 @@
|
|
|
590
592
|
transition: color 0.12s ease; }
|
|
591
593
|
.tl-preview-play:hover { color: #171717; }
|
|
592
594
|
.tl-preview-play:active { color: var(--c-blue); }
|
|
595
|
+
/* Play ⟷ Pause: cross-blur text swap — same mechanism as the index
|
|
596
|
+
Copy code → Copied tooltip. Both words stack in one slot; the active
|
|
597
|
+
one is sharp/opaque, the other blurred/transparent, and they cross-blur
|
|
598
|
+
in place while the slot width tweens between the two measured widths. */
|
|
599
|
+
.tl-play-swap { position: relative; display: inline-block; vertical-align: baseline;
|
|
600
|
+
line-height: 18px; width: var(--pw-a, auto);
|
|
601
|
+
transition: width 240ms cubic-bezier(0.22, 1, 0.36, 1); }
|
|
602
|
+
.tl-play-swap[data-state="pause"] { width: var(--pw-b, auto); }
|
|
603
|
+
.tl-play-label { white-space: pre;
|
|
604
|
+
transition: opacity 180ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
605
|
+
filter 180ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
606
|
+
will-change: opacity, filter; }
|
|
607
|
+
.tl-play-a { display: inline-block; }
|
|
608
|
+
.tl-play-b { position: absolute; top: 0; left: 0; }
|
|
609
|
+
.tl-play-swap .tl-play-a { opacity: 1; filter: blur(0); }
|
|
610
|
+
.tl-play-swap .tl-play-b { opacity: 0; filter: blur(2px); }
|
|
611
|
+
.tl-play-swap[data-state="pause"] .tl-play-a { opacity: 0; filter: blur(2px); }
|
|
612
|
+
.tl-play-swap[data-state="pause"] .tl-play-b { opacity: 1; filter: blur(0); }
|
|
613
|
+
@media (prefers-reduced-motion: reduce) {
|
|
614
|
+
.tl-play-swap, .tl-play-label { transition: none !important; }
|
|
615
|
+
}
|
|
593
616
|
/* position-preview track — Figma 580:9117: dot + dashed rail inside a
|
|
594
617
|
#f6f6f7 rounded card (58px tall, 21px horizontal inset for the dot) */
|
|
595
618
|
.tl-preview-track { height: 58px; background: #f6f6f7; border-radius: 8px;
|
|
@@ -660,6 +683,8 @@
|
|
|
660
683
|
.tl-menu-item.disabled { color: var(--c-disabled); pointer-events: none; }
|
|
661
684
|
.tl-menu-item-label { flex: 1; min-width: 0; display: flex; align-items: center; }
|
|
662
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: column; gap: 2px; }
|
|
687
|
+
.tl-menu-sub { font-size: 11px; line-height: 14px; }
|
|
663
688
|
.tl-menu-dim { color: #979797; }
|
|
664
689
|
.tl-menu-check { display: flex; color: var(--c-text-strong); flex: none; }
|
|
665
690
|
.tl-menu-empty { padding: 10px 8px; color: var(--c-disabled); font-size: 13px; }
|
|
@@ -1029,7 +1054,7 @@
|
|
|
1029
1054
|
label and the icon+status; whichever is hidden fades out with blur+scale.
|
|
1030
1055
|
overflow:hidden + nowrap keep the status copy on a single line while the
|
|
1031
1056
|
box is still narrow mid-morph. */
|
|
1032
|
-
.tl-scan-content { position: relative; z-index:
|
|
1057
|
+
.tl-scan-content { position: relative; z-index: 4; display: grid; width: 100%; height: 100%;
|
|
1033
1058
|
min-width: 0; overflow: hidden; }
|
|
1034
1059
|
.tl-scan-face { grid-area: 1 / 1; display: flex; align-items: center; gap: 8px;
|
|
1035
1060
|
min-width: 0; white-space: nowrap;
|
|
@@ -1051,8 +1076,14 @@
|
|
|
1051
1076
|
position:relative on its [data-beam] container (and it lands later in the
|
|
1052
1077
|
DOM, so it would win at equal specificity). Raise specificity here so the
|
|
1053
1078
|
beam stays an absolute overlay filling the button instead of collapsing
|
|
1054
|
-
into flow as a flex sibling (which hid the beam and skewed the width).
|
|
1055
|
-
|
|
1079
|
+
into flow as a flex sibling (which hid the beam and skewed the width).
|
|
1080
|
+
z-index:3 lifts the beam ABOVE the box's 1px hairline (the .is-scanning
|
|
1081
|
+
::after `inset 0 0 0 1px` ring at z-index:2). The beam's own stroke is a
|
|
1082
|
+
1px ring at inset 0 with the same radius, so its travelling light now lands
|
|
1083
|
+
pixel-for-pixel ON the hairline instead of being occluded by it — leaving
|
|
1084
|
+
only the diffuse bloom (which read as a thick, offset border). The face
|
|
1085
|
+
content sits at z-index:4 so the glow never washes over the label/status. */
|
|
1086
|
+
.tl-scan-morph .tl-scan-beam { position: absolute; inset: 0; z-index: 3;
|
|
1056
1087
|
border-radius: inherit; pointer-events: none;
|
|
1057
1088
|
animation: tl-fade-in 360ms var(--resize-ease) both; }
|
|
1058
1089
|
/* Beam radius is auto-detected from this fill child; inherit keeps it locked
|
|
@@ -1072,33 +1103,8 @@
|
|
|
1072
1103
|
.tl-gate-text { margin: 4px 0 0; max-width: 250px; font-size: 13px; font-weight: 400; line-height: 21px;
|
|
1073
1104
|
color: #676767; text-wrap: pretty; }
|
|
1074
1105
|
.tl-gate-text .tl-code { color: #4b4b4b; }
|
|
1075
|
-
/* "Agent is scanning your transitions" — pill with dot-matrix loader + a
|
|
1076
|
-
line-style border beam tracing the capsule edge, with a subtext below. */
|
|
1077
|
-
.tl-gate-pill-wrap { position: relative; }
|
|
1078
|
-
.tl-gate-pill { position: relative; z-index: 1; display: inline-flex; align-items: center; gap: 6px;
|
|
1079
|
-
height: 36px; padding: 6px 12px; border-radius: 36px; background: #fff; color: #17181c;
|
|
1080
|
-
font-size: 13px; font-weight: 400; line-height: 14px; white-space: nowrap;
|
|
1081
|
-
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06), inset 0 0 0 1px rgba(196,196,196,0.10); }
|
|
1082
|
-
.tl-gate-pill .tl-dotm { color: #17181c; }
|
|
1083
|
-
.tl-gate-pill-wrap .tl-gate-beam { position: absolute; inset: 0; z-index: 0; border-radius: 36px;
|
|
1084
|
-
pointer-events: none; animation: tl-fade-in 360ms var(--resize-ease, cubic-bezier(0.22,1,0.36,1)) both; }
|
|
1085
|
-
.tl-gate-beam-fill { display: block; width: 100%; height: 100%; border-radius: 36px; background: #fff; }
|
|
1086
|
-
.tl-gate-sub { margin: 16px 0 0; max-width: 244px; font-size: 12px; font-weight: 400; line-height: 14px;
|
|
1087
|
-
color: #6f6f6f; text-wrap: pretty; }
|
|
1088
|
-
/* recovery actions when a scan errored/timed out — never trap the panel */
|
|
1089
|
-
.tl-gate-actions { display: flex; align-items: center; gap: 8px; margin: 18px 0 0; }
|
|
1090
|
-
.tl-gate-btn { height: 32px; padding: 0 14px; border: 0; border-radius: 60px; cursor: pointer;
|
|
1091
|
-
font: 500 12px/14px inherit; color: #2c2c2c; background: #f3f3f3; white-space: nowrap;
|
|
1092
|
-
box-shadow: 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);
|
|
1093
|
-
transition: background 140ms cubic-bezier(0.4,0,0.2,1); }
|
|
1094
|
-
.tl-gate-btn:hover { background: #ececec; }
|
|
1095
|
-
.tl-gate-btn:active { background: #e6e6e6; }
|
|
1096
|
-
.tl-gate-btn-primary { color: #fff; background: #0a84ff;
|
|
1097
|
-
box-shadow: 0 0 0 1px rgba(0,101,208,0.10) inset, 0 -1px 0 0 rgba(3,66,142,0.15) inset, 0 1px 3px 0 rgba(4,41,117,0.08); }
|
|
1098
|
-
.tl-gate-btn-primary:hover { background: #0a78e6; }
|
|
1099
1106
|
@media (prefers-reduced-motion: reduce) {
|
|
1100
|
-
.tl-gate
|
|
1101
|
-
.tl-gate-btn { transition: none !important; } }
|
|
1107
|
+
.tl-gate { animation: none !important; } }
|
|
1102
1108
|
/* dot-matrix loader — ported from @dotmatrix/dotm-square-14
|
|
1103
1109
|
(github.com/zzzzshawn/matrix, MIT). A 5×5 dot grid cross-fades through four
|
|
1104
1110
|
frame masks in the sequence 0→1→2→3→2→1, dot opacities x=1 / o=0.52 / .=0.08.
|
|
@@ -1125,7 +1131,10 @@
|
|
|
1125
1131
|
@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} }
|
|
1126
1132
|
@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} }
|
|
1127
1133
|
@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} }
|
|
1128
|
-
|
|
1134
|
+
/* background-scan note in the header count slot — plain text, hover for the
|
|
1135
|
+
tooltip explaining what the agent is doing. Dotted underline hints it. */
|
|
1136
|
+
.tl-scan-note { cursor: help; color: var(--c-count);
|
|
1137
|
+
text-decoration: underline dotted rgba(0,0,0,0.28); text-underline-offset: 2px; }
|
|
1129
1138
|
/* loading status text — transitions.dev shimmer text (15) + text states swap (04) */
|
|
1130
1139
|
.tl-refine-status-text {
|
|
1131
1140
|
--shimmer-dur: 2000ms; --shimmer-base: #9a9a9a; --shimmer-highlight: #17181c;
|
|
@@ -1208,33 +1217,43 @@
|
|
|
1208
1217
|
/* applied state — GRAY, never green (matches Accept icon --c-text #17181c) */
|
|
1209
1218
|
.tl-pill-btn.is-applied { color: var(--c-text); cursor: default; }
|
|
1210
1219
|
.tl-pill-btn.is-applied svg path { stroke: var(--c-text); }
|
|
1211
|
-
/*
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
.tl-
|
|
1220
|
-
|
|
1220
|
+
/* Apply ⟷ Applied: cross-blur text swap — same mechanism as the Play/Pause
|
|
1221
|
+
button above (and the index Copy → Copied tooltip). Both words stack in one
|
|
1222
|
+
slot; the active one is sharp/opaque, the other blurred/transparent, and they
|
|
1223
|
+
cross-blur in place while the slot width tweens between the two measured widths.
|
|
1224
|
+
The "Applied" label (b) carries the success check to the left of its text. */
|
|
1225
|
+
.tl-apply-swap { position: relative; display: inline-block; vertical-align: middle;
|
|
1226
|
+
line-height: 14px; width: var(--aw-a, auto);
|
|
1227
|
+
transition: width 240ms cubic-bezier(0.22, 1, 0.36, 1); }
|
|
1228
|
+
.tl-apply-swap[data-state="applied"] { width: var(--aw-b, auto); }
|
|
1229
|
+
.tl-apply-label { white-space: pre;
|
|
1230
|
+
transition: opacity 180ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
1231
|
+
filter 180ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
1232
|
+
will-change: opacity, filter; }
|
|
1233
|
+
.tl-apply-a { display: inline-block; }
|
|
1234
|
+
.tl-apply-b { position: absolute; top: 0; left: 0; display: inline-flex;
|
|
1235
|
+
align-items: center; gap: 6px; }
|
|
1236
|
+
.tl-apply-swap .tl-apply-a { opacity: 1; filter: blur(0); }
|
|
1237
|
+
.tl-apply-swap .tl-apply-b { opacity: 0; filter: blur(2px); }
|
|
1238
|
+
.tl-apply-swap[data-state="applied"] .tl-apply-a { opacity: 0; filter: blur(2px); }
|
|
1239
|
+
.tl-apply-swap[data-state="applied"] .tl-apply-b { opacity: 1; filter: blur(0); }
|
|
1240
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1241
|
+
.tl-apply-swap, .tl-apply-label { transition: none !important; }
|
|
1242
|
+
}
|
|
1243
|
+
/* success check (transitions.dev 10-success-check): stroke-draw on the path with
|
|
1244
|
+
a plain opacity fade — IN PLACE, no rotate / Y-bob / scale, so the check never
|
|
1245
|
+
moves while it draws. GRAY stroke. */
|
|
1246
|
+
.tl-sug-check { display: inline-flex; opacity: 0; will-change: opacity; }
|
|
1221
1247
|
.tl-sug-check svg { display: block; overflow: visible; }
|
|
1222
1248
|
/* stroke-dasharray = path.getTotalLength() of ICONS.accept "M4 8.4268 L6.46155
|
|
1223
1249
|
11.19223 L12 4.97001" (~12.03 user units, rounded up to 13). JS also sets it
|
|
1224
1250
|
inline per-render via getTotalLength() for sub-pixel safety. */
|
|
1225
1251
|
.tl-sug-check svg path { stroke-dasharray: 13; stroke-dashoffset: 13; }
|
|
1226
1252
|
.tl-sug-check[data-state="in"] {
|
|
1227
|
-
animation:
|
|
1228
|
-
tl-check-fade 500ms cubic-bezier(0.22, 1, 0.36, 1) forwards,
|
|
1229
|
-
tl-check-rotate 500ms cubic-bezier(0.22, 1, 0.36, 1) forwards,
|
|
1230
|
-
tl-check-blur 500ms cubic-bezier(0.22, 1, 0.36, 1) forwards,
|
|
1231
|
-
tl-check-bob 500ms cubic-bezier(0.34, 1.35, 0.64, 1) forwards; }
|
|
1253
|
+
animation: tl-check-fade 500ms cubic-bezier(0.22, 1, 0.36, 1) forwards; }
|
|
1232
1254
|
.tl-sug-check[data-state="in"] svg path {
|
|
1233
1255
|
animation: tl-check-draw 500ms cubic-bezier(0.22, 1, 0.36, 1) 80ms forwards; }
|
|
1234
1256
|
@keyframes tl-check-fade { from { opacity: 0; } to { opacity: 1; } }
|
|
1235
|
-
@keyframes tl-check-rotate { from { transform: rotate(80deg); } to { transform: rotate(0deg); } }
|
|
1236
|
-
@keyframes tl-check-blur { from { filter: blur(10px); } to { filter: blur(0); } }
|
|
1237
|
-
@keyframes tl-check-bob { from { translate: 0 40px; } to { translate: 0 0; } }
|
|
1238
1257
|
@keyframes tl-check-draw { to { stroke-dashoffset: 0; } }
|
|
1239
1258
|
/* results action bar (Figma node 581:6285): white sticky bar with top hairline
|
|
1240
1259
|
holding two equal-width pill buttons (Apply all + Scan again). */
|
|
@@ -1536,6 +1555,59 @@
|
|
|
1536
1555
|
_sel(el){if(el.id)return"#"+el.id;const p=[];let c=el,d=0;while(c&&d<3){let s=c.tagName.toLowerCase();if(c.id){p.unshift("#"+c.id);break;}if(c.className&&typeof c.className==="string"){const cls=c.className.trim().split(/\s+/)[0];if(cls)s+="."+cls;}p.unshift(s);c=c.parentElement;d++;}return p.join(" > ");}
|
|
1537
1556
|
}
|
|
1538
1557
|
|
|
1558
|
+
// ── CSSOM hint harvest ──
|
|
1559
|
+
// The scan's slowest part isn't reasoning — it's the agent NOT knowing where
|
|
1560
|
+
// the source lives, so it globs the whole tree, greps, and wanders into wrong
|
|
1561
|
+
// files (this explodes on real/large repos). We sidestep that: for each
|
|
1562
|
+
// detected element we pull the CSS rules that drive it across ALL states
|
|
1563
|
+
// (base + open + close) straight from the live stylesheets via CSSOM, resolve
|
|
1564
|
+
// var() to concrete values, and hand them to the agent inline. With these the
|
|
1565
|
+
// agent groups + recovers the opposite phase from the payload alone and never
|
|
1566
|
+
// touches the filesystem. Falls back to source-reading when empty (CORS-locked
|
|
1567
|
+
// sheets, styled-components/Tailwind, etc.).
|
|
1568
|
+
const HINT_MAX_RULES = 16; // per element — base + a few state variants
|
|
1569
|
+
const HINT_MAX_RULE_LEN = 600; // chars per rule, trimmed
|
|
1570
|
+
function harvestCssHints(entry){
|
|
1571
|
+
try{
|
|
1572
|
+
const refs=(entry.bindings&&entry.bindings.elements)||[];
|
|
1573
|
+
let el=null;
|
|
1574
|
+
for(const r of refs){const e=r&&r.deref&&r.deref();if(e){el=e;break;}}
|
|
1575
|
+
if(!el)return [];
|
|
1576
|
+
// Own classes are the precise anchor: every state variant that targets
|
|
1577
|
+
// this element (`.dd.is-open .dd-panel`, `.dd[data-open] .dd-panel`) still
|
|
1578
|
+
// names the element's own leaf class, so matching on it scopes tightly to
|
|
1579
|
+
// this component without dragging in unrelated utility-class rules.
|
|
1580
|
+
const own=(typeof el.className==="string"?el.className.trim().split(/\s+/):Array.from(el.classList||[])).filter(Boolean);
|
|
1581
|
+
if(!own.length)return [];
|
|
1582
|
+
const esc=s=>s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");
|
|
1583
|
+
const tokRes=own.map(c=>new RegExp("\\."+esc(c)+"(?![\\w-])"));
|
|
1584
|
+
const matchTok=sel=>tokRes.some(re=>re.test(sel));
|
|
1585
|
+
const cs=getComputedStyle(el);
|
|
1586
|
+
const resolveVars=txt=>txt.replace(/var\(\s*(--[\w-]+)\s*(?:,[^()]*)?\)/g,(m,v)=>{const r=cs.getPropertyValue(v).trim();return r||m;});
|
|
1587
|
+
const out=[];const seen=new Set();
|
|
1588
|
+
const collect=list=>{
|
|
1589
|
+
for(const rule of Array.from(list||[])){
|
|
1590
|
+
if(out.length>=HINT_MAX_RULES)return;
|
|
1591
|
+
// @media / @supports etc. — recurse into nested rules.
|
|
1592
|
+
if(rule.cssRules&&rule.selectorText===undefined){collect(rule.cssRules);continue;}
|
|
1593
|
+
const sel=rule.selectorText;
|
|
1594
|
+
if(!sel||!matchTok(sel))continue;
|
|
1595
|
+
let txt=rule.cssText;if(!txt)continue;
|
|
1596
|
+
txt=resolveVars(txt).replace(/\s+/g," ").trim();
|
|
1597
|
+
if(txt.length>HINT_MAX_RULE_LEN)txt=txt.slice(0,HINT_MAX_RULE_LEN)+" …}";
|
|
1598
|
+
if(seen.has(txt))continue;seen.add(txt);
|
|
1599
|
+
out.push(txt);
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
for(const sheet of Array.from(document.styleSheets||[])){
|
|
1603
|
+
if(out.length>=HINT_MAX_RULES)break;
|
|
1604
|
+
let rules;try{rules=sheet.cssRules;}catch{continue;} // cross-origin → skip
|
|
1605
|
+
collect(rules);
|
|
1606
|
+
}
|
|
1607
|
+
return out;
|
|
1608
|
+
}catch{return [];}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1539
1611
|
// ── hooks ──
|
|
1540
1612
|
const TimelineCtx = createContext(null);
|
|
1541
1613
|
function useReg(){const{registry}=useContext(TimelineCtx);return useSyncExternalStore(useCallback(cb=>registry.subscribe(cb),[registry]),useCallback(()=>registry.getAll(),[registry]),useCallback(()=>registry.getAll(),[registry]));}
|
|
@@ -1753,51 +1825,62 @@
|
|
|
1753
1825
|
const names=[...new Set(matches.map(t=>t.member))];
|
|
1754
1826
|
return names.length===1?names[0]:matches[0].member;
|
|
1755
1827
|
}
|
|
1756
|
-
// Apply button —
|
|
1757
|
-
//
|
|
1758
|
-
//
|
|
1759
|
-
//
|
|
1828
|
+
// Apply button — "Apply" ⟷ "Applied" cross-blur text swap (transitions.dev 04,
|
|
1829
|
+
// same mechanism as the Play/Pause button): both labels stack in one slot, the
|
|
1830
|
+
// active one is sharp/opaque and the other blurred/transparent, and the slot
|
|
1831
|
+
// width tweens between the two measured widths (--aw-a -> --aw-b). The gray
|
|
1832
|
+
// checkmark draws its stroke path IN PLACE (transitions.dev 10, no rotate/bob/
|
|
1833
|
+
// scale) — just opacity fade + stroke-dashoffset draw. The same <button> node is
|
|
1834
|
+
// reused across states so the swap slot and its widths persist.
|
|
1760
1835
|
function ApplyBtn({applied,onApply}){
|
|
1761
1836
|
const ref=useRef(null);
|
|
1762
|
-
const lastW=useRef(0); // width captured while showing "Apply"
|
|
1763
1837
|
const prev=useRef(applied);
|
|
1838
|
+
// measure both labels once (after fonts/layout) and pin the two slot widths
|
|
1839
|
+
// so the swap can tween between "Apply" and the checked "Applied".
|
|
1764
1840
|
useLayoutEffect(()=>{
|
|
1765
1841
|
const btn=ref.current;if(!btn)return;
|
|
1842
|
+
const sw=btn.querySelector(".tl-apply-swap"); if(!sw) return;
|
|
1843
|
+
const measure=()=>{
|
|
1844
|
+
const a=sw.querySelector(".tl-apply-a"), b=sw.querySelector(".tl-apply-b");
|
|
1845
|
+
if(a) sw.style.setProperty("--aw-a", Math.ceil(a.getBoundingClientRect().width)+"px");
|
|
1846
|
+
if(b) sw.style.setProperty("--aw-b", Math.ceil(b.getBoundingClientRect().width)+"px");
|
|
1847
|
+
};
|
|
1848
|
+
measure();
|
|
1849
|
+
if(document.fonts&&document.fonts.ready) document.fonts.ready.then(measure).catch(()=>{});
|
|
1850
|
+
},[]);
|
|
1851
|
+
// drive the stroke-draw: set exact dasharray, and play on the flip to applied.
|
|
1852
|
+
// already-applied items mount fully drawn (data-state stays "in", no replay).
|
|
1853
|
+
useLayoutEffect(()=>{
|
|
1854
|
+
const btn=ref.current;if(!btn)return;
|
|
1855
|
+
const chk=btn.querySelector(".tl-sug-check");
|
|
1766
1856
|
const justApplied=applied&&!prev.current;
|
|
1767
|
-
if(
|
|
1768
|
-
const chk
|
|
1769
|
-
if(
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
path.style.strokeDasharray=len; path.style.strokeDashoffset=len; }
|
|
1857
|
+
if(chk){
|
|
1858
|
+
const path=chk.querySelector("svg path");
|
|
1859
|
+
if(path){ const len=Math.ceil(path.getTotalLength()); // exact path length
|
|
1860
|
+
path.style.strokeDasharray=len; path.style.strokeDashoffset=len; }
|
|
1861
|
+
if(applied){
|
|
1773
1862
|
if(justApplied){
|
|
1774
|
-
//
|
|
1775
|
-
const to=btn.getBoundingClientRect().width, from=lastW.current;
|
|
1776
|
-
if(from&&to&&Math.abs(from-to)>0.5){
|
|
1777
|
-
btn.style.width=from+"px";
|
|
1778
|
-
void btn.offsetWidth; // reflow so the tween starts from `from`
|
|
1779
|
-
btn.style.width=to+"px";
|
|
1780
|
-
const onEnd=(e)=>{ if(e.target===btn&&e.propertyName==="width"){
|
|
1781
|
-
btn.style.width=""; btn.removeEventListener("transitionend",onEnd); } };
|
|
1782
|
-
btn.addEventListener("transitionend",onEnd);
|
|
1783
|
-
}
|
|
1784
|
-
// success-check replay: reset, force reflow, play from offset 0
|
|
1863
|
+
// replay from offset 0: reset, force reflow, play
|
|
1785
1864
|
chk.setAttribute("data-state","out");
|
|
1786
1865
|
void chk.offsetWidth;
|
|
1787
1866
|
chk.setAttribute("data-state","in");
|
|
1788
1867
|
} else { chk.setAttribute("data-state","in"); }
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1868
|
+
} else { chk.setAttribute("data-state","out"); }
|
|
1869
|
+
}
|
|
1791
1870
|
prev.current=applied;
|
|
1792
1871
|
});
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1872
|
+
return h("button",{ref,
|
|
1873
|
+
className:cx("tl-pill-btn tl-apply-btn",applied&&"is-applied"),
|
|
1874
|
+
disabled:applied,onClick:applied?undefined:onApply},
|
|
1875
|
+
h("span",{className:"tl-apply-swap","data-state":applied?"applied":"apply"},
|
|
1876
|
+
h("span",{className:"tl-apply-label tl-apply-a"},"Apply"),
|
|
1877
|
+
h("span",{className:"tl-apply-label tl-apply-b"},
|
|
1878
|
+
h("span",{className:"tl-sug-check","data-state":"out","aria-hidden":"true"},
|
|
1879
|
+
h("svg",{width:13,height:13,viewBox:"0 0 16 16",fill:"none",
|
|
1880
|
+
xmlns:"http://www.w3.org/2000/svg",style:{display:"block"}},
|
|
1881
|
+
h("path",{d:"M4 8.4268L6.46155 11.19223L12 4.97001",stroke:"currentColor",
|
|
1882
|
+
strokeWidth:1.5,strokeLinecap:"round",strokeLinejoin:"round"}))),
|
|
1883
|
+
"Applied")));
|
|
1801
1884
|
}
|
|
1802
1885
|
function RefineResults({suggestions,appliedIds,onApply,lanes}){
|
|
1803
1886
|
const ref=useRef(null);
|
|
@@ -2037,10 +2120,14 @@
|
|
|
2037
2120
|
const noMatches=!!ql&&fGroups.length===0&&fFlat.length===0;
|
|
2038
2121
|
const phaseItem=(e)=>h(MenuItem,{key:e.id,active:active&&e.id===active.id,onClick:()=>{onSelect(e.id);setPick(false);},
|
|
2039
2122
|
right:active&&e.id===active.id&&h("span",{className:"tl-menu-check"},h(Ic,{name:"accept"}))},
|
|
2040
|
-
h("span",
|
|
2123
|
+
h("span",{className:"tl-menu-stack"},
|
|
2124
|
+
h("span",{className:"tl-menu-text"},e.phaseLabel),
|
|
2125
|
+
h("span",{className:"tl-menu-dim tl-menu-sub"},e.durationMs+"ms")));
|
|
2041
2126
|
const flatItem=(e)=>h(MenuItem,{key:e.id,active:active&&e.id===active.id,onClick:()=>{onSelect(e.id);setPick(false);},
|
|
2042
2127
|
right:active&&e.id===active.id&&h("span",{className:"tl-menu-check"},h(Ic,{name:"accept"}))},
|
|
2043
|
-
h("span",
|
|
2128
|
+
h("span",{className:"tl-menu-stack"},
|
|
2129
|
+
h("span",{className:"tl-menu-text"},e.label),
|
|
2130
|
+
h("span",{className:"tl-menu-dim tl-menu-sub"},e.durationMs+"ms")));
|
|
2044
2131
|
return h("div",{className:"tl-header"},
|
|
2045
2132
|
h("span",{className:"tl-header-label"},"Selected"),
|
|
2046
2133
|
h("button",{ref:pickRef,className:cx("tl-ghost-btn",pick&&"is-active"),disabled:!active,onClick:()=>setPick(v=>!v)},
|
|
@@ -2064,7 +2151,10 @@
|
|
|
2064
2151
|
...fFlat.map(flatItem))),
|
|
2065
2152
|
h("span",{className:"tl-header-count"},
|
|
2066
2153
|
scanning
|
|
2067
|
-
? h("span",{className:"
|
|
2154
|
+
? h("span",{className:"t-tt-wrap"},
|
|
2155
|
+
h("span",{className:"tl-scan-note",tabIndex:0},"Agent scanning transitions…"),
|
|
2156
|
+
h("span",{className:"t-tt tl-tt-below tl-tt-msg",role:"tooltip"},
|
|
2157
|
+
"Agent is working on understanding your transitions and creating grouping and naming. This usually takes a few seconds to a minute."))
|
|
2068
2158
|
: entries.length+" transition"+(entries.length===1?"":"s")+" found"),
|
|
2069
2159
|
h("button",{ref:gearRef,className:cx("tl-icon-btn",setg&&"is-active"),title:"Settings",onClick:()=>setSetg(v=>!v)},h(Ic,{name:"dotsv"})),
|
|
2070
2160
|
h(Dropdown,{open:setg,onClose:()=>setSetg(false),triggerRef:gearRef,width:210,align:"right"},
|
|
@@ -2232,7 +2322,10 @@
|
|
|
2232
2322
|
// fill the width, while PAD_Y keeps the 0..1 band vertically centered with
|
|
2233
2323
|
// equal headroom above/below so overshoot/undershoot handles (clamped to the
|
|
2234
2324
|
// -0.5..1.5 range) stay visible instead of being clipped at the edges.
|
|
2235
|
-
|
|
2325
|
+
// square plot with equal padding on all sides — standard cubic-bezier
|
|
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 };
|
|
2236
2329
|
|
|
2237
2330
|
// Unified "Preview" section (Figma node 580:11158) — collapsible, collapsed by
|
|
2238
2331
|
// default (transitions.dev · 21-accordion). Holds BOTH the timing curve card
|
|
@@ -2243,6 +2336,7 @@
|
|
|
2243
2336
|
const trackRef = useRef(null);
|
|
2244
2337
|
const dotRef = useRef(null);
|
|
2245
2338
|
const svgRef = useRef(null);
|
|
2339
|
+
const playSwapRef = useRef(null);
|
|
2246
2340
|
const [playing, setPlaying] = useState(false);
|
|
2247
2341
|
const [open, setOpen] = useState(false);
|
|
2248
2342
|
const safeEasing = (!easing || easing.trim()==="") ? "linear" : easing;
|
|
@@ -2316,6 +2410,20 @@
|
|
|
2316
2410
|
return ()=>{ cancelled = true; if(anim){try{anim.cancel();}catch{}} if(timer)clearTimeout(timer); };
|
|
2317
2411
|
},[playing, open, safeEasing, dur]);
|
|
2318
2412
|
|
|
2413
|
+
// measure both labels once the panel is open (fonts/layout ready) and
|
|
2414
|
+
// pin the two widths so the slot can tween between them on toggle.
|
|
2415
|
+
useEffect(()=>{
|
|
2416
|
+
if(!open) return;
|
|
2417
|
+
const sw = playSwapRef.current; if(!sw) return;
|
|
2418
|
+
const measure = ()=>{
|
|
2419
|
+
const a = sw.querySelector(".tl-play-a"), b = sw.querySelector(".tl-play-b");
|
|
2420
|
+
if(a) sw.style.setProperty("--pw-a", Math.ceil(a.getBoundingClientRect().width)+"px");
|
|
2421
|
+
if(b) sw.style.setProperty("--pw-b", Math.ceil(b.getBoundingClientRect().width)+"px");
|
|
2422
|
+
};
|
|
2423
|
+
measure();
|
|
2424
|
+
if(document.fonts&&document.fonts.ready) document.fonts.ready.then(measure).catch(()=>{});
|
|
2425
|
+
},[open]);
|
|
2426
|
+
|
|
2319
2427
|
const p1x = fx(cb[0]), p1y = fy(cb[1]), p2x = fx(cb[2]), p2y = fy(cb[3]);
|
|
2320
2428
|
|
|
2321
2429
|
return h("div",{className:"tl-preview","data-open":open?"true":"false"},
|
|
@@ -2346,7 +2454,10 @@
|
|
|
2346
2454
|
),
|
|
2347
2455
|
h("div",{className:"tl-preview-sub"},
|
|
2348
2456
|
h("span",{className:"tl-preview-sub-label"},"Position preview"),
|
|
2349
|
-
h("button",{className:"tl-preview-play",onClick:()=>setPlaying(p=>!p)},
|
|
2457
|
+
h("button",{className:"tl-preview-play",onClick:()=>setPlaying(p=>!p)},
|
|
2458
|
+
h("span",{ref:playSwapRef,className:"tl-play-swap","data-state":playing?"pause":"play"},
|
|
2459
|
+
h("span",{className:"tl-play-label tl-play-a"},"Play"),
|
|
2460
|
+
h("span",{className:"tl-play-label tl-play-b"},"Pause"))),
|
|
2350
2461
|
),
|
|
2351
2462
|
h("div",{className:"tl-preview-track"},
|
|
2352
2463
|
h("div",{className:"tl-preview-rail-wrap",ref:trackRef},
|
|
@@ -2730,9 +2841,6 @@
|
|
|
2730
2841
|
const[acceptError,setAcceptError]=useState(null);
|
|
2731
2842
|
// ── grouped scan (agent reads source → Open/Close phases) ──
|
|
2732
2843
|
const[groupScanState,setGroupScanState]=useState("idle"); // idle | scanning | done | error
|
|
2733
|
-
// Escape hatch for the gate: if a scan errors/times out the user can choose
|
|
2734
|
-
// to proceed with the flat (ungrouped) list instead of being trapped.
|
|
2735
|
-
const[skipGrouping,setSkipGrouping]=useState(false);
|
|
2736
2844
|
const didGroupScanRef=useRef(false);
|
|
2737
2845
|
const[refineLabel,setRefineLabel]=useState(null);
|
|
2738
2846
|
const[appliedIds,setAppliedIds]=useState({});
|
|
@@ -2981,11 +3089,13 @@
|
|
|
2981
3089
|
// Capped (~12s) so a page with no resolvable flat entries can never hang
|
|
2982
3090
|
// the panel on "Grouping…" — it falls through to the empty-flat check below.
|
|
2983
3091
|
let prev=-1,stable=0,iters=0;
|
|
2984
|
-
while(scanTokenRef.current===token&&iters++<
|
|
3092
|
+
while(scanTokenRef.current===token&&iters++<60){
|
|
2985
3093
|
const n=registry.getAll().filter(e=>e.kind!=="phase").length;
|
|
2986
|
-
|
|
3094
|
+
// two consecutive equal reads is enough; at 150ms ticks that's ~300ms
|
|
3095
|
+
// instead of the old ~900ms, shaving dead time off every scan.
|
|
3096
|
+
if(n>0&&n===prev){if(++stable>=1)break;}else stable=0;
|
|
2987
3097
|
prev=n;
|
|
2988
|
-
await new Promise(r=>setTimeout(r,
|
|
3098
|
+
await new Promise(r=>setTimeout(r,150));
|
|
2989
3099
|
}
|
|
2990
3100
|
if(scanTokenRef.current!==token)return;
|
|
2991
3101
|
const flat=registry.getAll().filter(e=>e.kind!=="phase");
|
|
@@ -2999,12 +3109,16 @@
|
|
|
2999
3109
|
setGroupScanState("scanning");
|
|
3000
3110
|
const raw=flat.map(e=>({label:e.label,selector:e.bindings&&e.bindings.selector,
|
|
3001
3111
|
properties:e.properties,
|
|
3002
|
-
timings:(e.baseLanes||[]).map(l=>({property:l.property,durationMs:l.durationMs,delayMs:l.delayMs,easing:l.easing}))
|
|
3112
|
+
timings:(e.baseLanes||[]).map(l=>({property:l.property,durationMs:l.durationMs,delayMs:l.delayMs,easing:l.easing})),
|
|
3113
|
+
// CSS rules (base + open + close states) for this element, var()s
|
|
3114
|
+
// resolved — lets the agent recover the opposite phase + toggled state
|
|
3115
|
+
// without reading source. Empty → agent falls back to source reads.
|
|
3116
|
+
cssRules:harvestCssHints(e)}));
|
|
3003
3117
|
try{
|
|
3004
3118
|
const{id}=await relayCreateJob({kind:"scan",url:location.href,raw});
|
|
3005
|
-
for(let i=0;i<
|
|
3119
|
+
for(let i=0;i<500;i++){
|
|
3006
3120
|
if(scanTokenRef.current!==token)return; // superseded / unmounted
|
|
3007
|
-
await new Promise(r=>setTimeout(r,
|
|
3121
|
+
await new Promise(r=>setTimeout(r,300));
|
|
3008
3122
|
if(scanTokenRef.current!==token)return;
|
|
3009
3123
|
const job=await relayGetJob(id);
|
|
3010
3124
|
if(job.status==="done"){
|
|
@@ -3025,7 +3139,6 @@
|
|
|
3025
3139
|
const rescanTransitions=useCallback(()=>{
|
|
3026
3140
|
try{localStorage.removeItem(GROUP_STORE_KEY);}catch{}
|
|
3027
3141
|
registry.clearGroups();
|
|
3028
|
-
setSkipGrouping(false); // re-engage the gate so a fresh scan can be shown
|
|
3029
3142
|
runGroupScan();
|
|
3030
3143
|
},[runGroupScan,GROUP_STORE_KEY,registry]);
|
|
3031
3144
|
// Gate the auto group-scan behind a live agent: the panel stays on the
|
|
@@ -3085,16 +3198,13 @@
|
|
|
3085
3198
|
// scanned the page's transitions.
|
|
3086
3199
|
// loading → first /health probe pending (blank, avoids a text flash)
|
|
3087
3200
|
// blocked → no live agent → "Before we start" (run /refine live)
|
|
3088
|
-
//
|
|
3089
|
-
//
|
|
3090
|
-
//
|
|
3091
|
-
//
|
|
3092
|
-
//
|
|
3093
|
-
// jobs) would trap the panel forever with no way out.
|
|
3201
|
+
// ready → live agent → the real UI, immediately. The group scan runs
|
|
3202
|
+
// in the BACKGROUND (no blocking screen); progress shows as a
|
|
3203
|
+
// small "Agent scanning transitions…" note in the header count
|
|
3204
|
+
// slot. A failed scan just leaves the flat list (Rescan in
|
|
3205
|
+
// Settings to retry) — it never traps the panel.
|
|
3094
3206
|
const gate = !live
|
|
3095
3207
|
? (llmAvailable===null ? "loading" : "blocked")
|
|
3096
|
-
: groupScanState==="scanning" ? "scanning"
|
|
3097
|
-
: (groupScanState==="error" && !skipGrouping) ? "scan-error"
|
|
3098
3208
|
: "ready";
|
|
3099
3209
|
return h(React.Fragment,null,
|
|
3100
3210
|
render&&h("div",{className:"t-panel-slide","data-timeline-panel":true,
|
|
@@ -3122,25 +3232,7 @@
|
|
|
3122
3232
|
h("div",{className:"tl-gate-title"},"Before we start"),
|
|
3123
3233
|
h("p",{className:"tl-gate-text"},
|
|
3124
3234
|
"Please run the ",h("code",{className:"tl-code"},"/refine live"),
|
|
3125
|
-
" command in your agent to enable live features, such as scanning and refining transitions."))),
|
|
3126
|
-
gate==="scanning" && h("div",{className:"tl-gate"},
|
|
3127
|
-
h("div",{className:"tl-gate-col"},
|
|
3128
|
-
h("div",{className:"tl-gate-pill-wrap"},
|
|
3129
|
-
h(BorderBeam,{size:"sm",colorVariant:"ocean",theme:"light",borderRadius:18,duration:2.6,saturation:2,brightness:1.6,className:"tl-gate-beam"},
|
|
3130
|
-
h("span",{className:"tl-gate-beam-fill"})),
|
|
3131
|
-
h("span",{className:"tl-gate-pill"},
|
|
3132
|
-
h(DotmLoader),
|
|
3133
|
-
h("span",{className:"tl-gate-pill-label"},"Agent is scanning your transitions"))),
|
|
3134
|
-
h("p",{className:"tl-gate-sub"},"Just a moment while we get things ready."))),
|
|
3135
|
-
gate==="scan-error" && h("div",{className:"tl-gate"},
|
|
3136
|
-
h("div",{className:"tl-gate-col"},
|
|
3137
|
-
h("div",{className:"tl-gate-title"},"We couldn’t scan your transitions"),
|
|
3138
|
-
h("p",{className:"tl-gate-text"},
|
|
3139
|
-
"The agent didn’t finish grouping. Make sure ",h("code",{className:"tl-code"},"/refine live"),
|
|
3140
|
-
" is running with the latest skill, then try again — or continue with the ungrouped list."),
|
|
3141
|
-
h("div",{className:"tl-gate-actions"},
|
|
3142
|
-
h("button",{className:"tl-gate-btn tl-gate-btn-primary",onClick:rescanTransitions},"Try again"),
|
|
3143
|
-
h("button",{className:"tl-gate-btn",onClick:()=>setSkipGrouping(true)},"Continue without grouping"))))),
|
|
3235
|
+
" command in your agent to enable live features, such as scanning and refining transitions.")))),
|
|
3144
3236
|
toast&&createPortal(
|
|
3145
3237
|
h("div",{className:"tl-toast-wrap","aria-live":"polite"},
|
|
3146
3238
|
h("div",{className:cx("tl-toast",toast.closing&&"is-closing")},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "transitions-refine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Live, agent-driven Refine panel for CSS/Motion transitions — injects a timeline + Refine UI and runs transitions.dev suggestions via your coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server/relay.mjs
CHANGED
|
@@ -50,7 +50,26 @@ function augmentAgentCmd(cmd) {
|
|
|
50
50
|
return extra.length ? `${cmd} ${extra.join(" ")}` : cmd;
|
|
51
51
|
}
|
|
52
52
|
const AGENT_CMD = augmentAgentCmd(process.env.REFINE_AGENT_CMD || null);
|
|
53
|
+
// Pin a fast model for scan jobs. Grouping is a structured task that doesn't
|
|
54
|
+
// need a heavy reasoning model, and the user's *default* model may be a slow one
|
|
55
|
+
// (Opus / GPT-5.5) — forcing a fast model here keeps the initial scan snappy.
|
|
56
|
+
// Override with REFINE_SCAN_MODEL="" to fall back to the agent's default.
|
|
57
|
+
const SCAN_MODEL = process.env.REFINE_SCAN_MODEL ?? "composer-2.5-fast";
|
|
53
58
|
const AGENT_TIMEOUT_MS = Number(process.env.REFINE_AGENT_TIMEOUT_MS) || 120000;
|
|
59
|
+
|
|
60
|
+
// Inject `--model <m>` into a `cursor-agent …` command (after the binary).
|
|
61
|
+
// IMPORTANT: `--model` and the SCAN_MODEL slug (e.g. "composer-2.5-fast") are
|
|
62
|
+
// cursor-agent-specific. If a user wired a different CLI (Codex, Claude Code, …)
|
|
63
|
+
// into REFINE_AGENT_CMD, appending the flag would be invalid and break the scan,
|
|
64
|
+
// so we leave non-cursor commands untouched — those agents still get the speedup
|
|
65
|
+
// from the trimmed scan prompt. Also a no-op when the model is empty
|
|
66
|
+
// (REFINE_SCAN_MODEL="") or a model is already pinned explicitly.
|
|
67
|
+
function withModel(cmd, model) {
|
|
68
|
+
if (!cmd || !model) return cmd;
|
|
69
|
+
if (!/cursor-agent/.test(cmd)) return cmd; // not cursor-agent → don't touch
|
|
70
|
+
if (/(^|\s)--model(\s|=)/.test(cmd)) return cmd; // respect an explicit choice
|
|
71
|
+
return cmd.replace(/^(\s*\S+)/, `$1 --model ${model}`);
|
|
72
|
+
}
|
|
54
73
|
const LONGPOLL_MS = Number(process.env.REFINE_LONGPOLL_MS) || 25000;
|
|
55
74
|
// Grace window after a `/refine live` agent's last poll during which LLM mode is
|
|
56
75
|
// still reported "available". Kept well above LONGPOLL_MS so the normal gaps
|
|
@@ -260,21 +279,19 @@ function buildScanPrompt(job) {
|
|
|
260
279
|
return [
|
|
261
280
|
"You are GROUPING UI transitions by reading the user's SOURCE CODE. A naive DOM scan only sees each element's current computed transition — it cannot tell open from close, and lists related elements separately. Fix that.",
|
|
262
281
|
"",
|
|
263
|
-
"Raw DOM-detected transitions (JSON) —
|
|
282
|
+
"Raw DOM-detected transitions (JSON). Each entry's `timings` are ALREADY ACCURATE for the component's CURRENT on-screen state — treat them as ground truth, do NOT re-derive them from source. Most entries also carry `cssRules`: the CSS rules harvested live from the page (CSSOM) that drive that element across ALL states (base + open + close), with var() already resolved to concrete values.",
|
|
264
283
|
JSON.stringify({ url: r.url, raw: r.raw }, null, 2),
|
|
265
284
|
"",
|
|
285
|
+
"FAST PATH — when an entry has `cssRules`, they are AUTHORITATIVE and contain everything you need: the opposite-phase timings live on a state-variant selector inside them (e.g. `.dd.is-closing .dd-panel`, `.modal[data-closing] .dialog`), and the toggled state is visible in those selectors. Derive grouping, phases, toggled state, and opposite-phase timings DIRECTLY from `cssRules` + `timings`. Do NOT glob, grep, or read files for any element whose `cssRules` is non-empty — it only wastes time. ONLY fall back to reading source for entries with an empty/missing `cssRules` (CORS-locked sheets, styled-components, Tailwind, etc.), and even then read the minimum.",
|
|
286
|
+
"",
|
|
266
287
|
"Steps:",
|
|
267
|
-
"1. Identify each animated UI component (dropdown, modal, tooltip, accordion, drawer, toast…).
|
|
268
|
-
"2. For each component, split into PHASES — typically `open` and `close` (a hover-only component may have a single phase).
|
|
288
|
+
"1. Identify each animated UI component (dropdown, modal, tooltip, accordion, drawer, toast…). The provided `label`/`selector`/`properties` usually make the grouping obvious; only read source when the grouping is genuinely unclear.",
|
|
289
|
+
"2. For each component, split into PHASES — typically `open` and `close` (a hover-only component may have a single phase). The phase matching the CURRENT DOM state reuses the provided timings verbatim. The OPPOSITE phase often lives on a different selector (e.g. `.is-open` vs `.is-closing`) with different timings — take it from this element's `cssRules` (or, only if it has none, read source). Report BOTH even though only one is in the DOM right now.",
|
|
269
290
|
"3. PHASE STATE — how the phase is driven (REQUIRED for playback to work). For each phase provide:",
|
|
270
291
|
" - `stateTarget`: a CSS selector for the ONE element whose class/attribute is toggled to drive the whole phase (e.g. the dropdown root, the `.modal`, the element with `[data-open]`). It MUST resolve in the live DOM RIGHT NOW, in any state — so it must NOT itself contain the toggled state (write `.t-morph`, never `.t-morph[data-open=\"true\"]`).",
|
|
271
292
|
" - `fromState` and `toState`: the class/attribute on `stateTarget` at the START and END of this phase, as a token: a class `\".is-open\"`, an attribute `\"[data-open=\\\"true\\\"]\"`, or `null`/`\"\"` for the base/no-class state. OPEN usually goes base→open (`fromState:null`, `toState:\".is-open\"`); CLOSE goes open→base (`fromState:\".is-open\"`, `toState:null`). Get the DIRECTION right — open must animate into the open look, close must animate back out.",
|
|
272
|
-
"4. For each phase, list its MEMBER elements (panel, backdrop, the staggered items…). Give each member a stable `id`, a human `label`, a CSS `selector`, and its
|
|
273
|
-
"5. TIMINGS
|
|
274
|
-
" - List one `propertyTimings` entry per animated property. Read EACH property's own duration/delay/easing from the shorthand `transition:` list (or the property-specific longhand). Do NOT copy one property's duration onto the others, and do NOT use the phase's longest/representative duration for every lane.",
|
|
275
|
-
" - Resolve CSS custom properties (e.g. `var(--morph-fade-dur)`) to concrete numbers by following the `:root`/scope where they're defined; convert `s`→ms (`0.25s`→250). Never emit a `var(...)` or a guess.",
|
|
276
|
-
" - It is normal and expected for properties within one phase to differ (e.g. opacity/filter 200ms but transform 350ms). If every property in a phase ends up identical, re-read the source — you probably collapsed them by mistake.",
|
|
277
|
-
" - Open and close usually have DIFFERENT durations/easings; report each from its own rule.",
|
|
293
|
+
"4. For each phase, list its MEMBER elements (panel, backdrop, the staggered items…). Give each member a stable `id`, a human `label`, a CSS `selector`, and its `propertyTimings`. For the phase that matches the current DOM, COPY each member's `propertyTimings` straight from the provided `raw.timings` (same per-property duration/delay/easing) — don't change numbers you were handed. The member `selector` MUST resolve in the live DOM RIGHT NOW regardless of phase — use the BASE element selector and do NOT bake the phase's toggled class/attribute into it (write `.t-morph .t-morph-plus`, never `.t-morph[data-open=\"true\"] .t-morph-plus`). The toggled state belongs only in the phase's `stateTarget`/`toState`.",
|
|
294
|
+
"5. TIMINGS ARE PER-PROPERTY. For the OPPOSITE phase (from `cssRules`, or source if none): list one `propertyTimings` entry per animated property with its own duration/delay/easing; `cssRules` already have var() resolved, but if you ever read a raw `var(...)` resolve it to a concrete number, and convert `s`→ms (`0.25s`→250); never emit a `var(...)` or a guess. Open and close usually have DIFFERENT durations/easings. (The current-state phase just reuses the provided numbers.)",
|
|
278
295
|
"",
|
|
279
296
|
"Output ONLY a JSON object — no prose, no markdown fences — shaped exactly like:",
|
|
280
297
|
'{"summary":"Grouped 3 components.","groups":[{"id":"dropdown","label":"Dropdown","component":"src/Dropdown.tsx","phases":[{"id":"dropdown:open","phase":"open","label":"Open","stateTarget":".dropdown","fromState":null,"toState":".is-open","members":[{"id":"panel","label":"Panel","selector":".dropdown .dropdown-panel","propertyTimings":[{"property":"opacity","durationMs":200,"delayMs":0,"easing":"ease-out"},{"property":"transform","durationMs":200,"delayMs":0,"easing":"cubic-bezier(0.22, 1, 0.36, 1)"}]}]},{"id":"dropdown:close","phase":"close","label":"Close","stateTarget":".dropdown","fromState":".is-open","toState":null,"members":[{"id":"panel","label":"Panel","selector":".dropdown .dropdown-panel","propertyTimings":[{"property":"opacity","durationMs":150,"delayMs":0,"easing":"ease-in"},{"property":"transform","durationMs":150,"delayMs":0,"easing":"ease-in"}]}]}]}]}',
|
|
@@ -340,7 +357,7 @@ async function answerJob(job) {
|
|
|
340
357
|
);
|
|
341
358
|
}
|
|
342
359
|
job.statusLog.push({ message: "Reading components from source…", at: now() });
|
|
343
|
-
result = await runAgentCmd(AGENT_CMD, buildScanPrompt(job), parseScanOutput);
|
|
360
|
+
result = await runAgentCmd(withModel(AGENT_CMD, SCAN_MODEL), buildScanPrompt(job), parseScanOutput);
|
|
344
361
|
job.result = { groups: result.groups, summary: result.summary };
|
|
345
362
|
job.status = "done";
|
|
346
363
|
job.updatedAt = now();
|