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