@agent-native/core 0.187.0 → 0.187.1-nightly-20260925003003

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 (158) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +4 -3
  3. package/corpus/templates/analytics/changelog/2026-09-24-analytics-sidebar-footer-aligns-consistently.md +6 -0
  4. package/corpus/templates/analytics/changelog/2026-09-24-dashboard-emails-match-stacked-area-totals.md +6 -0
  5. package/corpus/templates/analytics/server/lib/report-chart-svg.ts +47 -20
  6. package/corpus/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx +1 -1
  7. package/corpus/templates/calendar/changelog/2026-09-24-guest-inputs-wrap-beneath-existing-guests-so-the-active-inpu.md +6 -0
  8. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +8 -3
  9. package/corpus/templates/clips/app/lib/recording-link.ts +21 -0
  10. package/corpus/templates/clips/app/routes/record.tsx +27 -8
  11. package/corpus/templates/clips/changelog/2026-09-24-the-camera-bubble-stays-visible-while-you-reposition-it-befo.md +6 -0
  12. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +7 -0
  13. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +63 -55
  14. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +251 -74
  15. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +66 -56
  16. package/corpus/templates/design/app/components/design/EditPanel.tsx +19 -0
  17. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +59 -9
  18. package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +4 -0
  19. package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +119 -0
  20. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +48 -51
  21. package/corpus/templates/design/app/components/design/edit-panel/style-change-types.ts +1 -0
  22. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +298 -187
  23. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +2 -2
  24. package/corpus/templates/design/app/components/design/inspector/index.ts +4 -0
  25. package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -2
  26. package/corpus/templates/design/app/i18n/en-US.ts +6 -0
  27. package/corpus/templates/design/app/pages/Present.tsx +101 -87
  28. package/corpus/templates/design/app/pages/design-editor/clone-and-pen-edit.ts +75 -14
  29. package/corpus/templates/design/app/pages/design-editor/commands/README.md +1 -0
  30. package/corpus/templates/design/app/pages/design-editor/commands/commit-relative-style-delta-to-selected-layers.ts +69 -56
  31. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +16 -0
  32. package/corpus/templates/design/app/pages/design-editor/commands/scale-selection.ts +17 -2
  33. package/corpus/templates/design/app/pages/design-editor/commands/styles-change.ts +23 -30
  34. package/corpus/templates/design/app/pages/design-editor/commands/swap-fill-stroke.ts +105 -0
  35. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +21 -0
  36. package/corpus/templates/design/app/pages/present-review-state.ts +7 -1
  37. package/corpus/templates/design/changelog/2026-09-24-a-vector-s-stroke-row-shows-position-beside-weight-instead-o.md +6 -0
  38. package/corpus/templates/design/changelog/2026-09-24-css-margins-can-be-adjusted-on-the-canvas-and-in-the-layout-.md +6 -0
  39. package/corpus/templates/design/changelog/2026-09-24-design-reviews-show-the-real-canvas-beside-its-conversation.md +6 -0
  40. package/corpus/templates/design/changelog/2026-09-24-double-clicking-text-or-a-vector-on-the-board-starts-editing.md +6 -0
  41. package/corpus/templates/design/changelog/2026-09-24-interact-opens-a-focused-responsive-preview-without-reloadin.md +6 -0
  42. package/corpus/templates/design/changelog/2026-09-24-keep-gradient-values-visible-and-limit-canvas-paints-to-supp.md +6 -0
  43. package/corpus/templates/design/changelog/2026-09-24-open-pen-paths-no-longer-paint-a-filled-triangle-after-shift.md +6 -0
  44. package/corpus/templates/design/changelog/2026-09-24-re-clicking-the-active-responsive-preview-returns-to-the-can.md +6 -0
  45. package/corpus/templates/design/changelog/2026-09-24-resized-vectors-can-be-point-edited-and-the-k-scale-tool-sho.md +6 -0
  46. package/corpus/templates/design/changelog/2026-09-24-while-editing-text-click-to-place-the-caret-and-double-click.md +6 -0
  47. package/corpus/templates/design/e2e/helpers.ts +19 -2
  48. package/corpus/templates/mail/app/components/email/ComposeModal.tsx +1 -1
  49. package/corpus/templates/mail/changelog/2026-09-24-gmail-push-ack.md +6 -0
  50. package/corpus/templates/mail/changelog/2026-09-24-the-composer-keeps-its-taller-layout-in-a-narrower-window.md +6 -0
  51. package/corpus/templates/mail/server/db/schema.ts +26 -1
  52. package/corpus/templates/mail/server/lib/inbox-store.ts +68 -23
  53. package/corpus/templates/mail/server/lib/inbox-sync.ts +43 -17
  54. package/corpus/templates/mail/server/plugins/db.ts +19 -0
  55. package/corpus/templates/mail/server/routes/api/gmail/push.post.ts +4 -15
  56. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +45 -30
  57. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +18 -2
  58. package/corpus/templates/slides/actions/export-html.ts +28 -17
  59. package/corpus/templates/slides/app/hooks/use-deck-design-system.ts +27 -15
  60. package/corpus/templates/slides/app/pages/DeckEditor.tsx +1 -1
  61. package/corpus/templates/slides/changelog/2026-09-23-decks-with-no-design-system-now-follow-a-theme-chosen-for-th.md +6 -0
  62. package/corpus/templates/slides/shared/slide-background.ts +3 -1
  63. package/dist/agent/engine/builder-engine.js +11 -0
  64. package/dist/agent/engine/types.d.ts +1 -0
  65. package/dist/agent/production-agent.d.ts +2 -0
  66. package/dist/agent/production-agent.js +32 -4
  67. package/dist/agent/run-loop-with-resume.js +6 -0
  68. package/dist/cli/index.js +4 -4
  69. package/dist/cli/process.d.ts +10 -1
  70. package/dist/cli/process.js +66 -0
  71. package/dist/cli/workspace-dev.js +2 -0
  72. package/dist/client/agent-chat.js +13 -14
  73. package/dist/client/error-format.js +4 -4
  74. package/dist/client/observability/ObservabilityDashboard.js +95 -65
  75. package/dist/client/observability/OutputPreview.d.ts +2 -1
  76. package/dist/client/observability/OutputPreview.js +64 -7
  77. package/dist/client/observability/useObservability.d.ts +2 -1
  78. package/dist/client/observability/useObservability.js +5 -1
  79. package/dist/client/settings/UsageSection.js +105 -17
  80. package/dist/collab/routes.d.ts +1 -1
  81. package/dist/collab/struct-routes.d.ts +1 -1
  82. package/dist/db/client.d.ts +44 -0
  83. package/dist/db/client.js +62 -3
  84. package/dist/db/create-get-db.js +23 -1
  85. package/dist/db/embedded-runtime.d.ts +21 -0
  86. package/dist/db/embedded-runtime.js +27 -0
  87. package/dist/db/migrations.js +2 -0
  88. package/dist/db/server-runtime.d.ts +22 -0
  89. package/dist/db/server-runtime.js +26 -0
  90. package/dist/deploy/build.d.ts +29 -0
  91. package/dist/deploy/build.js +204 -121
  92. package/dist/feature-flags/index.d.ts +1 -1
  93. package/dist/feature-flags/index.js +1 -1
  94. package/dist/feature-flags/registry.d.ts +1 -0
  95. package/dist/feature-flags/registry.js +5 -0
  96. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  97. package/dist/index.d.ts +1 -1
  98. package/dist/index.js +1 -1
  99. package/dist/integrations/webhook-handler.js +4 -1
  100. package/dist/jobs/background-automation-runner.js +6 -1
  101. package/dist/localization/core-messages/ar-SA.js +9 -1
  102. package/dist/localization/core-messages/de-DE.js +9 -1
  103. package/dist/localization/core-messages/en-US.d.ts +9 -1
  104. package/dist/localization/core-messages/en-US.js +9 -1
  105. package/dist/localization/core-messages/es-ES.js +9 -1
  106. package/dist/localization/core-messages/fr-FR.js +9 -1
  107. package/dist/localization/core-messages/hi-IN.js +9 -1
  108. package/dist/localization/core-messages/ja-JP.js +9 -1
  109. package/dist/localization/core-messages/ko-KR.js +9 -1
  110. package/dist/localization/core-messages/pt-BR.js +9 -1
  111. package/dist/localization/core-messages/supplemental/ar-SA.js +1 -0
  112. package/dist/localization/core-messages/supplemental/de-DE.js +1 -0
  113. package/dist/localization/core-messages/supplemental/en-US.js +1 -0
  114. package/dist/localization/core-messages/supplemental/es-ES.js +1 -0
  115. package/dist/localization/core-messages/supplemental/fr-FR.js +1 -0
  116. package/dist/localization/core-messages/supplemental/hi-IN.js +1 -0
  117. package/dist/localization/core-messages/supplemental/ja-JP.js +1 -0
  118. package/dist/localization/core-messages/supplemental/ko-KR.js +1 -0
  119. package/dist/localization/core-messages/supplemental/pt-BR.js +1 -0
  120. package/dist/localization/core-messages/supplemental/zh-CN.js +1 -0
  121. package/dist/localization/core-messages/supplemental/zh-TW.js +1 -0
  122. package/dist/localization/core-messages/zh-CN.js +9 -1
  123. package/dist/localization/core-messages/zh-TW.js +9 -1
  124. package/dist/localization/core-messages.d.ts +9 -1
  125. package/dist/localization/mcp-settings-messages.d.ts +1 -0
  126. package/dist/mcp/connect-route.js +45 -8
  127. package/dist/mcp/connect-store.d.ts +7 -5
  128. package/dist/mcp/connect-store.js +30 -39
  129. package/dist/notifications/routes.d.ts +3 -3
  130. package/dist/observability/actions/get-observability-review-detail.d.ts +7 -0
  131. package/dist/observability/actions/get-observability-review-detail.js +25 -0
  132. package/dist/observability/reviews.d.ts +9 -1
  133. package/dist/observability/reviews.js +20 -0
  134. package/dist/observability/routes.d.ts +3 -3
  135. package/dist/observability/types.d.ts +8 -0
  136. package/dist/org/handlers.d.ts +1 -1
  137. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  138. package/dist/provider-api/actions/provider-api.d.ts +9 -9
  139. package/dist/scripts/db/query.js +6 -1
  140. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  141. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  142. package/dist/server/agent-teams.js +4 -1
  143. package/dist/server/auth.js +60 -20
  144. package/dist/server/better-auth-instance.js +7 -1
  145. package/dist/server/core-routes-plugin.d.ts +1 -1
  146. package/dist/server/core-routes-plugin.js +2 -0
  147. package/dist/server/embedded.js +6 -0
  148. package/dist/server/framework-request-handler.js +6 -0
  149. package/dist/server/realtime-token.d.ts +1 -1
  150. package/dist/usage/actions/get-usage-metrics.js +4 -1
  151. package/dist/usage/metrics-store.d.ts +19 -0
  152. package/dist/usage/metrics-store.js +106 -14
  153. package/dist/usage/store.d.ts +7 -2
  154. package/dist/usage/store.js +25 -4
  155. package/dist/vite/action-types-plugin.js +4 -0
  156. package/dist/vite/client.d.ts +6 -1
  157. package/dist/vite/client.js +40 -1
  158. package/package.json +4 -3
