@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
@@ -51,6 +51,7 @@ import {
51
51
  type CodeLayerTreeNode,
52
52
  } from "@shared/code-layer";
53
53
  import { componentNameFor, isComponentInstance } from "@shared/component-model";
54
+ import type { A11yFinding } from "@shared/design-review";
54
55
  import {
55
56
  DESIGN_CAPABILITY_NAMES,
56
57
  hasCapability,
@@ -60,7 +61,7 @@ import {
60
61
  resolveTweaksToCssVars,
61
62
  type TweakSelections,
62
63
  } from "@shared/resolve-tweaks";
63
- import { widthToPrefix } from "@shared/responsive-classes";
64
+ import { utilityStem, widthToPrefix } from "@shared/responsive-classes";
64
65
  import { normalizeDesignSourceType } from "@shared/source-mode";
65
66
  import {
66
67
  IconArrowLeft,
@@ -223,8 +224,6 @@ import {
223
224
  import { prettyScreenName } from "@/lib/screen-names";
224
225
  import { cn } from "@/lib/utils";
225
226
 
226
- import type { A11yFinding } from "../../shared/design-review.js";
227
-
228
227
  const TAB_ID = generateTabId();
229
228
 
230
229
  // Selection is tab-scoped (like navigation) so a second editor tab cannot
@@ -434,6 +433,32 @@ export function getLayerMoveSourceContent(args: {
434
433
  );
435
434
  }
436
435
 
436
+ export function getFreshActiveFileContent(args: {
437
+ activeContent: string;
438
+ latestContent?: string | null;
439
+ lastLocalContent?: string | null;
440
+ }) {
441
+ return args.latestContent ?? args.lastLocalContent ?? args.activeContent;
442
+ }
443
+
444
+ export function getFreshScreenContent(args: {
445
+ screenId: string;
446
+ activeFileId?: string | null;
447
+ freshActiveContent: string;
448
+ fileContentById: ReadonlyMap<string, string>;
449
+ }) {
450
+ return args.screenId === args.activeFileId
451
+ ? args.freshActiveContent
452
+ : (args.fileContentById.get(args.screenId) ?? "");
453
+ }
454
+
455
+ export function getLayerMoveIterationOrder<T>(
456
+ orderedIds: readonly T[],
457
+ placement: "before" | "after" | "inside",
458
+ ): T[] {
459
+ return placement === "after" ? [...orderedIds].reverse() : [...orderedIds];
460
+ }
461
+
437
462
  function resolveZoomUpdate(update: SetStateAction<number>, current: number) {
438
463
  return typeof update === "function" ? update(current) : update;
439
464
  }
@@ -939,6 +964,113 @@ function applyInlineStylesToHtml(
939
964
  }
940
965
  }
941
966
 
967
+ const CSS_PROPERTY_UTILITY_STEMS: Record<string, string[]> = {
968
+ color: ["text-color"],
969
+ "background-color": ["background-color"],
970
+ background: ["background-color", "background-image"],
971
+ "font-size": ["font-size"],
972
+ "font-weight": ["font-weight"],
973
+ "font-family": ["font-family"],
974
+ "text-align": ["text-align"],
975
+ display: ["display"],
976
+ position: ["position"],
977
+ width: ["w"],
978
+ height: ["h"],
979
+ opacity: ["opacity"],
980
+ "border-radius": ["rounded"],
981
+ padding: ["p"],
982
+ "padding-left": ["px", "pl"],
983
+ "padding-right": ["px", "pr"],
984
+ "padding-top": ["py", "pt"],
985
+ "padding-bottom": ["py", "pb"],
986
+ margin: ["m"],
987
+ "margin-left": ["mx", "ml"],
988
+ "margin-right": ["mx", "mr"],
989
+ "margin-top": ["my", "mt"],
990
+ "margin-bottom": ["my", "mb"],
991
+ gap: ["gap"],
992
+ "column-gap": ["gap-x"],
993
+ "row-gap": ["gap-y"],
994
+ };
995
+
996
+ const DEFAULT_STATES_PANEL_BREAKPOINTS = [
997
+ { id: "bp-mobile", label: "Mobile", widthPx: 390 },
998
+ { id: "bp-tablet", label: "Tablet", widthPx: 768 },
999
+ { id: "bp-desktop", label: "Desktop", widthPx: 1280 },
1000
+ ] as const;
1001
+
1002
+ function normalizeCssPropertyName(property: string): string {
1003
+ return property.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
1004
+ }
1005
+
1006
+ function looksLikeTailwindUtility(value: string): boolean {
1007
+ const trimmed = value.trim();
1008
+ if (!trimmed || /\s/.test(trimmed)) return false;
1009
+ if (/[;{}]/.test(trimmed) || /\/\*/.test(trimmed)) return false;
1010
+ if (/^(?:#|rgb\(|rgba\(|hsl\(|hsla\(|var\(|calc\()/i.test(trimmed)) {
1011
+ return false;
1012
+ }
1013
+ if (trimmed.includes(":")) return false;
1014
+ return /^[!-]?[a-z0-9][a-z0-9[\]()./%_-]*$/i.test(trimmed);
1015
+ }
1016
+
1017
+ function responsiveUtilityMatchesStyleProperty(
1018
+ property: string,
1019
+ value: string,
1020
+ ): boolean {
1021
+ if (!looksLikeTailwindUtility(value)) return false;
1022
+ const normalizedProperty = normalizeCssPropertyName(property);
1023
+ const stem = utilityStem(value.trim());
1024
+ const allowed = CSS_PROPERTY_UTILITY_STEMS[normalizedProperty];
1025
+ return allowed ? allowed.includes(stem) : stem === normalizedProperty;
1026
+ }
1027
+
1028
+ interface DesignStatePreviewRow {
1029
+ captureData?: Record<string, unknown> | null;
1030
+ fixtureData?: Record<string, unknown> | null;
1031
+ }
1032
+
1033
+ const STATE_PREVIEW_HTML_KEYS = [
1034
+ "domHtml",
1035
+ "domSnapshot",
1036
+ "documentHtml",
1037
+ "html",
1038
+ "content",
1039
+ "markup",
1040
+ ] as const;
1041
+
1042
+ function looksLikePreviewHtml(value: string): boolean {
1043
+ return /<!doctype|<html\b|<body\b|<[a-zA-Z][\s>]/i.test(value);
1044
+ }
1045
+
1046
+ function findStatePreviewHtml(value: unknown, depth = 0): string | null {
1047
+ if (typeof value === "string") {
1048
+ return looksLikePreviewHtml(value) ? value : null;
1049
+ }
1050
+ if (!value || typeof value !== "object" || Array.isArray(value) || depth > 2)
1051
+ return null;
1052
+ const record = value as Record<string, unknown>;
1053
+ for (const key of STATE_PREVIEW_HTML_KEYS) {
1054
+ const hit = findStatePreviewHtml(record[key], depth + 1);
1055
+ if (hit) return hit;
1056
+ }
1057
+ for (const entry of Object.values(record)) {
1058
+ const hit = findStatePreviewHtml(entry, depth + 1);
1059
+ if (hit) return hit;
1060
+ }
1061
+ return null;
1062
+ }
1063
+
1064
+ function designStatePreviewHtml(
1065
+ row: DesignStatePreviewRow | undefined,
1066
+ ): string | null {
1067
+ if (!row) return null;
1068
+ return (
1069
+ findStatePreviewHtml(row.captureData) ??
1070
+ findStatePreviewHtml(row.fixtureData)
1071
+ );
1072
+ }
1073
+
942
1074
  function escapeHtmlAttributeValue(value: string): string {
943
1075
  return value
944
1076
  .replace(/&/g, "&amp;")
@@ -2096,6 +2228,36 @@ function collectCodeLayerAncestors(
2096
2228
  return [];
2097
2229
  }
2098
2230
 
2231
+ export function sortCodeLayerIdsByTreeOrder(
2232
+ ids: readonly string[],
2233
+ tree: readonly CodeLayerTreeNode[],
2234
+ ): string[] {
2235
+ const treeOrder = new Map<string, number>();
2236
+ let index = 0;
2237
+ const visit = (nodes: readonly CodeLayerTreeNode[]) => {
2238
+ for (const node of nodes) {
2239
+ treeOrder.set(node.id, index);
2240
+ index += 1;
2241
+ visit(node.children);
2242
+ }
2243
+ };
2244
+ visit(tree);
2245
+
2246
+ const originalOrder = new Map(
2247
+ ids.map((id, originalIndex) => [id, originalIndex]),
2248
+ );
2249
+ return [...ids].sort((a, b) => {
2250
+ const aOrder = treeOrder.get(a);
2251
+ const bOrder = treeOrder.get(b);
2252
+ if (aOrder === undefined && bOrder === undefined) {
2253
+ return (originalOrder.get(a) ?? 0) - (originalOrder.get(b) ?? 0);
2254
+ }
2255
+ if (aOrder === undefined) return 1;
2256
+ if (bOrder === undefined) return -1;
2257
+ return aOrder - bOrder;
2258
+ });
2259
+ }
2260
+
2099
2261
  function findCodeLayerNodeInProjection(
2100
2262
  projection: CodeLayerProjection,
2101
2263
  previousNode: CodeLayerNode,
@@ -2819,6 +2981,11 @@ function buildAuthoritativeTweakSelections(
2819
2981
  ? persistedSelections[tweak.id]
2820
2982
  : tweak.defaultValue;
2821
2983
  }
2984
+ for (const [key, value] of Object.entries(persistedSelections)) {
2985
+ if (/^--[-_a-zA-Z0-9]+$/.test(key)) {
2986
+ selections[key] = value;
2987
+ }
2988
+ }
2822
2989
  return selections;
2823
2990
  }
2824
2991
 
@@ -3035,6 +3202,9 @@ export default function DesignEditor() {
3035
3202
  const [hiddenLayerIds, setHiddenLayerIds] = useState<Set<string>>(
3036
3203
  () => new Set(),
3037
3204
  );
3205
+ const layerStateOverridesRef = useRef<
3206
+ Map<string, { hidden?: boolean; locked?: boolean }>
3207
+ >(new Map());
3038
3208
  const [overviewSelectAllRequest, setOverviewSelectAllRequest] = useState(0);
3039
3209
  const [overviewClearSelectionRequest, setOverviewClearSelectionRequest] =
3040
3210
  useState(0);
@@ -3054,6 +3224,11 @@ export default function DesignEditor() {
3054
3224
  const [motionDockOpen, setMotionDockOpen] = useState(false);
3055
3225
  const [motionTracks, setMotionTracks] = useState<MotionDockTrack[]>([]);
3056
3226
  const [motionDurationMs, setMotionDurationMs] = useState(1000);
3227
+ const [shaderFillPreview, setShaderFillPreview] = useState<{
3228
+ selector?: string;
3229
+ nodeId?: string;
3230
+ css: string;
3231
+ } | null>(null);
3057
3232
 
3058
3233
  // ── Breakpoint preview state (§6.4) ─────────────────────────────────────────
3059
3234
  // Active breakpoint width for the current design (pixels). Controls which
@@ -3065,6 +3240,11 @@ export default function DesignEditor() {
3065
3240
  // ── Design state selection (§6.4 / §8) ───────────────────────────────────────
3066
3241
  // null = Default (live) view; a string id = one of the design_state rows.
3067
3242
  const [selectedStateId, setSelectedStateId] = useState<string | null>(null);
3243
+ const [reviewFileId, setReviewFileId] = useState<string | null>(null);
3244
+ const [reviewFindings, setReviewFindings] = useState<A11yFinding[]>([]);
3245
+ const [reviewAuditLoading, setReviewAuditLoading] = useState(false);
3246
+ const [reviewAuditedAt, setReviewAuditedAt] = useState<string | null>(null);
3247
+ const [reviewAuditError, setReviewAuditError] = useState<string | null>(null);
3068
3248
 
3069
3249
  useEffect(() => {
3070
3250
  if (!isBuilderDesignEmbed) return;
@@ -3213,6 +3393,14 @@ export default function DesignEditor() {
3213
3393
  geometryRedoStackRef.current.length > 0,
3214
3394
  );
3215
3395
  }, []);
3396
+ const clearLocalUndoRedoStacks = useCallback(() => {
3397
+ contentUndoStackRef.current = [];
3398
+ contentRedoStackRef.current = [];
3399
+ geometryUndoStackRef.current = [];
3400
+ geometryRedoStackRef.current = [];
3401
+ historyOrderRef.current = [];
3402
+ redoOrderRef.current = [];
3403
+ }, []);
3216
3404
  const persistedSelectionStateRef = useRef<string | null>(null);
3217
3405
  const designSelectionOwnerIdRef = useRef(`${TAB_ID}:${generateTabId()}`);
3218
3406
  const frameGeometrySaveTimerRef = useRef<number | null>(null);
@@ -4421,70 +4609,21 @@ export default function DesignEditor() {
4421
4609
  }, [files, activeFileId]);
4422
4610
 
4423
4611
  const activeFile = files.find((f) => f.id === activeFileId) ?? files[0];
4612
+ useEffect(() => {
4613
+ if (!reviewFileId || reviewFileId === activeFile?.id) return;
4614
+ setReviewFileId(null);
4615
+ setReviewFindings([]);
4616
+ setReviewAuditedAt(null);
4617
+ setReviewAuditError(null);
4618
+ setReviewAuditLoading(false);
4619
+ }, [activeFile?.id, reviewFileId]);
4620
+
4424
4621
  const initialGenerationReadOnly = shouldLockInspectorForInitialGeneration({
4425
4622
  fileCount: files.length,
4426
4623
  generating,
4427
4624
  pendingGenerationActive,
4428
4625
  });
4429
4626
 
4430
- // §6.5 Review panel — lazy, on-demand accessibility audit for the active
4431
- // file. The audit walks the file's rendered HTML, so it must never run on
4432
- // editor load: it is gated behind `auditRequested` (flipped by the panel's
4433
- // "Run" button) and reset whenever the active file changes so each file is
4434
- // audited explicitly. The ReviewPanel itself owns the apply-a11y-fix mutation
4435
- // behind its inline "Fix" button; we just supply the active design source and
4436
- // refetch on apply so a resolved finding drops out of the list.
4437
- const [auditRequested, setAuditRequested] = useState(false);
4438
- useEffect(() => {
4439
- setAuditRequested(false);
4440
- }, [activeFile?.id]);
4441
-
4442
- const auditQuery = useActionQuery<{
4443
- auditedAt: string;
4444
- findings: A11yFinding[];
4445
- }>(
4446
- "run-design-audit",
4447
- { designId: id ?? "", fileId: activeFile?.id },
4448
- {
4449
- enabled: auditRequested && !!id && !!activeFile?.id,
4450
- refetchOnWindowFocus: false,
4451
- },
4452
- );
4453
-
4454
- const reviewPanelProps = useMemo<
4455
- Omit<ReviewPanelProps, "className"> | undefined
4456
- >(() => {
4457
- if (!id || !activeFile) return undefined;
4458
- const audit = auditRequested ? auditQuery.data : undefined;
4459
- return {
4460
- findings: audit?.findings ?? [],
4461
- auditLoading: auditRequested && auditQuery.isFetching,
4462
- auditedAt: audit?.auditedAt ?? null,
4463
- auditError:
4464
- auditRequested && auditQuery.isError
4465
- ? auditQuery.error?.message ||
4466
- "Audit failed. Try again." /* i18n-ignore design review fallback */
4467
- : null,
4468
- onRunAudit: () => {
4469
- // First run flips the gate (the query auto-fetches); later runs refetch.
4470
- if (auditRequested) {
4471
- void auditQuery.refetch();
4472
- } else {
4473
- setAuditRequested(true);
4474
- }
4475
- },
4476
- fixSource: {
4477
- designId: id,
4478
- fileId: activeFile.id,
4479
- filename: activeFile.filename,
4480
- },
4481
- onFixApplied: () => {
4482
- // Re-run the audit so the resolved finding drops out of the list.
4483
- void auditQuery.refetch();
4484
- },
4485
- };
4486
- }, [id, activeFile, auditRequested, auditQuery]);
4487
-
4488
4627
  const selectedScreenIds = useMemo(
4489
4628
  () =>
4490
4629
  getSelectedScreenIdsForEditorState({
@@ -4873,6 +5012,7 @@ export default function DesignEditor() {
4873
5012
  // The last content this client itself wrote into the Y.Doc (inline-style
4874
5013
  // edits) — so the reconcile/observe doesn't treat our own echo as external.
4875
5014
  const lastLocalContentRef = useRef<string | null>(null);
5015
+ const latestActiveContentRef = useRef<string | null>(null);
4876
5016
  // Freshest known DB `updatedAt` for the active file, kept in a ref so the
4877
5017
  // Yjs observe handler can advance the reconcile watermark without re-subscribing.
4878
5018
  const documentFileUpdatedAtRef = useRef<string | null>(null);
@@ -4916,8 +5056,8 @@ export default function DesignEditor() {
4916
5056
  setCollabContentFileId(null);
4917
5057
  lastAppliedFileUpdatedAtRef.current = null;
4918
5058
  lastLocalContentRef.current = null;
4919
- contentUndoStackRef.current = [];
4920
- contentRedoStackRef.current = [];
5059
+ latestActiveContentRef.current = null;
5060
+ clearLocalUndoRedoStacks();
4921
5061
  clearStaleAgentCollabRecovery();
4922
5062
  syncUndoRedoState();
4923
5063
  return;
@@ -4928,13 +5068,14 @@ export default function DesignEditor() {
4928
5068
  setCollabContentFileId(null);
4929
5069
  lastAppliedFileUpdatedAtRef.current = null;
4930
5070
  lastLocalContentRef.current = null;
4931
- contentUndoStackRef.current = [];
4932
- contentRedoStackRef.current = [];
5071
+ latestActiveContentRef.current = null;
5072
+ clearLocalUndoRedoStacks();
4933
5073
  clearStaleAgentCollabRecovery();
4934
5074
  syncUndoRedoState();
4935
5075
  }
4936
5076
  }, [
4937
5077
  activeFileId,
5078
+ clearLocalUndoRedoStacks,
4938
5079
  clearStaleAgentCollabRecovery,
4939
5080
  syncUndoRedoState,
4940
5081
  viewMode,
@@ -4956,6 +5097,7 @@ export default function DesignEditor() {
4956
5097
  setCollabContent(pendingLocalContent);
4957
5098
  setCollabContentFileId(fileId);
4958
5099
  lastLocalContentRef.current = pendingLocalContent;
5100
+ latestActiveContentRef.current = pendingLocalContent;
4959
5101
  setContentRenderRevision((revision) => revision + 1);
4960
5102
  ydoc.transact(() => {
4961
5103
  ytext.delete(0, ytext.length);
@@ -4975,6 +5117,7 @@ export default function DesignEditor() {
4975
5117
  setCollabContent(storedContent);
4976
5118
  setCollabContentFileId(fileId);
4977
5119
  lastLocalContentRef.current = storedContent;
5120
+ latestActiveContentRef.current = storedContent;
4978
5121
  setContentRenderRevision((revision) => revision + 1);
4979
5122
  ydoc.transact(() => {
4980
5123
  ytext.delete(0, ytext.length);
@@ -4987,6 +5130,7 @@ export default function DesignEditor() {
4987
5130
  // confirms or applies the current DB content.
4988
5131
  setCollabContent(text);
4989
5132
  setCollabContentFileId(fileId);
5133
+ latestActiveContentRef.current = text;
4990
5134
  setContentRenderRevision((revision) => revision + 1);
4991
5135
  }
4992
5136
  }, [
@@ -5029,6 +5173,7 @@ export default function DesignEditor() {
5029
5173
  setCollabContent(pendingLocalContent);
5030
5174
  setCollabContentFileId(fileId);
5031
5175
  lastLocalContentRef.current = pendingLocalContent;
5176
+ latestActiveContentRef.current = pendingLocalContent;
5032
5177
  setContentRenderRevision((revision) => revision + 1);
5033
5178
  ydoc.transact(() => {
5034
5179
  ytext.delete(0, ytext.length);
@@ -5038,6 +5183,7 @@ export default function DesignEditor() {
5038
5183
  }
5039
5184
  setCollabContent(next);
5040
5185
  setCollabContentFileId(fileId);
5186
+ latestActiveContentRef.current = next;
5041
5187
  if (isLocalEdit) {
5042
5188
  lastLocalContentRef.current = next;
5043
5189
  } else {
@@ -5142,6 +5288,7 @@ export default function DesignEditor() {
5142
5288
  setCollabContent(dbContent);
5143
5289
  setCollabContentFileId(activeFile.id);
5144
5290
  lastLocalContentRef.current = dbContent;
5291
+ latestActiveContentRef.current = dbContent;
5145
5292
  if (dbUpdatedAt) lastAppliedFileUpdatedAtRef.current = dbUpdatedAt;
5146
5293
  setContentRenderRevision((revision) => revision + 1);
5147
5294
 
@@ -5195,6 +5342,7 @@ export default function DesignEditor() {
5195
5342
  setCollabContent(expectedContent);
5196
5343
  setCollabContentFileId(expectedFileId);
5197
5344
  lastLocalContentRef.current = expectedContent;
5345
+ latestActiveContentRef.current = expectedContent;
5198
5346
  lastAppliedFileUpdatedAtRef.current = expectedUpdatedAt;
5199
5347
  setContentRenderRevision((revision) => revision + 1);
5200
5348
 
@@ -5220,6 +5368,7 @@ export default function DesignEditor() {
5220
5368
  setCollabContent(dbContent);
5221
5369
  setCollabContentFileId(activeFile.id);
5222
5370
  lastLocalContentRef.current = dbContent;
5371
+ latestActiveContentRef.current = dbContent;
5223
5372
  if (dbUpdatedAt) lastAppliedFileUpdatedAtRef.current = dbUpdatedAt;
5224
5373
  setContentRenderRevision((revision) => revision + 1);
5225
5374
 
@@ -5281,6 +5430,56 @@ export default function DesignEditor() {
5281
5430
  [],
5282
5431
  );
5283
5432
 
5433
+ const handleRunDesignAudit = useCallback(async () => {
5434
+ if (!id || !activeFile?.id) return;
5435
+ const auditFileId = activeFile.id;
5436
+ setReviewFileId(auditFileId);
5437
+ setReviewAuditLoading(true);
5438
+ setReviewAuditError(null);
5439
+ try {
5440
+ const result = await callAction<{
5441
+ findings: A11yFinding[];
5442
+ auditedAt: string;
5443
+ }>("run-design-audit", {
5444
+ designId: id,
5445
+ fileId: auditFileId,
5446
+ } as any);
5447
+ setReviewFileId(auditFileId);
5448
+ setReviewFindings(Array.isArray(result.findings) ? result.findings : []);
5449
+ setReviewAuditedAt(result.auditedAt ?? new Date().toISOString());
5450
+ } catch (error) {
5451
+ const message =
5452
+ error instanceof Error
5453
+ ? error.message
5454
+ : t("designEditor.toasts.auditRunFailed");
5455
+ setReviewAuditError(message);
5456
+ toast.error(message);
5457
+ } finally {
5458
+ setReviewAuditLoading(false);
5459
+ }
5460
+ }, [activeFile?.id, id, t]);
5461
+
5462
+ const handleReviewFindingClick = useCallback(
5463
+ (finding: A11yFinding) => {
5464
+ const selector =
5465
+ finding.selector ??
5466
+ (finding.nodeId
5467
+ ? `[data-agent-native-node-id="${finding.nodeId.replace(/"/g, '\\"')}"]`
5468
+ : null);
5469
+ if (!selector) return;
5470
+ canvasIframeRef.current?.contentWindow?.postMessage(
5471
+ {
5472
+ type: "select-element",
5473
+ selector,
5474
+ nodeId: finding.nodeId ?? undefined,
5475
+ },
5476
+ "*",
5477
+ );
5478
+ if (finding.nodeId) setSelectedLayerIdsState([finding.nodeId]);
5479
+ },
5480
+ [canvasIframeRef],
5481
+ );
5482
+
5284
5483
  // Broadcast pointer position (normalized to canvas container) and
5285
5484
  // selected element selector so peers can see where the user is working.
5286
5485
  const handleCanvasPointerMove = useCallback(
@@ -5411,6 +5610,9 @@ export default function DesignEditor() {
5411
5610
  : (activeFile?.content ?? ""));
5412
5611
  const activeContent =
5413
5612
  typeof activeContentSource === "string" ? activeContentSource : "";
5613
+ useLayoutEffect(() => {
5614
+ latestActiveContentRef.current = activeContent;
5615
+ }, [activeContent]);
5414
5616
  const fileContentById = useMemo(() => {
5415
5617
  const map = new Map<string, string>();
5416
5618
  for (const file of files) {
@@ -5420,9 +5622,16 @@ export default function DesignEditor() {
5420
5622
  }, [files]);
5421
5623
  const getScreenContent = useCallback(
5422
5624
  (screenId: string) =>
5423
- screenId === activeFile?.id
5424
- ? activeContent
5425
- : (fileContentById.get(screenId) ?? ""),
5625
+ getFreshScreenContent({
5626
+ screenId,
5627
+ activeFileId: activeFile?.id,
5628
+ freshActiveContent: getFreshActiveFileContent({
5629
+ activeContent,
5630
+ latestContent: latestActiveContentRef.current,
5631
+ lastLocalContent: lastLocalContentRef.current,
5632
+ }),
5633
+ fileContentById,
5634
+ }),
5426
5635
  [activeContent, activeFile?.id, fileContentById],
5427
5636
  );
5428
5637
  const pageStyles = useMemo(
@@ -5449,6 +5658,38 @@ export default function DesignEditor() {
5449
5658
  }, [selectedCodeLayerNode, selectedElement?.selector]);
5450
5659
  const selectedCanvasSelector = selectedCanvasSelectorCandidates[0] ?? null;
5451
5660
 
5661
+ const handleDesignStateSelect = useCallback(
5662
+ (stateId: string | null, row?: DesignStatePreviewRow) => {
5663
+ setSelectedStateId(stateId);
5664
+ const win = canvasIframeRef.current?.contentWindow;
5665
+ if (!win) return;
5666
+
5667
+ if (stateId === null) {
5668
+ win.postMessage(
5669
+ {
5670
+ type: "replace-document-content",
5671
+ content: activeContent,
5672
+ forceFullDocument: true,
5673
+ },
5674
+ "*",
5675
+ );
5676
+ return;
5677
+ }
5678
+
5679
+ const html = designStatePreviewHtml(row);
5680
+ if (!html) return;
5681
+ win.postMessage(
5682
+ {
5683
+ type: "replace-document-content",
5684
+ content: html,
5685
+ forceFullDocument: true,
5686
+ },
5687
+ "*",
5688
+ );
5689
+ },
5690
+ [activeContent, canvasIframeRef],
5691
+ );
5692
+
5452
5693
  // ── Inspector header quick actions (Create component / Inspect code) ───────
5453
5694
  // Resolve the design-level source type + capability map so the inspector can
5454
5695
  // gate the real-app affordances (jump-to-source, prop write-back).
@@ -5497,10 +5738,14 @@ export default function DesignEditor() {
5497
5738
  const selectedComponentNodeId = useMemo(() => {
5498
5739
  if (!selectedCodeLayerNode) return undefined;
5499
5740
  return isComponentInstance(selectedCodeLayerNode)
5500
- ? selectedCodeLayerNode.id
5741
+ ? bridgeSourceIdForCodeLayerNode(selectedCodeLayerNode)
5501
5742
  : undefined;
5502
5743
  }, [selectedCodeLayerNode]);
5503
5744
 
5745
+ useEffect(() => {
5746
+ setShaderFillPreview(null);
5747
+ }, [activeFile?.id, selectedElement?.selector, selectedElement?.sourceId]);
5748
+
5504
5749
  // A friendly default name for the create-component dialog, derived from the
5505
5750
  // selected element's layer name / tag.
5506
5751
  const defaultComponentName = useMemo(() => {
@@ -5729,6 +5974,7 @@ export default function DesignEditor() {
5729
5974
  setCollabContent(nextContent);
5730
5975
  setCollabContentFileId(activeFile.id);
5731
5976
  lastLocalContentRef.current = nextContent;
5977
+ latestActiveContentRef.current = nextContent;
5732
5978
  if (id) {
5733
5979
  queryClient.setQueryData(
5734
5980
  ["action", "get-design", { id }],
@@ -5829,6 +6075,68 @@ export default function DesignEditor() {
5829
6075
  ],
5830
6076
  );
5831
6077
 
6078
+ const handleComponentPropApplied = useCallback(
6079
+ (fileId: string, nextContent: string) => {
6080
+ applyFileContentUpdate(fileId, nextContent, {
6081
+ refreshPreview: fileId === activeFile?.id,
6082
+ });
6083
+ },
6084
+ [activeFile?.id, applyFileContentUpdate],
6085
+ );
6086
+
6087
+ const handleReviewFixApplied = useCallback(
6088
+ (
6089
+ _finding: A11yFinding,
6090
+ result?: { fileId?: string; patchedContent?: string },
6091
+ ) => {
6092
+ setReviewFindings((prev) =>
6093
+ prev.filter((finding) => finding.id !== _finding.id),
6094
+ );
6095
+ if (
6096
+ typeof result?.fileId === "string" &&
6097
+ typeof result.patchedContent === "string"
6098
+ ) {
6099
+ applyFileContentUpdate(result.fileId, result.patchedContent, {
6100
+ refreshPreview: result.fileId === activeFile?.id,
6101
+ });
6102
+ }
6103
+ void handleRunDesignAudit();
6104
+ },
6105
+ [activeFile?.id, applyFileContentUpdate, handleRunDesignAudit],
6106
+ );
6107
+
6108
+ const resolvedReviewPanelProps = useMemo<
6109
+ Omit<ReviewPanelProps, "className"> | undefined
6110
+ >(() => {
6111
+ if (!id || !activeFile) return undefined;
6112
+ const reviewMatchesActiveFile = reviewFileId === activeFile.id;
6113
+ return {
6114
+ findings: reviewMatchesActiveFile ? reviewFindings : [],
6115
+ auditLoading: reviewMatchesActiveFile ? reviewAuditLoading : false,
6116
+ auditedAt: reviewMatchesActiveFile ? reviewAuditedAt : null,
6117
+ auditError: reviewMatchesActiveFile ? reviewAuditError : null,
6118
+ onRunAudit: handleRunDesignAudit,
6119
+ onFindingClick: handleReviewFindingClick,
6120
+ fixSource: {
6121
+ designId: id,
6122
+ fileId: activeFile.id,
6123
+ filename: activeFile.filename,
6124
+ },
6125
+ onFixApplied: handleReviewFixApplied,
6126
+ };
6127
+ }, [
6128
+ activeFile,
6129
+ handleReviewFindingClick,
6130
+ handleReviewFixApplied,
6131
+ handleRunDesignAudit,
6132
+ id,
6133
+ reviewAuditError,
6134
+ reviewAuditLoading,
6135
+ reviewAuditedAt,
6136
+ reviewFileId,
6137
+ reviewFindings,
6138
+ ]);
6139
+
5832
6140
  const handleCreatePrimitive = useCallback(
5833
6141
  (screenId: string, primitive: CanvasPrimitiveInsert) => {
5834
6142
  if (!canEditDesign) return false;
@@ -6321,6 +6629,15 @@ export default function DesignEditor() {
6321
6629
  mode,
6322
6630
  activeTool,
6323
6631
  tweakValues: tweakSelections,
6632
+ onShaderFillPreview: (_descriptor, css) => {
6633
+ setShaderFillPreview({
6634
+ selector: selectedElement?.selector ?? undefined,
6635
+ nodeId:
6636
+ selectedElement?.sourceId ?? selectedCodeLayerNode?.id ?? undefined,
6637
+ css,
6638
+ });
6639
+ },
6640
+ onShaderFillPreviewClear: () => setShaderFillPreview(null),
6324
6641
  }),
6325
6642
  [
6326
6643
  activeFile?.filename,
@@ -6333,6 +6650,7 @@ export default function DesignEditor() {
6333
6650
  mode,
6334
6651
  overviewSelectedScreenIds,
6335
6652
  selectedElement,
6653
+ selectedCodeLayerNode?.id,
6336
6654
  selectedScreenIds,
6337
6655
  tweakSelections,
6338
6656
  viewMode,
@@ -6557,7 +6875,10 @@ export default function DesignEditor() {
6557
6875
  // advance lastLocalContentRef.current to resolvedNextContent below, the
6558
6876
  // next synchronous call reads the previous call's result and the patches
6559
6877
  // compose. Falls back to activeContent when the ref is unset (file switch).
6560
- const baseContent = lastLocalContentRef.current ?? activeContent;
6878
+ const baseContent =
6879
+ latestActiveContentRef.current ??
6880
+ lastLocalContentRef.current ??
6881
+ activeContent;
6561
6882
  const [firstProperty, firstValue] = entries[0];
6562
6883
  const projection = buildCodeLayerProjection(baseContent);
6563
6884
  const targetInfo = options.elementInfo ?? selectedElement;
@@ -6624,8 +6945,19 @@ export default function DesignEditor() {
6624
6945
  });
6625
6946
  const sendStyleChange = (window as any).__designCanvasSendStyle;
6626
6947
  if (!options.runtimeApplied && typeof sendStyleChange === "function") {
6948
+ const selectorCandidates = targetNode
6949
+ ? codeLayerSelectorAliases(targetNode)
6950
+ : selector
6951
+ ? [selector]
6952
+ : [];
6953
+ const nodeId = targetNode
6954
+ ? bridgeSourceIdForCodeLayerNode(targetNode)
6955
+ : targetInfo?.sourceId;
6627
6956
  entries.forEach(([property, value]) => {
6628
- sendStyleChange(selector, property, value);
6957
+ sendStyleChange(selector, property, value, {
6958
+ selectorCandidates,
6959
+ nodeId,
6960
+ });
6629
6961
  });
6630
6962
  }
6631
6963
 
@@ -6661,16 +6993,19 @@ export default function DesignEditor() {
6661
6993
  if (current.failed) return current;
6662
6994
  // Try responsive-class path first when appropriate.
6663
6995
  if (hasResponsiveCapability && activeBreakpointPrefix) {
6664
- const rcPatch = applyVisualEdit(current.content, {
6665
- kind: "responsive-class",
6666
- target: targetNode ? { nodeId: targetNode.id } : { selector },
6667
- prefix: activeBreakpointPrefix,
6668
- operation: "replace",
6669
- utility: value,
6670
- stem: property,
6671
- });
6672
- if (rcPatch.result.status === "applied") {
6673
- return { content: rcPatch.content, failed: null };
6996
+ const utility = value.trim();
6997
+ if (responsiveUtilityMatchesStyleProperty(property, utility)) {
6998
+ const rcPatch = applyVisualEdit(current.content, {
6999
+ kind: "responsive-class",
7000
+ target: targetNode ? { nodeId: targetNode.id } : { selector },
7001
+ prefix: activeBreakpointPrefix,
7002
+ operation: "replace",
7003
+ utility,
7004
+ stem: utilityStem(utility),
7005
+ });
7006
+ if (rcPatch.result.status === "applied") {
7007
+ return { content: rcPatch.content, failed: null };
7008
+ }
6674
7009
  }
6675
7010
  // Responsive-class path didn't apply (e.g. value is a raw CSS value,
6676
7011
  // not a Tailwind utility); fall through to the inline-style path.
@@ -6744,6 +7079,7 @@ export default function DesignEditor() {
6744
7079
  // Mark as our own write so the get-design reconcile + Yjs observe don't
6745
7080
  // treat the echo as an external edit and fight the live value.
6746
7081
  lastLocalContentRef.current = resolvedNextContent;
7082
+ latestActiveContentRef.current = resolvedNextContent;
6747
7083
  // Write the edit into the shared Y.Doc so other open clients see it live
6748
7084
  // through Yjs (not only via the slower update-file → applyText round-trip).
6749
7085
  // Use LOCAL_EDIT_ORIGIN so the UndoManager captures this transaction.
@@ -6803,7 +7139,10 @@ export default function DesignEditor() {
6803
7139
  ) {
6804
7140
  const sendStyleChange = (window as any).__designCanvasSendStyle;
6805
7141
  if (typeof sendStyleChange === "function") {
6806
- sendStyleChange(selector, property, value);
7142
+ sendStyleChange(selector, property, value, {
7143
+ selectorCandidates: selectedCanvasSelectorCandidates,
7144
+ nodeId: selectedElement?.sourceId,
7145
+ });
6807
7146
  return;
6808
7147
  }
6809
7148
  }
@@ -6812,6 +7151,8 @@ export default function DesignEditor() {
6812
7151
  [
6813
7152
  commitVisualStyles,
6814
7153
  selectedElement?.selector,
7154
+ selectedElement?.sourceId,
7155
+ selectedCanvasSelectorCandidates,
6815
7156
  textEditingState.active,
6816
7157
  textEditingState.hasRange,
6817
7158
  textEditingState.selector,
@@ -6830,6 +7171,16 @@ export default function DesignEditor() {
6830
7171
  [commitVisualStyles, selectedElement?.selector],
6831
7172
  );
6832
7173
 
7174
+ const getFreshActiveContent = useCallback(
7175
+ () =>
7176
+ getFreshActiveFileContent({
7177
+ activeContent,
7178
+ latestContent: latestActiveContentRef.current,
7179
+ lastLocalContent: lastLocalContentRef.current,
7180
+ }),
7181
+ [activeContent],
7182
+ );
7183
+
6833
7184
  const handleVisualStyleChange = useCallback(
6834
7185
  (
6835
7186
  selector: string,
@@ -6858,7 +7209,8 @@ export default function DesignEditor() {
6858
7209
  ) => {
6859
7210
  if (!canEditDesign) return false;
6860
7211
  if (!activeFile) return false;
6861
- const projection = buildCodeLayerProjection(activeContent);
7212
+ const baseContent = getFreshActiveContent();
7213
+ const projection = buildCodeLayerProjection(baseContent);
6862
7214
  const resolveBridgeNode = (targetSelector: string, sourceId?: string) =>
6863
7215
  resolveCodeLayerNodeFromBridge(projection, targetSelector, sourceId);
6864
7216
  const targetInfo = elementInfo
@@ -6875,7 +7227,7 @@ export default function DesignEditor() {
6875
7227
  anchorSelector,
6876
7228
  details?.anchorSourceId,
6877
7229
  );
6878
- const patch = applyVisualEdit(activeContent, {
7230
+ const patch = applyVisualEdit(baseContent, {
6879
7231
  kind: "moveNode",
6880
7232
  target: targetNode ? { nodeId: targetNode.id } : { selector },
6881
7233
  anchor: anchorNode
@@ -6923,7 +7275,13 @@ export default function DesignEditor() {
6923
7275
  }
6924
7276
  return true;
6925
7277
  },
6926
- [activeContent, activeFile, applyLocalContentUpdate, canEditDesign, t],
7278
+ [
7279
+ activeFile,
7280
+ applyLocalContentUpdate,
7281
+ canEditDesign,
7282
+ getFreshActiveContent,
7283
+ t,
7284
+ ],
6927
7285
  );
6928
7286
 
6929
7287
  const handleVisualDuplicateChange = useCallback(
@@ -6940,7 +7298,8 @@ export default function DesignEditor() {
6940
7298
  ) => {
6941
7299
  if (!canEditDesign) return false;
6942
7300
  if (!activeFile) return false;
6943
- const projection = buildCodeLayerProjection(activeContent);
7301
+ const baseContent = getFreshActiveContent();
7302
+ const projection = buildCodeLayerProjection(baseContent);
6944
7303
  const targetInfo = elementInfo
6945
7304
  ? {
6946
7305
  ...elementInfo,
@@ -6960,7 +7319,7 @@ export default function DesignEditor() {
6960
7319
  details?.anchorSelector,
6961
7320
  details?.anchorSourceId,
6962
7321
  );
6963
- const nextContent = insertClonedHtmlLayer(activeContent, cloneHtml, {
7322
+ const nextContent = insertClonedHtmlLayer(baseContent, cloneHtml, {
6964
7323
  targetSelectors: targetNode
6965
7324
  ? codeLayerSelectorAliases(targetNode)
6966
7325
  : [selector],
@@ -6994,7 +7353,13 @@ export default function DesignEditor() {
6994
7353
  }
6995
7354
  return true;
6996
7355
  },
6997
- [activeContent, activeFile, applyLocalContentUpdate, canEditDesign, t],
7356
+ [
7357
+ activeFile,
7358
+ applyLocalContentUpdate,
7359
+ canEditDesign,
7360
+ getFreshActiveContent,
7361
+ t,
7362
+ ],
6998
7363
  );
6999
7364
 
7000
7365
  const handleTextContentChange = useCallback(
@@ -7006,7 +7371,8 @@ export default function DesignEditor() {
7006
7371
  ) => {
7007
7372
  if (!canEditDesign) return;
7008
7373
  if (!activeFile) return;
7009
- const projection = buildCodeLayerProjection(activeContent);
7374
+ const baseContent = getFreshActiveContent();
7375
+ const projection = buildCodeLayerProjection(baseContent);
7010
7376
  const targetInfo = elementInfo ? { ...elementInfo, selector } : null;
7011
7377
  const targetNode = targetInfo
7012
7378
  ? resolveCodeLayerNodeFromElementInfo(projection, targetInfo)
@@ -7014,10 +7380,10 @@ export default function DesignEditor() {
7014
7380
  const isEmpty = value.trim().length === 0;
7015
7381
  const removedContent =
7016
7382
  isEmpty && targetNode
7017
- ? removeCodeLayerNodeFromHtml(activeContent, targetNode)
7383
+ ? removeCodeLayerNodeFromHtml(baseContent, targetNode)
7018
7384
  : null;
7019
7385
  const patch = !removedContent
7020
- ? applyVisualEdit(activeContent, {
7386
+ ? applyVisualEdit(baseContent, {
7021
7387
  kind: "textContent",
7022
7388
  target: targetNode ? { nodeId: targetNode.id } : { selector },
7023
7389
  value,
@@ -7027,12 +7393,7 @@ export default function DesignEditor() {
7027
7393
  const nextContent =
7028
7394
  removedContent ??
7029
7395
  (patch?.result.status === "applied" ? patch.content : null) ??
7030
- updateElementContentInHtml(
7031
- activeContent,
7032
- selector,
7033
- value,
7034
- details?.html,
7035
- );
7396
+ updateElementContentInHtml(baseContent, selector, value, details?.html);
7036
7397
  if (!nextContent) {
7037
7398
  toast.error(
7038
7399
  codeLayerPatchMessage(
@@ -7081,7 +7442,13 @@ export default function DesignEditor() {
7081
7442
  : previous;
7082
7443
  });
7083
7444
  },
7084
- [activeContent, activeFile, applyLocalContentUpdate, canEditDesign, t],
7445
+ [
7446
+ activeFile,
7447
+ applyLocalContentUpdate,
7448
+ canEditDesign,
7449
+ getFreshActiveContent,
7450
+ t,
7451
+ ],
7085
7452
  );
7086
7453
 
7087
7454
  const handleScreenVisualStyleChange = useCallback(
@@ -7402,7 +7769,10 @@ export default function DesignEditor() {
7402
7769
 
7403
7770
  const handleCopySelection = useCallback(async () => {
7404
7771
  if (!selectedElement?.selector) return;
7405
- const html = getElementOuterHtml(activeContent, selectedElement.selector);
7772
+ const html = getElementOuterHtml(
7773
+ getFreshActiveContent(),
7774
+ selectedElement.selector,
7775
+ );
7406
7776
  if (!html) return;
7407
7777
  copiedLayerHtmlRef.current = html;
7408
7778
  pasteCascadeRef.current = 0;
@@ -7412,11 +7782,12 @@ export default function DesignEditor() {
7412
7782
  } catch {
7413
7783
  toast.error(t("designEditor.toasts.clipboardBlocked"));
7414
7784
  }
7415
- }, [activeContent, selectedElement, t]);
7785
+ }, [getFreshActiveContent, selectedElement, t]);
7416
7786
 
7417
7787
  const handlePasteSelection = useCallback(
7418
7788
  (position?: { x: number; y: number }) => {
7419
7789
  if (!activeFile || !canEditDesign || !copiedLayerHtmlRef.current) return;
7790
+ const baseContent = getFreshActiveContent();
7420
7791
 
7421
7792
  // B7 fix: when an element is selected and no explicit canvas position was
7422
7793
  // given, insert the clone as an in-flow sibling right AFTER the selected
@@ -7451,7 +7822,7 @@ export default function DesignEditor() {
7451
7822
  }
7452
7823
  })();
7453
7824
 
7454
- const nextContent = insertClonedHtmlLayer(activeContent, strippedHtml, {
7825
+ const nextContent = insertClonedHtmlLayer(baseContent, strippedHtml, {
7455
7826
  targetSelectors: [selector],
7456
7827
  placement: "after",
7457
7828
  });
@@ -7476,7 +7847,7 @@ export default function DesignEditor() {
7476
7847
  : { x: 120 + offset, y: 120 + offset };
7477
7848
  })();
7478
7849
  const nextContent = cloneHtmlLayerAtPosition(
7479
- activeContent,
7850
+ baseContent,
7480
7851
  copiedLayerHtmlRef.current,
7481
7852
  targetPosition,
7482
7853
  );
@@ -7485,10 +7856,10 @@ export default function DesignEditor() {
7485
7856
  applyLocalContentUpdate(nextContent);
7486
7857
  },
7487
7858
  [
7488
- activeContent,
7489
7859
  activeFile,
7490
7860
  applyLocalContentUpdate,
7491
7861
  canEditDesign,
7862
+ getFreshActiveContent,
7492
7863
  selectedCanvasSelector,
7493
7864
  selectedElement,
7494
7865
  ],
@@ -7496,10 +7867,11 @@ export default function DesignEditor() {
7496
7867
 
7497
7868
  const handlePasteOverSelection = useCallback(() => {
7498
7869
  if (!activeFile || !copiedLayerHtmlRef.current) return;
7870
+ const baseContent = getFreshActiveContent();
7499
7871
  if (selectedElement?.boundingRect) {
7500
7872
  const { x, y } = selectedElement.boundingRect;
7501
7873
  const nextContent = cloneHtmlLayerAtPosition(
7502
- activeContent,
7874
+ baseContent,
7503
7875
  copiedLayerHtmlRef.current,
7504
7876
  { x, y },
7505
7877
  );
@@ -7509,9 +7881,9 @@ export default function DesignEditor() {
7509
7881
  handlePasteSelection();
7510
7882
  }
7511
7883
  }, [
7512
- activeContent,
7513
7884
  activeFile,
7514
7885
  applyLocalContentUpdate,
7886
+ getFreshActiveContent,
7515
7887
  handlePasteSelection,
7516
7888
  selectedElement,
7517
7889
  ]);
@@ -7519,7 +7891,8 @@ export default function DesignEditor() {
7519
7891
  const handleDuplicateSelection = useCallback(() => {
7520
7892
  if (!canEditDesign) return;
7521
7893
  if (selectedElement?.selector) {
7522
- const html = getElementOuterHtml(activeContent, selectedElement.selector);
7894
+ const baseContent = getFreshActiveContent();
7895
+ const html = getElementOuterHtml(baseContent, selectedElement.selector);
7523
7896
  if (!html) {
7524
7897
  toast.error(t("designEditor.toasts.duplicateElementFailed"));
7525
7898
  return;
@@ -7550,7 +7923,7 @@ export default function DesignEditor() {
7550
7923
  return html;
7551
7924
  }
7552
7925
  })();
7553
- const nextContent = insertClonedHtmlLayer(activeContent, strippedHtml, {
7926
+ const nextContent = insertClonedHtmlLayer(baseContent, strippedHtml, {
7554
7927
  targetSelectors: [selector],
7555
7928
  placement: "after",
7556
7929
  });
@@ -7563,10 +7936,10 @@ export default function DesignEditor() {
7563
7936
  }
7564
7937
  if (activeFile) handleDuplicateScreen(activeFile.id);
7565
7938
  }, [
7566
- activeContent,
7567
7939
  activeFile,
7568
7940
  applyLocalContentUpdate,
7569
7941
  canEditDesign,
7942
+ getFreshActiveContent,
7570
7943
  handleDuplicateScreen,
7571
7944
  selectedCanvasSelector,
7572
7945
  selectedElement,
@@ -7575,6 +7948,7 @@ export default function DesignEditor() {
7575
7948
 
7576
7949
  const handleDeleteSelection = useCallback(() => {
7577
7950
  if (!canEditDesign) return;
7951
+ const baseContent = getFreshActiveContent();
7578
7952
  // Multi-select delete: when several DOM/code layers are selected in the
7579
7953
  // panel, remove all of them — not just the single focused element. Compose
7580
7954
  // the removals against the running content (re-projecting each pass) so
@@ -7586,7 +7960,7 @@ export default function DesignEditor() {
7586
7960
  !files.some((file) => file.id === layerId),
7587
7961
  );
7588
7962
  if (candidateIds.length > 1) {
7589
- let content = activeContent;
7963
+ let content = baseContent;
7590
7964
  const removedSelectors: string[] = [];
7591
7965
  for (const layerId of candidateIds) {
7592
7966
  const projection = buildCodeLayerProjection(content);
@@ -7600,7 +7974,7 @@ export default function DesignEditor() {
7600
7974
  if (selector) removedSelectors.push(selector);
7601
7975
  content = next;
7602
7976
  }
7603
- if (content !== activeContent) {
7977
+ if (content !== baseContent) {
7604
7978
  removedSelectors.forEach((selector) => deleteRuntimeElement(selector));
7605
7979
  applyLocalContentUpdate(content, { refreshPreview: false });
7606
7980
  setSelectedElement(null);
@@ -7611,27 +7985,26 @@ export default function DesignEditor() {
7611
7985
  }
7612
7986
 
7613
7987
  if (!selectedElement?.selector) return;
7614
- const projection = buildCodeLayerProjection(activeContent);
7988
+ const projection = buildCodeLayerProjection(baseContent);
7615
7989
  const targetNode = resolveCodeLayerNodeFromElementInfo(
7616
7990
  projection,
7617
7991
  selectedElement,
7618
7992
  );
7619
7993
  const nextContent =
7620
7994
  (targetNode
7621
- ? removeCodeLayerNodeFromHtml(activeContent, targetNode)
7622
- : null) ??
7623
- removeElementFromHtml(activeContent, selectedElement.selector);
7995
+ ? removeCodeLayerNodeFromHtml(baseContent, targetNode)
7996
+ : null) ?? removeElementFromHtml(baseContent, selectedElement.selector);
7624
7997
  if (!nextContent) return;
7625
7998
  deleteRuntimeElement(selectedElement.selector);
7626
7999
  applyLocalContentUpdate(nextContent, { refreshPreview: false });
7627
8000
  setSelectedElement(null);
7628
8001
  setSelectedLayerIdsState([]);
7629
8002
  }, [
7630
- activeContent,
7631
8003
  applyLocalContentUpdate,
7632
8004
  canEditDesign,
7633
8005
  deleteRuntimeElement,
7634
8006
  files,
8007
+ getFreshActiveContent,
7635
8008
  selectedElement,
7636
8009
  selectedLayerIdsState,
7637
8010
  ]);
@@ -7639,6 +8012,7 @@ export default function DesignEditor() {
7639
8012
  // Wrap the current multi-layer selection into a new group container.
7640
8013
  const handleGroupSelection = useCallback(() => {
7641
8014
  if (!canEditDesign || !activeFile) return;
8015
+ const baseContent = getFreshActiveContent();
7642
8016
  // Collect the DOM-node layer ids that belong to the active screen.
7643
8017
  // Build a set of ids present in the active content so stale ids from
7644
8018
  // other files (which can persist in selectedLayerIdsState after a
@@ -7648,14 +8022,14 @@ export default function DesignEditor() {
7648
8022
  // selection.
7649
8023
  const fileIds = new Set(files.map((f) => f.id));
7650
8024
  const activeNodeIdSet = buildActiveFileNodeIdSet(
7651
- buildCodeLayerProjection(activeContent),
8025
+ buildCodeLayerProjection(baseContent),
7652
8026
  );
7653
8027
  const nodeIds = selectedLayerIdsState.filter(
7654
8028
  (id) =>
7655
8029
  !id.startsWith("__") && !fileIds.has(id) && activeNodeIdSet.has(id),
7656
8030
  );
7657
8031
  if (nodeIds.length < 2) return;
7658
- const patch = applyVisualEdit(activeContent, {
8032
+ const patch = applyVisualEdit(baseContent, {
7659
8033
  kind: "wrapNodes",
7660
8034
  targetIds: nodeIds,
7661
8035
  autoLayout: false,
@@ -7684,11 +8058,11 @@ export default function DesignEditor() {
7684
8058
  }
7685
8059
  }
7686
8060
  }, [
7687
- activeContent,
7688
8061
  activeFile,
7689
8062
  applyLocalContentUpdate,
7690
8063
  canEditDesign,
7691
8064
  files,
8065
+ getFreshActiveContent,
7692
8066
  selectedLayerIdsState,
7693
8067
  t,
7694
8068
  ]);
@@ -7696,12 +8070,13 @@ export default function DesignEditor() {
7696
8070
  // Unwrap the currently selected single-container layer.
7697
8071
  const handleUngroupSelection = useCallback(() => {
7698
8072
  if (!canEditDesign || !activeFile) return;
8073
+ const baseContent = getFreshActiveContent();
7699
8074
  // Filter to active-file nodes only (mirrors handleGroupSelection fix).
7700
8075
  // A stale id from another file must not be passed to unwrap or it will
7701
8076
  // fail with "conflict" even though the actual selection is valid.
7702
8077
  const fileIds = new Set(files.map((f) => f.id));
7703
8078
  const activeNodeIdSet = buildActiveFileNodeIdSet(
7704
- buildCodeLayerProjection(activeContent),
8079
+ buildCodeLayerProjection(baseContent),
7705
8080
  );
7706
8081
  const nodeIds = selectedLayerIdsState.filter(
7707
8082
  (id) =>
@@ -7709,7 +8084,7 @@ export default function DesignEditor() {
7709
8084
  );
7710
8085
  const targetId = nodeIds[0];
7711
8086
  if (!targetId) return;
7712
- const patch = applyVisualEdit(activeContent, {
8087
+ const patch = applyVisualEdit(baseContent, {
7713
8088
  kind: "unwrap",
7714
8089
  targetId,
7715
8090
  });
@@ -7727,11 +8102,11 @@ export default function DesignEditor() {
7727
8102
  setSelectedElement(null);
7728
8103
  setSelectedLayerIdsState([]);
7729
8104
  }, [
7730
- activeContent,
7731
8105
  activeFile,
7732
8106
  applyLocalContentUpdate,
7733
8107
  canEditDesign,
7734
8108
  files,
8109
+ getFreshActiveContent,
7735
8110
  selectedLayerIdsState,
7736
8111
  t,
7737
8112
  ]);
@@ -7748,7 +8123,8 @@ export default function DesignEditor() {
7748
8123
  opts?: { direction?: "row" | "column"; gap?: string },
7749
8124
  ) => {
7750
8125
  if (!canEditDesign || !activeFile) return;
7751
- const patch = applyVisualEdit(activeContent, {
8126
+ const baseContent = getFreshActiveContent();
8127
+ const patch = applyVisualEdit(baseContent, {
7752
8128
  kind: "autoLayout",
7753
8129
  targetId: targetNodeId,
7754
8130
  enabled: true,
@@ -7777,7 +8153,13 @@ export default function DesignEditor() {
7777
8153
  setSelectedElement(elementInfoFromCodeLayerNode(containerNode));
7778
8154
  }
7779
8155
  },
7780
- [activeContent, activeFile, applyLocalContentUpdate, canEditDesign, t],
8156
+ [
8157
+ activeFile,
8158
+ applyLocalContentUpdate,
8159
+ canEditDesign,
8160
+ getFreshActiveContent,
8161
+ t,
8162
+ ],
7781
8163
  );
7782
8164
 
7783
8165
  /**
@@ -9703,14 +10085,29 @@ ${serializedHtml}
9703
10085
  )
9704
10086
  .map((node) => node.id),
9705
10087
  );
10088
+ const allLayerIds = new Set([
10089
+ ...fileIds,
10090
+ ...allCodeLayerNodes.map((node) => node.id),
10091
+ ]);
9706
10092
  const reconcile = (
9707
10093
  current: Set<string>,
9708
10094
  sourceIds: Set<string>,
10095
+ kind: "hidden" | "locked",
9709
10096
  ): Set<string> => {
9710
10097
  const next = new Set(sourceIds);
9711
10098
  current.forEach((id) => {
9712
10099
  if (fileIds.has(id)) next.add(id);
9713
10100
  });
10101
+ layerStateOverridesRef.current.forEach((override, id) => {
10102
+ if (!allLayerIds.has(id)) {
10103
+ layerStateOverridesRef.current.delete(id);
10104
+ return;
10105
+ }
10106
+ const value = override[kind];
10107
+ if (value === undefined) return;
10108
+ if (value) next.add(id);
10109
+ else next.delete(id);
10110
+ });
9714
10111
  if (
9715
10112
  next.size === current.size &&
9716
10113
  Array.from(next).every((id) => current.has(id))
@@ -9720,8 +10117,12 @@ ${serializedHtml}
9720
10117
  return next;
9721
10118
  };
9722
10119
 
9723
- setLockedLayerIds((current) => reconcile(current, lockedFromSource));
9724
- setHiddenLayerIds((current) => reconcile(current, hiddenFromSource));
10120
+ setLockedLayerIds((current) =>
10121
+ reconcile(current, lockedFromSource, "locked"),
10122
+ );
10123
+ setHiddenLayerIds((current) =>
10124
+ reconcile(current, hiddenFromSource, "hidden"),
10125
+ );
9725
10126
  }, [codeLayerModelsByFile, files]);
9726
10127
  const lockedLayerSelectors = useMemo(() => {
9727
10128
  const selectors = Array.from(lockedLayerIds)
@@ -9973,7 +10374,11 @@ ${serializedHtml}
9973
10374
  const match = statesPanelBreakpoints.find(
9974
10375
  (bp) => bp.widthPx === activeBreakpointWidthState,
9975
10376
  );
9976
- return match?.id ?? "auto";
10377
+ if (match) return match.id;
10378
+ const defaultMatch = DEFAULT_STATES_PANEL_BREAKPOINTS.find(
10379
+ (bp) => bp.widthPx === activeBreakpointWidthState,
10380
+ );
10381
+ return defaultMatch?.id ?? "auto";
9977
10382
  }, [activeBreakpointWidthState, statesPanelBreakpoints]);
9978
10383
 
9979
10384
  const handleOpenDesignPreview = useCallback(() => {
@@ -10090,10 +10495,11 @@ ${serializedHtml}
10090
10495
  ) {
10091
10496
  return;
10092
10497
  }
10498
+ const freshActiveContent = getFreshActiveContent();
10093
10499
  const destFile = files.find((file) => file.id === targetOwner.fileId);
10094
10500
  const destContent =
10095
10501
  targetOwner.fileId === activeFile?.id
10096
- ? activeContent
10502
+ ? freshActiveContent
10097
10503
  : (destFile?.content ?? "");
10098
10504
  if (!destContent) return;
10099
10505
 
@@ -10124,10 +10530,26 @@ ${serializedHtml}
10124
10530
  }
10125
10531
  }
10126
10532
 
10533
+ const targetTreeForMove =
10534
+ targetOwner.fileId === activeFile?.id
10535
+ ? buildCodeLayerTree(buildCodeLayerProjection(freshActiveContent))
10536
+ : targetOwner.tree;
10537
+ const orderedSameFileDragIds = sortCodeLayerIdsByTreeOrder(
10538
+ sameFileDragIds,
10539
+ targetTreeForMove,
10540
+ );
10541
+ const movedIdOrder = [
10542
+ ...orderedSameFileDragIds,
10543
+ ...crossFileDrags.map((drag) => drag.draggedId),
10544
+ ];
10545
+
10127
10546
  // --- Same-file moves (existing path) ---
10128
10547
  let nextDestContent = destContent;
10129
10548
  let moved = false;
10130
- for (const draggedId of sameFileDragIds) {
10549
+ for (const draggedId of getLayerMoveIterationOrder(
10550
+ orderedSameFileDragIds,
10551
+ intent.placement,
10552
+ )) {
10131
10553
  const patch = applyVisualEdit(nextDestContent, {
10132
10554
  kind: "moveNode",
10133
10555
  target: { nodeId: draggedId },
@@ -10158,7 +10580,7 @@ ${serializedHtml}
10158
10580
  const currentSourceContent = getLayerMoveSourceContent({
10159
10581
  sourceFileId,
10160
10582
  activeFileId: activeFile?.id,
10161
- activeContent,
10583
+ activeContent: freshActiveContent,
10162
10584
  sourceFileContent: srcFile.content,
10163
10585
  sourceContentMap,
10164
10586
  });
@@ -10207,7 +10629,7 @@ ${serializedHtml}
10207
10629
  const finalDestTree = finalDestProjection
10208
10630
  ? buildCodeLayerTree(finalDestProjection)
10209
10631
  : [];
10210
- const movedNodesAfterMove = intent.draggedIds
10632
+ const movedNodesAfterMove = movedIdOrder
10211
10633
  .map((draggedId) => movedNodeSnapshots.get(draggedId))
10212
10634
  .map((node) =>
10213
10635
  node && finalDestProjection
@@ -10249,14 +10671,14 @@ ${serializedHtml}
10249
10671
  }
10250
10672
  },
10251
10673
  [
10252
- activeContent,
10253
10674
  activeFile?.id,
10254
10675
  applyFileContentUpdate,
10255
10676
  canEditDesign,
10256
10677
  canMoveLayer,
10257
10678
  codeLayerOwnerByNodeId,
10258
- files,
10259
10679
  effectiveCodeLayerState,
10680
+ files,
10681
+ getFreshActiveContent,
10260
10682
  t,
10261
10683
  ],
10262
10684
  );
@@ -10406,7 +10828,7 @@ ${serializedHtml}
10406
10828
  const sourceFile = files.find((file) => file.id === owner.fileId);
10407
10829
  const sourceContent =
10408
10830
  owner.fileId === activeFile?.id
10409
- ? activeContent
10831
+ ? getFreshActiveContent()
10410
10832
  : (sourceFile?.content ?? "");
10411
10833
  if (!sourceContent) return;
10412
10834
  const nextContent = setCodeLayerAttributeInHtml(
@@ -10422,12 +10844,12 @@ ${serializedHtml}
10422
10844
  setSelectedLayerIdsState([layerId]);
10423
10845
  },
10424
10846
  [
10425
- activeContent,
10426
10847
  activeFile?.id,
10427
10848
  applyFileContentUpdate,
10428
10849
  canEditDesign,
10429
10850
  codeLayerOwnerByNodeId,
10430
10851
  files,
10852
+ getFreshActiveContent,
10431
10853
  queryClient,
10432
10854
  t,
10433
10855
  updateFileMutation,
@@ -10437,6 +10859,10 @@ ${serializedHtml}
10437
10859
  const handleToggleLayerLocked = useCallback(
10438
10860
  (layerId: string, locked: boolean) => {
10439
10861
  if (!canEditDesign) return;
10862
+ layerStateOverridesRef.current.set(layerId, {
10863
+ ...layerStateOverridesRef.current.get(layerId),
10864
+ locked,
10865
+ });
10440
10866
  const applyLockedState = () => {
10441
10867
  setLockedLayerIds((current) => {
10442
10868
  const next = new Set(current);
@@ -10451,38 +10877,47 @@ ${serializedHtml}
10451
10877
  }
10452
10878
  const owner = codeLayerOwnerByNodeId.get(layerId);
10453
10879
  const node = owner?.node;
10454
- if (!owner || !node) return;
10880
+ if (!owner || !node) {
10881
+ applyLockedState();
10882
+ return;
10883
+ }
10455
10884
  const sourceFile = files.find((file) => file.id === owner.fileId);
10456
10885
  const sourceContent =
10457
10886
  owner.fileId === activeFile?.id
10458
- ? activeContent
10887
+ ? getFreshActiveContent()
10459
10888
  : (sourceFile?.content ?? "");
10460
- if (!sourceContent) return;
10461
- const nextContent = setCodeLayerAttributeInHtml(
10462
- sourceContent,
10463
- node,
10464
- "data-agent-native-locked",
10465
- locked ? "true" : null,
10466
- );
10467
- if (!nextContent || nextContent === sourceContent) return;
10468
- applyFileContentUpdate(owner.fileId, nextContent, {
10469
- refreshPreview: false,
10470
- });
10889
+ if (sourceContent) {
10890
+ const nextContent = setCodeLayerAttributeInHtml(
10891
+ sourceContent,
10892
+ node,
10893
+ "data-agent-native-locked",
10894
+ locked ? "true" : null,
10895
+ );
10896
+ if (nextContent && nextContent !== sourceContent) {
10897
+ applyFileContentUpdate(owner.fileId, nextContent, {
10898
+ refreshPreview: false,
10899
+ });
10900
+ }
10901
+ }
10471
10902
  applyLockedState();
10472
10903
  },
10473
10904
  [
10474
- activeContent,
10475
10905
  activeFile?.id,
10476
10906
  applyFileContentUpdate,
10477
10907
  canEditDesign,
10478
10908
  codeLayerOwnerByNodeId,
10479
10909
  files,
10910
+ getFreshActiveContent,
10480
10911
  ],
10481
10912
  );
10482
10913
 
10483
10914
  const handleToggleLayerHidden = useCallback(
10484
10915
  (layerId: string, hidden: boolean) => {
10485
10916
  if (!canEditDesign) return;
10917
+ layerStateOverridesRef.current.set(layerId, {
10918
+ ...layerStateOverridesRef.current.get(layerId),
10919
+ hidden,
10920
+ });
10486
10921
  const applyHiddenState = () => {
10487
10922
  setHiddenLayerIds((current) => {
10488
10923
  const next = new Set(current);
@@ -10497,32 +10932,37 @@ ${serializedHtml}
10497
10932
  }
10498
10933
  const owner = codeLayerOwnerByNodeId.get(layerId);
10499
10934
  const node = owner?.node;
10500
- if (!owner || !node) return;
10935
+ if (!owner || !node) {
10936
+ applyHiddenState();
10937
+ return;
10938
+ }
10501
10939
  const sourceFile = files.find((file) => file.id === owner.fileId);
10502
10940
  const sourceContent =
10503
10941
  owner.fileId === activeFile?.id
10504
- ? activeContent
10942
+ ? getFreshActiveContent()
10505
10943
  : (sourceFile?.content ?? "");
10506
- if (!sourceContent) return;
10507
- const nextContent = setCodeLayerAttributeInHtml(
10508
- sourceContent,
10509
- node,
10510
- "data-agent-native-hidden",
10511
- hidden ? "true" : null,
10512
- );
10513
- if (!nextContent || nextContent === sourceContent) return;
10514
- applyFileContentUpdate(owner.fileId, nextContent, {
10515
- refreshPreview: false,
10516
- });
10944
+ if (sourceContent) {
10945
+ const nextContent = setCodeLayerAttributeInHtml(
10946
+ sourceContent,
10947
+ node,
10948
+ "data-agent-native-hidden",
10949
+ hidden ? "true" : null,
10950
+ );
10951
+ if (nextContent && nextContent !== sourceContent) {
10952
+ applyFileContentUpdate(owner.fileId, nextContent, {
10953
+ refreshPreview: false,
10954
+ });
10955
+ }
10956
+ }
10517
10957
  applyHiddenState();
10518
10958
  },
10519
10959
  [
10520
- activeContent,
10521
10960
  activeFile?.id,
10522
10961
  applyFileContentUpdate,
10523
10962
  canEditDesign,
10524
10963
  codeLayerOwnerByNodeId,
10525
10964
  files,
10965
+ getFreshActiveContent,
10526
10966
  ],
10527
10967
  );
10528
10968
 
@@ -11864,11 +12304,8 @@ ${serializedHtml}
11864
12304
  sourceType={designSourceType}
11865
12305
  fusionUrl={designFusionUrl}
11866
12306
  previewWidthPx={activeBreakpointWidthState}
12307
+ shaderFillPreview={shaderFillPreview}
11867
12308
  onComponentSourceJump={handleComponentSourceJump}
11868
- // TODO: thread motionTracks / shaderFillPreview live-preview
11869
- // props once the canvas-side live preview state is plumbed
11870
- // here (non-trivial new state; MotionDock already drives
11871
- // motion-preview directly via canvasIframeRef for now).
11872
12309
  motionTracks={motionTracksWire}
11873
12310
  editMode={mode === "edit"}
11874
12311
  interactMode={mode === "interact"}
@@ -12036,6 +12473,51 @@ ${serializedHtml}
12036
12473
  tweakValues={tweakSelections}
12037
12474
  extensionContext={designExtensionContext}
12038
12475
  readOnly={initialGenerationReadOnly}
12476
+ onComponentPropApplied={handleComponentPropApplied}
12477
+ onTokensApplied={(resolvedCssVars) => {
12478
+ if (!canEditDesign || !id) return;
12479
+ setTweakSelections((prev) => ({
12480
+ ...prev,
12481
+ ...resolvedCssVars,
12482
+ }));
12483
+ queryClient.setQueryData(
12484
+ ["action", "get-design", { id }],
12485
+ (old: any) => {
12486
+ if (!old || typeof old !== "object") return old;
12487
+ let currentData: Record<string, unknown> = {};
12488
+ if (typeof old.data === "string" && old.data) {
12489
+ try {
12490
+ const parsed = JSON.parse(old.data);
12491
+ if (
12492
+ parsed &&
12493
+ typeof parsed === "object" &&
12494
+ !Array.isArray(parsed)
12495
+ ) {
12496
+ currentData = parsed;
12497
+ }
12498
+ } catch {
12499
+ currentData = {};
12500
+ }
12501
+ }
12502
+ const currentSelections =
12503
+ currentData.tweakSelections &&
12504
+ typeof currentData.tweakSelections === "object" &&
12505
+ !Array.isArray(currentData.tweakSelections)
12506
+ ? currentData.tweakSelections
12507
+ : {};
12508
+ return {
12509
+ ...old,
12510
+ data: JSON.stringify({
12511
+ ...currentData,
12512
+ tweakSelections: {
12513
+ ...currentSelections,
12514
+ ...resolvedCssVars,
12515
+ },
12516
+ }),
12517
+ };
12518
+ },
12519
+ );
12520
+ }}
12039
12521
  onTweakChange={(tweakId, value) =>
12040
12522
  setTweakSelections((prev) => {
12041
12523
  if (!canEditDesign) return prev;
@@ -12068,9 +12550,7 @@ ${serializedHtml}
12068
12550
  activeStateId: selectedStateId,
12069
12551
  activeBreakpointId: statesPanelActiveBreakpointId,
12070
12552
  breakpoints: statesPanelBreakpoints,
12071
- onStateSelect: (stateId) => {
12072
- setSelectedStateId(stateId);
12073
- },
12553
+ onStateSelect: handleDesignStateSelect,
12074
12554
  onBreakpointSelect: (breakpointId) => {
12075
12555
  // "auto" = clear the active breakpoint (overview).
12076
12556
  if (breakpointId === "auto") {
@@ -12083,9 +12563,13 @@ ${serializedHtml}
12083
12563
  }
12084
12564
  return;
12085
12565
  }
12086
- const bp = statesPanelBreakpoints.find(
12087
- (b) => b.id === breakpointId,
12088
- );
12566
+ const bp =
12567
+ statesPanelBreakpoints.find(
12568
+ (b) => b.id === breakpointId,
12569
+ ) ??
12570
+ DEFAULT_STATES_PANEL_BREAKPOINTS.find(
12571
+ (b) => b.id === breakpointId,
12572
+ );
12089
12573
  if (!bp) return;
12090
12574
  setActiveBreakpointWidthState(bp.widthPx);
12091
12575
  if (id) {
@@ -12105,7 +12589,7 @@ ${serializedHtml}
12105
12589
  }
12106
12590
  : undefined
12107
12591
  }
12108
- reviewPanelProps={reviewPanelProps}
12592
+ reviewPanelProps={resolvedReviewPanelProps}
12109
12593
  />
12110
12594
  </div>
12111
12595
  ) : (
@@ -12132,12 +12616,33 @@ ${serializedHtml}
12132
12616
  selectedTarget={motionSelectedTarget}
12133
12617
  onApply={(tracks, durationMs) => {
12134
12618
  if (!id) return;
12135
- applyMotionEditMutation.mutate({
12136
- designId: id,
12137
- fileId: activeFile.id,
12138
- tracks: tracks.map(({ label: _label, ...t }) => t),
12139
- durationMs,
12140
- });
12619
+ applyMotionEditMutation.mutate(
12620
+ {
12621
+ designId: id,
12622
+ fileId: activeFile.id,
12623
+ tracks: tracks.map(({ label: _label, ...t }) => t),
12624
+ durationMs,
12625
+ includeContent: true,
12626
+ },
12627
+ {
12628
+ onSuccess: (result) => {
12629
+ const response = result as {
12630
+ fileId?: unknown;
12631
+ patchedContent?: unknown;
12632
+ };
12633
+ if (
12634
+ typeof response.fileId === "string" &&
12635
+ typeof response.patchedContent === "string"
12636
+ ) {
12637
+ applyFileContentUpdate(
12638
+ response.fileId,
12639
+ response.patchedContent,
12640
+ { refreshPreview: response.fileId === activeFile.id },
12641
+ );
12642
+ }
12643
+ },
12644
+ },
12645
+ );
12141
12646
  }}
12142
12647
  applying={applyMotionEditMutation.isPending}
12143
12648
  />