@agent-native/core 0.182.1 → 0.182.2-nightly-20260917225811

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 (119) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/app/root.tsx +59 -10
  3. package/corpus/templates/clips/changelog/2026-09-17-desktop-popover-closes-when-clicking-away.md +6 -0
  4. package/corpus/templates/clips/changelog/2026-09-17-self-hosted-extension-sign-in.md +6 -0
  5. package/corpus/templates/clips/chrome-extension/src/background.ts +45 -25
  6. package/corpus/templates/clips/chrome-extension/src/content-script.ts +43 -0
  7. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +55 -6
  8. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +107 -19
  9. package/corpus/templates/design/.generated/bridge/hit-test.generated.ts +1 -0
  10. package/corpus/templates/design/actions/add-localhost-screens.ts +54 -22
  11. package/corpus/templates/design/actions/apply-component-prop-edit.ts +174 -14
  12. package/corpus/templates/design/actions/apply-visual-edit.ts +3 -1
  13. package/corpus/templates/design/actions/create-component.ts +173 -12
  14. package/corpus/templates/design/actions/create-design-from-template.ts +50 -35
  15. package/corpus/templates/design/actions/create-design-version.ts +21 -0
  16. package/corpus/templates/design/actions/create-file.ts +35 -33
  17. package/corpus/templates/design/actions/delete-design.ts +29 -28
  18. package/corpus/templates/design/actions/delete-file.ts +154 -23
  19. package/corpus/templates/design/actions/generate-design.ts +28 -16
  20. package/corpus/templates/design/actions/get-component-details.ts +272 -33
  21. package/corpus/templates/design/actions/get-design-version.ts +9 -1
  22. package/corpus/templates/design/actions/go-to-main-component.ts +37 -35
  23. package/corpus/templates/design/actions/index-components.ts +241 -126
  24. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +12 -9
  25. package/corpus/templates/design/actions/present-design-variants.ts +68 -72
  26. package/corpus/templates/design/actions/rename-component.ts +294 -0
  27. package/corpus/templates/design/actions/rename-screen.ts +10 -10
  28. package/corpus/templates/design/actions/update-file.ts +327 -330
  29. package/corpus/templates/design/actions/update-screen-source.ts +2 -43
  30. package/corpus/templates/design/app/components/design/EditPanel.tsx +17 -1
  31. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +228 -24
  32. package/corpus/templates/design/app/components/design/ResponsiveInteractBar.tsx +17 -1
  33. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +151 -32
  34. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +4 -0
  35. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +1 -1
  36. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +8 -1
  37. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +55 -33
  38. package/corpus/templates/design/app/components/design/code-workbench/workspace/create-providers.ts +2 -0
  39. package/corpus/templates/design/app/components/design/code-workbench/workspace/inline-provider.ts +4 -2
  40. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +273 -60
  41. package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +9 -2
  42. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +57 -9
  43. package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +13 -0
  44. package/corpus/templates/design/app/components/design/multi-screen/types.ts +14 -0
  45. package/corpus/templates/design/app/components/design/types.ts +9 -1
  46. package/corpus/templates/design/app/hooks/use-design-versions.ts +3 -2
  47. package/corpus/templates/design/app/i18n-breakpoints.ts +0 -110
  48. package/corpus/templates/design/app/i18n-data.ts +0 -10
  49. package/corpus/templates/design/app/pages/DesignEditor.tsx +669 -161
  50. package/corpus/templates/design/app/pages/design-editor/clone-and-pen-edit.ts +20 -0
  51. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +2 -0
  52. package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +21 -3
  53. package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +2 -0
  54. package/corpus/templates/design/app/pages/design-editor/commands/create-component.ts +44 -3
  55. package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +69 -47
  56. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +157 -52
  57. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-selection.ts +76 -1
  58. package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +152 -44
  59. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +131 -2
  60. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +5 -1
  61. package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +26 -1
  62. package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +1 -0
  63. package/corpus/templates/design/app/pages/design-editor/editor-helpers.ts +58 -8
  64. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +63 -2
  65. package/corpus/templates/design/app/pages/design-editor/history.ts +3 -0
  66. package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +74 -22
  67. package/corpus/templates/design/app/pages/design-editor/source-publication.ts +14 -2
  68. package/corpus/templates/design/changelog/2026-09-16-design-components-retain-linked-identities-when-edited-and-d.md +6 -0
  69. package/corpus/templates/design/changelog/2026-09-16-fix-design-nested-layer-dragging-and-screen-duplication-pari.md +6 -0
  70. package/corpus/templates/design/changelog/2026-09-17-design-drag-and-drop-keeps-duplicates-cross-screen-moves-and.md +6 -0
  71. package/corpus/templates/design/changelog/2026-09-17-design-preserves-reusable-component-identity-and-props-acros.md +6 -0
  72. package/corpus/templates/design/changelog/2026-09-17-open-vector-paths-no-longer-show-fills-and-interact-dimensio.md +6 -0
  73. package/corpus/templates/design/changelog/2026-09-17-screen-deletion-is-immediate-and-undoable-with-history-that-.md +6 -0
  74. package/corpus/templates/design/e2e/fixtures/design-component-react/index.html +98 -0
  75. package/corpus/templates/design/e2e/fixtures/design-component-react/inline-component.html +65 -0
  76. package/corpus/templates/design/e2e/helpers.ts +6 -1
  77. package/corpus/templates/design/server/lib/design-data-mutation.ts +9 -5
  78. package/corpus/templates/design/server/lib/design-snapshot.ts +28 -16
  79. package/corpus/templates/design/server/lib/design-versions.ts +208 -130
  80. package/corpus/templates/design/server/lib/fusion-screens.ts +46 -20
  81. package/corpus/templates/design/server/lib/import-design-files.ts +5 -1
  82. package/corpus/templates/design/server/lib/localhost-connection.ts +54 -0
  83. package/corpus/templates/design/server/source-workspace.ts +412 -356
  84. package/corpus/templates/design/shared/component-library.ts +36 -6
  85. package/corpus/templates/design/shared/component-model.ts +12 -2
  86. package/corpus/templates/design/shared/local-jsx-visual-edit.ts +231 -5
  87. package/corpus/templates/design/shared/source-mode.ts +22 -0
  88. package/dist/automation/index.d.ts +1 -1
  89. package/dist/client/AssistantChat.d.ts +4 -1
  90. package/dist/client/AssistantChat.js +26 -3
  91. package/dist/client/chat/agentkit-protocol.js +148 -34
  92. package/dist/client/chat/message-components.js +7 -5
  93. package/dist/client/chat/tool-call-display.js +4 -3
  94. package/dist/client/conversation/AgentConversation.js +4 -2
  95. package/dist/client/tool-display.d.ts +9 -0
  96. package/dist/client/tool-display.js +15 -0
  97. package/dist/collab/awareness.d.ts +2 -2
  98. package/dist/collab/routes.d.ts +1 -1
  99. package/dist/localization/core-messages/ar-SA.js +2 -0
  100. package/dist/localization/core-messages/de-DE.js +2 -0
  101. package/dist/localization/core-messages/en-US.d.ts +2 -0
  102. package/dist/localization/core-messages/en-US.js +2 -0
  103. package/dist/localization/core-messages/es-ES.js +2 -0
  104. package/dist/localization/core-messages/fr-FR.js +2 -0
  105. package/dist/localization/core-messages/hi-IN.js +2 -0
  106. package/dist/localization/core-messages/ja-JP.js +2 -0
  107. package/dist/localization/core-messages/ko-KR.js +2 -0
  108. package/dist/localization/core-messages/pt-BR.js +2 -0
  109. package/dist/localization/core-messages/zh-CN.js +2 -0
  110. package/dist/localization/core-messages/zh-TW.js +2 -0
  111. package/dist/localization/core-messages.d.ts +2 -0
  112. package/dist/notifications/routes.d.ts +3 -3
  113. package/dist/observability/routes.d.ts +3 -3
  114. package/dist/resources/handlers.d.ts +1 -1
  115. package/dist/secrets/routes.d.ts +3 -3
  116. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  117. package/dist/server/core-routes-plugin.d.ts +1 -1
  118. package/package.json +3 -3
  119. package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +0 -66
