@agent-native/core 0.80.10 → 0.80.11

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.
Files changed (66) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +108 -15
  5. package/corpus/core/src/client/AssistantChat.tsx +123 -26
  6. package/corpus/core/src/client/sse-event-processor.ts +9 -0
  7. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
  8. package/corpus/templates/assets/AGENTS.md +4 -0
  9. package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
  10. package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
  11. package/corpus/templates/assets/actions/generate-image.ts +118 -67
  12. package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
  13. package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
  14. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
  15. package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
  16. package/corpus/templates/assets/app/i18n-data.ts +61 -0
  17. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  18. package/corpus/templates/assets/app/routes/library.tsx +145 -38
  19. package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
  20. package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
  21. package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
  22. package/corpus/templates/assets/server/lib/generation.ts +135 -11
  23. package/corpus/templates/assets/shared/api.ts +4 -0
  24. package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
  25. package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
  26. package/corpus/templates/design/actions/apply-component-prop-edit.ts +13 -5
  27. package/corpus/templates/design/actions/apply-design-state.ts +17 -4
  28. package/corpus/templates/design/actions/apply-motion-edit.ts +9 -46
  29. package/corpus/templates/design/actions/delete-design.ts +20 -0
  30. package/corpus/templates/design/actions/get-component-details.ts +16 -7
  31. package/corpus/templates/design/actions/index-design-tokens.ts +8 -4
  32. package/corpus/templates/design/actions/list-design-states.ts +19 -1
  33. package/corpus/templates/design/actions/open-component-source.ts +7 -2
  34. package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
  35. package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
  36. package/corpus/templates/design/actions/run-design-audit.ts +21 -6
  37. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +139 -14
  38. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +20 -3
  39. package/corpus/templates/design/app/components/design/EditPanel.tsx +428 -31
  40. package/corpus/templates/design/app/components/design/LayersPanel.tsx +110 -30
  41. package/corpus/templates/design/app/components/design/MotionDock.tsx +9 -0
  42. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1 -1
  43. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +12 -5
  44. package/corpus/templates/design/app/components/design/StatesPanel.tsx +4 -2
  45. package/corpus/templates/design/app/components/design/TokensPanel.tsx +13 -8
  46. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  47. package/corpus/templates/design/app/i18n-data.ts +11 -0
  48. package/corpus/templates/design/app/pages/DesignEditor.tsx +684 -179
  49. package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
  50. package/corpus/templates/design/e2e/global-setup.ts +94 -2
  51. package/corpus/templates/design/shared/component-model.ts +35 -0
  52. package/corpus/templates/design/shared/motion-compiler.ts +79 -8
  53. package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
  54. package/dist/agent/production-agent.d.ts.map +1 -1
  55. package/dist/agent/production-agent.js +94 -10
  56. package/dist/agent/production-agent.js.map +1 -1
  57. package/dist/client/AssistantChat.d.ts +14 -0
  58. package/dist/client/AssistantChat.d.ts.map +1 -1
  59. package/dist/client/AssistantChat.js +115 -22
  60. package/dist/client/AssistantChat.js.map +1 -1
  61. package/dist/client/sse-event-processor.d.ts.map +1 -1
  62. package/dist/client/sse-event-processor.js +10 -0
  63. package/dist/client/sse-event-processor.js.map +1 -1
  64. package/dist/observability/routes.d.ts +3 -3
  65. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  66. package/package.json +1 -1
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Clips AI request bridge
3
3
  *
4
- * Watches the `clips-ai-request-:id` application_state queue. The bridge sends
5
- * queued recording work to the agent chat exactly once per
4
+ * Polls the `list-ai-requests` action a single access-scoped call that returns
5
+ * every pending `clips-ai-request-*` entry for recordings the user can access.
6
+ * The bridge sends queued recording work to the agent chat exactly once per
6
7
  * (recordingId, kind, requestedAt).
7
8
  *
8
9
  * Once handled we DELETE the request entry so the next page load / tab switch
@@ -21,6 +22,7 @@ import { useRecordings, type RecordingSummary } from "./use-library";
21
22
 
22
23
  const DEFAULT_TITLE = "Untitled recording";
