@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +7 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/design-connect.ts +28 -2
- package/corpus/core/src/cli/skills.ts +45 -24
- package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
- package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
- package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
- package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +433 -0
- package/corpus/templates/design/actions/apply-design-state.ts +200 -0
- package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
- package/corpus/templates/design/actions/apply-motion-edit.ts +450 -0
- package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
- package/corpus/templates/design/actions/apply-visual-edit.ts +191 -5
- package/corpus/templates/design/actions/capture-design-state.ts +224 -0
- package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
- package/corpus/templates/design/actions/create-component.ts +447 -0
- package/corpus/templates/design/actions/create-design-branch.ts +263 -0
- package/corpus/templates/design/actions/create-design-state.ts +162 -0
- package/corpus/templates/design/actions/delete-design-state.ts +55 -0
- package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
- package/corpus/templates/design/actions/get-component-details.ts +242 -0
- package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
- package/corpus/templates/design/actions/get-design-review.ts +314 -0
- package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
- package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
- package/corpus/templates/design/actions/index-components.ts +258 -0
- package/corpus/templates/design/actions/index-design-tokens.ts +273 -0
- package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
- package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
- package/corpus/templates/design/actions/list-design-states.ts +72 -0
- package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
- package/corpus/templates/design/actions/open-component-source.ts +237 -0
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +254 -0
- package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
- package/corpus/templates/design/actions/preview-shader-fill.ts +189 -0
- package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
- package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
- package/corpus/templates/design/actions/run-design-audit.ts +421 -0
- package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
- package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1414 -71
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +632 -43
- package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1391 -52
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +4 -3
- package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +1062 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1052 -98
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +759 -0
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +579 -0
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +573 -0
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
- package/corpus/templates/design/app/components/design/index.ts +16 -0
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +15 -6
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +66 -3
- package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
- package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +3 -1
- package/corpus/templates/design/app/i18n/zh-TW.ts +31 -0
- package/corpus/templates/design/app/i18n-data.ts +269 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +2478 -386
- package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
- package/corpus/templates/design/e2e/helpers.ts +10 -4
- package/corpus/templates/design/server/db/schema.ts +123 -0
- package/corpus/templates/design/server/plugins/db.ts +90 -0
- package/corpus/templates/design/shared/builder-app.ts +297 -0
- package/corpus/templates/design/shared/capability-resolver.ts +123 -0
- package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
- package/corpus/templates/design/shared/code-layer.ts +1016 -71
- package/corpus/templates/design/shared/component-model.ts +239 -0
- package/corpus/templates/design/shared/design-review.ts +275 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
- package/corpus/templates/design/shared/design-state.ts +112 -0
- package/corpus/templates/design/shared/design-surface-index.ts +258 -0
- package/corpus/templates/design/shared/motion-compiler.ts +278 -0
- package/corpus/templates/design/shared/motion-timeline.ts +193 -0
- package/corpus/templates/design/shared/responsive-classes.ts +452 -0
- package/corpus/templates/design/shared/shader-fill.ts +323 -0
- package/corpus/templates/design/shared/source-mode.ts +245 -0
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
- package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +28 -2
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +37 -21
- package/dist/cli/skills.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +7 -7
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/transcribe-voice.d.ts +1 -1
- 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, "&")
|
|
153
|
+
.replace(/"/g, """)
|
|
154
|
+
.replace(/</g, "<")
|
|
155
|
+
.replace(/>/g, ">");
|
|
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
|
+
});
|