@agent-native/core 0.80.9 → 0.80.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +7 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/design-connect.ts +28 -2
  5. package/corpus/core/src/cli/skills.ts +45 -24
  6. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
  7. package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
  8. package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
  9. package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
  10. package/corpus/templates/design/actions/apply-component-prop-edit.ts +433 -0
  11. package/corpus/templates/design/actions/apply-design-state.ts +200 -0
  12. package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
  13. package/corpus/templates/design/actions/apply-motion-edit.ts +450 -0
  14. package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
  15. package/corpus/templates/design/actions/apply-visual-edit.ts +191 -5
  16. package/corpus/templates/design/actions/capture-design-state.ts +224 -0
  17. package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
  18. package/corpus/templates/design/actions/create-component.ts +447 -0
  19. package/corpus/templates/design/actions/create-design-branch.ts +263 -0
  20. package/corpus/templates/design/actions/create-design-state.ts +162 -0
  21. package/corpus/templates/design/actions/delete-design-state.ts +55 -0
  22. package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
  23. package/corpus/templates/design/actions/get-component-details.ts +242 -0
  24. package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
  25. package/corpus/templates/design/actions/get-design-review.ts +314 -0
  26. package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
  27. package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
  28. package/corpus/templates/design/actions/index-components.ts +258 -0
  29. package/corpus/templates/design/actions/index-design-tokens.ts +273 -0
  30. package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
  31. package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
  32. package/corpus/templates/design/actions/list-design-states.ts +72 -0
  33. package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
  34. package/corpus/templates/design/actions/open-component-source.ts +237 -0
  35. package/corpus/templates/design/actions/preview-component-prop-edit.ts +254 -0
  36. package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
  37. package/corpus/templates/design/actions/preview-shader-fill.ts +189 -0
  38. package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
  39. package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
  40. package/corpus/templates/design/actions/run-design-audit.ts +421 -0
  41. package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
  42. package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
  43. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1414 -71
  44. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +632 -43
  45. package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
  46. package/corpus/templates/design/app/components/design/EditPanel.tsx +1391 -52
  47. package/corpus/templates/design/app/components/design/LayersPanel.tsx +4 -3
  48. package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
  49. package/corpus/templates/design/app/components/design/MotionDock.tsx +1062 -0
  50. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1052 -98
  51. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +759 -0
  52. package/corpus/templates/design/app/components/design/StatesPanel.tsx +579 -0
  53. package/corpus/templates/design/app/components/design/TokensPanel.tsx +573 -0
  54. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
  55. package/corpus/templates/design/app/components/design/index.ts +16 -0
  56. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +15 -6
  57. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +66 -3
  58. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
  59. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
  60. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
  61. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
  62. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +3 -1
  63. package/corpus/templates/design/app/i18n/zh-TW.ts +31 -0
  64. package/corpus/templates/design/app/i18n-data.ts +269 -0
  65. package/corpus/templates/design/app/pages/DesignEditor.tsx +2478 -386
  66. package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
  67. package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
  68. package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
  69. package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
  70. package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
  71. package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
  72. package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
  73. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
  74. package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
  75. package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
  76. package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
  77. package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
  78. package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
  79. package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
  80. package/corpus/templates/design/e2e/helpers.ts +10 -4
  81. package/corpus/templates/design/server/db/schema.ts +123 -0
  82. package/corpus/templates/design/server/plugins/db.ts +90 -0
  83. package/corpus/templates/design/shared/builder-app.ts +297 -0
  84. package/corpus/templates/design/shared/capability-resolver.ts +123 -0
  85. package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
  86. package/corpus/templates/design/shared/code-layer.ts +1016 -71
  87. package/corpus/templates/design/shared/component-model.ts +239 -0
  88. package/corpus/templates/design/shared/design-review.ts +275 -0
  89. package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
  90. package/corpus/templates/design/shared/design-state.ts +112 -0
  91. package/corpus/templates/design/shared/design-surface-index.ts +258 -0
  92. package/corpus/templates/design/shared/motion-compiler.ts +278 -0
  93. package/corpus/templates/design/shared/motion-timeline.ts +193 -0
  94. package/corpus/templates/design/shared/responsive-classes.ts +452 -0
  95. package/corpus/templates/design/shared/shader-fill.ts +323 -0
  96. package/corpus/templates/design/shared/source-mode.ts +245 -0
  97. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
  98. package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
  99. package/dist/cli/design-connect.d.ts.map +1 -1
  100. package/dist/cli/design-connect.js +28 -2
  101. package/dist/cli/design-connect.js.map +1 -1
  102. package/dist/cli/skills.d.ts.map +1 -1
  103. package/dist/cli/skills.js +37 -21
  104. package/dist/cli/skills.js.map +1 -1
  105. package/dist/collab/routes.d.ts +1 -1
  106. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  107. package/dist/notifications/routes.d.ts +2 -2
  108. package/dist/observability/routes.d.ts +7 -7
  109. package/dist/resources/handlers.d.ts +2 -2
  110. package/dist/server/transcribe-voice.d.ts +1 -1
  111. package/package.json +1 -1
