@arbidocs/blocks 0.5.123 → 0.5.124
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/{ClippingMediaViewer-IEIBT74X.cjs → ClippingMediaViewer-ZFEX5A3U.cjs} +11 -10
- package/dist/ClippingMediaViewer-ZFEX5A3U.cjs.map +1 -0
- package/dist/{ClippingPdfViewer-OGYLMVCE.cjs → ClippingPdfViewer-3LRTEPHV.cjs} +45 -11
- package/dist/ClippingPdfViewer-3LRTEPHV.cjs.map +1 -0
- package/dist/ConnectedNativeDocxComparison-QKHCYD6W.cjs +1 -1
- package/dist/{ConnectedPresentation-5DOJ35XL.cjs → ConnectedPresentation-OV2KBOLK.cjs} +27 -153
- package/dist/ConnectedPresentation-OV2KBOLK.cjs.map +1 -0
- package/dist/{ConnectedSlidesTab-7YMEDGUU.cjs → ConnectedSlidesTab-LUAGZ6FD.cjs} +29 -27
- package/dist/ConnectedSlidesTab-LUAGZ6FD.cjs.map +1 -0
- package/dist/ConnectedStreamedWordArtifact-AG62232A.cjs +1 -1
- package/dist/{FilesViewerDialog-2QCCRWF2.cjs → FilesViewerDialog-JWKNJLWG.cjs} +12 -9
- package/dist/FilesViewerDialog-JWKNJLWG.cjs.map +1 -0
- package/dist/SpotlightStrip-ED22UYTM.cjs +398 -0
- package/dist/SpotlightStrip-ED22UYTM.cjs.map +1 -0
- package/dist/agents/index.cjs +176 -176
- package/dist/{chunk-46P7IDBX.cjs → chunk-3IH55CDE.cjs} +3 -3
- package/dist/chunk-3IH55CDE.cjs.map +1 -0
- package/dist/chunk-3ZZZO4F7.cjs +145 -0
- package/dist/chunk-3ZZZO4F7.cjs.map +1 -0
- package/dist/{chunk-WFMWKPGY.cjs → chunk-ADPRIBSH.cjs} +9 -33
- package/dist/chunk-ADPRIBSH.cjs.map +1 -0
- package/dist/{chunk-JD2HX2BR.cjs → chunk-BX6G2ZCI.cjs} +58 -86
- package/dist/chunk-BX6G2ZCI.cjs.map +1 -0
- package/dist/{chunk-NQML7IUC.cjs → chunk-CQQQIZU4.cjs} +3 -3
- package/dist/{chunk-NQML7IUC.cjs.map → chunk-CQQQIZU4.cjs.map} +1 -1
- package/dist/{chunk-SNPK3R3O.cjs → chunk-EMRY62HS.cjs} +5 -5
- package/dist/{chunk-SNPK3R3O.cjs.map → chunk-EMRY62HS.cjs.map} +1 -1
- package/dist/{chunk-DWMWEALB.cjs → chunk-H236SNEV.cjs} +143 -1634
- package/dist/chunk-H236SNEV.cjs.map +1 -0
- package/dist/chunk-JITYC4OS.cjs +66 -0
- package/dist/chunk-JITYC4OS.cjs.map +1 -0
- package/dist/chunk-JY7UUZQG.cjs +1513 -0
- package/dist/chunk-JY7UUZQG.cjs.map +1 -0
- package/dist/chunk-MVXHTAJ4.cjs +111 -0
- package/dist/chunk-MVXHTAJ4.cjs.map +1 -0
- package/dist/{chunk-4TTX6ZNS.cjs → chunk-ORNLCBNE.cjs} +24 -2
- package/dist/chunk-ORNLCBNE.cjs.map +1 -0
- package/dist/{chunk-L4CIFLCU.cjs → chunk-QG2U2JGX.cjs} +4 -4
- package/dist/{chunk-L4CIFLCU.cjs.map → chunk-QG2U2JGX.cjs.map} +1 -1
- package/dist/chunk-WVZLAAIZ.cjs +31 -0
- package/dist/chunk-WVZLAAIZ.cjs.map +1 -0
- package/dist/{chunk-WV7BCGCK.cjs → chunk-XGM4VVNV.cjs} +19 -19
- package/dist/{chunk-WV7BCGCK.cjs.map → chunk-XGM4VVNV.cjs.map} +1 -1
- package/dist/chunk-ZZZITHGK.cjs +119 -0
- package/dist/chunk-ZZZITHGK.cjs.map +1 -0
- package/dist/connect/docx.cjs +1 -1
- package/dist/connect/index.cjs +175 -175
- package/dist/connect/viewer.cjs +10 -7
- package/dist/connection/presentation/ClippingMediaViewer.js +3 -3
- package/dist/connection/presentation/ClippingMediaViewer.js.map +2 -2
- package/dist/connection/presentation/ClippingPdfViewer.d.ts +4 -3
- package/dist/connection/presentation/ClippingPdfViewer.js +48 -9
- package/dist/connection/presentation/ClippingPdfViewer.js.map +2 -2
- package/dist/connection/presentation/SpotlightLayer.d.ts +5 -0
- package/dist/connection/presentation/SpotlightLayer.js +19 -0
- package/dist/connection/presentation/SpotlightLayer.js.map +7 -0
- package/dist/connection/presentation/SpotlightStrip.d.ts +10 -0
- package/dist/connection/presentation/SpotlightStrip.js +237 -0
- package/dist/connection/presentation/SpotlightStrip.js.map +7 -0
- package/dist/connection/presentation/useViewerClipping.d.ts +4 -3
- package/dist/connection/presentation/useViewerClipping.js +8 -4
- package/dist/connection/presentation/useViewerClipping.js.map +2 -2
- package/dist/connection/viewer/ConnectedDocumentViewer.js +1 -1
- package/dist/connection/viewer/ConnectedDocumentViewer.js.map +2 -2
- package/dist/connection/viewer/pdf/passageHighlights.d.ts +5 -0
- package/dist/connection/viewer/pdf/passageHighlights.js +14 -1
- package/dist/connection/viewer/pdf/passageHighlights.js.map +2 -2
- package/dist/connection/viewer/useCleanView.d.ts +3 -1
- package/dist/connection/viewer/useCleanView.js +6 -3
- package/dist/connection/viewer/useCleanView.js.map +2 -2
- package/dist/document-tools/DocumentToolButtons.js +1 -1
- package/dist/document-tools/DocumentToolButtons.js.map +1 -1
- package/dist/document-tools/index.cjs +13 -13
- package/dist/embed/index.cjs +2 -2
- package/dist/index.cjs +239 -239
- package/dist/presentation/ClipCaptureOverlay.d.ts +7 -3
- package/dist/presentation/ClipCaptureOverlay.js +43 -14
- package/dist/presentation/ClipCaptureOverlay.js.map +2 -2
- package/dist/presentation/PresentControls.d.ts +8 -0
- package/dist/presentation/PresentControls.js +1 -0
- package/dist/presentation/PresentControls.js.map +2 -2
- package/dist/presentation/PresentationStage.d.ts +10 -2
- package/dist/presentation/PresentationStage.js +6 -3
- package/dist/presentation/PresentationStage.js.map +2 -2
- package/dist/presentation/PresenterView.js +1 -22
- package/dist/presentation/PresenterView.js.map +2 -2
- package/dist/presentation/SpotlightView.d.ts +19 -0
- package/dist/presentation/SpotlightView.js +180 -0
- package/dist/presentation/SpotlightView.js.map +7 -0
- package/dist/presentation/passage-clip.d.ts +17 -0
- package/dist/presentation/passage-clip.js +25 -0
- package/dist/presentation/passage-clip.js.map +7 -0
- package/dist/presentation/spotlight.d.ts +58 -0
- package/dist/presentation/spotlight.js +110 -0
- package/dist/presentation/spotlight.js.map +7 -0
- package/dist/presentation/store.d.ts +5 -0
- package/dist/presentation/store.js +19 -0
- package/dist/presentation/store.js.map +2 -2
- package/dist/presentation/usePointerActive.d.ts +2 -0
- package/dist/presentation/usePointerActive.js +28 -0
- package/dist/presentation/usePointerActive.js.map +7 -0
- package/dist/styles.css +1 -1
- package/dist/workflow-canvas/index.cjs +12 -12
- package/node_modules/@arbidocs/coedit/package.json +1 -1
- package/package.json +5 -5
- package/dist/ClippingMediaViewer-IEIBT74X.cjs.map +0 -1
- package/dist/ClippingPdfViewer-OGYLMVCE.cjs.map +0 -1
- package/dist/ConnectedPresentation-5DOJ35XL.cjs.map +0 -1
- package/dist/ConnectedSlidesTab-7YMEDGUU.cjs.map +0 -1
- package/dist/FilesViewerDialog-2QCCRWF2.cjs.map +0 -1
- package/dist/chunk-46P7IDBX.cjs.map +0 -1
- package/dist/chunk-4TTX6ZNS.cjs.map +0 -1
- package/dist/chunk-DWMWEALB.cjs.map +0 -1
- package/dist/chunk-JD2HX2BR.cjs.map +0 -1
- package/dist/chunk-WFMWKPGY.cjs.map +0 -1
- package/dist/chunk-XK4TVJKD.cjs +0 -73
- package/dist/chunk-XK4TVJKD.cjs.map +0 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Check, ChevronLeft, ChevronRight, Presentation, X } from "lucide-react";
|
|
5
|
+
import { cn } from "@arbidocs/react/ui";
|
|
6
|
+
import { Control } from "./PresentControls.js";
|
|
7
|
+
import { PresentationStage } from "./PresentationStage.js";
|
|
8
|
+
import { spotlightSlide, spotlightSteps, useSpotlight } from "./spotlight.js";
|
|
9
|
+
import { usePresentation } from "./store.js";
|
|
10
|
+
import { usePointerActive } from "./usePointerActive.js";
|
|
11
|
+
const SPOTLIGHT_MS = 300;
|
|
12
|
+
const EASING = "cubic-bezier(0.2, 0, 0, 1)";
|
|
13
|
+
const prefersReducedMotion = (view) => !!view?.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
14
|
+
function SpotlightView({ fromRect }) {
|
|
15
|
+
const clips = useSpotlight((s) => s.clips);
|
|
16
|
+
const note = useSpotlight((s) => s.note);
|
|
17
|
+
const step = useSpotlight((s) => s.step);
|
|
18
|
+
const captions = usePresentation((s) => s.captions);
|
|
19
|
+
const theme = usePresentation((s) => s.theme);
|
|
20
|
+
const steps = useMemo(() => spotlightSteps(clips), [clips]);
|
|
21
|
+
const ids = steps[step] ?? [];
|
|
22
|
+
const stepKey = ids.join();
|
|
23
|
+
const slide = useMemo(
|
|
24
|
+
() => spotlightSlide(clips, ids, note, { captions, theme }),
|
|
25
|
+
// `ids` is new each render; its key says when it changed.
|
|
26
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
27
|
+
[clips, stepKey, note, captions, theme]
|
|
28
|
+
);
|
|
29
|
+
const show = useMemo(() => ({ slide, clips }), [slide, clips]);
|
|
30
|
+
const [view, setView] = useState(null);
|
|
31
|
+
const root = useRef(null);
|
|
32
|
+
const rootRef = useCallback((node) => {
|
|
33
|
+
root.current = node;
|
|
34
|
+
if (node) setView(node.ownerDocument.defaultView);
|
|
35
|
+
}, []);
|
|
36
|
+
const active = usePointerActive(view);
|
|
37
|
+
const [kept, setKept] = useState(null);
|
|
38
|
+
const last = useRef(/* @__PURE__ */ new Map());
|
|
39
|
+
const fromRectRef = useRef(fromRect);
|
|
40
|
+
fromRectRef.current = fromRect;
|
|
41
|
+
useLayoutEffect(() => {
|
|
42
|
+
const node = root.current;
|
|
43
|
+
if (!node) return;
|
|
44
|
+
const reduce = prefersReducedMotion(node.ownerDocument.defaultView);
|
|
45
|
+
const next = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const id of stepKey.split(",").filter(Boolean)) {
|
|
47
|
+
const element = node.querySelector(`[data-testid="stage-item-${id}"]`);
|
|
48
|
+
const clip = useSpotlight.getState().clips.find((entry) => entry.id === id);
|
|
49
|
+
if (!element || !clip) continue;
|
|
50
|
+
const to = element.getBoundingClientRect();
|
|
51
|
+
next.set(id, to);
|
|
52
|
+
const from = last.current.get(id) ?? fromRectRef.current(clip);
|
|
53
|
+
if (reduce || !from || !to.width || !to.height || typeof element.animate !== "function") {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
element.animate(
|
|
57
|
+
[
|
|
58
|
+
{
|
|
59
|
+
transform: `translate(${from.left - to.left}px, ${from.top - to.top}px) scale(${from.width / to.width}, ${from.height / to.height})`
|
|
60
|
+
},
|
|
61
|
+
{ transform: "none" }
|
|
62
|
+
],
|
|
63
|
+
{ duration: SPOTLIGHT_MS, easing: EASING }
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
last.current = next;
|
|
67
|
+
}, [stepKey]);
|
|
68
|
+
const backdrop = useRef(null);
|
|
69
|
+
useLayoutEffect(() => {
|
|
70
|
+
const element = backdrop.current;
|
|
71
|
+
if (!element || typeof element.animate !== "function") return;
|
|
72
|
+
if (prefersReducedMotion(element.ownerDocument.defaultView)) return;
|
|
73
|
+
element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: SPOTLIGHT_MS, easing: EASING });
|
|
74
|
+
}, []);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!view) return;
|
|
77
|
+
const onKeyDown = (event) => {
|
|
78
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
79
|
+
const target = event.target;
|
|
80
|
+
if (target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;
|
|
81
|
+
const state = useSpotlight.getState();
|
|
82
|
+
if (event.key === "Escape") state.hide();
|
|
83
|
+
else if (event.key === "ArrowRight" || event.key === "PageDown") state.goTo(state.step + 1);
|
|
84
|
+
else if (event.key === "ArrowLeft" || event.key === "PageUp") state.goTo(state.step - 1);
|
|
85
|
+
else return;
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
};
|
|
89
|
+
view.addEventListener("keydown", onKeyDown, true);
|
|
90
|
+
return () => view.removeEventListener("keydown", onKeyDown, true);
|
|
91
|
+
}, [view]);
|
|
92
|
+
const actions = useSpotlight.getState();
|
|
93
|
+
const isKept = kept === `${stepKey}|${note}`;
|
|
94
|
+
return /* @__PURE__ */ jsxs(
|
|
95
|
+
"div",
|
|
96
|
+
{
|
|
97
|
+
ref: rootRef,
|
|
98
|
+
className: "absolute inset-0 z-30 select-none",
|
|
99
|
+
style: { cursor: active ? "default" : "none" },
|
|
100
|
+
role: "dialog",
|
|
101
|
+
"aria-label": "Spotlight",
|
|
102
|
+
"data-testid": "spotlight",
|
|
103
|
+
children: [
|
|
104
|
+
/* @__PURE__ */ jsx("div", { ref: backdrop, className: "absolute inset-0 bg-black/85" }),
|
|
105
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 p-4 pb-16", children: /* @__PURE__ */ jsx(PresentationStage, { show, className: "bg-transparent" }) }),
|
|
106
|
+
/* @__PURE__ */ jsxs(
|
|
107
|
+
"div",
|
|
108
|
+
{
|
|
109
|
+
className: cn(
|
|
110
|
+
"absolute bottom-3 left-1/2 z-10 flex -translate-x-1/2 items-center gap-0.5 rounded-full bg-black/60 p-1 text-white shadow-lg backdrop-blur transition-opacity duration-300",
|
|
111
|
+
active ? "opacity-100" : "pointer-events-none opacity-0"
|
|
112
|
+
),
|
|
113
|
+
role: "toolbar",
|
|
114
|
+
"aria-label": "Spotlight",
|
|
115
|
+
"data-testid": "spotlight-controls",
|
|
116
|
+
children: [
|
|
117
|
+
/* @__PURE__ */ jsx(
|
|
118
|
+
Control,
|
|
119
|
+
{
|
|
120
|
+
label: "Previous (\u2190)",
|
|
121
|
+
onClick: () => actions.goTo(step - 1),
|
|
122
|
+
disabled: step === 0,
|
|
123
|
+
testId: "spotlight-previous",
|
|
124
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4" })
|
|
125
|
+
}
|
|
126
|
+
),
|
|
127
|
+
/* @__PURE__ */ jsxs(
|
|
128
|
+
"span",
|
|
129
|
+
{
|
|
130
|
+
className: "min-w-12 text-center text-xs tabular-nums text-white/85",
|
|
131
|
+
"data-testid": "spotlight-position",
|
|
132
|
+
children: [
|
|
133
|
+
step + 1,
|
|
134
|
+
" / ",
|
|
135
|
+
steps.length
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
),
|
|
139
|
+
/* @__PURE__ */ jsx(
|
|
140
|
+
Control,
|
|
141
|
+
{
|
|
142
|
+
label: "Next (\u2192)",
|
|
143
|
+
onClick: () => actions.goTo(step + 1),
|
|
144
|
+
disabled: step >= steps.length - 1,
|
|
145
|
+
testId: "spotlight-next",
|
|
146
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4" })
|
|
147
|
+
}
|
|
148
|
+
),
|
|
149
|
+
/* @__PURE__ */ jsx("div", { className: "mx-1 h-4 w-px bg-white/25" }),
|
|
150
|
+
/* @__PURE__ */ jsxs(
|
|
151
|
+
"button",
|
|
152
|
+
{
|
|
153
|
+
type: "button",
|
|
154
|
+
className: "inline-flex h-8 items-center gap-1.5 whitespace-nowrap rounded-full px-3 text-xs text-white/85 hover:bg-white/15 hover:text-white disabled:opacity-60 disabled:hover:bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/60 pointer-coarse:h-11",
|
|
155
|
+
disabled: isKept,
|
|
156
|
+
onClick: () => {
|
|
157
|
+
actions.keep();
|
|
158
|
+
setKept(`${stepKey}|${note}`);
|
|
159
|
+
},
|
|
160
|
+
title: "Add this as a slide of the open presentation",
|
|
161
|
+
"data-testid": "spotlight-keep",
|
|
162
|
+
children: [
|
|
163
|
+
isKept ? /* @__PURE__ */ jsx(Check, { className: "size-3.5" }) : /* @__PURE__ */ jsx(Presentation, { className: "size-3.5" }),
|
|
164
|
+
isKept ? "Kept as slide" : "Keep as slide"
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
),
|
|
168
|
+
/* @__PURE__ */ jsx(Control, { label: "Back to the document (Esc)", onClick: actions.hide, testId: "spotlight-close", children: /* @__PURE__ */ jsx(X, { className: "size-4" }) })
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
export {
|
|
177
|
+
SPOTLIGHT_MS,
|
|
178
|
+
SpotlightView
|
|
179
|
+
};
|
|
180
|
+
//# sourceMappingURL=SpotlightView.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/presentation/SpotlightView.tsx"],
|
|
4
|
+
"sourcesContent": ["/**\n * The spotlight on screen: the document dims and the gathered clips fly from where they are on\n * their pages into a slide over it, in the same window (a call sharing it goes on sharing it). It is\n * the presentation's own stage (`PresentationStage`), so it looks as a slide of the deck would.\n *\n * \u2190 \u2192 step through the clips together and each alone, Esc goes back to the document. Each move is\n * a FLIP: a clip is drawn where it ends and animated from where it was (its page, or its place on\n * the previous step) by a transform, about 300 ms; with reduced motion it simply appears.\n */\n\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'\nimport { Check, ChevronLeft, ChevronRight, Presentation, X } from 'lucide-react'\nimport { cn } from '@arbidocs/react/ui'\nimport type { Clip } from './model'\nimport { Control } from './PresentControls'\nimport { PresentationStage } from './PresentationStage'\nimport { spotlightSlide, spotlightSteps, useSpotlight } from './spotlight'\nimport { usePresentation } from './store'\nimport { usePointerActive } from './usePointerActive'\n\n/** How long a clip takes to fly into place. */\nexport const SPOTLIGHT_MS = 300\nconst EASING = 'cubic-bezier(0.2, 0, 0, 1)'\n\n/** A box on screen, in CSS pixels. */\nexport type ScreenRect = Pick<DOMRect, 'left' | 'top' | 'width' | 'height'>\n\nexport interface SpotlightViewProps {\n /** Where a clip is on screen before the spotlight (its passage on the page), if it shows. */\n fromRect: (clip: Clip) => ScreenRect | null\n}\n\nconst prefersReducedMotion = (view: Window | null) =>\n !!view?.matchMedia?.('(prefers-reduced-motion: reduce)').matches\n\nexport function SpotlightView({ fromRect }: SpotlightViewProps) {\n const clips = useSpotlight((s) => s.clips)\n const note = useSpotlight((s) => s.note)\n const step = useSpotlight((s) => s.step)\n const captions = usePresentation((s) => s.captions)\n const theme = usePresentation((s) => s.theme)\n const steps = useMemo(() => spotlightSteps(clips), [clips])\n const ids = steps[step] ?? []\n const stepKey = ids.join()\n const slide = useMemo(\n () => spotlightSlide(clips, ids, note, { captions, theme }),\n // `ids` is new each render; its key says when it changed.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [clips, stepKey, note, captions, theme]\n )\n const show = useMemo(() => ({ slide, clips }), [slide, clips])\n\n const [view, setView] = useState<Window | null>(null)\n const root = useRef<HTMLDivElement | null>(null)\n const rootRef = useCallback((node: HTMLDivElement | null) => {\n root.current = node\n if (node) setView(node.ownerDocument.defaultView)\n }, [])\n const active = usePointerActive(view)\n const [kept, setKept] = useState<string | null>(null)\n\n // FLIP: each clip from where it was (on its page, or on the step before) to where it is now.\n const last = useRef(new Map<string, ScreenRect>())\n const fromRectRef = useRef(fromRect)\n fromRectRef.current = fromRect\n useLayoutEffect(() => {\n const node = root.current\n if (!node) return\n const reduce = prefersReducedMotion(node.ownerDocument.defaultView)\n const next = new Map<string, ScreenRect>()\n for (const id of stepKey.split(',').filter(Boolean)) {\n const element = node.querySelector<HTMLElement>(`[data-testid=\"stage-item-${id}\"]`)\n const clip = useSpotlight.getState().clips.find((entry) => entry.id === id)\n if (!element || !clip) continue\n const to = element.getBoundingClientRect()\n next.set(id, to)\n const from = last.current.get(id) ?? fromRectRef.current(clip)\n if (reduce || !from || !to.width || !to.height || typeof element.animate !== 'function') {\n continue\n }\n element.animate(\n [\n {\n transform: `translate(${from.left - to.left}px, ${from.top - to.top}px) scale(${\n from.width / to.width\n }, ${from.height / to.height})`,\n },\n { transform: 'none' },\n ],\n { duration: SPOTLIGHT_MS, easing: EASING }\n )\n }\n last.current = next\n }, [stepKey])\n\n // The document dims as the clips arrive.\n const backdrop = useRef<HTMLDivElement>(null)\n useLayoutEffect(() => {\n const element = backdrop.current\n if (!element || typeof element.animate !== 'function') return\n if (prefersReducedMotion(element.ownerDocument.defaultView)) return\n element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: SPOTLIGHT_MS, easing: EASING })\n }, [])\n\n // Esc back to the document, \u2190 \u2192 through the steps; taken before the viewer's own keys.\n useEffect(() => {\n if (!view) return\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.metaKey || event.ctrlKey || event.altKey) return\n const target = event.target as HTMLElement | null\n if (target?.closest?.('input, textarea, select, [contenteditable=\"true\"]')) return\n const state = useSpotlight.getState()\n if (event.key === 'Escape') state.hide()\n else if (event.key === 'ArrowRight' || event.key === 'PageDown') state.goTo(state.step + 1)\n else if (event.key === 'ArrowLeft' || event.key === 'PageUp') state.goTo(state.step - 1)\n else return\n event.preventDefault()\n event.stopPropagation()\n }\n view.addEventListener('keydown', onKeyDown, true)\n return () => view.removeEventListener('keydown', onKeyDown, true)\n }, [view])\n\n const actions = useSpotlight.getState()\n const isKept = kept === `${stepKey}|${note}`\n return (\n <div\n ref={rootRef}\n className=\"absolute inset-0 z-30 select-none\"\n style={{ cursor: active ? 'default' : 'none' }}\n role=\"dialog\"\n aria-label=\"Spotlight\"\n data-testid=\"spotlight\"\n >\n <div ref={backdrop} className=\"absolute inset-0 bg-black/85\" />\n <div className=\"absolute inset-0 p-4 pb-16\">\n <PresentationStage show={show} className=\"bg-transparent\" />\n </div>\n <div\n className={cn(\n 'absolute bottom-3 left-1/2 z-10 flex -translate-x-1/2 items-center gap-0.5 rounded-full bg-black/60 p-1 text-white shadow-lg backdrop-blur transition-opacity duration-300',\n active ? 'opacity-100' : 'pointer-events-none opacity-0'\n )}\n role=\"toolbar\"\n aria-label=\"Spotlight\"\n data-testid=\"spotlight-controls\"\n >\n <Control\n label=\"Previous (\u2190)\"\n onClick={() => actions.goTo(step - 1)}\n disabled={step === 0}\n testId=\"spotlight-previous\"\n >\n <ChevronLeft className=\"size-4\" />\n </Control>\n <span\n className=\"min-w-12 text-center text-xs tabular-nums text-white/85\"\n data-testid=\"spotlight-position\"\n >\n {step + 1} / {steps.length}\n </span>\n <Control\n label=\"Next (\u2192)\"\n onClick={() => actions.goTo(step + 1)}\n disabled={step >= steps.length - 1}\n testId=\"spotlight-next\"\n >\n <ChevronRight className=\"size-4\" />\n </Control>\n <div className=\"mx-1 h-4 w-px bg-white/25\" />\n <button\n type=\"button\"\n className=\"inline-flex h-8 items-center gap-1.5 whitespace-nowrap rounded-full px-3 text-xs text-white/85 hover:bg-white/15 hover:text-white disabled:opacity-60 disabled:hover:bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/60 pointer-coarse:h-11\"\n disabled={isKept}\n onClick={() => {\n actions.keep()\n setKept(`${stepKey}|${note}`)\n }}\n title=\"Add this as a slide of the open presentation\"\n data-testid=\"spotlight-keep\"\n >\n {isKept ? <Check className=\"size-3.5\" /> : <Presentation className=\"size-3.5\" />}\n {isKept ? 'Kept as slide' : 'Keep as slide'}\n </button>\n <Control label=\"Back to the document (Esc)\" onClick={actions.hide} testId=\"spotlight-close\">\n <X className=\"size-4\" />\n </Control>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clipping a passage with one click: the parser places each passage on its page (`page_bbox`,
|
|
3
|
+
* fractions of the page), so in clip mode the passage under the pointer is outlined and a click
|
|
4
|
+
* clips exactly its box, with a little room around it — the same room the server leaves when it
|
|
5
|
+
* clips a passage for a slide (`PASSAGE_MARGIN_X` / `_Y` in its presentation tool).
|
|
6
|
+
*/
|
|
7
|
+
import type { Rect } from './model';
|
|
8
|
+
/** Room kept around a clipped passage, as fractions of the page's width and height. */
|
|
9
|
+
export declare const PASSAGE_MARGIN_X = 0.015;
|
|
10
|
+
export declare const PASSAGE_MARGIN_Y = 0.008;
|
|
11
|
+
/** A passage's box `[x0, y0, x1, y1]` as the area to clip: with its margin, kept on the page. */
|
|
12
|
+
export declare function passageClipRect([x0, y0, x1, y1]: readonly [number, number, number, number]): Rect;
|
|
13
|
+
/**
|
|
14
|
+
* The passage at a point of the page (fractions), if any: the smallest one holding it, so a
|
|
15
|
+
* caption inside a figure's box wins over the figure.
|
|
16
|
+
*/
|
|
17
|
+
export declare function passageAt(passages: readonly Rect[], x: number, y: number): Rect | null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const PASSAGE_MARGIN_X = 0.015;
|
|
3
|
+
const PASSAGE_MARGIN_Y = 8e-3;
|
|
4
|
+
function passageClipRect([x0, y0, x1, y1]) {
|
|
5
|
+
const left = Math.max(0, x0 - PASSAGE_MARGIN_X);
|
|
6
|
+
const top = Math.max(0, y0 - PASSAGE_MARGIN_Y);
|
|
7
|
+
const right = Math.min(1, x1 + PASSAGE_MARGIN_X);
|
|
8
|
+
const bottom = Math.min(1, y1 + PASSAGE_MARGIN_Y);
|
|
9
|
+
return { x: left, y: top, w: right - left, h: bottom - top };
|
|
10
|
+
}
|
|
11
|
+
function passageAt(passages, x, y) {
|
|
12
|
+
let best = null;
|
|
13
|
+
for (const passage of passages) {
|
|
14
|
+
const inside = x >= passage.x && x <= passage.x + passage.w && y >= passage.y && y <= passage.y + passage.h;
|
|
15
|
+
if (inside && (!best || passage.w * passage.h < best.w * best.h)) best = passage;
|
|
16
|
+
}
|
|
17
|
+
return best;
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
PASSAGE_MARGIN_X,
|
|
21
|
+
PASSAGE_MARGIN_Y,
|
|
22
|
+
passageAt,
|
|
23
|
+
passageClipRect
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=passage-clip.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/presentation/passage-clip.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Clipping a passage with one click: the parser places each passage on its page (`page_bbox`,\n * fractions of the page), so in clip mode the passage under the pointer is outlined and a click\n * clips exactly its box, with a little room around it \u2014 the same room the server leaves when it\n * clips a passage for a slide (`PASSAGE_MARGIN_X` / `_Y` in its presentation tool).\n */\n\nimport type { Rect } from './model'\n\n/** Room kept around a clipped passage, as fractions of the page's width and height. */\nexport const PASSAGE_MARGIN_X = 0.015\nexport const PASSAGE_MARGIN_Y = 0.008\n\n/** A passage's box `[x0, y0, x1, y1]` as the area to clip: with its margin, kept on the page. */\nexport function passageClipRect([x0, y0, x1, y1]: readonly [number, number, number, number]): Rect {\n const left = Math.max(0, x0 - PASSAGE_MARGIN_X)\n const top = Math.max(0, y0 - PASSAGE_MARGIN_Y)\n const right = Math.min(1, x1 + PASSAGE_MARGIN_X)\n const bottom = Math.min(1, y1 + PASSAGE_MARGIN_Y)\n return { x: left, y: top, w: right - left, h: bottom - top }\n}\n\n/**\n * The passage at a point of the page (fractions), if any: the smallest one holding it, so a\n * caption inside a figure's box wins over the figure.\n */\nexport function passageAt(passages: readonly Rect[], x: number, y: number): Rect | null {\n let best: Rect | null = null\n for (const passage of passages) {\n const inside =\n x >= passage.x && x <= passage.x + passage.w && y >= passage.y && y <= passage.y + passage.h\n if (inside && (!best || passage.w * passage.h < best.w * best.h)) best = passage\n }\n return best\n}\n"],
|
|
5
|
+
"mappings": ";AAUO,MAAM,mBAAmB;AACzB,MAAM,mBAAmB;AAGzB,SAAS,gBAAgB,CAAC,IAAI,IAAI,IAAI,EAAE,GAAoD;AACjG,QAAM,OAAO,KAAK,IAAI,GAAG,KAAK,gBAAgB;AAC9C,QAAM,MAAM,KAAK,IAAI,GAAG,KAAK,gBAAgB;AAC7C,QAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,gBAAgB;AAC/C,QAAM,SAAS,KAAK,IAAI,GAAG,KAAK,gBAAgB;AAChD,SAAO,EAAE,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,MAAM,GAAG,SAAS,IAAI;AAC7D;AAMO,SAAS,UAAU,UAA2B,GAAW,GAAwB;AACtF,MAAI,OAAoB;AACxB,aAAW,WAAW,UAAU;AAC9B,UAAM,SACJ,KAAK,QAAQ,KAAK,KAAK,QAAQ,IAAI,QAAQ,KAAK,KAAK,QAAQ,KAAK,KAAK,QAAQ,IAAI,QAAQ;AAC7F,QAAI,WAAW,CAAC,QAAQ,QAAQ,IAAI,QAAQ,IAAI,KAAK,IAAI,KAAK,GAAI,QAAO;AAAA,EAC3E;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The spotlight: clips gathered in the document viewer (from one document or several) and shown,
|
|
3
|
+
* on request, as a slide over the viewer itself — the same window, so a call sharing it keeps
|
|
4
|
+
* sharing it. It is transient: nothing reaches the presentation unless "Keep as slide" (or "Add to
|
|
5
|
+
* the presentation") puts it there.
|
|
6
|
+
*
|
|
7
|
+
* Kept in memory only, like the presentation (a clip holds its decrypted file), and cleared when
|
|
8
|
+
* another user signs in (`owner`).
|
|
9
|
+
*/
|
|
10
|
+
import { type Clip, type Slide } from './model';
|
|
11
|
+
import { reserveOf, type NewClip } from './store';
|
|
12
|
+
/** A spotlight slide holds this many clips at most; more are shown on further slides. */
|
|
13
|
+
export declare const SPOTLIGHT_PER_SLIDE = 4;
|
|
14
|
+
interface SpotlightState {
|
|
15
|
+
clips: Clip[];
|
|
16
|
+
/** A line of text shown as the spotlight's title (empty: none). */
|
|
17
|
+
note: string;
|
|
18
|
+
/** The spotlight is on screen. */
|
|
19
|
+
showing: boolean;
|
|
20
|
+
/** Which of its steps (`spotlightSteps`) is shown. */
|
|
21
|
+
step: number;
|
|
22
|
+
/** The user the clips were gathered by. */
|
|
23
|
+
owner: string | null;
|
|
24
|
+
/**
|
|
25
|
+
* The viewer's Clip mode is on: kept here, not in the viewer, so it stays on as the viewer opens
|
|
26
|
+
* one document after another.
|
|
27
|
+
*/
|
|
28
|
+
clipping: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface SpotlightActions {
|
|
31
|
+
gather(clip: NewClip): string;
|
|
32
|
+
remove(clipId: string): void;
|
|
33
|
+
setNote(note: string): void;
|
|
34
|
+
show(): void;
|
|
35
|
+
hide(): void;
|
|
36
|
+
goTo(step: number): void;
|
|
37
|
+
/** The step on screen, as a slide of the open presentation (after its current slide). */
|
|
38
|
+
keep(): void;
|
|
39
|
+
/** The gathered clips, into the presentation's tray (not placed); the spotlight is emptied. */
|
|
40
|
+
addToTray(): void;
|
|
41
|
+
clear(): void;
|
|
42
|
+
/** Gathering for this user: another's clips are dropped. */
|
|
43
|
+
own(userId: string | null): void;
|
|
44
|
+
setClipping(on: boolean): void;
|
|
45
|
+
}
|
|
46
|
+
export type SpotlightStore = SpotlightState & SpotlightActions;
|
|
47
|
+
/**
|
|
48
|
+
* What ← → step through: the clips together (up to four a slide), and, where a slide shows more
|
|
49
|
+
* than one, each of them alone after it. Each step lists clip ids.
|
|
50
|
+
*/
|
|
51
|
+
export declare function spotlightSteps(clips: readonly Pick<Clip, 'id'>[]): string[][];
|
|
52
|
+
/**
|
|
53
|
+
* A step as a slide, arranged as the presentation arranges one (`autoLayout`: two tall clips go
|
|
54
|
+
* side by side, two wide ones stack), with the note as its title and the presentation's look.
|
|
55
|
+
*/
|
|
56
|
+
export declare function spotlightSlide(clips: readonly Clip[], ids: readonly string[], note: string, look: Parameters<typeof reserveOf>[1]): Slide;
|
|
57
|
+
export declare const useSpotlight: import("zustand").UseBoundStore<import("zustand").StoreApi<SpotlightStore>>;
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { create } from "zustand";
|
|
3
|
+
import { releasePdfRegionSource } from "@arbidocs/react/viewers";
|
|
4
|
+
import { autoLayout, defaultCaption } from "./model.js";
|
|
5
|
+
import { reserveOf, usePresentation } from "./store.js";
|
|
6
|
+
const SPOTLIGHT_PER_SLIDE = 4;
|
|
7
|
+
function spotlightSteps(clips) {
|
|
8
|
+
const steps = [];
|
|
9
|
+
for (let i = 0; i < clips.length; i += SPOTLIGHT_PER_SLIDE) {
|
|
10
|
+
const group = clips.slice(i, i + SPOTLIGHT_PER_SLIDE).map((clip) => clip.id);
|
|
11
|
+
steps.push(group);
|
|
12
|
+
if (group.length > 1) steps.push(...group.map((id) => [id]));
|
|
13
|
+
}
|
|
14
|
+
return steps;
|
|
15
|
+
}
|
|
16
|
+
function spotlightSlide(clips, ids, note, look) {
|
|
17
|
+
const shown = ids.map((id) => clips.find((clip) => clip.id === id)).filter((clip) => !!clip);
|
|
18
|
+
const base = { caption: note.trim(), layout: "clips" };
|
|
19
|
+
const frames = autoLayout(
|
|
20
|
+
shown.map((clip) => clip.aspect),
|
|
21
|
+
reserveOf(base, look)
|
|
22
|
+
);
|
|
23
|
+
return {
|
|
24
|
+
...base,
|
|
25
|
+
id: "spotlight",
|
|
26
|
+
auto: true,
|
|
27
|
+
points: "",
|
|
28
|
+
boxes: [],
|
|
29
|
+
items: shown.map((clip, i) => ({ clipId: clip.id, frame: frames[i] }))
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
let sequence = 0;
|
|
33
|
+
function release(gone, kept) {
|
|
34
|
+
const inUse = new Set([...kept, ...usePresentation.getState().clips].map((clip) => clip.source));
|
|
35
|
+
for (const source of new Set(gone.map((clip) => clip.source))) {
|
|
36
|
+
if (!inUse.has(source)) releasePdfRegionSource(source);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const EMPTY = {
|
|
40
|
+
clips: [],
|
|
41
|
+
note: "",
|
|
42
|
+
showing: false,
|
|
43
|
+
step: 0
|
|
44
|
+
};
|
|
45
|
+
const useSpotlight = create()((set, get) => ({
|
|
46
|
+
...EMPTY,
|
|
47
|
+
owner: null,
|
|
48
|
+
clipping: false,
|
|
49
|
+
gather(clip) {
|
|
50
|
+
const id = `spot-${Date.now().toString(36)}-${++sequence}`;
|
|
51
|
+
const full = {
|
|
52
|
+
...clip,
|
|
53
|
+
id,
|
|
54
|
+
caption: clip.caption ?? defaultCaption(clip.fileName, clip.page),
|
|
55
|
+
zoomBase: clip.zoomBase ?? clip.rect.w
|
|
56
|
+
};
|
|
57
|
+
set((state) => ({ clips: [...state.clips, full] }));
|
|
58
|
+
return id;
|
|
59
|
+
},
|
|
60
|
+
remove(clipId) {
|
|
61
|
+
const { clips, step } = get();
|
|
62
|
+
const gone = clips.filter((clip) => clip.id === clipId);
|
|
63
|
+
if (!gone.length) return;
|
|
64
|
+
const rest = clips.filter((clip) => clip.id !== clipId);
|
|
65
|
+
const steps = spotlightSteps(rest).length;
|
|
66
|
+
set({
|
|
67
|
+
clips: rest,
|
|
68
|
+
step: Math.min(step, Math.max(0, steps - 1)),
|
|
69
|
+
...rest.length ? {} : { showing: false, note: "" }
|
|
70
|
+
});
|
|
71
|
+
release(gone, rest);
|
|
72
|
+
},
|
|
73
|
+
setNote: (note) => set({ note }),
|
|
74
|
+
show: () => set((state) => state.clips.length ? { showing: true, step: 0 } : state),
|
|
75
|
+
hide: () => set({ showing: false }),
|
|
76
|
+
goTo: (step) => set(
|
|
77
|
+
(state) => step < 0 || step >= spotlightSteps(state.clips).length || step === state.step ? state : { step }
|
|
78
|
+
),
|
|
79
|
+
keep() {
|
|
80
|
+
const { clips, step, note } = get();
|
|
81
|
+
const ids = spotlightSteps(clips)[step];
|
|
82
|
+
if (!ids) return;
|
|
83
|
+
usePresentation.getState().addSlideOf(
|
|
84
|
+
ids.map((id) => clips.find((clip) => clip.id === id)),
|
|
85
|
+
note.trim()
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
addToTray() {
|
|
89
|
+
usePresentation.getState().adoptClips(get().clips);
|
|
90
|
+
set({ ...EMPTY });
|
|
91
|
+
},
|
|
92
|
+
clear() {
|
|
93
|
+
const gone = get().clips;
|
|
94
|
+
set({ ...EMPTY });
|
|
95
|
+
release(gone, []);
|
|
96
|
+
},
|
|
97
|
+
own(userId) {
|
|
98
|
+
if (get().owner === userId) return;
|
|
99
|
+
get().clear();
|
|
100
|
+
set({ owner: userId, clipping: false });
|
|
101
|
+
},
|
|
102
|
+
setClipping: (clipping) => set({ clipping })
|
|
103
|
+
}));
|
|
104
|
+
export {
|
|
105
|
+
SPOTLIGHT_PER_SLIDE,
|
|
106
|
+
spotlightSlide,
|
|
107
|
+
spotlightSteps,
|
|
108
|
+
useSpotlight
|
|
109
|
+
};
|
|
110
|
+
//# sourceMappingURL=spotlight.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/presentation/spotlight.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * The spotlight: clips gathered in the document viewer (from one document or several) and shown,\n * on request, as a slide over the viewer itself \u2014 the same window, so a call sharing it keeps\n * sharing it. It is transient: nothing reaches the presentation unless \"Keep as slide\" (or \"Add to\n * the presentation\") puts it there.\n *\n * Kept in memory only, like the presentation (a clip holds its decrypted file), and cleared when\n * another user signs in (`owner`).\n */\n\nimport { create } from 'zustand'\nimport { releasePdfRegionSource } from '@arbidocs/react/viewers'\nimport { autoLayout, defaultCaption, type Clip, type Slide } from './model'\nimport { reserveOf, usePresentation, type NewClip } from './store'\n\n/** A spotlight slide holds this many clips at most; more are shown on further slides. */\nexport const SPOTLIGHT_PER_SLIDE = 4\n\ninterface SpotlightState {\n clips: Clip[]\n /** A line of text shown as the spotlight's title (empty: none). */\n note: string\n /** The spotlight is on screen. */\n showing: boolean\n /** Which of its steps (`spotlightSteps`) is shown. */\n step: number\n /** The user the clips were gathered by. */\n owner: string | null\n /**\n * The viewer's Clip mode is on: kept here, not in the viewer, so it stays on as the viewer opens\n * one document after another.\n */\n clipping: boolean\n}\n\ninterface SpotlightActions {\n gather(clip: NewClip): string\n remove(clipId: string): void\n setNote(note: string): void\n show(): void\n hide(): void\n goTo(step: number): void\n /** The step on screen, as a slide of the open presentation (after its current slide). */\n keep(): void\n /** The gathered clips, into the presentation's tray (not placed); the spotlight is emptied. */\n addToTray(): void\n clear(): void\n /** Gathering for this user: another's clips are dropped. */\n own(userId: string | null): void\n setClipping(on: boolean): void\n}\n\nexport type SpotlightStore = SpotlightState & SpotlightActions\n\n/**\n * What \u2190 \u2192 step through: the clips together (up to four a slide), and, where a slide shows more\n * than one, each of them alone after it. Each step lists clip ids.\n */\nexport function spotlightSteps(clips: readonly Pick<Clip, 'id'>[]): string[][] {\n const steps: string[][] = []\n for (let i = 0; i < clips.length; i += SPOTLIGHT_PER_SLIDE) {\n const group = clips.slice(i, i + SPOTLIGHT_PER_SLIDE).map((clip) => clip.id)\n steps.push(group)\n if (group.length > 1) steps.push(...group.map((id) => [id]))\n }\n return steps\n}\n\n/**\n * A step as a slide, arranged as the presentation arranges one (`autoLayout`: two tall clips go\n * side by side, two wide ones stack), with the note as its title and the presentation's look.\n */\nexport function spotlightSlide(\n clips: readonly Clip[],\n ids: readonly string[],\n note: string,\n look: Parameters<typeof reserveOf>[1]\n): Slide {\n const shown = ids\n .map((id) => clips.find((clip) => clip.id === id))\n .filter((clip): clip is Clip => !!clip)\n const base = { caption: note.trim(), layout: 'clips' as const }\n const frames = autoLayout(\n shown.map((clip) => clip.aspect),\n reserveOf(base, look)\n )\n return {\n ...base,\n id: 'spotlight',\n auto: true,\n points: '',\n boxes: [],\n items: shown.map((clip, i) => ({ clipId: clip.id, frame: frames[i] })),\n }\n}\n\nlet sequence = 0\n\n/** Its file goes from the PDF engine once neither the spotlight nor the presentation shows it. */\nfunction release(gone: readonly Clip[], kept: readonly Clip[]) {\n const inUse = new Set([...kept, ...usePresentation.getState().clips].map((clip) => clip.source))\n for (const source of new Set(gone.map((clip) => clip.source))) {\n if (!inUse.has(source)) releasePdfRegionSource(source)\n }\n}\n\nconst EMPTY: Omit<SpotlightState, 'owner' | 'clipping'> = {\n clips: [],\n note: '',\n showing: false,\n step: 0,\n}\n\nexport const useSpotlight = create<SpotlightStore>()((set, get) => ({\n ...EMPTY,\n owner: null,\n clipping: false,\n\n gather(clip) {\n const id = `spot-${Date.now().toString(36)}-${++sequence}`\n const full: Clip = {\n ...clip,\n id,\n caption: clip.caption ?? defaultCaption(clip.fileName, clip.page),\n zoomBase: clip.zoomBase ?? clip.rect.w,\n }\n set((state) => ({ clips: [...state.clips, full] }))\n return id\n },\n\n remove(clipId) {\n const { clips, step } = get()\n const gone = clips.filter((clip) => clip.id === clipId)\n if (!gone.length) return\n const rest = clips.filter((clip) => clip.id !== clipId)\n const steps = spotlightSteps(rest).length\n set({\n clips: rest,\n step: Math.min(step, Math.max(0, steps - 1)),\n ...(rest.length ? {} : { showing: false, note: '' }),\n })\n release(gone, rest)\n },\n\n setNote: (note) => set({ note }),\n show: () => set((state) => (state.clips.length ? { showing: true, step: 0 } : state)),\n hide: () => set({ showing: false }),\n goTo: (step) =>\n set((state) =>\n step < 0 || step >= spotlightSteps(state.clips).length || step === state.step\n ? state\n : { step }\n ),\n\n keep() {\n const { clips, step, note } = get()\n const ids = spotlightSteps(clips)[step]\n if (!ids) return\n usePresentation.getState().addSlideOf(\n ids.map((id) => clips.find((clip) => clip.id === id)!),\n note.trim()\n )\n },\n\n addToTray() {\n usePresentation.getState().adoptClips(get().clips)\n set({ ...EMPTY })\n },\n\n clear() {\n const gone = get().clips\n set({ ...EMPTY })\n release(gone, [])\n },\n\n own(userId) {\n if (get().owner === userId) return\n get().clear()\n set({ owner: userId, clipping: false })\n },\n\n setClipping: (clipping) => set({ clipping }),\n}))\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -154,6 +154,11 @@ interface PresentationActions {
|
|
|
154
154
|
bind(boundTo: string | null): void;
|
|
155
155
|
/** Clips gathered before (with no presentation open), added to the tray, not placed. */
|
|
156
156
|
adoptClips(clips: readonly Clip[]): void;
|
|
157
|
+
/**
|
|
158
|
+
* A slide of these clips (added to the tray too), arranged, with `caption` as its title: after
|
|
159
|
+
* the current slide, or in its place while that is empty. It becomes the current slide.
|
|
160
|
+
*/
|
|
161
|
+
addSlideOf(clips: readonly Clip[], caption?: string): void;
|
|
157
162
|
/** Replace the presentation with one opened from a file (its clips' sources fetched). */
|
|
158
163
|
load(opened: {
|
|
159
164
|
title: string;
|
|
@@ -371,6 +371,25 @@ const usePresentation = create()((set, get) => {
|
|
|
371
371
|
...adopted.filter((clip) => !state.clips.some((entry) => entry.id === clip.id))
|
|
372
372
|
]
|
|
373
373
|
})),
|
|
374
|
+
addSlideOf: (added, caption = "") => set((state) => {
|
|
375
|
+
const fresh = added.map((clip) => ({ ...clip, id: nextId("clip") }));
|
|
376
|
+
const clips = [...state.clips, ...fresh];
|
|
377
|
+
const slide = arranged(
|
|
378
|
+
{
|
|
379
|
+
...emptySlide("clips"),
|
|
380
|
+
caption,
|
|
381
|
+
items: fresh.map((clip) => ({ clipId: clip.id, frame: fitFrame(clip.aspect) }))
|
|
382
|
+
},
|
|
383
|
+
clips,
|
|
384
|
+
state
|
|
385
|
+
);
|
|
386
|
+
const here = state.slides[state.current];
|
|
387
|
+
const empty = !!here && !here.items.length && !here.boxes.length && !here.caption.trim() && !here.points.trim();
|
|
388
|
+
const at = empty ? state.current : state.current + 1;
|
|
389
|
+
const slides = state.slides.slice();
|
|
390
|
+
slides.splice(at, empty ? 1 : 0, slide);
|
|
391
|
+
return { clips, slides, current: at };
|
|
392
|
+
}),
|
|
374
393
|
load(opened) {
|
|
375
394
|
get().reset();
|
|
376
395
|
const clips = opened.clips.map((clip) => ({ ...clip, id: nextId("clip") }));
|