@lemonriver/core 0.1.0 → 0.2.0
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/LICENSE +21 -0
- package/dist/{build-f-fM2ZY4.js → build-Df_xwIpg.js} +1 -1
- package/dist/cli/bin.js +4 -4
- package/dist/{config-DO45GU3e.js → config-BjWCLoMQ.js} +2343 -363
- package/dist/{config-B6JaKJM4.d.ts → config-rMGCkz6i.d.ts} +6 -2
- package/dist/{dev-CqS2xpgE.js → dev-Ctsx9IeO.js} +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -3
- package/dist/locale/index.d.ts +5 -2
- package/dist/locale/index.js +2 -2
- package/dist/{preview-BihgMlEl.js → preview-CJgcqsPk.js} +1 -1
- package/dist/{types-BjW4glip.d.ts → types-8VUyA2bu.d.ts} +133 -5
- package/dist/{update-C_R0ycVC.js → update-BKOA3Z6o.js} +153 -21
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +1 -1
- package/dist/{zh-tw-Ci0jK8eg.js → zh-tw-CeLrYwRE.js} +1178 -25
- package/package.json +21 -15
- package/skills/apply-comments/SKILL.md +10 -6
- package/skills/create-slide/SKILL.md +2 -0
- package/skills/current-slide/SKILL.md +1 -0
- package/skills/slide-authoring/SKILL.md +31 -1
- package/src/agent/prompt.ts +141 -0
- package/src/agent/provider.ts +123 -0
- package/src/agent/types.ts +151 -0
- package/src/app/app.tsx +2 -0
- package/src/app/assets/open-slide.svg +39 -0
- package/src/app/components/agent/agent-dock.tsx +35 -0
- package/src/app/components/agent/agent-markdown.tsx +45 -0
- package/src/app/components/agent/agent-panel.tsx +692 -0
- package/src/app/components/agent/agent-presence.tsx +74 -0
- package/src/app/components/agent/composer.tsx +120 -0
- package/src/app/components/agent/connect-view.tsx +184 -0
- package/src/app/components/agent/history-view.tsx +135 -0
- package/src/app/components/agent/provider-logo.tsx +34 -0
- package/src/app/components/agent/transcript.tsx +609 -0
- package/src/app/components/board/board-canvas.tsx +143 -12
- package/src/app/components/board/board.css +51 -0
- package/src/app/components/board/layout.test.ts +53 -1
- package/src/app/components/board/layout.ts +50 -2
- package/src/app/components/board/page-node.tsx +287 -13
- package/src/app/components/board/use-board-cards.ts +210 -0
- package/src/app/components/board/use-page-order.ts +17 -9
- package/src/app/components/command/slide-command-menu.tsx +10 -0
- package/src/app/components/icon-switcher-indicator.tsx +1 -1
- package/src/app/components/inspector/comment-layer.tsx +1011 -0
- package/src/app/components/inspector/image-crop-dialog.tsx +1 -5
- package/src/app/components/inspector/inspect-overlay.tsx +2 -39
- package/src/app/components/inspector/inspector-panel.tsx +10 -93
- package/src/app/components/inspector/inspector-provider.tsx +121 -47
- package/src/app/components/panel/editor-sidebar.tsx +85 -16
- package/src/app/components/panel/panel-shell.tsx +2 -2
- package/src/app/components/panel/resize-handle.tsx +129 -0
- package/src/app/components/player.tsx +0 -1
- package/src/app/components/preview-step-host.tsx +16 -0
- package/src/app/components/sidebar/sidebar.tsx +2 -2
- package/src/app/components/style-panel/style-panel.tsx +3 -2
- package/src/app/components/thumbnail-rail.tsx +76 -1
- package/src/app/favicon.ico +0 -0
- package/src/app/favicon.svg +39 -0
- package/src/app/index.html +2 -1
- package/src/app/lib/agent/use-agent.ts +543 -0
- package/src/app/lib/inspector/freeze-motion.ts +61 -0
- package/src/app/lib/inspector/use-comments.ts +32 -3
- package/src/app/lib/locale-store.ts +6 -1
- package/src/app/lib/print-ready.ts +9 -6
- package/src/app/lib/use-auto-grow.ts +15 -0
- package/src/app/lib/utils.ts +4 -0
- package/src/app/routes/board.tsx +48 -11
- package/src/app/routes/home.tsx +43 -7
- package/src/app/routes/presenter.tsx +1 -16
- package/src/app/routes/preview.tsx +169 -0
- package/src/app/routes/slide.tsx +221 -173
- package/src/app/styles.css +234 -80
- package/src/editing/placeholder-id.ts +1 -0
- package/src/locale/en.ts +127 -5
- package/src/locale/es.ts +692 -0
- package/src/locale/index.ts +1 -0
- package/src/locale/ja.ts +120 -6
- package/src/locale/types.ts +132 -5
- package/src/locale/zh-cn.ts +117 -5
- package/src/locale/zh-tw.ts +117 -5
- package/src/app/assets/open-slide.png +0 -0
- package/src/app/components/inspector/comment-widget.tsx +0 -119
package/src/app/routes/slide.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
MoreHorizontal,
|
|
15
15
|
Play,
|
|
16
16
|
Presentation,
|
|
17
|
+
Sparkles,
|
|
17
18
|
Terminal,
|
|
18
19
|
} from 'lucide-react';
|
|
19
20
|
import {
|
|
@@ -21,15 +22,19 @@ import {
|
|
|
21
22
|
type RefObject,
|
|
22
23
|
useCallback,
|
|
23
24
|
useEffect,
|
|
25
|
+
useLayoutEffect,
|
|
24
26
|
useMemo,
|
|
25
27
|
useRef,
|
|
26
28
|
useState,
|
|
27
29
|
} from 'react';
|
|
28
30
|
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
|
29
31
|
import { toast } from 'sonner';
|
|
32
|
+
import { AgentToggleButton } from '@/components/agent/agent-dock';
|
|
33
|
+
import { AgentPanel } from '@/components/agent/agent-panel';
|
|
34
|
+
import { AgentPresence } from '@/components/agent/agent-presence';
|
|
30
35
|
import { AssetView } from '@/components/asset-view';
|
|
31
36
|
import { HistoryProvider } from '@/components/history-provider';
|
|
32
|
-
import {
|
|
37
|
+
import { CommentLayer } from '@/components/inspector/comment-layer';
|
|
33
38
|
import { InlineEditLayer } from '@/components/inspector/inline-text-editor';
|
|
34
39
|
import { InspectOverlay } from '@/components/inspector/inspect-overlay';
|
|
35
40
|
import {
|
|
@@ -40,19 +45,25 @@ import {
|
|
|
40
45
|
} from '@/components/inspector/inspector-provider';
|
|
41
46
|
import { SaveBar } from '@/components/inspector/save-bar';
|
|
42
47
|
import { EditorSidebar } from '@/components/panel/editor-sidebar';
|
|
48
|
+
import { ResizeHandle, useStoredWidth } from '@/components/panel/resize-handle';
|
|
43
49
|
import { DesignProvider } from '@/components/style-panel/design-provider';
|
|
44
50
|
import { DesignToggleButton } from '@/components/style-panel/style-panel';
|
|
45
51
|
import { Button, buttonVariants } from '@/components/ui/button';
|
|
46
52
|
import {
|
|
47
53
|
DropdownMenu,
|
|
54
|
+
DropdownMenuCheckboxItem,
|
|
48
55
|
DropdownMenuContent,
|
|
56
|
+
DropdownMenuGroup,
|
|
49
57
|
DropdownMenuItem,
|
|
58
|
+
DropdownMenuRadioGroup,
|
|
59
|
+
DropdownMenuRadioItem,
|
|
50
60
|
DropdownMenuSeparator,
|
|
51
61
|
DropdownMenuShortcut,
|
|
52
62
|
DropdownMenuTrigger,
|
|
53
63
|
} from '@/components/ui/dropdown-menu';
|
|
54
64
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
55
65
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
66
|
+
import { setDockOpen, useAgentActivity, useAgentDock } from '@/lib/agent/use-agent';
|
|
56
67
|
import { useFolders } from '@/lib/folders';
|
|
57
68
|
import {
|
|
58
69
|
hasModifier,
|
|
@@ -67,8 +78,10 @@ import { useClickPageNavigation } from '@/lib/use-click-page-navigation';
|
|
|
67
78
|
import { useDocumentTitle } from '@/lib/use-document-title';
|
|
68
79
|
import { useIsMobile } from '@/lib/use-is-mobile';
|
|
69
80
|
import { format, useLocale } from '@/lib/use-locale';
|
|
81
|
+
import { useMediaQuery } from '@/lib/use-media-query';
|
|
70
82
|
import { useWheelPageNavigation } from '@/lib/use-wheel-page-navigation';
|
|
71
83
|
import { cn } from '@/lib/utils';
|
|
84
|
+
import type { AgentTurnContext } from '../../agent/types';
|
|
72
85
|
import { SlideCommandMenu } from '../components/command/slide-command-menu';
|
|
73
86
|
import { PdfProgressToast, PptxProgressToast } from '../components/export-progress-toast';
|
|
74
87
|
import { NotesDrawer } from '../components/notes-drawer';
|
|
@@ -127,6 +140,13 @@ export function Slide() {
|
|
|
127
140
|
const [designOpen, setDesignOpen] = useState(false);
|
|
128
141
|
const [overviewOpen, setOverviewOpen] = useState(false);
|
|
129
142
|
const [commandOpen, setCommandOpen] = useState(false);
|
|
143
|
+
const { open: agentOpen, toggle: toggleAgent } = useAgentDock();
|
|
144
|
+
useLayoutEffect(() => {
|
|
145
|
+
if (agentOpen) setDesignOpen(false);
|
|
146
|
+
}, [agentOpen]);
|
|
147
|
+
useLayoutEffect(() => {
|
|
148
|
+
if (designOpen) setDockOpen(false);
|
|
149
|
+
}, [designOpen]);
|
|
130
150
|
const [, setWarmedTick] = useState(0);
|
|
131
151
|
const handleAssetsWarmed = useCallback(() => {
|
|
132
152
|
markDeckWarmed(slideId);
|
|
@@ -142,6 +162,8 @@ export function Slide() {
|
|
|
142
162
|
const slideViewportRef = useRef<HTMLElement>(null);
|
|
143
163
|
const t = useLocale();
|
|
144
164
|
const isMobile = useIsMobile();
|
|
165
|
+
const compactToolbar = useMediaQuery('(max-width: 1023.98px)');
|
|
166
|
+
const narrowToolbar = useMediaQuery('(max-width: 639.98px)');
|
|
145
167
|
const prefersReducedMotion = usePrefersReducedMotion();
|
|
146
168
|
|
|
147
169
|
const modulePages = useMemo(() => slide?.default ?? [], [slide]);
|
|
@@ -188,6 +210,16 @@ export function Slide() {
|
|
|
188
210
|
[pageCount, setSearchParams],
|
|
189
211
|
);
|
|
190
212
|
|
|
213
|
+
// While an agent writes this deck page by page, jump to each page as it
|
|
214
|
+
// lands so the user can follow along.
|
|
215
|
+
const agentActivity = useAgentActivity(slideId);
|
|
216
|
+
const agentWriting = agentActivity !== null;
|
|
217
|
+
const seenPageCount = useRef(pageCount);
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (agentWriting && pageCount > seenPageCount.current) goTo(pageCount - 1);
|
|
220
|
+
seenPageCount.current = pageCount;
|
|
221
|
+
}, [agentWriting, pageCount, goTo]);
|
|
222
|
+
|
|
191
223
|
const reorderPage = useCallback(
|
|
192
224
|
async (from: number, to: number) => {
|
|
193
225
|
if (from === to) return;
|
|
@@ -349,11 +381,13 @@ export function Slide() {
|
|
|
349
381
|
setPlayMode('window');
|
|
350
382
|
} else if (import.meta.env.DEV && (e.key === 'd' || e.key === 'D')) {
|
|
351
383
|
setDesignOpen((v) => !v);
|
|
384
|
+
} else if (import.meta.env.DEV && (e.key === 'a' || e.key === 'A')) {
|
|
385
|
+
toggleAgent();
|
|
352
386
|
}
|
|
353
387
|
};
|
|
354
388
|
window.addEventListener('keydown', onKey);
|
|
355
389
|
return () => window.removeEventListener('keydown', onKey);
|
|
356
|
-
}, [index, goTo, playMode, slideId, overviewOpen, navigate]);
|
|
390
|
+
}, [index, goTo, playMode, slideId, overviewOpen, navigate, toggleAgent]);
|
|
357
391
|
|
|
358
392
|
if (error) {
|
|
359
393
|
return (
|
|
@@ -591,7 +625,7 @@ export function Slide() {
|
|
|
591
625
|
};
|
|
592
626
|
|
|
593
627
|
const exportMenuItems = (
|
|
594
|
-
|
|
628
|
+
<DropdownMenuGroup>
|
|
595
629
|
<DropdownMenuItem disabled={exporting} onClick={exportHtml}>
|
|
596
630
|
<FileCode2 />
|
|
597
631
|
{t.slide.exportAsHtml}
|
|
@@ -609,7 +643,7 @@ export function Slide() {
|
|
|
609
643
|
<FileImage />
|
|
610
644
|
{t.slide.exportAsImagePptx}
|
|
611
645
|
</DropdownMenuItem>
|
|
612
|
-
|
|
646
|
+
</DropdownMenuGroup>
|
|
613
647
|
);
|
|
614
648
|
|
|
615
649
|
return (
|
|
@@ -617,14 +651,16 @@ export function Slide() {
|
|
|
617
651
|
<InspectorProvider
|
|
618
652
|
slideId={slideId}
|
|
619
653
|
pageIndex={index}
|
|
620
|
-
panelHidden={designOpen}
|
|
621
|
-
onPanelOpen={() =>
|
|
654
|
+
panelHidden={designOpen || agentOpen}
|
|
655
|
+
onPanelOpen={() => {
|
|
656
|
+
setDesignOpen(false);
|
|
657
|
+
setDockOpen(false);
|
|
658
|
+
}}
|
|
622
659
|
>
|
|
623
660
|
<SelectionReporter />
|
|
624
661
|
<div className="flex h-dvh flex-col overflow-hidden bg-sidebar text-foreground">
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
<div className="flex flex-1 items-center gap-1.5 md:flex-none md:gap-2">
|
|
662
|
+
<header className="grid min-h-12 shrink-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-2 gap-y-1 px-2 py-2 md:px-3">
|
|
663
|
+
<div className="col-start-1 row-start-1 flex min-w-0 items-center gap-1.5 md:gap-2">
|
|
628
664
|
{showSlideBrowser && (
|
|
629
665
|
<button
|
|
630
666
|
type="button"
|
|
@@ -661,29 +697,23 @@ export function Slide() {
|
|
|
661
697
|
{import.meta.env.DEV && <AgentConnectedBadge />}
|
|
662
698
|
</div>
|
|
663
699
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
and min-w-0 lets it truncate instead of overlapping the icons on narrow widths. */}
|
|
667
|
-
<div className="pointer-events-none relative flex min-w-0 justify-center px-2 md:absolute md:inset-x-0">
|
|
668
|
-
<div className="pointer-events-auto min-w-0 max-w-[34rem]">
|
|
700
|
+
<div className="col-span-3 row-start-2 flex min-w-0 justify-center sm:col-span-1 sm:col-start-2 sm:row-start-1">
|
|
701
|
+
<div className="min-w-0 max-w-[34rem]">
|
|
669
702
|
<InlineTitleEditor title={title} onSubmit={(next) => renameSlide(slideId, next)} />
|
|
670
703
|
</div>
|
|
671
704
|
</div>
|
|
672
705
|
|
|
673
|
-
<div className="
|
|
674
|
-
{view === 'slides' && <InspectModeSwitcher />}
|
|
675
|
-
{import.meta.env.DEV && view === 'slides' && (
|
|
706
|
+
<div className="col-start-3 row-start-1 flex items-center justify-end gap-1">
|
|
707
|
+
{view === 'slides' && !narrowToolbar && <InspectModeSwitcher />}
|
|
708
|
+
{import.meta.env.DEV && view === 'slides' && !compactToolbar && (
|
|
676
709
|
<span aria-hidden className="mx-0.5 hidden h-5 w-px bg-hairline md:block" />
|
|
677
710
|
)}
|
|
678
|
-
{view === 'slides' && (
|
|
711
|
+
{view === 'slides' && !compactToolbar && (
|
|
679
712
|
<button
|
|
680
713
|
type="button"
|
|
681
714
|
aria-label={t.slide.copyLink}
|
|
682
715
|
title={t.slide.copyLink}
|
|
683
|
-
className={
|
|
684
|
-
buttonVariants({ variant: 'ghost', size: 'icon-sm' }),
|
|
685
|
-
'hidden md:inline-flex',
|
|
686
|
-
)}
|
|
716
|
+
className={buttonVariants({ variant: 'ghost', size: 'icon-sm' })}
|
|
687
717
|
onClick={copyLink}
|
|
688
718
|
>
|
|
689
719
|
<span className="relative grid size-4 place-items-center">
|
|
@@ -702,17 +732,14 @@ export function Slide() {
|
|
|
702
732
|
</span>
|
|
703
733
|
</button>
|
|
704
734
|
)}
|
|
705
|
-
{view === 'slides' && allowHtmlDownload && (
|
|
735
|
+
{view === 'slides' && allowHtmlDownload && !compactToolbar && (
|
|
706
736
|
<DropdownMenu>
|
|
707
737
|
<DropdownMenuTrigger
|
|
708
738
|
type="button"
|
|
709
739
|
disabled={exporting}
|
|
710
740
|
aria-label={t.slide.download}
|
|
711
741
|
title={t.slide.download}
|
|
712
|
-
className={
|
|
713
|
-
buttonVariants({ variant: 'ghost', size: 'icon-sm' }),
|
|
714
|
-
'hidden md:inline-flex',
|
|
715
|
-
)}
|
|
742
|
+
className={buttonVariants({ variant: 'ghost', size: 'icon-sm' })}
|
|
716
743
|
>
|
|
717
744
|
{exporting ? (
|
|
718
745
|
<Loader2 className="size-4 animate-spin motion-reduce:animate-none" />
|
|
@@ -725,17 +752,13 @@ export function Slide() {
|
|
|
725
752
|
</DropdownMenuContent>
|
|
726
753
|
</DropdownMenu>
|
|
727
754
|
)}
|
|
728
|
-
{view === 'slides' && (
|
|
729
|
-
<DropdownMenu>
|
|
755
|
+
{view === 'slides' && compactToolbar && (
|
|
756
|
+
<DropdownMenu key={narrowToolbar ? 'narrow' : 'compact'}>
|
|
730
757
|
<DropdownMenuTrigger
|
|
731
758
|
type="button"
|
|
732
|
-
disabled={exporting}
|
|
733
759
|
aria-label={t.slide.moreActions}
|
|
734
760
|
title={t.slide.moreActions}
|
|
735
|
-
className={
|
|
736
|
-
buttonVariants({ variant: 'ghost', size: 'icon-sm' }),
|
|
737
|
-
'inline-flex md:hidden',
|
|
738
|
-
)}
|
|
761
|
+
className={buttonVariants({ variant: 'ghost', size: 'icon-sm' })}
|
|
739
762
|
>
|
|
740
763
|
{exporting ? (
|
|
741
764
|
<Loader2 className="size-4 animate-spin motion-reduce:animate-none" />
|
|
@@ -744,34 +767,50 @@ export function Slide() {
|
|
|
744
767
|
)}
|
|
745
768
|
</DropdownMenuTrigger>
|
|
746
769
|
<DropdownMenuContent align="end" className="min-w-[200px]">
|
|
747
|
-
|
|
748
|
-
<
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
770
|
+
{narrowToolbar && import.meta.env.DEV && (
|
|
771
|
+
<EditorMenuItems
|
|
772
|
+
designOpen={designOpen}
|
|
773
|
+
onDesignToggle={() => setDesignOpen((v) => !v)}
|
|
774
|
+
/>
|
|
775
|
+
)}
|
|
776
|
+
<DropdownMenuGroup>
|
|
777
|
+
{narrowToolbar && import.meta.env.DEV && (
|
|
778
|
+
<DropdownMenuItem onClick={toggleAgent}>
|
|
779
|
+
<Sparkles />
|
|
780
|
+
{t.agent.openTitle}
|
|
781
|
+
</DropdownMenuItem>
|
|
782
|
+
)}
|
|
783
|
+
<DropdownMenuItem onClick={() => setCommandOpen(true)}>
|
|
784
|
+
<Terminal />
|
|
785
|
+
{t.commandMenu.trigger}
|
|
786
|
+
</DropdownMenuItem>
|
|
787
|
+
<DropdownMenuItem onClick={copyLink}>
|
|
788
|
+
<Link2 />
|
|
789
|
+
{t.slide.copyLink}
|
|
790
|
+
</DropdownMenuItem>
|
|
791
|
+
</DropdownMenuGroup>
|
|
755
792
|
{allowHtmlDownload && <DropdownMenuSeparator />}
|
|
756
793
|
{allowHtmlDownload && exportMenuItems}
|
|
757
794
|
</DropdownMenuContent>
|
|
758
795
|
</DropdownMenu>
|
|
759
796
|
)}
|
|
760
|
-
{view === 'slides' &&
|
|
797
|
+
{import.meta.env.DEV && view === 'slides' && !narrowToolbar && <AgentToggleButton />}
|
|
798
|
+
{view === 'slides' && !narrowToolbar && (
|
|
761
799
|
<DesignToggleButton active={designOpen} onToggle={() => setDesignOpen((v) => !v)} />
|
|
762
800
|
)}
|
|
763
|
-
{view === 'slides' && <InspectPanelButton />}
|
|
801
|
+
{view === 'slides' && !narrowToolbar && <InspectPanelButton />}
|
|
764
802
|
<span aria-hidden className="mx-0.5 hidden h-5 w-px bg-hairline md:block" />
|
|
765
803
|
{view === 'slides' && (
|
|
766
804
|
<div className="inline-flex items-stretch">
|
|
767
805
|
<Button
|
|
768
806
|
size="sm"
|
|
769
807
|
variant="brand"
|
|
808
|
+
aria-label={t.slide.present}
|
|
770
809
|
onClick={() => setPlayMode(isMobile ? 'window' : 'fullscreen')}
|
|
771
|
-
className="px-2.5
|
|
810
|
+
className="rounded-r-none px-2.5 lg:px-3"
|
|
772
811
|
>
|
|
773
812
|
<Play className="size-3.5 fill-current" />
|
|
774
|
-
<span className="hidden
|
|
813
|
+
<span className="hidden lg:inline">{t.slide.present}</span>
|
|
775
814
|
</Button>
|
|
776
815
|
<DropdownMenu>
|
|
777
816
|
<DropdownMenuTrigger
|
|
@@ -780,32 +819,34 @@ export function Slide() {
|
|
|
780
819
|
title={t.slide.presentMenuAria}
|
|
781
820
|
className={cn(
|
|
782
821
|
buttonVariants({ variant: 'brand', size: 'sm' }),
|
|
783
|
-
'
|
|
822
|
+
'rounded-l-none px-1.5 shadow-[inset_1px_0_0_oklch(0_0_0/0.12),inset_0_1px_0_oklch(1_0_0/0.18),0_1px_0_oklch(0_0_0/0.16)]',
|
|
784
823
|
)}
|
|
785
824
|
>
|
|
786
825
|
<ChevronDown className="size-3.5" />
|
|
787
826
|
</DropdownMenuTrigger>
|
|
788
827
|
<DropdownMenuContent align="end" className="min-w-[200px]">
|
|
789
|
-
<
|
|
790
|
-
<
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
<
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
828
|
+
<DropdownMenuGroup>
|
|
829
|
+
<DropdownMenuItem onClick={() => setPlayMode('window')}>
|
|
830
|
+
<Play />
|
|
831
|
+
{t.slide.presentInWindow}
|
|
832
|
+
<DropdownMenuShortcut>↵</DropdownMenuShortcut>
|
|
833
|
+
</DropdownMenuItem>
|
|
834
|
+
<DropdownMenuItem onClick={() => setPlayMode('fullscreen')}>
|
|
835
|
+
<Maximize />
|
|
836
|
+
{t.slide.presentFullscreen}
|
|
837
|
+
<DropdownMenuShortcut>F</DropdownMenuShortcut>
|
|
838
|
+
</DropdownMenuItem>
|
|
839
|
+
<DropdownMenuItem
|
|
840
|
+
onClick={() => {
|
|
841
|
+
if (slideId) openPresenterWindow(slideId);
|
|
842
|
+
setPlayMode('window');
|
|
843
|
+
}}
|
|
844
|
+
>
|
|
845
|
+
<MonitorSpeaker />
|
|
846
|
+
{t.slide.presentPresenter}
|
|
847
|
+
<DropdownMenuShortcut>P</DropdownMenuShortcut>
|
|
848
|
+
</DropdownMenuItem>
|
|
849
|
+
</DropdownMenuGroup>
|
|
809
850
|
</DropdownMenuContent>
|
|
810
851
|
</DropdownMenu>
|
|
811
852
|
</div>
|
|
@@ -840,6 +881,7 @@ export function Slide() {
|
|
|
840
881
|
ref={slideViewportRef}
|
|
841
882
|
data-inspector-root
|
|
842
883
|
data-slide-id={slideId}
|
|
884
|
+
data-agent-provider={agentActivity?.provider}
|
|
843
885
|
className="relative min-h-0 min-w-0 flex-1 bg-background p-2 md:mx-2 md:mb-2 md:rounded-[10px] md:p-10 md:shadow-edge md:ring-1 md:ring-foreground/[0.06]"
|
|
844
886
|
>
|
|
845
887
|
<SlideViewportNavigation
|
|
@@ -861,7 +903,8 @@ export function Slide() {
|
|
|
861
903
|
<InspectOverlay />
|
|
862
904
|
<InlineEditLayer />
|
|
863
905
|
<SaveBar />
|
|
864
|
-
{import.meta.env.DEV && <
|
|
906
|
+
{import.meta.env.DEV && <CommentLayer />}
|
|
907
|
+
{agentActivity && <AgentPresence activity={agentActivity} revision={pages} />}
|
|
865
908
|
</main>
|
|
866
909
|
{/* Mobile-only horizontal rail. Sits below the canvas and
|
|
867
910
|
pads its bottom for the iOS home indicator / Safari URL bar. */}
|
|
@@ -881,6 +924,17 @@ export function Slide() {
|
|
|
881
924
|
<EditorSidebar
|
|
882
925
|
designOpen={designOpen}
|
|
883
926
|
onCloseDesign={() => setDesignOpen(false)}
|
|
927
|
+
agentOpen={agentOpen}
|
|
928
|
+
agent={
|
|
929
|
+
import.meta.env.DEV && (
|
|
930
|
+
<SlideAgentDock
|
|
931
|
+
slideId={slideId}
|
|
932
|
+
slideTitle={title}
|
|
933
|
+
pageNumber={index + 1}
|
|
934
|
+
totalPages={pageCount}
|
|
935
|
+
/>
|
|
936
|
+
)
|
|
937
|
+
}
|
|
884
938
|
/>
|
|
885
939
|
</div>
|
|
886
940
|
{import.meta.env.DEV && (
|
|
@@ -922,6 +976,7 @@ export function Slide() {
|
|
|
922
976
|
onCopyLink: copyLink,
|
|
923
977
|
onOverview: () => setOverviewOpen(true),
|
|
924
978
|
onToggleDesignPanel: () => setDesignOpen((v) => !v),
|
|
979
|
+
onToggleAgentPanel: toggleAgent,
|
|
925
980
|
onExportHtml: exportHtml,
|
|
926
981
|
onExportPdf: exportPdf,
|
|
927
982
|
onExportPptx: exportPptx,
|
|
@@ -941,14 +996,6 @@ const DEFAULT_RAIL_WIDTH = 264;
|
|
|
941
996
|
const MIN_RAIL_WIDTH = 200;
|
|
942
997
|
const MAX_RAIL_WIDTH = 480;
|
|
943
998
|
|
|
944
|
-
function readStoredRailWidth(): number {
|
|
945
|
-
if (typeof window === 'undefined') return DEFAULT_RAIL_WIDTH;
|
|
946
|
-
const raw = window.localStorage.getItem(RAIL_WIDTH_STORAGE_KEY);
|
|
947
|
-
const parsed = raw == null ? Number.NaN : Number(raw);
|
|
948
|
-
if (!Number.isFinite(parsed)) return DEFAULT_RAIL_WIDTH;
|
|
949
|
-
return Math.min(MAX_RAIL_WIDTH, Math.max(MIN_RAIL_WIDTH, parsed));
|
|
950
|
-
}
|
|
951
|
-
|
|
952
999
|
function ResizableRail(props: {
|
|
953
1000
|
pages: SlideModule['default'];
|
|
954
1001
|
design?: SlideModule['design'];
|
|
@@ -961,107 +1008,104 @@ function ResizableRail(props: {
|
|
|
961
1008
|
onBoard?: () => void;
|
|
962
1009
|
}) {
|
|
963
1010
|
const t = useLocale();
|
|
964
|
-
const [width, setWidth] =
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
window.localStorage.setItem(RAIL_WIDTH_STORAGE_KEY, String(width));
|
|
971
|
-
}, [width]);
|
|
972
|
-
|
|
973
|
-
useEffect(() => {
|
|
974
|
-
if (!resizing) return;
|
|
975
|
-
const prev = {
|
|
976
|
-
cursor: document.body.style.cursor,
|
|
977
|
-
userSelect: document.body.style.userSelect,
|
|
978
|
-
};
|
|
979
|
-
document.body.style.cursor = 'col-resize';
|
|
980
|
-
document.body.style.userSelect = 'none';
|
|
981
|
-
return () => {
|
|
982
|
-
document.body.style.cursor = prev.cursor;
|
|
983
|
-
document.body.style.userSelect = prev.userSelect;
|
|
984
|
-
};
|
|
985
|
-
}, [resizing]);
|
|
986
|
-
|
|
987
|
-
const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
988
|
-
e.preventDefault();
|
|
989
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
990
|
-
dragRef.current = { startX: e.clientX, startWidth: width };
|
|
991
|
-
setResizing(true);
|
|
992
|
-
};
|
|
993
|
-
|
|
994
|
-
const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
995
|
-
if (!dragRef.current) return;
|
|
996
|
-
const delta = e.clientX - dragRef.current.startX;
|
|
997
|
-
const next = Math.min(
|
|
998
|
-
MAX_RAIL_WIDTH,
|
|
999
|
-
Math.max(MIN_RAIL_WIDTH, dragRef.current.startWidth + delta),
|
|
1000
|
-
);
|
|
1001
|
-
setWidth(next);
|
|
1002
|
-
};
|
|
1003
|
-
|
|
1004
|
-
const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
1005
|
-
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
1006
|
-
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
1007
|
-
}
|
|
1008
|
-
dragRef.current = null;
|
|
1009
|
-
setResizing(false);
|
|
1010
|
-
};
|
|
1011
|
-
|
|
1012
|
-
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
1013
|
-
const step = e.shiftKey ? 32 : 8;
|
|
1014
|
-
if (e.key === 'ArrowLeft') {
|
|
1015
|
-
e.preventDefault();
|
|
1016
|
-
e.stopPropagation();
|
|
1017
|
-
setWidth((w) => Math.max(MIN_RAIL_WIDTH, w - step));
|
|
1018
|
-
} else if (e.key === 'ArrowRight') {
|
|
1019
|
-
e.preventDefault();
|
|
1020
|
-
e.stopPropagation();
|
|
1021
|
-
setWidth((w) => Math.min(MAX_RAIL_WIDTH, w + step));
|
|
1022
|
-
} else if (e.key === 'Home') {
|
|
1023
|
-
e.preventDefault();
|
|
1024
|
-
e.stopPropagation();
|
|
1025
|
-
setWidth(DEFAULT_RAIL_WIDTH);
|
|
1026
|
-
}
|
|
1027
|
-
};
|
|
1011
|
+
const [width, setWidth] = useStoredWidth(
|
|
1012
|
+
RAIL_WIDTH_STORAGE_KEY,
|
|
1013
|
+
DEFAULT_RAIL_WIDTH,
|
|
1014
|
+
MIN_RAIL_WIDTH,
|
|
1015
|
+
MAX_RAIL_WIDTH,
|
|
1016
|
+
);
|
|
1028
1017
|
|
|
1029
1018
|
return (
|
|
1030
1019
|
<div className="relative hidden shrink-0 md:block" style={{ width }}>
|
|
1031
1020
|
<ThumbnailRail width={width} {...props} />
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
onPointerMove={onPointerMove}
|
|
1043
|
-
onPointerUp={onPointerUp}
|
|
1044
|
-
onPointerCancel={onPointerUp}
|
|
1045
|
-
onKeyDown={onKeyDown}
|
|
1046
|
-
onDoubleClick={() => setWidth(DEFAULT_RAIL_WIDTH)}
|
|
1047
|
-
className={cn(
|
|
1048
|
-
'group/resize absolute inset-y-0 right-0 z-20 w-1.5 translate-x-1/2 cursor-col-resize touch-none outline-none',
|
|
1049
|
-
'focus-visible:bg-brand/20',
|
|
1050
|
-
)}
|
|
1051
|
-
>
|
|
1052
|
-
<span
|
|
1053
|
-
aria-hidden
|
|
1054
|
-
className={cn(
|
|
1055
|
-
'pointer-events-none absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-brand opacity-0 transition-opacity duration-150',
|
|
1056
|
-
'group-hover/resize:opacity-100 group-focus-visible/resize:opacity-100',
|
|
1057
|
-
resizing && 'opacity-100',
|
|
1058
|
-
)}
|
|
1059
|
-
/>
|
|
1060
|
-
</div>
|
|
1021
|
+
<ResizeHandle
|
|
1022
|
+
width={width}
|
|
1023
|
+
min={MIN_RAIL_WIDTH}
|
|
1024
|
+
max={MAX_RAIL_WIDTH}
|
|
1025
|
+
defaultWidth={DEFAULT_RAIL_WIDTH}
|
|
1026
|
+
edge="right"
|
|
1027
|
+
label={t.thumbnailRail.resizeRail}
|
|
1028
|
+
onResize={setWidth}
|
|
1029
|
+
className="right-0 translate-x-1/2"
|
|
1030
|
+
/>
|
|
1061
1031
|
</div>
|
|
1062
1032
|
);
|
|
1063
1033
|
}
|
|
1064
1034
|
|
|
1035
|
+
function EditorMenuItems({
|
|
1036
|
+
designOpen,
|
|
1037
|
+
onDesignToggle,
|
|
1038
|
+
}: {
|
|
1039
|
+
designOpen: boolean;
|
|
1040
|
+
onDesignToggle: () => void;
|
|
1041
|
+
}) {
|
|
1042
|
+
const t = useLocale();
|
|
1043
|
+
const { active, toggle, panelOpen, panelHidden, togglePanel } = useInspector();
|
|
1044
|
+
return (
|
|
1045
|
+
<>
|
|
1046
|
+
<DropdownMenuGroup>
|
|
1047
|
+
<DropdownMenuRadioGroup
|
|
1048
|
+
value={active ? 'edit' : 'preview'}
|
|
1049
|
+
onValueChange={(value) => {
|
|
1050
|
+
if ((value === 'edit') !== active) toggle();
|
|
1051
|
+
}}
|
|
1052
|
+
>
|
|
1053
|
+
<DropdownMenuRadioItem value="preview">{t.inspector.previewMode}</DropdownMenuRadioItem>
|
|
1054
|
+
<DropdownMenuRadioItem value="edit">{t.inspector.editMode}</DropdownMenuRadioItem>
|
|
1055
|
+
</DropdownMenuRadioGroup>
|
|
1056
|
+
<DropdownMenuCheckboxItem
|
|
1057
|
+
checked={designOpen}
|
|
1058
|
+
onCheckedChange={onDesignToggle}
|
|
1059
|
+
closeOnClick
|
|
1060
|
+
>
|
|
1061
|
+
{t.stylePanel.designToggle}
|
|
1062
|
+
</DropdownMenuCheckboxItem>
|
|
1063
|
+
<DropdownMenuCheckboxItem
|
|
1064
|
+
checked={active && panelOpen && !panelHidden}
|
|
1065
|
+
onCheckedChange={togglePanel}
|
|
1066
|
+
closeOnClick
|
|
1067
|
+
>
|
|
1068
|
+
{t.inspector.format}
|
|
1069
|
+
</DropdownMenuCheckboxItem>
|
|
1070
|
+
</DropdownMenuGroup>
|
|
1071
|
+
<DropdownMenuSeparator />
|
|
1072
|
+
</>
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function SlideAgentDock({
|
|
1077
|
+
slideId,
|
|
1078
|
+
slideTitle,
|
|
1079
|
+
pageNumber,
|
|
1080
|
+
totalPages,
|
|
1081
|
+
}: {
|
|
1082
|
+
slideId: string;
|
|
1083
|
+
slideTitle: string;
|
|
1084
|
+
pageNumber: number;
|
|
1085
|
+
totalPages: number;
|
|
1086
|
+
}) {
|
|
1087
|
+
const { selected, comments } = useInspector();
|
|
1088
|
+
const context = useMemo<AgentTurnContext>(
|
|
1089
|
+
() => ({
|
|
1090
|
+
slideId,
|
|
1091
|
+
slideTitle,
|
|
1092
|
+
pageNumber,
|
|
1093
|
+
totalPages,
|
|
1094
|
+
pendingComments: comments.length,
|
|
1095
|
+
selection: selected
|
|
1096
|
+
? {
|
|
1097
|
+
line: selected.line,
|
|
1098
|
+
column: selected.column,
|
|
1099
|
+
tagName: selected.anchor.tagName.toLowerCase(),
|
|
1100
|
+
text: (selected.anchor.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 120),
|
|
1101
|
+
}
|
|
1102
|
+
: null,
|
|
1103
|
+
}),
|
|
1104
|
+
[slideId, slideTitle, pageNumber, totalPages, comments.length, selected],
|
|
1105
|
+
);
|
|
1106
|
+
return <AgentPanel context={context} onClose={() => setDockOpen(false)} />;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1065
1109
|
function AgentConnectedBadge() {
|
|
1066
1110
|
const t = useLocale();
|
|
1067
1111
|
const connected = useAgentSocketConnected();
|
|
@@ -1072,6 +1116,7 @@ function AgentConnectedBadge() {
|
|
|
1072
1116
|
render={
|
|
1073
1117
|
<button
|
|
1074
1118
|
type="button"
|
|
1119
|
+
aria-label={connected ? t.slide.agentConnected : t.slide.agentDisconnected}
|
|
1075
1120
|
className="ml-1 flex shrink-0 cursor-help items-center gap-1.5 rounded-[3px] border border-hairline bg-card px-1.5 py-0.5 text-[10.5px] text-foreground/85 outline-none transition-colors duration-150 hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
1076
1121
|
>
|
|
1077
1122
|
<span aria-hidden className="relative flex size-1.5 items-center justify-center">
|
|
@@ -1084,7 +1129,9 @@ function AgentConnectedBadge() {
|
|
|
1084
1129
|
<span className="relative inline-flex size-1.5 rounded-full bg-rose-500" />
|
|
1085
1130
|
)}
|
|
1086
1131
|
</span>
|
|
1087
|
-
|
|
1132
|
+
<span className="hidden xl:inline">
|
|
1133
|
+
{connected ? t.slide.agentConnected : t.slide.agentDisconnected}
|
|
1134
|
+
</span>
|
|
1088
1135
|
</button>
|
|
1089
1136
|
}
|
|
1090
1137
|
/>
|
|
@@ -1130,13 +1177,14 @@ function SlideViewportNavigation({
|
|
|
1130
1177
|
canPrev: boolean;
|
|
1131
1178
|
canNext: boolean;
|
|
1132
1179
|
}) {
|
|
1133
|
-
const {
|
|
1180
|
+
const { mode, inlineEdit } = useInspector();
|
|
1134
1181
|
const isMobile = useIsMobile();
|
|
1135
1182
|
const editing = !!inlineEdit;
|
|
1183
|
+
const browsing = mode === 'preview';
|
|
1136
1184
|
|
|
1137
1185
|
useWheelPageNavigation({
|
|
1138
1186
|
ref: targetRef,
|
|
1139
|
-
enabled:
|
|
1187
|
+
enabled: browsing && !editing,
|
|
1140
1188
|
canPrev,
|
|
1141
1189
|
canNext,
|
|
1142
1190
|
onPrev,
|
|
@@ -1148,7 +1196,7 @@ function SlideViewportNavigation({
|
|
|
1148
1196
|
// zones). Interactive slide content keeps its tap via the hook's passthrough.
|
|
1149
1197
|
useClickPageNavigation({
|
|
1150
1198
|
ref: targetRef,
|
|
1151
|
-
enabled: isMobile &&
|
|
1199
|
+
enabled: isMobile && browsing && !editing,
|
|
1152
1200
|
edgeRatio: 0.18,
|
|
1153
1201
|
canPrev,
|
|
1154
1202
|
canNext,
|