transitions-refine 0.1.3 → 0.3.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transitions-refine",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
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/inject.mjs CHANGED
@@ -86,15 +86,13 @@ function buildEpilogue(css) {
86
86
 
87
87
  function InjectedRoot(){
88
88
  const registry = useMemo(() => new TransitionRegistry(), []);
89
- const preview = useMemo(() => new PreviewController(), []);
90
89
  const [activeId, setActiveId] = useState(null);
91
90
  useEffect(() => {
92
91
  const scanner = new DomScanner(document.body, registry);
93
- preview.setScanner(scanner);
94
92
  scanner.start();
95
- return () => { scanner.stop(); preview.setScanner(null); };
96
- }, [registry, preview]);
97
- const ctx = useMemo(() => ({ registry, preview, activeId, setActiveId }), [registry, preview, activeId]);
93
+ return () => { scanner.stop(); };
94
+ }, [registry]);
95
+ const ctx = useMemo(() => ({ registry, activeId, setActiveId }), [registry, activeId]);
98
96
  return h(TimelineCtx.Provider, { value: ctx }, h(TimelinePanel));
99
97
  }
100
98
 
package/server/relay.mjs CHANGED
@@ -168,6 +168,14 @@ function parseApplyOutput(stdout) {
168
168
  return { applied: Boolean(obj.applied), summary: obj.summary ?? null, files: Array.isArray(obj.files) ? obj.files : null };
169
169
  }
170
170
 