@@ -0,0 +1,447 @@
1
+ /**
2
+ * create-component — convert a selected element into a reusable component.
3
+ *
4
+ * Stamps the deterministic component-instance annotations on the selected node
5
+ * so the rest of the Design Studio (component-model detection, the canvas
6
+ * component outline, and the contextual Component inspector section) recognises
7
+ * it as a component instance:
8
+ *
9
+ * - `data-agent-native-component="<Name>"` marks the node as a component root.
10
+ * - `data-agent-native-prop-<name>="<value>"` is stamped for any obvious
11
+ * variant-like attributes already on the node (e.g. `data-variant`,
12
+ * `data-size`, `data-state`, `aria-pressed`) so the Component section shows
13
+ * prop controls immediately.
14
+ *
15
+ * Writes go through the same deterministic raw-HTML attribute-splice path used
16
+ * by `apply-component-prop-edit` (the `replace-document-content` + Yjs/collab
17
+ * seam shared by every HTML write), so the change persists into SQL and the
18
+ * collab document together.
19
+ *
20
+ * **Tier A (Alpine / inline):** always available — the design HTML is the
21
+ * source of truth.
22
+ *
23
+ * **Tier B (real-app, localhost / fusion):** source writes require the
24
+ * `applyEdit` capability (bridge write hardening). Until that lands the action
25
+ * returns `ctaRequired: true` without modifying any source.
26
+ *
27
+ * See DESIGN-STUDIO-PLAN.md §6.1 (component model) and §7 (action surface).
28
+ */
29
+
30
+ import { defineAction } from "@agent-native/core";
31
+ import {
32
+ agentEnterDocument,
33
+ agentLeaveDocument,
34
+ agentUpdateSelection,
35
+ applyText,
36
+ getText,
37
+ hasCollabState,
38
+ seedFromText,
39
+ } from "@agent-native/core/collab";
40
+ import {
41
+ accessFilter,
42
+ assertAccess,
43
+ resolveAccess,
44
+ } from "@agent-native/core/sharing";
45
+ import { and, eq } from "drizzle-orm";
46
+ import { z } from "zod";
47
+
48
+ import { getDb, schema } from "../server/db/index.js";
49
+ import "../server/db/index.js"; // ensure registerShareableResource runs
50
+ import { resolveSourceCapabilities } from "../shared/capability-resolver.js";
51
+ import { buildCodeLayerProjection } from "../shared/code-layer.js";
52
+ import type { CodeLayerNode, CodeLayerSource } from "../shared/code-layer.js";
53
+ import {
54
+ COMPONENT_NAME_ATTR,
55
+ COMPONENT_PROP_PREFIX,
56
+ } from "../shared/component-model.js";
57
+ import { hasCapability } from "../shared/design-source-capabilities.js";
58
+ import { normalizeDesignSourceType } from "../shared/source-mode.js";
59
+
60
+ // ─── Pure helpers ─────────────────────────────────────────────────────────────
61
+
62
+ /**
63
+ * A single attribute to stamp onto the component root.
64
+ */
65
+ export interface ComponentAttributeStamp {
66
+ name: string;
67
+ value: string;
68
+ }
69
+
70
+ /** Attributes that commonly carry variant-like meaning on an element. */
71
+ const VARIANT_LIKE_DATA_ATTRS = [
72
+ "data-variant",
73
+ "data-size",
74
+ "data-state",
75
+ "data-color",
76
+ "data-tone",
77
+ "data-intent",
78
+ ] as const;
79
+
80
+ /** ARIA attributes that map cleanly to a boolean/value prop. */
81
+ const VARIANT_LIKE_ARIA_ATTRS = [
82
+ "aria-pressed",
83
+ "aria-selected",
84
+ "aria-checked",
85
+ "aria-disabled",
86
+ ] as const;
87
+
88
+ /** Normalize a component name into a safe PascalCase-ish identifier. */
89
+ export function normalizeComponentName(raw: string): string {
90
+ const cleaned = raw
91
+ .replace(/[^A-Za-z0-9]+/g, " ")
92
+ .trim()
93
+ .split(/\s+/)
94
+ .filter(Boolean)
95
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
96
+ .join("");
97
+ return cleaned || "Component";
98
+ }
99
+
100
+ /** Convert a kebab-case attribute suffix to a camelCase prop name. */
101
+ function attrSuffixToPropName(suffix: string): string {
102
+ return suffix.replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase());
103
+ }
104
+
105
+ /**
106
+ * Derive the `data-agent-native-prop-*` stamps to apply when promoting a node
107
+ * to a component, based on obvious variant-like attributes already present on
108
+ * the node. Pure — operates on a `CodeLayerNode`'s attribute maps.
109
+ *
110
+ * Returns one stamp per recognised variant-like attribute; never includes the
111
+ * component-name attribute itself or any pre-existing prop attribute.
112
+ */
113
+ export function deriveComponentPropStamps(
114
+ node: Pick<CodeLayerNode, "dataAttributes" | "attributes">,
115
+ ): ComponentAttributeStamp[] {
116
+ const stamps: ComponentAttributeStamp[] = [];
117
+ const seen = new Set<string>();
118
+
119
+ const push = (propName: string, value: string) => {
120
+ const key = propName.toLowerCase();
121
+ if (!propName || seen.has(key)) return;
122
+ seen.add(key);
123
+ stamps.push({
124
+ name: `${COMPONENT_PROP_PREFIX}${propName}`,
125
+ value,
126
+ });
127
+ };
128
+
129
+ // data-variant / data-size / data-state / ... → prop name without "data-".
130
+ for (const attr of VARIANT_LIKE_DATA_ATTRS) {
131
+ const value = node.dataAttributes[attr];
132
+ if (typeof value === "string" && value.trim()) {
133
+ push(attrSuffixToPropName(attr.slice("data-".length)), value.trim());
134
+ }
135
+ }
136
+
137
+ // aria-pressed / aria-selected / ... → prop name without "aria-".
138
+ for (const attr of VARIANT_LIKE_ARIA_ATTRS) {
139
+ const raw = node.attributes[attr];
140
+ const value = raw === true ? "true" : raw;
141
+ if (typeof value === "string" && value.trim()) {
142
+ push(attrSuffixToPropName(attr.slice("aria-".length)), value.trim());
143
+ }
144
+ }
145
+
146
+ return stamps;
147
+ }
148
+
149
+ /** Escape a raw attribute value for safe insertion inside double quotes. */
150
+ function escapeAttrValue(value: string): string {
151
+ return value
152
+ .replace(/&/g, "&amp;")
153
+ .replace(/"/g, "&quot;")
154
+ .replace(/</g, "&lt;")
155
+ .replace(/>/g, "&gt;");
156
+ }
157
+
158
+ /**
159
+ * Splice a `name="value"` attribute into an opening tag string, replacing the
160
+ * attribute if it already exists. Pure string helper mirroring the technique
161
+ * used by `apply-component-prop-edit`.
162
+ */
163
+ export function setAttributeOnOpenTag(
164
+ openTag: string,
165
+ name: string,
166
+ value: string,
167
+ ): string {
168
+ const attrRe = new RegExp(
169
+ `(\\s${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=\\s*)(?:"[^"]*"|'[^']*'|[^\\s>"']+)`,
170
+ "i",
171
+ );
172
+ const escaped = escapeAttrValue(value);
173
+ if (attrRe.test(openTag)) {
174
+ return openTag.replace(attrRe, `$1"${escaped}"`);
175
+ }
176
+ const insertOffset = openTag.endsWith("/>")
177
+ ? openTag.length - 2
178
+ : openTag.length - 1;
179
+ return `${openTag.slice(0, insertOffset)} ${name}="${escaped}"${openTag.slice(insertOffset)}`;
180
+ }
181
+
182
+ /**
183
+ * Stamp the component-name attribute plus prop stamps onto the node's opening
184
+ * tag within `html`, returning the patched HTML. Returns the original HTML
185
+ * unchanged when the node has no resolvable source span.
186
+ */
187
+ export function applyComponentAnnotations(
188
+ html: string,
189
+ node: Pick<CodeLayerNode, "source">,
190
+ componentName: string,
191
+ propStamps: ComponentAttributeStamp[],
192
+ ): { content: string; changed: boolean } {
193
+ const src = node.source;
194
+ if (!src) return { content: html, changed: false };
195
+
196
+ let openTag = html.slice(src.openStart, src.openEnd);
197
+ const before = openTag;
198
+
199
+ openTag = setAttributeOnOpenTag(openTag, COMPONENT_NAME_ATTR, componentName);
200
+ for (const stamp of propStamps) {
201
+ openTag = setAttributeOnOpenTag(openTag, stamp.name, stamp.value);
202
+ }
203
+
204
+ if (openTag === before) return { content: html, changed: false };
205
+ return {
206
+ content: html.slice(0, src.openStart) + openTag + html.slice(src.openEnd),
207
+ changed: true,
208
+ };
209
+ }
210
+
211
+ // ─── DB helpers ───────────────────────────────────────────────────────────────
212
+
213
+ async function liveContent(
214
+ fileId: string,
215
+ storedContent: string,
216
+ ): Promise<string> {
217
+ try {
218
+ if (await hasCollabState(fileId)) {
219
+ const live = await getText(fileId, "content");
220
+ if (typeof live === "string") return live;
221
+ }
222
+ } catch {
223
+ // Collab reads are best-effort; SQL content is the fallback.
224
+ }
225
+ return storedContent;
226
+ }
227
+
228
+ async function persistEdit(file: {
229
+ id: string;
230
+ designId: string;
231
+ content: string;
232
+ }): Promise<void> {
233
+ await assertAccess("design", file.designId, "editor");
234
+ const db = getDb();
235
+ const now = new Date().toISOString();
236
+
237
+ agentEnterDocument(file.id);
238
+ try {
239
+ await db
240
+ .update(schema.designFiles)
241
+ .set({ content: file.content, updatedAt: now })
242
+ .where(eq(schema.designFiles.id, file.id));
243
+
244
+ if (await hasCollabState(file.id)) {
245
+ await applyText(file.id, file.content, "content", "agent");
246
+ } else {
247
+ await seedFromText(file.id, file.content);
248
+ }
249
+
250
+ await db
251
+ .update(schema.designs)
252
+ .set({ updatedAt: now })
253
+ .where(eq(schema.designs.id, file.designId));
254
+ } finally {
255
+ agentLeaveDocument(file.id);
256
+ }
257
+ }
258
+
259
+ // ─── Action ───────────────────────────────────────────────────────────────────
260
+
261
+ export default defineAction({
262
+ description:
263
+ "Promote a selected element into a reusable component by stamping the " +
264
+ "deterministic component-instance annotations on its root node: " +
265
+ 'data-agent-native-component="<Name>" plus data-agent-native-prop-* for ' +
266
+ "obvious variant-like attributes (data-variant/size/state, aria-pressed, " +
267
+ "etc.). For inline/Alpine designs this writes the HTML directly via the " +
268
+ "deterministic patch path. For real-app sources the applyEdit capability " +
269
+ "must be available; otherwise returns ctaRequired=true without modifying " +
270
+ "any file. After it runs the node is a recognised component instance, so " +
271
+ "component-model detection, the canvas outline, and the Component section " +
272
+ "all pick it up.",
273
+ schema: z.object({
274
+ designId: z.string().describe("Design project ID"),
275
+ nodeId: z
276
+ .string()
277
+ .optional()
278
+ .describe(
279
+ "data-agent-native-node-id (code-layer node id) of the element to promote. Provide nodeId or selector.",
280
+ ),
281
+ selector: z
282
+ .string()
283
+ .optional()
284
+ .describe(
285
+ "CSS selector for the element to promote. Used when nodeId is not available.",
286
+ ),
287
+ name: z
288
+ .string()
289
+ .min(1)
290
+ .describe(
291
+ 'Component name, e.g. "PrimaryButton". Normalized to PascalCase.',
292
+ ),
293
+ fileId: z
294
+ .string()
295
+ .optional()
296
+ .describe("Design file id; defaults to index.html"),
297
+ }),
298
+ run: async ({ designId, nodeId, selector, name, fileId }) => {
299
+ if (!nodeId && !selector) {
300
+ throw new Error(
301
+ "Provide either nodeId or selector for the element to promote.",
302
+ );
303
+ }
304
+
305
+ const db = getDb();
306
+
307
+ // ── Access check ────────────────────────────────────────────────────────
308
+ const access = await resolveAccess("design", designId);
309
+ if (!access) throw new Error("Design not found");
310
+
311
+ // ── Source type + capability gate ────────────────────────────────────────
312
+ let rawSourceType: unknown = "inline";
313
+ const rawData = (access.resource as { data?: unknown }).data;
314
+ if (typeof rawData === "string") {
315
+ try {
316
+ const parsed: unknown = JSON.parse(rawData);
317
+ if (
318
+ parsed !== null &&
319
+ typeof parsed === "object" &&
320
+ "sourceType" in (parsed as object)
321
+ ) {
322
+ rawSourceType = (parsed as { sourceType: unknown }).sourceType;
323
+ }
324
+ } catch {
325
+ // Default to inline.
326
+ }
327
+ }
328
+
329
+ const sourceType = normalizeDesignSourceType(rawSourceType) ?? "inline";
330
+ const caps = resolveSourceCapabilities(sourceType);
331
+
332
+ // Real-app sources gate on `applyEdit` (bridge write hardening).
333
+ if (sourceType !== "inline" && !hasCapability(caps, "applyEdit")) {
334
+ return {
335
+ designId,
336
+ sourceType,
337
+ persisted: false,
338
+ ctaRequired: true,
339
+ ctaMessage:
340
+ "Creating a component from a real-app source requires the bridge " +
341
+ "applyEdit capability, which lands with bridge write hardening.",
342
+ };
343
+ }
344
+
345
+ await assertAccess("design", designId, "editor");
346
+
347
+ // ── Fetch file ───────────────────────────────────────────────────────────
348
+ const conditions = [
349
+ accessFilter(schema.designs, schema.designShares),
350
+ eq(schema.designFiles.designId, designId),
351
+ fileId
352
+ ? eq(schema.designFiles.id, fileId)
353
+ : eq(schema.designFiles.filename, "index.html"),
354
+ ];
355
+
356
+ const [file] = await db
357
+ .select({
358
+ id: schema.designFiles.id,
359
+ designId: schema.designFiles.designId,
360
+ filename: schema.designFiles.filename,
361
+ content: schema.designFiles.content,
362
+ })
363
+ .from(schema.designFiles)
364
+ .innerJoin(
365
+ schema.designs,
366
+ eq(schema.designFiles.designId, schema.designs.id),
367
+ )
368
+ .where(and(...conditions))
369
+ .limit(1);
370
+
371
+ if (!file) throw new Error("Design HTML file not found.");
372
+
373
+ const html = await liveContent(file.id, file.content ?? "");
374
+
375
+ // ── Resolve node ─────────────────────────────────────────────────────────
376
+ const codeLayerSource: CodeLayerSource = {
377
+ kind: "design-file",
378
+ designId: file.designId,
379
+ fileId: file.id,
380
+ filename: file.filename,
381
+ };
382
+
383
+ const projection = buildCodeLayerProjection(html, {
384
+ source: codeLayerSource,
385
+ });
386
+
387
+ const node = nodeId
388
+ ? projection.nodes.find((n) => n.id === nodeId)
389
+ : projection.nodes.find(
390
+ (n) =>
391
+ n.selector === selector || n.selectors.includes(selector ?? ""),
392
+ );
393
+
394
+ if (!node) {
395
+ throw new Error(
396
+ `Element not found. Run get-code-layer-projection to list current node ids and selectors.`,
397
+ );
398
+ }
399
+
400
+ // ── Build annotations ─────────────────────────────────────────────────────
401
+ const componentName = normalizeComponentName(name);
402
+ const propStamps = deriveComponentPropStamps(node);
403
+ const { content: patchedContent, changed } = applyComponentAnnotations(
404
+ html,
405
+ node,
406
+ componentName,
407
+ propStamps,
408
+ );
409
+
410
+ // ── Persist ──────────────────────────────────────────────────────────────
411
+ if (changed) {
412
+ await persistEdit({
413
+ id: file.id,
414
+ designId: file.designId,
415
+ content: patchedContent,
416
+ });
417
+
418
+ agentUpdateSelection(file.id, {
419
+ selection: node.selector,
420
+ nodeId: node.id,
421
+ editingFile: file.filename,
422
+ designId: file.designId,
423
+ });
424
+ }
425
+
426
+ return {
427
+ designId,
428
+ nodeId: node.id,
429
+ selector: node.selector,
430
+ componentName,
431
+ sourceType,
432
+ props: propStamps.map((stamp) => ({
433
+ name: stamp.name.slice(COMPONENT_PROP_PREFIX.length),
434
+ value: stamp.value,
435
+ })),
436
+ persisted: changed,
437
+ ctaRequired: false,
438
+ fileId: file.id,
439
+ filename: file.filename,
440
+ bytesBefore: html.length,
441
+ bytesAfter: patchedContent.length,
442
+ note: changed
443
+ ? "Element promoted to a component instance and persisted via the deterministic HTML-patch path."
444
+ : "No change applied — the element could not be annotated (missing source span).",
445
+ };
446
+ },
447
+ });