@agent-native/core 0.149.1 → 0.149.2

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 (115) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +2 -0
  3. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +150 -46
  4. package/corpus/templates/analytics/app/global.css +1 -1
  5. package/corpus/templates/analytics/app/i18n-data.ts +37 -0
  6. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +1 -1
  7. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -0
  8. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +2 -0
  9. package/corpus/templates/analytics/changelog/2026-08-10-dashboard-tables-can-be-copied-and-sorted-by-multiple-column.md +6 -0
  10. package/corpus/templates/analytics/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  11. package/corpus/templates/assets/app/global.css +1 -1
  12. package/corpus/templates/assets/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  13. package/corpus/templates/brain/app/global.css +1 -1
  14. package/corpus/templates/brain/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  15. package/corpus/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  16. package/corpus/templates/clips/changelog/2026-08-10-clips-desktop-sign-in-now-starts-with-magic-links-and-google.md +6 -0
  17. package/corpus/templates/clips/chrome-extension/src/overlay.css +1 -0
  18. package/corpus/templates/clips/desktop/src/app.tsx +274 -137
  19. package/corpus/templates/clips/desktop/src/styles.css +35 -0
  20. package/corpus/templates/content/app/global.css +1 -0
  21. package/corpus/templates/crm/app/global.css +1 -1
  22. package/corpus/templates/crm/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  23. package/corpus/templates/dispatch/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  24. package/corpus/templates/factory/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  25. package/corpus/templates/forms/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  26. package/corpus/templates/plan/app/global.css +1 -0
  27. package/corpus/templates/plan/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  28. package/corpus/templates/plan/server/ui-plan-html.ts +4 -1
  29. package/corpus/templates/slides/AGENTS.md +11 -10
  30. package/corpus/templates/slides/actions/add-deck.ts +2 -0
  31. package/corpus/templates/slides/actions/add-slide.ts +15 -1
  32. package/corpus/templates/slides/actions/create-deck.ts +18 -6
  33. package/corpus/templates/slides/actions/get-deck.ts +29 -1
  34. package/corpus/templates/slides/actions/patch-deck.ts +122 -9
  35. package/corpus/templates/slides/actions/save-deck.ts +24 -1
  36. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +134 -618
  37. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +69 -60
  38. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -1
  39. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +13 -1
  40. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +56 -174
  41. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +239 -116
  42. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +3 -3
  43. package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +1 -1
  44. package/corpus/templates/slides/app/components/layout/Layout.tsx +34 -35
  45. package/corpus/templates/slides/app/components/layout/layout-route-policy.ts +2 -5
  46. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +45 -40
  47. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +63 -16
  48. package/corpus/templates/slides/app/hooks/use-generating-slide-preview.ts +25 -3
  49. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  50. package/corpus/templates/slides/app/lib/create-deck-generation.ts +5 -4
  51. package/corpus/templates/slides/app/lib/recent-references.ts +23 -1
  52. package/corpus/templates/slides/app/lib/slide-animation-elements.ts +41 -3
  53. package/corpus/templates/slides/app/pages/DeckEditor.tsx +4 -27
  54. package/corpus/templates/slides/app/pages/Index.tsx +240 -89
  55. package/corpus/templates/slides/changelog/2026-08-10-ai-editing-avatar-badge.md +6 -0
  56. package/corpus/templates/slides/changelog/2026-08-10-generated-decks-keep-a-clear-human-readable-title.md +6 -0
  57. package/corpus/templates/slides/changelog/2026-08-10-new-deck-creation-now-shows-progress-opens-the-editor-direct.md +6 -0
  58. package/corpus/templates/slides/changelog/2026-08-10-presentation-reveals-now-follow-valid-targets.md +6 -0
  59. package/corpus/templates/slides/changelog/2026-08-10-slides-editing-controls-are-consolidated-into-one-focused-me.md +6 -0
  60. package/corpus/templates/slides/changelog/2026-08-10-slides-now-verify-and-restyle-every-imported-slide-in-one-pa.md +6 -0
  61. package/corpus/templates/slides/changelog/2026-08-10-slides-only-suggests-a-google-connection-for-relevant-reques.md +6 -0
  62. package/corpus/templates/slides/changelog/2026-08-10-the-google-slides-import-option-is-now-labeled-slides.md +6 -0
  63. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  64. package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -3
  65. package/corpus/templates/slides/shared/api.ts +2 -1
  66. package/corpus/templates/slides/shared/deck-title.ts +135 -0
  67. package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +4 -1
  68. package/dist/agent/engine/ai-sdk-engine.js +1 -1
  69. package/dist/agent/production-agent.d.ts +21 -1
  70. package/dist/agent/production-agent.js +129 -10
  71. package/dist/agent/tool-approval-store.d.ts +18 -0
  72. package/dist/agent/tool-approval-store.js +122 -0
  73. package/dist/agent/types.d.ts +1 -1
  74. package/dist/cli/code-agent-executor.js +11 -2
  75. package/dist/client/AgentPanel.js +31 -6
  76. package/dist/client/AssistantChat.d.ts +3 -2
  77. package/dist/client/AssistantChat.js +7 -20
  78. package/dist/client/agent-chat/index.d.ts +1 -1
  79. package/dist/client/agent-chat-adapter.js +11 -0
  80. package/dist/client/agent-chat.d.ts +1 -1
  81. package/dist/client/chat/markdown-renderer.js +14 -2
  82. package/dist/client/chat/message-components.d.ts +6 -2
  83. package/dist/client/chat/message-components.js +25 -47
  84. package/dist/client/chat/tool-call-display.d.ts +3 -5
  85. package/dist/client/chat/tool-call-display.js +7 -212
  86. package/dist/client/chat-first/apps-rail.js +3 -3
  87. package/dist/client/chat-first/browser-pane.js +4 -1
  88. package/dist/client/chat-first/chat-history.d.ts +3 -1
  89. package/dist/client/chat-first/chat-history.js +2 -2
  90. package/dist/client/chat-first/surface-tabs.d.ts +1 -1
  91. package/dist/client/chat-first/surface-tabs.js +52 -41
  92. package/dist/client/chat-first/types.d.ts +3 -0
  93. package/dist/client/chat-first.d.ts +10 -0
  94. package/dist/client/chat-first.js +37 -2
  95. package/dist/client/components/ui/message-scroller.js +4 -1
  96. package/dist/client/i18n.js +3 -1
  97. package/dist/client/resources/McpConnectionSuggestion.js +9 -1
  98. package/dist/client/sse-event-processor.js +10 -0
  99. package/dist/client/use-chat-models.js +156 -80
  100. package/dist/collab/struct-routes.d.ts +1 -1
  101. package/dist/integrations/webhook-handler.js +1 -1
  102. package/dist/observability/routes.d.ts +1 -1
  103. package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
  104. package/dist/resources/handlers.d.ts +1 -1
  105. package/dist/secrets/routes.d.ts +9 -9
  106. package/dist/server/auth.js +262 -31
  107. package/dist/server/onboarding-html.js +56 -9
  108. package/dist/shared/reasoning-effort.d.ts +8 -8
  109. package/dist/shared/reasoning-effort.js +10 -10
  110. package/dist/shared/streaming-text-smoothing.js +41 -11
  111. package/dist/styles/agent-native.css +19 -32
  112. package/dist/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  113. package/docs/content/template-chat.mdx +1 -1
  114. package/package.json +2 -2
  115. package/src/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
