@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
@@ -67,6 +67,7 @@ export default function Present() {
67
67
  const location = useLocation();
68
68
  const navigate = useNavigate();
69
69
  const [searchParams] = useSearchParams();
70
+ const reviewEmbed = searchParams.get("reviewEmbed") === "1";
70
71
  const { session } = useSession();
71
72
  const [currentPage, setCurrentPage] = useState(0);
72
73
  const [commentMode, setCommentMode] = useState(false);
@@ -107,7 +108,7 @@ export default function Present() {
107
108
  newestFirst: true,
108
109
  limit: 500,
109
110
  },
110
- { enabled: Boolean(id) },
111
+ { enabled: Boolean(id) && !reviewEmbed },
111
112
  );
112
113
  const canPost = Boolean(
113
114
  session?.email &&
@@ -164,6 +165,7 @@ export default function Present() {
164
165
  );
165
166
 
166
167
  useEffect(() => {
168
+ if (reviewEmbed) return;
167
169
  const commentId =
168
170
  reviewThreadIdFromHash(location.hash) ?? searchParams.get("comment");
169
171
  const comments = reviewQuery.data?.comments ?? [];
@@ -191,6 +193,7 @@ export default function Present() {
191
193
  handleReviewThreadSelect,
192
194
  reviewQuery.data?.comments,
193
195
  location.hash,
196
+ reviewEmbed,
194
197
  searchParams,
195
198
  ]);
196
199
 
@@ -198,10 +201,13 @@ export default function Present() {
198
201
  const handleKeyDown = useCallback(
199
202
  (e: KeyboardEvent) => {
200
203
  if (e.key === "Escape") {
201
- const action = resolvePresentEscapeAction({
202
- commentsOpen,
203
- commentMode,
204
- });
204
+ const action = resolvePresentEscapeAction(
205
+ {
206
+ commentsOpen,
207
+ commentMode,
208
+ },
209
+ reviewEmbed,
210
+ );
205
211
  if (action === "close-comments") setCommentsOpen(false);
206
212
  if (action === "exit-presentation") void navigate(`/design/${id}`);
207
213
  // ReviewCanvasPins owns "defer-to-comment-mode" so it can dismiss an
@@ -222,7 +228,7 @@ export default function Present() {
222
228
  setCurrentPage((p) => Math.max(p - 1, 0));
223
229
  }
224
230
  },
225
- [commentMode, commentsOpen, files.length, id, navigate],
231
+ [commentMode, commentsOpen, files.length, id, navigate, reviewEmbed],
226
232
  );
227
233
 
228
234
  useEffect(() => {
@@ -276,88 +282,94 @@ export default function Present() {
276
282
  className="h-full w-full border-0"
277
283
  title={`${design.title} — ${activeFile.filename}`}
278
284
  />
279
- <ReviewCanvasPins
280
- active={commentMode}
281
- onClose={() => setCommentMode(false)}
282
- canvasSelector=".present-review-canvas"
283
- resourceType="design"
284
- resourceId={id}
285
- targetId={activeFile.id}
286
- canPost={canPost}
287
- canResolve={canResolve}
288
- currentUserEmail={session?.email}
289
- focusRequest={reviewFocusRequest}
290
- />
291
- </div>
292
-
293
- <div className="fixed right-4 top-4 z-[70] flex items-center gap-2">
294
- <ReviewStatusBadge
295
- status={reviewQuery.data?.reviewStatus?.status ?? "draft"}
296
- />
297
- <Button
298
- type="button"
299
- variant="secondary"
300
- size="sm"
301
- className="gap-1.5 rounded-full bg-black/75 text-white shadow-lg hover:bg-black"
302
- onClick={() => {
303
- setCommentMode(false);
304
- setCommentsOpen(true);
305
- }}
306
- >
307
- <IconMessageCircle className="size-4" />
308
- {t("review.presentComments")}
309
- {reviewCommentCount > 0 ? ` · ${reviewCommentCount}` : ""}
310
- </Button>
285
+ {!reviewEmbed ? (
286
+ <ReviewCanvasPins
287
+ active={commentMode}
288
+ onClose={() => setCommentMode(false)}
289
+ canvasSelector=".present-review-canvas"
290
+ resourceType="design"
291
+ resourceId={id}
292
+ targetId={activeFile.id}
293
+ canPost={canPost}
294
+ canResolve={canResolve}
295
+ currentUserEmail={session?.email}
296
+ focusRequest={reviewFocusRequest}
297
+ />
298
+ ) : null}
311
299
  </div>
312
300
 
313
- <Sheet open={commentsOpen} onOpenChange={setCommentsOpen}>
314
- <SheetContent
315
- side="right"
316
- className="flex w-[min(92vw,380px)] flex-col overflow-hidden p-0"
317
- >
318
- <SheetHeader className="border-b border-border px-4 py-3">
319
- <SheetTitle className="flex items-center gap-2 text-sm">
301
+ {!reviewEmbed ? (
302
+ <>
303
+ <div className="fixed right-4 top-4 z-[70] flex items-center gap-2">
304
+ <ReviewStatusBadge
305
+ status={reviewQuery.data?.reviewStatus?.status ?? "draft"}
306
+ />
307
+ <Button
308
+ type="button"
309
+ variant="secondary"
310
+ size="sm"
311
+ className="gap-1.5 rounded-full bg-foreground/75 text-background shadow-lg hover:bg-foreground"
312
+ onClick={() => {
313
+ setCommentMode(false);
314
+ setCommentsOpen(true);
315
+ }}
316
+ >
320
317
  <IconMessageCircle className="size-4" />
321
318
  {t("review.presentComments")}
322
- </SheetTitle>
323
- <SheetDescription className="sr-only">
324
- {t("review.commentsTitle")}
325
- </SheetDescription>
326
- </SheetHeader>
327
- <ReviewCommentsPanel
328
- designId={id}
329
- canComment={canPost}
330
- canResolve={canResolve}
331
- currentTargetId={activeFile?.id ?? null}
332
- currentUserEmail={session?.email}
333
- canDeleteComment={(comment) =>
334
- canResolve ||
335
- ("canDelete" in comment && comment.canDelete === true) ||
336
- comment.authorEmail === session?.email
337
- }
338
- signInHref={signInHref}
339
- onSelectThread={handleReviewThreadSelect}
340
- className="min-h-0 flex-1"
341
- />
342
- {canPost ? (
343
- <div className="shrink-0 border-t border-border p-3">
344
- <Button
345
- type="button"
346
- variant="outline"
347
- size="sm"
348
- className="w-full gap-1.5"
349
- onClick={() => {
350
- setCommentsOpen(false);
351
- setCommentMode(true);
352
- }}
353
- >
354
- <IconMessageCircle className="size-3.5" />
355
- {t("review.presentCommentMode")}
356
- </Button>
357
- </div>
358
- ) : null}
359
- </SheetContent>
360
- </Sheet>
319
+ {reviewCommentCount > 0 ? ` · ${reviewCommentCount}` : ""}
320
+ </Button>
321
+ </div>
322
+
323
+ <Sheet open={commentsOpen} onOpenChange={setCommentsOpen}>
324
+ <SheetContent
325
+ side="right"
326
+ className="flex w-[min(92vw,380px)] flex-col overflow-hidden p-0"
327
+ >
328
+ <SheetHeader className="border-b border-border px-4 py-3">
329
+ <SheetTitle className="flex items-center gap-2 text-sm">
330
+ <IconMessageCircle className="size-4" />
331
+ {t("review.presentComments")}
332
+ </SheetTitle>
333
+ <SheetDescription className="sr-only">
334
+ {t("review.commentsTitle")}
335
+ </SheetDescription>
336
+ </SheetHeader>
337
+ <ReviewCommentsPanel
338
+ designId={id}
339
+ canComment={canPost}
340
+ canResolve={canResolve}
341
+ currentTargetId={activeFile?.id ?? null}
342
+ currentUserEmail={session?.email}
343
+ canDeleteComment={(comment) =>
344
+ canResolve ||
345
+ ("canDelete" in comment && comment.canDelete === true) ||
346
+ comment.authorEmail === session?.email
347
+ }
348
+ signInHref={signInHref}
349
+ onSelectThread={handleReviewThreadSelect}
350
+ className="min-h-0 flex-1"
351
+ />
352
+ {canPost ? (
353
+ <div className="shrink-0 border-t border-border p-3">
354
+ <Button
355
+ type="button"
356
+ variant="outline"
357
+ size="sm"
358
+ className="w-full gap-1.5"
359
+ onClick={() => {
360
+ setCommentsOpen(false);
361
+ setCommentMode(true);
362
+ }}
363
+ >
364
+ <IconMessageCircle className="size-3.5" />
365
+ {t("review.presentCommentMode")}
366
+ </Button>
367
+ </div>
368
+ ) : null}
369
+ </SheetContent>
370
+ </Sheet>
371
+ </>
372
+ ) : null}
361
373
 
362
374
  {/* Page indicator */}
363
375
  {files.length > 1 && (
@@ -375,9 +387,11 @@ export default function Present() {
375
387
  )}
376
388
 
377
389
  {/* Exit hint */}
378
- <div className="fixed left-4 top-4 text-xs text-white/20">
379
- {t("pages.presentExitHint")}
380
- </div>
390
+ {!reviewEmbed ? (
391
+ <div className="fixed left-4 top-4 text-xs text-foreground/20">
392
+ {t("pages.presentExitHint")}
393
+ </div>
394
+ ) : null}
381
395
  </div>
382
396
  );
383
397
  }
@@ -25,7 +25,9 @@ import {
25
25
  penCornerRadiusFromAttribute,
26
26
  serializePenPath,
27
27
  serializePenNodes,
28
+ scalePenPathToGeometry,
28
29
  serializeRoundedPenPath,
30
+ translatePenPath,
29
31
  type PenPath,
30
32
  } from "@shared/pen-path";
31
33
 
@@ -259,6 +261,33 @@ export function writeBackVectorEditedPenPath(
259
261
  }
260
262
  }
261
263
 
264
+ /** Axis-aligned scale plus the authored-to-screen-content translation. */
265
+ function penPathScreenContentMapping(svg: SVGSVGElement): {
266
+ scaleX: number;
267
+ scaleY: number;
268
+ offset: { x: number; y: number };
269
+ viewBox: { x: number; y: number; width: number; height: number };
270
+ } | null {
271
+ const viewBox = parseViewBox(svg.getAttribute("viewBox"));
272
+ const matrix = svg.getScreenCTM();
273
+ if (!viewBox || !matrix) return null;
274
+ if (
275
+ Math.abs(matrix.b) > 0.001 ||
276
+ Math.abs(matrix.c) > 0.001 ||
277
+ !(matrix.a > 0) ||
278
+ !(matrix.d > 0)
279
+ ) {
280
+ return null;
281
+ }
282
+ const view = svg.ownerDocument.defaultView;
283
+ const offset = {
284
+ x: matrix.a * viewBox.x + matrix.e + (view?.scrollX ?? 0) - viewBox.x,
285
+ y: matrix.d * viewBox.y + matrix.f + (view?.scrollY ?? 0) - viewBox.y,
286
+ };
287
+ if (!Number.isFinite(offset.x) || !Number.isFinite(offset.y)) return null;
288
+ return { scaleX: matrix.a, scaleY: matrix.d, offset, viewBox };
289
+ }
290
+
262
291
  /**
263
292
  * Returns the translation between an SVG PenPath's authored coordinates and
264
293
  * its current screen-content position. Vector edit handles use screen-content
@@ -273,25 +302,57 @@ export function penPathScreenContentOffset(svg: SVGSVGElement): {
273
302
  x: number;
274
303
  y: number;
275
304
  } | null {
276
- const viewBox = parseViewBox(svg.getAttribute("viewBox"));
277
- const matrix = svg.getScreenCTM();
278
- if (!viewBox || !matrix) return null;
305
+ const mapping = penPathScreenContentMapping(svg);
279
306
  if (
280
- Math.abs(matrix.a - 1) > 0.001 ||
281
- Math.abs(matrix.b) > 0.001 ||
282
- Math.abs(matrix.c) > 0.001 ||
283
- Math.abs(matrix.d - 1) > 0.001
307
+ !mapping ||
308
+ Math.abs(mapping.scaleX - 1) > 0.001 ||
309
+ Math.abs(mapping.scaleY - 1) > 0.001
284
310
  ) {
285
311
  return null;
286
312
  }
287
- const view = svg.ownerDocument.defaultView;
288
- const x = matrix.a * viewBox.x + matrix.c * viewBox.y + matrix.e;
289
- const y = matrix.b * viewBox.x + matrix.d * viewBox.y + matrix.f;
290
- const offset = {
291
- x: x + (view?.scrollX ?? 0) - viewBox.x,
292
- y: y + (view?.scrollY ?? 0) - viewBox.y,
313
+ return mapping.offset;
314
+ }
315
+
316
+ /**
317
+ * A drawn vector's nodes in screen-content space for vector edit. A resize or
318
+ * K-scale leaves the viewBox behind, so the SVG draws at a scale; scaling the
319
+ * nodes about the viewBox origin lets writeBackVectorEditedPenPath re-base the
320
+ * SVG at 1:1 on commit, where `sourceOffset` maps them back.
321
+ */
322
+ export function penPathForVectorEdit(
323
+ svg: SVGSVGElement,
324
+ path: PenPath,
325
+ renderOffset: { x: number; y: number },
326
+ ): { path: PenPath; sourceOffset: { x: number; y: number } } | null {
327
+ const mapping = penPathScreenContentMapping(svg);
328
+ if (!mapping) return null;
329
+ const { viewBox, scaleX, scaleY } = mapping;
330
+ // Only the box-vs-viewBox stretch is re-based; a CSS transform survives
331
+ // write-back, so baking its scale into the nodes would apply it twice.
332
+ const boxStretchMatches = (scale: number, box: string, extent: number) => {
333
+ const size = parsePixelValue(box);
334
+ if (size === null) return false;
335
+ return extent === 0 || Math.abs(scale - size / extent) <= 0.001;
336
+ };
337
+ if (
338
+ !boxStretchMatches(scaleX, svg.style.width, viewBox.width) ||
339
+ !boxStretchMatches(scaleY, svg.style.height, viewBox.height)
340
+ ) {
341
+ return null;
342
+ }
343
+ const sourceOffset = {
344
+ x: mapping.offset.x - renderOffset.x,
345
+ y: mapping.offset.y - renderOffset.y,
346
+ };
347
+ const unscaled = scalePenPathToGeometry(path, viewBox, {
348
+ ...viewBox,
349
+ width: viewBox.width * scaleX,
350
+ height: viewBox.height * scaleY,
351
+ });
352
+ return {
353
+ path: translatePenPath(unscaled, sourceOffset.x, sourceOffset.y),
354
+ sourceOffset,
293
355
  };
294
- return Number.isFinite(offset.x) && Number.isFinite(offset.y) ? offset : null;
295
356
  }
296
357
 
297
358
  /**
@@ -67,6 +67,7 @@ a module breaks those — re-point the spec in the same commit.
67
67
  | `screen-visual-style-change.ts` | Canvas style change addressed by explicit `screenId` |
68
68
  | `record-pending-visual-style-edit.ts` | Record an uncommitted style gesture, stamped with breakpoint scope |
69
69
  | `change-selected-z-index.ts` | Raise or lower the selection's z-order |
70
+ | `swap-fill-stroke.ts` | Shift+X: swap fill and stroke, folding paint opacity into the colours |
70
71
 
71
72
  ## Text
72
73
 
@@ -128,7 +128,7 @@ export function runCommitRelativeStyleDeltaToSelectedLayers(
128
128
  reportLinkedEditUnavailable,
129
129
  setSelectedElement,
130
130
  }: CommitRelativeStyleDeltaToSelectedLayersArgs,
131
- property: string,
131
+ property: string | string[],
132
132
  operation: number | ScrubRelativeExpression,
133
133
  pendingUndoGestureId?: string,
134
134
  ) {
@@ -142,8 +142,37 @@ export function runCommitRelativeStyleDeltaToSelectedLayers(
142
142
  !effectiveLayerState.hiddenIds.has(target.layerId),
143
143
  );
144
144
  if (targets.length === 0) return false;
145
+ const properties = [
146
+ ...new Set(Array.isArray(property) ? property : [property]),
147
+ ];
148
+ if (properties.length === 0) return false;
149
+ const relativeStylesByTarget = new Map<
150
+ SelectedLayerTarget,
151
+ Record<string, string>
152
+ >();
153
+ const stylesForTarget = (target: SelectedLayerTarget) => {
154
+ const styles: Record<string, string> = {};
155
+ for (const property of properties) {
156
+ const writeProperty = property === "rotation" ? "transform" : property;
157
+ const currentValue =
158
+ property === "rotation"
159
+ ? (target.elementInfo.inlineStyles?.transform ??
160
+ target.elementInfo.computedStyles.transform)
161
+ : (target.elementInfo.computedStyles[
162
+ property as keyof typeof target.elementInfo.computedStyles
163
+ ] as string | undefined);
164
+ const nextValue =
165
+ property === "rotation"
166
+ ? applyRelativeRotationToTransform(currentValue, operation)
167
+ : typeof operation === "number"
168
+ ? applyRelativeDeltaToStyleValue(currentValue, operation)
169
+ : applyRelativeExpressionToStyleValue(currentValue, operation);
170
+ if (nextValue === null) return null;
171
+ styles[writeProperty] = nextValue;
172
+ }
173
+ return styles;
174
+ };
145
175
 
146
- const writeProperty = property === "rotation" ? "transform" : property;
147
176
  const styleTargets: Array<{
148
177
  fileId: string;
149
178
  nodeId: string;
@@ -180,29 +209,18 @@ export function runCommitRelativeStyleDeltaToSelectedLayers(
180
209
  targetsResolved = false;
181
210
  return;
182
211
  }
183
- const currentValue =
184
- property === "rotation"
185
- ? (target.elementInfo.inlineStyles?.transform ??
186
- target.elementInfo.computedStyles.transform)
187
- : (target.elementInfo.computedStyles[
188
- property as keyof typeof target.elementInfo.computedStyles
189
- ] as string | undefined);
190
- const nextValue =
191
- property === "rotation"
192
- ? applyRelativeRotationToTransform(currentValue, operation)
193
- : typeof operation === "number"
194
- ? applyRelativeDeltaToStyleValue(currentValue, operation)
195
- : applyRelativeExpressionToStyleValue(currentValue, operation);
196
- if (nextValue === null) {
212
+ const styles = stylesForTarget(target);
213
+ if (!styles) {
197
214
  targetsResolved = false;
198
215
  return;
199
216
  }
217
+ relativeStylesByTarget.set(target, styles);
200
218
  if (linkedComponentRootForNode(node, projection))
201
219
  includesLinkedTarget = true;
202
220
  styleTargets.push({
203
221
  fileId: target.fileId,
204
222
  nodeId: node.dataAttributes["data-agent-native-node-id"] ?? "",
205
- styles: { [writeProperty]: nextValue },
223
+ styles,
206
224
  });
207
225
  });
208
226
  if (includesLinkedTarget) {
@@ -293,7 +311,7 @@ export function runCommitRelativeStyleDeltaToSelectedLayers(
293
311
  });
294
312
 
295
313
  let appliedAny = false;
296
- const appliedValueByLayerId = new Map<string, string>();
314
+ const appliedStylesByLayerId = new Map<string, Record<string, string>>();
297
315
  const acceptedNodeByLayerId = new Map<
298
316
  string,
299
317
  NonNullable<ReturnType<typeof mapAcceptedSelectionNode>>
@@ -312,20 +330,8 @@ export function runCommitRelativeStyleDeltaToSelectedLayers(
312
330
  let nextContent = baseContent;
313
331
  let projection = buildCodeLayerProjection(nextContent, { source });
314
332
  fileTargets.forEach((target) => {
315
- const currentValue =
316
- property === "rotation"
317
- ? (target.elementInfo.inlineStyles?.transform ??
318
- target.elementInfo.computedStyles.transform)
319
- : (target.elementInfo.computedStyles[
320
- property as keyof typeof target.elementInfo.computedStyles
321
- ] as string | undefined);
322
- const nextValue =
323
- property === "rotation"
324
- ? applyRelativeRotationToTransform(currentValue, operation)
325
- : typeof operation === "number"
326
- ? applyRelativeDeltaToStyleValue(currentValue, operation)
327
- : applyRelativeExpressionToStyleValue(currentValue, operation);
328
- if (nextValue === null) return;
333
+ const styles = relativeStylesByTarget.get(target);
334
+ if (!styles) return;
329
335
  const sourceId = bridgeSourceIdForCodeLayerNode(target.node);
330
336
  const selector = preferredCodeLayerSelector(target.node);
331
337
  const node =
@@ -336,22 +342,28 @@ export function runCommitRelativeStyleDeltaToSelectedLayers(
336
342
  if (!node) return;
337
343
  // §6.4 — relative-delta commits (mixed-value arrow steps) route
338
344
  // through the same breakpoint scoping as absolute commits.
339
- const patch = applyScopedVisualStyleEdit({
340
- content: nextContent,
341
- target: { nodeId: node.id },
342
- property: writeProperty,
343
- value: nextValue,
344
- source,
345
- upperBoundPx: activeBreakpointUpperBoundPx,
346
- lowerBoundPx:
347
- responsiveEditScopeRef.current === "only"
348
- ? activeBreakpointWidthStateRef.current
349
- : null,
350
- });
351
- if (patch.result.status !== "applied") return;
352
- nextContent = patch.content;
353
- projection = patch.projection;
354
- appliedValueByLayerId.set(target.layerId, nextValue);
345
+ let targetNextContent = nextContent;
346
+ let targetProjection = projection;
347
+ for (const [writeProperty, value] of Object.entries(styles)) {
348
+ const patch = applyScopedVisualStyleEdit({
349
+ content: targetNextContent,
350
+ target: { nodeId: node.id },
351
+ property: writeProperty,
352
+ value,
353
+ source,
354
+ upperBoundPx: activeBreakpointUpperBoundPx,
355
+ lowerBoundPx:
356
+ responsiveEditScopeRef.current === "only"
357
+ ? activeBreakpointWidthStateRef.current
358
+ : null,
359
+ });
360
+ if (patch.result.status !== "applied") return;
361
+ targetNextContent = patch.content;
362
+ targetProjection = patch.projection;
363
+ }
364
+ nextContent = targetNextContent;
365
+ projection = targetProjection;
366
+ appliedStylesByLayerId.set(target.layerId, styles);
355
367
  });
356
368
  if (nextContent === baseContent) return;
357
369
  appliedAny = true;
@@ -386,10 +398,10 @@ export function runCommitRelativeStyleDeltaToSelectedLayers(
386
398
 
387
399
  if (appliedAny) {
388
400
  const primaryTarget = targets[targets.length - 1];
389
- const primaryValue = primaryTarget
390
- ? appliedValueByLayerId.get(primaryTarget.layerId)
401
+ const primaryStyles = primaryTarget
402
+ ? appliedStylesByLayerId.get(primaryTarget.layerId)
391
403
  : undefined;
392
- if (primaryTarget && primaryValue !== undefined) {
404
+ if (primaryTarget && primaryStyles !== undefined) {
393
405
  const acceptedPrimaryTarget = acceptedNodeByLayerId.get(
394
406
  primaryTarget.layerId,
395
407
  );
@@ -412,14 +424,15 @@ export function runCommitRelativeStyleDeltaToSelectedLayers(
412
424
  ...base,
413
425
  computedStyles: {
414
426
  ...base.computedStyles,
415
- [writeProperty]: primaryValue,
427
+ ...primaryStyles,
416
428
  },
417
- inlineStyles: patchAuthoredInlineStyles(base.inlineStyles, {
418
- [writeProperty]: primaryValue,
419
- }),
429
+ inlineStyles: patchAuthoredInlineStyles(
430
+ base.inlineStyles,
431
+ primaryStyles,
432
+ ),
420
433
  authoredSizeStyles: clearAuthoredSizeStylesForCommit(
421
434
  base.authoredSizeStyles,
422
- { [writeProperty]: primaryValue },
435
+ primaryStyles,
423
436
  ),
424
437
  };
425
438
  });
@@ -34,6 +34,8 @@ export interface ModeChangeArgs {
34
34
  setMode: Dispatch<SetStateAction<EditorMode>>;
35
35
  setPinMode: Dispatch<SetStateAction<boolean>>;
36
36
  setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
37
+ overviewInteractScreenId: string | null;
38
+ setOverviewInteractScreenId: Dispatch<SetStateAction<string | null>>;
37
39
  t: (key: string, options?: Record<string, unknown>) => string;
38
40
  viewModeRef: RefObject<"single" | "overview">;
39
41
  }
@@ -56,6 +58,8 @@ export function runModeChange(
56
58
  setMode,
57
59
  setPinMode,
58
60
  setSelectedElement,
61
+ overviewInteractScreenId,
62
+ setOverviewInteractScreenId,
59
63
  t,
60
64
  viewModeRef,
61
65
  }: ModeChangeArgs,
@@ -94,14 +98,26 @@ export function runModeChange(
94
98
  viewMode: viewModeRef.current,
95
99
  });
96
100
  if (routing === "enter-single-interact") {
101
+ setOverviewInteractScreenId(nextActiveFile!.id);
97
102
  enterSingleScreen(nextActiveFile?.id);
98
103
  return;
99
104
  }
100
105
  if (routing === "enter-overview") {
106
+ setOverviewInteractScreenId(null);
101
107
  if (options?.targetFileId) setActiveFileId(options.targetFileId);
102
108
  enterOverviewFromZoom(next);
103
109
  return;
104
110
  }
111
+ if (next === "interact" && overviewInteractScreenId) {
112
+ setOverviewInteractScreenId(nextActiveFile!.id);
113
+ if (
114
+ options?.targetFileId &&
115
+ nextActiveFile!.id !== overviewInteractScreenId
116
+ ) {
117
+ enterSingleScreen(nextActiveFile!.id);
118
+ return;
119
+ }
120
+ }
105
121
  if (options?.targetFileId) setActiveFileId(options.targetFileId);
106
122
  setMode(next);
107
123
  setSelectedElement(null);
@@ -1,5 +1,8 @@
1
1
  import type { ScaleAnchor } from "@/components/design/edit-panel/scale-properties";
2
- import { findCanvasIframeForScreen } from "@/components/design/multi-screen/iframe-targeting";
2
+ import {
3
+ findCanvasIframeForScreen,
4
+ getBreakpointIframeId,
5
+ } from "@/components/design/multi-screen/iframe-targeting";
3
6
  import type { ElementInfo } from "@/components/design/types";
4
7
 
5
8
  /**
@@ -11,6 +14,7 @@ export function runScaleSelection(
11
14
  args: {
12
15
  selectedElement: ElementInfo | null;
13
16
  boardFileId: string | null | undefined;
17
+ activeBreakpointWidthPx: number | undefined;
14
18
  fallbackIframe: HTMLIFrameElement | null;
15
19
  },
16
20
  factor: number,
@@ -19,14 +23,25 @@ export function runScaleSelection(
19
23
  const selector = args.selectedElement?.selector;
20
24
  if (!selector) return;
21
25
  const screenId = args.selectedElement?.sourceLayerIdentity?.screenId;
26
+ // While a breakpoint is being edited, its own frame holds the selection
27
+ // at that width; the screen's primary frame would scale the wrong size.
28
+ const breakpointIframe =
29
+ screenId && args.activeBreakpointWidthPx !== undefined
30
+ ? findCanvasIframeForScreen(
31
+ document.body,
32
+ getBreakpointIframeId(screenId, args.activeBreakpointWidthPx),
33
+ )
34
+ : null;
22
35
  const iframe =
36
+ breakpointIframe ??
23
37
  (screenId
24
38
  ? findCanvasIframeForScreen(
25
39
  document.body,
26
40
  screenId,
27
41
  args.boardFileId ?? undefined,
28
42
  )
29
- : null) ?? args.fallbackIframe;
43
+ : null) ??
44
+ args.fallbackIframe;
30
45
  iframe?.contentWindow?.postMessage(
31
46
  {
32
47
  type: "agent-native:scale-selection",