@open-slide/core 0.0.7 → 0.0.9
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-cUKUY4bh.js → build-pqF4W1Yi.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-DOcMmFJ7.js → config-CtwxMYv9.js} +375 -45
- package/dist/{dev-Brzmgu64.js → dev-CJX97uiy.js} +1 -1
- package/dist/{preview-Bf8iFXA-.js → preview-IuLPcL5y.js} +1 -1
- package/dist/vite/index.js +1 -1
- package/package.json +3 -1
- package/src/app/App.tsx +2 -0
- package/src/app/components/PdfProgressToast.tsx +23 -0
- package/src/app/components/Player.tsx +18 -3
- package/src/app/components/inspector/CommentWidget.tsx +1 -1
- package/src/app/components/inspector/InspectOverlay.tsx +81 -41
- package/src/app/components/inspector/InspectorPanel.tsx +805 -0
- package/src/app/components/inspector/InspectorProvider.tsx +199 -13
- package/src/app/components/inspector/SaveBar.tsx +77 -0
- package/src/app/components/ui/input.tsx +21 -0
- package/src/app/components/ui/label.tsx +24 -0
- package/src/app/components/ui/progress.tsx +31 -0
- package/src/app/components/ui/select.tsx +190 -0
- package/src/app/components/ui/slider.tsx +61 -0
- package/src/app/components/ui/sonner.tsx +38 -0
- package/src/app/components/ui/textarea.tsx +18 -0
- package/src/app/components/ui/toggle-group.tsx +83 -0
- package/src/app/components/ui/toggle.tsx +45 -0
- package/src/app/components/ui/tooltip.tsx +55 -0
- package/src/app/lib/export-pdf.ts +197 -0
- package/src/app/lib/inspector/fiber.ts +40 -5
- package/src/app/lib/inspector/useEditor.ts +61 -0
- package/src/app/lib/print-ready.ts +58 -0
- package/src/app/lib/useWheelPageNavigation.ts +92 -0
- package/src/app/routes/Slide.tsx +91 -6
- package/src/app/components/inspector/CommentPopover.tsx +0 -94
package/src/app/routes/Slide.tsx
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import { ChevronLeft, Download, FileCode2, Loader2, Pencil, Play } from 'lucide-react';
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { Link, useParams, useSearchParams } from 'react-router-dom';
|
|
4
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';
|
|
5
6
|
import { CommentWidget } from '@/components/inspector/CommentWidget';
|
|
6
7
|
import { InspectOverlay } from '@/components/inspector/InspectOverlay';
|
|
7
|
-
import {
|
|
8
|
+
import { InspectorPanel } from '@/components/inspector/InspectorPanel';
|
|
9
|
+
import {
|
|
10
|
+
InspectorProvider,
|
|
11
|
+
InspectToggleButton,
|
|
12
|
+
useInspector,
|
|
13
|
+
} from '@/components/inspector/InspectorProvider';
|
|
14
|
+
import { SaveBar } from '@/components/inspector/SaveBar';
|
|
8
15
|
import { Button, buttonVariants } from '@/components/ui/button';
|
|
9
16
|
import {
|
|
10
17
|
DropdownMenu,
|
|
@@ -14,12 +21,15 @@ import {
|
|
|
14
21
|
} from '@/components/ui/dropdown-menu';
|
|
15
22
|
import { Separator } from '@/components/ui/separator';
|
|
16
23
|
import { useFolders } from '@/lib/folders';
|
|
24
|
+
import { useWheelPageNavigation } from '@/lib/useWheelPageNavigation';
|
|
17
25
|
import { cn } from '@/lib/utils';
|
|
18
26
|
import { ClickNavZones } from '../components/ClickNavZones';
|
|
27
|
+
import { PdfProgressToast } from '../components/PdfProgressToast';
|
|
19
28
|
import { Player } from '../components/Player';
|
|
20
29
|
import { SlideCanvas } from '../components/SlideCanvas';
|
|
21
30
|
import { ThumbnailRail } from '../components/ThumbnailRail';
|
|
22
31
|
import { exportSlideAsHtml } from '../lib/export-html';
|
|
32
|
+
import { exportSlideAsPdf } from '../lib/export-pdf';
|
|
23
33
|
import type { SlideModule } from '../lib/sdk';
|
|
24
34
|
import { loadSlide } from '../lib/slides';
|
|
25
35
|
|
|
@@ -33,6 +43,7 @@ export function Slide() {
|
|
|
33
43
|
const [playing, setPlaying] = useState(false);
|
|
34
44
|
const [exporting, setExporting] = useState(false);
|
|
35
45
|
const { renameSlide } = useFolders();
|
|
46
|
+
const slideViewportRef = useRef<HTMLElement>(null);
|
|
36
47
|
|
|
37
48
|
useEffect(() => {
|
|
38
49
|
let cancelled = false;
|
|
@@ -212,6 +223,44 @@ export function Slide() {
|
|
|
212
223
|
<FileCode2 />
|
|
213
224
|
Download HTML
|
|
214
225
|
</DropdownMenuItem>
|
|
226
|
+
<DropdownMenuItem
|
|
227
|
+
disabled={exporting}
|
|
228
|
+
onSelect={async () => {
|
|
229
|
+
if (!slide || exporting) return;
|
|
230
|
+
setExporting(true);
|
|
231
|
+
const toastId = `pdf-export-${slideId}`;
|
|
232
|
+
toast.custom(
|
|
233
|
+
() => (
|
|
234
|
+
<PdfProgressToast
|
|
235
|
+
progress={{
|
|
236
|
+
phase: 'processing',
|
|
237
|
+
current: 0,
|
|
238
|
+
total: pages.length,
|
|
239
|
+
percent: 0,
|
|
240
|
+
}}
|
|
241
|
+
/>
|
|
242
|
+
),
|
|
243
|
+
{ id: toastId, duration: Infinity },
|
|
244
|
+
);
|
|
245
|
+
try {
|
|
246
|
+
await exportSlideAsPdf(slide, slideId, (p) => {
|
|
247
|
+
toast.custom(() => <PdfProgressToast progress={p} />, {
|
|
248
|
+
id: toastId,
|
|
249
|
+
duration: Infinity,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
} catch (err) {
|
|
253
|
+
console.error('[open-slide] pdf export failed', err);
|
|
254
|
+
toast.error('PDF export failed', { id: toastId, duration: 4000 });
|
|
255
|
+
} finally {
|
|
256
|
+
setExporting(false);
|
|
257
|
+
toast.dismiss(toastId);
|
|
258
|
+
}
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
<FileText />
|
|
262
|
+
Download PDF
|
|
263
|
+
</DropdownMenuItem>
|
|
215
264
|
</DropdownMenuContent>
|
|
216
265
|
</DropdownMenu>
|
|
217
266
|
)}
|
|
@@ -231,9 +280,17 @@ export function Slide() {
|
|
|
231
280
|
<ThumbnailRail pages={pages} current={index} onSelect={goTo} />
|
|
232
281
|
</div>
|
|
233
282
|
<main
|
|
283
|
+
ref={slideViewportRef}
|
|
234
284
|
data-inspector-root
|
|
235
285
|
className="relative min-h-0 min-w-0 flex-1 bg-background p-2 md:p-8"
|
|
236
286
|
>
|
|
287
|
+
<SlideWheelNavigation
|
|
288
|
+
targetRef={slideViewportRef}
|
|
289
|
+
onPrev={() => goTo(index - 1)}
|
|
290
|
+
onNext={() => goTo(index + 1)}
|
|
291
|
+
canPrev={index > 0}
|
|
292
|
+
canNext={index < pageCount - 1}
|
|
293
|
+
/>
|
|
237
294
|
<SlideCanvas>
|
|
238
295
|
<CurrentPage />
|
|
239
296
|
</SlideCanvas>
|
|
@@ -244,18 +301,46 @@ export function Slide() {
|
|
|
244
301
|
canNext={index < pageCount - 1}
|
|
245
302
|
/>
|
|
246
303
|
<InspectOverlay />
|
|
304
|
+
<SaveBar />
|
|
305
|
+
<CommentWidget />
|
|
247
306
|
<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">
|
|
248
307
|
{index + 1} / {pageCount}
|
|
249
308
|
</div>
|
|
250
309
|
</main>
|
|
310
|
+
<InspectorPanel />
|
|
251
311
|
</div>
|
|
252
|
-
|
|
253
|
-
<CommentWidget />
|
|
254
312
|
</div>
|
|
255
313
|
</InspectorProvider>
|
|
256
314
|
);
|
|
257
315
|
}
|
|
258
316
|
|
|
317
|
+
function SlideWheelNavigation({
|
|
318
|
+
targetRef,
|
|
319
|
+
onPrev,
|
|
320
|
+
onNext,
|
|
321
|
+
canPrev,
|
|
322
|
+
canNext,
|
|
323
|
+
}: {
|
|
324
|
+
targetRef: RefObject<HTMLElement>;
|
|
325
|
+
onPrev: () => void;
|
|
326
|
+
onNext: () => void;
|
|
327
|
+
canPrev: boolean;
|
|
328
|
+
canNext: boolean;
|
|
329
|
+
}) {
|
|
330
|
+
const { active } = useInspector();
|
|
331
|
+
|
|
332
|
+
useWheelPageNavigation({
|
|
333
|
+
ref: targetRef,
|
|
334
|
+
enabled: !active,
|
|
335
|
+
canPrev,
|
|
336
|
+
canNext,
|
|
337
|
+
onPrev,
|
|
338
|
+
onNext,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
259
344
|
function InlineTitleEditor({
|
|
260
345
|
title,
|
|
261
346
|
onSubmit,
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { createPortal } from 'react-dom';
|
|
3
|
-
import { useInspector } from './InspectorProvider';
|
|
4
|
-
|
|
5
|
-
const POPOVER_W = 320;
|
|
6
|
-
const POPOVER_H = 180;
|
|
7
|
-
|
|
8
|
-
export function CommentPopover() {
|
|
9
|
-
const { pending, setPending, add, cancel } = useInspector();
|
|
10
|
-
const [text, setText] = useState('');
|
|
11
|
-
const [submitting, setSubmitting] = useState(false);
|
|
12
|
-
const [error, setError] = useState<string | null>(null);
|
|
13
|
-
const taRef = useRef<HTMLTextAreaElement>(null);
|
|
14
|
-
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
taRef.current?.focus();
|
|
17
|
-
}, []);
|
|
18
|
-
|
|
19
|
-
if (!pending) return null;
|
|
20
|
-
|
|
21
|
-
const left = clamp(pending.clickX + 12, 8, window.innerWidth - POPOVER_W - 8);
|
|
22
|
-
const top = clamp(pending.clickY + 12, 8, window.innerHeight - POPOVER_H - 8);
|
|
23
|
-
|
|
24
|
-
const onSubmit = async () => {
|
|
25
|
-
const trimmed = text.trim();
|
|
26
|
-
if (!trimmed) return;
|
|
27
|
-
setSubmitting(true);
|
|
28
|
-
try {
|
|
29
|
-
await add(pending.line, pending.column, trimmed);
|
|
30
|
-
setPending(null);
|
|
31
|
-
} catch (e) {
|
|
32
|
-
setError(String((e as Error).message ?? e));
|
|
33
|
-
setSubmitting(false);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
return createPortal(
|
|
38
|
-
<div
|
|
39
|
-
data-inspector-ui
|
|
40
|
-
className="fixed z-50 rounded-md border bg-card p-3 shadow-xl"
|
|
41
|
-
style={{ left, top, width: POPOVER_W }}
|
|
42
|
-
>
|
|
43
|
-
<div className="mb-2 flex items-center justify-between">
|
|
44
|
-
<span className="text-xs font-medium text-muted-foreground">
|
|
45
|
-
Line {pending.line} · Comment
|
|
46
|
-
</span>
|
|
47
|
-
<button
|
|
48
|
-
type="button"
|
|
49
|
-
className="text-xs text-muted-foreground hover:text-foreground"
|
|
50
|
-
onClick={cancel}
|
|
51
|
-
>
|
|
52
|
-
✕
|
|
53
|
-
</button>
|
|
54
|
-
</div>
|
|
55
|
-
<textarea
|
|
56
|
-
ref={taRef}
|
|
57
|
-
value={text}
|
|
58
|
-
onChange={(e) => setText(e.target.value)}
|
|
59
|
-
placeholder="Describe the change…"
|
|
60
|
-
className="h-20 w-full resize-none rounded border bg-background p-2 text-sm outline-none focus:ring-2 focus:ring-primary/40"
|
|
61
|
-
onKeyDown={(e) => {
|
|
62
|
-
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
63
|
-
e.preventDefault();
|
|
64
|
-
onSubmit();
|
|
65
|
-
}
|
|
66
|
-
}}
|
|
67
|
-
/>
|
|
68
|
-
{error && <p className="mt-1 text-xs text-red-600">{error}</p>}
|
|
69
|
-
<div className="mt-2 flex items-center justify-end gap-2">
|
|
70
|
-
<button
|
|
71
|
-
type="button"
|
|
72
|
-
onClick={cancel}
|
|
73
|
-
className="rounded border px-2 py-1 text-xs hover:bg-muted"
|
|
74
|
-
>
|
|
75
|
-
Cancel
|
|
76
|
-
</button>
|
|
77
|
-
<button
|
|
78
|
-
type="button"
|
|
79
|
-
disabled={submitting || !text.trim()}
|
|
80
|
-
onClick={onSubmit}
|
|
81
|
-
className="rounded bg-primary px-3 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50"
|
|
82
|
-
>
|
|
83
|
-
{submitting ? 'Saving…' : 'Submit'}
|
|
84
|
-
</button>
|
|
85
|
-
</div>
|
|
86
|
-
<p className="mt-2 text-[10px] text-muted-foreground">⌘/Ctrl + Enter to submit</p>
|
|
87
|
-
</div>,
|
|
88
|
-
document.body,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function clamp(n: number, lo: number, hi: number): number {
|
|
93
|
-
return Math.max(lo, Math.min(hi, n));
|
|
94
|
-
}
|