23
24
  const POLL_INTERVAL_MS = 3000;
25
+ const TWO_MINUTES_MS = 2 * 60 * 1000;
24
26
 
25
27
  /** True when `title` is blank or equal to the server-seeded default. */
26
28
  export function isDefaultTitle(title: string | null | undefined): boolean {
@@ -62,22 +64,21 @@ const DISPATCHABLE_REQUESTS = new Set([
62
64
  "generate-workflow",
63
65
  ]);
64
66
 
65
- async function readRequest(recordingId: string): Promise<AiRequest | null> {
66
- const url = agentNativePath(
67
- `/_agent-native/application-state/${encodeURIComponent(
68
- `clips-ai-request-${recordingId}`,
69
- )}`,
70
- );
67
+ async function listRequests(): Promise<Map<string, AiRequest>> {
71
68
  try {
72
- const res = await fetch(url);
73
- if (!res.ok) return null;
74
- const payload = await res.json().catch(() => null);
75
- if (!payload || typeof payload !== "object") return null;
76
- // The application-state endpoint wraps stored values under `.value`.
77
- const value = (payload as any).value ?? payload;
78
- return value as AiRequest;
69
+ const result = (await callAction("list-ai-requests", {} as any, {
70
+ method: "GET",
71
+ })) as { requests?: AiRequest[] } | null | undefined;
72
+ return new Map(
73
+ (result?.requests ?? [])
74
+ .filter(
75
+ (r): r is AiRequest & { recordingId: string } => !!r?.recordingId,
76
+ )
77
+ .map((r) => [r.recordingId, r]),
78
+ );
79
79
  } catch {
80
- return null;
80
+ // Swallow — the next tick retries.
81
+ return new Map();
81
82
  }
82
83
  }
83
84
 
@@ -120,16 +121,15 @@ export function useAutoTitleBridge(): void {
120
121
  if (cancelled || inflight.current) return;
121
122
  inflight.current = true;
122
123
  try {
124
+ const requestsById = await listRequests();
125
+ if (cancelled) return;
126
+
123
127
  for (const rec of readyRecordings) {
124
128
  if (cancelled) return;
125
129
 
126
- const request = await readRequest(rec.id);
130
+ const request = requestsById.get(rec.id) ?? null;
127
131
 
128
- if (
129
- request?.kind &&
130
- DISPATCHABLE_REQUESTS.has(request.kind) &&
131
- request.recordingId === rec.id
132
- ) {
132
+ if (request?.kind && DISPATCHABLE_REQUESTS.has(request.kind)) {
133
133
  // Server queued a delegation — use the full context it provided.
134
134
  // Key includes requestedAt so each distinct server request fires
135
135
  // exactly once, independent of any prior fallback dispatch.
@@ -164,9 +164,8 @@ export function useAutoTitleBridge(): void {
164
164
  continue;
165
165
  }
166
166
 
167
- const ageMs = Date.now() - new Date(rec.createdAt).getTime();
168
- const TWO_MINUTES_MS = 2 * 60 * 1000;
169
- if (ageMs < TWO_MINUTES_MS) continue;
167
+ if (Date.now() - new Date(rec.createdAt).getTime() < TWO_MINUTES_MS)
168
+ continue;
170
169
 
171
170
  // Use a dedicated key so a later server-queued request (e.g. from
172
171
  // a long transcription that finishes after the 2-min window) is
@@ -49,7 +49,10 @@ import type {
49
49
  ClassEditIntent,
50
50
  StyleEditIntent,
51
51
  } from "../shared/code-layer.js";
52
- import { componentNameFor } from "../shared/component-model.js";
52
+ import {
53
+ componentNameFor,
54
+ componentNodeIdMatches,
55
+ } from "../shared/component-model.js";
53
56
  import { hasCapability } from "../shared/design-source-capabilities.js";
54
57
  import { normalizeDesignSourceType } from "../shared/source-mode.js";
55
58
 
@@ -296,7 +299,9 @@ export default defineAction({
296
299
  source: codeLayerSource,
297
300
  });
298
301
 
299
- const node = projection.nodes.find((n) => n.id === nodeId);
302
+ const node = projection.nodes.find((n) =>
303
+ componentNodeIdMatches(n, nodeId),
304
+ );
300
305
  if (!node) {
301
306
  throw new Error(
302
307
  `Node "${nodeId}" not found. Run get-code-layer-projection to list current ids.`,
@@ -398,7 +403,9 @@ export default defineAction({
398
403
  }
399
404
 
400
405
  // ── Persist ──────────────────────────────────────────────────────────────
401
- if (changed) {
406
+ const shouldPersist = changed || patchedContent !== (file.content ?? "");
407
+
408
+ if (shouldPersist) {
402
409
  await persistEdit({
403
410
  id: file.id,
404
411
  designId: file.designId,
@@ -419,13 +426,14 @@ export default defineAction({
419
426
  componentName,
420
427
  sourceType,
421
428
  editKind: edit.kind,
422
- persisted: changed,
429
+ persisted: shouldPersist,
423
430
  ctaRequired: false,
424
431
  fileId: file.id,
425
432
  filename: file.filename,
433
+ content: patchedContent,
426
434
  bytesBefore: html.length,
427
435
  bytesAfter: patchedContent.length,
428
- note: changed
436
+ note: shouldPersist
429
437
  ? "Edit applied and persisted via the deterministic HTML-patch path."
430
438
  : "No change applied — the edit produced the same result as the current content.",
431
439
  };
@@ -152,10 +152,23 @@ export default defineAction({
152
152
  if (route !== undefined) patch.route = route;
153
153
  if (fixtureData !== undefined) {
154
154
  // Sanitise replayed markup (stored-XSS guard) before persisting.
155
- patch.fixtureData =
156
- fixtureData !== null
157
- ? JSON.stringify(sanitizeStatePayload(fixtureData))
158
- : null;
155
+ if (fixtureData !== null) {
156
+ const fixtureDataJson = JSON.stringify(
157
+ sanitizeStatePayload(fixtureData),
158
+ );
159
+ if (
160
+ Buffer.byteLength(fixtureDataJson, "utf8") > CAPTURE_DATA_MAX_BYTES
161
+ ) {
162
+ throw new Error(
163
+ `fixtureData exceeds the ${Math.round(
164
+ CAPTURE_DATA_MAX_BYTES / 1024,
165
+ )}KB limit. Trim the payload before updating this fixture.`,
166
+ );
167
+ }
168
+ patch.fixtureData = fixtureDataJson;
169
+ } else {
170
+ patch.fixtureData = null;
171
+ }
159
172
  }
160
173
  if (captureData !== undefined) {
161
174
  if (captureData !== null) {
@@ -36,7 +36,11 @@ import { z } from "zod";
36
36
 
37
37
  import { getDb, schema } from "../server/db/index.js";
38
38
  import "../server/db/index.js"; // ensure registerShareableResource runs
39
- import { compile } from "../shared/motion-compiler.js";
39
+ import {
40
+ assertSafeMotionCssProperty,
41
+ assertSafeMotionCssToken,
42
+ compile,
43
+ } from "../shared/motion-compiler.js";
40
44
  import type { MotionTrack } from "../shared/motion-timeline.js";
41
45
 
42
46
  // ─── Zod schemas ─────────────────────────────────────────────────────────────
@@ -85,47 +89,6 @@ const MOTION_STYLE_CLOSE = "</style>";
85
89
  */
86
90
  const STYLE_CLOSE_RE = /<\s*\/\s*style\b[^>]*>/i;
87
91
 
88
- /**
89
- * Reject `<`, `>`, and any `</style` sequence in a CSS token before it is
90
- * compiled into the managed `<style>` block. Keyframe `value` / `ease` come
91
- * straight from the caller and are interpolated raw into the CSS, so an
92
- * unescaped `</style>` (or angle bracket) would break out of the style block
93
- * and inject arbitrary markup. Returns the original string when safe; throws on
94
- * a breakout attempt so the whole atomic write fails loudly rather than
95
- * persisting a poisoned block.
96
- */
97
- function assertSafeCssToken(value: string, field: string): string {
98
- if (/[<>]/.test(value) || /<\s*\/\s*style/i.test(value)) {
99
- throw new Error(
100
- `Invalid ${field}: "<", ">", and "</style" are not allowed in motion CSS values.`,
101
- );
102
- }
103
- return value;
104
- }
105
-
106
- /**
107
- * Validate that a CSS property name is a safe CSS identifier.
108
- *
109
- * Accepts standard and vendor-prefixed property names (e.g. "opacity",
110
- * "transform", "-webkit-transform") and nothing else. Rejects any string
111
- * containing `:`, `;`, `{`, `}`, `<`, `/`, whitespace, or other characters
112
- * that could break out of a CSS declaration or `<style>` block context when
113
- * the property is interpolated as `${property}: ${value};` inside
114
- * `@keyframes`.
115
- *
116
- * Uses the strict CSS ident regex `^-?[a-zA-Z][a-zA-Z0-9-]*$` which covers
117
- * every real animatable CSS property while blocking injection payloads.
118
- */
119
- function assertSafeCssProperty(property: string, field: string): string {
120
- if (!/^-?[a-zA-Z][a-zA-Z0-9-]*$/.test(property)) {
121
- throw new Error(
122
- `Invalid ${field}: "${property}" is not a valid CSS property identifier. ` +
123
- "Only ASCII letters, digits, hyphens, and an optional leading hyphen are allowed.",
124
- );
125
- }
126
- return property;
127
- }
128
-
129
92
  /**
130
93
  * Inject or replace the managed `<style data-agent-native-motion>` block in
131
94
  * the HTML content. Inserts before `</head>` when not already present.
@@ -328,15 +291,15 @@ export default defineAction({
328
291
  // keyframe values, and easing strings before they are compiled into the
329
292
  // managed <style> block.
330
293
  for (const track of typedTracks) {
331
- assertSafeCssProperty(track.property, "track.property");
294
+ assertSafeMotionCssProperty(track.property, "track.property");
332
295
  for (const kf of track.keyframes) {
333
- assertSafeCssToken(kf.value, "keyframe value");
296
+ assertSafeMotionCssToken(kf.value, "keyframe value");
334
297
  if (kf.ease !== undefined) {
335
- assertSafeCssToken(kf.ease, "keyframe ease");
298
+ assertSafeMotionCssToken(kf.ease, "keyframe ease");
336
299
  }
337
300
  }
338
301
  }
339
- assertSafeCssToken(defaultEase, "defaultEase");
302
+ assertSafeMotionCssToken(defaultEase, "defaultEase");
340
303
 
341
304
  const { css, hash } = compile({
342
305
  id: timelineId ?? "",
@@ -16,6 +16,26 @@ export default defineAction({
16
16
 
17
17
  const db = getDb();
18
18
 
19
+ await db
20
+ .delete(schema.designShares)
21
+ .where(eq(schema.designShares.resourceId, id));
22
+
23
+ await db
24
+ .delete(schema.componentIndex)
25
+ .where(eq(schema.componentIndex.designId, id));
26
+
27
+ await db
28
+ .delete(schema.motionTimeline)
29
+ .where(eq(schema.motionTimeline.designId, id));
30
+
31
+ await db
32
+ .delete(schema.designState)
33
+ .where(eq(schema.designState.designId, id));
34
+
35
+ await db
36
+ .delete(schema.designReviewSnapshot)
37
+ .where(eq(schema.designReviewSnapshot.designId, id));
38
+
19
39
  // Delete associated files first
20
40
  await db
21
41
  .delete(schema.designFiles)
@@ -27,6 +27,7 @@ import { buildCodeLayerProjection } from "../shared/code-layer.js";
27
27
  import type { CodeLayerSource } from "../shared/code-layer.js";
28
28
  import {
29
29
  componentNameFor,
30
+ componentNodeIdMatches,
30
31
  extractProps,
31
32
  type ComponentInstance,
32
33
  } from "../shared/component-model.js";
@@ -112,6 +113,14 @@ export default defineAction({
112
113
  const caps = resolveSourceCapabilities(sourceType);
113
114
  const canResolveToFile = hasCapability(caps, "resolveNodeToFile");
114
115
  const hasFullIndex = hasCapability(caps, "indexComponents");
116
+ const canEditProps =
117
+ sourceType === "inline" || hasCapability(caps, "applyEdit");
118
+ const ctaRequired = !hasFullIndex || !canEditProps;
119
+ const ctaMessage = !hasFullIndex
120
+ ? "Full prop controls (TypeScript prop types, cva variants, Storybook stories) require a connected Builder app. Connect Builder to unlock."
121
+ : !canEditProps
122
+ ? "Prop write-back requires the bridge applyEdit capability. Preview controls remain available until source write hardening is enabled."
123
+ : undefined;
115
124
 
116
125
  // ── Fetch design file ────────────────────────────────────────────────────
117
126
  const conditions = [
@@ -153,7 +162,9 @@ export default defineAction({
153
162
  source: codeLayerSource,
154
163
  });
155
164
 
156
- const node = projection.nodes.find((n) => n.id === nodeId);
165
+ const node = projection.nodes.find((n) =>
166
+ componentNodeIdMatches(n, nodeId),
167
+ );
157
168
  if (!node) {
158
169
  throw new Error(
159
170
  `Node "${nodeId}" not found in projection. Run get-code-layer-projection to list current node ids.`,
@@ -180,7 +191,7 @@ export default defineAction({
180
191
  props: observedProps,
181
192
  alpineData,
182
193
  selector: node.selector,
183
- nodeId: node.id,
194
+ nodeId,
184
195
  };
185
196
 
186
197
  // ── Lookup persisted component_index row ─────────────────────────────────
@@ -231,11 +242,9 @@ export default defineAction({
231
242
  capabilities: {
232
243
  canResolveToFile,
233
244
  hasFullIndex,
234
- canEditProps: true, // preview-only on inline; persists on real-app
235
- ctaRequired: !hasFullIndex,
236
- ctaMessage: !hasFullIndex
237
- ? "Full prop controls (TypeScript prop types, cva variants, Storybook stories) require a connected Builder app. Connect Builder to unlock."
238
- : undefined,
245
+ canEditProps,
246
+ ctaRequired,
247
+ ctaMessage,
239
248
  },
240
249
  };
241
250
  },
@@ -7,7 +7,10 @@ import { z } from "zod";
7
7
  import { getDb, schema } from "../server/db/index.js";
8
8
  import "../server/db/index.js"; // ensure registerShareableResource runs
9
9
  import type { DesignSystemData } from "../shared/api.js";
10
- import { resolveTweaksToCssVars } from "../shared/resolve-tweaks.js";
10
+ import {
11
+ isDirectCssVarSelectionKey,
12
+ resolveTweaksToCssVars,
13
+ } from "../shared/resolve-tweaks.js";
11
14
 
12
15
  // ---------------------------------------------------------------------------
13
16
  // Token type classification
@@ -224,9 +227,10 @@ export default defineAction({
224
227
  tweaks as TweakDef[],
225
228
  tweakSelections,
226
229
  );
227
- const tweakCssVars = new Set(
228
- tweaks.map((t) => t.cssVar).filter(Boolean) as string[],
229
- );
230
+ const tweakCssVars = new Set([
231
+ ...(tweaks.map((t) => t.cssVar).filter(Boolean) as string[]),
232
+ ...Object.keys(tweakSelections).filter(isDirectCssVarSelectionKey),
233
+ ]);
230
234
  for (const [cssVar, value] of Object.entries(resolvedOverrides)) {
231
235
  rawTokens.set(cssVar, { value, source: "Tweaks" });
232
236
  }
@@ -6,6 +6,18 @@ import { z } from "zod";
6
6
  import { getDb, schema } from "../server/db/index.js";
7
7
  import "../server/db/index.js"; // ensure registerShareableResource runs
8
8
 
9
+ function parseJsonRecord(raw: string | null): Record<string, unknown> | null {
10
+ if (!raw) return null;
11
+ try {
12
+ const parsed = JSON.parse(raw) as unknown;
13
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
14
+ ? (parsed as Record<string, unknown>)
15
+ : null;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
9
21
  export default defineAction({
10
22
  description:
11
23
  "List all design states, fixtures, and captures for a design. " +
@@ -52,6 +64,8 @@ export default defineAction({
52
64
  kind: schema.designState.kind,
53
65
  breakpoint: schema.designState.breakpoint,
54
66
  route: schema.designState.route,
67
+ fixtureData: schema.designState.fixtureData,
68
+ captureData: schema.designState.captureData,
55
69
  previewRef: schema.designState.previewRef,
56
70
  createdAt: schema.designState.createdAt,
57
71
  updatedAt: schema.designState.updatedAt,
@@ -66,7 +80,11 @@ export default defineAction({
66
80
 
67
81
  return {
68
82
  count: rows.length,
69
- states: rows,
83
+ states: rows.map((row) => ({
84
+ ...row,
85
+ fixtureData: parseJsonRecord(row.fixtureData),
86
+ captureData: parseJsonRecord(row.captureData),
87
+ })),
70
88
  };
71
89
  },
72
90
  });
@@ -36,7 +36,10 @@ import "../server/db/index.js"; // ensure registerShareableResource runs
36
36
  import { resolveSourceCapabilities } from "../shared/capability-resolver.js";
37
37
  import { buildCodeLayerProjection } from "../shared/code-layer.js";
38
38
  import type { CodeLayerSource } from "../shared/code-layer.js";
39
- import { componentNameFor } from "../shared/component-model.js";
39
+ import {
40
+ componentNameFor,
41
+ componentNodeIdMatches,
42
+ } from "../shared/component-model.js";
40
43
  import { hasCapability } from "../shared/design-source-capabilities.js";
41
44
  import { normalizeDesignSourceType } from "../shared/source-mode.js";
42
45
 
@@ -148,7 +151,9 @@ export default defineAction({
148
151
  source: codeLayerSource,
149
152
  });
150
153
 
151
- const node = projection.nodes.find((n) => n.id === nodeId);
154
+ const node = projection.nodes.find((n) =>
155
+ componentNodeIdMatches(n, nodeId),
156
+ );
152
157
  if (!node) {
153
158
  throw new Error(
154
159
  `Node "${nodeId}" not found. Run get-code-layer-projection to list current ids.`,
@@ -32,7 +32,11 @@ import { getDb, schema } from "../server/db/index.js";
32
32
  import "../server/db/index.js"; // ensure registerShareableResource runs
33
33
  import { buildCodeLayerProjection } from "../shared/code-layer.js";
34
34
  import type { CodeLayerSource } from "../shared/code-layer.js";
35
- import { componentNameFor, extractProps } from "../shared/component-model.js";
35
+ import {
36
+ componentNameFor,
37
+ componentNodeIdMatches,
38
+ extractProps,
39
+ } from "../shared/component-model.js";
36
40
  import { normalizeDesignSourceType } from "../shared/source-mode.js";
37
41
 
38
42
  // ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -62,7 +66,15 @@ async function liveContent(
62
66
  export interface ComponentPropPreviewMessage {
63
67
  /** Matches the existing parent→iframe postMessage type vocabulary. */
64
68
  type: "style-change" | "replace-document-content" | "select-element";
65
- payload: Record<string, unknown>;
69
+ selector?: string;
70
+ nodeId?: string;
71
+ attributeOverrides?: Record<string, string>;
72
+ classEdit?: {
73
+ kind: "class";
74
+ operation: "replace";
75
+ from: string;
76
+ to: string;
77
+ };
66
78
  }
67
79
 
68
80
  // ─── Action ───────────────────────────────────────────────────────────────────
@@ -156,7 +168,9 @@ export default defineAction({
156
168
  source: codeLayerSource,
157
169
  });
158
170
 
159
- const node = projection.nodes.find((n) => n.id === nodeId);
171
+ const node = projection.nodes.find((n) =>
172
+ componentNodeIdMatches(n, nodeId),
173
+ );
160
174
  if (!node) {
161
175
  throw new Error(
162
176
  `Node "${nodeId}" not found. Run get-code-layer-projection to list current ids.`,
@@ -182,35 +196,29 @@ export default defineAction({
182
196
  // attribute without a full HTML replace.
183
197
  messages.push({
184
198
  type: "style-change",
185
- payload: {
186
- selector: node.selector,
187
- nodeId,
188
- attributeOverrides: { "x-data": edit.value },
189
- },
199
+ selector: node.selector,
200
+ nodeId,
201
+ attributeOverrides: { "x-data": edit.value },
190
202
  });
191
203
  } else if (edit.kind === "attribute") {
192
204
  messages.push({
193
205
  type: "style-change",
194
- payload: {
195
- selector: node.selector,
196
- nodeId,
197
- attributeOverrides: { [edit.attribute]: edit.value },
198
- },
206
+ selector: node.selector,
207
+ nodeId,
208
+ attributeOverrides: { [edit.attribute]: edit.value },
199
209
  });
200
210
  } else if (edit.kind === "classReplace") {
201
211
  // Communicate as a responsive-class intent so the bridge applies it via
202
212
  // the existing deterministic class-patch path.
203
213
  messages.push({
204
214
  type: "style-change",
205
- payload: {
206
- selector: node.selector,
207
- nodeId,
208
- classEdit: {
209
- kind: "class",
210
- operation: "replace",
211
- from: edit.from,
212
- to: edit.to,
213
- },
215
+ selector: node.selector,
216
+ nodeId,
217
+ classEdit: {
218
+ kind: "class",
219
+ operation: "replace",
220
+ from: edit.from,
221
+ to: edit.to,
214
222
  },
215
223
  });
216
224
  }
@@ -218,10 +226,8 @@ export default defineAction({
218
226
  // Also emit a select-element message so the canvas highlights the node.
219
227
  messages.push({
220
228
  type: "select-element",
221
- payload: {
222
- selector: node.selector,
223
- nodeId,
224
- },
229
+ selector: node.selector,
230
+ nodeId,
225
231
  });
226
232
 
227
233
  const sourceType =
@@ -2,8 +2,8 @@
2
2
  * preview-shader-fill — preview-only action.
3
3
  *
4
4
  * Returns the CSS mesh-gradient approximation for a shader fill so the caller
5
- * can apply it to the selected node via the bridge (`tweak-values` /
6
- * `style-change` messages) **without persisting anything**.
5
+ * can apply it to the selected node via the `shader-fill-preview` bridge
6
+ * message **without persisting anything**.
7
7
  *
8
8
  * Deliberately lightweight:
9
9
  * - No DB access.
@@ -84,11 +84,11 @@ export default defineAction({
84
84
  Preview a CSS mesh-gradient shader fill on the selected design node without persisting anything.
85
85
 
86
86
  Returns:
87
- - previewCss — a CSS \`background\` value for the live preview (inject via bridge style-change or tweak-values).
87
+ - previewCss — a CSS \`background\` value for the live preview (inject via the shader-fill-preview bridge message).
88
88
  - fallbackCss — a simpler static CSS \`background\` for export / PDF / SSR contexts.
89
89
  - fallbackBlock — a complete CSS rule block (selector + fallback background) ready to embed.
90
90
  - descriptor — the resolved and validated ShaderDescriptor.
91
- - bridgeMessage — a ready-to-use JSON bridge payload for the iframe style-change message.
91
+ - bridgeMessage — a ready-to-use JSON bridge payload for the iframe shader-fill-preview message.
92
92
 
93
93
  Call this before apply-shader-fill. The preview is purely CSS — no WebGL, no canvas, no writes.
94
94
 
@@ -139,14 +139,12 @@ support; it has no effect on the static CSS preview output today.
139
139
  const fallbackBlock = buildShaderFillFallbackBlock(selector, descriptor);
140
140
 
141
141
  // Build a ready-to-post bridge payload for the iframe.
142
- // The `style-change` message type is already handled by DesignCanvas.tsx.
142
+ // The `shader-fill-preview` message type is handled by DesignCanvas.tsx.
143
143
  const bridgeMessage = {
144
- type: "style-change",
145
- nodeId: target?.nodeId ?? null,
144
+ type: "shader-fill-preview",
146
145
  selector: target?.selector ?? null,
147
- styles: {
148
- background: previewCss,
149
- },
146
+ nodeId: target?.nodeId ?? null,
147
+ css: previewCss,
150
148
  // Metadata so the client can attach a "preview only" badge.
151
149
  _preview: true,
152
150
  _source: "shader-fill",
@@ -167,7 +165,7 @@ support; it has no effect on the static CSS preview output today.
167
165
  JSON.stringify(bridgeMessage, null, 2),
168
166
  ``,
169
167
  `Apply steps:`,
170
- `1. Inject the previewCss via a bridge "style-change" message (no write, no persist).`,
168
+ `1. Inject the previewCss via a bridge "shader-fill-preview" message (no write, no persist).`,
171
169
  `2. If the user approves, call apply-shader-fill — but note that action is currently GATED`,
172
170
  ` and will return a clear not-yet-available result until runtime rendering +`,
173
171
  ` source-write + diff proof are all in place.`,
@@ -170,7 +170,7 @@ function checkTapTargets(html: string): A11yFinding[] {
170
170
  nodeId: extractNodeId(tag),
171
171
  selector: extractSelector(tag, "button"),
172
172
  wcag: "2.5.5",
173
- fixAvailable: false,
173
+ fixAvailable: true,
174
174
  });
175
175
  }
176
176
  idx++;
@@ -272,7 +272,7 @@ function checkFocusVisibility(html: string): A11yFinding[] {
272
272
  nodeId: extractNodeId(tag),
273
273
  selector: extractSelector(tag, tag.match(/^<([a-z]+)/i)?.[1] ?? "*"),
274
274
  wcag: "2.4.7",
275
- fixAvailable: false,
275
+ fixAvailable: true,
276
276
  });
277
277
  }
278
278
  inlineIdx++;
@@ -286,10 +286,23 @@ function checkContrastHint(html: string): A11yFinding[] {
286
286
  // Static analysis cannot compute real contrast ratios without a DOM/CSS
287
287
  // cascade resolver. We flag the presence of explicit low-opacity text colors
288
288
  // as a human-review hint — the UI will show these as "info" prompts.
289
- const lowOpacityTextPattern =
290
- /class\s*=\s*"[^"]*\btext-(?:white|black|gray-\d+)\s+(?:opacity-[0-3]\d|text-opacity-[0-3]\d)[^"]*"/gi;
289
+ const tagPattern = /<([a-z][a-z0-9:-]*)\b[^>]*>/gi;
290
+ const textColorPattern = /\btext-(?:white|black|gray-\d+)\b/i;
291
+ const lowOpacityPattern = /\b(?:opacity-[0-3]\d|text-opacity-[0-3]\d)\b/i;
291
292
  let idx = 0;
292
- for (const m of html.matchAll(lowOpacityTextPattern)) {
293
+ for (const m of html.matchAll(tagPattern)) {
294
+ const tag = m[0];
295
+ const classMatch = tag.match(/\bclass\s*=\s*(?:"([^"]*?)"|'([^']*?)')/i);
296
+ const className = classMatch?.[1] ?? classMatch?.[2] ?? "";
297
+ if (
298
+ !textColorPattern.test(className) ||
299
+ !lowOpacityPattern.test(className)
300
+ ) {
301
+ continue;
302
+ }
303
+ const tagName = m[1] ?? "*";
304
+ const nodeId = extractNodeId(tag);
305
+ const selector = extractSelector(tag, tagName);
293
306
  findings.push({
294
307
  id: `contrast:low-opacity-${idx}`,
295
308
  severity: "info" as A11ySeverity,
@@ -298,6 +311,8 @@ function checkContrastHint(html: string): A11yFinding[] {
298
311
  "Text element has a low-opacity modifier — verify contrast ratio meets 4.5:1 minimum.",
299
312
  detail:
300
313
  "Low-opacity text can fail WCAG 1.4.3. Run a live contrast check in the browser.",
314
+ nodeId,
315
+ selector,
301
316
  wcag: "1.4.3",
302
317
  fixAvailable: false,
303
318
  });
@@ -353,7 +368,7 @@ export default defineAction({
353
368
  ),
354
369
  }),
355
370
  readOnly: true,
356
- http: { method: "GET" },
371
+ http: { method: "POST" },
357
372
  run: async ({ designId, fileId, filename }) => {
358
373
  const db = getDb();
359
374