@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,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