@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +108 -15
- package/corpus/core/src/client/AssistantChat.tsx +123 -26
- package/corpus/core/src/client/sse-event-processor.ts +9 -0
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
- package/corpus/templates/assets/AGENTS.md +4 -0
- package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
- package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
- package/corpus/templates/assets/actions/generate-image.ts +118 -67
- package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
- package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
- package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
- package/corpus/templates/assets/app/i18n-data.ts +61 -0
- package/corpus/templates/assets/app/routes/_index.tsx +4 -0
- package/corpus/templates/assets/app/routes/library.tsx +145 -38
- package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
- package/corpus/templates/assets/server/lib/generation.ts +135 -11
- package/corpus/templates/assets/shared/api.ts +4 -0
- package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +13 -5
- package/corpus/templates/design/actions/apply-design-state.ts +17 -4
- package/corpus/templates/design/actions/apply-motion-edit.ts +9 -46
- package/corpus/templates/design/actions/delete-design.ts +20 -0
- package/corpus/templates/design/actions/get-component-details.ts +16 -7
- package/corpus/templates/design/actions/index-design-tokens.ts +8 -4
- package/corpus/templates/design/actions/list-design-states.ts +19 -1
- package/corpus/templates/design/actions/open-component-source.ts +7 -2
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
- package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
- package/corpus/templates/design/actions/run-design-audit.ts +21 -6
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +139 -14
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +20 -3
- package/corpus/templates/design/app/components/design/EditPanel.tsx +428 -31
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +110 -30
- package/corpus/templates/design/app/components/design/MotionDock.tsx +9 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1 -1
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +12 -5
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +4 -2
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +13 -8
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/i18n-data.ts +11 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +684 -179
- package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +94 -2
- package/corpus/templates/design/shared/component-model.ts +35 -0
- package/corpus/templates/design/shared/motion-compiler.ts +79 -8
- package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +94 -10
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +14 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +115 -22
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +10 -0
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
rgbaToHex,
|
|
12
12
|
withColorOpacity,
|
|
13
13
|
} from "@shared/color-utils";
|
|
14
|
+
import { propNameToDataAttribute } from "@shared/component-model";
|
|
14
15
|
import {
|
|
15
16
|
IconAlignCenter,
|
|
16
17
|
IconAlignJustified,
|
|
@@ -69,6 +70,7 @@ import {
|
|
|
69
70
|
type ReactNode,
|
|
70
71
|
} from "react";
|
|
71
72
|
import { useParams } from "react-router";
|
|
73
|
+
import { toast } from "sonner";
|
|
72
74
|
|
|
73
75
|
import { Button } from "@/components/ui/button";
|
|
74
76
|
import {
|
|
@@ -186,6 +188,11 @@ interface EditPanelProps {
|
|
|
186
188
|
// -------------------------------------------------------------------------
|
|
187
189
|
/** The active design's id — required to mount Tokens / States / Review. */
|
|
188
190
|
designId?: string;
|
|
191
|
+
/**
|
|
192
|
+
* Called after a component prop edit returns the patched source so the parent
|
|
193
|
+
* editor can sync local/Yjs content instead of waiting for query invalidation.
|
|
194
|
+
*/
|
|
195
|
+
onComponentPropApplied?: (fileId: string, content: string) => void;
|
|
189
196
|
/**
|
|
190
197
|
* Called after a token edit is applied so the parent can push the resolved
|
|
191
198
|
* CSS-var map into the iframe via the tweak-values postMessage.
|
|
@@ -2149,6 +2156,292 @@ export function serializeAlpineDataObject(obj: Record<string, string>): string {
|
|
|
2149
2156
|
return parts.length ? `{ ${parts.join(", ")} }` : "{}";
|
|
2150
2157
|
}
|
|
2151
2158
|
|
|
2159
|
+
/**
|
|
2160
|
+
* Format a single editable prop value as an `x-data` literal: bare for
|
|
2161
|
+
* boolean / number values, single-quoted (with escaping) for strings.
|
|
2162
|
+
*
|
|
2163
|
+
* Pure — exported for tests.
|
|
2164
|
+
*/
|
|
2165
|
+
export function alpineDataValueLiteral(value: string): string {
|
|
2166
|
+
const isBoolean = value === "true" || value === "false";
|
|
2167
|
+
const isNumber = /^-?\d+(\.\d+)?$/.test(value);
|
|
2168
|
+
return isBoolean || isNumber ? value : `'${value.replace(/'/g, "\\'")}'`;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
/**
|
|
2172
|
+
* Surgically replace a single top-level key's value inside an Alpine `x-data`
|
|
2173
|
+
* object literal, preserving everything else byte-for-byte — methods
|
|
2174
|
+
* (`toggle() { … }`), nested objects, escaped strings, quoted keys, comments,
|
|
2175
|
+
* and whitespace are all left untouched.
|
|
2176
|
+
*
|
|
2177
|
+
* Unlike a `parseAlpineDataObject` → mutate → `serializeAlpineDataObject`
|
|
2178
|
+
* round-trip (which only understands a flat object of simple literals and so
|
|
2179
|
+
* *drops* anything it can't model), this walks the original string, finds the
|
|
2180
|
+
* `key:` token at the top level (depth 0, not inside a string/comment), and
|
|
2181
|
+
* rewrites only the value literal that immediately follows it.
|
|
2182
|
+
*
|
|
2183
|
+
* Returns `null` when the key cannot be located surgically (e.g. the value is
|
|
2184
|
+
* an expression/function/object rather than a simple string/boolean/number, or
|
|
2185
|
+
* the literal isn't a `{ … }` object) so the caller can fail safe instead of
|
|
2186
|
+
* persisting a lossy rewrite.
|
|
2187
|
+
*
|
|
2188
|
+
* Pure — exported for tests.
|
|
2189
|
+
*/
|
|
2190
|
+
export function replaceAlpineDataKeyValue(
|
|
2191
|
+
xData: string | null | undefined,
|
|
2192
|
+
key: string,
|
|
2193
|
+
nextValue: string,
|
|
2194
|
+
): string | null {
|
|
2195
|
+
if (!xData) return null;
|
|
2196
|
+
const trimmed = xData.trim();
|
|
2197
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
|
|
2198
|
+
|
|
2199
|
+
const s = xData;
|
|
2200
|
+
const n = s.length;
|
|
2201
|
+
// Walk the whole string tracking nesting depth and skipping over strings,
|
|
2202
|
+
// template literals, regex-ish slashes are not handled (Alpine x-data does
|
|
2203
|
+
// not use them at the object-key level), and line / block comments. Only at
|
|
2204
|
+
// object depth 1 (directly inside the outermost `{ … }`) do we look for the
|
|
2205
|
+
// target `key :` token.
|
|
2206
|
+
let depth = 0;
|
|
2207
|
+
let i = 0;
|
|
2208
|
+
|
|
2209
|
+
/** Advance `i` past a quoted string starting at `i` (handles escapes). */
|
|
2210
|
+
const skipString = (quote: string): void => {
|
|
2211
|
+
i += 1; // opening quote
|
|
2212
|
+
while (i < n) {
|
|
2213
|
+
const c = s[i];
|
|
2214
|
+
if (c === "\\") {
|
|
2215
|
+
i += 2;
|
|
2216
|
+
continue;
|
|
2217
|
+
}
|
|
2218
|
+
if (c === quote) {
|
|
2219
|
+
i += 1;
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
i += 1;
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
|
|
2226
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2227
|
+
// Bare identifier key at a token boundary: `key` then optional ws then `:`.
|
|
2228
|
+
const bareRe = new RegExp(`^(${escapedKey})(\\s*:\\s*)`);
|
|
2229
|
+
// Quoted key: `'key'` or `"key"` then optional ws then `:`.
|
|
2230
|
+
const quotedRe = new RegExp(`^(['"]${escapedKey}['"])(\\s*:\\s*)`);
|
|
2231
|
+
|
|
2232
|
+
while (i < n) {
|
|
2233
|
+
const c = s[i];
|
|
2234
|
+
|
|
2235
|
+
// At the top level, a `{` / `,` (or the string start) opens a fresh value
|
|
2236
|
+
// slot. Try to match the target key here *before* treating a quote as an
|
|
2237
|
+
// opaque string — this is how quoted keys (`'size': …`) are recognised.
|
|
2238
|
+
if (depth === 1) {
|
|
2239
|
+
const prev = lastNonSpaceBefore(s, i);
|
|
2240
|
+
if (prev === "{" || prev === ",") {
|
|
2241
|
+
const rest = s.slice(i);
|
|
2242
|
+
const m = bareRe.exec(rest) ?? quotedRe.exec(rest);
|
|
2243
|
+
if (m) {
|
|
2244
|
+
const valueStart = i + m[1].length + m[2].length;
|
|
2245
|
+
const valueEnd = simpleValueEnd(s, valueStart);
|
|
2246
|
+
if (valueEnd === null) return null; // value is not a simple literal
|
|
2247
|
+
return (
|
|
2248
|
+
s.slice(0, valueStart) +
|
|
2249
|
+
alpineDataValueLiteral(nextValue) +
|
|
2250
|
+
s.slice(valueEnd)
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// Skip strings / template literals wholesale.
|
|
2257
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
2258
|
+
skipString(c);
|
|
2259
|
+
continue;
|
|
2260
|
+
}
|
|
2261
|
+
// Skip comments.
|
|
2262
|
+
if (c === "/" && s[i + 1] === "/") {
|
|
2263
|
+
i += 2;
|
|
2264
|
+
while (i < n && s[i] !== "\n") i += 1;
|
|
2265
|
+
continue;
|
|
2266
|
+
}
|
|
2267
|
+
if (c === "/" && s[i + 1] === "*") {
|
|
2268
|
+
i += 2;
|
|
2269
|
+
while (i < n && !(s[i] === "*" && s[i + 1] === "/")) i += 1;
|
|
2270
|
+
i += 2;
|
|
2271
|
+
continue;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
if (c === "{" || c === "[" || c === "(") {
|
|
2275
|
+
depth += 1;
|
|
2276
|
+
i += 1;
|
|
2277
|
+
continue;
|
|
2278
|
+
}
|
|
2279
|
+
if (c === "}" || c === "]" || c === ")") {
|
|
2280
|
+
depth -= 1;
|
|
2281
|
+
i += 1;
|
|
2282
|
+
continue;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
i += 1;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
return null;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
/** Last non-whitespace char strictly before index `i` (or `""`). */
|
|
2292
|
+
function lastNonSpaceBefore(s: string, i: number): string {
|
|
2293
|
+
let j = i - 1;
|
|
2294
|
+
while (j >= 0 && /\s/.test(s[j]!)) j -= 1;
|
|
2295
|
+
return j >= 0 ? s[j]! : "";
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
/**
|
|
2299
|
+
* Given the start index of a value in an `x-data` literal, return the index
|
|
2300
|
+
* just past a *simple* literal value (single/double-quoted string with
|
|
2301
|
+
* escapes, boolean, or number). Returns `null` when the value is anything else
|
|
2302
|
+
* (an expression, function, object, array, template literal, etc.) so the
|
|
2303
|
+
* caller can fail safe rather than mangle it.
|
|
2304
|
+
*/
|
|
2305
|
+
function simpleValueEnd(s: string, start: number): number | null {
|
|
2306
|
+
const c = s[start];
|
|
2307
|
+
if (c === "'" || c === '"') {
|
|
2308
|
+
let i = start + 1;
|
|
2309
|
+
while (i < s.length) {
|
|
2310
|
+
if (s[i] === "\\") {
|
|
2311
|
+
i += 2;
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
if (s[i] === c) return i + 1;
|
|
2315
|
+
i += 1;
|
|
2316
|
+
}
|
|
2317
|
+
return null; // unterminated string
|
|
2318
|
+
}
|
|
2319
|
+
// Boolean / number: read the bare token, then confirm it is exactly one.
|
|
2320
|
+
const m = /^[A-Za-z0-9_.+-]+/.exec(s.slice(start));
|
|
2321
|
+
if (!m) return null;
|
|
2322
|
+
const token = m[0];
|
|
2323
|
+
const isBoolean = token === "true" || token === "false";
|
|
2324
|
+
const isNumber = /^-?\d+(\.\d+)?$/.test(token);
|
|
2325
|
+
if (!isBoolean && !isNumber) return null;
|
|
2326
|
+
return start + token.length;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
/**
|
|
2330
|
+
* True when an `x-data` literal can be rebuilt from its flat parsed map with
|
|
2331
|
+
* no loss — i.e. there is nothing richer than the simple `key: literal` pairs
|
|
2332
|
+
* that `serializeAlpineDataObject` already round-trips. Used as the gate for
|
|
2333
|
+
* falling back to a full rebuild when a surgical single-key replace is not
|
|
2334
|
+
* possible (e.g. when adding a brand-new key).
|
|
2335
|
+
*
|
|
2336
|
+
* Returns `true` for an empty / absent literal (nothing to lose) and for a
|
|
2337
|
+
* flat object whose `parse → serialize` round-trip is semantically stable.
|
|
2338
|
+
* Returns `false` when the original holds methods, nested objects, comments,
|
|
2339
|
+
* or expressions that a rebuild would silently drop.
|
|
2340
|
+
*
|
|
2341
|
+
* Pure — exported for tests.
|
|
2342
|
+
*/
|
|
2343
|
+
export function canRebuildAlpineDataLosslessly(
|
|
2344
|
+
xData: string | null | undefined,
|
|
2345
|
+
): boolean {
|
|
2346
|
+
const trimmed = (xData ?? "").trim();
|
|
2347
|
+
// No object literal at all → there is nothing richer to preserve.
|
|
2348
|
+
if (!trimmed || trimmed === "{}" || trimmed === "{ }") return true;
|
|
2349
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return false;
|
|
2350
|
+
|
|
2351
|
+
const parsed = parseAlpineDataObject(trimmed);
|
|
2352
|
+
if (!parsed) return false;
|
|
2353
|
+
|
|
2354
|
+
// Re-serialize and re-parse; if the round-trip is stable AND the parsed map
|
|
2355
|
+
// accounts for every top-level key actually present in the original, a
|
|
2356
|
+
// rebuild loses nothing.
|
|
2357
|
+
const reserialized = serializeAlpineDataObject(parsed);
|
|
2358
|
+
const reparsed = parseAlpineDataObject(reserialized);
|
|
2359
|
+
if (!reparsed) return false;
|
|
2360
|
+
const keysA = Object.keys(parsed).sort().join(",");
|
|
2361
|
+
const keysB = Object.keys(reparsed).sort().join(",");
|
|
2362
|
+
if (keysA !== keysB) return false;
|
|
2363
|
+
|
|
2364
|
+
// Guard against dropped content the flat parser ignores (e.g. a trailing
|
|
2365
|
+
// method): the number of top-level `key:` tokens in the original must match
|
|
2366
|
+
// the number of parsed keys. Count top-level `:` separators conservatively.
|
|
2367
|
+
return countTopLevelKeys(trimmed) === Object.keys(parsed).length;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
/**
|
|
2371
|
+
* Count top-level `key:` entries in an `x-data` object literal, skipping
|
|
2372
|
+
* strings, comments, and nested braces/brackets/parens. A method like
|
|
2373
|
+
* `toggle() { … }` is counted as a key too (its `:`-less form still occupies a
|
|
2374
|
+
* top-level slot), so a mismatch against the flat parser's key count reveals
|
|
2375
|
+
* dropped content.
|
|
2376
|
+
*/
|
|
2377
|
+
function countTopLevelKeys(xData: string): number {
|
|
2378
|
+
const s = xData;
|
|
2379
|
+
const n = s.length;
|
|
2380
|
+
let depth = 0;
|
|
2381
|
+
let i = 0;
|
|
2382
|
+
let count = 0;
|
|
2383
|
+
let sawTokenInSlot = false;
|
|
2384
|
+
|
|
2385
|
+
const skipString = (quote: string): void => {
|
|
2386
|
+
i += 1;
|
|
2387
|
+
while (i < n) {
|
|
2388
|
+
if (s[i] === "\\") {
|
|
2389
|
+
i += 2;
|
|
2390
|
+
continue;
|
|
2391
|
+
}
|
|
2392
|
+
if (s[i] === quote) {
|
|
2393
|
+
i += 1;
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
i += 1;
|
|
2397
|
+
}
|
|
2398
|
+
};
|
|
2399
|
+
|
|
2400
|
+
while (i < n) {
|
|
2401
|
+
const c = s[i]!;
|
|
2402
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
2403
|
+
if (depth === 1) sawTokenInSlot = true;
|
|
2404
|
+
skipString(c);
|
|
2405
|
+
continue;
|
|
2406
|
+
}
|
|
2407
|
+
if (c === "/" && s[i + 1] === "/") {
|
|
2408
|
+
i += 2;
|
|
2409
|
+
while (i < n && s[i] !== "\n") i += 1;
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
2412
|
+
if (c === "/" && s[i + 1] === "*") {
|
|
2413
|
+
i += 2;
|
|
2414
|
+
while (i < n && !(s[i] === "*" && s[i + 1] === "/")) i += 1;
|
|
2415
|
+
i += 2;
|
|
2416
|
+
continue;
|
|
2417
|
+
}
|
|
2418
|
+
if (c === "{" || c === "[" || c === "(") {
|
|
2419
|
+
depth += 1;
|
|
2420
|
+
i += 1;
|
|
2421
|
+
continue;
|
|
2422
|
+
}
|
|
2423
|
+
if (c === "}" || c === "]" || c === ")") {
|
|
2424
|
+
if (depth === 1 && c === "}" && sawTokenInSlot) {
|
|
2425
|
+
count += 1;
|
|
2426
|
+
sawTokenInSlot = false;
|
|
2427
|
+
}
|
|
2428
|
+
depth -= 1;
|
|
2429
|
+
i += 1;
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
if (depth === 1) {
|
|
2433
|
+
if (c === ",") {
|
|
2434
|
+
if (sawTokenInSlot) count += 1;
|
|
2435
|
+
sawTokenInSlot = false;
|
|
2436
|
+
} else if (!/\s/.test(c)) {
|
|
2437
|
+
sawTokenInSlot = true;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
i += 1;
|
|
2441
|
+
}
|
|
2442
|
+
return count;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2152
2445
|
/** A boolean-ish prop value (`"true"` / `"false"`), case-insensitive. */
|
|
2153
2446
|
export function isBooleanPropValue(value: string): boolean {
|
|
2154
2447
|
const v = value.trim().toLowerCase();
|
|
@@ -5372,6 +5665,7 @@ function MakeItRealCard({
|
|
|
5372
5665
|
* typed here; the action may return additional fields.
|
|
5373
5666
|
*/
|
|
5374
5667
|
interface ComponentDetailsResult {
|
|
5668
|
+
nodeId: string;
|
|
5375
5669
|
name: string;
|
|
5376
5670
|
sourceType: string;
|
|
5377
5671
|
observedProps: Array<{ name: string; value: string }>;
|
|
@@ -5405,32 +5699,57 @@ interface ComponentDetailsResult {
|
|
|
5405
5699
|
*
|
|
5406
5700
|
* Matches the workbench artboard spec in DESIGN-STUDIO-PLAN.md §6.1.
|
|
5407
5701
|
*/
|
|
5408
|
-
function ComponentSection({
|
|
5702
|
+
export function ComponentSection({
|
|
5409
5703
|
designId,
|
|
5704
|
+
fileId,
|
|
5410
5705
|
nodeId,
|
|
5706
|
+
onComponentPropApplied,
|
|
5411
5707
|
sourceCapabilities = [],
|
|
5412
5708
|
}: {
|
|
5413
5709
|
designId: string;
|
|
5710
|
+
fileId?: string;
|
|
5414
5711
|
nodeId: string;
|
|
5712
|
+
onComponentPropApplied?: (fileId: string, content: string) => void;
|
|
5415
5713
|
/** Capability names advertised by the current source. */
|
|
5416
5714
|
sourceCapabilities?: string[];
|
|
5417
5715
|
}) {
|
|
5418
5716
|
const queryClient = useQueryClient();
|
|
5419
|
-
const
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5717
|
+
const detailsParams = { designId, nodeId, ...(fileId ? { fileId } : {}) };
|
|
5718
|
+
const detailsKey = ["action", "get-component-details", detailsParams];
|
|
5719
|
+
|
|
5720
|
+
const { data, isLoading, error, refetch } =
|
|
5721
|
+
useActionQuery<ComponentDetailsResult>(
|
|
5722
|
+
"get-component-details",
|
|
5723
|
+
detailsParams,
|
|
5724
|
+
{ refetchOnMount: "always" },
|
|
5725
|
+
);
|
|
5425
5726
|
|
|
5426
5727
|
const openSourceMutation = useActionMutation("open-component-source");
|
|
5427
5728
|
const applyPropMutation = useActionMutation("apply-component-prop-edit");
|
|
5428
5729
|
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5730
|
+
const postComponentPropPreview = useCallback(
|
|
5731
|
+
(attribute: string, value: string) => {
|
|
5732
|
+
if (typeof document === "undefined") return;
|
|
5733
|
+
|
|
5734
|
+
const iframe = document.querySelector<HTMLIFrameElement>(
|
|
5735
|
+
"iframe[data-design-preview-iframe]",
|
|
5736
|
+
);
|
|
5737
|
+
iframe?.contentWindow?.postMessage(
|
|
5738
|
+
{
|
|
5739
|
+
type: "style-change",
|
|
5740
|
+
selector: data?.instance?.selector ?? "",
|
|
5741
|
+
nodeId: data?.instance?.nodeId ?? nodeId,
|
|
5742
|
+
attributeOverrides: { [attribute]: value },
|
|
5743
|
+
},
|
|
5744
|
+
"*",
|
|
5745
|
+
);
|
|
5746
|
+
},
|
|
5747
|
+
[data?.instance?.nodeId, data?.instance?.selector, nodeId],
|
|
5748
|
+
);
|
|
5749
|
+
|
|
5750
|
+
// Persist a single prop change through apply-component-prop-edit. Attribute
|
|
5751
|
+
// props also preview immediately in the iframe so the selected component
|
|
5752
|
+
// changes without waiting for the write/refetch round-trip.
|
|
5434
5753
|
const persistPropEdit = (
|
|
5435
5754
|
edit:
|
|
5436
5755
|
| { kind: "alpineData"; value: string }
|
|
@@ -5440,20 +5759,51 @@ function ComponentSection({
|
|
|
5440
5759
|
queryClient.setQueryData<ComponentDetailsResult>(detailsKey, (prev) =>
|
|
5441
5760
|
prev ? optimistic(prev) : prev,
|
|
5442
5761
|
);
|
|
5762
|
+
if (edit.kind === "attribute") {
|
|
5763
|
+
postComponentPropPreview(edit.attribute, edit.value);
|
|
5764
|
+
}
|
|
5443
5765
|
applyPropMutation.mutate(
|
|
5444
|
-
{ designId, nodeId, edit },
|
|
5766
|
+
{ designId, nodeId, ...(fileId ? { fileId } : {}), edit },
|
|
5445
5767
|
{
|
|
5768
|
+
onSuccess: (result) => {
|
|
5769
|
+
const response = result as {
|
|
5770
|
+
content?: unknown;
|
|
5771
|
+
fileId?: unknown;
|
|
5772
|
+
};
|
|
5773
|
+
if (
|
|
5774
|
+
typeof response.fileId === "string" &&
|
|
5775
|
+
typeof response.content === "string"
|
|
5776
|
+
) {
|
|
5777
|
+
onComponentPropApplied?.(response.fileId, response.content);
|
|
5778
|
+
}
|
|
5779
|
+
},
|
|
5446
5780
|
onSettled: () => {
|
|
5447
|
-
// Re-render the canvas from the new content and re-read the props.
|
|
5448
5781
|
void queryClient.invalidateQueries({
|
|
5449
5782
|
queryKey: ["action", "get-design"],
|
|
5450
5783
|
});
|
|
5451
5784
|
void queryClient.invalidateQueries({ queryKey: detailsKey });
|
|
5785
|
+
void refetch();
|
|
5452
5786
|
},
|
|
5453
5787
|
},
|
|
5454
5788
|
);
|
|
5455
5789
|
};
|
|
5456
5790
|
|
|
5791
|
+
useEffect(() => {
|
|
5792
|
+
if (typeof document === "undefined") return;
|
|
5793
|
+
|
|
5794
|
+
const handleMessage = (event: MessageEvent) => {
|
|
5795
|
+
if (
|
|
5796
|
+
(event.data as { type?: unknown } | null)?.type === "element-select"
|
|
5797
|
+
) {
|
|
5798
|
+
void refetch();
|
|
5799
|
+
}
|
|
5800
|
+
};
|
|
5801
|
+
window.addEventListener("message", handleMessage);
|
|
5802
|
+
return () => {
|
|
5803
|
+
window.removeEventListener("message", handleMessage);
|
|
5804
|
+
};
|
|
5805
|
+
}, [refetch]);
|
|
5806
|
+
|
|
5457
5807
|
// While loading, show a compact skeleton that matches the section width.
|
|
5458
5808
|
if (isLoading) {
|
|
5459
5809
|
return (
|
|
@@ -5491,7 +5841,7 @@ function ComponentSection({
|
|
|
5491
5841
|
// Real-app sources keep the deeper source-prop controls gated as-is, so for
|
|
5492
5842
|
// non-inline sources the controls are read-only here.
|
|
5493
5843
|
const isInline = sourceType === "inline";
|
|
5494
|
-
const editingEnabled = isInline; // gated; real-app stays read-only for now
|
|
5844
|
+
const editingEnabled = isInline && capabilities.canEditProps; // gated; real-app stays read-only for now
|
|
5495
5845
|
const alpineData = parseAlpineDataObject(instance?.alpineData);
|
|
5496
5846
|
|
|
5497
5847
|
// Each editable row: name + current value + how it persists + its options.
|
|
@@ -5552,22 +5902,51 @@ function ComponentSection({
|
|
|
5552
5902
|
if (!editingEnabled || nextValue === row.value) return;
|
|
5553
5903
|
|
|
5554
5904
|
if (row.surface === "alpineData") {
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5905
|
+
// Surgically replace only the edited key's value inside the original
|
|
5906
|
+
// x-data string so methods, nested objects, escaped strings, quoted
|
|
5907
|
+
// keys, and whitespace survive byte-for-byte. A full
|
|
5908
|
+
// parse→mutate→serialize round-trip would drop anything
|
|
5909
|
+
// parseAlpineDataObject can't model (e.g. `toggle() { … }`).
|
|
5910
|
+
const original = instance?.alpineData ?? "";
|
|
5911
|
+
const surgical = replaceAlpineDataKeyValue(original, row.name, nextValue);
|
|
5912
|
+
|
|
5913
|
+
let serialized: string;
|
|
5914
|
+
if (surgical != null) {
|
|
5915
|
+
serialized = surgical;
|
|
5916
|
+
} else if (canRebuildAlpineDataLosslessly(original)) {
|
|
5917
|
+
// The key isn't present yet (or there is no original literal). Rebuild
|
|
5918
|
+
// from the flat map — safe here precisely because the original holds
|
|
5919
|
+
// nothing richer than the flat literals serialize already preserves.
|
|
5920
|
+
const nextData = { ...(alpineData ?? {}), [row.name]: nextValue };
|
|
5921
|
+
serialized = serializeAlpineDataObject(nextData);
|
|
5922
|
+
} else {
|
|
5923
|
+
// The original carries content (methods / nested / expressions) we
|
|
5924
|
+
// can't rewrite for this key without dropping it. Fail safe: skip the
|
|
5925
|
+
// edit rather than persist a lossy rewrite, and tell the user why so
|
|
5926
|
+
// the change doesn't silently vanish.
|
|
5927
|
+
toast.error(
|
|
5928
|
+
// i18n-ignore
|
|
5929
|
+
"Can’t safely edit this prop inline — this component’s Alpine state is too complex. Edit the source instead.",
|
|
5930
|
+
);
|
|
5931
|
+
return;
|
|
5932
|
+
}
|
|
5933
|
+
|
|
5934
|
+
const nextSerialized = serialized;
|
|
5935
|
+
persistPropEdit(
|
|
5936
|
+
{ kind: "alpineData", value: nextSerialized },
|
|
5937
|
+
(prev) => ({
|
|
5938
|
+
...prev,
|
|
5939
|
+
instance: { ...(prev.instance ?? {}), alpineData: nextSerialized },
|
|
5940
|
+
observedProps: prev.observedProps.map((p) =>
|
|
5941
|
+
p.name === row.name ? { ...p, value: nextValue } : p,
|
|
5942
|
+
),
|
|
5943
|
+
}),
|
|
5944
|
+
);
|
|
5564
5945
|
} else {
|
|
5565
|
-
// camelCase prop name → data-agent-native-prop-<kebab>
|
|
5566
|
-
const kebab = row.name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
5567
5946
|
persistPropEdit(
|
|
5568
5947
|
{
|
|
5569
5948
|
kind: "attribute",
|
|
5570
|
-
attribute:
|
|
5949
|
+
attribute: propNameToDataAttribute(row.name),
|
|
5571
5950
|
value: nextValue,
|
|
5572
5951
|
},
|
|
5573
5952
|
(prev) => {
|
|
@@ -5624,7 +6003,11 @@ function ComponentSection({
|
|
|
5624
6003
|
"Edit component source" /* i18n-ignore design inspector action */
|
|
5625
6004
|
}
|
|
5626
6005
|
onClick={() => {
|
|
5627
|
-
openSourceMutation.mutate({
|
|
6006
|
+
openSourceMutation.mutate({
|
|
6007
|
+
designId,
|
|
6008
|
+
nodeId,
|
|
6009
|
+
...(fileId ? { fileId } : {}),
|
|
6010
|
+
});
|
|
5628
6011
|
}}
|
|
5629
6012
|
>
|
|
5630
6013
|
<IconExternalLink className="size-3.5" />
|
|
@@ -5667,6 +6050,7 @@ function ComponentSection({
|
|
|
5667
6050
|
{rows.map((row) => {
|
|
5668
6051
|
const hasOptions = (row.options?.length ?? 0) > 0;
|
|
5669
6052
|
const isBoolean = !hasOptions && isBooleanPropValue(row.value);
|
|
6053
|
+
const disabled = !editingEnabled || applyPropMutation.isPending;
|
|
5670
6054
|
return (
|
|
5671
6055
|
<div key={row.name} className="flex items-center gap-1.5">
|
|
5672
6056
|
<Label className="w-[64px] shrink-0 truncate text-[11px] font-medium capitalize text-muted-foreground">
|
|
@@ -5677,7 +6061,7 @@ function ComponentSection({
|
|
|
5677
6061
|
<Select
|
|
5678
6062
|
value={row.value || row.options![0] || ""}
|
|
5679
6063
|
onValueChange={(v) => commitProp(row, v)}
|
|
5680
|
-
disabled={
|
|
6064
|
+
disabled={disabled}
|
|
5681
6065
|
>
|
|
5682
6066
|
<SelectTrigger className="h-6 min-w-0 flex-1 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-1.5 text-[11px] shadow-none focus:ring-1 focus:ring-[var(--design-editor-accent-color)]">
|
|
5683
6067
|
<SelectValue />
|
|
@@ -5702,7 +6086,7 @@ function ComponentSection({
|
|
|
5702
6086
|
onCheckedChange={(checked) =>
|
|
5703
6087
|
commitProp(row, checked ? "true" : "false")
|
|
5704
6088
|
}
|
|
5705
|
-
disabled={
|
|
6089
|
+
disabled={disabled}
|
|
5706
6090
|
className="h-4 w-7 [&>span]:size-3 [&>span]:data-[state=checked]:translate-x-3"
|
|
5707
6091
|
aria-label={
|
|
5708
6092
|
row.name /* i18n-ignore dynamic prop name */
|
|
@@ -5714,7 +6098,7 @@ function ComponentSection({
|
|
|
5714
6098
|
<Input
|
|
5715
6099
|
defaultValue={row.value}
|
|
5716
6100
|
key={`${row.name}:${row.value}`}
|
|
5717
|
-
disabled={
|
|
6101
|
+
disabled={disabled}
|
|
5718
6102
|
onBlur={(e) => commitProp(row, e.target.value)}
|
|
5719
6103
|
onKeyDown={(e) => {
|
|
5720
6104
|
if (e.key === "Enter") {
|
|
@@ -5764,6 +6148,7 @@ export function EditPanel({
|
|
|
5764
6148
|
fileId,
|
|
5765
6149
|
filename,
|
|
5766
6150
|
designId,
|
|
6151
|
+
onComponentPropApplied,
|
|
5767
6152
|
onTokensApplied,
|
|
5768
6153
|
statesPanelProps,
|
|
5769
6154
|
reviewPanelProps,
|
|
@@ -5825,6 +6210,7 @@ export function EditPanel({
|
|
|
5825
6210
|
// over-scroll bounce, could briefly un-shield the canvas and allow a stray
|
|
5826
6211
|
// pointer event to deselect the selected canvas element (R3 regression).
|
|
5827
6212
|
const scrolledRecentlyRef = useRef(false);
|
|
6213
|
+
const userScrollIntentRef = useRef(false);
|
|
5828
6214
|
const scrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
5829
6215
|
|
|
5830
6216
|
if (readOnly) {
|
|
@@ -5904,7 +6290,14 @@ export function EditPanel({
|
|
|
5904
6290
|
|
|
5905
6291
|
<div
|
|
5906
6292
|
className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain"
|
|
6293
|
+
onWheelCapture={() => {
|
|
6294
|
+
userScrollIntentRef.current = true;
|
|
6295
|
+
}}
|
|
6296
|
+
onTouchMoveCapture={() => {
|
|
6297
|
+
userScrollIntentRef.current = true;
|
|
6298
|
+
}}
|
|
5907
6299
|
onScroll={() => {
|
|
6300
|
+
if (!userScrollIntentRef.current) return;
|
|
5908
6301
|
// Mark that a scroll just happened so the click that some
|
|
5909
6302
|
// browsers fire at the end of a scroll gesture (or after an
|
|
5910
6303
|
// overscroll/rubber-band bounce) is suppressed. Crucially this
|
|
@@ -5919,6 +6312,7 @@ export function EditPanel({
|
|
|
5919
6312
|
}
|
|
5920
6313
|
scrollTimerRef.current = setTimeout(() => {
|
|
5921
6314
|
scrolledRecentlyRef.current = false;
|
|
6315
|
+
userScrollIntentRef.current = false;
|
|
5922
6316
|
scrollTimerRef.current = null;
|
|
5923
6317
|
}, 300);
|
|
5924
6318
|
}}
|
|
@@ -5930,6 +6324,7 @@ export function EditPanel({
|
|
|
5930
6324
|
// browsers generate after a touch-scroll ends.
|
|
5931
6325
|
if (!scrolledRecentlyRef.current) return;
|
|
5932
6326
|
scrolledRecentlyRef.current = false;
|
|
6327
|
+
userScrollIntentRef.current = false;
|
|
5933
6328
|
if (scrollTimerRef.current !== null) {
|
|
5934
6329
|
clearTimeout(scrollTimerRef.current);
|
|
5935
6330
|
scrollTimerRef.current = null;
|
|
@@ -5971,7 +6366,9 @@ export function EditPanel({
|
|
|
5971
6366
|
{designId && componentNodeId && (
|
|
5972
6367
|
<ComponentSection
|
|
5973
6368
|
designId={designId}
|
|
6369
|
+
fileId={fileId}
|
|
5974
6370
|
nodeId={componentNodeId}
|
|
6371
|
+
onComponentPropApplied={onComponentPropApplied}
|
|
5975
6372
|
sourceCapabilities={sourceCapabilities}
|
|
5976
6373
|
/>
|
|
5977
6374
|
)}
|