@agent-native/core 0.137.1 → 0.137.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.
- package/corpus/README.md +1 -1
- package/corpus/templates/slides/app/components/editor/AddSlidePopover.tsx +258 -0
- package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +130 -0
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +25 -337
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +73 -53
- package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +804 -0
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +101 -45
- package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +7 -4
- package/corpus/templates/slides/app/components/editor/bullet-editing.ts +11 -3
- package/corpus/templates/slides/app/components/editor/commit-active-edit.ts +21 -0
- package/corpus/templates/slides/app/components/editor/list-editing.ts +219 -0
- package/corpus/templates/slides/app/components/editor/selection-overlay-measurement.ts +0 -3
- package/corpus/templates/slides/app/components/editor/slide-style.ts +199 -0
- package/corpus/templates/slides/app/global.css +14 -3
- package/corpus/templates/slides/app/i18n/en-US.ts +8 -1
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +48 -22
- package/corpus/templates/slides/changelog/2026-08-01-add-slide-moved-to-the-toolbar-and-the-slide-rail-is-now-mor.md +9 -0
- package/corpus/templates/slides/changelog/2026-08-01-add-slide-undo-redo-and-the-text-tool-now-lead-the-slide-too.md +9 -0
- package/corpus/templates/slides/changelog/2026-08-01-slide-styling-now-appears-in-a-contextual-toolbar-above-the-.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-01-the-slide-style-side-panel-is-retired-all-styling-now-lives-.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-03-the-slide-toolbar-is-denser-swatch-only-colors-dropdowns-for.md +10 -0
- package/corpus/templates/slides/changelog/2026-08-04-bullet-and-numbered-list-buttons-in-the-slide-toolbar-conver.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-04-italic-and-underline-are-now-one-click-away-in-the-slide-too.md +6 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +2 -2
- package/corpus/templates/slides/app/components/editor/SlideStyleInspector.tsx +0 -763
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
type AttributedRecentEdit,
|
|
7
7
|
type CollabUser,
|
|
8
8
|
} from "@agent-native/core/client/collab";
|
|
9
|
-
import { PromptComposer } from "@agent-native/core/client/composer";
|
|
10
9
|
import { useAvatarUrl } from "@agent-native/core/client/hooks";
|
|
11
10
|
import { useT } from "@agent-native/core/client/i18n";
|
|
12
11
|
import { RecentEditHighlights } from "@agent-native/toolkit/collab-ui";
|
|
@@ -19,34 +18,23 @@ import { CSS } from "@dnd-kit/utilities";
|
|
|
19
18
|
import { appStateKeyForBrowserTab } from "@shared/app-state-tabs";
|
|
20
19
|
import { hashSlideContent, type DeckFitState } from "@shared/slide-fit";
|
|
21
20
|
import {
|
|
22
|
-
IconPlus,
|
|
23
21
|
IconGripVertical,
|
|
24
22
|
IconCopy,
|
|
25
23
|
IconTrash,
|
|
26
24
|
IconLoader2,
|
|
27
|
-
IconSquarePlus,
|
|
28
25
|
} from "@tabler/icons-react";
|
|
29
|
-
import {
|
|
26
|
+
import { useRef, useEffect } from "react";
|
|
30
27
|
import { useCallback } from "react";
|
|
31
|
-
import { createPortal } from "react-dom";
|
|
32
|
-
import { toast } from "sonner";
|
|
33
28
|
|
|
34
29
|
import SlideRenderer from "@/components/deck/SlideRenderer";
|
|
35
30
|
import type { SlideOverflowInfo } from "@/components/deck/SlideRenderer";
|
|
36
31
|
import GeneratingSlidePreview from "@/components/editor/GeneratingSlidePreview";
|
|
37
|
-
import { GoogleDocImportHint } from "@/components/editor/GoogleDocImportHint";
|
|
38
|
-
import {
|
|
39
|
-
isInsidePortaledLayer,
|
|
40
|
-
type UploadedFile,
|
|
41
|
-
} from "@/components/editor/PromptDialog";
|
|
42
32
|
import {
|
|
43
33
|
Tooltip,
|
|
44
34
|
TooltipContent,
|
|
45
35
|
TooltipTrigger,
|
|
46
36
|
} from "@/components/ui/tooltip";
|
|
47
37
|
import type { Slide } from "@/context/DeckContext";
|
|
48
|
-
import { useAgentGenerating } from "@/hooks/use-agent-generating";
|
|
49
|
-
import { addSlideAgentMessage } from "@/lib/agent-visible-message";
|
|
50
38
|
import type { AspectRatio } from "@/lib/aspect-ratios";
|
|
51
39
|
import { TAB_ID } from "@/lib/tab-id";
|
|
52
40
|
|
|
@@ -54,11 +42,9 @@ interface EditorSidebarProps {
|
|
|
54
42
|
slides: Slide[];
|
|
55
43
|
activeSlideId: string;
|
|
56
44
|
deckId: string;
|
|
57
|
-
deckTitle: string;
|
|
58
45
|
onSelectSlide: (id: string) => void;
|
|
59
46
|
onDuplicateSlide: (id: string) => void;
|
|
60
47
|
onDeleteSlide: (id: string) => void;
|
|
61
|
-
onAddEmptySlide: () => void;
|
|
62
48
|
/** Viewer-role decks get thumbnails only: no add, duplicate, or delete. */
|
|
63
49
|
readOnly?: boolean;
|
|
64
50
|
/** Presence map: slideId → list of users currently viewing that slide */
|
|
@@ -71,8 +57,6 @@ interface EditorSidebarProps {
|
|
|
71
57
|
generatingSlide?: { index: number; content?: string | null };
|
|
72
58
|
generatingSlideSelected?: boolean;
|
|
73
59
|
onSelectGeneratingSlide?: () => void;
|
|
74
|
-
addSlideGenerating?: boolean;
|
|
75
|
-
onAddSlideGeneratingChange?: (generating: boolean) => void;
|
|
76
60
|
}
|
|
77
61
|
|
|
78
62
|
const DECK_FIT_STATE_KEYS = [
|
|
@@ -92,46 +76,6 @@ function slideIdFromEdit(edit: AttributedRecentEdit): string | null {
|
|
|
92
76
|
return null;
|
|
93
77
|
}
|
|
94
78
|
|
|
95
|
-
const MAX_SOURCE_CONTEXT_CHARS = 60_000;
|
|
96
|
-
|
|
97
|
-
function truncateSourceForContext(prompt: string): {
|
|
98
|
-
text: string;
|
|
99
|
-
truncated: boolean;
|
|
100
|
-
} {
|
|
101
|
-
if (prompt.length <= MAX_SOURCE_CONTEXT_CHARS) {
|
|
102
|
-
return { text: prompt, truncated: false };
|
|
103
|
-
}
|
|
104
|
-
return {
|
|
105
|
-
text: prompt.slice(0, MAX_SOURCE_CONTEXT_CHARS),
|
|
106
|
-
truncated: true,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function describeUploadedFilesForAgent(
|
|
111
|
-
files: UploadedFile[],
|
|
112
|
-
deckId: string,
|
|
113
|
-
): string {
|
|
114
|
-
if (files.length === 0) return "";
|
|
115
|
-
const fileList = files
|
|
116
|
-
.map(
|
|
117
|
-
(f) =>
|
|
118
|
-
`- ${f.originalName} (${f.type}, ${(f.size / 1024).toFixed(1)}KB) at path: ${f.path}${f.url ? `; embeddable URL: ${f.url}` : ""}`,
|
|
119
|
-
)
|
|
120
|
-
.join("\n");
|
|
121
|
-
return [
|
|
122
|
-
"",
|
|
123
|
-
`The user uploaded ${files.length} file(s). These paths are real uploaded files; process them with import actions before using their contents:`,
|
|
124
|
-
fileList,
|
|
125
|
-
"",
|
|
126
|
-
"File handling rules:",
|
|
127
|
-
`- PPTX files: call \`import-pptx --filePath "<path>" --deckId ${deckId}\` when the user wants the deck/slides imported, or to extract slide source from a presentation.`,
|
|
128
|
-
`- PDF and DOCX files: call \`import-file --filePath "<path>" --format auto --deckId ${deckId}\` and use the returned extracted text as source material. The returned text is capped for reliability; re-run with maxChars only if more context is needed.`,
|
|
129
|
-
"- Text-like files: use the uploaded-text-file blocks already included in the prompt; do not call import-file for them.",
|
|
130
|
-
'- Image files with an embeddable URL can be inserted directly into slide HTML as `<img src="...">` or used as visual references.',
|
|
131
|
-
"- Image files without a URL are visual/reference assets only; do not claim to have processed a PPTX/PDF/DOCX unless the relevant import action succeeds.",
|
|
132
|
-
].join("\n");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
79
|
/** Small presence avatar circle with hover card showing name + email */
|
|
136
80
|
function PresenceAvatarTip({
|
|
137
81
|
user,
|
|
@@ -192,6 +136,14 @@ function PresenceAvatarTip({
|
|
|
192
136
|
);
|
|
193
137
|
}
|
|
194
138
|
|
|
139
|
+
// Thumbnail overlays sit on top of rendered slide artwork, which is arbitrary
|
|
140
|
+
// user content rather than a themed app surface, so they use a fixed scrim.
|
|
141
|
+
const THUMB_OVERLAY_CLASS =
|
|
142
|
+
// guard:allow-raw-color — matches the duplicate/delete overlays below.
|
|
143
|
+
"absolute top-2 left-7 rounded bg-black/60 p-1 backdrop-blur-sm border border-white/10 cursor-grab active:cursor-grabbing sm:opacity-0 sm:group-hover:opacity-100";
|
|
144
|
+
// guard:allow-raw-color — same fixed scrim as the class above.
|
|
145
|
+
const THUMB_OVERLAY_ICON_CLASS = "w-3 h-3 text-white/60";
|
|
146
|
+
|
|
195
147
|
function SortableSlideThumb({
|
|
196
148
|
slide,
|
|
197
149
|
index,
|
|
@@ -245,28 +197,19 @@ function SortableSlideThumb({
|
|
|
245
197
|
aria-label={t("editorSidebar.selectSlide", { number: index + 1 })}
|
|
246
198
|
aria-current={isActive ? "true" : undefined}
|
|
247
199
|
data-slide-thumbnail-id={slide.id}
|
|
248
|
-
className={`w-full text-left flex items-start gap-
|
|
200
|
+
className={`w-full text-left flex items-start gap-1.5 p-1.5 rounded-lg transition-[background-color,box-shadow] duration-150 ${
|
|
249
201
|
isActive ? "bg-accent ring-1 ring-[#609FF8]/50" : "hover:bg-accent"
|
|
250
202
|
} focus:outline-none`}
|
|
251
203
|
>
|
|
252
|
-
{/* Drag handle */}
|
|
253
|
-
<div
|
|
254
|
-
{...attributes}
|
|
255
|
-
{...listeners}
|
|
256
|
-
className="flex-shrink-0 mt-2 cursor-grab active:cursor-grabbing sm:opacity-0 sm:group-hover:opacity-100"
|
|
257
|
-
>
|
|
258
|
-
<IconGripVertical className="w-3.5 h-3.5 text-muted-foreground/70" />
|
|
259
|
-
</div>
|
|
260
|
-
|
|
261
204
|
{/* Index and slide presence share the fixed rail so presence does not resize the row. */}
|
|
262
|
-
<div className="relative flex-shrink-0 w-
|
|
263
|
-
<span className="block text-center text-[10px] font-medium text-muted-foreground/70">
|
|
205
|
+
<div className="relative flex-shrink-0 w-4 self-stretch">
|
|
206
|
+
<span className="block text-center text-[10px] font-medium leading-5 text-muted-foreground/70">
|
|
264
207
|
{index + 1}
|
|
265
208
|
</span>
|
|
266
209
|
{presenceUsers.length > 0 && (
|
|
267
|
-
<div className="absolute left-1/2 top-
|
|
210
|
+
<div className="absolute left-1/2 top-5 z-10 flex -translate-x-1/2 flex-col items-center gap-1">
|
|
268
211
|
{presenceUsers.slice(0, 4).map((u, i) => (
|
|
269
|
-
<PresenceAvatarTip key={i} user={u} size={
|
|
212
|
+
<PresenceAvatarTip key={i} user={u} size={14} />
|
|
270
213
|
))}
|
|
271
214
|
{presenceUsers.length > 4 && (
|
|
272
215
|
<span className="flex h-4 min-w-4 items-center justify-center rounded-full bg-muted px-1 text-[8px] font-medium leading-none text-muted-foreground ring-1 ring-black/40">
|
|
@@ -297,6 +240,14 @@ function SortableSlideThumb({
|
|
|
297
240
|
</div>
|
|
298
241
|
</button>
|
|
299
242
|
|
|
243
|
+
{/* Drag handle — overlaid on the thumbnail instead of holding its own
|
|
244
|
+
* column, so the rail stays narrow. Mirrors the action buttons opposite. */}
|
|
245
|
+
{!readOnly && (
|
|
246
|
+
<div {...attributes} {...listeners} className={THUMB_OVERLAY_CLASS}>
|
|
247
|
+
<IconGripVertical className={THUMB_OVERLAY_ICON_CLASS} />
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
|
|
300
251
|
{/* Actions - always visible on touch devices */}
|
|
301
252
|
{!readOnly && (
|
|
302
253
|
<div className="absolute top-2 right-2 flex gap-0.5 sm:opacity-0 sm:group-hover:opacity-100">
|
|
@@ -360,9 +311,8 @@ function GeneratingSlideSkeleton({
|
|
|
360
311
|
aria-current={selected ? "true" : undefined}
|
|
361
312
|
onClick={onSelect}
|
|
362
313
|
>
|
|
363
|
-
<div className="w-full flex items-start gap-
|
|
364
|
-
<
|
|
365
|
-
<span className="flex-shrink-0 w-5 mt-2 text-[10px] font-medium text-muted-foreground/70">
|
|
314
|
+
<div className="w-full flex items-start gap-1.5 p-1.5 rounded-lg bg-accent/30">
|
|
315
|
+
<span className="flex-shrink-0 w-4 text-center text-[10px] font-medium leading-5 text-muted-foreground/70">
|
|
366
316
|
{index + 1}
|
|
367
317
|
</span>
|
|
368
318
|
<div className="flex-1 min-w-0">
|
|
@@ -377,218 +327,13 @@ function GeneratingSlideSkeleton({
|
|
|
377
327
|
);
|
|
378
328
|
}
|
|
379
329
|
|
|
380
|
-
function AddSlidePopover({
|
|
381
|
-
open,
|
|
382
|
-
onOpenChange,
|
|
383
|
-
anchorRef,
|
|
384
|
-
deckId,
|
|
385
|
-
deckTitle,
|
|
386
|
-
activeSlideId,
|
|
387
|
-
slideCount,
|
|
388
|
-
activeSlideIndex,
|
|
389
|
-
agentSubmit,
|
|
390
|
-
onDuplicateCurrent,
|
|
391
|
-
onAddEmpty,
|
|
392
|
-
}: {
|
|
393
|
-
open: boolean;
|
|
394
|
-
onOpenChange: (open: boolean) => void;
|
|
395
|
-
anchorRef: React.RefObject<HTMLElement | null>;
|
|
396
|
-
deckId: string;
|
|
397
|
-
deckTitle: string;
|
|
398
|
-
activeSlideId: string;
|
|
399
|
-
slideCount: number;
|
|
400
|
-
activeSlideIndex: number;
|
|
401
|
-
agentSubmit: (message: string, context: string) => void;
|
|
402
|
-
onDuplicateCurrent?: () => void;
|
|
403
|
-
onAddEmpty?: () => void;
|
|
404
|
-
}) {
|
|
405
|
-
const t = useT();
|
|
406
|
-
const panelRef = useRef<HTMLDivElement>(null);
|
|
407
|
-
const [promptText, setPromptText] = useState("");
|
|
408
|
-
const [googleDocContext, setGoogleDocContext] = useState("");
|
|
409
|
-
|
|
410
|
-
useEffect(() => {
|
|
411
|
-
if (!open) return;
|
|
412
|
-
const handleClick = (e: MouseEvent) => {
|
|
413
|
-
if (isInsidePortaledLayer(e.target)) return;
|
|
414
|
-
if (
|
|
415
|
-
panelRef.current &&
|
|
416
|
-
!panelRef.current.contains(e.target as Node) &&
|
|
417
|
-
anchorRef.current &&
|
|
418
|
-
!anchorRef.current.contains(e.target as Node)
|
|
419
|
-
) {
|
|
420
|
-
onOpenChange(false);
|
|
421
|
-
}
|
|
422
|
-
};
|
|
423
|
-
const handleKey = (e: KeyboardEvent) => {
|
|
424
|
-
if (e.key === "Escape") onOpenChange(false);
|
|
425
|
-
};
|
|
426
|
-
document.addEventListener("mousedown", handleClick);
|
|
427
|
-
document.addEventListener("keydown", handleKey);
|
|
428
|
-
return () => {
|
|
429
|
-
document.removeEventListener("mousedown", handleClick);
|
|
430
|
-
document.removeEventListener("keydown", handleKey);
|
|
431
|
-
};
|
|
432
|
-
}, [open, onOpenChange, anchorRef]);
|
|
433
|
-
|
|
434
|
-
const handleSubmit = useCallback(
|
|
435
|
-
async (text: string, files: File[]) => {
|
|
436
|
-
let uploaded: UploadedFile[] = [];
|
|
437
|
-
if (files.length > 0) {
|
|
438
|
-
try {
|
|
439
|
-
const formData = new FormData();
|
|
440
|
-
files.forEach((f) => formData.append("files", f));
|
|
441
|
-
const res = await fetch(`${appBasePath()}/api/uploads`, {
|
|
442
|
-
method: "POST",
|
|
443
|
-
body: formData,
|
|
444
|
-
});
|
|
445
|
-
if (!res.ok) {
|
|
446
|
-
const data = await res.json().catch(() => null);
|
|
447
|
-
throw new Error(data?.error || t("editorSidebar.uploadFailed"));
|
|
448
|
-
}
|
|
449
|
-
uploaded = (await res.json()) as UploadedFile[];
|
|
450
|
-
} catch (error) {
|
|
451
|
-
toast.error(t("editorSidebar.uploadFailed"), {
|
|
452
|
-
description:
|
|
453
|
-
error instanceof Error
|
|
454
|
-
? error.message
|
|
455
|
-
: t("editorSidebar.uploadAttachedFileFailed"),
|
|
456
|
-
});
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const trimmedText = text.trim();
|
|
462
|
-
const googleDocSourceForContext =
|
|
463
|
-
truncateSourceForContext(googleDocContext);
|
|
464
|
-
const fileContext = describeUploadedFilesForAgent(uploaded, deckId);
|
|
465
|
-
const context = [
|
|
466
|
-
`Add a new slide to deck "${deckTitle}" (id: ${deckId}).`,
|
|
467
|
-
`Insert after slide ${activeSlideIndex + 1} of ${slideCount} (active slide id: ${activeSlideId}).`,
|
|
468
|
-
"The visible user message above contains the user's request and/or pasted source material for the new slide(s). Treat pasted memo content as source material even if the user did not explicitly say they are pasting it.",
|
|
469
|
-
googleDocSourceForContext.text,
|
|
470
|
-
googleDocSourceForContext.truncated
|
|
471
|
-
? `The pasted source was longer than ${MAX_SOURCE_CONTEXT_CHARS} characters, so only the first ${MAX_SOURCE_CONTEXT_CHARS} characters were included to keep the agent request reliable.`
|
|
472
|
-
: "",
|
|
473
|
-
fileContext,
|
|
474
|
-
"",
|
|
475
|
-
"Create the slide content and insert it at the correct position using `add-slide` with --deckId=" +
|
|
476
|
-
deckId +
|
|
477
|
-
".",
|
|
478
|
-
"Every slide is rendered into a fixed native canvas (default 16:9 is 960x540 CSS pixels). Keep each slide within the density limits in AGENTS.md; split dense source material across more slides instead of packing it tightly.",
|
|
479
|
-
"If the user asked for multiple slides, call `add-slide` once per slide. Use positions starting at " +
|
|
480
|
-
(activeSlideIndex + 1) +
|
|
481
|
-
" so the new slides land after the active slide in order.",
|
|
482
|
-
"For larger requests, keep adding slides sequentially: wait for each add-slide result, then call add-slide for the next slide. Start slide 1 immediately; do not wait to design the entire sequence before adding it.",
|
|
483
|
-
].join("\n");
|
|
484
|
-
|
|
485
|
-
agentSubmit(addSlideAgentMessage(trimmedText), context);
|
|
486
|
-
onOpenChange(false);
|
|
487
|
-
},
|
|
488
|
-
[
|
|
489
|
-
activeSlideId,
|
|
490
|
-
activeSlideIndex,
|
|
491
|
-
agentSubmit,
|
|
492
|
-
deckId,
|
|
493
|
-
deckTitle,
|
|
494
|
-
googleDocContext,
|
|
495
|
-
onOpenChange,
|
|
496
|
-
slideCount,
|
|
497
|
-
],
|
|
498
|
-
);
|
|
499
|
-
|
|
500
|
-
useEffect(() => {
|
|
501
|
-
if (!open) {
|
|
502
|
-
setPromptText("");
|
|
503
|
-
setGoogleDocContext("");
|
|
504
|
-
}
|
|
505
|
-
}, [open]);
|
|
506
|
-
|
|
507
|
-
if (!open || !anchorRef.current) return null;
|
|
508
|
-
|
|
509
|
-
const rect = anchorRef.current.getBoundingClientRect();
|
|
510
|
-
const panelWidth = Math.min(420, window.innerWidth - 24);
|
|
511
|
-
const left = Math.max(
|
|
512
|
-
12,
|
|
513
|
-
Math.min(rect.left, window.innerWidth - panelWidth - 12),
|
|
514
|
-
);
|
|
515
|
-
|
|
516
|
-
return createPortal(
|
|
517
|
-
<div
|
|
518
|
-
ref={panelRef}
|
|
519
|
-
className="fixed w-[min(420px,calc(100vw-24px))] rounded-xl border border-border bg-popover shadow-2xl shadow-black/60 z-[200] p-3"
|
|
520
|
-
style={{
|
|
521
|
-
top: rect.bottom + 8,
|
|
522
|
-
left,
|
|
523
|
-
}}
|
|
524
|
-
>
|
|
525
|
-
<p className="px-1 pb-2 text-sm font-medium text-foreground/90">
|
|
526
|
-
{t("editorSidebar.addSlides")}
|
|
527
|
-
</p>
|
|
528
|
-
{(onAddEmpty || (onDuplicateCurrent && slideCount > 0)) && (
|
|
529
|
-
<>
|
|
530
|
-
{onAddEmpty && (
|
|
531
|
-
<button
|
|
532
|
-
type="button"
|
|
533
|
-
onClick={() => {
|
|
534
|
-
onAddEmpty();
|
|
535
|
-
onOpenChange(false);
|
|
536
|
-
}}
|
|
537
|
-
className="w-full mb-1 px-2.5 py-2 text-left text-sm rounded-md hover:bg-accent transition-colors flex items-center gap-2 text-foreground/90 cursor-pointer"
|
|
538
|
-
>
|
|
539
|
-
<IconSquarePlus className="w-4 h-4 text-muted-foreground" />
|
|
540
|
-
<span>{t("editorSidebar.addEmptySlide")}</span>
|
|
541
|
-
<span className="ml-auto text-[11px] text-muted-foreground">
|
|
542
|
-
{t("editorSidebar.noAi")}
|
|
543
|
-
</span>
|
|
544
|
-
</button>
|
|
545
|
-
)}
|
|
546
|
-
{onDuplicateCurrent && slideCount > 0 && (
|
|
547
|
-
<button
|
|
548
|
-
type="button"
|
|
549
|
-
onClick={() => {
|
|
550
|
-
onDuplicateCurrent();
|
|
551
|
-
onOpenChange(false);
|
|
552
|
-
}}
|
|
553
|
-
className="w-full mb-2 px-2.5 py-2 text-left text-sm rounded-md hover:bg-accent transition-colors flex items-center gap-2 text-foreground/90 cursor-pointer"
|
|
554
|
-
>
|
|
555
|
-
<IconCopy className="w-4 h-4 text-muted-foreground" />
|
|
556
|
-
<span>{t("editorSidebar.duplicateCurrentSlide")}</span>
|
|
557
|
-
<span className="ml-auto text-[11px] text-muted-foreground">
|
|
558
|
-
{t("editorSidebar.noAi")}
|
|
559
|
-
</span>
|
|
560
|
-
</button>
|
|
561
|
-
)}
|
|
562
|
-
<div className="-mx-3 mb-2 h-px bg-border" />
|
|
563
|
-
</>
|
|
564
|
-
)}
|
|
565
|
-
<PromptComposer
|
|
566
|
-
autoFocus
|
|
567
|
-
placeholder={t("editorSidebar.promptPlaceholder")}
|
|
568
|
-
draftScope={`slides:add-slide:${deckId}`}
|
|
569
|
-
onSubmit={handleSubmit}
|
|
570
|
-
onTextChange={setPromptText}
|
|
571
|
-
/>
|
|
572
|
-
<div className="-mx-1 mt-2">
|
|
573
|
-
<GoogleDocImportHint
|
|
574
|
-
promptText={promptText}
|
|
575
|
-
onSourceContextChange={setGoogleDocContext}
|
|
576
|
-
/>
|
|
577
|
-
</div>
|
|
578
|
-
</div>,
|
|
579
|
-
document.body,
|
|
580
|
-
);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
330
|
export default function EditorSidebar({
|
|
584
331
|
slides,
|
|
585
332
|
activeSlideId,
|
|
586
333
|
deckId,
|
|
587
|
-
deckTitle,
|
|
588
334
|
onSelectSlide,
|
|
589
335
|
onDuplicateSlide,
|
|
590
336
|
onDeleteSlide,
|
|
591
|
-
onAddEmptySlide,
|
|
592
337
|
readOnly = false,
|
|
593
338
|
slidePresence,
|
|
594
339
|
recentEdits,
|
|
@@ -596,13 +341,7 @@ export default function EditorSidebar({
|
|
|
596
341
|
generatingSlide,
|
|
597
342
|
generatingSlideSelected = false,
|
|
598
343
|
onSelectGeneratingSlide,
|
|
599
|
-
addSlideGenerating = false,
|
|
600
|
-
onAddSlideGeneratingChange,
|
|
601
344
|
}: EditorSidebarProps) {
|
|
602
|
-
const t = useT();
|
|
603
|
-
const activeIndex = slides.findIndex((s) => s.id === activeSlideId);
|
|
604
|
-
const [addOpen, setAddOpen] = useState(false);
|
|
605
|
-
const headerAddRef = useRef<HTMLButtonElement>(null);
|
|
606
345
|
const slideButtonRefs = useRef(new Map<string, HTMLButtonElement>());
|
|
607
346
|
const thumbScrollRef = useRef<HTMLDivElement>(null);
|
|
608
347
|
const measurementsRef = useRef(
|
|
@@ -698,8 +437,6 @@ export default function EditorSidebar({
|
|
|
698
437
|
},
|
|
699
438
|
[],
|
|
700
439
|
);
|
|
701
|
-
const { generating, submit: agentSubmit } = useAgentGenerating();
|
|
702
|
-
|
|
703
440
|
const registerSlideButton = useCallback(
|
|
704
441
|
(slideId: string, node: HTMLButtonElement | null) => {
|
|
705
442
|
if (node) {
|
|
@@ -711,11 +448,6 @@ export default function EditorSidebar({
|
|
|
711
448
|
[],
|
|
712
449
|
);
|
|
713
450
|
|
|
714
|
-
// Reset addSlideGenerating when global generating stops
|
|
715
|
-
useEffect(() => {
|
|
716
|
-
if (!generating) onAddSlideGeneratingChange?.(false);
|
|
717
|
-
}, [generating, onAddSlideGeneratingChange]);
|
|
718
|
-
|
|
719
451
|
// Arrow key navigation for slides
|
|
720
452
|
useEffect(() => {
|
|
721
453
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
@@ -754,30 +486,7 @@ export default function EditorSidebar({
|
|
|
754
486
|
}, [slides, activeSlideId, onSelectSlide]);
|
|
755
487
|
|
|
756
488
|
return (
|
|
757
|
-
<div className="flex h-full min-h-0 w-
|
|
758
|
-
<div className="flex items-center justify-between border-b border-border/70 p-3">
|
|
759
|
-
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
760
|
-
{t("editorSidebar.slides")}
|
|
761
|
-
</span>
|
|
762
|
-
{readOnly ? null : addSlideGenerating ? (
|
|
763
|
-
<IconLoader2 className="w-4 h-4 text-muted-foreground animate-spin" />
|
|
764
|
-
) : (
|
|
765
|
-
<Tooltip>
|
|
766
|
-
<TooltipTrigger asChild>
|
|
767
|
-
<button
|
|
768
|
-
ref={headerAddRef}
|
|
769
|
-
onClick={() => setAddOpen(!addOpen)}
|
|
770
|
-
className="p-2 rounded-md hover:bg-accent transition-colors"
|
|
771
|
-
aria-label={t("editorSidebar.addSlides")}
|
|
772
|
-
>
|
|
773
|
-
<IconPlus className="w-4 h-4 text-muted-foreground" />
|
|
774
|
-
</button>
|
|
775
|
-
</TooltipTrigger>
|
|
776
|
-
<TooltipContent>{t("editorSidebar.addSlides")}</TooltipContent>
|
|
777
|
-
</Tooltip>
|
|
778
|
-
)}
|
|
779
|
-
</div>
|
|
780
|
-
|
|
489
|
+
<div className="flex h-full min-h-0 w-48 flex-shrink-0 flex-col border-r border-border/70 bg-background/95 sm:w-52">
|
|
781
490
|
<div
|
|
782
491
|
ref={thumbScrollRef}
|
|
783
492
|
className="relative min-h-0 flex-1 space-y-1 overflow-y-auto overscroll-contain p-2"
|
|
@@ -824,27 +533,6 @@ export default function EditorSidebar({
|
|
|
824
533
|
/>
|
|
825
534
|
)}
|
|
826
535
|
</div>
|
|
827
|
-
|
|
828
|
-
{!readOnly && (
|
|
829
|
-
<AddSlidePopover
|
|
830
|
-
open={addOpen}
|
|
831
|
-
onOpenChange={setAddOpen}
|
|
832
|
-
anchorRef={headerAddRef}
|
|
833
|
-
deckId={deckId}
|
|
834
|
-
deckTitle={deckTitle}
|
|
835
|
-
activeSlideId={activeSlideId}
|
|
836
|
-
slideCount={slides.length}
|
|
837
|
-
activeSlideIndex={activeIndex >= 0 ? activeIndex : 0}
|
|
838
|
-
agentSubmit={(msg, ctx) => {
|
|
839
|
-
onAddSlideGeneratingChange?.(true);
|
|
840
|
-
agentSubmit(msg, ctx);
|
|
841
|
-
}}
|
|
842
|
-
onDuplicateCurrent={
|
|
843
|
-
activeSlideId ? () => onDuplicateSlide(activeSlideId) : undefined
|
|
844
|
-
}
|
|
845
|
-
onAddEmpty={onAddEmptySlide}
|
|
846
|
-
/>
|
|
847
|
-
)}
|
|
848
536
|
</div>
|
|
849
537
|
);
|
|
850
538
|
}
|