171
+ // Scan jobs ask the agent to read the source and group related transitions into
172
+ // components with open/close phases and member elements.
173
+ function parseScanOutput(stdout) {
174
+ const obj = parseJsonish(stdout);
175
+ if (!obj || !Array.isArray(obj.groups)) throw new Error("agent output missing groups[]");
176
+ return { groups: obj.groups, summary: obj.summary ?? null };
177
+ }
178
+
171
179
  function runAgentCmd(cmd, prompt, parse = parseAgentOutput) {
172
180
  return new Promise((resolve, reject) => {
173
181
  const child = spawn("sh", ["-c", cmd], { stdio: ["pipe", "pipe", "pipe"] });
@@ -205,11 +213,13 @@ function buildApplyPrompt(job) {
205
213
  "You are APPLYING an approved transition change to the user's SOURCE CODE. Edit files; do not just suggest.",
206
214
  "",
207
215
  "Change context (JSON):",
208
- JSON.stringify({ label: r.label, selector: r.selector, changes: r.changes }, null, 2),
216
+ JSON.stringify({ label: r.label, selector: r.selector, component: r.component, group: r.group, phase: r.phase, changes: r.changes }, null, 2),
217
+ "",
218
+ "If `phase` is set (e.g. \"open\"/\"close\"), the change targets ONE state of a component — edit the rule for THAT state (e.g. the `.is-open` rule for open, the `.is-closing`/base rule for close), not the other phase. Each change may carry its own `member` + `selector` identifying which element it belongs to.",
209
219
  "",
210
220
  "Steps:",
211
- "1. Find where this transition is defined in the source. Search by the selector/label/class names. Handle plain CSS, CSS Modules, styled-components/emotion template literals, Tailwind utilities/config, and inline style objects — the browser selector is a hint, the real declaration may live in any of these.",
212
- "2. For each change, edit the source so that property's transition uses the `to` values: durationMs (ms), easing, delayMs (ms). Keep the file's existing unit/format conventions (e.g. `0.25s` vs `250ms`) and only touch the timing of the named property. If a CSS variable / design token backs the value, update it at the most sensible single place.",
221
+ "1. Find where this transition is defined in the source. Search by the per-change `selector`/`member`, the `component` hint, and class names. Handle plain CSS, CSS Modules, styled-components/emotion template literals, Tailwind utilities/config, inline style objects, and Motion/Framer variants — the browser selector is a hint, the real declaration may live in any of these.",
222
+ "2. For each change, edit the source so that property's transition uses the `to` values: durationMs (ms), easing, delayMs (ms). Keep the file's existing unit/format conventions (e.g. `0.25s` vs `250ms`) and only touch the timing of the named property on the right member + phase. If a CSS variable / design token backs the value, update it at the most sensible single place.",
213
223
  "3. Make the minimal edit. Do not reformat or change unrelated code.",
214
224
  "",
215
225
  'Output ONLY a JSON object — no prose, no markdown fences — shaped exactly like:',
@@ -218,6 +228,35 @@ function buildApplyPrompt(job) {
218
228
  ].join("\n");
219
229
  }
220
230
 
231
+ // Prompt for a "scan" job: the agent reads the source and groups the raw,
232
+ // DOM-detected transitions into components with open/close phases and members.
233
+ function buildScanPrompt(job) {
234
+ const r = job.request || {};
235
+ return [
236
+ "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.",
237
+ "",
238
+ "Raw DOM-detected transitions (JSON) — use as hints to locate the components, not as the final answer:",
239
+ JSON.stringify({ url: r.url, raw: r.raw }, null, 2),
240
+ "",
241
+ "Steps:",
242
+ "1. Identify each animated UI component (dropdown, modal, tooltip, accordion, drawer, toast…). Read its source (CSS/CSS Modules, styled-components/emotion, Tailwind, inline styles, Motion/Framer variants).",
243
+ "2. For each component, split into PHASES — typically `open` and `close` (a hover-only component may have a single phase). Open and close often live on different selectors (e.g. `.is-open` vs `.is-closing`) with different timings; report BOTH even though only one is in the DOM right now.",
244
+ "3. PHASE STATE — how the phase is driven (REQUIRED for playback to work). For each phase provide:",
245
+ " - `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\"]`).",
246
+ " - `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.",
247
+ "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 real `propertyTimings`. 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`.",
248
+ "5. TIMINGS MUST BE EXACT AND PER-PROPERTY. This is the most common mistake — do not make it:",
249
+ " - 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.",
250
+ " - 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.",
251
+ " - 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.",
252
+ " - Open and close usually have DIFFERENT durations/easings; report each from its own rule.",
253
+ "",
254
+ "Output ONLY a JSON object — no prose, no markdown fences — shaped exactly like:",
255
+ '{"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"}]}]}]}]}',
256
+ "If you cannot confidently group anything, return an empty groups array with a short summary; the panel keeps its flat DOM scan.",
257
+ ].join("\n");
258
+ }
259
+
221
260
  function refineDeterministic(job) {
222
261
  // Whole-transition replacement needs usage inference + recipe selection, which
223
262
  // only the agent (LLM) path can do. Deterministic can only snap to tokens.
@@ -240,11 +279,15 @@ async function answerJob(job) {
240
279
  job.status = "working";
241
280
  job.updatedAt = now();
242
281
  const isApply = job.request?.kind === "apply";
282
+ const isScan = job.request?.kind === "scan";
243
283
  const label = job.request?.label || job.request?.selector || "transition";
244
284
  // The browser picks the mode per job via the LLM / Deterministic tabs.
245
285
  // Default: LLM when a command is configured, otherwise deterministic.
246
286
  const mode = job.request?.mode || (AGENT_CMD ? "llm" : "deterministic");
247
- job.statusLog.push({ message: isApply ? `Writing "${label}" to your code…` : `Scanning "${label}"…`, at: now() });
287
+ job.statusLog.push({
288
+ message: isApply ? `Writing "${label}" to your code…` : isScan ? "Grouping transitions from your source…" : `Scanning "${label}"…`,
289
+ at: now(),
290
+ });
248
291
  try {
249
292
  let result;
250
293
  if (isApply) {
@@ -263,6 +306,22 @@ async function answerJob(job) {
263
306
  console.log(` ✓ apply ${job.id.slice(0, 8)} — applied=${result.applied}`);
264
307
  return;
265
308
  }
309
+ if (isScan) {
310
+ // Reading source to group transitions can only be done by the agent.
311
+ if (!AGENT_CMD) {
312
+ throw new Error(
313
+ "Grouping transitions needs the agent. Run `/refine live` in your editor, " +
314
+ "or start the relay with REFINE_AGENT_CMD set."
315
+ );
316
+ }
317
+ job.statusLog.push({ message: "Reading components from source…", at: now() });
318
+ result = await runAgentCmd(AGENT_CMD, buildScanPrompt(job), parseScanOutput);
319
+ job.result = { groups: result.groups, summary: result.summary };
320
+ job.status = "done";
321
+ job.updatedAt = now();
322
+ console.log(` ✓ scan ${job.id.slice(0, 8)} — ${result.groups.length} group(s)`);
323
+ return;
324
+ }
266
325
  if (mode === "llm") {
267
326
  if (!AGENT_CMD) {
268
327
  throw new Error(
@@ -362,8 +421,8 @@ const server = createServer(async (req, res) => {
362
421
  return send(res, 400, { error: "Body must be { request: {...} }" });
363
422
  }
364
423
  const job = createJob(body.request);
365
- // Apply jobs edit source — agent only, never deterministic.
366
- const mode = job.request.kind === "apply"
424
+ // Apply and scan jobs read/edit source — agent only, never deterministic.
425
+ const mode = (job.request.kind === "apply" || job.request.kind === "scan")
367
426
  ? "llm"
368
427
  : (job.request.mode || (llmAvailable() ? "llm" : "deterministic"));
369
428
  job.request.mode = mode;
@@ -440,11 +499,14 @@ const server = createServer(async (req, res) => {
440
499
  const body = await readJson(req);
441
500
  if (body && Array.isArray(body.suggestions)) {
442
501
  job.result = { suggestions: body.suggestions, summary: body.summary ?? null };
502
+ } else if (body && Array.isArray(body.groups)) {
503
+ // scan-job result from a `/refine live` agent
504
+ job.result = { groups: body.groups, summary: body.summary ?? null };
443
505
  } else if (body && typeof body.applied !== "undefined") {
444
506
  // apply-job result from a `/refine live` agent
445
507
  job.result = { applied: Boolean(body.applied), summary: body.summary ?? null, files: Array.isArray(body.files) ? body.files : null };
446
508
  } else {
447
- return send(res, 400, { error: "Body must be { suggestions: [...] } or { applied, summary }" });
509
+ return send(res, 400, { error: "Body must be { suggestions: [...] }, { groups: [...] }, or { applied, summary }" });
448
510
  }
449
511
  job.status = "done";
450
512
  job.updatedAt = now();