transitions-refine 0.3.6 → 0.3.7
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 +18 -8
- package/demo.html +7 -5
- package/package.json +1 -1
- package/server/relay.mjs +27 -10
|
@@ -203,21 +203,31 @@ separately. You fix that by reading the source. The request looks like:
|
|
|
203
203
|
}
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
+
**Be fast.** The `raw.timings` are already accurate for each element's *current*
|
|
207
|
+
on-screen state — treat them as ground truth and reuse them verbatim. Read as
|
|
208
|
+
little source as you need: only to (a) group elements into components, (b)
|
|
209
|
+
recover the *opposite* phase (e.g. close) that isn't in the DOM right now, and
|
|
210
|
+
(c) find the toggled state. Don't open files just to re-read timings you were
|
|
211
|
+
already given.
|
|
212
|
+
|
|
206
213
|
Do this:
|
|
207
214
|
|
|
208
215
|
1. **Identify each animated component** the raw entries belong to (dropdown,
|
|
209
|
-
modal, tooltip, accordion, drawer, toast…).
|
|
210
|
-
|
|
211
|
-
Tailwind, inline styles,
|
|
216
|
+
modal, tooltip, accordion, drawer, toast…). The selectors/labels usually make
|
|
217
|
+
this obvious — only read source (plain CSS / CSS Modules,
|
|
218
|
+
styled-components/emotion, Tailwind, inline styles, Motion/Framer variants)
|
|
219
|
+
when the grouping is genuinely unclear.
|
|
212
220
|
2. **Split each component into phases** — usually `open` and `close` (a hover-only
|
|
213
|
-
component can be a single phase).
|
|
214
|
-
|
|
215
|
-
|
|
221
|
+
component can be a single phase). The phase matching the current DOM reuses the
|
|
222
|
+
provided timings; the *opposite* phase often lives on a different selector
|
|
223
|
+
(`.is-open` vs `.is-closing`) with different timings — read source for that one.
|
|
224
|
+
Report **both** even though only one is in the DOM right now.
|
|
216
225
|
3. **List each phase's members** — the elements that animate in that phase. Give
|
|
217
226
|
each a stable `id`, a human `label`, a live-resolvable CSS `selector`, an
|
|
218
227
|
optional `toState` hint (the class/attribute that drives the phase, e.g.
|
|
219
|
-
`.is-open`), and its
|
|
220
|
-
|
|
228
|
+
`.is-open`), and its `propertyTimings`. For the current-state phase, **copy the
|
|
229
|
+
provided `raw.timings` verbatim**; for the opposite phase, **quote the real
|
|
230
|
+
timings from source — never invent.**
|
|
221
231
|
4. **Post the groups** (this completes the job):
|
|
222
232
|
|
|
223
233
|
```bash
|
package/demo.html
CHANGED
|
@@ -2981,11 +2981,13 @@
|
|
|
2981
2981
|
// Capped (~12s) so a page with no resolvable flat entries can never hang
|
|
2982
2982
|
// the panel on "Grouping…" — it falls through to the empty-flat check below.
|
|
2983
2983
|
let prev=-1,stable=0,iters=0;
|
|
2984
|
-
while(scanTokenRef.current===token&&iters++<
|
|
2984
|
+
while(scanTokenRef.current===token&&iters++<60){
|
|
2985
2985
|
const n=registry.getAll().filter(e=>e.kind!=="phase").length;
|
|
2986
|
-
|
|
2986
|
+
// two consecutive equal reads is enough; at 150ms ticks that's ~300ms
|
|
2987
|
+
// instead of the old ~900ms, shaving dead time off every scan.
|
|
2988
|
+
if(n>0&&n===prev){if(++stable>=1)break;}else stable=0;
|
|
2987
2989
|
prev=n;
|
|
2988
|
-
await new Promise(r=>setTimeout(r,
|
|
2990
|
+
await new Promise(r=>setTimeout(r,150));
|
|
2989
2991
|
}
|
|
2990
2992
|
if(scanTokenRef.current!==token)return;
|
|
2991
2993
|
const flat=registry.getAll().filter(e=>e.kind!=="phase");
|
|
@@ -3002,9 +3004,9 @@
|
|
|
3002
3004
|
timings:(e.baseLanes||[]).map(l=>({property:l.property,durationMs:l.durationMs,delayMs:l.delayMs,easing:l.easing}))}));
|
|
3003
3005
|
try{
|
|
3004
3006
|
const{id}=await relayCreateJob({kind:"scan",url:location.href,raw});
|
|
3005
|
-
for(let i=0;i<
|
|
3007
|
+
for(let i=0;i<500;i++){
|
|
3006
3008
|
if(scanTokenRef.current!==token)return; // superseded / unmounted
|
|
3007
|
-
await new Promise(r=>setTimeout(r,
|
|
3009
|
+
await new Promise(r=>setTimeout(r,300));
|
|
3008
3010
|
if(scanTokenRef.current!==token)return;
|
|
3009
3011
|
const job=await relayGetJob(id);
|
|
3010
3012
|
if(job.status==="done"){
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "transitions-refine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
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). These timings are ALREADY ACCURATE for the component's CURRENT on-screen state — treat them as ground truth, do NOT re-derive them from source:",
|
|
264
283
|
JSON.stringify({ url: r.url, raw: r.raw }, null, 2),
|
|
265
284
|
"",
|
|
285
|
+
"To stay fast, read as little source as you need — only to (a) group elements into components, (b) recover the OPPOSITE phase (e.g. close) that isn't in the DOM right now, and (c) find the toggled state. Do not open files just to re-read timings you were already given.",
|
|
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 — read source for that one. 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 you read from source: list one `propertyTimings` entry per animated property with its own duration/delay/easing; resolve CSS custom properties (e.g. `var(--morph-fade-dur)`) to concrete numbers 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();
|