@@ -2369,15 +2369,19 @@ export function DesignCanvas({
2369
2369
  // srcdoc is rebuilt per document, so unlike the keyed live-edit bundle above
2370
2370
  // it can carry the real first-paint value: forwarding that arrives only by
2371
2371
  // postMessage stays dead until the handshake, and is stranded by a swap.
2372
+ const initialInteractModeRef = useRef(interactMode);
2372
2373
  const embeddedGestureBridgeForSrcdoc = useMemo(
2373
2374
  () =>
2374
2375
  EMBEDDED_WHEEL_BRIDGE_SCRIPT.replace(
2375
2376
  "__EMBEDDED_WHEEL_FORWARDING_ENABLED__",
2376
- isEmbeddedFrame && !interactMode ? "true" : "false",
2377
+ isEmbeddedFrame && !initialInteractModeRef.current ? "true" : "false",
2377
2378
  )
2378
2379
  .replace("__EMBEDDED_SPACE_KEY_FORWARDING_ENABLED__", "false")
2379
- .replace("__EDITING_SAFETY_ENABLED__", interactMode ? "false" : "true"),
2380
- [interactMode, isEmbeddedFrame],
2380
+ .replace(
2381
+ "__EDITING_SAFETY_ENABLED__",
2382
+ initialInteractModeRef.current ? "false" : "true",
2383
+ ),
2384
+ [isEmbeddedFrame],
2381
2385
  );
2382
2386
  const includeLiveEditEditorChrome = !readOnly;
2383
2387
  const liveEditBridgeScript = useMemo(() => {
@@ -3571,15 +3575,13 @@ export function DesignCanvas({
3571
3575
  // eslint-disable-next-line react-hooks/exhaustive-deps
3572
3576
  }, [centerInteractPreview, deviceFrame, contentKey, previewWidthPx]);
3573
3577
 
3574
- // Build the srcdoc. The tweak bridge ALWAYS goes in so the panel works
3575
- // outside Edit mode. The editor chrome bridge is omitted for Interact mode
3576
- // so preview/app users can interact with the app normally.
3578
+ // Build the srcdoc. Both mode bridges stay installed; live postMessages
3579
+ // switch pointer ownership without replacing the iframe document.
3577
3580
  //
3578
- // readOnly and editMode are intentionally NOT deps here: the bridge is
3579
- // injected whenever !interactMode and the initial __READ_ONLY__ /
3580
- // __TEXT_EDITING_ENABLED__ placeholders are baked from the *first* render
3581
- // only (so first paint never flashes the wrong mode). After that, live
3582
- // readOnly / editMode changes flow through the set-read-only and
3581
+ // readOnly and editMode are intentionally NOT deps here: the initial
3582
+ // __READ_ONLY__ / __TEXT_EDITING_ENABLED__ placeholders are baked from the
3583
+ // first render only (so first paint never flashes the wrong mode). After
3584
+ // that, live readOnly / editMode changes flow through set-read-only and
3583
3585
  // set-text-editing-enabled postMessages (see the useEffects below) so
3584
3586
  // switching the active surface (board ↔ screen) or toggling Edit ⇄ Preview
3585
3587
  // never rebuilds srcdoc / reloads every screen iframe.
@@ -3589,50 +3591,49 @@ export function DesignCanvas({
3589
3591
  // mounting an iframe, then mounts the real `src` exactly once.
3590
3592
  if (rawExternalPreviewUrl) return undefined;
3591
3593
  const localizedContent = withLocalRuntimes(iframeRenderContent);
3592
- const editorChromeBridge = interactMode
3593
- ? ""
3594
- : createEditorBridgeThemeScript(readEditorBridgeThemeVars()) +
3595
- EDITOR_CHROME_BRIDGE_SCRIPT.replace(
3596
- "__READ_ONLY__",
3597
- readOnly ? "true" : "false",
3594
+ const editorChromeBridge =
3595
+ createEditorBridgeThemeScript(readEditorBridgeThemeVars()) +
3596
+ EDITOR_CHROME_BRIDGE_SCRIPT.replace(
3597
+ "__READ_ONLY__",
3598
+ readOnly ? "true" : "false",
3599
+ )
3600
+ .replace("__TEXT_EDITING_ENABLED__", editMode ? "true" : "false")
3601
+ .replace(
3602
+ "__EDITOR_CHROME_SCALE_X__",
3603
+ String(effectiveEditorChromeScaleX),
3598
3604
  )
3599
- .replace("__TEXT_EDITING_ENABLED__", editMode ? "true" : "false")
3600
- .replace(
3601
- "__EDITOR_CHROME_SCALE_X__",
3602
- String(effectiveEditorChromeScaleX),
3603
- )
3604
- .replace(
3605
- "__EDITOR_CHROME_SCALE_Y__",
3606
- String(effectiveEditorChromeScaleY),
3607
- )
3608
- .replace(
3609
- "__DESIGN_CANVAS_SCREEN_ID__",
3610
- JSON.stringify(screenId ?? contentKey ?? ""),
3611
- )
3612
- .replace(
3613
- "__DESIGN_CANVAS_BOARD_SURFACE__",
3614
- boardSurface ? "true" : "false",
3615
- )
3616
- .replace(
3617
- "__DESIGN_CANVAS_CONTENT_OFFSET_X__",
3618
- String(Math.round(embeddedFrame?.contentOffsetX ?? 0)),
3619
- )
3620
- .replace(
3621
- "__DESIGN_CANVAS_CONTENT_OFFSET_Y__",
3622
- String(Math.round(embeddedFrame?.contentOffsetY ?? 0)),
3623
- )
3624
- .replace("__RUNTIME_LAYER_SNAPSHOT_ENABLED__", "false")
3625
- .replace(
3626
- "__LIVE_REFLOW_ENABLED__",
3627
- LIVE_REFLOW_ENABLED ? "true" : "false",
3628
- )
3629
- .replace(
3630
- "__SELECTED_LAYER_DRAG_PRIORITY__",
3631
- SELECTED_LAYER_DRAG_PRIORITY_ENABLED ? "true" : "false",
3632
- )
3633
- .replace(/__INITIAL_SOURCE_HEAD__/g, () =>
3634
- inlineScriptJson(sourceHeadInnerHtml(localizedContent)),
3635
- );
3605
+ .replace(
3606
+ "__EDITOR_CHROME_SCALE_Y__",
3607
+ String(effectiveEditorChromeScaleY),
3608
+ )
3609
+ .replace(
3610
+ "__DESIGN_CANVAS_SCREEN_ID__",
3611
+ JSON.stringify(screenId ?? contentKey ?? ""),
3612
+ )
3613
+ .replace(
3614
+ "__DESIGN_CANVAS_BOARD_SURFACE__",
3615
+ boardSurface ? "true" : "false",
3616
+ )
3617
+ .replace(
3618
+ "__DESIGN_CANVAS_CONTENT_OFFSET_X__",
3619
+ String(Math.round(embeddedFrame?.contentOffsetX ?? 0)),
3620
+ )
3621
+ .replace(
3622
+ "__DESIGN_CANVAS_CONTENT_OFFSET_Y__",
3623
+ String(Math.round(embeddedFrame?.contentOffsetY ?? 0)),
3624
+ )
3625
+ .replace("__RUNTIME_LAYER_SNAPSHOT_ENABLED__", "false")
3626
+ .replace(
3627
+ "__LIVE_REFLOW_ENABLED__",
3628
+ LIVE_REFLOW_ENABLED ? "true" : "false",
3629
+ )
3630
+ .replace(
3631
+ "__SELECTED_LAYER_DRAG_PRIORITY__",
3632
+ SELECTED_LAYER_DRAG_PRIORITY_ENABLED ? "true" : "false",
3633
+ )
3634
+ .replace(/__INITIAL_SOURCE_HEAD__/g, () =>
3635
+ inlineScriptJson(sourceHeadInnerHtml(localizedContent)),
3636
+ );
3636
3637
  // ALWAYS injected (like the other always-on bridges above) so
3637
3638
  // MultiScreenCanvas's cross-screen drag hit-testing
3638
3639
  // (agent-native:hit-test / agent-native:hit-test-result) resolves an
@@ -3696,7 +3697,6 @@ export function DesignCanvas({
3696
3697
  boardSurface,
3697
3698
  fitRootBodyToFrame,
3698
3699
  rawExternalPreviewUrl,
3699
- interactMode,
3700
3700
  isEmbeddedFrame,
3701
3701
  embeddedFrameBackground,
3702
3702
  embeddedGestureBridgeForSrcdoc,
@@ -4102,6 +4102,7 @@ export function DesignCanvas({
4102
4102
  return;
4103
4103
  }
4104
4104
  if (e.data.type === "clear-selection") {
4105
+ iframeClearedSelectorRef.current = selectedSelectorRef.current ?? null;
4105
4106
  onClearSelection?.();
4106
4107
  return;
4107
4108
  }
@@ -5188,6 +5189,9 @@ export function DesignCanvas({
5188
5189
  // Selectors from the iframe's own click; skip mirroring them back once to
5189
5190
  // avoid the fast-click bounce.
5190
5191
  const suppressMirrorSelectorsRef = useRef<string[] | null>(null);
5192
+ // The iframe cleared this selection itself. A ready handshake can land
5193
+ // before the parent renders the clear, and replaying then re-selects it.
5194
+ const iframeClearedSelectorRef = useRef<string | null>(null);
5191
5195
  // Latest replayIframeEditorState, synced during render (below) so the message
5192
5196
  // handler can force a corrective resync without a stale closure.
5193
5197
  const replayIframeEditorStateRef = useRef<(() => void) | null>(null);
@@ -5256,7 +5260,13 @@ export function DesignCanvas({
5256
5260
  !forceSelectionMirrorResyncRef.current &&
5257
5261
  !!selectedSelector &&
5258
5262
  (suppressMirrorSelectorsRef.current?.includes(selectedSelector) ?? false);
5259
- if (isIframeOriginatedEcho) {
5263
+ const staleIframeClear =
5264
+ !!selectedSelector &&
5265
+ iframeClearedSelectorRef.current === selectedSelector;
5266
+ if (!staleIframeClear) iframeClearedSelectorRef.current = null;
5267
+ if (staleIframeClear) {
5268
+ // Wait for the parent's clear to render; it mirrors down on its own.
5269
+ } else if (isIframeOriginatedEcho) {
5260
5270
  lastSelectionMirrorSignatureRef.current = selectionMirrorSignature;
5261
5271
  suppressMirrorSelectorsRef.current = null;
5262
5272
  } else if (
@@ -162,6 +162,10 @@ import {
162
162
  textStrokeIsVisible,
163
163
  } from "./edit-panel/position-helpers";
164
164
  import { PositionLayoutProperties } from "./edit-panel/position-layout-properties";
165
+ import {
166
+ ScaleProperties,
167
+ type ScaleToolControls,
168
+ } from "./edit-panel/scale-properties";
165
169
  import { mixedElementFromSelection } from "./edit-panel/selection-helpers";
166
170
  import { StrokeProperties } from "./edit-panel/stroke-properties";
167
171
  import {
@@ -538,6 +542,8 @@ interface EditPanelProps {
538
542
  * type without EditPanel importing it.
539
543
  */
540
544
  activeTool?: string;
545
+ /** Shows Figma's Scale section while `activeTool === "scale"`. */
546
+ scaleToolControls?: ScaleToolControls;
541
547
  /**
542
548
  * Creates a new screen sized to the clicked preset. Only takes effect while
543
549
  * `activeTool === "frame"`; when omitted the frame tool falls back to the
@@ -1913,6 +1919,7 @@ function PageProperties({
1913
1919
  <ColorInput
1914
1920
  label={t("editPanel.labels.background")}
1915
1921
  value={canvasBackground ?? canvasBackgroundFallback ?? ""}
1922
+ supportedPaintTypes={["solid", "none"]}
1916
1923
  // meta carries phase: "preview" while dragging vs "commit" on
1917
1924
  // release. Dropping it persists every tick and the picker jumps.
1918
1925
  onChange={(value, meta) => onCanvasBackgroundChange(value, meta)}
@@ -2568,6 +2575,7 @@ export const EditPanel = memo(function EditPanel({
2568
2575
  inspectCode,
2569
2576
  aiActions,
2570
2577
  activeTool,
2578
+ scaleToolControls,
2571
2579
  onCreateScreenFromPreset,
2572
2580
  onAlignSelection,
2573
2581
  alignSelectionDisabled = false,
@@ -3371,6 +3379,17 @@ export const EditPanel = memo(function EditPanel({
3371
3379
  motionKeyframeContext={motionKeyframeFieldContext}
3372
3380
  breakpointOverrideContext={breakpointOverrideFieldContext}
3373
3381
  />
3382
+ {activeTool === "scale" &&
3383
+ scaleToolControls &&
3384
+ effectiveSelectedElements.length === 1 ? (
3385
+ <ScaleProperties
3386
+ key={`scale:${inspectorElementSectionKey}`}
3387
+ element={
3388
+ stateResolvedInspectorElement ?? inspectorElement
3389
+ }
3390
+ controls={scaleToolControls}
3391
+ />
3392
+ ) : null}
3374
3393
  <LayoutContextProperties
3375
3394
  key={`layout-context:${inspectorElementSectionKey}`}
3376
3395
  element={stateResolvedInspectorElement ?? inspectorElement}
@@ -616,6 +616,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
616
616
  onReviewPendingScreen,
617
617
  interactMode = false,
618
618
  interactScreenId = null,
619
+ focusedInteractViewport = null,
619
620
  readOnly = false,
620
621
  editableScreenIds,
621
622
  activeScreenHasHoveredChild = false,
@@ -8936,6 +8937,11 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
8936
8937
  if (pressTarget) {
8937
8938
  dispatchAt(pressTarget, "mousedown", point, ev, 1);
8938
8939
  dispatchAt(pressTarget, "mouseup", point, ev, 0);
8940
+ // This surface covers the selected object, so the native dblclick
8941
+ // that starts text editing never reaches the board bridge.
8942
+ if (ev.detail >= 2) {
8943
+ dispatchAt(pressTarget, "dblclick", point, ev, 0);
8944
+ }
8939
8945
  }
8940
8946
  finishDrag();
8941
8947
  return;
@@ -10840,7 +10846,16 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
10840
10846
  !autoHeight || autoHeight === rawGeometry.height
10841
10847
  ? rawGeometry
10842
10848
  : { ...rawGeometry, height: autoHeight };
10843
- const geometry = clampScreenFrameSize(sizedGeometry, sizeConstraints);
10849
+ const baseGeometry = clampScreenFrameSize(sizedGeometry, sizeConstraints);
10850
+ const geometry =
10851
+ screen.id === interactScreenId && focusedInteractViewport
10852
+ ? {
10853
+ ...baseGeometry,
10854
+ width: Math.max(1, Math.round(focusedInteractViewport.width)),
10855
+ height: Math.max(1, Math.round(focusedInteractViewport.height)),
10856
+ rotation: undefined,
10857
+ }
10858
+ : baseGeometry;
10844
10859
  const prior = cache.get(screen.id);
10845
10860
  if (
10846
10861
  prior &&
@@ -10866,15 +10881,23 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
10866
10881
  return next;
10867
10882
  }, [
10868
10883
  frameGeometry,
10884
+ focusedInteractViewport?.height,
10885
+ focusedInteractViewport?.width,
10869
10886
  geometryById,
10870
10887
  geometryOverridesById,
10871
10888
  getResolvedMetadata,
10889
+ interactScreenId,
10872
10890
  measuredIframeHeights,
10873
10891
  measuredIframeNaturalHeights,
10874
10892
  renderedScreens,
10875
10893
  screenIndexById,
10876
10894
  screenRootComputedStylesById,
10877
10895
  ]);
10896
+ const focusedInteractFrame =
10897
+ focusedInteractViewport && interactScreenId
10898
+ ? canvasFrames.find(({ screen }) => screen.id === interactScreenId)
10899
+ : undefined;
10900
+ const focusedInteract = Boolean(focusedInteractFrame);
10878
10901
  useLayoutEffect(() => {
10879
10902
  renderedFrameGeometryRef.current = Object.fromEntries(
10880
10903
  canvasFrames.map(({ screen, geometry }) => [screen.id, geometry]),
@@ -11448,6 +11471,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
11448
11471
  : null;
11449
11472
  const boardSelectionBoxVisible =
11450
11473
  !boardTextEditing &&
11474
+ !vectorEdit &&
11451
11475
  (boardFileId
11452
11476
  ? (selectedLayerSelectorGroupsByScreen[boardFileId]?.length ?? 0)
11453
11477
  : 0) <= 1 &&
@@ -11491,9 +11515,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
11491
11515
  isolation: "isolate",
11492
11516
  overscrollBehavior: "none",
11493
11517
  touchAction: "none",
11518
+ visibility: focusedInteract ? "hidden" : undefined,
11494
11519
  }}
11495
11520
  >
11496
- {showPixelGrid ? (
11521
+ {showPixelGrid && !focusedInteract ? (
11497
11522
  <div
11498
11523
  ref={pixelGridRef}
11499
11524
  className="pointer-events-none absolute inset-0 opacity-60"
@@ -11506,7 +11531,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
11506
11531
  />
11507
11532
  ) : null}
11508
11533
 
11509
- {showBoardStaticPreview &&
11534
+ {!focusedInteract &&
11535
+ showBoardStaticPreview &&
11510
11536
  boardFrameGeometry &&
11511
11537
  boardStaticPreviewViewport &&
11512
11538
  boardStaticPreviewContent ? (
@@ -11570,8 +11596,13 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
11570
11596
  {
11571
11597
  left: 0,
11572
11598
  top: 0,
11573
- transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale})`,
11599
+ right: focusedInteract ? 0 : undefined,
11600
+ bottom: focusedInteract ? 0 : undefined,
11601
+ transform: focusedInteractFrame
11602
+ ? `translate(${surfaceSize.width / 2 - (SURFACE_PADDING + focusedInteractFrame.geometry.x + focusedInteractFrame.geometry.width / 2) * scale}px, ${surfaceSize.height / 2 - (SURFACE_PADDING + focusedInteractFrame.geometry.y + focusedInteractFrame.geometry.height / 2) * scale}px) scale(${scale})`
11603
+ : `translate(${pan.x}px, ${pan.y}px) scale(${scale})`,
11574
11604
  transformOrigin: "top left",
11605
+ visibility: focusedInteract ? "hidden" : undefined,
11575
11606
  // Controlled zoom also renders the world transform, so it must
11576
11607
  // replace any counter-scale left by an imperative camera tick.
11577
11608
  [CHROME_SCALE_CSS_VAR]: chromeScale,
@@ -11842,6 +11873,9 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
11842
11873
  }
11843
11874
  geometry={geometry}
11844
11875
  measuredIframeHeights={measuredIframeHeights}
11876
+ focusedInteract={
11877
+ screen.id === interactScreenId && focusedInteract
11878
+ }
11845
11879
  locked={lockedScreenIdSet.has(screen.id)}
11846
11880
  screenContent={screenContentById.get(screen.id)}
11847
11881
  bootDeferred={bootDeferredScreenIds.has(screen.id)}
@@ -12002,7 +12036,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
12002
12036
  geometry={singleSelectedFrame.geometry}
12003
12037
  chromeScale={chromeScale}
12004
12038
  chromeSettling={chromeSettling}
12005
- handlesEnabled={!readOnly}
12039
+ handlesEnabled={!readOnly && !focusedInteract}
12006
12040
  showRotate
12007
12041
  onStartResize={(handle, event) =>
12008
12042
  beginResize(singleSelectedFrame.id, handle, event)
@@ -13389,6 +13423,7 @@ interface ScreenProps {
13389
13423
  locked: boolean;
13390
13424
  isActive: boolean;
13391
13425
  interactMode: boolean;
13426
+ focusedInteract: boolean;
13392
13427
  isSelected: boolean;
13393
13428
  /** True while the current selection is an element INSIDE this screen (not
13394
13429
  * the screen/frame itself) — see selectedElementScreenId. The screen's own
@@ -13490,6 +13525,7 @@ const Screen = memo(function Screen({
13490
13525
  locked,
13491
13526
  isActive,
13492
13527
  interactMode,
13528
+ focusedInteract,
13493
13529
  isSelected,
13494
13530
  elementSelectedInScreen,
13495
13531
  isTopScreen,
@@ -13538,7 +13574,10 @@ const Screen = memo(function Screen({
13538
13574
  const display = screenDisplayName(screen, metadata);
13539
13575
  const previewUrl = metadata.previewUrl ?? getPreviewUrl(screen.content);
13540
13576
  const externalPreviewPendingOrigin = Boolean(previewUrl && !browserOrigin);
13541
- const previewViewport = getScreenPreviewViewport(metadata, geometry);
13577
+ const previewViewport = getScreenPreviewViewport(
13578
+ focusedInteract ? geometry : metadata,
13579
+ geometry,
13580
+ );
13542
13581
  const suppressNextClick = useRef(false);
13543
13582
  // Overview viewport culling (PF22): mounting only. Unmounting a culled screen
13544
13583
  // would lose its iframe's scroll/form/Alpine state, so it stays mounted and
@@ -13669,7 +13708,9 @@ const Screen = memo(function Screen({
13669
13708
  useEffect(() => {
13670
13709
  onHoverIntent?.(screen.id, hoverWantsLiveEditor);
13671
13710
  }, [hoverWantsLiveEditor, onHoverIntent, screen.id]);
13672
- const frameLabelHeight = FRAME_LABEL_HEIGHT * chromeScale;
13711
+ const frameLabelHeight = focusedInteract
13712
+ ? 0
13713
+ : FRAME_LABEL_HEIGHT * chromeScale;
13673
13714
  const frameScreenWidth = geometry.width / Math.max(chromeScale, 0.001);
13674
13715
  // Keep frame actions inside their own frame so closely spaced screens cannot
13675
13716
  // cover one another. Narrow frames collapse the action to its familiar icon;
@@ -13710,6 +13751,7 @@ const Screen = memo(function Screen({
13710
13751
  transform: geometry.rotation
13711
13752
  ? `rotate(${geometry.rotation}deg)`
13712
13753
  : undefined,
13754
+ visibility: focusedInteract ? "visible" : undefined,
13713
13755
  transformOrigin: `${geometry.width / 2}px ${frameLabelHeight + geometry.height / 2}px`,
13714
13756
  zIndex: isTopScreen
13715
13757
  ? (geometry.z ?? 0) + TOP_SCREEN_Z_BOOST
@@ -13718,7 +13760,10 @@ const Screen = memo(function Screen({
13718
13760
  >
13719
13761
  <div
13720
13762
  className="relative w-full cursor-default"
13721
- style={{ height: frameLabelHeight }}
13763
+ style={{
13764
+ height: frameLabelHeight,
13765
+ display: focusedInteract ? "none" : undefined,
13766
+ }}
13722
13767
  onClick={(e) => {
13723
13768
  e.stopPropagation();
13724
13769
  if (suppressNextClick.current) {
@@ -13839,6 +13884,7 @@ const Screen = memo(function Screen({
13839
13884
  fullViewVisible && "opacity-100",
13840
13885
  )}
13841
13886
  style={{
13887
+ display: focusedInteract ? "none" : undefined,
13842
13888
  maxWidth: fullViewMaxWidth,
13843
13889
  transform: `translate(-50%, -50%) scale(var(${CHROME_SCALE_CSS_VAR}, ${chromeScale}))`,
13844
13890
  transformOrigin: "center center",
@@ -14097,6 +14143,7 @@ const Screen = memo(function Screen({
14097
14143
  <ResizeHandles
14098
14144
  active={false}
14099
14145
  enabled={
14146
+ !focusedInteract &&
14100
14147
  !selectionOutlined &&
14101
14148
  !elementSelectedInScreen &&
14102
14149
  !penActive &&
@@ -14120,7 +14167,9 @@ const Screen = memo(function Screen({
14120
14167
  the screen has breakpointWidths set. Each frame shares the same
14121
14168
  srcdoc content at a different viewport width. The active breakpoint
14122
14169
  is highlighted and clicking a frame header sets the edit scope. */}
14123
- {screen.breakpointWidths && screen.breakpointWidths.length > 0 ? (
14170
+ {!focusedInteract &&
14171
+ screen.breakpointWidths &&
14172
+ screen.breakpointWidths.length > 0 ? (
14124
14173
  <BreakpointPreviewRow
14125
14174
  screen={screen}
14126
14175
  primaryGeometry={geometry}
@@ -14227,6 +14276,7 @@ function areScreenPropsEqual(prev: ScreenProps, next: ScreenProps) {
14227
14276
  sameResolvedMetadata(prev.metadata, next.metadata) &&
14228
14277
  sameFrameGeometry(prev.geometry, next.geometry) &&
14229
14278
  prev.isActive === next.isActive &&
14279
+ prev.focusedInteract === next.focusedInteract &&
14230
14280
  prev.isSelected === next.isSelected &&
14231
14281
  prev.elementSelectedInScreen === next.elementSelectedInScreen &&
14232
14282
  prev.isTopScreen === next.isTopScreen &&
@@ -26,6 +26,10 @@ export const AUTHORED_INLINE_STYLE_PROPERTIES = [
26
26
  "paddingRight",
27
27
  "paddingBottom",
28
28
  "paddingLeft",
29
+ "marginTop",
30
+ "marginRight",
31
+ "marginBottom",
32
+ "marginLeft",
29
33
  "alignItems",
30
34
  "alignContent",
31
35
  "justifyItems",
@@ -23,11 +23,16 @@ import { cn } from "@/lib/utils";
23
23
 
24
24
  import {
25
25
  AutoLayoutMatrix,
26
+ MarginProperties,
26
27
  SizingField,
27
28
  type AutoLayoutFlow,
28
29
  type AutoLayoutGridTrackSizing,
29
30
  type AutoLayoutGridValue,
31
+ type AutoLayoutMargin,
32
+ type AutoLayoutMarginTextValues,
33
+ type AutoLayoutMatrixLabels,
30
34
  type AutoLayoutMatrixValue,
35
+ type AutoLayoutSidesMixed,
31
36
  type ScrubInputChangeMeta,
32
37
  } from "../inspector";
33
38
  import { IconLayoutSettings } from "../inspector/design-icons";
@@ -367,6 +372,71 @@ export function gridValueForElement(element: ElementInfo): AutoLayoutGridValue {
367
372
  };
368
373
  }
369
374
 
375
+ function marginValuesForStyles(
376
+ styles: Record<string, string>,
377
+ inlineStyles?: Record<string, string>,
378
+ ) {
379
+ const marginValue = (property: string) => {
380
+ const authored = inlineStyles?.[property];
381
+ return (
382
+ (isMixedValue(authored) || authored?.trim().toLowerCase() === "auto"
383
+ ? authored
384
+ : styles[property]) || "0"
385
+ );
386
+ };
387
+ const raw = {
388
+ top: marginValue("marginTop"),
389
+ right: marginValue("marginRight"),
390
+ bottom: marginValue("marginBottom"),
391
+ left: marginValue("marginLeft"),
392
+ };
393
+ const value: AutoLayoutMargin = {
394
+ top: parseNumericValue(raw.top),
395
+ right: parseNumericValue(raw.right),
396
+ bottom: parseNumericValue(raw.bottom),
397
+ left: parseNumericValue(raw.left),
398
+ };
399
+ const mixed: AutoLayoutSidesMixed = {
400
+ top: isMixedValue(raw.top),
401
+ right: isMixedValue(raw.right),
402
+ bottom: isMixedValue(raw.bottom),
403
+ left: isMixedValue(raw.left),
404
+ };
405
+ const textValues: AutoLayoutMarginTextValues = {
406
+ top: !mixed.top && raw.top.trim() === "auto" ? "auto" : undefined,
407
+ right: !mixed.right && raw.right.trim() === "auto" ? "auto" : undefined,
408
+ bottom: !mixed.bottom && raw.bottom.trim() === "auto" ? "auto" : undefined,
409
+ left: !mixed.left && raw.left.trim() === "auto" ? "auto" : undefined,
410
+ };
411
+ return { value, mixed, textValues };
412
+ }
413
+
414
+ function marginStylesForSides(
415
+ margin: AutoLayoutMargin,
416
+ sides: Array<keyof AutoLayoutMargin>,
417
+ ): Record<string, string> {
418
+ const styles: Record<string, string> = {};
419
+ for (const side of sides) {
420
+ const property = `margin${side[0].toUpperCase()}${side.slice(1)}`;
421
+ styles[property] = `${margin[side]}px`;
422
+ }
423
+ return styles;
424
+ }
425
+
426
+ function marginInspectorLabels(
427
+ t: ReturnType<typeof useT>,
428
+ ): Partial<AutoLayoutMatrixLabels> {
429
+ return {
430
+ margin: t("editPanel.labels.margin"),
431
+ linkMargin: t("editPanel.labels.linkMarginSides"),
432
+ unlinkMargin: t("editPanel.labels.unlinkMarginSides"),
433
+ marginTop: t("editPanel.labels.marginTop"),
434
+ marginRight: t("editPanel.labels.marginRight"),
435
+ marginBottom: t("editPanel.labels.marginBottom"),
436
+ marginLeft: t("editPanel.labels.marginLeft"),
437
+ };
438
+ }
439
+
370
440
  /**
371
441
  * Style patch for a Grid-control change: template writes plus gap, with
372
442
  * `gridAutoFlow: "row"` included only when converting a non-grid element
@@ -405,7 +475,10 @@ function FlexContainerControls({
405
475
  onApplyLayoutFlow?: ApplyLayoutFlowHandler;
406
476
  showSizingControls: boolean;
407
477
  }) {
478
+ const t = useT();
408
479
  const styles = element.computedStyles;
480
+ const marginLabels = marginInspectorLabels(t);
481
+ const marginProperties = marginValuesForStyles(styles, element.inlineStyles);
409
482
  // The element's CURRENT layout flow as authored in code, read from its own
410
483
  // computed `display`: block/flow-root/grid/etc. = "normal flow",
411
484
  // flex/inline-flex = auto layout. We forward it so the AutoLayoutMatrix Flow
@@ -542,6 +615,9 @@ function FlexContainerControls({
542
615
  left: isMixedValue(styles.paddingLeft),
543
616
  },
544
617
  paddingLinked,
618
+ margin: marginProperties.value,
619
+ marginMixed: marginProperties.mixed,
620
+ marginTextValues: marginProperties.textValues,
545
621
  childSizing: {
546
622
  horizontal: inferElementSizing(element, "horizontal"),
547
623
  vertical: inferElementSizing(element, "vertical"),
@@ -570,6 +646,7 @@ function FlexContainerControls({
570
646
  <div className="space-y-2">
571
647
  <AutoLayoutMatrix
572
648
  value={autoLayoutValue}
649
+ labels={marginLabels}
573
650
  onFlowChange={(flow) => {
574
651
  const nodeId = element.sourceId ?? element.pendingNodeId;
575
652
  // A raw display:block leaves the children in flow, so they re-stack
@@ -694,6 +771,21 @@ function FlexContainerControls({
694
771
  // each linked axis's representative value and applies both sides on
695
772
  // the next real field edit, so no style write belongs here.
696
773
  }}
774
+ onMarginChange={(nextMargin, meta, changedSides) => {
775
+ const patch = marginStylesForSides(nextMargin, changedSides);
776
+ const changeMeta =
777
+ meta &&
778
+ (meta.relativeDelta !== undefined || meta.relativeExpression)
779
+ ? { ...meta, relativeDeltaProperties: Object.keys(patch) }
780
+ : meta;
781
+ if (onStylesChange) {
782
+ onStylesChange(patch, changeMeta);
783
+ return;
784
+ }
785
+ Object.entries(patch).forEach(([property, value]) =>
786
+ onStyleChange(property, value, changeMeta),
787
+ );
788
+ }}
697
789
  onClipContentChange={(clipContent) =>
698
790
  onStyleChange("overflow", clipContent ? "hidden" : "visible")
699
791
  }
@@ -934,6 +1026,11 @@ export function LayoutContextProperties({
934
1026
  const availableSizing = availableSizingForElement(element);
935
1027
  const isContainer = isContainerElement(element);
936
1028
  const aspectLock = useAspectRatioLock(element);
1029
+ const marginLabels = marginInspectorLabels(t);
1030
+ const marginProperties = marginValuesForStyles(
1031
+ element.computedStyles,
1032
+ element.inlineStyles,
1033
+ );
937
1034
 
938
1035
  const childActions =
939
1036
  flexChild || gridChild ? (
@@ -1170,6 +1267,28 @@ export function LayoutContextProperties({
1170
1267
  </Tooltip>
1171
1268
  </InspectorGridCell>
1172
1269
  </InspectorGrid>
1270
+ <MarginProperties
1271
+ key={elementStableKey(element)}
1272
+ value={marginProperties.value}
1273
+ mixed={marginProperties.mixed}
1274
+ textValues={marginProperties.textValues}
1275
+ labels={marginLabels}
1276
+ onChange={(margin, meta, changedSides) => {
1277
+ const patch = marginStylesForSides(margin, changedSides);
1278
+ const changeMeta =
1279
+ meta &&
1280
+ (meta.relativeDelta !== undefined || meta.relativeExpression)
1281
+ ? { ...meta, relativeDeltaProperties: Object.keys(patch) }
1282
+ : meta;
1283
+ if (onStylesChange) {
1284
+ onStylesChange(patch, changeMeta);
1285
+ return;
1286
+ }
1287
+ Object.entries(patch).forEach(([property, value]) =>
1288
+ onStyleChange(property, value, changeMeta),
1289
+ );
1290
+ }}
1291
+ />
1173
1292
  </PanelSection>
1174
1293
  );
1175
1294
  }