@@ -60,6 +60,7 @@ import {
60
60
  TooltipTrigger,
61
61
  } from "@/components/ui/tooltip";
62
62
 
63
+ import type { LocalhostWriteConsentPayload } from "../LocalhostWriteConsentDialog";
63
64
  import { findCanvasIframeForScreen } from "../multi-screen/iframe-targeting";
64
65
  import {
65
66
  canRebuildAlpineDataLosslessly,
@@ -74,6 +75,15 @@ import {
74
75
  InspectorGridCell,
75
76
  } from "./inspector-grid";
76
77
 
78
+ function isLocalhostWriteConsentError(error: unknown): boolean {
79
+ return (
80
+ error instanceof Error &&
81
+ (error.name === "LocalWriteConsentRequiredError" ||
82
+ error.name === "WriteConsentRequiredError" ||
83
+ /write-consent grant|grant expired/i.test(error.message))
84
+ );
85
+ }
86
+
77
87
  // ─── Make it real — inline upgrade card (§3, §6.6) ──────────────────────────
78
88
 
79
89
  /**
@@ -311,6 +321,7 @@ interface ComponentDetailsResult {
311
321
  isMain?: boolean;
312
322
  canRestore?: boolean;
313
323
  observedProps: Array<{ name: string; value: string }>;
324
+ literalProps?: Array<{ name: string; value: string }>;
314
325
  persistedVariants: Record<string, string[]>;
315
326
  sourceLocation?: { filePath: string; exportName?: string } | null;
316
327
  /** Component instance shape, including the Alpine `x-data` expression. */
@@ -328,6 +339,37 @@ interface ComponentDetailsResult {
328
339
  };
329
340
  }
