@agent-native/core 0.153.10 → 0.154.1

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 (149) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/AGENTS.md +20 -30
  3. package/corpus/templates/analytics/actions/search-dashboard-references.ts +58 -0
  4. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
  5. package/corpus/templates/analytics/app/lib/utils.ts +1 -1
  6. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewSqlPopover.tsx +1 -1
  7. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +2 -0
  8. package/corpus/templates/analytics/server/lib/dashboards-store.ts +366 -3
  9. package/corpus/templates/analytics/server/lib/real-data-actions.ts +3 -1
  10. package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -2
  11. package/corpus/templates/calendar/app/components/calendar/EventDialog.tsx +1 -1
  12. package/corpus/templates/calendar/app/lib/utils.ts +1 -1
  13. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +32 -5
  14. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +15 -15
  15. package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
  16. package/corpus/templates/clips/app/lib/utils.ts +5 -6
  17. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +43 -8
  18. package/corpus/templates/clips/desktop/src/app.tsx +26 -12
  19. package/corpus/templates/clips/desktop/src/lib/recorder.ts +23 -9
  20. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +261 -11
  21. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +21 -3
  22. package/corpus/templates/clips/shared/mic-audio-cleanup.ts +350 -0
  23. package/corpus/templates/content/actions/resolve-content-landing.ts +162 -32
  24. package/corpus/templates/content/app/i18n-data.ts +10 -10
  25. package/corpus/templates/content/scripts/check-native-deps.mjs +181 -15
  26. package/corpus/templates/content/shared/content-landing.ts +5 -0
  27. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +79 -7
  28. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +14 -2
  29. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +2 -2
  30. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +35 -11
  31. package/corpus/templates/design/actions/get-design-system.ts +103 -3
  32. package/corpus/templates/design/actions/import-design-tokens.ts +37 -8
  33. package/corpus/templates/design/actions/import-github.ts +32 -138
  34. package/corpus/templates/design/actions/index-design-system-with-builder.ts +49 -16
  35. package/corpus/templates/design/actions/present-design-variants.ts +93 -4
  36. package/corpus/templates/design/actions/run-design-audit.ts +229 -0
  37. package/corpus/templates/design/actions/sync-design-system-with-builder.ts +64 -0
  38. package/corpus/templates/design/app/components/design/code-workbench/commands.ts +2 -2
  39. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +17 -4
  40. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +5 -1
  41. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +5 -1
  42. package/corpus/templates/design/app/hooks/use-question-flow.ts +81 -5
  43. package/corpus/templates/design/app/i18n/en-US.ts +1 -1
  44. package/corpus/templates/design/app/i18n-data.ts +32 -1
  45. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +10 -10
  46. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +83 -11
  47. package/corpus/templates/design/app/pages/DesignSystems.tsx +14 -0
  48. package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +22 -0
  49. package/corpus/templates/design/changelog/2026-08-13-a-screen-that-would-render-behind-an-invisible-alpine-overla.md +6 -0
  50. package/corpus/templates/design/changelog/2026-08-13-attach-a-ui-screenshot-and-the-design-is-reproduced-not-rein.md +6 -0
  51. package/corpus/templates/design/changelog/2026-08-13-linking-a-design-system-means-you-never-get-generic-directio.md +6 -0
  52. package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-reports-when-a-screen-ignores-its-linke.md +6 -0
  53. package/corpus/templates/design/changelog/2026-08-13-the-design-system-preview-now-shows-how-many-named-tokens-we.md +6 -0
  54. package/corpus/templates/design/changelog/2026-08-13-your-design-system-prompt-and-reference-screenshots-now-reac.md +6 -0
  55. package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +18 -2
  56. package/corpus/templates/design/shared/design-review.ts +1 -0
  57. package/corpus/templates/design/shared/html-integrity.ts +171 -0
  58. package/corpus/templates/mail/app/lib/utils.ts +1 -1
  59. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +10 -6
  60. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +41 -3
  61. package/corpus/templates/slides/actions/patch-deck.ts +12 -3
  62. package/corpus/templates/slides/actions/sync-design-system-with-builder.ts +64 -0
  63. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
  64. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +193 -20
  65. package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +73 -1
  66. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +64 -34
  67. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +34 -41
  68. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +119 -91
  69. package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +16 -0
  70. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +15 -0
  71. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +158 -31
  72. package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +30 -14
  73. package/corpus/templates/slides/app/components/editor/SpeakerNotesPanel.tsx +2 -2
  74. package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +4 -1
  75. package/corpus/templates/slides/app/components/editor/rich-text-selection.ts +14 -0
  76. package/corpus/templates/slides/app/components/editor/slide-text-targets.ts +24 -0
  77. package/corpus/templates/slides/app/context/DeckContext.tsx +19 -31
  78. package/corpus/templates/slides/app/global.css +24 -0
  79. package/corpus/templates/slides/app/hooks/use-navigation-state.ts +20 -17
  80. package/corpus/templates/slides/app/i18n/en-US.ts +19 -10
  81. package/corpus/templates/slides/app/lib/deck-filter.ts +47 -0
  82. package/corpus/templates/slides/app/lib/export-google-slides-client.ts +15 -4
  83. package/corpus/templates/slides/app/lib/export-pptx-client.ts +637 -1
  84. package/corpus/templates/slides/app/lib/image-download.ts +84 -0
  85. package/corpus/templates/slides/app/lib/utils.ts +1 -1
  86. package/corpus/templates/slides/app/pages/DeckEditor.tsx +9 -0
  87. package/corpus/templates/slides/app/pages/Index.tsx +25 -9
  88. package/corpus/templates/slides/changelog/2026-08-13-clicking-a-slide-thumbnail-now-focuses-it-so-the-slide-shortc.md +6 -0
  89. package/corpus/templates/slides/changelog/2026-08-13-duplicating-or-restoring-a-slide-now-keeps-its-transition-ani.md +6 -0
  90. package/corpus/templates/slides/changelog/2026-08-13-opening-presentation-mode-from-the-agent-now-starts-on-the-re.md +6 -0
  91. package/corpus/templates/slides/changelog/2026-08-13-slide-transitions-can-be-set-from-a-visible-control-in-the-sl.md +6 -0
  92. package/corpus/templates/slides/netlify.toml +1 -0
  93. package/corpus/templates/slides/server/lib/builder-design-system-proxy.ts +14 -2
  94. package/dist/a2a/audience.d.ts +2 -0
  95. package/dist/a2a/audience.js +23 -0
  96. package/dist/a2a/client.d.ts +4 -0
  97. package/dist/a2a/client.js +32 -14
  98. package/dist/a2a/server.d.ts +9 -4
  99. package/dist/a2a/server.js +52 -25
  100. package/dist/a2a/types.d.ts +4 -4
  101. package/dist/agent/production-agent.js +1 -1
  102. package/dist/brand-kit/types.d.ts +10 -2
  103. package/dist/cli/code.js +1 -1
  104. package/dist/cli/index.js +30 -1
  105. package/dist/cli/native-dependencies.d.ts +24 -0
  106. package/dist/cli/native-dependencies.js +264 -0
  107. package/dist/cli/plan-local.js +1 -1
  108. package/dist/cli/skills-content/design-visual-edit-skill.d.ts +1 -1
  109. package/dist/cli/skills-content/design-visual-edit-skill.js +2 -2
  110. package/dist/cli/visualize-repo.js +1 -1
  111. package/dist/client/FeedbackButton.js +12 -12
  112. package/dist/client/agent-chat-adapter.js +3 -4
  113. package/dist/client/assistant-ui-recovery.d.ts +1 -1
  114. package/dist/client/assistant-ui-recovery.js +4 -0
  115. package/dist/client/chat/message-components.d.ts +11 -3
  116. package/dist/client/chat/message-components.js +31 -5
  117. package/dist/client/chat/tool-call-display.d.ts +2 -1
  118. package/dist/client/chat/tool-call-display.js +33 -15
  119. package/dist/client/db-admin/SqlEditor.js +1 -1
  120. package/dist/client/dev-overlay/DevOverlay.js +2 -2
  121. package/dist/client/extensions/portable-extension.js +4 -0
  122. package/dist/client/guided-questions.d.ts +8 -0
  123. package/dist/client/guided-questions.js +12 -3
  124. package/dist/client/sharing/ShareButton.js +16 -24
  125. package/dist/client/sse-event-processor.js +35 -15
  126. package/dist/client/tool-display.d.ts +9 -0
  127. package/dist/client/tool-display.js +39 -0
  128. package/dist/client/vite-dev-recovery-script.js +1 -1
  129. package/dist/collab/awareness.d.ts +2 -2
  130. package/dist/collab/routes.d.ts +1 -1
  131. package/dist/collab/struct-routes.d.ts +1 -1
  132. package/dist/extensions/html-shell.js +5 -0
  133. package/dist/notifications/routes.d.ts +1 -1
  134. package/dist/observability/routes.d.ts +3 -3
  135. package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
  136. package/dist/server/agent-capabilities.js +0 -0
  137. package/dist/server/agent-chat/action-filters-a2a.d.ts +14 -13
  138. package/dist/server/agent-chat/action-filters-a2a.js +59 -23
  139. package/dist/server/agent-chat/prompt-resources.js +17 -16
  140. package/dist/server/agent-chat-plugin.d.ts +9 -0
  141. package/dist/server/agent-chat-plugin.js +29 -7
  142. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  143. package/dist/server/builder-design-systems.d.ts +33 -1
  144. package/dist/server/builder-design-systems.js +293 -11
  145. package/dist/server/design-token-utils.d.ts +11 -0
  146. package/dist/server/design-token-utils.js +68 -16
  147. package/dist/server/index.d.ts +1 -1
  148. package/dist/server/index.js +1 -1
  149. package/package.json +2 -2
