@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.
Files changed (32) hide show
  1. package/dist/{build-cUKUY4bh.js → build-pqF4W1Yi.js} +1 -1
  2. package/dist/cli/bin.js +3 -3
  3. package/dist/{config-DOcMmFJ7.js → config-CtwxMYv9.js} +375 -45
  4. package/dist/{dev-Brzmgu64.js → dev-CJX97uiy.js} +1 -1
  5. package/dist/{preview-Bf8iFXA-.js → preview-IuLPcL5y.js} +1 -1
  6. package/dist/vite/index.js +1 -1
  7. package/package.json +3 -1
  8. package/src/app/App.tsx +2 -0
  9. package/src/app/components/PdfProgressToast.tsx +23 -0
  10. package/src/app/components/Player.tsx +18 -3
  11. package/src/app/components/inspector/CommentWidget.tsx +1 -1
  12. package/src/app/components/inspector/InspectOverlay.tsx +81 -41
  13. package/src/app/components/inspector/InspectorPanel.tsx +805 -0
  14. package/src/app/components/inspector/InspectorProvider.tsx +199 -13
  15. package/src/app/components/inspector/SaveBar.tsx +77 -0
  16. package/src/app/components/ui/input.tsx +21 -0
  17. package/src/app/components/ui/label.tsx +24 -0
  18. package/src/app/components/ui/progress.tsx +31 -0
  19. package/src/app/components/ui/select.tsx +190 -0
  20. package/src/app/components/ui/slider.tsx +61 -0
  21. package/src/app/components/ui/sonner.tsx +38 -0
  22. package/src/app/components/ui/textarea.tsx +18 -0
  23. package/src/app/components/ui/toggle-group.tsx +83 -0
  24. package/src/app/components/ui/toggle.tsx +45 -0
  25. package/src/app/components/ui/tooltip.tsx +55 -0
  26. package/src/app/lib/export-pdf.ts +197 -0
  27. package/src/app/lib/inspector/fiber.ts +40 -5
  28. package/src/app/lib/inspector/useEditor.ts +61 -0
  29. package/src/app/lib/print-ready.ts +58 -0
  30. package/src/app/lib/useWheelPageNavigation.ts +92 -0
  31. package/src/app/routes/Slide.tsx +91 -6
  32. package/src/app/components/inspector/CommentPopover.tsx +0 -94
@@ -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 { InspectorProvider, InspectToggleButton } from '@/components/inspector/InspectorProvider';
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
- }