@inkbar/react 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +166 -2
- package/dist/index.d.mts +1992 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1358 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -3
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1358 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, createElement, useCallback, useContext, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { InkError, acceptSuggestion, confidenceBucket, createSnapshotHistory, isAbandonedNote, openDocument, repliesTo, threadOf, threadRoot } from "@inkbar/core";
|
|
4
|
+
import { TOOL_LABELS, ZOOM_LEVELS, createFocusReturn, cssVariableDefaults, mountViewer, observeAnnotationElement, shouldKeepOpenOnAnnotation } from "@inkbar/core/dom";
|
|
5
|
+
import { Popover, Select, Slot, Toolbar } from "radix-ui";
|
|
6
|
+
import { createPortal, flushSync } from "react-dom";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { Check, ChevronDown, ChevronLeft, ChevronRight, Hand, Highlighter, ICON_ATTRIBUTES, MessageSquare, MousePointer2, Redo2, Square, Undo2, ZoomIn, ZoomOut } from "@inkbar/icons";
|
|
9
|
+
//#region src/context.ts
|
|
10
|
+
const InkContextObject = createContext(null);
|
|
11
|
+
/**
|
|
12
|
+
* The context for hooks: throws a readable error when called outside a component rendered
|
|
13
|
+
* inside `InkRoot`.
|
|
14
|
+
*/
|
|
15
|
+
function useInkContext(consumer) {
|
|
16
|
+
const ctx = useContext(InkContextObject);
|
|
17
|
+
if (!ctx) throw new Error(`[inkbar] ${consumer} must be called in a component inside <InkRoot>.`);
|
|
18
|
+
return ctx;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/use-readable.ts
|
|
22
|
+
const noop = () => void 0;
|
|
23
|
+
/**
|
|
24
|
+
* Mirrors a core `Readable` into React. A missing readable yields `fallback`, which must be a
|
|
25
|
+
* stable reference (a module constant): `useSyncExternalStore` compares snapshots by identity
|
|
26
|
+
* and loops on a fresh value. On the server the fallback is rendered.
|
|
27
|
+
*/
|
|
28
|
+
function useReadable(readable, fallback) {
|
|
29
|
+
const subscribe = useCallback((onChange) => readable ? readable.subscribe(() => onChange()) : noop, [readable]);
|
|
30
|
+
const get = useCallback(() => readable ? readable.get() : fallback, [readable, fallback]);
|
|
31
|
+
const server = useCallback(() => fallback, [fallback]);
|
|
32
|
+
return useSyncExternalStore(subscribe, get, server);
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/hooks/useDocument.ts
|
|
36
|
+
const NO_PAGES = [];
|
|
37
|
+
function useDocument() {
|
|
38
|
+
const { document, status } = useInkContext("useDocument()");
|
|
39
|
+
return useMemo(() => ({
|
|
40
|
+
document,
|
|
41
|
+
status,
|
|
42
|
+
isReady: status.state === "ready",
|
|
43
|
+
error: status.state === "error" ? status.error : null,
|
|
44
|
+
pages: document?.pages ?? NO_PAGES,
|
|
45
|
+
info: document?.info ?? null,
|
|
46
|
+
exportAnnotations: () => document?.exportAnnotations("json"),
|
|
47
|
+
importAnnotations: (data, opts) => document?.importAnnotations(data, opts)
|
|
48
|
+
}), [document, status]);
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/hooks/useAnnotations.ts
|
|
52
|
+
function useAnnotations() {
|
|
53
|
+
const { annotations, selection, document } = useInkContext("useAnnotations()");
|
|
54
|
+
const store = document?.annotations;
|
|
55
|
+
const selected = useMemo(() => selection.map((id) => annotations.find((a) => a.id === id)).filter((a) => !!a), [annotations, selection]);
|
|
56
|
+
const actions = useMemo(() => ({
|
|
57
|
+
add: (input) => store?.add(input),
|
|
58
|
+
update: (id, patch) => store?.update(id, patch),
|
|
59
|
+
remove: (id) => store?.remove(id) ?? false,
|
|
60
|
+
select: (ids) => store?.select(ids),
|
|
61
|
+
/** Keeps a suggested redaction as the user's own, in the default redaction colour. */
|
|
62
|
+
accept: (id) => store ? acceptSuggestion(store, id) : void 0,
|
|
63
|
+
dismiss: (id) => store?.remove(id) ?? false
|
|
64
|
+
}), [store]);
|
|
65
|
+
return useMemo(() => ({
|
|
66
|
+
annotations,
|
|
67
|
+
selection,
|
|
68
|
+
selected,
|
|
69
|
+
byPage: (page) => annotations.filter((a) => a.page === page),
|
|
70
|
+
/** Replies in creation order for a note id. */
|
|
71
|
+
replies: (id) => repliesTo(annotations, id),
|
|
72
|
+
...actions
|
|
73
|
+
}), [
|
|
74
|
+
annotations,
|
|
75
|
+
selection,
|
|
76
|
+
selected,
|
|
77
|
+
actions
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/hooks/useTool.ts
|
|
82
|
+
function useTool() {
|
|
83
|
+
const { tool, setTool } = useInkContext("useTool()");
|
|
84
|
+
return useMemo(() => ({
|
|
85
|
+
tool,
|
|
86
|
+
setTool
|
|
87
|
+
}), [tool, setTool]);
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/hooks/useSelection.ts
|
|
91
|
+
function useSelection() {
|
|
92
|
+
const { selection, document } = useInkContext("useSelection()");
|
|
93
|
+
const store = document?.annotations;
|
|
94
|
+
return useMemo(() => ({
|
|
95
|
+
selection,
|
|
96
|
+
select: (ids) => store?.select(ids),
|
|
97
|
+
clear: () => store?.select([])
|
|
98
|
+
}), [selection, store]);
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/hooks/useZoom.ts
|
|
102
|
+
function useZoom() {
|
|
103
|
+
const { viewport } = useInkContext("useZoom()");
|
|
104
|
+
const zoom = useReadable(viewport?.zoom, "fit-width");
|
|
105
|
+
const scale = useReadable(viewport?.scale, 1);
|
|
106
|
+
return useMemo(() => ({
|
|
107
|
+
zoom,
|
|
108
|
+
scale,
|
|
109
|
+
setZoom: (z) => viewport?.setZoom(z),
|
|
110
|
+
zoomIn: () => viewport?.zoomBy(1.25),
|
|
111
|
+
zoomOut: () => viewport?.zoomBy(.8)
|
|
112
|
+
}), [
|
|
113
|
+
viewport,
|
|
114
|
+
zoom,
|
|
115
|
+
scale
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/hooks/usePage.ts
|
|
120
|
+
function usePage() {
|
|
121
|
+
const { viewport, document } = useInkContext("usePage()");
|
|
122
|
+
const current = useReadable(viewport?.currentPage, 0);
|
|
123
|
+
return useMemo(() => ({
|
|
124
|
+
/** 0-based. */
|
|
125
|
+
current,
|
|
126
|
+
count: document?.pages.length ?? 0,
|
|
127
|
+
goTo: (index) => viewport?.scrollToPage(index),
|
|
128
|
+
next: () => viewport?.scrollToPage(viewport.currentPage.get() + 1),
|
|
129
|
+
previous: () => viewport?.scrollToPage(viewport.currentPage.get() - 1),
|
|
130
|
+
scrollToAnnotation(id) {
|
|
131
|
+
const a = document?.annotations.get(id);
|
|
132
|
+
if (a) viewport?.scrollToRect(a.page, a.rect);
|
|
133
|
+
}
|
|
134
|
+
}), [
|
|
135
|
+
viewport,
|
|
136
|
+
document,
|
|
137
|
+
current
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/hooks/useHistory.ts
|
|
142
|
+
/**
|
|
143
|
+
* Undo and redo. Uncontrolled documents use the core history; controlled hosts that pass
|
|
144
|
+
* `history` to InkRoot get a snapshot stack over their own list, applied through
|
|
145
|
+
* `onAnnotationsChange`.
|
|
146
|
+
*/
|
|
147
|
+
function useHistory() {
|
|
148
|
+
return useInkContext("useHistory()").history;
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/hooks/useEmbedded.ts
|
|
152
|
+
const NONE = [];
|
|
153
|
+
function useEmbedded() {
|
|
154
|
+
const summary = useInkContext("useEmbedded()").document?.embedded ?? NONE;
|
|
155
|
+
return useMemo(() => ({
|
|
156
|
+
summary,
|
|
157
|
+
total: summary.reduce((n, s) => n + s.count, 0),
|
|
158
|
+
hasAny: summary.length > 0
|
|
159
|
+
}), [summary]);
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/use-latest.ts
|
|
163
|
+
/**
|
|
164
|
+
* A ref that always holds the latest render's value, for callbacks that outlive a render
|
|
165
|
+
* (core's `onChange`, effects keyed on something else). Updated in a layout effect, so it is
|
|
166
|
+
* current before the parent's own layout and passive effects and before any callback fired
|
|
167
|
+
* after commit; declare it before the effects that read it.
|
|
168
|
+
*/
|
|
169
|
+
function useLatest(value) {
|
|
170
|
+
const ref = useRef(value);
|
|
171
|
+
useLayoutEffect(() => {
|
|
172
|
+
ref.current = value;
|
|
173
|
+
});
|
|
174
|
+
return ref;
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/components/InkRoot.tsx
|
|
178
|
+
const IDLE = { state: "idle" };
|
|
179
|
+
const NO_ANNOTATIONS = [];
|
|
180
|
+
const NO_IDS = [];
|
|
181
|
+
/** Signed URLs change; the key does not. Bytes and Blobs are their own identity. */
|
|
182
|
+
function identity(src) {
|
|
183
|
+
if (src === null) return null;
|
|
184
|
+
if (typeof src === "string") return src;
|
|
185
|
+
if ("key" in src) return src.key;
|
|
186
|
+
return src;
|
|
187
|
+
}
|
|
188
|
+
function isDevelopment() {
|
|
189
|
+
try {
|
|
190
|
+
return process.env.NODE_ENV !== "production";
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function setTestHook(el, doc) {
|
|
196
|
+
if (el && isDevelopment()) el.__inkDocument = doc;
|
|
197
|
+
}
|
|
198
|
+
function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsChange, onChange, tool, onToolChange, page, onPageChange, zoom, onZoomChange, history, asChild = false, onReady, onError, onActivate, children, ref, ...rest }) {
|
|
199
|
+
const rootElRef = useRef(null);
|
|
200
|
+
const [rootEl, setRootElState] = useState(null);
|
|
201
|
+
const setRootEl = useCallback((el) => {
|
|
202
|
+
rootElRef.current = el;
|
|
203
|
+
setRootElState(el);
|
|
204
|
+
}, []);
|
|
205
|
+
const documentRef = useRef(null);
|
|
206
|
+
const [document, setDocument] = useState(null);
|
|
207
|
+
/** Overrides the document's status while opening, and when opening itself failed. */
|
|
208
|
+
const [override, setOverride] = useState(null);
|
|
209
|
+
const [internalTool, setInternalTool] = useState(tool ?? "view");
|
|
210
|
+
const [activated, setActivatedState] = useState(null);
|
|
211
|
+
const [viewport, setViewportState] = useState(null);
|
|
212
|
+
const [tools, setToolsState] = useState(null);
|
|
213
|
+
const documentStatus = useReadable(document?.status, IDLE);
|
|
214
|
+
const status = override ?? documentStatus;
|
|
215
|
+
const annotationList = useReadable(document?.annotations.list, NO_ANNOTATIONS);
|
|
216
|
+
const selection = useReadable(document?.annotations.selection, NO_IDS);
|
|
217
|
+
const controlledProp = annotations !== void 0;
|
|
218
|
+
const controlled = document ? document.annotations.mode === "controlled" : controlledProp;
|
|
219
|
+
const historyEnabled = controlled && history === true;
|
|
220
|
+
const latest = useLatest({
|
|
221
|
+
src,
|
|
222
|
+
assets,
|
|
223
|
+
defaults,
|
|
224
|
+
annotations,
|
|
225
|
+
controlled: controlledProp,
|
|
226
|
+
history,
|
|
227
|
+
historyEnabled,
|
|
228
|
+
onAnnotationsChange,
|
|
229
|
+
onChange,
|
|
230
|
+
onReady,
|
|
231
|
+
onError,
|
|
232
|
+
onActivate,
|
|
233
|
+
onToolChange,
|
|
234
|
+
onPageChange,
|
|
235
|
+
onZoomChange
|
|
236
|
+
});
|
|
237
|
+
const snapshotHistory = useMemo(() => createSnapshotHistory({
|
|
238
|
+
apply: (list) => documentRef.current?.annotations.replaceAll(list, "history"),
|
|
239
|
+
enabled: () => latest.current.historyEnabled
|
|
240
|
+
}), [latest]);
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
snapshotHistory.observe(annotations);
|
|
243
|
+
}, [annotations, snapshotHistory]);
|
|
244
|
+
const srcIdentity = identity(src);
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
let cancelled = false;
|
|
247
|
+
snapshotHistory.clear();
|
|
248
|
+
const source = latest.current.src;
|
|
249
|
+
if (source === null) {
|
|
250
|
+
setOverride(null);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
setOverride({ state: "loading" });
|
|
254
|
+
let opened = null;
|
|
255
|
+
const run = async () => {
|
|
256
|
+
try {
|
|
257
|
+
const doc = await openDocument(source, {
|
|
258
|
+
assets: latest.current.assets,
|
|
259
|
+
password,
|
|
260
|
+
defaults: latest.current.defaults ?? (rootElRef.current ? cssVariableDefaults(rootElRef.current) : void 0),
|
|
261
|
+
history: latest.current.controlled ? false : latest.current.history ?? true,
|
|
262
|
+
annotations: latest.current.controlled ? {
|
|
263
|
+
mode: "controlled",
|
|
264
|
+
initial: latest.current.annotations ?? [],
|
|
265
|
+
onChange: (next, change) => {
|
|
266
|
+
queueMicrotask(() => {
|
|
267
|
+
if (cancelled) return;
|
|
268
|
+
flushSync(() => {
|
|
269
|
+
latest.current.onAnnotationsChange?.(next);
|
|
270
|
+
latest.current.onChange?.(change);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
} : { mode: "uncontrolled" }
|
|
275
|
+
});
|
|
276
|
+
if (cancelled) {
|
|
277
|
+
doc.destroy();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
opened = doc;
|
|
281
|
+
const current = latest.current.annotations;
|
|
282
|
+
if (doc.annotations.mode === "controlled" && current !== void 0) doc.annotations.setControlledValue(current);
|
|
283
|
+
documentRef.current = doc;
|
|
284
|
+
setDocument(doc);
|
|
285
|
+
setOverride(null);
|
|
286
|
+
setTestHook(rootElRef.current, doc);
|
|
287
|
+
const s = doc.status.get();
|
|
288
|
+
if (s.state === "ready") latest.current.onReady?.(doc);
|
|
289
|
+
else if (s.state === "error") latest.current.onError?.(s.error);
|
|
290
|
+
} catch (cause) {
|
|
291
|
+
if (cancelled) return;
|
|
292
|
+
const error = cause instanceof InkError ? cause : new InkError("InvalidPdf", cause instanceof Error ? cause.message : String(cause), {}, cause);
|
|
293
|
+
setOverride({
|
|
294
|
+
state: "error",
|
|
295
|
+
error
|
|
296
|
+
});
|
|
297
|
+
latest.current.onError?.(error);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
Promise.resolve().then(() => {
|
|
301
|
+
if (!cancelled) return run();
|
|
302
|
+
});
|
|
303
|
+
return () => {
|
|
304
|
+
cancelled = true;
|
|
305
|
+
opened?.destroy();
|
|
306
|
+
documentRef.current = null;
|
|
307
|
+
setDocument(null);
|
|
308
|
+
setTestHook(rootElRef.current, void 0);
|
|
309
|
+
};
|
|
310
|
+
}, [
|
|
311
|
+
srcIdentity,
|
|
312
|
+
password,
|
|
313
|
+
snapshotHistory,
|
|
314
|
+
latest
|
|
315
|
+
]);
|
|
316
|
+
useLayoutEffect(() => {
|
|
317
|
+
if (annotations && document?.annotations.mode === "controlled") document.annotations.setControlledValue(annotations);
|
|
318
|
+
}, [annotations, document]);
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
if (tool) setInternalTool(tool);
|
|
321
|
+
}, [tool]);
|
|
322
|
+
const setTool = useCallback((id) => {
|
|
323
|
+
setInternalTool(id);
|
|
324
|
+
latest.current.onToolChange?.(id);
|
|
325
|
+
}, [latest]);
|
|
326
|
+
const setActivated = useCallback((id) => {
|
|
327
|
+
setActivatedState(id);
|
|
328
|
+
if (id) latest.current.onActivate?.(id);
|
|
329
|
+
}, [latest]);
|
|
330
|
+
const reportZoom = useCallback((z) => latest.current.onZoomChange?.(z), [latest]);
|
|
331
|
+
const reportPage = useCallback((p) => latest.current.onPageChange?.(p), [latest]);
|
|
332
|
+
const setViewport = useCallback((vp, tc) => {
|
|
333
|
+
setViewportState(vp);
|
|
334
|
+
setToolsState(tc);
|
|
335
|
+
}, []);
|
|
336
|
+
const snapCanUndo = useReadable(snapshotHistory.canUndo, false);
|
|
337
|
+
const snapCanRedo = useReadable(snapshotHistory.canRedo, false);
|
|
338
|
+
const coreCanUndo = useReadable(document?.history.canUndo, false);
|
|
339
|
+
const coreCanRedo = useReadable(document?.history.canRedo, false);
|
|
340
|
+
const inkHistory = useMemo(() => ({
|
|
341
|
+
canUndo: controlled ? historyEnabled && snapCanUndo : coreCanUndo,
|
|
342
|
+
canRedo: controlled ? historyEnabled && snapCanRedo : coreCanRedo,
|
|
343
|
+
undo: () => controlled ? snapshotHistory.undo() : documentRef.current?.history.undo(),
|
|
344
|
+
redo: () => controlled ? snapshotHistory.redo() : documentRef.current?.history.redo(),
|
|
345
|
+
clear: () => controlled ? snapshotHistory.clear() : documentRef.current?.history.clear(),
|
|
346
|
+
batch: (fn) => {
|
|
347
|
+
if (controlled) snapshotHistory.batch(fn);
|
|
348
|
+
else if (documentRef.current) documentRef.current.history.batch(fn);
|
|
349
|
+
else fn();
|
|
350
|
+
}
|
|
351
|
+
}), [
|
|
352
|
+
controlled,
|
|
353
|
+
historyEnabled,
|
|
354
|
+
snapCanUndo,
|
|
355
|
+
snapCanRedo,
|
|
356
|
+
coreCanUndo,
|
|
357
|
+
coreCanRedo,
|
|
358
|
+
snapshotHistory
|
|
359
|
+
]);
|
|
360
|
+
useImperativeHandle(ref, () => ({
|
|
361
|
+
document,
|
|
362
|
+
status,
|
|
363
|
+
rootEl,
|
|
364
|
+
history: inkHistory
|
|
365
|
+
}), [
|
|
366
|
+
document,
|
|
367
|
+
status,
|
|
368
|
+
rootEl,
|
|
369
|
+
inkHistory
|
|
370
|
+
]);
|
|
371
|
+
const ctx = useMemo(() => ({
|
|
372
|
+
assets,
|
|
373
|
+
document,
|
|
374
|
+
status,
|
|
375
|
+
annotations: annotationList,
|
|
376
|
+
selection,
|
|
377
|
+
viewport,
|
|
378
|
+
tools,
|
|
379
|
+
tool: internalTool,
|
|
380
|
+
setTool,
|
|
381
|
+
zoomModel: zoom,
|
|
382
|
+
pageModel: page,
|
|
383
|
+
reportZoom,
|
|
384
|
+
reportPage,
|
|
385
|
+
activated,
|
|
386
|
+
setActivated,
|
|
387
|
+
history: inkHistory,
|
|
388
|
+
rootEl,
|
|
389
|
+
setViewport
|
|
390
|
+
}), [
|
|
391
|
+
assets,
|
|
392
|
+
document,
|
|
393
|
+
status,
|
|
394
|
+
annotationList,
|
|
395
|
+
selection,
|
|
396
|
+
viewport,
|
|
397
|
+
tools,
|
|
398
|
+
internalTool,
|
|
399
|
+
setTool,
|
|
400
|
+
zoom,
|
|
401
|
+
page,
|
|
402
|
+
reportZoom,
|
|
403
|
+
reportPage,
|
|
404
|
+
activated,
|
|
405
|
+
setActivated,
|
|
406
|
+
inkHistory,
|
|
407
|
+
rootEl,
|
|
408
|
+
setViewport
|
|
409
|
+
]);
|
|
410
|
+
const Comp = asChild ? Slot.Root : "div";
|
|
411
|
+
return /* @__PURE__ */ jsx(InkContextObject.Provider, {
|
|
412
|
+
value: ctx,
|
|
413
|
+
children: /* @__PURE__ */ jsx(Comp, {
|
|
414
|
+
ref: setRootEl,
|
|
415
|
+
"data-ink-root": "",
|
|
416
|
+
"data-state": status.state,
|
|
417
|
+
...rest,
|
|
418
|
+
children: typeof children === "function" ? children({
|
|
419
|
+
status,
|
|
420
|
+
document
|
|
421
|
+
}) : children
|
|
422
|
+
})
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
//#endregion
|
|
426
|
+
//#region src/class-names.ts
|
|
427
|
+
/** Joins class names, dropping empties. */
|
|
428
|
+
function cx(...parts) {
|
|
429
|
+
const out = parts.filter(Boolean).join(" ");
|
|
430
|
+
return out === "" ? void 0 : out;
|
|
431
|
+
}
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/components/InkAnnotation.tsx
|
|
434
|
+
function readState(el) {
|
|
435
|
+
const s = el.dataset.state;
|
|
436
|
+
return s === "idle" || s === "hover" || s === "selected" ? s : null;
|
|
437
|
+
}
|
|
438
|
+
function InkAnnotation({ annotation, element, children }) {
|
|
439
|
+
const ctx = useInkContext("InkAnnotation");
|
|
440
|
+
const [observed, setObserved] = useState(() => element ? readState(element) : null);
|
|
441
|
+
useEffect(() => {
|
|
442
|
+
if (!element) {
|
|
443
|
+
setObserved(null);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
setObserved(readState(element));
|
|
447
|
+
const observer = new MutationObserver(() => setObserved(readState(element)));
|
|
448
|
+
observer.observe(element, {
|
|
449
|
+
attributes: true,
|
|
450
|
+
attributeFilter: ["data-state"]
|
|
451
|
+
});
|
|
452
|
+
return () => observer.disconnect();
|
|
453
|
+
}, [element]);
|
|
454
|
+
const { id } = annotation;
|
|
455
|
+
const isSuggestion = annotation.type === "redact" && annotation.origin === "suggested";
|
|
456
|
+
const locked = annotation.locked === true;
|
|
457
|
+
const selected = ctx.selection.includes(id);
|
|
458
|
+
const store = ctx.document?.annotations;
|
|
459
|
+
const slotProps = useMemo(() => ({
|
|
460
|
+
annotation,
|
|
461
|
+
state: observed ?? (selected ? "selected" : "idle"),
|
|
462
|
+
locked,
|
|
463
|
+
select: () => store?.select([id]),
|
|
464
|
+
remove: () => void store?.remove(id),
|
|
465
|
+
accept: () => {
|
|
466
|
+
if (store && isSuggestion) acceptSuggestion(store, id);
|
|
467
|
+
},
|
|
468
|
+
dismiss: () => {
|
|
469
|
+
if (isSuggestion) store?.remove(id);
|
|
470
|
+
}
|
|
471
|
+
}), [
|
|
472
|
+
annotation,
|
|
473
|
+
observed,
|
|
474
|
+
selected,
|
|
475
|
+
locked,
|
|
476
|
+
store,
|
|
477
|
+
id,
|
|
478
|
+
isSuggestion
|
|
479
|
+
]);
|
|
480
|
+
return /* @__PURE__ */ jsx(Fragment, { children: children(slotProps) });
|
|
481
|
+
}
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region src/components/InkPage.tsx
|
|
484
|
+
function sameTargets(a, b) {
|
|
485
|
+
return a.length === b.length && a.every((t, i) => t.annotation === b[i].annotation && t.el === b[i].el);
|
|
486
|
+
}
|
|
487
|
+
function InkPage({ page, host, mount, error, customAnnotations = false, renderAnnotation, renderPageError }) {
|
|
488
|
+
const doc = useInkContext("InkPage").document;
|
|
489
|
+
const [targets, setTargets] = useState([]);
|
|
490
|
+
useEffect(() => {
|
|
491
|
+
if (!doc || !customAnnotations) return;
|
|
492
|
+
const read = () => {
|
|
493
|
+
const next = doc.annotations.byPage(page).flatMap((annotation) => {
|
|
494
|
+
const el = mount.overlay.elementFor(annotation.id);
|
|
495
|
+
return el ? [{
|
|
496
|
+
annotation,
|
|
497
|
+
el
|
|
498
|
+
}] : [];
|
|
499
|
+
});
|
|
500
|
+
setTargets((prev) => sameTargets(prev, next) ? prev : next);
|
|
501
|
+
};
|
|
502
|
+
const stop = doc.annotations.list.subscribe(read);
|
|
503
|
+
read();
|
|
504
|
+
return () => {
|
|
505
|
+
stop();
|
|
506
|
+
setTargets([]);
|
|
507
|
+
};
|
|
508
|
+
}, [
|
|
509
|
+
doc,
|
|
510
|
+
page,
|
|
511
|
+
mount,
|
|
512
|
+
customAnnotations
|
|
513
|
+
]);
|
|
514
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [error ? createPortal(/* @__PURE__ */ jsx("div", {
|
|
515
|
+
className: "ink-page-error",
|
|
516
|
+
"data-ink-page-error": "",
|
|
517
|
+
role: "alert",
|
|
518
|
+
children: renderPageError ? renderPageError({
|
|
519
|
+
error,
|
|
520
|
+
page
|
|
521
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("p", { children: [
|
|
522
|
+
"Page ",
|
|
523
|
+
page + 1,
|
|
524
|
+
" could not be shown."
|
|
525
|
+
] }), /* @__PURE__ */ jsx("code", {
|
|
526
|
+
className: "ink-error-code",
|
|
527
|
+
children: error.code
|
|
528
|
+
})] })
|
|
529
|
+
}), host) : null, renderAnnotation ? targets.map((t) => createPortal(/* @__PURE__ */ jsx(InkAnnotation, {
|
|
530
|
+
annotation: t.annotation,
|
|
531
|
+
element: t.el,
|
|
532
|
+
children: renderAnnotation
|
|
533
|
+
}), t.el, t.annotation.id)) : null] });
|
|
534
|
+
}
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/components/InkViewport.tsx
|
|
537
|
+
function InkViewport({ mode = "continuous", rotation = 0, gap = 16, buffer = 2, loading, empty, error, renderAnnotation, renderPageError, className, ...rest }) {
|
|
538
|
+
const ctx = useInkContext("InkViewport");
|
|
539
|
+
const scroll = useRef(null);
|
|
540
|
+
const custom = !!renderAnnotation;
|
|
541
|
+
const latest = useLatest({
|
|
542
|
+
ctx,
|
|
543
|
+
mode,
|
|
544
|
+
rotation,
|
|
545
|
+
gap,
|
|
546
|
+
buffer,
|
|
547
|
+
custom
|
|
548
|
+
});
|
|
549
|
+
const engine = useRef(null);
|
|
550
|
+
/** Page hosts, mounts and errors the engine reported; InkPage renders portals for them. */
|
|
551
|
+
const [pages, setPages] = useState([]);
|
|
552
|
+
/**
|
|
553
|
+
* Numbered before each build starts, so the pages a build mounts carry its own number. A
|
|
554
|
+
* rebuild gives each page a new host and mount, so pages from an earlier build must remount,
|
|
555
|
+
* not be reused.
|
|
556
|
+
*/
|
|
557
|
+
const buildCount = useRef(0);
|
|
558
|
+
/** The construction options the engine was last built with, so a rebuild runs only on a change. */
|
|
559
|
+
const builtWith = useRef(null);
|
|
560
|
+
/** The zoom and page last reported to the host. */
|
|
561
|
+
const lastReportedZoom = useRef(void 0);
|
|
562
|
+
const lastReportedPage = useRef(void 0);
|
|
563
|
+
useEffect(() => {
|
|
564
|
+
const { ctx: c, mode: m, rotation: r, gap: g, buffer: b, custom: cu } = latest.current;
|
|
565
|
+
const doc = c.document;
|
|
566
|
+
const el = scroll.current;
|
|
567
|
+
lastReportedZoom.current = void 0;
|
|
568
|
+
lastReportedPage.current = void 0;
|
|
569
|
+
if (!doc || !el || c.status.state !== "ready") return;
|
|
570
|
+
let first = true;
|
|
571
|
+
let stops = [];
|
|
572
|
+
buildCount.current += 1;
|
|
573
|
+
builtWith.current = {
|
|
574
|
+
gap: g,
|
|
575
|
+
buffer: b,
|
|
576
|
+
custom: cu
|
|
577
|
+
};
|
|
578
|
+
const e = mountViewer({
|
|
579
|
+
container: el,
|
|
580
|
+
document: doc,
|
|
581
|
+
zoom: c.zoomModel ?? "fit-width",
|
|
582
|
+
...c.pageModel === void 0 ? {} : { page: c.pageModel },
|
|
583
|
+
mode: m,
|
|
584
|
+
rotation: r,
|
|
585
|
+
gap: g,
|
|
586
|
+
buffer: b,
|
|
587
|
+
tool: c.tool,
|
|
588
|
+
customAnnotations: cu,
|
|
589
|
+
onActivate: (id) => latest.current.ctx.setActivated(id),
|
|
590
|
+
onBuild: (vp, tc) => {
|
|
591
|
+
latest.current.ctx.setViewport(vp, tc);
|
|
592
|
+
if (first) {
|
|
593
|
+
first = false;
|
|
594
|
+
lastReportedZoom.current = vp.zoom.get();
|
|
595
|
+
lastReportedPage.current = 0;
|
|
596
|
+
}
|
|
597
|
+
for (const stop of stops) stop();
|
|
598
|
+
const reportZoom = (z) => {
|
|
599
|
+
if (z === lastReportedZoom.current) return;
|
|
600
|
+
lastReportedZoom.current = z;
|
|
601
|
+
latest.current.ctx.reportZoom(z);
|
|
602
|
+
};
|
|
603
|
+
const reportPage = (p) => {
|
|
604
|
+
if (p === lastReportedPage.current) return;
|
|
605
|
+
lastReportedPage.current = p;
|
|
606
|
+
latest.current.ctx.reportPage(p);
|
|
607
|
+
};
|
|
608
|
+
stops = [vp.zoom.subscribe(reportZoom), vp.currentPage.subscribe(reportPage)];
|
|
609
|
+
reportZoom(vp.zoom.get());
|
|
610
|
+
reportPage(vp.currentPage.get());
|
|
611
|
+
},
|
|
612
|
+
onPageMount: (page, mount, host) => {
|
|
613
|
+
const build = buildCount.current;
|
|
614
|
+
const custom = builtWith.current?.custom ?? false;
|
|
615
|
+
setPages((prev) => [...prev, {
|
|
616
|
+
build,
|
|
617
|
+
page,
|
|
618
|
+
host,
|
|
619
|
+
mount,
|
|
620
|
+
error: null,
|
|
621
|
+
custom
|
|
622
|
+
}]);
|
|
623
|
+
},
|
|
624
|
+
onPageUnmount: (page) => setPages((prev) => prev.filter((p) => p.page !== page)),
|
|
625
|
+
onPageError: (page, err) => setPages((prev) => prev.map((p) => p.page === page ? {
|
|
626
|
+
...p,
|
|
627
|
+
error: err
|
|
628
|
+
} : p))
|
|
629
|
+
});
|
|
630
|
+
engine.current = e;
|
|
631
|
+
return () => {
|
|
632
|
+
for (const stop of stops) stop();
|
|
633
|
+
e.destroy();
|
|
634
|
+
engine.current = null;
|
|
635
|
+
builtWith.current = null;
|
|
636
|
+
c.setViewport(null, null);
|
|
637
|
+
setPages((prev) => prev.length === 0 ? prev : []);
|
|
638
|
+
};
|
|
639
|
+
}, [
|
|
640
|
+
ctx.document,
|
|
641
|
+
ctx.status.state,
|
|
642
|
+
latest
|
|
643
|
+
]);
|
|
644
|
+
useEffect(() => {
|
|
645
|
+
const built = builtWith.current;
|
|
646
|
+
if (!engine.current || !built) return;
|
|
647
|
+
if (built.gap === gap && built.buffer === buffer && built.custom === custom) return;
|
|
648
|
+
buildCount.current += 1;
|
|
649
|
+
builtWith.current = {
|
|
650
|
+
gap,
|
|
651
|
+
buffer,
|
|
652
|
+
custom
|
|
653
|
+
};
|
|
654
|
+
engine.current.rebuild({
|
|
655
|
+
gap,
|
|
656
|
+
buffer,
|
|
657
|
+
customAnnotations: custom
|
|
658
|
+
});
|
|
659
|
+
}, [
|
|
660
|
+
gap,
|
|
661
|
+
buffer,
|
|
662
|
+
custom
|
|
663
|
+
]);
|
|
664
|
+
useEffect(() => {
|
|
665
|
+
const vp = engine.current?.viewport;
|
|
666
|
+
const z = ctx.zoomModel;
|
|
667
|
+
if (vp && z !== void 0 && z !== vp.zoom.get()) vp.setZoom(z);
|
|
668
|
+
}, [ctx.zoomModel]);
|
|
669
|
+
useEffect(() => {
|
|
670
|
+
const vp = engine.current?.viewport;
|
|
671
|
+
const p = ctx.pageModel;
|
|
672
|
+
if (!vp || p === void 0 || p === lastReportedPage.current) return;
|
|
673
|
+
if (p !== vp.currentPage.get()) vp.scrollToPage(p);
|
|
674
|
+
}, [ctx.pageModel]);
|
|
675
|
+
useEffect(() => {
|
|
676
|
+
const tc = engine.current?.tools;
|
|
677
|
+
if (tc && tc.active.get() !== ctx.tool) tc.set(ctx.tool);
|
|
678
|
+
}, [ctx.tool]);
|
|
679
|
+
useEffect(() => {
|
|
680
|
+
const vp = engine.current?.viewport;
|
|
681
|
+
if (vp && vp.mode.get() !== mode) vp.setMode(mode);
|
|
682
|
+
}, [mode]);
|
|
683
|
+
useEffect(() => {
|
|
684
|
+
const vp = engine.current?.viewport;
|
|
685
|
+
if (vp && vp.rotation.get() !== rotation) vp.setRotation(rotation);
|
|
686
|
+
}, [rotation]);
|
|
687
|
+
const state = ctx.status.state;
|
|
688
|
+
const err = ctx.status.state === "error" ? ctx.status.error : null;
|
|
689
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
690
|
+
...rest,
|
|
691
|
+
className: cx("ink-viewport-root", className),
|
|
692
|
+
"data-ink-viewport-root": "",
|
|
693
|
+
"data-state": state,
|
|
694
|
+
children: [
|
|
695
|
+
state === "loading" ? /* @__PURE__ */ jsx("div", {
|
|
696
|
+
className: "ink-status",
|
|
697
|
+
role: "status",
|
|
698
|
+
"aria-live": "polite",
|
|
699
|
+
children: loading === void 0 ? "Loading document…" : loading
|
|
700
|
+
}) : err ? /* @__PURE__ */ jsx("div", {
|
|
701
|
+
className: "ink-status ink-error",
|
|
702
|
+
role: "alert",
|
|
703
|
+
children: typeof error === "function" ? error({ error: err }) : error !== void 0 ? error : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("p", {
|
|
704
|
+
className: "ink-error-title",
|
|
705
|
+
children: "This document could not be opened."
|
|
706
|
+
}), /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("code", {
|
|
707
|
+
className: "ink-error-code",
|
|
708
|
+
title: err.message,
|
|
709
|
+
children: err.code
|
|
710
|
+
}) })] })
|
|
711
|
+
}) : state === "idle" ? /* @__PURE__ */ jsx("div", {
|
|
712
|
+
className: "ink-status ink-empty",
|
|
713
|
+
children: empty
|
|
714
|
+
}) : null,
|
|
715
|
+
/* @__PURE__ */ jsx("div", {
|
|
716
|
+
ref: scroll,
|
|
717
|
+
className: "ink-scroll",
|
|
718
|
+
"data-ink-scroll": "",
|
|
719
|
+
tabIndex: 0,
|
|
720
|
+
role: "document",
|
|
721
|
+
"aria-label": "Document"
|
|
722
|
+
}),
|
|
723
|
+
pages.map((p) => /* @__PURE__ */ jsx(InkPage, {
|
|
724
|
+
page: p.page,
|
|
725
|
+
host: p.host,
|
|
726
|
+
mount: p.mount,
|
|
727
|
+
error: p.error,
|
|
728
|
+
customAnnotations: p.custom,
|
|
729
|
+
renderAnnotation,
|
|
730
|
+
renderPageError
|
|
731
|
+
}, `${p.build}:${p.page}`))
|
|
732
|
+
]
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
//#endregion
|
|
736
|
+
//#region src/components/InkToolbar.tsx
|
|
737
|
+
function InkToolbar({ label = "Document tools", asChild = false, className, children, ...rest }) {
|
|
738
|
+
return /* @__PURE__ */ jsx(Toolbar.Root, {
|
|
739
|
+
asChild,
|
|
740
|
+
orientation: "horizontal",
|
|
741
|
+
"aria-label": label,
|
|
742
|
+
"data-ink-toolbar": "",
|
|
743
|
+
className: cx("ink-toolbar", className),
|
|
744
|
+
...rest,
|
|
745
|
+
children
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
//#endregion
|
|
749
|
+
//#region src/components/InkIcon.tsx
|
|
750
|
+
/** `stroke-width` to `strokeWidth`: React names SVG attributes in camelCase. */
|
|
751
|
+
const camel = (name) => name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
752
|
+
const SVG_ATTRIBUTES = Object.fromEntries(Object.entries(ICON_ATTRIBUTES).map(([k, v]) => [camel(k), v]));
|
|
753
|
+
function InkIcon({ icon, size = 18 }) {
|
|
754
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
755
|
+
...SVG_ATTRIBUTES,
|
|
756
|
+
width: size,
|
|
757
|
+
height: size,
|
|
758
|
+
className: "ink-icon",
|
|
759
|
+
"aria-hidden": "true",
|
|
760
|
+
children: icon.map(([tag, attrs], i) => createElement(tag, {
|
|
761
|
+
key: i,
|
|
762
|
+
...attrs
|
|
763
|
+
}))
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
//#endregion
|
|
767
|
+
//#region src/components/InkToolButton.tsx
|
|
768
|
+
const ICONS = {
|
|
769
|
+
view: Hand,
|
|
770
|
+
select: MousePointer2,
|
|
771
|
+
highlight: Highlighter,
|
|
772
|
+
"redact-area": Square,
|
|
773
|
+
note: MessageSquare
|
|
774
|
+
};
|
|
775
|
+
function InkToolButton({ tool, label, asChild = false, className, children, onClick, ...rest }) {
|
|
776
|
+
const { tool: current, setTool } = useTool();
|
|
777
|
+
const active = current === tool;
|
|
778
|
+
const name = label ?? TOOL_LABELS[tool];
|
|
779
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
780
|
+
asChild,
|
|
781
|
+
className: cx("ink-tool-button", className),
|
|
782
|
+
"data-ink-tool-button": "",
|
|
783
|
+
"data-tool": tool,
|
|
784
|
+
"data-state": active ? "on" : "off",
|
|
785
|
+
"aria-pressed": active,
|
|
786
|
+
"aria-label": name,
|
|
787
|
+
title: name,
|
|
788
|
+
onClick: (e) => {
|
|
789
|
+
onClick?.(e);
|
|
790
|
+
if (!e.defaultPrevented) setTool(tool);
|
|
791
|
+
},
|
|
792
|
+
...rest,
|
|
793
|
+
children: typeof children === "function" ? children({
|
|
794
|
+
active,
|
|
795
|
+
label: name
|
|
796
|
+
}) : children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ICONS[tool] })
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
//#endregion
|
|
800
|
+
//#region src/components/InkToolbarSeparator.tsx
|
|
801
|
+
/**
|
|
802
|
+
* A divider between groups of controls in a horizontal toolbar. Not focusable. A plain div
|
|
803
|
+
* rather than Radix's separator, which reads as horizontal inside a horizontal toolbar.
|
|
804
|
+
*/
|
|
805
|
+
function InkToolbarSeparator({ className, ...rest }) {
|
|
806
|
+
return /* @__PURE__ */ jsx("div", {
|
|
807
|
+
role: "separator",
|
|
808
|
+
"aria-orientation": "vertical",
|
|
809
|
+
className: cx("ink-toolbar-separator", className),
|
|
810
|
+
"data-ink-toolbar-separator": "",
|
|
811
|
+
...rest
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
//#endregion
|
|
815
|
+
//#region src/components/InkUndo.tsx
|
|
816
|
+
function InkUndo({ asChild = false, className, children, disabled, onClick, ...rest }) {
|
|
817
|
+
const { canUndo, undo } = useHistory();
|
|
818
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
819
|
+
asChild,
|
|
820
|
+
className: cx("ink-undo", className),
|
|
821
|
+
"data-ink-undo": "",
|
|
822
|
+
"aria-label": "Undo",
|
|
823
|
+
title: "Undo",
|
|
824
|
+
...rest,
|
|
825
|
+
disabled: disabled === true || !canUndo,
|
|
826
|
+
onClick: (e) => {
|
|
827
|
+
onClick?.(e);
|
|
828
|
+
if (!e.defaultPrevented) undo();
|
|
829
|
+
},
|
|
830
|
+
children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Undo2 })
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
//#endregion
|
|
834
|
+
//#region src/components/InkRedo.tsx
|
|
835
|
+
function InkRedo({ asChild = false, className, children, disabled, onClick, ...rest }) {
|
|
836
|
+
const { canRedo, redo } = useHistory();
|
|
837
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
838
|
+
asChild,
|
|
839
|
+
className: cx("ink-redo", className),
|
|
840
|
+
"data-ink-redo": "",
|
|
841
|
+
"aria-label": "Redo",
|
|
842
|
+
title: "Redo",
|
|
843
|
+
...rest,
|
|
844
|
+
disabled: disabled === true || !canRedo,
|
|
845
|
+
onClick: (e) => {
|
|
846
|
+
onClick?.(e);
|
|
847
|
+
if (!e.defaultPrevented) redo();
|
|
848
|
+
},
|
|
849
|
+
children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Redo2 })
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
//#endregion
|
|
853
|
+
//#region src/components/InkZoomIn.tsx
|
|
854
|
+
function InkZoomIn({ asChild = false, className, children, onClick, ...rest }) {
|
|
855
|
+
const { zoomIn } = useZoom();
|
|
856
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
857
|
+
asChild,
|
|
858
|
+
className: cx("ink-zoom-in", className),
|
|
859
|
+
"data-ink-zoom-in": "",
|
|
860
|
+
"aria-label": "Zoom in",
|
|
861
|
+
title: "Zoom in",
|
|
862
|
+
...rest,
|
|
863
|
+
onClick: (e) => {
|
|
864
|
+
onClick?.(e);
|
|
865
|
+
if (!e.defaultPrevented) zoomIn();
|
|
866
|
+
},
|
|
867
|
+
children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ZoomIn })
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
//#endregion
|
|
871
|
+
//#region src/components/InkZoomOut.tsx
|
|
872
|
+
function InkZoomOut({ asChild = false, className, children, onClick, ...rest }) {
|
|
873
|
+
const { zoomOut } = useZoom();
|
|
874
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
875
|
+
asChild,
|
|
876
|
+
className: cx("ink-zoom-out", className),
|
|
877
|
+
"data-ink-zoom-out": "",
|
|
878
|
+
"aria-label": "Zoom out",
|
|
879
|
+
title: "Zoom out",
|
|
880
|
+
...rest,
|
|
881
|
+
onClick: (e) => {
|
|
882
|
+
onClick?.(e);
|
|
883
|
+
if (!e.defaultPrevented) zoomOut();
|
|
884
|
+
},
|
|
885
|
+
children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ZoomOut })
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
//#endregion
|
|
889
|
+
//#region src/components/InkZoomSelect.tsx
|
|
890
|
+
const CUSTOM = "custom:";
|
|
891
|
+
const level = (z) => ZOOM_LEVELS.find((l) => Math.abs(l - z) < .001);
|
|
892
|
+
const percent = (z) => `${Math.round(z * 100)}%`;
|
|
893
|
+
const FITS = [{
|
|
894
|
+
value: "fit-width",
|
|
895
|
+
label: "Fit width"
|
|
896
|
+
}, {
|
|
897
|
+
value: "fit-page",
|
|
898
|
+
label: "Fit page"
|
|
899
|
+
}];
|
|
900
|
+
function Item({ value, label }) {
|
|
901
|
+
return /* @__PURE__ */ jsxs(Select.Item, {
|
|
902
|
+
value,
|
|
903
|
+
className: "ink-select-item",
|
|
904
|
+
"data-ink-select-item": "",
|
|
905
|
+
children: [/* @__PURE__ */ jsx(Select.ItemText, { children: label }), /* @__PURE__ */ jsx(Select.ItemIndicator, {
|
|
906
|
+
className: "ink-select-indicator",
|
|
907
|
+
children: /* @__PURE__ */ jsx(InkIcon, {
|
|
908
|
+
icon: Check,
|
|
909
|
+
size: 16
|
|
910
|
+
})
|
|
911
|
+
})]
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
function InkZoomSelect({ className, ...rest }) {
|
|
915
|
+
const { zoom, scale, setZoom } = useZoom();
|
|
916
|
+
/** A numeric zoom that is not one of the levels (zoom in and out multiply). */
|
|
917
|
+
const custom = typeof zoom === "number" && level(zoom) === void 0 ? zoom : null;
|
|
918
|
+
const value = typeof zoom !== "number" ? zoom : custom === null ? String(level(zoom)) : `${CUSTOM}${zoom}`;
|
|
919
|
+
/** The levels, with a custom zoom in its place among them. */
|
|
920
|
+
const levels = ZOOM_LEVELS.map((l) => ({
|
|
921
|
+
z: l,
|
|
922
|
+
value: String(l),
|
|
923
|
+
label: percent(l)
|
|
924
|
+
}));
|
|
925
|
+
if (custom !== null) levels.push({
|
|
926
|
+
z: custom,
|
|
927
|
+
value: `${CUSTOM}${custom}`,
|
|
928
|
+
label: percent(scale)
|
|
929
|
+
});
|
|
930
|
+
levels.sort((a, b) => a.z - b.z);
|
|
931
|
+
function onValueChange(v) {
|
|
932
|
+
if (v === value) return;
|
|
933
|
+
const next = v === "fit-width" || v === "fit-page" ? v : Number(v.replace(CUSTOM, ""));
|
|
934
|
+
setZoom(next);
|
|
935
|
+
}
|
|
936
|
+
return /* @__PURE__ */ jsxs(Select.Root, {
|
|
937
|
+
value,
|
|
938
|
+
onValueChange,
|
|
939
|
+
children: [/* @__PURE__ */ jsxs(Select.Trigger, {
|
|
940
|
+
className: cx("ink-zoom-select ink-select-trigger", className),
|
|
941
|
+
"data-ink-zoom-select": "",
|
|
942
|
+
"aria-label": "Zoom",
|
|
943
|
+
...rest,
|
|
944
|
+
children: [/* @__PURE__ */ jsx(Select.Value, { className: "ink-select-value" }), /* @__PURE__ */ jsx(Select.Icon, {
|
|
945
|
+
className: "ink-select-icon",
|
|
946
|
+
children: /* @__PURE__ */ jsx(InkIcon, {
|
|
947
|
+
icon: ChevronDown,
|
|
948
|
+
size: 16
|
|
949
|
+
})
|
|
950
|
+
})]
|
|
951
|
+
}), /* @__PURE__ */ jsx(Select.Portal, { children: /* @__PURE__ */ jsx(Select.Content, {
|
|
952
|
+
className: "ink-select-content",
|
|
953
|
+
"data-ink-select-content": "",
|
|
954
|
+
position: "popper",
|
|
955
|
+
side: "top",
|
|
956
|
+
sideOffset: 4,
|
|
957
|
+
children: /* @__PURE__ */ jsxs(Select.Viewport, { children: [
|
|
958
|
+
FITS.map((o) => /* @__PURE__ */ jsx(Item, {
|
|
959
|
+
value: o.value,
|
|
960
|
+
label: o.label
|
|
961
|
+
}, o.value)),
|
|
962
|
+
/* @__PURE__ */ jsx(Select.Separator, { className: "ink-select-separator" }),
|
|
963
|
+
levels.map((o) => /* @__PURE__ */ jsx(Item, {
|
|
964
|
+
value: o.value,
|
|
965
|
+
label: o.label
|
|
966
|
+
}, o.value))
|
|
967
|
+
] })
|
|
968
|
+
}) })]
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
//#endregion
|
|
972
|
+
//#region src/components/InkPageInput.tsx
|
|
973
|
+
function InkPageInput({ className, onKeyDown: onKeyDownProp, ...rest }) {
|
|
974
|
+
const { current, count, goTo } = usePage();
|
|
975
|
+
const [draft, setDraft] = useState(String(current + 1));
|
|
976
|
+
useEffect(() => {
|
|
977
|
+
setDraft(String(current + 1));
|
|
978
|
+
}, [current]);
|
|
979
|
+
const input = useRef(null);
|
|
980
|
+
const latest = useLatest({
|
|
981
|
+
current,
|
|
982
|
+
count,
|
|
983
|
+
goTo
|
|
984
|
+
});
|
|
985
|
+
/** 1-based in the input, 0-based in the viewport. Out of range resets to the current page. */
|
|
986
|
+
const commit = useCallback((value) => {
|
|
987
|
+
const { current: page, count: total, goTo: go } = latest.current;
|
|
988
|
+
const n = Number(value);
|
|
989
|
+
if (Number.isInteger(n) && n >= 1 && n <= total) {
|
|
990
|
+
if (n - 1 !== page) go(n - 1);
|
|
991
|
+
} else setDraft(String(page + 1));
|
|
992
|
+
}, [latest]);
|
|
993
|
+
useEffect(() => {
|
|
994
|
+
const el = input.current;
|
|
995
|
+
if (!el) return;
|
|
996
|
+
const onNativeChange = () => commit(el.value);
|
|
997
|
+
el.addEventListener("change", onNativeChange);
|
|
998
|
+
return () => el.removeEventListener("change", onNativeChange);
|
|
999
|
+
}, [commit]);
|
|
1000
|
+
function onKeyDown(e) {
|
|
1001
|
+
onKeyDownProp?.(e);
|
|
1002
|
+
if (e.defaultPrevented || e.key !== "Enter") return;
|
|
1003
|
+
e.preventDefault();
|
|
1004
|
+
commit(draft);
|
|
1005
|
+
}
|
|
1006
|
+
return /* @__PURE__ */ jsx("input", {
|
|
1007
|
+
ref: input,
|
|
1008
|
+
className: cx("ink-page-input", className),
|
|
1009
|
+
"data-ink-page-input": "",
|
|
1010
|
+
type: "number",
|
|
1011
|
+
inputMode: "numeric",
|
|
1012
|
+
"aria-label": "Page",
|
|
1013
|
+
min: 1,
|
|
1014
|
+
max: count,
|
|
1015
|
+
value: draft,
|
|
1016
|
+
onChange: (e) => setDraft(e.target.value),
|
|
1017
|
+
onKeyDown,
|
|
1018
|
+
...rest
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
//#endregion
|
|
1022
|
+
//#region src/components/InkPageCount.tsx
|
|
1023
|
+
function InkPageCount({ className, ...rest }) {
|
|
1024
|
+
const { count } = usePage();
|
|
1025
|
+
return /* @__PURE__ */ jsx("span", {
|
|
1026
|
+
className: cx("ink-page-count", className),
|
|
1027
|
+
"data-ink-page-count": "",
|
|
1028
|
+
"aria-live": "polite",
|
|
1029
|
+
...rest,
|
|
1030
|
+
children: `of ${count}`
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
//#endregion
|
|
1034
|
+
//#region src/components/InkPrevPage.tsx
|
|
1035
|
+
function InkPrevPage({ asChild = false, className, children, disabled, onClick, ...rest }) {
|
|
1036
|
+
const { current, previous } = usePage();
|
|
1037
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
1038
|
+
asChild,
|
|
1039
|
+
className: cx("ink-prev-page", className),
|
|
1040
|
+
"data-ink-prev-page": "",
|
|
1041
|
+
"aria-label": "Previous page",
|
|
1042
|
+
title: "Previous page",
|
|
1043
|
+
disabled: disabled === true || current <= 0,
|
|
1044
|
+
...rest,
|
|
1045
|
+
onClick: (e) => {
|
|
1046
|
+
onClick?.(e);
|
|
1047
|
+
if (!e.defaultPrevented) previous();
|
|
1048
|
+
},
|
|
1049
|
+
children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ChevronLeft })
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
//#endregion
|
|
1053
|
+
//#region src/components/InkNextPage.tsx
|
|
1054
|
+
function InkNextPage({ asChild = false, className, children, disabled, onClick, ...rest }) {
|
|
1055
|
+
const { current, count, next } = usePage();
|
|
1056
|
+
return /* @__PURE__ */ jsx(Toolbar.Button, {
|
|
1057
|
+
asChild,
|
|
1058
|
+
className: cx("ink-next-page", className),
|
|
1059
|
+
"data-ink-next-page": "",
|
|
1060
|
+
"aria-label": "Next page",
|
|
1061
|
+
title: "Next page",
|
|
1062
|
+
disabled: disabled === true || current >= count - 1,
|
|
1063
|
+
...rest,
|
|
1064
|
+
onClick: (e) => {
|
|
1065
|
+
onClick?.(e);
|
|
1066
|
+
if (!e.defaultPrevented) next();
|
|
1067
|
+
},
|
|
1068
|
+
children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ChevronRight })
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
//#endregion
|
|
1072
|
+
//#region src/components/annotation-element.ts
|
|
1073
|
+
/**
|
|
1074
|
+
* The overlay element core draws for an annotation, for anchoring a popover to it. Core
|
|
1075
|
+
* creates and removes these elements outside React, so this follows the root's subtree while
|
|
1076
|
+
* an id is wanted.
|
|
1077
|
+
*/
|
|
1078
|
+
function useAnnotationElement(root, id) {
|
|
1079
|
+
const [element, setElement] = useState(null);
|
|
1080
|
+
useEffect(() => {
|
|
1081
|
+
if (!root || !id) {
|
|
1082
|
+
setElement(null);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
return observeAnnotationElement(root, id, setElement);
|
|
1086
|
+
}, [root, id]);
|
|
1087
|
+
return element;
|
|
1088
|
+
}
|
|
1089
|
+
//#endregion
|
|
1090
|
+
//#region src/components/popover-focus.ts
|
|
1091
|
+
/** Radix's `onInteractOutside` handler: a press that will activate an annotation keeps the popover open. */
|
|
1092
|
+
function keepOpenOnAnnotation(event) {
|
|
1093
|
+
if (shouldKeepOpenOnAnnotation(event.detail.originalEvent.target)) event.preventDefault();
|
|
1094
|
+
}
|
|
1095
|
+
function useFocusReturn(ctx) {
|
|
1096
|
+
const latest = useLatest(ctx);
|
|
1097
|
+
return useMemo(() => createFocusReturn({
|
|
1098
|
+
root: () => latest.current.rootEl,
|
|
1099
|
+
activated: () => latest.current.activated
|
|
1100
|
+
}), [latest]);
|
|
1101
|
+
}
|
|
1102
|
+
//#endregion
|
|
1103
|
+
//#region src/components/InkNotePopover.tsx
|
|
1104
|
+
function InkNotePopover({ children }) {
|
|
1105
|
+
const ctx = useInkContext("InkNotePopover");
|
|
1106
|
+
const { annotations, add, update, remove } = useAnnotations();
|
|
1107
|
+
const note = useMemo(() => {
|
|
1108
|
+
const a = ctx.activated ? annotations.find((x) => x.id === ctx.activated) : void 0;
|
|
1109
|
+
return a?.type === "note" ? threadRoot(annotations, a) : null;
|
|
1110
|
+
}, [annotations, ctx.activated]);
|
|
1111
|
+
const anchor = useAnnotationElement(ctx.rootEl, note?.id ?? null);
|
|
1112
|
+
const anchorRef = useMemo(() => ({ current: anchor }), [anchor]);
|
|
1113
|
+
const focusReturn = useFocusReturn(ctx);
|
|
1114
|
+
const open = note !== null && anchor !== null;
|
|
1115
|
+
/** The root, then every reply below it, depth first, each level in creation order. */
|
|
1116
|
+
const thread = useMemo(() => note ? threadOf(annotations, note) : [], [annotations, note]);
|
|
1117
|
+
/** True while the default form writes the root note's own text rather than a reply. */
|
|
1118
|
+
const editingRoot = note !== null && !note.locked && !note.contents?.trim();
|
|
1119
|
+
/**
|
|
1120
|
+
* Notes this popover wrote text or a reply for. In controlled mode the write is a proposal
|
|
1121
|
+
* the host may not have applied yet, so the applied list still shows the note empty; these
|
|
1122
|
+
* are never dropped as abandoned.
|
|
1123
|
+
*/
|
|
1124
|
+
const written = useRef(/* @__PURE__ */ new Set());
|
|
1125
|
+
/** Notes this popover has already asked to remove, so a slow host is asked once. */
|
|
1126
|
+
const dropped = useRef(/* @__PURE__ */ new Set());
|
|
1127
|
+
const latest = useLatest({
|
|
1128
|
+
annotations,
|
|
1129
|
+
remove
|
|
1130
|
+
});
|
|
1131
|
+
function dropIfEmpty(id) {
|
|
1132
|
+
if (dropped.current.has(id) || written.current.has(id) || !isAbandonedNote(latest.current.annotations, id)) return false;
|
|
1133
|
+
dropped.current.add(id);
|
|
1134
|
+
latest.current.remove(id);
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
const noteId = note?.id;
|
|
1138
|
+
const previousId = useRef(void 0);
|
|
1139
|
+
useEffect(() => {
|
|
1140
|
+
const previous = previousId.current;
|
|
1141
|
+
if (previous === noteId) return;
|
|
1142
|
+
previousId.current = noteId;
|
|
1143
|
+
if (previous !== void 0) dropIfEmpty(previous);
|
|
1144
|
+
}, [noteId]);
|
|
1145
|
+
function reply(text) {
|
|
1146
|
+
const root = note;
|
|
1147
|
+
const contents = text.trim();
|
|
1148
|
+
if (!root || !contents) return;
|
|
1149
|
+
written.current.add(root.id);
|
|
1150
|
+
add({
|
|
1151
|
+
type: "note",
|
|
1152
|
+
page: root.page,
|
|
1153
|
+
rect: root.rect,
|
|
1154
|
+
contents,
|
|
1155
|
+
inReplyTo: root.id
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
function isEmptyRoot(root) {
|
|
1159
|
+
return !written.current.has(root.id) && !root.locked && !root.contents?.trim() && thread.length === 1;
|
|
1160
|
+
}
|
|
1161
|
+
function close() {
|
|
1162
|
+
const root = note;
|
|
1163
|
+
if (root && annotations.some((a) => a.id === root.id) && isEmptyRoot(root)) {
|
|
1164
|
+
focusReturn.beforeRemove(anchor);
|
|
1165
|
+
dropIfEmpty(root.id);
|
|
1166
|
+
}
|
|
1167
|
+
ctx.setActivated(null);
|
|
1168
|
+
}
|
|
1169
|
+
function updateContents(contents) {
|
|
1170
|
+
if (!note || note.locked) return;
|
|
1171
|
+
if (contents.trim()) written.current.add(note.id);
|
|
1172
|
+
update(note.id, { contents });
|
|
1173
|
+
}
|
|
1174
|
+
/** Locked notes cannot be removed: removing one does nothing and keeps the popover. */
|
|
1175
|
+
function removeThread() {
|
|
1176
|
+
const root = note;
|
|
1177
|
+
if (!root || root.locked) return;
|
|
1178
|
+
focusReturn.beforeRemove(anchor);
|
|
1179
|
+
dropped.current.add(root.id);
|
|
1180
|
+
remove(root.id);
|
|
1181
|
+
ctx.setActivated(null);
|
|
1182
|
+
}
|
|
1183
|
+
return /* @__PURE__ */ jsxs(Popover.Root, {
|
|
1184
|
+
open,
|
|
1185
|
+
onOpenChange: (next) => !next && close(),
|
|
1186
|
+
children: [/* @__PURE__ */ jsx(Popover.Anchor, { virtualRef: anchorRef }), /* @__PURE__ */ jsx(Popover.Portal, { children: note && anchor ? /* @__PURE__ */ jsx(Popover.Content, {
|
|
1187
|
+
className: "ink-note-popover",
|
|
1188
|
+
"data-ink-note-popover": "",
|
|
1189
|
+
side: "right",
|
|
1190
|
+
sideOffset: 8,
|
|
1191
|
+
collisionPadding: 8,
|
|
1192
|
+
"aria-label": "Note thread",
|
|
1193
|
+
onInteractOutside: keepOpenOnAnnotation,
|
|
1194
|
+
onCloseAutoFocus: (e) => focusReturn.onCloseAutoFocus(e, anchor),
|
|
1195
|
+
children: children ? children({
|
|
1196
|
+
note,
|
|
1197
|
+
replies: thread.slice(1),
|
|
1198
|
+
reply,
|
|
1199
|
+
update: updateContents,
|
|
1200
|
+
remove: removeThread,
|
|
1201
|
+
close
|
|
1202
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [!editingRoot || thread.length > 1 ? /* @__PURE__ */ jsx("ol", {
|
|
1203
|
+
className: "ink-thread",
|
|
1204
|
+
"data-ink-thread": "",
|
|
1205
|
+
children: thread.map((item) => /* @__PURE__ */ jsxs("li", {
|
|
1206
|
+
className: "ink-thread-item",
|
|
1207
|
+
"data-ink-thread-item": "",
|
|
1208
|
+
"data-reply": item.inReplyTo ? "" : void 0,
|
|
1209
|
+
children: [
|
|
1210
|
+
/* @__PURE__ */ jsx("span", {
|
|
1211
|
+
className: "ink-thread-author",
|
|
1212
|
+
children: item.author ?? "Anonymous"
|
|
1213
|
+
}),
|
|
1214
|
+
/* @__PURE__ */ jsx("time", {
|
|
1215
|
+
className: "ink-thread-time",
|
|
1216
|
+
dateTime: item.createdAt,
|
|
1217
|
+
children: new Date(item.createdAt).toLocaleString()
|
|
1218
|
+
}),
|
|
1219
|
+
/* @__PURE__ */ jsx("p", {
|
|
1220
|
+
className: "ink-thread-text",
|
|
1221
|
+
children: item.contents
|
|
1222
|
+
})
|
|
1223
|
+
]
|
|
1224
|
+
}, item.id))
|
|
1225
|
+
}) : null, /* @__PURE__ */ jsx(ThreadForm, {
|
|
1226
|
+
editingRoot,
|
|
1227
|
+
locked: !!note.locked,
|
|
1228
|
+
onSubmit: (text) => editingRoot ? updateContents(text) : reply(text),
|
|
1229
|
+
onRemove: removeThread,
|
|
1230
|
+
onClose: close
|
|
1231
|
+
}, note.id)] })
|
|
1232
|
+
}) : null })]
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
/** The default form: the root note's own text while it is empty, replies after that. */
|
|
1236
|
+
function ThreadForm({ editingRoot, locked, onSubmit, onRemove, onClose }) {
|
|
1237
|
+
const [draft, setDraft] = useState("");
|
|
1238
|
+
function submit(e) {
|
|
1239
|
+
e.preventDefault();
|
|
1240
|
+
const text = draft.trim();
|
|
1241
|
+
if (!text) return;
|
|
1242
|
+
onSubmit(text);
|
|
1243
|
+
setDraft("");
|
|
1244
|
+
}
|
|
1245
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
1246
|
+
className: "ink-thread-reply",
|
|
1247
|
+
"data-ink-thread-reply": "",
|
|
1248
|
+
onSubmit: submit,
|
|
1249
|
+
children: [/* @__PURE__ */ jsx("textarea", {
|
|
1250
|
+
"aria-label": editingRoot ? "Comment" : "Reply",
|
|
1251
|
+
rows: 2,
|
|
1252
|
+
placeholder: editingRoot ? "Comment" : "Reply",
|
|
1253
|
+
value: draft,
|
|
1254
|
+
onChange: (e) => setDraft(e.target.value)
|
|
1255
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1256
|
+
className: "ink-thread-actions",
|
|
1257
|
+
children: [
|
|
1258
|
+
/* @__PURE__ */ jsx("button", {
|
|
1259
|
+
type: "submit",
|
|
1260
|
+
disabled: !draft.trim(),
|
|
1261
|
+
children: editingRoot ? "Comment" : "Reply"
|
|
1262
|
+
}),
|
|
1263
|
+
!locked ? /* @__PURE__ */ jsx("button", {
|
|
1264
|
+
type: "button",
|
|
1265
|
+
onClick: onRemove,
|
|
1266
|
+
children: "Delete"
|
|
1267
|
+
}) : null,
|
|
1268
|
+
/* @__PURE__ */ jsx("button", {
|
|
1269
|
+
type: "button",
|
|
1270
|
+
onClick: onClose,
|
|
1271
|
+
children: "Close"
|
|
1272
|
+
})
|
|
1273
|
+
]
|
|
1274
|
+
})]
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
//#endregion
|
|
1278
|
+
//#region src/components/InkSuggestionCard.tsx
|
|
1279
|
+
function InkSuggestionCard({ children }) {
|
|
1280
|
+
const ctx = useInkContext("InkSuggestionCard");
|
|
1281
|
+
const { annotations, selection, accept: acceptId, dismiss: dismissId, select } = useAnnotations();
|
|
1282
|
+
function suggestion(id) {
|
|
1283
|
+
const a = id ? annotations.find((x) => x.id === id) : void 0;
|
|
1284
|
+
return a?.type === "redact" && a.origin === "suggested" ? a : null;
|
|
1285
|
+
}
|
|
1286
|
+
const target = suggestion(ctx.activated) ?? (selection.length === 1 ? suggestion(selection[0]) : null);
|
|
1287
|
+
const anchor = useAnnotationElement(ctx.rootEl, target?.id ?? null);
|
|
1288
|
+
const anchorRef = useMemo(() => ({ current: anchor }), [anchor]);
|
|
1289
|
+
const focusReturn = useFocusReturn(ctx);
|
|
1290
|
+
const open = target !== null && anchor !== null;
|
|
1291
|
+
const bucket = target?.suggestion ? confidenceBucket(target.suggestion.confidence) : void 0;
|
|
1292
|
+
function close() {
|
|
1293
|
+
ctx.setActivated(null);
|
|
1294
|
+
select([]);
|
|
1295
|
+
}
|
|
1296
|
+
function accept() {
|
|
1297
|
+
if (target) acceptId(target.id);
|
|
1298
|
+
close();
|
|
1299
|
+
}
|
|
1300
|
+
/** Locked annotations cannot be removed: dismissing one does nothing and keeps the card. */
|
|
1301
|
+
function dismiss() {
|
|
1302
|
+
if (!target || target.locked) return;
|
|
1303
|
+
focusReturn.beforeRemove(anchor, "[data-origin=\"suggested\"]");
|
|
1304
|
+
dismissId(target.id);
|
|
1305
|
+
close();
|
|
1306
|
+
}
|
|
1307
|
+
return /* @__PURE__ */ jsxs(Popover.Root, {
|
|
1308
|
+
open,
|
|
1309
|
+
onOpenChange: (next) => !next && close(),
|
|
1310
|
+
children: [/* @__PURE__ */ jsx(Popover.Anchor, { virtualRef: anchorRef }), /* @__PURE__ */ jsx(Popover.Portal, { children: target && anchor ? /* @__PURE__ */ jsx(Popover.Content, {
|
|
1311
|
+
className: "ink-suggestion-card",
|
|
1312
|
+
"data-ink-suggestion-card": "",
|
|
1313
|
+
side: "bottom",
|
|
1314
|
+
sideOffset: 6,
|
|
1315
|
+
collisionPadding: 8,
|
|
1316
|
+
"aria-label": "Suggested redaction",
|
|
1317
|
+
onInteractOutside: keepOpenOnAnnotation,
|
|
1318
|
+
onCloseAutoFocus: (e) => focusReturn.onCloseAutoFocus(e, anchor),
|
|
1319
|
+
children: children ? children({
|
|
1320
|
+
annotation: target,
|
|
1321
|
+
suggestion: target.suggestion,
|
|
1322
|
+
bucket,
|
|
1323
|
+
accept,
|
|
1324
|
+
dismiss,
|
|
1325
|
+
close
|
|
1326
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1327
|
+
/* @__PURE__ */ jsxs("p", {
|
|
1328
|
+
className: "ink-suggestion-reason",
|
|
1329
|
+
children: [/* @__PURE__ */ jsx("strong", { children: target.suggestion?.reason ?? "Suggested redaction" }), bucket ? /* @__PURE__ */ jsxs("span", {
|
|
1330
|
+
className: "ink-suggestion-confidence",
|
|
1331
|
+
"data-confidence": bucket,
|
|
1332
|
+
children: [bucket, " confidence"]
|
|
1333
|
+
}) : null]
|
|
1334
|
+
}),
|
|
1335
|
+
target.suggestion?.explanation ? /* @__PURE__ */ jsx("p", {
|
|
1336
|
+
className: "ink-suggestion-explanation",
|
|
1337
|
+
children: target.suggestion.explanation
|
|
1338
|
+
}) : null,
|
|
1339
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1340
|
+
className: "ink-suggestion-actions",
|
|
1341
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1342
|
+
type: "button",
|
|
1343
|
+
onClick: accept,
|
|
1344
|
+
children: "Accept"
|
|
1345
|
+
}), !target.locked ? /* @__PURE__ */ jsx("button", {
|
|
1346
|
+
type: "button",
|
|
1347
|
+
onClick: dismiss,
|
|
1348
|
+
children: "Dismiss"
|
|
1349
|
+
}) : null]
|
|
1350
|
+
})
|
|
1351
|
+
] })
|
|
1352
|
+
}) : null })]
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
//#endregion
|
|
1356
|
+
export { InkAnnotation, InkIcon, InkNextPage, InkNotePopover, InkPage, InkPageCount, InkPageInput, InkPrevPage, InkRedo, InkRoot, InkSuggestionCard, InkToolButton, InkToolbar, InkToolbarSeparator, InkUndo, InkViewport, InkZoomIn, InkZoomOut, InkZoomSelect, useAnnotations, useDocument, useEmbedded, useHistory, useInkContext, usePage, useReadable, useSelection, useTool, useZoom };
|
|
1357
|
+
|
|
1358
|
+
//# sourceMappingURL=index.mjs.map
|