@@ -6,6 +6,7 @@ import {
6
6
  type CollabUser,
7
7
  } from "@agent-native/core/client/collab";
8
8
  import {
9
+ readClientAppState,
9
10
  setClientAppState,
10
11
  usePinchZoom,
11
12
  useAvatarUrl,
@@ -17,15 +18,7 @@ import {
17
18
  } from "@agent-native/toolkit/collab-ui";
18
19
  import { appStateKeyForBrowserTab } from "@shared/app-state-tabs";
19
20
  import { hashSlideContent } from "@shared/slide-fit";
20
- import {
21
- IconBrush,
22
- IconClipboard,
23
- IconCopy,
24
- IconStackBack,
25
- IconStackFront,
26
- IconTrash,
27
- IconX,
28
- } from "@tabler/icons-react";
21
+ import { IconX } from "@tabler/icons-react";
29
22
  import {
30
23
  useState,
31
24
  useCallback,
@@ -73,6 +66,7 @@ import {
73
66
  MAX_CANVAS_ZOOM,
74
67
  MIN_CANVAS_ZOOM,
75
68
  } from "@/lib/canvas-zoom";
69
+ import { downloadImage } from "@/lib/image-download";
76
70
  import { extractMermaidBlocks } from "@/lib/mermaid-blocks";
77
71
  import { publishSlidesSelection } from "@/lib/slide-agent-context";
78
72
  import {
@@ -113,6 +107,7 @@ import {
113
107
  getInlineTextStyleSnapshot,
114
108
  getInlineTextStyleSnapshotForRange,
115
109
  restoreEditableTextRange,
110
+ selectAllEditableText,
116
111
  snapshotEditableTextRange,
117
112
  type InlineTextStylePatch,
118
113
  type InlineTextStyleSnapshot,
@@ -167,6 +162,7 @@ import { type SlideStylePatch, type SlideStyleSnapshot } from "./slide-style";
167
162
  import {
168
163
  findSmartBlock,
169
164
  isInlineTextElement,
165
+ isSlideTextEditingTarget,
170
166
  isTextLeaf,
171
167
  shouldStampBuilderId,
172
168
  } from "./slide-text-targets";
@@ -1064,6 +1060,13 @@ function syncOverflowToAppState(
1064
1060
  }
1065
1061
  }
1066
1062
 
1063
+ function layoutWarningDismissalStateKey(
1064
+ deckId: string | undefined,
1065
+ slideId: string,
1066
+ ): string {
1067
+ return `slides-layout-warning-dismissed:${deckId ?? "local"}:${slideId}`;
1068
+ }
1069
+
1067
1070
  export default function SlideEditor({
1068
1071
  slide,
1069
1072
  onUpdateSlide,
@@ -1222,12 +1225,54 @@ export default function SlideEditor({
1222
1225
  const repairRequestTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
1223
1226
  null,
1224
1227
  );
1225
- const [dismissedOverflowWarningKey, setDismissedOverflowWarningKey] =
1228
+ const overflowWarningStateKey = layoutWarningDismissalStateKey(
1229
+ deckId,
1230
+ slide.id,
1231
+ );
1232
+ const overflowContentHash = hashSlideContent(slide.content);
1233
+ const [dismissedOverflowWarningHash, setDismissedOverflowWarningHash] =
1226
1234
  useState<string | null>(null);
1227
- const overflowWarningKey = `${slide.id}:${hashSlideContent(slide.content)}`;
1228
- const warningVisible = dismissedOverflowWarningKey !== overflowWarningKey;
1229
- const dismissWarning = () =>
1230
- setDismissedOverflowWarningKey(overflowWarningKey);
1235
+ const [overflowWarningDismissalStatus, setOverflowWarningDismissalStatus] =
1236
+ useState<"loading" | "loaded" | "error">("loading");
1237
+ useEffect(() => {
1238
+ let cancelled = false;
1239
+ setDismissedOverflowWarningHash(null);
1240
+ setOverflowWarningDismissalStatus("loading");
1241
+ void readClientAppState<unknown>(overflowWarningStateKey)
1242
+ .then((value) => {
1243
+ if (cancelled) return;
1244
+ setDismissedOverflowWarningHash(
1245
+ typeof value === "string" ? value : null,
1246
+ );
1247
+ setOverflowWarningDismissalStatus("loaded");
1248
+ })
1249
+ .catch((error: unknown) => {
1250
+ if (cancelled) return;
1251
+ console.error("Could not read slide overflow warning dismissal", error);
1252
+ setDismissedOverflowWarningHash(null);
1253
+ setOverflowWarningDismissalStatus("error");
1254
+ });
1255
+ return () => {
1256
+ cancelled = true;
1257
+ };
1258
+ }, [overflowWarningStateKey]);
1259
+ const warningVisible =
1260
+ overflowWarningDismissalStatus === "error" ||
1261
+ (overflowWarningDismissalStatus === "loaded" &&
1262
+ dismissedOverflowWarningHash !== overflowContentHash);
1263
+ const dismissWarning = useCallback(() => {
1264
+ setDismissedOverflowWarningHash(overflowContentHash);
1265
+ setOverflowWarningDismissalStatus("loaded");
1266
+ void setClientAppState(overflowWarningStateKey, overflowContentHash, {
1267
+ keepalive: true,
1268
+ requestSource: TAB_ID,
1269
+ }).catch((error: unknown) => {
1270
+ console.error(
1271
+ "Could not persist slide overflow warning dismissal",
1272
+ error,
1273
+ );
1274
+ });
1275
+ }, [overflowContentHash, overflowWarningStateKey]);
1231
1276
  const dims = getAspectRatioDims(aspectRatio);
1232
1277
  const [fitCanvasZoom, setFitCanvasZoom] = useState(100);
1233
1278
  const userSetCanvasZoomRef = useRef(false);
@@ -1930,6 +1975,17 @@ export default function SlideEditor({
1930
1975
  return () => editingEl.removeEventListener("input", handleInput);
1931
1976
  }, [captureInlineEditDraft, editingEl, slide.id]);
1932
1977
 
1978
+ // Keep canvas gesture handlers from stealing the browser's native text
1979
+ // selection stream once an inline edit has started.
1980
+ useEffect(() => {
1981
+ if (!editingEl) return;
1982
+ const onPointerDown = (event: PointerEvent) => {
1983
+ if (event.button === 0) event.stopPropagation();
1984
+ };
1985
+ editingEl.addEventListener("pointerdown", onPointerDown);
1986
+ return () => editingEl.removeEventListener("pointerdown", onPointerDown);
1987
+ }, [editingEl]);
1988
+
1933
1989
  useEffect(() => {
1934
1990
  if (!editingEl) return;
1935
1991
 
@@ -1981,6 +2037,17 @@ export default function SlideEditor({
1981
2037
  (isTextLeaf(editingEl) && RICH_BLOCK_TAGS.has(editingEl.tagName)) ||
1982
2038
  isBulletList(editingEl);
1983
2039
  const onKey = (e: KeyboardEvent) => {
2040
+ const isSelectAll =
2041
+ (e.metaKey || e.ctrlKey) && !e.altKey && e.key.toLowerCase() === "a";
2042
+ if (isSelectAll) {
2043
+ const editing = editingElRef.current;
2044
+ if (!editing || !editing.isConnected) return;
2045
+ e.preventDefault();
2046
+ e.stopPropagation();
2047
+ selectAllEditableText(editing);
2048
+ return;
2049
+ }
2050
+
1984
2051
  // Ignore keys from outside the slide (e.g. the style dock's font-size
1985
2052
  // input). Guard against the LIVE slide content, not `editingEl`, which a
1986
2053
  // re-render may have detached — a stale ref would wrongly bail here and
@@ -2036,6 +2103,7 @@ export default function SlideEditor({
2036
2103
  exitInlineEdit,
2037
2104
  editingEl,
2038
2105
  captureInlineEditDraft,
2106
+ selectAllEditableText,
2039
2107
  slide.id,
2040
2108
  getSlideContent,
2041
2109
  ]);
@@ -4123,10 +4191,12 @@ export default function SlideEditor({
4123
4191
 
4124
4192
  useEffect(() => {
4125
4193
  if (readOnly || editingEl) return;
4126
- if (multiSelection.size === 0 && !selectedElementSelector) return;
4194
+ if (multiSelection.size === 0 && !selectedElementSelector && !selectedImg)
4195
+ return;
4127
4196
  const onKey = (e: KeyboardEvent) => {
4128
4197
  if (!e.key.startsWith("Arrow")) return;
4129
4198
  const active = document.activeElement;
4199
+ if (isSlideTextEditingTarget(e.target, active, editingEl)) return;
4130
4200
  if (
4131
4201
  active?.tagName === "INPUT" ||
4132
4202
  active?.tagName === "TEXTAREA" ||
@@ -4139,7 +4209,7 @@ export default function SlideEditor({
4139
4209
  if (!nudge) return;
4140
4210
  const { x: dx, y: dy } = nudge.delta;
4141
4211
 
4142
- if (multiSelection.size > 0 && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
4212
+ if (multiSelection.size > 0 && !e.metaKey && !e.ctrlKey) {
4143
4213
  const slideContent = getSlideContent();
4144
4214
  if (!slideContent) return;
4145
4215
  const elements = Array.from(multiSelection)
@@ -4188,17 +4258,51 @@ export default function SlideEditor({
4188
4258
  return;
4189
4259
  }
4190
4260
 
4191
- const element = resolveSelectedElement();
4192
- if (!element || !isPersistedFreeformObject(element)) return;
4261
+ const slideContent = getSlideContent();
4262
+ const element =
4263
+ resolveSelectedElement() ??
4264
+ (selectedImg && slideContent
4265
+ ? (findPersistedImageObject(selectedImg, slideContent) ?? selectedImg)
4266
+ : null);
4267
+ if (!element) return;
4268
+
4269
+ // Arrow nudging is also a first-class way to move flow-layout text and
4270
+ // images. Promote those elements using the same reversible freeform
4271
+ // boundary as a real drag, then persist the exact resulting HTML.
4272
+ const frozen = isPersistedFreeformObject(element)
4273
+ ? { element }
4274
+ : freezeElementForFreeformSelection(element);
4275
+ if (!frozen || !isPersistedFreeformObject(frozen.element)) {
4276
+ frozen?.restoreMarkdownTree?.();
4277
+ return;
4278
+ }
4193
4279
  e.preventDefault();
4194
- const geometry = getObjectGeometry(element);
4280
+ const geometry = getObjectGeometry(frozen.element);
4195
4281
  geometry.x += dx;
4196
4282
  geometry.y += dy;
4197
- applyObjectGeometry(element, geometry);
4283
+ applyObjectGeometry(frozen.element, geometry);
4198
4284
  const html = readCurrentSlideContentHtml();
4285
+
4286
+ if (frozen.restoreMarkdownTree) {
4287
+ const objectId = frozen.element.getAttribute("data-slide-object-id");
4288
+ if (objectId) {
4289
+ const owner =
4290
+ frozen.element.parentElement ?? frozen.element.ownerDocument;
4291
+ owner
4292
+ .querySelectorAll<HTMLElement>("[data-slide-layout-spacer-for]")
4293
+ .forEach((spacer) => {
4294
+ if (
4295
+ spacer.getAttribute("data-slide-layout-spacer-for") === objectId
4296
+ ) {
4297
+ spacer.remove();
4298
+ }
4299
+ });
4300
+ }
4301
+ frozen.restoreMarkdownTree();
4302
+ }
4199
4303
  if (html !== null) onUpdateSlideRef.current({ content: html });
4200
- const selector = getBuilderSelector(element);
4201
- if (selector) selectElementForStyling(element, selector);
4304
+ const selector = getBuilderSelector(frozen.element);
4305
+ if (selector) selectElementForStyling(frozen.element, selector);
4202
4306
  };
4203
4307
  window.addEventListener("keydown", onKey);
4204
4308
  return () => window.removeEventListener("keydown", onKey);
@@ -4206,6 +4310,8 @@ export default function SlideEditor({
4206
4310
  applyObjectGeometry,
4207
4311
  commitMultiObjectChange,
4208
4312
  editingEl,
4313
+ findPersistedImageObject,
4314
+ freezeElementForFreeformSelection,
4209
4315
  getObjectGeometry,
4210
4316
  getSlideContent,
4211
4317
  multiSelection,
@@ -4214,6 +4320,7 @@ export default function SlideEditor({
4214
4320
  refreshMultiSelectionRects,
4215
4321
  resolveSelectedElement,
4216
4322
  selectElementForStyling,
4323
+ selectedImg,
4217
4324
  selectedElementSelector,
4218
4325
  ]);
4219
4326
 
@@ -4268,7 +4375,11 @@ export default function SlideEditor({
4268
4375
  onExitTextBoxMode?.();
4269
4376
  return;
4270
4377
  }
4271
- if (editingEl) return; // avoid interfering with an active inline edit
4378
+ if (
4379
+ isSlideTextEditingTarget(e.target, document.activeElement, editingEl)
4380
+ ) {
4381
+ return;
4382
+ }
4272
4383
 
4273
4384
  // Pointer-down on a member of the current multi-selection drags the
4274
4385
  // whole group instead of the single-object flow below.
@@ -5185,6 +5296,28 @@ export default function SlideEditor({
5185
5296
  horizontalOverflow={
5186
5297
  overflowInfo.horizontalOverflow
5187
5298
  }
5299
+ warningLabel={t(
5300
+ "deckEditor.layoutOverflowWarning",
5301
+ )}
5302
+ overflowDetails={[
5303
+ overflowInfo.verticalOverflow > 0
5304
+ ? t("deckEditor.layoutOverflowVertical", {
5305
+ pixels: overflowInfo.verticalOverflow,
5306
+ })
5307
+ : null,
5308
+ overflowInfo.horizontalOverflow > 0
5309
+ ? t("deckEditor.layoutOverflowHorizontal", {
5310
+ pixels: overflowInfo.horizontalOverflow,
5311
+ })
5312
+ : null,
5313
+ ]
5314
+ .filter((detail): detail is string =>
5315
+ Boolean(detail),
5316
+ )
5317
+ .join(" · ")}
5318
+ overflowDetailsLabel={t(
5319
+ "deckEditor.layoutOverflowDetails",
5320
+ )}
5188
5321
  isAskingAgentToFix={isAskingAgentToFix}
5189
5322
  dismissLabel={t(
5190
5323
  "deckEditor.dismissLayoutWarning",
@@ -5200,7 +5333,6 @@ export default function SlideEditor({
5200
5333
  disabled={!hasObjectSelection}
5201
5334
  onSelect={copySelectedObjects}
5202
5335
  >
5203
- <IconCopy className="me-2 h-4 w-4" />
5204
5336
  {t("styleInspector.copy")}
5205
5337
  <ContextMenuShortcut>
5206
5338
  {shortcutLabel("cmd+c")}
@@ -5210,7 +5342,6 @@ export default function SlideEditor({
5210
5342
  disabled={!hasObjectSelection}
5211
5343
  onSelect={pasteSelectedObjects}
5212
5344
  >
5213
- <IconClipboard className="me-2 h-4 w-4" />
5214
5345
  {t("styleInspector.paste")}
5215
5346
  <ContextMenuShortcut>
5216
5347
  {shortcutLabel("cmd+v")}
@@ -5220,7 +5351,6 @@ export default function SlideEditor({
5220
5351
  disabled={!hasObjectSelection}
5221
5352
  onSelect={duplicateSelectedObjects}
5222
5353
  >
5223
- <IconCopy className="me-2 h-4 w-4" />
5224
5354
  {t("editorSidebar.duplicate")}
5225
5355
  <ContextMenuShortcut>
5226
5356
  {shortcutLabel("cmd+d")}
@@ -5230,7 +5360,6 @@ export default function SlideEditor({
5230
5360
  disabled={!hasObjectSelection}
5231
5361
  onSelect={deleteSelectedElements}
5232
5362
  >
5233
- <IconTrash className="me-2 h-4 w-4" />
5234
5363
  {t("editorSidebar.delete")}
5235
5364
  <ContextMenuShortcut>⌫</ContextMenuShortcut>
5236
5365
  </ContextMenuItem>
@@ -5239,7 +5368,6 @@ export default function SlideEditor({
5239
5368
  disabled={getStyleTargets().length === 0}
5240
5369
  onSelect={copySelectedElementStyle}
5241
5370
  >
5242
- <IconBrush className="me-2 h-4 w-4" />
5243
5371
  {t("styleInspector.copyStyle")}
5244
5372
  <ContextMenuShortcut>
5245
5373
  {shortcutLabel("cmd+alt+c")}
@@ -5252,7 +5380,6 @@ export default function SlideEditor({
5252
5380
  }
5253
5381
  onSelect={pasteCopiedElementStyle}
5254
5382
  >
5255
- <IconBrush className="me-2 h-4 w-4" />
5256
5383
  {t("styleInspector.pasteStyle")}
5257
5384
  <ContextMenuShortcut>
5258
5385
  {shortcutLabel("cmd+alt+v")}
@@ -5263,14 +5390,12 @@ export default function SlideEditor({
5263
5390
  disabled={!selectedElementSelector}
5264
5391
  onSelect={() => handleArrangeSelected("front")}
5265
5392
  >
5266
- <IconStackFront className="me-2 h-4 w-4" />
5267
5393
  {t("styleInspector.bringToFront")}
5268
5394
  </ContextMenuItem>
5269
5395
  <ContextMenuItem
5270
5396
  disabled={!selectedElementSelector}
5271
5397
  onSelect={() => handleArrangeSelected("back")}
5272
5398
  >
5273
- <IconStackBack className="me-2 h-4 w-4" />
5274
5399
  {t("styleInspector.sendToBack")}
5275
5400
  </ContextMenuItem>
5276
5401
  </ContextMenuContent>
@@ -5357,10 +5482,12 @@ export default function SlideEditor({
5357
5482
  {imageOverlay && (
5358
5483
  <ImageOverlay
5359
5484
  anchorRect={imageOverlay.rect}
5485
+ src={imageOverlay.src}
5360
5486
  objectFit={imageOverlay.objectFit}
5361
5487
  onGenerate={onGenerateImage}
5362
5488
  onLibrary={() => onOpenAssetLibrary(imageOverlay.src)}
5363
5489
  onUpload={() => onUploadImage(imageOverlay.src)}
5490
+ onDownload={() => void downloadImage(imageOverlay.src)}
5364
5491
  onToggleObjectFit={() => {
5365
5492
  const newFit =
5366
5493
  imageOverlay.objectFit === "cover" ? "contain" : "cover";
@@ -1,10 +1,19 @@
1
- import { IconAlertTriangle, IconX } from "@tabler/icons-react";
1
+ import { IconAlertTriangle, IconInfoCircle, IconX } from "@tabler/icons-react";
2
2
 
3
3
  import { Button } from "@/components/ui/button";
4
+ import {
5
+ Tooltip,
6
+ TooltipContent,
7
+ TooltipProvider,
8
+ TooltipTrigger,
9
+ } from "@/components/ui/tooltip";
4
10
 
5
11
  interface SlideOverflowWarningProps {
6
12
  verticalOverflow: number;
7
13
  horizontalOverflow?: number;
14
+ warningLabel: string;
15
+ overflowDetails: string;
16
+ overflowDetailsLabel: string;
8
17
  isAskingAgentToFix: boolean;
9
18
  dismissLabel: string;
10
19
  onFix: () => void;
@@ -12,21 +21,14 @@ interface SlideOverflowWarningProps {
12
21
  }
13
22
 
14
23
  export function SlideOverflowWarning({
15
- verticalOverflow,
16
- horizontalOverflow = 0,
24
+ warningLabel,
25
+ overflowDetails,
26
+ overflowDetailsLabel,
17
27
  isAskingAgentToFix,
18
28
  dismissLabel,
19
29
  onFix,
20
30
  onDismiss,
21
31
  }: SlideOverflowWarningProps) {
22
- const overflowLabel = [
23
- verticalOverflow > 0 ? `vertical ${verticalOverflow}px` : "",
24
- horizontalOverflow > 0 ? `horizontal ${horizontalOverflow}px` : "",
25
- ]
26
- .filter(Boolean)
27
- .join(", ");
28
- const visibleOverflowLabel =
29
- horizontalOverflow > 0 ? overflowLabel : `${verticalOverflow}px`;
30
32
  return (
31
33
  <div
32
34
  role="status"
@@ -42,9 +44,23 @@ export function SlideOverflowWarning({
42
44
  className="h-3.5 w-3.5 flex-shrink-0 text-foreground"
43
45
  stroke={2}
44
46
  />
45
- <span className="leading-tight">
46
- Layout overflows by {visibleOverflowLabel}
47
- </span>
47
+ <span className="leading-tight">{warningLabel}</span>
48
+ <TooltipProvider delayDuration={200}>
49
+ <Tooltip>
50
+ <TooltipTrigger asChild>
51
+ <Button
52
+ type="button"
53
+ size="icon"
54
+ variant="ghost"
55
+ className="size-6 cursor-help text-muted-foreground hover:bg-transparent hover:text-foreground"
56
+ aria-label={overflowDetailsLabel}
57
+ >
58
+ <IconInfoCircle className="size-3.5" />
59
+ </Button>
60
+ </TooltipTrigger>
61
+ <TooltipContent side="top">{overflowDetails}</TooltipContent>
62
+ </Tooltip>
63
+ </TooltipProvider>
48
64
  <Button
49
65
  size="sm"
50
66
  variant="ghost"
@@ -85,9 +85,9 @@ export function SpeakerNotesPanel({
85
85
  )}
86
86
  <button
87
87
  onClick={toggle}
88
- className="flex w-full cursor-pointer items-center justify-between px-4 py-1.5"
88
+ className="group flex w-full cursor-pointer items-center justify-between px-4 py-1.5"
89
89
  >
90
- <span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
90
+ <span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider transition-colors group-hover:text-foreground/80">
91
91
  {t("raw.speakerNotes")}
92
92
  </span>
93
93
  {expanded ? (
@@ -151,8 +151,11 @@ export function resolveSlidesCanvasPointerIntent({
151
151
  ) {
152
152
  return "move-object-perimeter";
153
153
  }
154
+ // The interior of an editable text object belongs to native text selection.
155
+ // Only the measured outer edge is reserved for moving the selected object.
156
+ if (targetIsEditableText) return "edit-text";
154
157
  if (hasSelectedObject && targetWithinSelectedObject) {
155
158
  return "move-object-body";
156
159
  }
157
- return targetIsEditableText ? "edit-text" : "none";
160
+ return "none";
158
161
  }
@@ -151,6 +151,20 @@ export function snapshotEditableTextRange(
151
151
  return getEditableTextRange(editable, selection)?.cloneRange() ?? null;
152
152
  }
153
153
 
154
+ /** Selects the complete contents of one live slide text block. */
155
+ export function selectAllEditableText(
156
+ editable: HTMLElement,
157
+ selection: Selection | null = window.getSelection(),
158
+ ) {
159
+ if (!selection) return false;
160
+ editable.focus({ preventScroll: true });
161
+ const range = document.createRange();
162
+ range.selectNodeContents(editable);
163
+ selection.removeAllRanges();
164
+ selection.addRange(range);
165
+ return true;
166
+ }
167
+
154
168
  /** Restores a captured range only while it is still safe to use in this block. */
155
169
  export function restoreEditableTextRange(
156
170
  editable: HTMLElement,
@@ -1,5 +1,7 @@
1
1
  import { findEnclosingList } from "./bullet-editing";
2
2
 
3
+ type EditingTarget = EventTarget | Element | null;
4
+
3
5
  /** Inline markup changes presentation, but does not create a canvas target. */
4
6
  const INLINE_TEXT_TAGS = new Set([
5
7
  "SPAN",
@@ -23,6 +25,28 @@ export function isInlineTextElement(element: Element): boolean {
23
25
  return INLINE_TEXT_TAGS.has(element.tagName);
24
26
  }
25
27
 
28
+ function isEditingTarget(target: EditingTarget): boolean {
29
+ if (!(target instanceof Element)) return false;
30
+ return (
31
+ (target instanceof HTMLElement && target.isContentEditable) ||
32
+ target.closest('[contenteditable="true"], [data-editing-block="true"]') !==
33
+ null
34
+ );
35
+ }
36
+
37
+ /** Whether a keyboard/pointer event belongs to an active slide text edit. */
38
+ export function isSlideTextEditingTarget(
39
+ target: EditingTarget,
40
+ activeElement: Element | null = null,
41
+ editingElement: Element | null = null,
42
+ ): boolean {
43
+ return (
44
+ isEditingTarget(target) ||
45
+ isEditingTarget(activeElement) ||
46
+ isEditingTarget(editingElement)
47
+ );
48
+ }
49
+
26
50
  export function shouldStampBuilderId(element: HTMLElement): boolean {
27
51
  return (
28
52
  !element.classList.contains("fmd-layout-spacer") &&
@@ -40,12 +40,10 @@ type GranularOp =
40
40
  op: "add-slide";
41
41
  slideId: string;
42
42
  afterSlideId?: string;
43
- fields: {
44
- content: string;
45
- notes?: string;
46
- layout?: string;
47
- background?: string;
48
- };
43
+ /** Everything but `id` and the transient `imageLoading` flag. A slide
44
+ * copied or restored through this op keeps its transition, animations,
45
+ * and image/Excalidraw data instead of silently losing them on reload. */
46
+ fields: Omit<Partial<Slide>, "id" | "imageLoading"> & { content: string };
49
47
  }
50
48
  | {
51
49
  op: "patch-deck-fields";
@@ -57,6 +55,16 @@ type GranularOp =
57
55
  | { op: "full-replace"; deck: Deck };
58
56
 
59
57
  export type PatchDeckOp = Exclude<GranularOp, { op: "full-replace" }>;
58
+
59
+ /** Slide payload for an `add-slide` op. `imageLoading` is transient UI state
60
+ * and must not persist; everything else on the slide has to survive the round
61
+ * trip or a duplicated/restored slide comes back missing fields. */
62
+ function addSlideFields(
63
+ slide: Slide,
64
+ ): Extract<GranularOp, { op: "add-slide" }>["fields"] {
65
+ const { id: _id, imageLoading: _imageLoading, ...fields } = slide;
66
+ return fields;
67
+ }
60
68
  export type DeckReloadStatus = "loaded" | "failed" | "stale";
61
69
  export interface UpdateSlideOptions {
62
70
  persistence?: "debounced" | "immediate";
@@ -683,13 +691,11 @@ export function applyOpToDeck(deck: Deck, op: PatchDeckOp): Deck {
683
691
  case "add-slide": {
684
692
  if (deck.slides.some((s) => s.id === op.slideId)) return deck; // idempotent
685
693
  const newSlide: Slide = {
694
+ ...op.fields,
686
695
  id: op.slideId,
687
696
  content: op.fields.content,
688
697
  notes: op.fields.notes ?? "",
689
698
  layout: (op.fields.layout as SlideLayout) ?? "content",
690
- ...(op.fields.background !== undefined
691
- ? { background: op.fields.background }
692
- : {}),
693
699
  };
694
700
  const slides = [...deck.slides];
695
701
  const afterIdx = op.afterSlideId
@@ -822,14 +828,7 @@ export function deriveInverseOp(
822
828
  op: "add-slide",
823
829
  slideId: prior.id,
824
830
  afterSlideId,
825
- fields: {
826
- content: prior.content,
827
- notes: prior.notes,
828
- layout: prior.layout,
829
- ...(prior.background !== undefined
830
- ? { background: prior.background }
831
- : {}),
832
- },
831
+ fields: addSlideFields(prior),
833
832
  },
834
833
  {
835
834
  op: "reorder-slides",
@@ -2127,12 +2126,7 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2127
2126
  op: "add-slide",
2128
2127
  slideId: newSlide.id,
2129
2128
  afterSlideId,
2130
- fields: {
2131
- content: newSlide.content,
2132
- notes: newSlide.notes,
2133
- layout: newSlide.layout,
2134
- background: newSlide.background,
2135
- },
2129
+ fields: addSlideFields(newSlide),
2136
2130
  };
2137
2131
  enqueueDeckOp(deckId, op);
2138
2132
  if (before) recordUndo(before, op, { label: "Add slide" });
@@ -2240,17 +2234,11 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2240
2234
  // Granular add-slide op — inserts the copy after the original. Build it
2241
2235
  // from the current deck before scheduling the React state update; the
2242
2236
  // functional updater runs later and cannot be used to produce the op.
2243
- const { id: newSlideId, ...rest } = copiedSlide;
2244
2237
  const op: PatchDeckOp = {
2245
2238
  op: "add-slide",
2246
- slideId: newSlideId,
2239
+ slideId: copiedSlide.id,
2247
2240
  afterSlideId: slideId,
2248
- fields: {
2249
- content: rest.content,
2250
- notes: rest.notes,
2251
- layout: rest.layout,
2252
- background: rest.background,
2253
- },
2241
+ fields: addSlideFields(copiedSlide),
2254
2242
  };
2255
2243
  enqueueDeckOp(deckId, op);
2256
2244
  recordUndo(before, op, { label: "Duplicate slide" });
@@ -69,6 +69,7 @@ button[title="Open agent sidebar"] {
69
69
  --sidebar-ring: 0 0% 50%;
70
70
 
71
71
  --accent-cyan: 187 100% 50%;
72
+ --slides-thumbnail-shadow: 0 0% 0%;
72
73
  }
73
74
 
74
75
  .light {
@@ -115,6 +116,29 @@ button[title="Open agent sidebar"] {
115
116
  --accent-cyan: 193 82% 31%;
116
117
  }
117
118
 
119
+ [data-slides-thumbnail-scroll]::before {
120
+ position: absolute;
121
+ z-index: 20;
122
+ inset: 0.5rem 0.5rem auto;
123
+ height: 1rem;
124
+ pointer-events: none;
125
+ content: "";
126
+ opacity: 0;
127
+ background: radial-gradient(
128
+ ellipse 82% 125% at 50% 0%,
129
+ hsl(var(--slides-thumbnail-shadow) / 0.18) 0%,
130
+ hsl(var(--slides-thumbnail-shadow) / 0.08) 48%,
131
+ transparent 100%
132
+ );
133
+ box-shadow: 0 0.2rem 0.45rem -0.25rem
134
+ hsl(var(--slides-thumbnail-shadow) / 0.64);
135
+ transition: opacity 150ms ease-out;
136
+ }
137
+
138
+ [data-slides-thumbnail-scroll="scrolled"]::before {
139
+ opacity: 1;
140
+ }
141
+
118
142
  /* Slides keeps the shared visual inspector behavior, but the dock and context toolbar need the
119
143
  flatter, filled-control treatment used by the slide editor. */
120
144
  .slide-style-inspector,