@open-slide/core 0.0.8 → 0.0.10

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 (34) hide show
  1. package/dist/{build-CXY2DSzy.js → build-DHiRlpjn.js} +1 -1
  2. package/dist/cli/bin.js +3 -3
  3. package/dist/{config-BYTf0qVz.js → config-LZM903FE.js} +742 -44
  4. package/dist/{dev-BxCKugi3.js → dev-B3JzCYn7.js} +1 -1
  5. package/dist/{preview-C1F-rHfx.js → preview-UikovHEt.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/AssetView.tsx +846 -0
  10. package/src/app/components/ClickNavZones.tsx +2 -2
  11. package/src/app/components/PdfProgressToast.tsx +23 -0
  12. package/src/app/components/ThumbnailRail.tsx +2 -2
  13. package/src/app/components/inspector/CommentWidget.tsx +1 -1
  14. package/src/app/components/inspector/InspectOverlay.tsx +81 -41
  15. package/src/app/components/inspector/InspectorPanel.tsx +948 -0
  16. package/src/app/components/inspector/InspectorProvider.tsx +229 -13
  17. package/src/app/components/inspector/SaveBar.tsx +77 -0
  18. package/src/app/components/ui/input.tsx +21 -0
  19. package/src/app/components/ui/label.tsx +24 -0
  20. package/src/app/components/ui/progress.tsx +31 -0
  21. package/src/app/components/ui/select.tsx +190 -0
  22. package/src/app/components/ui/slider.tsx +61 -0
  23. package/src/app/components/ui/sonner.tsx +38 -0
  24. package/src/app/components/ui/textarea.tsx +18 -0
  25. package/src/app/components/ui/toggle-group.tsx +83 -0
  26. package/src/app/components/ui/toggle.tsx +45 -0
  27. package/src/app/components/ui/tooltip.tsx +55 -0
  28. package/src/app/lib/assets.ts +166 -0
  29. package/src/app/lib/export-pdf.ts +194 -0
  30. package/src/app/lib/inspector/fiber.ts +40 -5
  31. package/src/app/lib/inspector/useEditor.ts +62 -0
  32. package/src/app/lib/print-ready.ts +58 -0
  33. package/src/app/routes/Slide.tsx +140 -51
  34. package/src/app/components/inspector/CommentPopover.tsx +0 -94
@@ -19,7 +19,7 @@ export function ClickNavZones({ onPrev, onNext, canPrev, canNext }: Props) {
19
19
  onClick={onPrev}
20
20
  disabled={!canPrev}
21
21
  data-inspector-ui
22
- className="absolute inset-y-0 left-0 z-20 w-[18%] min-w-12"
22
+ className="absolute inset-y-0 left-0 z-20 w-[18%] min-w-12 md:hidden"
23
23
  />
24
24
  <button
25
25
  type="button"
@@ -27,7 +27,7 @@ export function ClickNavZones({ onPrev, onNext, canPrev, canNext }: Props) {
27
27
  onClick={onNext}
28
28
  disabled={!canNext}
29
29
  data-inspector-ui
30
- className="absolute inset-y-0 right-0 z-20 w-[18%] min-w-12"
30
+ className="absolute inset-y-0 right-0 z-20 w-[18%] min-w-12 md:hidden"
31
31
  />
32
32
  </>
33
33
  );
@@ -0,0 +1,23 @@
1
+ import { Loader2 } from 'lucide-react';
2
+ import type { PdfExportProgress } from '../lib/export-pdf';
3
+ import { Progress } from './ui/progress';
4
+
5
+ export function PdfProgressToast({ progress }: { progress: PdfExportProgress }) {
6
+ const text =
7
+ progress.phase === 'processing'
8
+ ? `Processing slide ${progress.current} / ${progress.total}`
9
+ : progress.phase === 'printing'
10
+ ? 'Opening print dialog…'
11
+ : 'Done';
12
+
13
+ return (
14
+ <div className="flex w-80 items-start gap-3 rounded-md border bg-popover px-4 py-3 text-popover-foreground shadow-lg">
15
+ <Loader2 className="mt-0.5 size-4 shrink-0 animate-spin text-primary" />
16
+ <div className="min-w-0 flex-1">
17
+ <p className="text-sm font-medium">Exporting PDF</p>
18
+ <p className="truncate text-xs text-muted-foreground">{text}</p>
19
+ <Progress value={Math.round(progress.percent)} className="mt-2 h-1.5" />
20
+ </div>
21
+ </div>
22
+ );
23
+ }
@@ -1,9 +1,9 @@
1
1
  import { useEffect, useRef } from 'react';
2
- import { cn } from '@/lib/utils';
3
2
  import { ScrollArea } from '@/components/ui/scroll-area';
3
+ import { cn } from '@/lib/utils';
4
4
  import type { Page } from '../lib/sdk';
5
+ import { CANVAS_HEIGHT, CANVAS_WIDTH } from '../lib/sdk';
5
6
  import { SlideCanvas } from './SlideCanvas';
6
- import { CANVAS_WIDTH, CANVAS_HEIGHT } from '../lib/sdk';
7
7
 
8
8
  type Props = {
9
9
  pages: Page[];
@@ -8,7 +8,7 @@ export function CommentWidget() {
8
8
  const count = comments.length;
9
9
 
10
10
  return (
11
- <div data-inspector-ui className="fixed right-4 bottom-4 z-40 flex flex-col items-end gap-2">
11
+ <div data-inspector-ui className="absolute right-4 bottom-4 z-20 flex flex-col items-end gap-2">
12
12
  {open && (
13
13
  <div className="w-80 rounded-md border bg-card shadow-xl animate-in fade-in-0 slide-in-from-bottom-2 duration-200">
14
14
  <div className="flex items-center justify-between border-b px-3 py-2">
@@ -1,12 +1,16 @@
1
- import { useEffect, useRef, useState } from 'react';
1
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
2
2
  import { findSlideSource, type SlideSourceHit } from '@/lib/inspector/fiber';
3
- import { CommentPopover } from './CommentPopover';
4
3
  import { useInspector } from './InspectorProvider';
5
4
 
6
5
  type Highlight = { rect: DOMRect; hit: SlideSourceHit };
7
6
 
7
+ type RelRect = { left: number; top: number; width: number; height: number };
8
+
9
+ const FRAME_FADE_MS = 150;
10
+ const FRAME_MORPH_MS = 180;
11
+
8
12
  export function InspectOverlay() {
9
- const { active, slideId, pending, setPending, cancel } = useInspector();
13
+ const { active, slideId, selected, setSelected, cancel } = useInspector();
10
14
  const overlayRef = useRef<HTMLDivElement>(null);
11
15
  const [hover, setHover] = useState<Highlight | null>(null);
12
16
 
@@ -25,32 +29,23 @@ export function InspectOverlay() {
25
29
  };
26
30
 
27
31
  const onMove = (e: PointerEvent) => {
28
- if (pending) return;
29
32
  const el = pickElement(e.clientX, e.clientY);
30
33
  if (!el) return setHover(null);
31
- const hit = findSlideSource(el, slideId);
34
+ const hit = findSlideSource(el, slideId, { hostOnly: true });
32
35
  if (!hit) return setHover(null);
33
36
  setHover({ rect: hit.anchor.getBoundingClientRect(), hit });
34
37
  };
35
38
 
36
39
  const onClick = (e: MouseEvent) => {
37
- if (pending) return;
38
40
  if (e.target instanceof Element && e.target.closest('[data-inspector-ui]')) return;
39
41
  const el = pickElement(e.clientX, e.clientY);
40
42
  if (!el) return;
41
- const hit = findSlideSource(el, slideId);
43
+ const hit = findSlideSource(el, slideId, { hostOnly: true });
42
44
  if (!hit) return;
43
45
  e.preventDefault();
44
46
  e.stopPropagation();
45
- const anchorRect = hit.anchor.getBoundingClientRect();
46
- setPending({
47
- line: hit.line,
48
- column: hit.column,
49
- anchorRect,
50
- clickX: e.clientX,
51
- clickY: e.clientY,
52
- });
53
- setHover({ rect: anchorRect, hit });
47
+ setSelected({ line: hit.line, column: hit.column, anchor: hit.anchor });
48
+ setHover({ rect: hit.anchor.getBoundingClientRect(), hit });
54
49
  };
55
50
 
56
51
  window.addEventListener('pointermove', onMove, true);
@@ -61,36 +56,81 @@ export function InspectOverlay() {
61
56
  window.removeEventListener('click', onClick, true);
62
57
  window.removeEventListener('keydown', onKey, true);
63
58
  };
64
- }, [active, slideId, pending, setPending, cancel]);
59
+ }, [active, slideId, setSelected, cancel]);
65
60
 
66
- if (!active) return null;
61
+ return (
62
+ <FrameOverlay
63
+ active={active}
64
+ overlayRef={overlayRef}
65
+ // Pin to the selection so the highlight tracks what the panel
66
+ // is editing even after the cursor moves away.
67
+ targetRect={selected?.anchor.getBoundingClientRect() ?? hover?.rect ?? null}
68
+ />
69
+ );
70
+ }
67
71
 
72
+ function FrameOverlay({
73
+ active,
74
+ overlayRef,
75
+ targetRect,
76
+ }: {
77
+ active: boolean;
78
+ overlayRef: React.RefObject<HTMLDivElement>;
79
+ targetRect: DOMRect | null;
80
+ }) {
68
81
  const overlayRect = overlayRef.current?.getBoundingClientRect();
69
- const show = hover && overlayRect;
82
+ const visible = !!(active && targetRect && overlayRect);
83
+
84
+ // Hold the last rect so the frame stays put during fade-out, when
85
+ // `targetRect` has already gone null.
86
+ const lastRectRef = useRef<RelRect | null>(null);
87
+ if (visible && targetRect && overlayRect) {
88
+ lastRectRef.current = {
89
+ left: targetRect.left - overlayRect.left,
90
+ top: targetRect.top - overlayRect.top,
91
+ width: targetRect.width,
92
+ height: targetRect.height,
93
+ };
94
+ }
95
+
96
+ // First render after appearing: snap to the new rect (no transition).
97
+ // Subsequent rect changes in the same visible session: animate.
98
+ const [morph, setMorph] = useState(false);
99
+ useLayoutEffect(() => {
100
+ if (visible) {
101
+ setMorph(true);
102
+ return;
103
+ }
104
+ const t = setTimeout(() => setMorph(false), FRAME_FADE_MS);
105
+ return () => clearTimeout(t);
106
+ }, [visible]);
107
+
108
+ if (!active) return null;
109
+ const rect = lastRectRef.current;
110
+ const transition = morph
111
+ ? `left ${FRAME_MORPH_MS}ms ease-out, top ${FRAME_MORPH_MS}ms ease-out, ` +
112
+ `width ${FRAME_MORPH_MS}ms ease-out, height ${FRAME_MORPH_MS}ms ease-out, ` +
113
+ `opacity ${FRAME_FADE_MS}ms ease-out`
114
+ : `opacity ${FRAME_FADE_MS}ms ease-out`;
70
115
 
71
116
  return (
72
- <>
73
- <div
74
- ref={overlayRef}
75
- className="pointer-events-none absolute inset-0 z-30"
76
- style={{ cursor: 'crosshair' }}
77
- >
78
- {show && (
79
- <div
80
- className="absolute"
81
- style={{
82
- left: hover.rect.left - overlayRect.left,
83
- top: hover.rect.top - overlayRect.top,
84
- width: hover.rect.width,
85
- height: hover.rect.height,
86
- outline: '2px solid #3b82f6',
87
- background: 'rgba(59,130,246,0.1)',
88
- }}
89
- />
90
- )}
91
- </div>
92
- {pending && <CommentPopover />}
93
- </>
117
+ <div ref={overlayRef} data-inspector-ui className="pointer-events-none absolute inset-0 z-30">
118
+ {rect && (
119
+ <div
120
+ className="absolute"
121
+ style={{
122
+ left: rect.left,
123
+ top: rect.top,
124
+ width: rect.width,
125
+ height: rect.height,
126
+ opacity: visible ? 1 : 0,
127
+ transition,
128
+ outline: '2px solid #3b82f6',
129
+ background: 'rgba(59,130,246,0.1)',
130
+ }}
131
+ />
132
+ )}
133
+ </div>
94
134
  );
95
135
  }
96
136