@open-slide/core 0.0.11 → 0.0.12
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/dist/{build-DHiRlpjn.js → build-aiY_8kwE.js} +2 -1
- package/dist/cli/bin.js +43 -4
- package/dist/{config-LZM903FE.js → config-CVqRAagl.js} +592 -63
- package/dist/design-CROQh0AA.js +35 -0
- package/dist/{dev-B3JzCYn7.js → dev-R2we2iaF.js} +2 -1
- package/dist/index.d.ts +55 -4
- package/dist/index.js +110 -1
- package/dist/{preview-UikovHEt.js → preview-CU4zSyGp.js} +2 -1
- package/dist/sync-3oqN1WyK.js +139 -0
- package/dist/sync-B4eLo2H6.js +3 -0
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +2 -1
- package/package.json +2 -1
- package/skills/apply-comments/SKILL.md +83 -0
- package/skills/create-slide/SKILL.md +81 -0
- package/skills/create-theme/SKILL.md +194 -0
- package/skills/slide-authoring/SKILL.md +288 -0
- package/src/app/{App.tsx → app.tsx} +8 -6
- package/src/app/components/{AssetView.tsx → asset-view.tsx} +41 -33
- package/src/app/components/{ClickNavZones.tsx → click-nav-zones.tsx} +1 -1
- package/src/app/components/history-provider.tsx +120 -0
- package/src/app/components/image-placeholder.tsx +121 -0
- package/src/app/components/inspector/{CommentWidget.tsx → comment-widget.tsx} +1 -1
- package/src/app/components/inspector/{InspectOverlay.tsx → inspect-overlay.tsx} +1 -1
- package/src/app/components/inspector/{InspectorPanel.tsx → inspector-panel.tsx} +164 -212
- package/src/app/components/inspector/{InspectorProvider.tsx → inspector-provider.tsx} +186 -18
- package/src/app/components/inspector/save-bar.tsx +47 -0
- package/src/app/components/panel/panel-fields.tsx +60 -0
- package/src/app/components/panel/panel-shell.tsx +78 -0
- package/src/app/components/panel/save-card.tsx +139 -0
- package/src/app/components/pdf-progress-toast.tsx +25 -0
- package/src/app/components/player.tsx +341 -0
- package/src/app/components/present/blackout-overlay.tsx +18 -0
- package/src/app/components/present/control-bar.tsx +204 -0
- package/src/app/components/present/help-overlay.tsx +56 -0
- package/src/app/components/present/jump-input.tsx +74 -0
- package/src/app/components/present/laser-pointer.tsx +40 -0
- package/src/app/components/present/overview-grid.tsx +184 -0
- package/src/app/components/present/progress-bar.tsx +26 -0
- package/src/app/components/present/use-idle.ts +44 -0
- package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
- package/src/app/components/present/use-presenter-channel.ts +71 -0
- package/src/app/components/present/use-touch-swipe.ts +63 -0
- package/src/app/components/sidebar/{FolderItem.tsx → folder-item.tsx} +62 -27
- package/src/app/components/sidebar/{IconPicker.tsx → icon-picker.tsx} +13 -10
- package/src/app/components/sidebar/{Sidebar.tsx → sidebar.tsx} +40 -34
- package/src/app/components/{SlideCanvas.tsx → slide-canvas.tsx} +35 -10
- package/src/app/components/style-panel/design-provider.tsx +139 -0
- package/src/app/components/style-panel/style-panel.tsx +326 -0
- package/src/app/components/style-panel/use-design.ts +112 -0
- package/src/app/components/theme-toggle.tsx +57 -0
- package/src/app/components/thumbnail-rail.tsx +151 -0
- package/src/app/components/ui/button.tsx +51 -19
- package/src/app/components/ui/card.tsx +1 -1
- package/src/app/components/ui/dialog.tsx +25 -9
- package/src/app/components/ui/dropdown-menu.tsx +29 -12
- package/src/app/components/ui/input.tsx +13 -9
- package/src/app/components/ui/popover.tsx +5 -2
- package/src/app/components/ui/progress.tsx +2 -2
- package/src/app/components/ui/select.tsx +11 -5
- package/src/app/components/ui/separator.tsx +1 -1
- package/src/app/components/ui/slider.tsx +4 -4
- package/src/app/components/ui/sonner.tsx +11 -1
- package/src/app/components/ui/tabs.tsx +6 -6
- package/src/app/components/ui/textarea.tsx +11 -7
- package/src/app/components/ui/toggle-group.tsx +2 -2
- package/src/app/components/ui/toggle.tsx +6 -6
- package/src/app/components/ui/tooltip.tsx +5 -2
- package/src/app/lib/export-html.ts +10 -1
- package/src/app/lib/export-pdf.ts +7 -0
- package/src/app/lib/folders.ts +1 -1
- package/src/app/lib/inspector/{useEditor.ts → use-editor.ts} +2 -1
- package/src/app/lib/sdk.ts +5 -0
- package/src/app/lib/slides.ts +1 -1
- package/src/app/lib/utils.ts +1 -1
- package/src/app/main.tsx +5 -2
- package/src/app/routes/{Home.tsx → home.tsx} +266 -97
- package/src/app/routes/presenter.tsx +400 -0
- package/src/app/routes/slide.tsx +519 -0
- package/src/app/styles.css +338 -67
- package/src/app/components/PdfProgressToast.tsx +0 -23
- package/src/app/components/Player.tsx +0 -100
- package/src/app/components/ThumbnailRail.tsx +0 -68
- package/src/app/components/inspector/SaveBar.tsx +0 -77
- package/src/app/routes/Slide.tsx +0 -478
- /package/dist/{config-SXL5qIl6.d.ts → config-DweCbRkQ.d.ts} +0 -0
- /package/src/app/lib/inspector/{useComments.ts → use-comments.ts} +0 -0
- /package/src/app/lib/{useWheelPageNavigation.ts → use-wheel-page-navigation.ts} +0 -0
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { Check, Loader2, Save, Undo2 } from 'lucide-react';
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
3
|
-
import { Button } from '@/components/ui/button';
|
|
4
|
-
import { useInspector } from './InspectorProvider';
|
|
5
|
-
|
|
6
|
-
// Optimistic DOM updates make the canvas *look* saved, so without
|
|
7
|
-
// this affordance a user could close the tab thinking their tweaks
|
|
8
|
-
// hit disk when they're still buffered in memory.
|
|
9
|
-
export function SaveBar() {
|
|
10
|
-
const { pendingCount, commitEdits, cancelEdits, committing } = useInspector();
|
|
11
|
-
const [justSaved, setJustSaved] = useState(false);
|
|
12
|
-
|
|
13
|
-
// Brief "Saved" hold so the bar's disappearance feels intentional.
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
if (!justSaved) return;
|
|
16
|
-
const t = setTimeout(() => setJustSaved(false), 1200);
|
|
17
|
-
return () => clearTimeout(t);
|
|
18
|
-
}, [justSaved]);
|
|
19
|
-
|
|
20
|
-
const visible = pendingCount > 0 || committing || justSaved;
|
|
21
|
-
if (!visible) return null;
|
|
22
|
-
|
|
23
|
-
const onSave = async () => {
|
|
24
|
-
await commitEdits();
|
|
25
|
-
setJustSaved(true);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<div
|
|
30
|
-
data-inspector-ui
|
|
31
|
-
className="pointer-events-none absolute bottom-6 left-1/2 z-30 -translate-x-1/2 animate-in fade-in slide-in-from-bottom-2 duration-200"
|
|
32
|
-
>
|
|
33
|
-
<div className="pointer-events-auto flex items-center gap-2 rounded-full border bg-card/95 py-1 pr-1 pl-3 shadow-lg backdrop-blur">
|
|
34
|
-
{justSaved ? (
|
|
35
|
-
<span className="flex items-center gap-1.5 text-xs font-medium text-foreground">
|
|
36
|
-
<Check className="size-3.5 text-emerald-600" />
|
|
37
|
-
Saved
|
|
38
|
-
</span>
|
|
39
|
-
) : (
|
|
40
|
-
<span className="text-xs font-medium text-foreground">
|
|
41
|
-
{pendingCount} unsaved {pendingCount === 1 ? 'change' : 'changes'}
|
|
42
|
-
</span>
|
|
43
|
-
)}
|
|
44
|
-
{!justSaved && (
|
|
45
|
-
<Button
|
|
46
|
-
size="sm"
|
|
47
|
-
variant="ghost"
|
|
48
|
-
className="h-7 rounded-full px-2.5 text-[11px] text-muted-foreground hover:text-foreground"
|
|
49
|
-
onClick={cancelEdits}
|
|
50
|
-
disabled={committing || pendingCount === 0}
|
|
51
|
-
>
|
|
52
|
-
<Undo2 className="size-3.5" />
|
|
53
|
-
Discard
|
|
54
|
-
</Button>
|
|
55
|
-
)}
|
|
56
|
-
<Button
|
|
57
|
-
size="sm"
|
|
58
|
-
className="h-7 rounded-full px-3 text-[11px]"
|
|
59
|
-
onClick={onSave}
|
|
60
|
-
disabled={committing || pendingCount === 0}
|
|
61
|
-
>
|
|
62
|
-
{committing ? (
|
|
63
|
-
<>
|
|
64
|
-
<Loader2 className="size-3.5 animate-spin" />
|
|
65
|
-
Saving
|
|
66
|
-
</>
|
|
67
|
-
) : (
|
|
68
|
-
<>
|
|
69
|
-
<Save className="size-3.5" />
|
|
70
|
-
Save
|
|
71
|
-
</>
|
|
72
|
-
)}
|
|
73
|
-
</Button>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
);
|
|
77
|
-
}
|
package/src/app/routes/Slide.tsx
DELETED
|
@@ -1,478 +0,0 @@
|
|
|
1
|
-
import config from 'virtual:open-slide/config';
|
|
2
|
-
import { ChevronLeft, Download, FileCode2, FileText, Loader2, Pencil, Play } from 'lucide-react';
|
|
3
|
-
import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import { Link, useParams, useSearchParams } from 'react-router-dom';
|
|
5
|
-
import { toast } from 'sonner';
|
|
6
|
-
import { AssetView } from '@/components/AssetView';
|
|
7
|
-
import { CommentWidget } from '@/components/inspector/CommentWidget';
|
|
8
|
-
import { InspectOverlay } from '@/components/inspector/InspectOverlay';
|
|
9
|
-
import { InspectorPanel } from '@/components/inspector/InspectorPanel';
|
|
10
|
-
import {
|
|
11
|
-
InspectorProvider,
|
|
12
|
-
InspectToggleButton,
|
|
13
|
-
useInspector,
|
|
14
|
-
} from '@/components/inspector/InspectorProvider';
|
|
15
|
-
import { SaveBar } from '@/components/inspector/SaveBar';
|
|
16
|
-
import { Button, buttonVariants } from '@/components/ui/button';
|
|
17
|
-
import {
|
|
18
|
-
DropdownMenu,
|
|
19
|
-
DropdownMenuContent,
|
|
20
|
-
DropdownMenuItem,
|
|
21
|
-
DropdownMenuTrigger,
|
|
22
|
-
} from '@/components/ui/dropdown-menu';
|
|
23
|
-
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
24
|
-
import { useFolders } from '@/lib/folders';
|
|
25
|
-
import { useWheelPageNavigation } from '@/lib/useWheelPageNavigation';
|
|
26
|
-
import { cn } from '@/lib/utils';
|
|
27
|
-
import { ClickNavZones } from '../components/ClickNavZones';
|
|
28
|
-
import { PdfProgressToast } from '../components/PdfProgressToast';
|
|
29
|
-
import { Player } from '../components/Player';
|
|
30
|
-
import { SlideCanvas } from '../components/SlideCanvas';
|
|
31
|
-
import { ThumbnailRail } from '../components/ThumbnailRail';
|
|
32
|
-
import { exportSlideAsHtml } from '../lib/export-html';
|
|
33
|
-
import { exportSlideAsPdf } from '../lib/export-pdf';
|
|
34
|
-
import type { SlideModule } from '../lib/sdk';
|
|
35
|
-
import { loadSlide } from '../lib/slides';
|
|
36
|
-
|
|
37
|
-
const { showSlideUi, showSlideBrowser, allowHtmlDownload } = config.build;
|
|
38
|
-
|
|
39
|
-
export function Slide() {
|
|
40
|
-
const { slideId = '' } = useParams();
|
|
41
|
-
const [searchParams, setSearchParams] = useSearchParams();
|
|
42
|
-
const [slide, setSlide] = useState<SlideModule | null>(null);
|
|
43
|
-
const [error, setError] = useState<string | null>(null);
|
|
44
|
-
const [playing, setPlaying] = useState(false);
|
|
45
|
-
const [exporting, setExporting] = useState(false);
|
|
46
|
-
const { renameSlide } = useFolders();
|
|
47
|
-
const slideViewportRef = useRef<HTMLElement>(null);
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
let cancelled = false;
|
|
51
|
-
setSlide(null);
|
|
52
|
-
setError(null);
|
|
53
|
-
loadSlide(slideId)
|
|
54
|
-
.then((mod) => {
|
|
55
|
-
if (!cancelled) setSlide(mod);
|
|
56
|
-
})
|
|
57
|
-
.catch((e) => {
|
|
58
|
-
if (!cancelled) setError(String(e?.message ?? e));
|
|
59
|
-
});
|
|
60
|
-
return () => {
|
|
61
|
-
cancelled = true;
|
|
62
|
-
};
|
|
63
|
-
}, [slideId]);
|
|
64
|
-
|
|
65
|
-
const pages = useMemo(() => slide?.default ?? [], [slide]);
|
|
66
|
-
const pageCount = pages.length;
|
|
67
|
-
const rawIndex = Number(searchParams.get('p') ?? '1') - 1;
|
|
68
|
-
const index = Number.isFinite(rawIndex) ? Math.max(0, Math.min(pageCount - 1, rawIndex)) : 0;
|
|
69
|
-
const view = searchParams.get('view') === 'assets' ? 'assets' : 'slides';
|
|
70
|
-
|
|
71
|
-
const goTo = useCallback(
|
|
72
|
-
(i: number) => {
|
|
73
|
-
const clamped = Math.max(0, Math.min(pageCount - 1, i));
|
|
74
|
-
setSearchParams(
|
|
75
|
-
(prev) => {
|
|
76
|
-
const next = new URLSearchParams(prev);
|
|
77
|
-
next.set('p', String(clamped + 1));
|
|
78
|
-
return next;
|
|
79
|
-
},
|
|
80
|
-
{ replace: true },
|
|
81
|
-
);
|
|
82
|
-
},
|
|
83
|
-
[pageCount, setSearchParams],
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
if (playing) return;
|
|
88
|
-
const onKey = (e: KeyboardEvent) => {
|
|
89
|
-
if (e.target instanceof HTMLElement && e.target.matches('input, textarea')) return;
|
|
90
|
-
if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === 'PageDown') {
|
|
91
|
-
e.preventDefault();
|
|
92
|
-
goTo(index + 1);
|
|
93
|
-
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'PageUp') {
|
|
94
|
-
e.preventDefault();
|
|
95
|
-
goTo(index - 1);
|
|
96
|
-
} else if (e.key === 'f' || e.key === 'F') {
|
|
97
|
-
setPlaying(true);
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
window.addEventListener('keydown', onKey);
|
|
101
|
-
return () => window.removeEventListener('keydown', onKey);
|
|
102
|
-
}, [index, goTo, playing]);
|
|
103
|
-
|
|
104
|
-
if (error) {
|
|
105
|
-
return (
|
|
106
|
-
<div className="mx-auto max-w-3xl px-8 py-16 text-muted-foreground">
|
|
107
|
-
{showSlideBrowser && (
|
|
108
|
-
<Link to="/" className="text-sm font-medium text-primary hover:underline">
|
|
109
|
-
← Home
|
|
110
|
-
</Link>
|
|
111
|
-
)}
|
|
112
|
-
<h2 className="mt-4 text-xl font-semibold text-foreground">Failed to load slide</h2>
|
|
113
|
-
<pre className="mt-4 overflow-auto rounded-md border bg-card p-4 text-xs whitespace-pre-wrap shadow-sm">
|
|
114
|
-
{error}
|
|
115
|
-
</pre>
|
|
116
|
-
</div>
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (!slide) {
|
|
121
|
-
return (
|
|
122
|
-
<div className="mx-auto max-w-3xl px-8 py-16 text-sm text-muted-foreground">
|
|
123
|
-
Loading {slideId}…
|
|
124
|
-
</div>
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (pageCount === 0) {
|
|
129
|
-
return (
|
|
130
|
-
<div className="mx-auto max-w-3xl px-8 py-16 text-muted-foreground">
|
|
131
|
-
{showSlideBrowser && (
|
|
132
|
-
<Link to="/" className="text-sm font-medium text-primary hover:underline">
|
|
133
|
-
← Home
|
|
134
|
-
</Link>
|
|
135
|
-
)}
|
|
136
|
-
<h2 className="mt-4 text-xl font-semibold text-foreground">Empty slide</h2>
|
|
137
|
-
<p className="mt-2 text-sm">
|
|
138
|
-
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
|
|
139
|
-
slides/{slideId}/index.tsx
|
|
140
|
-
</code>{' '}
|
|
141
|
-
must{' '}
|
|
142
|
-
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">export default</code> a
|
|
143
|
-
non-empty array of components.
|
|
144
|
-
</p>
|
|
145
|
-
</div>
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (!showSlideUi) {
|
|
150
|
-
return (
|
|
151
|
-
<Player
|
|
152
|
-
pages={pages}
|
|
153
|
-
index={index}
|
|
154
|
-
onIndexChange={goTo}
|
|
155
|
-
onExit={() => {}}
|
|
156
|
-
allowExit={false}
|
|
157
|
-
/>
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (playing) {
|
|
162
|
-
return (
|
|
163
|
-
<Player pages={pages} index={index} onIndexChange={goTo} onExit={() => setPlaying(false)} />
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const CurrentPage = pages[index];
|
|
168
|
-
const title = slide.meta?.title ?? slideId;
|
|
169
|
-
|
|
170
|
-
return (
|
|
171
|
-
<InspectorProvider slideId={slideId}>
|
|
172
|
-
<div className="flex h-screen flex-col overflow-hidden bg-background">
|
|
173
|
-
<header className="relative flex shrink-0 items-center justify-between border-b bg-card px-3 py-2 md:px-5 md:py-3">
|
|
174
|
-
<div className="flex items-center gap-2 md:gap-3">
|
|
175
|
-
{showSlideBrowser && (
|
|
176
|
-
<Button asChild variant="ghost" size="sm" className="px-2 md:px-3">
|
|
177
|
-
<Link to="/">
|
|
178
|
-
<ChevronLeft className="size-4" />
|
|
179
|
-
<span className="hidden md:inline">Home</span>
|
|
180
|
-
</Link>
|
|
181
|
-
</Button>
|
|
182
|
-
)}
|
|
183
|
-
{import.meta.env.DEV && (
|
|
184
|
-
<Tabs
|
|
185
|
-
value={view}
|
|
186
|
-
onValueChange={(next) => {
|
|
187
|
-
setSearchParams(
|
|
188
|
-
(prev) => {
|
|
189
|
-
const params = new URLSearchParams(prev);
|
|
190
|
-
if (next === 'assets') params.set('view', 'assets');
|
|
191
|
-
else params.delete('view');
|
|
192
|
-
return params;
|
|
193
|
-
},
|
|
194
|
-
{ replace: true },
|
|
195
|
-
);
|
|
196
|
-
}}
|
|
197
|
-
>
|
|
198
|
-
<TabsList className="relative h-7 rounded-md p-0.5 group-data-[orientation=horizontal]/tabs:h-7">
|
|
199
|
-
<div
|
|
200
|
-
aria-hidden
|
|
201
|
-
className="pointer-events-none absolute top-0.5 bottom-0.5 left-0.5 w-[calc(50%-2px)] rounded-[5px] bg-background shadow-sm transition-transform duration-200 ease-out"
|
|
202
|
-
style={{
|
|
203
|
-
transform: view === 'assets' ? 'translateX(100%)' : 'translateX(0)',
|
|
204
|
-
}}
|
|
205
|
-
/>
|
|
206
|
-
<TabsTrigger
|
|
207
|
-
value="slides"
|
|
208
|
-
className="relative z-10 h-6 px-3 text-xs data-[state=active]:bg-transparent data-[state=active]:shadow-none dark:data-[state=active]:bg-transparent"
|
|
209
|
-
>
|
|
210
|
-
Slides
|
|
211
|
-
</TabsTrigger>
|
|
212
|
-
<TabsTrigger
|
|
213
|
-
value="assets"
|
|
214
|
-
className="relative z-10 h-6 px-3 text-xs data-[state=active]:bg-transparent data-[state=active]:shadow-none dark:data-[state=active]:bg-transparent"
|
|
215
|
-
>
|
|
216
|
-
Assets
|
|
217
|
-
</TabsTrigger>
|
|
218
|
-
</TabsList>
|
|
219
|
-
</Tabs>
|
|
220
|
-
)}
|
|
221
|
-
</div>
|
|
222
|
-
|
|
223
|
-
<div className="pointer-events-none absolute inset-x-0 top-1/2 flex -translate-y-1/2 justify-center px-2">
|
|
224
|
-
<div className="pointer-events-auto min-w-0 max-w-[min(32rem,calc(100vw-20rem))]">
|
|
225
|
-
<InlineTitleEditor title={title} onSubmit={(next) => renameSlide(slideId, next)} />
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
228
|
-
|
|
229
|
-
<div className="flex items-center gap-1.5">
|
|
230
|
-
{view === 'slides' && allowHtmlDownload && (
|
|
231
|
-
<DropdownMenu>
|
|
232
|
-
<DropdownMenuTrigger
|
|
233
|
-
type="button"
|
|
234
|
-
disabled={exporting}
|
|
235
|
-
aria-label="Download"
|
|
236
|
-
title="Download"
|
|
237
|
-
className={cn(buttonVariants({ variant: 'outline', size: 'icon-sm' }))}
|
|
238
|
-
>
|
|
239
|
-
{exporting ? (
|
|
240
|
-
<Loader2 className="size-4 animate-spin" />
|
|
241
|
-
) : (
|
|
242
|
-
<Download className="size-4" />
|
|
243
|
-
)}
|
|
244
|
-
</DropdownMenuTrigger>
|
|
245
|
-
<DropdownMenuContent align="end" className="min-w-[180px]">
|
|
246
|
-
<DropdownMenuItem
|
|
247
|
-
disabled={exporting}
|
|
248
|
-
onSelect={async () => {
|
|
249
|
-
if (!slide || exporting) return;
|
|
250
|
-
setExporting(true);
|
|
251
|
-
try {
|
|
252
|
-
await exportSlideAsHtml(slide, slideId);
|
|
253
|
-
} catch (err) {
|
|
254
|
-
console.error('[open-slide] export failed', err);
|
|
255
|
-
} finally {
|
|
256
|
-
setExporting(false);
|
|
257
|
-
}
|
|
258
|
-
}}
|
|
259
|
-
>
|
|
260
|
-
<FileCode2 />
|
|
261
|
-
Download HTML
|
|
262
|
-
</DropdownMenuItem>
|
|
263
|
-
<DropdownMenuItem
|
|
264
|
-
disabled={exporting}
|
|
265
|
-
onSelect={async () => {
|
|
266
|
-
if (!slide || exporting) return;
|
|
267
|
-
setExporting(true);
|
|
268
|
-
const toastId = `pdf-export-${slideId}`;
|
|
269
|
-
toast.custom(
|
|
270
|
-
() => (
|
|
271
|
-
<PdfProgressToast
|
|
272
|
-
progress={{
|
|
273
|
-
phase: 'processing',
|
|
274
|
-
current: 0,
|
|
275
|
-
total: pages.length,
|
|
276
|
-
percent: 0,
|
|
277
|
-
}}
|
|
278
|
-
/>
|
|
279
|
-
),
|
|
280
|
-
{ id: toastId, duration: Infinity },
|
|
281
|
-
);
|
|
282
|
-
try {
|
|
283
|
-
await exportSlideAsPdf(slide, slideId, (p) => {
|
|
284
|
-
toast.custom(() => <PdfProgressToast progress={p} />, {
|
|
285
|
-
id: toastId,
|
|
286
|
-
duration: Infinity,
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
} catch (err) {
|
|
290
|
-
console.error('[open-slide] pdf export failed', err);
|
|
291
|
-
toast.error('PDF export failed', { id: toastId, duration: 4000 });
|
|
292
|
-
} finally {
|
|
293
|
-
setExporting(false);
|
|
294
|
-
toast.dismiss(toastId);
|
|
295
|
-
}
|
|
296
|
-
}}
|
|
297
|
-
>
|
|
298
|
-
<FileText />
|
|
299
|
-
Download PDF
|
|
300
|
-
</DropdownMenuItem>
|
|
301
|
-
</DropdownMenuContent>
|
|
302
|
-
</DropdownMenu>
|
|
303
|
-
)}
|
|
304
|
-
{view === 'slides' && <InspectToggleButton />}
|
|
305
|
-
{view === 'slides' && (
|
|
306
|
-
<Button size="sm" onClick={() => setPlaying(true)} className="px-2 md:px-3">
|
|
307
|
-
<Play className="size-4" />
|
|
308
|
-
<span className="hidden md:inline">Play</span>
|
|
309
|
-
<kbd className="ml-1 hidden rounded bg-primary-foreground/20 px-1 text-[10px] md:inline">
|
|
310
|
-
F
|
|
311
|
-
</kbd>
|
|
312
|
-
</Button>
|
|
313
|
-
)}
|
|
314
|
-
</div>
|
|
315
|
-
</header>
|
|
316
|
-
|
|
317
|
-
{view === 'assets' ? (
|
|
318
|
-
<div className="min-h-0 flex-1">
|
|
319
|
-
<AssetView slideId={slideId} />
|
|
320
|
-
</div>
|
|
321
|
-
) : (
|
|
322
|
-
<div className="flex min-h-0 flex-1">
|
|
323
|
-
<div className="hidden w-[17rem] shrink-0 md:block">
|
|
324
|
-
<ThumbnailRail pages={pages} current={index} onSelect={goTo} />
|
|
325
|
-
</div>
|
|
326
|
-
<main
|
|
327
|
-
ref={slideViewportRef}
|
|
328
|
-
data-inspector-root
|
|
329
|
-
className="relative min-h-0 min-w-0 flex-1 bg-background p-2 md:p-8"
|
|
330
|
-
>
|
|
331
|
-
<SlideWheelNavigation
|
|
332
|
-
targetRef={slideViewportRef}
|
|
333
|
-
onPrev={() => goTo(index - 1)}
|
|
334
|
-
onNext={() => goTo(index + 1)}
|
|
335
|
-
canPrev={index > 0}
|
|
336
|
-
canNext={index < pageCount - 1}
|
|
337
|
-
/>
|
|
338
|
-
<SlideCanvas>
|
|
339
|
-
<CurrentPage />
|
|
340
|
-
</SlideCanvas>
|
|
341
|
-
<ClickNavZones
|
|
342
|
-
onPrev={() => goTo(index - 1)}
|
|
343
|
-
onNext={() => goTo(index + 1)}
|
|
344
|
-
canPrev={index > 0}
|
|
345
|
-
canNext={index < pageCount - 1}
|
|
346
|
-
/>
|
|
347
|
-
<InspectOverlay />
|
|
348
|
-
<SaveBar />
|
|
349
|
-
<CommentWidget />
|
|
350
|
-
<div className="pointer-events-none absolute bottom-3 left-1/2 z-10 -translate-x-1/2 rounded-full bg-black/50 px-2.5 py-0.5 text-[11px] font-medium tabular-nums text-white backdrop-blur md:hidden">
|
|
351
|
-
{index + 1} / {pageCount}
|
|
352
|
-
</div>
|
|
353
|
-
</main>
|
|
354
|
-
<InspectorPanel />
|
|
355
|
-
</div>
|
|
356
|
-
)}
|
|
357
|
-
</div>
|
|
358
|
-
</InspectorProvider>
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function SlideWheelNavigation({
|
|
363
|
-
targetRef,
|
|
364
|
-
onPrev,
|
|
365
|
-
onNext,
|
|
366
|
-
canPrev,
|
|
367
|
-
canNext,
|
|
368
|
-
}: {
|
|
369
|
-
targetRef: RefObject<HTMLElement>;
|
|
370
|
-
onPrev: () => void;
|
|
371
|
-
onNext: () => void;
|
|
372
|
-
canPrev: boolean;
|
|
373
|
-
canNext: boolean;
|
|
374
|
-
}) {
|
|
375
|
-
const { active } = useInspector();
|
|
376
|
-
|
|
377
|
-
useWheelPageNavigation({
|
|
378
|
-
ref: targetRef,
|
|
379
|
-
enabled: !active,
|
|
380
|
-
canPrev,
|
|
381
|
-
canNext,
|
|
382
|
-
onPrev,
|
|
383
|
-
onNext,
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
return null;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function InlineTitleEditor({
|
|
390
|
-
title,
|
|
391
|
-
onSubmit,
|
|
392
|
-
}: {
|
|
393
|
-
title: string;
|
|
394
|
-
onSubmit: (name: string) => Promise<void> | void;
|
|
395
|
-
}) {
|
|
396
|
-
const [editing, setEditing] = useState(false);
|
|
397
|
-
const [value, setValue] = useState(title);
|
|
398
|
-
const [saving, setSaving] = useState(false);
|
|
399
|
-
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
400
|
-
|
|
401
|
-
useEffect(() => {
|
|
402
|
-
if (!editing) setValue(title);
|
|
403
|
-
}, [title, editing]);
|
|
404
|
-
|
|
405
|
-
useEffect(() => {
|
|
406
|
-
if (editing) {
|
|
407
|
-
queueMicrotask(() => {
|
|
408
|
-
inputRef.current?.focus();
|
|
409
|
-
inputRef.current?.select();
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
}, [editing]);
|
|
413
|
-
|
|
414
|
-
const commit = async () => {
|
|
415
|
-
const trimmed = value.trim();
|
|
416
|
-
if (!trimmed || trimmed === title) {
|
|
417
|
-
setValue(title);
|
|
418
|
-
setEditing(false);
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
setSaving(true);
|
|
422
|
-
try {
|
|
423
|
-
await onSubmit(trimmed);
|
|
424
|
-
setEditing(false);
|
|
425
|
-
} finally {
|
|
426
|
-
setSaving(false);
|
|
427
|
-
}
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
const cancel = () => {
|
|
431
|
-
setValue(title);
|
|
432
|
-
setEditing(false);
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
if (editing) {
|
|
436
|
-
return (
|
|
437
|
-
<div className="flex flex-1 items-center justify-center">
|
|
438
|
-
<input
|
|
439
|
-
ref={inputRef}
|
|
440
|
-
value={value}
|
|
441
|
-
disabled={saving}
|
|
442
|
-
onChange={(e) => setValue(e.target.value)}
|
|
443
|
-
onBlur={() => {
|
|
444
|
-
if (!saving) commit();
|
|
445
|
-
}}
|
|
446
|
-
onKeyDown={(e) => {
|
|
447
|
-
if (e.key === 'Enter') {
|
|
448
|
-
e.preventDefault();
|
|
449
|
-
commit();
|
|
450
|
-
} else if (e.key === 'Escape') {
|
|
451
|
-
e.preventDefault();
|
|
452
|
-
cancel();
|
|
453
|
-
}
|
|
454
|
-
}}
|
|
455
|
-
maxLength={80}
|
|
456
|
-
className="min-w-0 max-w-[min(32rem,90%)] rounded-md border bg-background px-2 py-0.5 text-center text-xs font-semibold tracking-tight outline-none ring-ring/40 focus:ring-2 md:text-sm"
|
|
457
|
-
/>
|
|
458
|
-
</div>
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
return (
|
|
463
|
-
<div className="group/title flex flex-1 items-center justify-center gap-1.5 min-w-0">
|
|
464
|
-
<h1 className="truncate text-xs font-semibold tracking-tight md:text-sm">{title}</h1>
|
|
465
|
-
<button
|
|
466
|
-
type="button"
|
|
467
|
-
onClick={() => setEditing(true)}
|
|
468
|
-
aria-label="Rename slide"
|
|
469
|
-
className={cn(
|
|
470
|
-
'flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition-opacity hover:bg-muted hover:text-foreground',
|
|
471
|
-
'opacity-0 group-hover/title:opacity-100 focus-visible:opacity-100',
|
|
472
|
-
)}
|
|
473
|
-
>
|
|
474
|
-
<Pencil className="size-3.5" />
|
|
475
|
-
</button>
|
|
476
|
-
</div>
|
|
477
|
-
);
|
|
478
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|