@@ -13,32 +13,23 @@ import { PresenceBar } from "@agent-native/toolkit/collab-ui";
13
13
  import {
14
14
  IconArrowLeft,
15
15
  IconPlayerPlay,
16
- IconLayout,
17
16
  IconLayoutSidebar,
18
17
  IconPhoto,
19
18
  IconHistory,
20
19
  IconFolderOpen,
21
- IconSettings,
22
- IconSchema,
23
- IconPencil,
24
- IconTransform,
25
20
  IconMessage,
26
- IconBolt,
27
- IconAdjustments,
28
- IconPencilPlus,
29
- IconPin,
30
- IconTool,
31
21
  IconDownload,
32
22
  IconSun,
33
23
  IconMoon,
34
24
  IconDotsVertical,
35
25
  IconLoader2,
36
- IconArrowBackUp,
37
- IconArrowForwardUp,
26
+ IconBolt,
27
+ IconAdjustments,
28
+ IconPencilPlus,
29
+ IconPin,
38
30
  } from "@tabler/icons-react";
39
31
  import { useTheme } from "next-themes";
40
32
  import { useState, useRef, useEffect } from "react";
41
- import { createPortal } from "react-dom";
42
33
  import { Link } from "react-router";
43
34
  import { toast } from "sonner";
44
35
 