330
341
 
342
+ export interface ComponentLocalSource {
343
+ connectionId: string;
344
+ path: string;
345
+ line: number;
346
+ column: number;
347
+ positionPrecision?: "authored" | "transformed" | "unknown";
348
+ runtimeMultiplicity?: number;
349
+ scope?:
350
+ | "single-instance"
351
+ | "repeated-render"
352
+ | "shared-component-definition"
353
+ | "unknown";
354
+ expectedVersionHash?: string;
355
+ expectedValue?: string;
356
+ propStamps?: Array<{ name: string; value: string }>;
357
+ }
358
+
359
+ export interface RuntimeComponentDetails {
360
+ name: string;
361
+ nodeId: string;
362
+ selector: string;
363
+ props: Array<{ name: string; value: string }>;
364
+ literalProps?: Array<{ name: string; value: string }>;
365
+ alpineData?: string | null;
366
+ componentId?: string;
367
+ componentRef?: string;
368
+ isMain?: boolean;
369
+ sourceLocation?: { filePath: string; exportName?: string };
370
+ local?: ComponentLocalSource;
371
+ }
372
+
331
373
  /** Shape returned by `go-to-main-component`. */
332
374
  interface GoToMainComponentResult {
333
375
  isMain?: boolean;
@@ -368,6 +410,7 @@ interface DetachComponentInstanceResult {
368
410
  export type PropRow = {
369
411
  name: string;
370
412
  value: string;
413
+ literalValue?: string;
371
414
  /** Variant/enum options when the prop is a known group. */
372
415
  options?: string[];
373
416
  /** Persist surface for this prop. */
@@ -421,9 +464,18 @@ export function buildComponentPropRows(data: {
421
464
  instance?: { alpineData?: string | null } | null;
422
465
  observedProps: Array<{ name: string; value: string }>;
423
466
  persistedVariants: Record<string, string[]>;
467
+ literalProps?: Array<{ name: string; value: string }>;
424
468
  }): PropRow[] {
425
- const { observedProps, persistedVariants, instance } = data;
469
+ const {
470
+ observedProps,
471
+ persistedVariants,
472
+ instance,
473
+ literalProps = [],
474
+ } = data;
426
475
  const alpineData = parseAlpineDataObject(instance?.alpineData);
476
+ const literalValues = new Map(
477
+ literalProps.map(({ name, value }) => [name, value]),
478
+ );
427
479
 
428
480
  const rows: PropRow[] = [];
429
481
  const seen = new Set<string>();
@@ -447,13 +499,29 @@ export function buildComponentPropRows(data: {
447
499
  rows.push({
448
500
  name: prop.name,
449
501
  value: prop.value,
502
+ ...(literalValues.has(prop.name)
503
+ ? { literalValue: literalValues.get(prop.name) }
504
+ : {}),
450
505
  options: persistedVariants[prop.name],
451
506
  surface: "attribute",
452
507
  });
453
508
  seen.add(prop.name);
454
509
  }
455
510
 
456
- // 3) persistedVariant groups with no observed value yet (default to first).
511
+ // 3) Verified literal invocation props that are not reflected in the host DOM.
512
+ for (const prop of literalProps) {
513
+ if (seen.has(prop.name)) continue;
514
+ rows.push({
515
+ name: prop.name,
516
+ value: prop.value,
517
+ literalValue: prop.value,
518
+ options: persistedVariants[prop.name],
519
+ surface: "attribute",
520
+ });
521
+ seen.add(prop.name);
522
+ }
523
+
524
+ // 4) persistedVariant groups with no observed value yet (default to first).
457
525
  // Surface is always "attribute" here, NOT "alpineData" even when this
458
526
  // instance's x-data happens to be non-empty for other keys: x-data blocks
459
527
  // for a real component instance are written with every prop the component
@@ -470,7 +538,10 @@ export function buildComponentPropRows(data: {
470
538
  if (seen.has(group)) continue;
471
539
  rows.push({
472
540
  name: group,
473
- value: options[0] ?? "",
541
+ value: literalValues.get(group) ?? options[0] ?? "",
542
+ ...(literalValues.has(group)
543
+ ? { literalValue: literalValues.get(group) }
544
+ : {}),
474
545
  options,
475
546
  surface: "attribute",
476
547
  });
@@ -533,6 +604,8 @@ export function ComponentSection({
533
604
  onRestoreComponent,
534
605
  onComponentPropApplied,
535
606
  sourceCapabilities = [],
607
+ runtime,
608
+ requestLocalhostWrite,
536
609
  }: {
537
610
  designId: string;
538
611
  fileId?: string;
@@ -560,10 +633,40 @@ export function ComponentSection({
560
633
  ) => void;
561
634
  /** Capability names advertised by the current source. */
562
635
  sourceCapabilities?: string[];
636
+ /** Live component metadata used when a URL file stores only its route URL. */
637
+ runtime?: RuntimeComponentDetails;
638
+ /** Request the existing localhost write-consent dialog before source writes. */
639
+ requestLocalhostWrite?: (opts: {
640
+ files: string[];
641
+ onGranted: LocalhostWriteConsentPayload["onGranted"];
642
+ onCancel?: () => void;
643
+ }) => void;
563
644
  }) {
564
645
  const t = useT();
565
646
  const queryClient = useQueryClient();
566
- const detailsParams = { designId, nodeId, ...(fileId ? { fileId } : {}) };
647
+ const [runtimePropOverride, setRuntimePropOverride] = useState<Array<{
648
+ name: string;
649
+ value: string;
650
+ }> | null>(null);
651
+ const effectiveRuntime = runtime
652
+ ? {
653
+ ...runtime,
654
+ props: runtimePropOverride ?? runtime.props,
655
+ }
656
+ : undefined;
657
+ const propPreviewStateRef = useRef(
658
+ new Map<string, { generation: number; authoritativeValue: string }>(),
659
+ );
660
+ useEffect(() => {
661
+ setRuntimePropOverride(null);
662
+ propPreviewStateRef.current.clear();
663
+ }, [nodeId, runtime?.nodeId, runtime?.props]);
664
+ const detailsParams = {
665
+ designId,
666
+ nodeId,
667
+ ...(fileId ? { fileId } : {}),
668
+ ...(effectiveRuntime ? { runtime: effectiveRuntime } : {}),
669
+ };
567
670
  const detailsKey = ["action", "get-component-details", detailsParams];
568
671
  const latestSourceRef = useRef<{
569
672
  content: string;
@@ -661,6 +764,7 @@ export function ComponentSection({
661
764
  };
662
765
 
663
766
  const sourceForMutation = () => {
767
+ if (effectiveRuntime?.local) return undefined;
664
768
  const latestSource = latestSourceRef.current;
665
769
  return latestSource.content
666
770
  ? {
@@ -794,56 +898,161 @@ export function ComponentSection({
794
898
  ],
795
899
  );
796
900
 
901
+ const updateLocalSourceCache = (content: unknown, versionHash: unknown) => {
902
+ if (
903
+ !effectiveRuntime?.local ||
904
+ (typeof content !== "string" && typeof versionHash !== "string")
905
+ )
906
+ return;
907
+ queryClient.setQueryData(
908
+ [
909
+ "action",
910
+ "read-local-file",
911
+ {
912
+ designId,
913
+ connectionId: effectiveRuntime.local.connectionId,
914
+ path: effectiveRuntime.local.path,
915
+ },
916
+ ],
917
+ (previous: { content?: string; versionHash?: string } | undefined) => ({
918
+ ...previous,
919
+ ...(typeof content === "string" ? { content } : {}),
920
+ ...(typeof versionHash === "string" ? { versionHash } : {}),
921
+ }),
922
+ );
923
+ };
924
+
925
+ const updateRuntimeProp = (propName: string | undefined, value: string) => {
926
+ if (!effectiveRuntime || !propName) return;
927
+ setRuntimePropOverride((previous) => {
928
+ const props = previous ?? effectiveRuntime.props;
929
+ const index = props.findIndex((prop) => prop.name === propName);
930
+ if (index === -1) return [...props, { name: propName, value }];
931
+ return props.map((prop, propIndex) =>
932
+ propIndex === index ? { ...prop, value } : prop,
933
+ );
934
+ });
935
+ };
936
+
797
937
  // Persist a single prop change through apply-component-prop-edit. Attribute
798
938
  // props also preview immediately in the iframe so the selected component
799
939
  // changes without waiting for the write/refetch round-trip.
800
940
  const persistPropEdit = (
801
941
  edit:
802
942
  | { kind: "alpineData"; value: string }
803
- | { kind: "attribute"; attribute: string; value: string },
804
- optimistic: (prev: ComponentDetailsResult) => ComponentDetailsResult,
943
+ | {
944
+ kind: "attribute";
945
+ attribute: string;
946
+ value: string;
947
+ expectedValue?: string;
948
+ previewValue?: string;
949
+ propName?: string;
950
+ },
805
951
  ) => {
806
- queryClient.setQueryData<ComponentDetailsResult>(detailsKey, (prev) =>
807
- prev ? optimistic(prev) : prev,
808
- );
952
+ const previousPreviewValue =
953
+ edit.kind === "attribute"
954
+ ? (edit.previewValue ?? edit.expectedValue ?? "")
955
+ : undefined;
956
+ const previewGeneration =
957
+ edit.kind === "attribute"
958
+ ? (propPreviewStateRef.current.get(edit.attribute)?.generation ?? 0) + 1
959
+ : undefined;
960
+ if (edit.kind === "attribute") {
961
+ const previous = propPreviewStateRef.current.get(edit.attribute);
962
+ propPreviewStateRef.current.set(edit.attribute, {
963
+ generation: previewGeneration!,
964
+ authoritativeValue:
965
+ previous?.authoritativeValue ?? previousPreviewValue!,
966
+ });
967
+ }
968
+ const rollbackPreview = () => {
969
+ if (edit.kind !== "attribute") return;
970
+ const current = propPreviewStateRef.current.get(edit.attribute);
971
+ if (!current || current.generation !== previewGeneration) return;
972
+ postComponentPropPreview(edit.attribute, current.authoritativeValue);
973
+ };
809
974
  if (edit.kind === "attribute") {
810
975
  postComponentPropPreview(edit.attribute, edit.value);
811
976
  }
812
977
  const latestSource = latestSourceRef.current;
813
- applyPropMutation.mutate(
814
- {
815
- designId,
816
- nodeId,
817
- ...(fileId ? { fileId } : {}),
818
- edit,
819
- ...(latestSource.content
978
+ const mutationSource = effectiveRuntime?.local
979
+ ? {
980
+ local: {
981
+ ...effectiveRuntime.local,
982
+ ...(edit.kind === "attribute" && edit.expectedValue !== undefined
983
+ ? { expectedValue: edit.expectedValue }
984
+ : {}),
985
+ },
986
+ }
987
+ : latestSource.content
988
+ ? {
989
+ currentContent: latestSource.content,
990
+ ...(latestSource.revision
991
+ ? { revision: latestSource.revision }
992
+ : {}),
993
+ }
994
+ : undefined;
995
+ const payload = {
996
+ designId,
997
+ nodeId,
998
+ ...(fileId ? { fileId } : {}),
999
+ edit:
1000
+ edit.kind === "attribute"
820
1001
  ? {
821
- source: {
822
- currentContent: latestSource.content,
823
- ...(latestSource.revision
824
- ? { revision: latestSource.revision }
825
- : {}),
826
- },
1002
+ kind: "attribute" as const,
1003
+ attribute: edit.attribute,
1004
+ value: edit.value,
827
1005
  }
828
- : {}),
829
- },
830
- {
1006
+ : edit,
1007
+ ...(mutationSource ? { source: mutationSource } : {}),
1008
+ };
1009
+ const submit = (retriedAfterConsent = false) => {
1010
+ applyPropMutation.mutate(payload, {
831
1011
  onSuccess: (result) => {
832
1012
  const response = result as {
833
1013
  content?: unknown;
834
1014
  fileId?: unknown;
835
1015
  updatedAt?: unknown;
836
1016
  conflict?: unknown;
1017
+ ctaRequired?: unknown;
1018
+ persisted?: unknown;
837
1019
  error?: unknown;
1020
+ source?: {
1021
+ connectionId?: unknown;
1022
+ path?: unknown;
1023
+ versionHash?: unknown;
1024
+ };
1025
+ result?: { status?: unknown; message?: unknown };
838
1026
  };
839
- if (response.conflict) {
1027
+ const resultStatus = response.result?.status;
1028
+ updateLocalSourceCache(
1029
+ response.content,
1030
+ response.source?.versionHash,
1031
+ );
1032
+ if (
1033
+ response.conflict ||
1034
+ response.ctaRequired ||
1035
+ response.persisted === false ||
1036
+ (typeof resultStatus === "string" && resultStatus !== "applied")
1037
+ ) {
1038
+ rollbackPreview();
840
1039
  toast.error(
841
1040
  typeof response.error === "string"
842
1041
  ? response.error
843
- : "This file changed since this component prop edit was prepared. Refresh and try again.",
1042
+ : t("designEditor.toasts.componentCreateFailed"),
844
1043
  );
845
1044
  return;
846
1045
  }
1046
+ if (edit.kind === "attribute") {
1047
+ const current = propPreviewStateRef.current.get(edit.attribute);
1048
+ if (current) {
1049
+ propPreviewStateRef.current.set(edit.attribute, {
1050
+ ...current,
1051
+ authoritativeValue: edit.value,
1052
+ });
1053
+ }
1054
+ updateRuntimeProp(edit.propName, edit.value);
1055
+ }
847
1056
  if (
848
1057
  typeof response.fileId === "string" &&
849
1058
  typeof response.content === "string"
@@ -863,14 +1072,36 @@ export function ComponentSection({
863
1072
  );
864
1073
  }
865
1074
  },
1075
+ onError: (error: unknown) => {
1076
+ if (
1077
+ !retriedAfterConsent &&
1078
+ effectiveRuntime?.local &&
1079
+ requestLocalhostWrite &&
1080
+ isLocalhostWriteConsentError(error)
1081
+ ) {
1082
+ requestLocalhostWrite({
1083
+ files: [effectiveRuntime.local.path],
1084
+ onGranted: () => submit(true),
1085
+ onCancel: rollbackPreview,
1086
+ });
1087
+ return;
1088
+ }
1089
+ rollbackPreview();
1090
+ toast.error(
1091
+ error instanceof Error
1092
+ ? error.message
1093
+ : t("designEditor.toasts.componentCreateFailed"),
1094
+ );
1095
+ },
866
1096
  onSettled: () => {
867
1097
  void queryClient.invalidateQueries({
868
1098
  queryKey: ["action", "get-design"],
869
1099
  });
870
1100
  void queryClient.invalidateQueries({ queryKey: detailsKey });
871
1101
  },
872
- },
873
- );
1102
+ });
1103
+ };
1104
+ submit();
874
1105
  };
875
1106
 
876
1107
  useEffect(() => {
@@ -933,19 +1164,20 @@ export function ComponentSection({
933
1164
  // Real-app sources keep the deeper source-prop controls gated as-is, so for
934
1165
  // non-inline sources the controls are read-only here.
935
1166
  const isInline = sourceType === "inline";
936
- const editingEnabled = isInline && capabilities.canEditProps; // gated; real-app stays read-only for now
1167
+ const editingEnabled =
1168
+ (isInline || Boolean(effectiveRuntime?.local)) && capabilities.canEditProps;
937
1169
  const alpineData = parseAlpineDataObject(instance?.alpineData);
938
1170
 
939
1171
  const rows: PropRow[] = buildComponentPropRows({
940
1172
  instance,
941
1173
  observedProps,
942
1174
  persistedVariants,
1175
+ literalProps: data.literalProps ?? effectiveRuntime?.literalProps,
943
1176
  });
944
1177
 
945
1178
  const hasRows = rows.length > 0;
946
1179
 
947
- // Build the apply-component-prop-edit payload + optimistic cache patch for a
948
- // single prop change.
1180
+ // Build the apply-component-prop-edit payload for a single prop change.
949
1181
  const commitProp = (row: PropRow, nextValue: string) => {
950
1182
  if (!editingEnabled || nextValue === row.value) return;
951
1183
 
@@ -977,35 +1209,16 @@ export function ComponentSection({
977
1209
  }
978
1210
 
979
1211
  const nextSerialized = serialized;
980
- persistPropEdit(
981
- { kind: "alpineData", value: nextSerialized },
982
- (prev) => ({
983
- ...prev,
984
- instance: { ...(prev.instance ?? {}), alpineData: nextSerialized },
985
- observedProps: prev.observedProps.map((p) =>
986
- p.name === row.name ? { ...p, value: nextValue } : p,
987
- ),
988
- }),
989
- );
1212
+ persistPropEdit({ kind: "alpineData", value: nextSerialized });
990
1213
  } else {
991
- persistPropEdit(
992
- {
993
- kind: "attribute",
994
- attribute: propNameToDataAttribute(row.name),
995
- value: nextValue,
996
- },
997
- (prev) => {
998
- const exists = prev.observedProps.some((p) => p.name === row.name);
999
- return {
1000
- ...prev,
1001
- observedProps: exists
1002
- ? prev.observedProps.map((p) =>
1003
- p.name === row.name ? { ...p, value: nextValue } : p,
1004
- )
1005
- : [...prev.observedProps, { name: row.name, value: nextValue }],
1006
- };
1007
- },
1008
- );
1214
+ persistPropEdit({
1215
+ kind: "attribute",
1216
+ attribute: propNameToDataAttribute(row.name),
1217
+ value: nextValue,
1218
+ expectedValue: row.literalValue,
1219
+ previewValue: row.value,
1220
+ propName: row.name,
1221
+ });
1009
1222
  }
1010
1223
  };
1011
1224
 
@@ -60,7 +60,10 @@ export function elementIsComponentSelection(
60
60
  export function elementHasComponentAnnotation(
61
61
  element: ElementInfo | null | undefined,
62
62
  ): boolean {
63
- return Boolean(element?.componentName?.trim());
63
+ return Boolean(
64
+ element?.componentAnnotation?.trim() ||
65
+ (!element?.runtimeComponent && element?.componentName?.trim()),
66
+ );
64
67
  }
65
68
 
66
69
  export function displayLabel(value: string | undefined): string {
@@ -460,7 +463,11 @@ export function commitElementMinMax(
460
463
  onStyleChange(property, kind === "min" ? "0px" : "none", meta);
461
464
  return;
462
465
  }
463
- onStyleChange(property, `${Math.max(0, Math.round(value))}px`, meta);
466
+ onStyleChange(
467
+ property,
468
+ `${Math.max(0, Math.round(value * 10) / 10)}px`,
469
+ meta,
470
+ );
464
471
  }
465
472
 
466
473
  export function inferElementSizing(
@@ -128,6 +128,35 @@ export interface AutoLayoutPadding {
128
128
  left: number;
129
129
  }
130
130
 
131
+ const OPPOSITE_PADDING_SIDE: Record<
132
+ keyof AutoLayoutPadding,
133
+ keyof AutoLayoutPadding
134
+ > = {
135
+ top: "bottom",
136
+ right: "left",
137
+ bottom: "top",
138
+ left: "right",
139
+ };
140
+
141
+ type PaddingChangeMeta = {
142
+ source: ScrubInputChangeMeta["source"];
143
+ phase: ScrubInputChangeMeta["phase"];
144
+ altKey?: boolean;
145
+ };
146
+
147
+ /** Apply Figma's Alt/Option mirror to one unlinked padding side. */
148
+ export function mirrorPaddingChange(
149
+ padding: AutoLayoutPadding,
150
+ side: keyof AutoLayoutPadding,
151
+ meta?: PaddingChangeMeta,
152
+ ): AutoLayoutPadding {
153
+ if (!meta?.altKey) return padding;
154
+ return {
155
+ ...padding,
156
+ [OPPOSITE_PADDING_SIDE[side]]: padding[side],
157
+ };
158
+ }
159
+
131
160
  export interface AutoLayoutMatrixValue {
132
161
  direction: AutoLayoutDirection;
133
162
  wrap: AutoLayoutWrap;
@@ -424,6 +453,14 @@ export function AutoLayoutMatrix({
424
453
  value.paddingMixed?.top || value.paddingMixed?.bottom,
425
454
  );
426
455
 
456
+ const updatePadding = (
457
+ side: keyof AutoLayoutPadding,
458
+ padding: AutoLayoutPadding,
459
+ meta?: PaddingChangeMeta,
460
+ ) => {
461
+ onPaddingChange(mirrorPaddingChange(padding, side, meta), meta);
462
+ };
463
+
427
464
  const activeFlow = getFlowOption(value);
428
465
  const isBlock = activeFlow === "normal";
429
466
  const canResizeToFit =
@@ -731,7 +768,8 @@ export function AutoLayoutMatrix({
731
768
  value={horizontalPaddingValue}
732
769
  mixed={horizontalPaddingMixed}
733
770
  onChange={(next, meta) =>
734
- onPaddingChange(
771
+ updatePadding(
772
+ "left",
735
773
  {
736
774
  top: value.padding.top,
737
775
  bottom: value.padding.bottom,
@@ -752,7 +790,8 @@ export function AutoLayoutMatrix({
752
790
  value={verticalPaddingValue}
753
791
  mixed={verticalPaddingMixed}
754
792
  onChange={(next, meta) =>
755
- onPaddingChange(
793
+ updatePadding(
794
+ "top",
756
795
  {
757
796
  top: next,
758
797
  bottom: next,
@@ -788,7 +827,11 @@ export function AutoLayoutMatrix({
788
827
  value={value.padding.top}
789
828
  mixed={value.paddingMixed?.top}
790
829
  onChange={(next, meta) =>
791
- onPaddingChange({ ...value.padding, top: next }, meta)
830
+ updatePadding(
831
+ "top",
832
+ { ...value.padding, top: next },
833
+ meta,
834
+ )
792
835
  }
793
836
  disabled={disabled}
794
837
  />
@@ -804,7 +847,8 @@ export function AutoLayoutMatrix({
804
847
  value={value.padding.right}
805
848
  mixed={value.paddingMixed?.right}
806
849
  onChange={(next, meta) =>
807
- onPaddingChange(
850
+ updatePadding(
851
+ "right",
808
852
  { ...value.padding, right: next },
809
853
  meta,
810
854
  )
@@ -819,7 +863,8 @@ export function AutoLayoutMatrix({
819
863
  value={value.padding.bottom}
820
864
  mixed={value.paddingMixed?.bottom}
821
865
  onChange={(next, meta) =>
822
- onPaddingChange(
866
+ updatePadding(
867
+ "bottom",
823
868
  { ...value.padding, bottom: next },
824
869
  meta,
825
870
  )
@@ -838,7 +883,8 @@ export function AutoLayoutMatrix({
838
883
  value={value.padding.left}
839
884
  mixed={value.paddingMixed?.left}
840
885
  onChange={(next, meta) =>
841
- onPaddingChange(
886
+ updatePadding(
887
+ "left",
842
888
  { ...value.padding, left: next },
843
889
  meta,
844
890
  )
@@ -1945,7 +1991,7 @@ export function SizingField({
1945
1991
  ? "Mixed"
1946
1992
  : resolvedSize == null
1947
1993
  ? ""
1948
- : String(Math.round(resolvedSize));
1994
+ : String(roundToOneDecimal(resolvedSize));
1949
1995
 
1950
1996
  const addMinLabel = isWidth ? labels.addMinWidth : labels.addMinHeight;
1951
1997
  const addMaxLabel = isWidth ? labels.addMaxWidth : labels.addMaxHeight;
@@ -1958,7 +2004,7 @@ export function SizingField({
1958
2004
  const openEditor = (kind: "min" | "max") => {
1959
2005
  // Commit immediately so the shown row always reflects real state and
1960
2006
  // persists across selection changes, remounts, and parent re-renders.
1961
- const seed = Math.max(0, Math.round(resolvedSize ?? 0));
2007
+ const seed = Math.max(0, roundToOneDecimal(resolvedSize ?? 0));
1962
2008
  const seedValue = kind === "min" ? seed : seed || 1;
1963
2009
  onMinMaxChange?.(sizingAxis, kind, seedValue);
1964
2010
  };
@@ -2250,7 +2296,9 @@ function ConstraintSubRow({
2250
2296
  icon={icon}
2251
2297
  prefix="icon"
2252
2298
  value={value}
2253
- onChange={(next, meta) => onChange(Math.max(0, Math.round(next)), meta)}
2299
+ onChange={(next, meta) =>
2300
+ onChange(Math.max(0, roundToOneDecimal(next)), meta)
2301
+ }
2254
2302
  unit="px"
2255
2303
  min={0}
2256
2304
  step={1}
@@ -668,6 +668,19 @@ export function getPrimitiveDropTargetForPoint(
668
668
  continue;
669
669
  }
670
670
  const boardRect = toBoardRect(primitive, topScreen.geometry, metadata);
671
+ // In-screen reparenting only accepts a frame that can contain the dragged
672
+ // layer's rendered bounds. A nested cross-screen target may fall back to
673
+ // its ancestor, but a top-level target has no larger fallback available.
674
+ if (
675
+ draggedBoardRect &&
676
+ (draggedScreenId === topScreen.screen.id ||
677
+ primitive.parentNodeId !== undefined ||
678
+ primitive.parentProjectionNodeId !== undefined) &&
679
+ (draggedBoardRect.width > boardRect.width + 1 ||
680
+ draggedBoardRect.height > boardRect.height + 1)
681
+ ) {
682
+ continue;
683
+ }
671
684
  if (
672
685
  draggedBoardRect &&
673
686
  draggedScreenId === topScreen.screen.id &&