@@ -46,6 +37,7 @@ import {
46
37
  DropdownMenu,
47
38
  DropdownMenuContent,
48
39
  DropdownMenuGroup,
40
+ DropdownMenuLabel,
49
41
  DropdownMenuItem,
50
42
  DropdownMenuTrigger,
51
43
  DropdownMenuSeparator,
@@ -56,23 +48,12 @@ import {
56
48
  TooltipTrigger,
57
49
  } from "@/components/ui/tooltip";
58
50
  import { SaveStatusIndicator } from "@/components/visual-editor";
59
- import type { Deck, Slide, SlideLayout } from "@/context/DeckContext";
60
- import {
61
- defaultSlideContent,
62
- useDecks,
63
- useSaveState,
64
- } from "@/context/DeckContext";
65
- import {
66
- ASPECT_RATIO_VALUES,
67
- type AspectRatio,
68
- DEFAULT_ASPECT_RATIO,
69
- } from "@/lib/aspect-ratios";
51
+ import type { Deck, Slide } from "@/context/DeckContext";
52
+ import { useSaveState } from "@/context/DeckContext";
70
53
  import { getDeckShareLinkOrder } from "@/lib/deck-share-links";
71
54
  import type { GoogleSlidesExportResult } from "@/lib/export-google-slides-client";
72
55
  import { parseUploadResponse } from "@/lib/upload-response";
73
- import { shortcutLabel } from "@/lib/utils";
74
56
 
75
- import { commitActiveEditThenRun } from "./commit-active-edit";
76
57
  import { EditorActionCluster } from "./EditorActionCluster";
77
58
  import { ExportMenu } from "./ExportMenu";
78
59
  interface EditorToolbarProps {
@@ -90,16 +71,9 @@ interface EditorToolbarProps {
90
71
  onToggleSidebar: () => void;
91
72
  onGenerateImage: () => void;
92
73
  onOpenAssetLibrary: () => void;
93
- imageGenButtonRef: React.RefObject<HTMLButtonElement | null>;
94
- assetsButtonRef: React.RefObject<HTMLButtonElement | null>;
95
- historyOpen: boolean;
96
74
  onShowHistory: () => void;
97
75
  historyButtonRef: React.RefObject<HTMLButtonElement | null>;
98
76
  currentSlide?: Slide;
99
- onUpdateSlide?: (
100
- updates: Partial<Omit<Slide, "id">>,
101
- slideIdOverride?: string,
102
- ) => void;
103
77
  /** Active users on the current slide (from collab awareness) */
104
78
  activeUsers?: CollabUser[];
105
79
  /** Whether the agent has a durable presence entry on this slide */
@@ -142,10 +116,6 @@ interface EditorToolbarProps {
142
116
  onExportPptx?: () => Promise<void> | void;
143
117
  /** Create the deck in the user's Google Drive as native Google Slides */
144
118
  onExportGoogleSlides?: () => Promise<GoogleSlidesExportResult>;
145
- /** Active deck aspect ratio (defaults to 16:9 when omitted) */
146
- aspectRatio?: AspectRatio;
147
- /** Change the deck's aspect ratio */
148
- onSetAspectRatio?: (ratio: AspectRatio) => void;
149
119
  /** Insert a blank slide after the current one */
150
120
  onAddEmptySlide?: () => void;
151
121
  /** Duplicate the current slide */
@@ -158,92 +128,9 @@ interface EditorToolbarProps {
158
128
  onAddSlideGeneratingChange?: (generating: boolean) => void;
159
129
  }
160
130
 
161
- const slideLayoutOptions: { value: SlideLayout; labelKey: string }[] = [
162
- { value: "title", labelKey: "editorToolbar.layoutTitle" },
163
- { value: "section", labelKey: "editorToolbar.layoutSection" },
164
- { value: "content", labelKey: "editorToolbar.layoutContent" },
165
- { value: "two-column", labelKey: "editorToolbar.layoutTwoColumn" },
166
- { value: "image", labelKey: "editorToolbar.layoutImage" },
167
- { value: "statement", labelKey: "editorToolbar.layoutStatement" },
168
- { value: "full-image", labelKey: "editorToolbar.layoutFullImage" },
169
- { value: "blank", labelKey: "editorToolbar.layoutBlank" },
170
- ];
171
-
172
- const backgroundOptions = [
173
- "bg-[#000000]",
174
- "bg-[#0a0a0a]",
175
- "bg-[#0f0f11]",
176
- "bg-[#111114]",
177
- "bg-[#141418]",
178
- "bg-gradient-to-br from-[#000000] to-[#0a0a14]",
179
- "bg-gradient-to-br from-[#0a0a0a] to-[#0f1a14]",
180
- "bg-[#ffffff]",
181
- ];
182
-
183
- const HEX_COLOR_PATTERN = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
184
131
  const TOOLBAR_ICON_BUTTON_CLASS =
185
132
  "inline-flex size-8 flex-shrink-0 items-center justify-center rounded-md transition-colors";
186
133
 
187
- /** Native <input type="color"> only accepts 3/6-digit hex — fall back to
188
- * black for gradients or other raw CSS values so the picker still opens. */
189
- function toColorInputValue(background: string | undefined): string {
190
- return background && HEX_COLOR_PATTERN.test(background)
191
- ? background
192
- : "#000000";
193
- }
194
-
195
- /** Popover anchored to a button ref */
196
- function ToolbarPopover({
197
- open,
198
- anchorRef,
199
- onClose,
200
- children,
201
- width = 160,
202
- align = "right",
203
- }: {
204
- open: boolean;
205
- anchorRef: React.RefObject<HTMLButtonElement | null>;
206
- onClose: () => void;
207
- children: React.ReactNode;
208
- width?: number;
209
- align?: "left" | "right";
210
- }) {
211
- const menuRef = useRef<HTMLDivElement>(null);
212
-
213
- useEffect(() => {
214
- if (!open) return;
215
- const handleClick = (e: MouseEvent) => {
216
- if (
217
- menuRef.current &&
218
- !menuRef.current.contains(e.target as Node) &&
219
- anchorRef.current &&
220
- !anchorRef.current.contains(e.target as Node)
221
- ) {
222
- onClose();
223
- }
224
- };
225
- document.addEventListener("mousedown", handleClick);
226
- return () => document.removeEventListener("mousedown", handleClick);
227
- }, [open, onClose, anchorRef]);
228
-
229
- if (!open || !anchorRef.current) return null;
230
- const rect = anchorRef.current.getBoundingClientRect();
231
- const vw = window.innerWidth;
232
- let left = align === "right" ? rect.right - width : rect.left;
233
- left = Math.max(8, Math.min(left, vw - width - 8));
234
-
235
- return createPortal(
236
- <div
237
- ref={menuRef}
238
- className="fixed rounded-lg border border-border bg-popover shadow-xl z-[200] max-h-[80vh] overflow-y-auto animate-in fade-in-0 zoom-in-95 duration-150 origin-top-left"
239
- style={{ top: rect.bottom + 4, left, width: Math.min(width, vw - 16) }}
240
- >
241
- {children}
242
- </div>,
243
- document.body,
244
- );
245
- }
246
-
247
134
  export default function EditorToolbar({
248
135
  deck,
249
136
  deckId,
@@ -255,12 +142,9 @@ export default function EditorToolbar({
255
142
  onToggleSidebar,
256
143
  onGenerateImage,
257
144
  onOpenAssetLibrary,
258
- imageGenButtonRef,
259
- assetsButtonRef,
260
145
  onShowHistory,
261
146
  historyButtonRef,
262
147
  currentSlide,
263
- onUpdateSlide,
264
148
  activeUsers,
265
149
  agentPresent,
266
150
  agentActive,
@@ -282,8 +166,6 @@ export default function EditorToolbar({
282
166
  onExportPdf,
283
167
  onExportPptx,
284
168
  onExportGoogleSlides,
285
- aspectRatio,
286
- onSetAspectRatio,
287
169
  onAddEmptySlide,
288
170
  onDuplicateCurrentSlide,
289
171
  currentSlideId,
@@ -336,43 +218,16 @@ export default function EditorToolbar({
336
218
  };
337
219
  }, []);
338
220
 
339
- const activeAspectRatio: AspectRatio = aspectRatio ?? DEFAULT_ASPECT_RATIO;
340
- const isCustomBackground =
341
- !!currentSlide?.background &&
342
- !backgroundOptions.includes(currentSlide.background);
343
- const [layoutOpen, setLayoutOpen] = useState(false);
344
- const layoutRef = useRef<HTMLButtonElement>(null);
345
-
346
- const handleLayoutSelect = (layout: SlideLayout) => {
347
- if (!currentSlide || !onUpdateSlide) return;
348
- if (currentSlide.layout !== layout) {
349
- onUpdateSlide({ layout, content: defaultSlideContent[layout] });
350
- }
351
- setLayoutOpen(false);
352
- };
353
- const [toolsOpen, setToolsOpen] = useState(false);
354
221
  // The contextual toolbar hosts the action cluster whenever it is on screen.
355
222
  // That row rides on SlideEditor, which only mounts for a real slide, so an
356
223
  // empty deck must keep this fallback or it has no way to add one.
357
224
  const contextToolbarVisible = canEdit && Boolean(currentSlide);
358
- const { undo, redo, canUndo, canRedo, getDeck } = useDecks();
359
225
  const fileInputRef = useRef<HTMLInputElement>(null);
360
226
  const [importing, setImporting] = useState(false);
361
227
  const { setTheme, resolvedTheme } = useTheme();
362
228
  const [themeMounted, setThemeMounted] = useState(false);
363
229
  useEffect(() => setThemeMounted(true), []);
364
230
  const isDark = themeMounted ? resolvedTheme === "dark" : false;
365
- // The secondary tools share an "active when something is on" indicator so
366
- // the dot on the consolidated button reflects any of them.
367
- const anyToolActive = Boolean(
368
- animationsOpen || tweaksOpen || drawMode || pinMode,
369
- );
370
-
371
- const closeAll = () => {
372
- setLayoutOpen(false);
373
- setToolsOpen(false);
374
- };
375
-
376
231
  const handleImportFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
377
232
  const file = e.target.files?.[0];
378
233
  if (!file) return;
@@ -450,7 +305,7 @@ export default function EditorToolbar({
450
305
  };
451
306
 
452
307
  return (
453
- <div className="flex h-11 shrink-0 items-center gap-1 overflow-x-auto whitespace-nowrap border-b border-border/70 bg-background/95 px-2 shadow-[0_1px_0_hsl(var(--border)/0.35)] backdrop-blur sm:px-3">
308
+ <div className="flex h-11 shrink-0 items-center gap-1 overflow-x-auto whitespace-nowrap border-b border-border/70 bg-background/95 px-2 shadow-[0_1px_0_hsl(var(--border)/0.35)] sm:px-3">
454
309
  {/* Back button */}
455
310
  <Tooltip>
456
311
  <TooltipTrigger asChild>
@@ -525,407 +380,6 @@ export default function EditorToolbar({
525
380
  </span>
526
381
  )}
527
382
 
528
- {/* Slide settings cog menu */}
529
- {canEdit && currentSlide && onUpdateSlide && (
530
- <>
531
- <Tooltip>
532
- <TooltipTrigger asChild>
533
- <button
534
- ref={layoutRef}
535
- onClick={() => {
536
- closeAll();
537
- setLayoutOpen(!layoutOpen);
538
- }}
539
- className={`${TOOLBAR_ICON_BUTTON_CLASS} ${
540
- layoutOpen
541
- ? "text-foreground/90 bg-accent"
542
- : "text-muted-foreground hover:text-foreground/70 hover:bg-accent"
543
- }`}
544
- aria-label={t("editorToolbar.slideSettings")}
545
- >
546
- <IconSettings className="size-4" />
547
- </button>
548
- </TooltipTrigger>
549
- <TooltipContent>{t("editorToolbar.slideSettings")}</TooltipContent>
550
- </Tooltip>
551
- <ToolbarPopover
552
- open={layoutOpen}
553
- anchorRef={layoutRef}
554
- onClose={() => setLayoutOpen(false)}
555
- width={220}
556
- >
557
- <div className="py-1.5">
558
- {/* Layout section */}
559
- <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
560
- {t("editorToolbar.layout")}
561
- </div>
562
- {slideLayoutOptions.map((opt) => (
563
- <button
564
- key={opt.value}
565
- onClick={() => handleLayoutSelect(opt.value)}
566
- className={`flex items-center gap-2 w-full px-3 py-1.5 text-xs transition-colors ${
567
- currentSlide.layout === opt.value
568
- ? "text-[#609FF8] bg-accent/50"
569
- : "text-muted-foreground hover:text-foreground hover:bg-accent/50"
570
- }`}
571
- >
572
- <IconLayout className="w-3 h-3" />
573
- {t(opt.labelKey)}
574
- </button>
575
- ))}
576
-
577
- {/* Background section */}
578
- <div className="mx-2 my-1.5 border-t border-border" />
579
- <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
580
- {t("editorToolbar.background")}
581
- </div>
582
- <div className="px-3 pb-2">
583
- <div className="grid grid-cols-4 gap-2">
584
- {backgroundOptions.map((bg, i) => (
585
- <button
586
- key={i}
587
- onClick={() => {
588
- onUpdateSlide!({ background: bg });
589
- }}
590
- className={`w-10 h-7 rounded-md border transition-all ${bg} ${
591
- currentSlide.background === bg
592
- ? "border-[#609FF8] ring-1 ring-[#609FF8]/30"
593
- : "border-border hover:border-foreground/20"
594
- }`}
595
- />
596
- ))}
597
- <label
598
- className={`relative w-10 h-7 rounded-md border cursor-pointer overflow-hidden transition-all ${
599
- isCustomBackground
600
- ? "border-[#609FF8] ring-1 ring-[#609FF8]/30"
601
- : "border-border hover:border-foreground/20"
602
- }`}
603
- style={{
604
- background: isCustomBackground
605
- ? currentSlide.background
606
- : "conic-gradient(from 180deg, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000)",
607
- }}
608
- title={t("editorToolbar.customColor")}
609
- >
610
- <input
611
- type="color"
612
- value={toColorInputValue(currentSlide.background)}
613
- onChange={(e) => {
614
- onUpdateSlide!({ background: e.target.value });
615
- }}
616
- className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
617
- aria-label={t("editorToolbar.customColor")}
618
- />
619
- </label>
620
- </div>
621
- </div>
622
-
623
- {/* Image & Assets section */}
624
- <div className="mx-2 my-1.5 border-t border-border" />
625
- <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
626
- {t("editorToolbar.media")}
627
- </div>
628
- <button
629
- ref={imageGenButtonRef}
630
- onClick={() => {
631
- onGenerateImage();
632
- setLayoutOpen(false);
633
- }}
634
- className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
635
- >
636
- <IconPhoto className="w-3 h-3" />
637
- {t("editorToolbar.generateImage")}
638
- </button>
639
- <button
640
- ref={assetsButtonRef}
641
- onClick={() => {
642
- onOpenAssetLibrary();
643
- setLayoutOpen(false);
644
- }}
645
- className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
646
- >
647
- <IconFolderOpen className="w-3 h-3" />
648
- {t("editorToolbar.assetLibrary")}
649
- </button>
650
-
651
- {/* Diagrams section */}
652
- <div className="mx-2 my-1.5 border-t border-border" />
653
- <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
654
- {t("editorToolbar.diagrams")}
655
- </div>
656
- <button
657
- onClick={async () => {
658
- if (!onUpdateSlide || !currentSlide) return;
659
- // Pin the slide this insert targets before the async
660
- // import/parse below, so navigating to another slide in the
661
- // meantime can't redirect where the diagram lands.
662
- const targetSlideId = currentSlide.id;
663
- const defaultMermaidDefinition = `graph TD
664
- A[Start] --> B{Decision}
665
- B -->|Yes| C[Action A]
666
- B -->|No| D[Action B]
667
- C --> E[End]
668
- D --> E`;
669
- try {
670
- const { convertMermaidToExcalidraw } =
671
- await import("./MermaidToExcalidrawPanel");
672
- const data = await convertMermaidToExcalidraw(
673
- defaultMermaidDefinition,
674
- );
675
- // The target slide may have been deleted while the import/
676
- // parse above was in flight — patch-deck rejects the whole
677
- // batch for a missing slide id, so re-check with the live
678
- // deck (not the stale `currentSlide` this closure captured)
679
- // and drop the result instead of enqueueing a dead op.
680
- const stillExists = getDeck(deckId)?.slides.some(
681
- (s) => s.id === targetSlideId,
682
- );
683
- if (!stillExists) return;
684
- onUpdateSlide({ excalidrawData: data }, targetSlideId);
685
- setLayoutOpen(false);
686
- } catch (err) {
687
- console.error("Insert Mermaid diagram failed:", err);
688
- toast.error(t("editorToolbar.insertMermaidFailed"), {
689
- description:
690
- err instanceof Error ? err.message : undefined,
691
- });
692
- }
693
- }}
694
- className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
695
- >
696
- <IconSchema className="w-3 h-3" />
697
- {t("editorToolbar.insertMermaidDiagram")}
698
- </button>
699
- <button
700
- onClick={() => {
701
- if (!onUpdateSlide) return;
702
- onUpdateSlide({
703
- excalidrawData: JSON.stringify({
704
- elements: [],
705
- appState: { viewBackgroundColor: "transparent" },
706
- files: {},
707
- }),
708
- });
709
- setLayoutOpen(false);
710
- }}
711
- className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
712
- >
713
- <IconPencil className="w-3 h-3" />
714
- {t("editorToolbar.excalidrawCanvas")}
715
- </button>
716
- {typeof currentSlide?.content === "string" &&
717
- currentSlide.content.includes('class="mermaid"') && (
718
- <button
719
- onClick={async () => {
720
- if (!onUpdateSlide || !currentSlide) return;
721
- // Same pinning as the insert handler above.
722
- const targetSlideId = currentSlide.id;
723
- try {
724
- const match = currentSlide.content.match(
725
- /<div\s+class="mermaid"[^>]*>([\s\S]*?)<\/div>/i,
726
- );
727
- if (!match) return;
728
- const { convertMermaidToExcalidraw } =
729
- await import("./MermaidToExcalidrawPanel");
730
- const data = await convertMermaidToExcalidraw(
731
- match[1].trim(),
732
- );
733
- // Same stale-target guard as the insert handler above.
734
- const stillExists = getDeck(deckId)?.slides.some(
735
- (s) => s.id === targetSlideId,
736
- );
737
- if (!stillExists) return;
738
- onUpdateSlide({ excalidrawData: data }, targetSlideId);
739
- setLayoutOpen(false);
740
- } catch (err) {
741
- console.error("Mermaid to Excalidraw failed:", err);
742
- toast.error(t("editorToolbar.convertMermaidFailed"), {
743
- description:
744
- err instanceof Error ? err.message : undefined,
745
- });
746
- }
747
- }}
748
- className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-[hsl(var(--accent-cyan))] hover:bg-accent/50 transition-colors"
749
- >
750
- <IconTransform className="w-3 h-3" />
751
- {t("editorToolbar.convertMermaidToExcalidraw")}
752
- </button>
753
- )}
754
- {currentSlide?.excalidrawData && (
755
- <button
756
- onClick={() => {
757
- if (!onUpdateSlide) return;
758
- onUpdateSlide({ excalidrawData: undefined });
759
- setLayoutOpen(false);
760
- }}
761
- className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-muted-foreground hover:text-muted-foreground hover:bg-accent/50 transition-colors"
762
- >
763
- <IconPencil className="w-3 h-3" />
764
- {t("editorToolbar.removeExcalidrawCanvas")}
765
- </button>
766
- )}
767
-
768
- {/* Transitions section */}
769
- <div className="mx-2 my-1.5 border-t border-border" />
770
- <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
771
- {t("editorToolbar.transition")}
772
- </div>
773
- <div className="px-3 pb-2.5 grid grid-cols-4 gap-1">
774
- {(["instant", "fade", "slide", "zoom"] as const).map(
775
- (transition) => {
776
- const active =
777
- transition === "instant"
778
- ? !currentSlide.transition ||
779
- currentSlide.transition === "instant" ||
780
- currentSlide.transition === "none"
781
- : currentSlide.transition === transition;
782
- return (
783
- <button
784
- key={transition}
785
- onClick={() => onUpdateSlide!({ transition })}
786
- className={`px-1.5 py-1 rounded text-[10px] font-medium capitalize border ${
787
- active
788
- ? "bg-[#609FF8]/20 text-[#609FF8] border-[#609FF8]/30"
789
- : "text-muted-foreground hover:text-foreground/70 hover:bg-accent/50 border-transparent"
790
- }`}
791
- >
792
- {t(`editorToolbar.transition_${transition}`)}
793
- </button>
794
- );
795
- },
796
- )}
797
- </div>
798
-
799
- {/* Aspect Ratio section (deck-level) */}
800
- {onSetAspectRatio && (
801
- <>
802
- <div className="mx-2 my-1.5 border-t border-border" />
803
- <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
804
- {t("editorToolbar.aspectRatio")}
805
- </div>
806
- <div className="px-3 pb-2.5 grid grid-cols-4 gap-1">
807
- {ASPECT_RATIO_VALUES.map((r) => {
808
- const active = activeAspectRatio === r;
809
- return (
810
- <Tooltip key={r}>
811
- <TooltipTrigger asChild>
812
- <button
813
- onClick={() => onSetAspectRatio(r)}
814
- className={`px-1.5 py-1 rounded text-[10px] font-medium border ${
815
- active
816
- ? "bg-[#609FF8]/20 text-[#609FF8] border-[#609FF8]/30"
817
- : "text-muted-foreground hover:text-foreground hover:bg-accent/50 border-transparent"
818
- }`}
819
- >
820
- {r}
821
- </button>
822
- </TooltipTrigger>
823
- <TooltipContent>
824
- {t("editorToolbar.setAspectRatio", { ratio: r })}
825
- </TooltipContent>
826
- </Tooltip>
827
- );
828
- })}
829
- </div>
830
- </>
831
- )}
832
- </div>
833
- </ToolbarPopover>
834
- </>
835
- )}
836
-
837
- {/* Slide tools palette — animations, tweaks, draw, comment-pin all live
838
- * inside one popover so the toolbar doesn't drown in icons. Hidden in
839
- * view-only mode since none of these affordances apply. */}
840
- {canEdit &&
841
- (onToggleAnimations ||
842
- onToggleTweaks ||
843
- onToggleDrawMode ||
844
- onTogglePinMode) && (
845
- <DropdownMenu
846
- open={toolsOpen}
847
- onOpenChange={(open) => {
848
- if (open) setLayoutOpen(false);
849
- setToolsOpen(open);
850
- }}
851
- >
852
- <Tooltip>
853
- <TooltipTrigger asChild>
854
- <DropdownMenuTrigger asChild>
855
- <button
856
- className={`${TOOLBAR_ICON_BUTTON_CLASS} relative cursor-pointer ${
857
- anyToolActive || toolsOpen
858
- ? "bg-accent text-foreground"
859
- : "text-muted-foreground hover:text-foreground/70 hover:bg-accent"
860
- }`}
861
- aria-label={t("editorToolbar.slideTools")}
862
- >
863
- <IconTool className="size-4" />
864
- {anyToolActive && !toolsOpen && (
865
- <span className="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-[#609FF8]" />
866
- )}
867
- </button>
868
- </DropdownMenuTrigger>
869
- </TooltipTrigger>
870
- <TooltipContent>{t("editorToolbar.slideTools")}</TooltipContent>
871
- </Tooltip>
872
- <DropdownMenuContent align="end" className="w-52">
873
- <DropdownMenuGroup>
874
- {currentSlide && onToggleAnimations && (
875
- <DropdownMenuItem
876
- onSelect={onToggleAnimations}
877
- className={
878
- animationsOpen
879
- ? "bg-accent text-accent-foreground"
880
- : undefined
881
- }
882
- >
883
- <IconBolt className="size-4" />
884
- {t("editorToolbar.elementAnimations")}
885
- </DropdownMenuItem>
886
- )}
887
- {onToggleTweaks && (
888
- <DropdownMenuItem
889
- onSelect={onToggleTweaks}
890
- className={
891
- tweaksOpen
892
- ? "bg-accent text-accent-foreground"
893
- : undefined
894
- }
895
- >
896
- <IconAdjustments className="size-4" />
897
- {t("editorToolbar.tweaks")}
898
- </DropdownMenuItem>
899
- )}
900
- {onToggleDrawMode && (
901
- <DropdownMenuItem
902
- onSelect={onToggleDrawMode}
903
- data-toolbar-draw-button
904
- className={
905
- drawMode ? "bg-accent text-accent-foreground" : undefined
906
- }
907
- >
908
- <IconPencilPlus className="size-4" />
909
- {t("editorToolbar.drawOnSlide")}
910
- </DropdownMenuItem>
911
- )}
912
- {onTogglePinMode && (
913
- <DropdownMenuItem
914
- onSelect={onTogglePinMode}
915
- data-toolbar-pin-button
916
- className={
917
- pinMode ? "bg-accent text-accent-foreground" : undefined
918
- }
919
- >
920
- <IconPin className="size-4" />
921
- {t("editorToolbar.pinComments")}
922
- </DropdownMenuItem>
923
- )}
924
- </DropdownMenuGroup>
925
- </DropdownMenuContent>
926
- </DropdownMenu>
927
- )}
928
-
929
383
  {/* Save status — subtle "Saving…" / "Saved" / offline pill. Renders
930
384
  nothing when idle. Only meaningful for editors. */}
931
385
  {canEdit && (
@@ -941,47 +395,11 @@ export default function EditorToolbar({
941
395
  activeUsers={activeUsers ?? []}
942
396
  agentPresent={agentPresent}
943
397
  agentActive={agentActive}
398
+ showAgentEditingDot={false}
944
399
  currentUserEmail={currentUserEmail}
945
400
  className="flex-shrink-0 mr-0.5"
946
401
  />
947
402
 
948
- {/* Comments toggle */}
949
- {onToggleComments && (
950
- <Tooltip>
951
- <TooltipTrigger asChild>
952
- <button
953
- onClick={onToggleComments}
954
- className={`${TOOLBAR_ICON_BUTTON_CLASS} relative ${
955
- commentsOpen
956
- ? "text-foreground bg-accent"
957
- : "text-muted-foreground hover:text-foreground/70 hover:bg-accent"
958
- }`}
959
- aria-label={t("editorToolbar.comments")}
960
- >
961
- <IconMessage className="size-4" />
962
- {unresolvedCommentCount > 0 && (
963
- <span className="absolute -top-0.5 -right-0.5 w-3.5 h-3.5 rounded-full bg-[#609FF8] text-[8px] font-bold text-black flex items-center justify-center leading-none">
964
- {unresolvedCommentCount > 9 ? "9+" : unresolvedCommentCount}
965
- </span>
966
- )}
967
- </button>
968
- </TooltipTrigger>
969
- <TooltipContent>{t("editorToolbar.comments")}</TooltipContent>
970
- </Tooltip>
971
- )}
972
-
973
- {/* Export / Share menu (export, duplicate, share) */}
974
- <div className="flex-shrink-0">
975
- <ExportMenu
976
- deckId={deckId}
977
- deckTitle={deckTitle}
978
- onDuplicate={onDuplicateDeck ?? (() => {})}
979
- onExportPdf={onExportPdf ?? (() => {})}
980
- onExportPptx={onExportPptx ?? (() => {})}
981
- onExportGoogleSlides={onExportGoogleSlides}
982
- />
983
- </div>
984
-
985
403
  {/* Framework share (ownership, per-user/org grants, visibility) */}
986
404
  <div className="flex-shrink-0">
987
405
  <ShareButton
@@ -1018,7 +436,7 @@ export default function EditorToolbar({
1018
436
  </div>
1019
437
  {/* Present button — matches Share trigger height (h-9) */}
1020
438
  <Link
1021
- to={`/deck/${deckId}/present`}
439
+ to={`/deck/${deckId}/present?slide=${currentSlideIndex + 1}`}
1022
440
  className="inline-flex h-9 flex-shrink-0 items-center justify-center gap-1.5 rounded-md border border-border bg-primary px-3 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
1023
441
  >
1024
442
  <IconPlayerPlay className="w-3.5 h-3.5" />
@@ -1034,7 +452,7 @@ export default function EditorToolbar({
1034
452
  className="hidden"
1035
453
  />
1036
454
 
1037
- {/* Overflow — Import / History / Theme tucked away to clean the bar. */}
455
+ {/* Consolidated editor menu */}
1038
456
  <DropdownMenu>
1039
457
  <Tooltip>
1040
458
  <TooltipTrigger asChild>
@@ -1050,53 +468,152 @@ export default function EditorToolbar({
1050
468
  </TooltipTrigger>
1051
469
  <TooltipContent>{t("editorToolbar.more")}</TooltipContent>
1052
470
  </Tooltip>
1053
- <DropdownMenuContent align="end" className="w-48">
471
+ <DropdownMenuContent
472
+ align="end"
473
+ className="max-h-[min(80vh,32rem)] w-64 overflow-y-auto"
474
+ >
1054
475
  {canEdit && (
1055
476
  <>
477
+ <DropdownMenuLabel>{t("editorToolbar.media")}</DropdownMenuLabel>
478
+ <DropdownMenuGroup>
479
+ <DropdownMenuItem onSelect={onGenerateImage}>
480
+ <IconPhoto className="size-4" />
481
+ {t("editorToolbar.generateImage")}
482
+ </DropdownMenuItem>
483
+ <DropdownMenuItem onSelect={onOpenAssetLibrary}>
484
+ <IconFolderOpen className="size-4" />
485
+ {t("editorToolbar.assetLibrary")}
486
+ </DropdownMenuItem>
487
+ </DropdownMenuGroup>
488
+ </>
489
+ )}
490
+
491
+ {canEdit &&
492
+ (onToggleAnimations ||
493
+ onToggleTweaks ||
494
+ onToggleDrawMode ||
495
+ onTogglePinMode) && (
496
+ <>
497
+ <DropdownMenuSeparator />
498
+ <DropdownMenuLabel>
499
+ {t("editorToolbar.slideTools")}
500
+ </DropdownMenuLabel>
501
+ <DropdownMenuGroup>
502
+ {currentSlide && onToggleAnimations && (
503
+ <DropdownMenuItem
504
+ onSelect={onToggleAnimations}
505
+ className={
506
+ animationsOpen
507
+ ? "bg-accent text-accent-foreground"
508
+ : undefined
509
+ }
510
+ >
511
+ <IconBolt className="size-4" />
512
+ {t("editorToolbar.elementAnimations")}
513
+ </DropdownMenuItem>
514
+ )}
515
+ {onToggleTweaks && (
516
+ <DropdownMenuItem
517
+ onSelect={onToggleTweaks}
518
+ className={
519
+ tweaksOpen
520
+ ? "bg-accent text-accent-foreground"
521
+ : undefined
522
+ }
523
+ >
524
+ <IconAdjustments className="size-4" />
525
+ {t("editorToolbar.tweaks")}
526
+ </DropdownMenuItem>
527
+ )}
528
+ {onToggleDrawMode && (
529
+ <DropdownMenuItem
530
+ onSelect={onToggleDrawMode}
531
+ data-toolbar-draw-button
532
+ className={
533
+ drawMode
534
+ ? "bg-accent text-accent-foreground"
535
+ : undefined
536
+ }
537
+ >
538
+ <IconPencilPlus className="size-4" />
539
+ {t("editorToolbar.drawOnSlide")}
540
+ </DropdownMenuItem>
541
+ )}
542
+ {onTogglePinMode && (
543
+ <DropdownMenuItem
544
+ onSelect={onTogglePinMode}
545
+ data-toolbar-pin-button
546
+ className={
547
+ pinMode ? "bg-accent text-accent-foreground" : undefined
548
+ }
549
+ >
550
+ <IconPin className="size-4" />
551
+ {t("editorToolbar.pinComments")}
552
+ </DropdownMenuItem>
553
+ )}
554
+ </DropdownMenuGroup>
555
+ </>
556
+ )}
557
+
558
+ {onToggleComments && (
559
+ <>
560
+ <DropdownMenuSeparator />
561
+ <DropdownMenuLabel>
562
+ {t("editorToolbar.comments")}
563
+ </DropdownMenuLabel>
1056
564
  <DropdownMenuItem
1057
- disabled={!canUndo}
1058
- onSelect={() => commitActiveEditThenRun(undo)}
1059
- >
1060
- <IconArrowBackUp className="w-4 h-4 mr-2" />
1061
- {t("editorToolbar.undoWithShortcut", {
1062
- shortcut: shortcutLabel("Cmd+Z"),
1063
- })}
1064
- </DropdownMenuItem>
1065
- <DropdownMenuItem
1066
- disabled={!canRedo}
1067
- onSelect={() => commitActiveEditThenRun(redo)}
565
+ onSelect={onToggleComments}
566
+ className={
567
+ commentsOpen ? "bg-accent text-accent-foreground" : undefined
568
+ }
1068
569
  >
1069
- <IconArrowForwardUp className="w-4 h-4 mr-2" />
1070
- {t("editorToolbar.redoWithShortcut", {
1071
- shortcut: shortcutLabel("Cmd+Shift+Z"),
1072
- })}
570
+ <IconMessage className="size-4" />
571
+ {t("editorToolbar.comments")}
572
+ {unresolvedCommentCount > 0 && (
573
+ <span className="ml-auto rounded-full bg-primary px-1.5 text-[10px] font-semibold text-primary-foreground">
574
+ {unresolvedCommentCount > 9 ? "9+" : unresolvedCommentCount}
575
+ </span>
576
+ )}
1073
577
  </DropdownMenuItem>
1074
- <DropdownMenuSeparator />
1075
578
  </>
1076
579
  )}
580
+
581
+ <DropdownMenuSeparator />
582
+ <ExportMenu
583
+ inline
584
+ deckId={deckId}
585
+ deckTitle={deckTitle}
586
+ onDuplicate={onDuplicateDeck ?? (() => {})}
587
+ onExportPdf={onExportPdf ?? (() => {})}
588
+ onExportPptx={onExportPptx ?? (() => {})}
589
+ onExportGoogleSlides={onExportGoogleSlides}
590
+ />
591
+ <DropdownMenuSeparator />
1077
592
  <DropdownMenuItem
1078
593
  disabled={importing}
1079
594
  onSelect={() => fileInputRef.current?.click()}
1080
595
  >
1081
596
  {importing ? (
1082
- <IconLoader2 className="w-4 h-4 mr-2 animate-spin" />
597
+ <IconLoader2 className="size-4 animate-spin" />
1083
598
  ) : (
1084
- <IconDownload className="w-4 h-4 mr-2" />
599
+ <IconDownload className="size-4" />
1085
600
  )}
1086
601
  {importing
1087
602
  ? t("editorToolbar.importing")
1088
603
  : t("editorToolbar.importFile")}
1089
604
  </DropdownMenuItem>
1090
- <DropdownMenuItem onClick={onShowHistory}>
1091
- <IconHistory className="w-4 h-4 mr-2" />
605
+ <DropdownMenuItem onSelect={onShowHistory}>
606
+ <IconHistory className="size-4" />
1092
607
  {t("editorToolbar.savedVersions")}
1093
608
  </DropdownMenuItem>
1094
609
  <DropdownMenuSeparator />
1095
- <DropdownMenuItem onClick={() => setTheme(isDark ? "light" : "dark")}>
610
+ <DropdownMenuItem
611
+ onSelect={() => setTheme(isDark ? "light" : "dark")}
612
+ >
1096
613
  {isDark ? (
1097
- <IconSun className="w-4 h-4 mr-2" />
614
+ <IconSun className="size-4" />
1098
615
  ) : (
1099
- <IconMoon className="w-4 h-4 mr-2" />
616
+ <IconMoon className="size-4" />
1100
617
  )}
1101
618
  {isDark
1102
619
  ? t("editorToolbar.lightTheme")
@@ -1104,7 +621,6 @@ export default function EditorToolbar({
1104
621
  </DropdownMenuItem>
1105
622
  </DropdownMenuContent>
1106
623
  </DropdownMenu>
1107
-
1108
624
  <RunsTray pollMs={0} />
1109
625
  <AgentToggleButton />
1110
626
  </div>