@inkbar/react 0.8.0 → 0.10.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 CHANGED
@@ -1,11 +1,11 @@
1
1
  "use client";
2
2
  import { Fragment, createContext, createElement, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
3
  import { InkError, createMemorySignatureStore, createSignatureLibrary, createSnapshotHistory, isAbandonedNote, openDocument, readonly, repliesTo, signField, threadOf, threadRoot, writable } from "@inkbar/core";
4
- import { COLOR_COLUMNS, FONT_FAMILIES, FORM_LABELS, MARKUP_ICONS, MARKUP_KINDS, SHAPE_COLUMNS, SHAPE_ICONS, SHAPE_KINDS, SIGNATURE_FONTS, SIGNATURE_LABELS, SIGNATURE_TABS, TOOL_LABELS, ZOOM_LEVELS, attachHistoryKeys, colorMenuModel, createFocusReturn, createPluginHost, createSearchController, createSignatureDraft, createToolOptions, cssVariableDefaults, findAnnouncer, findCountText, focusInitialMenuItem, fontFamilyValue, handleFindKey, handleMenuKeys, handleRadioKeys, handleTabKeys, highlightMenuModel, keepHistoryFocus, lineStyleMenuModel, menuItems, mountViewer, observeAnnotationElement, pageSelectionText, shapeMenuModel, shouldKeepOpenOnAnnotation, signatureMenuModel, signaturePreview, textStyleMenuModel, threadDeleteLabel, threadPopoverLabel, trackToolStyle, unknownCommand, viewerSearchOf, watchDetached } from "@inkbar/core/dom";
4
+ import { COLOR_COLUMNS, FONT_FAMILIES, FORM_LABELS, MARKUP_ICONS, MARKUP_KINDS, SHAPE_COLUMNS, SHAPE_ICONS, SHAPE_KINDS, SIGNATURE_FONTS, SIGNATURE_LABELS, SIGNATURE_TABS, THUMBNAIL_WIDTH, TOOL_LABELS, ZOOM_LEVELS, attachFileKeys, attachHistoryKeys, colorMenuModel, createFocusReturn, createPluginHost, createRecordListController, createSearchController, createSidebarController, createSignatureDraft, createThumbnailPainter, createThumbnailsController, createToolOptions, cssVariableDefaults, downloadDocument, findAnnouncer, findCountText, focusInitialMenuItem, fontFamilyValue, handleFindKey, handleMenuKeys, handleRadioKeys, handleResultsFieldKey, handleTabKeys, highlightMenuModel, isBlankQuery, keepHistoryFocus, lineStyleMenuModel, menuItems, mountViewer, observeAnnotationElement, pageSelectionText, printDocument, recordsSource, searchSource, shapeMenuModel, shouldKeepOpenOnAnnotation, sidebarBadge, signatureMenuModel, signaturePreview, textStyleMenuModel, threadDeleteLabel, threadPopoverLabel, trackToolStyle, unknownCommand, unknownTool, viewerSearchOf, watchDetached, withHostTools } from "@inkbar/core/dom";
5
5
  import { Dialog, Popover, Select, Slot, Toolbar } from "radix-ui";
6
6
  import { createPortal, flushSync } from "react-dom";
7
7
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
- import { ALargeSmall, AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, CaseSensitive, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Highlighter, ICON_ATTRIBUTES, Italic, MessageSquare, Minus, PenLine, PenTool, Plus, Redo2, Search, Signature, Square, SquareDashedMousePointer, TextCursor, Trash2, Type, Underline, Undo2, X, ZoomIn, ZoomOut } from "@inkbar/icons";
8
+ import { ALargeSmall, AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, CaseSensitive, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Download, ICON_ATTRIBUTES, Italic, Minus, Plus, Printer, Redo2, Search, Signature, SquareDashedMousePointer, TextCursor, Trash2, Underline, Undo2, X, ZoomIn, ZoomOut } from "@inkbar/icons";
9
9
  //#region src/context.ts
10
10
  const InkContextObject = createContext(null);
11
11
  /**
@@ -19,14 +19,14 @@ function useInkContext(consumer) {
19
19
  }
20
20
  //#endregion
21
21
  //#region src/use-readable.ts
22
- const noop$2 = () => void 0;
22
+ const noop$3 = () => void 0;
23
23
  /**
24
24
  * Mirrors a core `Readable` into React. A missing readable yields `fallback`, which must be a
25
25
  * stable reference (a module constant): `useSyncExternalStore` compares snapshots by identity
26
26
  * and loops on a fresh value. On the server the fallback is rendered.
27
27
  */
28
28
  function useReadable(readable, fallback) {
29
- const subscribe = useCallback((onChange) => readable ? readable.subscribe(() => onChange()) : noop$2, [readable]);
29
+ const subscribe = useCallback((onChange) => readable ? readable.subscribe(() => onChange()) : noop$3, [readable]);
30
30
  const get = useCallback(() => readable ? readable.get() : fallback, [readable, fallback]);
31
31
  const server = useCallback(() => fallback, [fallback]);
32
32
  return useSyncExternalStore(subscribe, get, server);
@@ -87,7 +87,7 @@ function useTool() {
87
87
  //#region src/hooks/useToolStyle.ts
88
88
  const NO_STYLE = {};
89
89
  const NO_PROPS = /* @__PURE__ */ new Set();
90
- const EMPTY = {};
90
+ const EMPTY$1 = {};
91
91
  function useToolStyle(group) {
92
92
  const style = useInkContext("useToolStyle()").toolStyle;
93
93
  const scope = group && style ? style.scoped(group) : void 0;
@@ -98,7 +98,7 @@ function useToolStyle(group) {
98
98
  current,
99
99
  supported,
100
100
  set: (patch, opts) => scope ? scope.set(patch) : style?.set(patch, opts),
101
- get: (g) => style?.get(g) ?? EMPTY
101
+ get: (g) => style?.get(g) ?? EMPTY$1
102
102
  }), [
103
103
  current,
104
104
  supported,
@@ -156,7 +156,7 @@ const CLOSED = {
156
156
  current: -1,
157
157
  scanning: false
158
158
  };
159
- const noop$1 = () => void 0;
159
+ const noop$2 = () => void 0;
160
160
  /**
161
161
  * The document search for hosts building their own control: the state, and the actions the
162
162
  * find bar uses. Search is set up once InkRoot has mounted, so until then (and on the server)
@@ -167,12 +167,13 @@ function useSearch() {
167
167
  const search = useInkContext("useSearch()").search;
168
168
  return {
169
169
  state: useReadable(search?.state, CLOSED),
170
- open: search ? (query) => search.open(query) : noop$1,
171
- setQuery: search ? (query) => search.setQuery(query) : noop$1,
172
- setMatchCase: search ? (on) => search.setMatchCase(on) : noop$1,
173
- next: search ? () => search.next() : noop$1,
174
- previous: search ? () => search.previous() : noop$1,
175
- close: search ? () => search.close() : noop$1
170
+ open: search ? (query) => search.open(query) : noop$2,
171
+ setQuery: search ? (query) => search.setQuery(query) : noop$2,
172
+ setMatchCase: search ? (on) => search.setMatchCase(on) : noop$2,
173
+ next: search ? () => search.next() : noop$2,
174
+ previous: search ? () => search.previous() : noop$2,
175
+ select: search ? (index) => search.select(index) : noop$2,
176
+ close: search ? () => search.close() : noop$2
176
177
  };
177
178
  }
178
179
  //#endregion
@@ -209,9 +210,9 @@ function useHistory() {
209
210
  }
210
211
  //#endregion
211
212
  //#region src/hooks/useEmbedded.ts
212
- const NONE = [];
213
+ const NONE$1 = [];
213
214
  function useEmbedded() {
214
- const summary = useInkContext("useEmbedded()").document?.embedded ?? NONE;
215
+ const summary = useInkContext("useEmbedded()").document?.embedded ?? NONE$1;
215
216
  return useMemo(() => ({
216
217
  summary,
217
218
  total: summary.reduce((n, s) => n + s.count, 0),
@@ -277,6 +278,41 @@ function useForm() {
277
278
  ]);
278
279
  }
279
280
  //#endregion
281
+ //#region src/hooks/useRecords.ts
282
+ const NONE = Object.freeze([]);
283
+ /**
284
+ * The records a plugin keeps under `key` (`<plugin>.<name>`), for host components: the live list
285
+ * for the open document, and the handle to read and write them, each write one undo step.
286
+ * Plugins are set up once InkRoot has mounted, so until then (and on the server) `handle` is
287
+ * null and `records` is empty. `T` is the plugin's record type.
288
+ */
289
+ function useRecords(key) {
290
+ const { pluginHost } = useInkContext("useRecords()");
291
+ const handle = pluginHost?.records(key) ?? null;
292
+ return {
293
+ records: useReadable(handle?.list, NONE),
294
+ handle
295
+ };
296
+ }
297
+ //#endregion
298
+ //#region src/hooks/useRecordSelection.ts
299
+ const noop$1 = () => void 0;
300
+ /**
301
+ * The selected record under `key` (`<plugin>.<name>`): the one selection the marks and every
302
+ * record list over those records share. `select` takes an id, or null to clear; an id that has
303
+ * no mark changes nothing. Until InkRoot has mounted (and on the server) nothing is selected
304
+ * and `select` does nothing.
305
+ */
306
+ function useRecordSelection(key) {
307
+ const { pluginHost } = useInkContext("useRecordSelection()");
308
+ const selection = pluginHost?.selection(key) ?? null;
309
+ const select = useCallback((id) => selection ? selection.select(id) : noop$1(), [selection]);
310
+ return {
311
+ selected: useReadable(selection?.selected, null),
312
+ select
313
+ };
314
+ }
315
+ //#endregion
280
316
  //#region src/use-latest.ts
281
317
  /**
282
318
  * A ref that always holds the latest render's value, for callbacks that outlive a render
@@ -292,6 +328,51 @@ function useLatest(value) {
292
328
  return ref;
293
329
  }
294
330
  //#endregion
331
+ //#region src/hooks/useRegisteredTool.ts
332
+ /** `select-text` and `select`, then the host's tools, then the plugins' tools, by id. */
333
+ function toolIdsOf(hostTools, pluginHost) {
334
+ return withHostTools([...hostTools ?? [], ...pluginHost?.tools ?? []]).map((t) => t.id);
335
+ }
336
+ /**
337
+ * The tools a control may use: those in `tools` and the plugins now that the open viewer was
338
+ * also built with, so a control never offers a tool the viewer would refuse. A tool added to
339
+ * `tools` counts from the next open; one removed stops at once. With no viewer, `tools` and
340
+ * the plugins alone.
341
+ */
342
+ function registeredToolIds(hostTools, pluginHost, opened) {
343
+ const ids = toolIdsOf(hostTools, pluginHost);
344
+ return opened ? ids.filter((id) => opened.includes(id)) : ids;
345
+ }
346
+ const idsIn = (ctx) => registeredToolIds(ctx.hostTools, ctx.pluginHost, ctx.openedToolIds);
347
+ /** Whether a tool by this id is registered, without logging anything when it is not. */
348
+ function useHasTool(id) {
349
+ return idsIn(useInkContext("useHasTool()")).includes(id);
350
+ }
351
+ /**
352
+ * Whether a tool by this id is registered: always for `select-text` and `select`, else when
353
+ * InkRoot's `tools` or a plugin has it. When it is not, once the plugins are set up, this logs
354
+ * `InkError('UnknownTool')` to the console, once per component.
355
+ */
356
+ function useRegisteredTool(id) {
357
+ const ctx = useInkContext("useRegisteredTool()");
358
+ const ids = idsIn(ctx);
359
+ const registered = ids.includes(id);
360
+ const ready = ctx.pluginHost !== null;
361
+ const logged = useRef(false);
362
+ const latestIds = useLatest(ids);
363
+ useEffect(() => {
364
+ if (!ready || registered || logged.current) return;
365
+ logged.current = true;
366
+ console.error(unknownTool(id, latestIds.current));
367
+ }, [
368
+ ready,
369
+ registered,
370
+ id,
371
+ latestIds
372
+ ]);
373
+ return registered;
374
+ }
375
+ //#endregion
295
376
  //#region src/components/InkRoot.tsx
296
377
  const IDLE$1 = { state: "idle" };
297
378
  const NO_ANNOTATIONS = [];
@@ -304,7 +385,7 @@ function identity(src) {
304
385
  if ("key" in src) return src.key;
305
386
  return src;
306
387
  }
307
- function isDevelopment() {
388
+ function isDevelopment$1() {
308
389
  try {
309
390
  return process.env.NODE_ENV !== "production";
310
391
  } catch {
@@ -312,7 +393,7 @@ function isDevelopment() {
312
393
  }
313
394
  }
314
395
  function setTestHook(el, doc) {
315
- if (el && isDevelopment()) el.__inkDocument = doc;
396
+ if (el && isDevelopment$1()) el.__inkDocument = doc;
316
397
  }
317
398
  function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsChange, onChange, fields, onFieldsChange, onFormAction, formHighlight, tool, onToolChange, tools: hostTools, plugins: pluginsProp, decorateAnnotation, fonts: fontsProp, signatures: signatureStore, signatureFonts: signatureFontsProp, page, onPageChange, zoom, onZoomChange, history, asChild = false, onReady, onError, onActivate, children, ref, ...rest }) {
318
399
  const rootElRef = useRef(null);
@@ -567,15 +648,57 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
567
648
  useLayoutEffect(() => {
568
649
  if (fields && document) document.form.setControlledValue(fields);
569
650
  }, [fields, document]);
651
+ const ready = pluginHost !== null;
652
+ /** The ids of the tools the current viewer was built with; InkViewport sets them. */
653
+ const [openedToolIds, setOpenedToolIds] = useState(null);
654
+ const toolKey = registeredToolIds(hostTools, pluginHost, openedToolIds).join("\n");
655
+ const toolIds = useMemo(() => toolKey.split("\n"), [toolKey]);
656
+ const registration = useLatest({
657
+ ready,
658
+ toolIds
659
+ });
660
+ const isRegistered = useCallback((id) => {
661
+ const r = registration.current;
662
+ return !r.ready || r.toolIds.includes(id);
663
+ }, [registration]);
664
+ const logUnknownTool = useCallback((id) => console.error(unknownTool(id, registration.current.toolIds)), [registration]);
665
+ const fallBack = useCallback((id) => {
666
+ logUnknownTool(id);
667
+ setInternalTool("select-text");
668
+ latest.current.onToolChange?.("select-text");
669
+ }, [latest, logUnknownTool]);
670
+ /** The `tool` prop last applied, so a host that keeps passing it does not apply it again. */
671
+ const appliedTool = useRef(tool);
570
672
  useEffect(() => {
571
- if (tool) setInternalTool(tool);
572
- }, [tool]);
673
+ let next = internalTool;
674
+ if (tool !== appliedTool.current) {
675
+ appliedTool.current = tool;
676
+ if (tool) next = tool;
677
+ }
678
+ if (!isRegistered(next)) fallBack(next);
679
+ else if (next !== internalTool) setInternalTool(next);
680
+ }, [
681
+ tool,
682
+ ready,
683
+ toolIds,
684
+ internalTool,
685
+ isRegistered,
686
+ fallBack
687
+ ]);
573
688
  const setTool = useCallback((id) => {
689
+ if (!isRegistered(id)) {
690
+ logUnknownTool(id);
691
+ return;
692
+ }
574
693
  const tc = latest.current.tools;
575
694
  if (latest.current.tool === id && tc?.active.get() === id) tc.set(id);
576
695
  setInternalTool(id);
577
696
  latest.current.onToolChange?.(id);
578
- }, [latest]);
697
+ }, [
698
+ latest,
699
+ isRegistered,
700
+ logUnknownTool
701
+ ]);
579
702
  const reportZoom = useCallback((z) => latest.current.onZoomChange?.(z), [latest]);
580
703
  const reportPage = useCallback((p) => latest.current.onPageChange?.(p), [latest]);
581
704
  const reportFormAction = useCallback((a) => {
@@ -636,9 +759,11 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
636
759
  };
637
760
  });
638
761
  const stopFocus = keepHistoryFocus(rootEl, () => rootEl.querySelector("[data-ink-scroll]"));
762
+ const stopFileKeys = attachFileKeys(rootEl, () => documentRef.current);
639
763
  return () => {
640
764
  stopKeys();
641
765
  stopFocus();
766
+ stopFileKeys();
642
767
  };
643
768
  }, [
644
769
  rootEl,
@@ -707,6 +832,8 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
707
832
  tool: internalTool,
708
833
  setTool,
709
834
  hostTools,
835
+ openedToolIds,
836
+ setOpenedToolIds,
710
837
  decorateAnnotation: decorate,
711
838
  onDecorateAnnotationChange,
712
839
  fonts,
@@ -740,6 +867,7 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
740
867
  internalTool,
741
868
  setTool,
742
869
  hostTools,
870
+ openedToolIds,
743
871
  decorate,
744
872
  onDecorateAnnotationChange,
745
873
  fonts,
@@ -1005,7 +1133,7 @@ const IDLE = {
1005
1133
  function SignatureForm({ fonts, select, onSave, onClose }) {
1006
1134
  const ctx = useInkContext("InkSignatureDialog");
1007
1135
  const { setTool } = useTool();
1008
- const [, setOptions] = useToolOptions("signature");
1136
+ const hasSignatureTool = useHasTool("signature");
1009
1137
  const [draft] = useState(() => createSignatureDraft({ fonts: fonts ?? ctx.signatureFonts }));
1010
1138
  const s = useReadable(draft.state, IDLE);
1011
1139
  const uid = useId();
@@ -1021,8 +1149,9 @@ function SignatureForm({ fonts, select, onSave, onClose }) {
1021
1149
  const save = () => {
1022
1150
  draft.save(ctx.signatures).then((t) => {
1023
1151
  if (!t) return;
1024
- if (select) {
1025
- setOptions({ templateId: t.id });
1152
+ if (select && hasSignatureTool) {
1153
+ const patch = { templateId: t.id };
1154
+ ctx.tools?.setOptions("signature", patch);
1026
1155
  setTool("signature");
1027
1156
  }
1028
1157
  onSave?.(t);
@@ -1456,6 +1585,7 @@ function InkViewport({ mode = "continuous", rotation = 0, gap = 16, buffer = 2,
1456
1585
  buffer: b,
1457
1586
  custom: cu
1458
1587
  };
1588
+ c.setOpenedToolIds(toolIdsOf(c.hostTools, c.pluginHost));
1459
1589
  const e = mountViewer({
1460
1590
  container: el,
1461
1591
  document: doc,
@@ -1538,6 +1668,7 @@ function InkViewport({ mode = "continuous", rotation = 0, gap = 16, buffer = 2,
1538
1668
  engine.current = null;
1539
1669
  builtWith.current = null;
1540
1670
  c.setViewport(null, null);
1671
+ c.setOpenedToolIds(null);
1541
1672
  setPages((prev) => prev.length === 0 ? prev : []);
1542
1673
  setSignRequest(null);
1543
1674
  setAlert(null);
@@ -1680,21 +1811,23 @@ function InkToolbar({ label = "Document tools", asChild = false, className, chil
1680
1811
  //#region src/components/InkToolButton.tsx
1681
1812
  const ICONS = {
1682
1813
  "select-text": TextCursor,
1683
- select: SquareDashedMousePointer,
1684
- highlight: Highlighter,
1685
- "redact-area": Square,
1686
- sketch: PenTool,
1687
- draw: PenLine,
1688
- note: MessageSquare,
1689
- freetext: Type,
1690
- signature: Signature
1814
+ select: SquareDashedMousePointer
1815
+ };
1816
+ const MISSING = {
1817
+ disabled: true,
1818
+ "aria-hidden": true,
1819
+ "aria-pressed": void 0,
1820
+ "aria-label": void 0,
1821
+ title: void 0,
1822
+ onClick: void 0
1691
1823
  };
1692
1824
  function InkToolButton({ tool, label, asChild = false, className, children, onClick, ...rest }) {
1693
1825
  const { hostTools, pluginHost } = useInkContext("InkToolButton");
1694
1826
  const { tool: current, setTool } = useTool();
1695
1827
  const active = current === tool;
1696
- /** A host or plugin tool by this id, for the name and icon core has none for. */
1697
- const own = [...hostTools ?? [], ...pluginHost?.tools ?? []].find((t) => t.id === tool);
1828
+ const registered = useRegisteredTool(tool);
1829
+ /** The registered tool by this id, for the name and icon core has none for. */
1830
+ const own = withHostTools([...hostTools ?? [], ...pluginHost?.tools ?? []]).find((t) => t.id === tool);
1698
1831
  const name = label ?? TOOL_LABELS[tool] ?? own?.label ?? tool;
1699
1832
  const icon = ICONS[tool] ?? own?.icon;
1700
1833
  return /* @__PURE__ */ jsx(Toolbar.Button, {
@@ -1711,7 +1844,8 @@ function InkToolButton({ tool, label, asChild = false, className, children, onCl
1711
1844
  if (!e.defaultPrevented) setTool(tool);
1712
1845
  },
1713
1846
  ...rest,
1714
- children: typeof children === "function" ? children({
1847
+ ...registered ? {} : MISSING,
1848
+ children: !registered ? null : typeof children === "function" ? children({
1715
1849
  active,
1716
1850
  label: name
1717
1851
  }) : children ?? (icon ? /* @__PURE__ */ jsx(InkIcon, { icon }) : null)
@@ -1750,6 +1884,21 @@ function InkCommand({ id, asChild = false, children, disabled, onClick, ...rest
1750
1884
  });
1751
1885
  }
1752
1886
  //#endregion
1887
+ //#region src/components/InkToolbarButton.tsx
1888
+ /**
1889
+ * Your own button in an `InkToolbar`. It joins the toolbar's arrow-key focus and takes the
1890
+ * theme's toolbar button styles; what it shows and does is up to you.
1891
+ */
1892
+ function InkToolbarButton({ asChild = false, type = "button", className, ...rest }) {
1893
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
1894
+ asChild,
1895
+ type,
1896
+ className: cx("ink-toolbar-button", className),
1897
+ "data-ink-toolbar-button": "",
1898
+ ...rest
1899
+ });
1900
+ }
1901
+ //#endregion
1753
1902
  //#region src/components/InkToolbarSeparator.tsx
1754
1903
  /**
1755
1904
  * A divider between groups of controls in a horizontal toolbar. Not focusable. A plain div
@@ -1784,6 +1933,56 @@ function InkUndo({ asChild = false, className, children, disabled, onClick, ...r
1784
1933
  });
1785
1934
  }
1786
1935
  //#endregion
1936
+ //#region src/components/InkDownloadButton.tsx
1937
+ /**
1938
+ * Saves the open document's file through the browser's download, under its `fileName` or
1939
+ * `filename`. Disabled until the document is ready. Today that is the file as it was opened,
1940
+ * without this session's annotations.
1941
+ */
1942
+ function InkDownloadButton({ asChild = false, filename, className, children, disabled, onClick, ...rest }) {
1943
+ const { document: doc, status } = useInkContext("InkDownloadButton");
1944
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
1945
+ asChild,
1946
+ className: cx("ink-download", className),
1947
+ "data-ink-download": "",
1948
+ "aria-label": "Download",
1949
+ title: "Download",
1950
+ ...rest,
1951
+ disabled: disabled === true || doc === null || status.state !== "ready",
1952
+ onClick: (e) => {
1953
+ onClick?.(e);
1954
+ if (e.defaultPrevented || !doc) return;
1955
+ downloadDocument(doc, filename === void 0 ? {} : { filename }).catch(() => void 0);
1956
+ },
1957
+ children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Download })
1958
+ });
1959
+ }
1960
+ //#endregion
1961
+ //#region src/components/InkPrintButton.tsx
1962
+ /**
1963
+ * Prints the open document's file with the browser's own PDF viewer (on iOS, in a new tab).
1964
+ * Disabled until the document is ready. Today that is the file as it was opened, without this
1965
+ * session's annotations.
1966
+ */
1967
+ function InkPrintButton({ asChild = false, className, children, disabled, onClick, ...rest }) {
1968
+ const { document: doc, status } = useInkContext("InkPrintButton");
1969
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
1970
+ asChild,
1971
+ className: cx("ink-print", className),
1972
+ "data-ink-print": "",
1973
+ "aria-label": "Print",
1974
+ title: "Print",
1975
+ ...rest,
1976
+ disabled: disabled === true || doc === null || status.state !== "ready",
1977
+ onClick: (e) => {
1978
+ onClick?.(e);
1979
+ if (e.defaultPrevented || !doc) return;
1980
+ printDocument(doc).catch(() => void 0);
1981
+ },
1982
+ children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Printer })
1983
+ });
1984
+ }
1985
+ //#endregion
1787
1986
  //#region src/components/InkRedo.tsx
1788
1987
  function InkRedo({ asChild = false, className, children, disabled, onClick, ...rest }) {
1789
1988
  const { canRedo, redo } = useHistory();
@@ -1923,19 +2122,25 @@ function InkFindBar({ className, ...rest }) {
1923
2122
  role: "search",
1924
2123
  hidden: !state.open,
1925
2124
  children: [
1926
- /* @__PURE__ */ jsx("input", {
1927
- ref: field,
1928
- type: "text",
1929
- "aria-label": "Find in document",
1930
- autoComplete: "off",
1931
- spellCheck: false,
1932
- onInput: (e) => setQuery(e.currentTarget.value),
1933
- onKeyDown: (e) => {
1934
- if (e.key !== "Enter" || e.nativeEvent.isComposing) return;
1935
- e.preventDefault();
1936
- if (e.shiftKey) previous();
1937
- else next();
1938
- }
2125
+ /* @__PURE__ */ jsxs("div", {
2126
+ className: "ink-input-group",
2127
+ children: [/* @__PURE__ */ jsx("span", {
2128
+ className: "ink-input-accessory",
2129
+ children: /* @__PURE__ */ jsx(InkIcon, { icon: Search })
2130
+ }), /* @__PURE__ */ jsx("input", {
2131
+ ref: field,
2132
+ type: "text",
2133
+ "aria-label": "Find in document",
2134
+ autoComplete: "off",
2135
+ spellCheck: false,
2136
+ onInput: (e) => setQuery(e.currentTarget.value),
2137
+ onKeyDown: (e) => {
2138
+ if (e.key !== "Enter" || e.nativeEvent.isComposing) return;
2139
+ e.preventDefault();
2140
+ if (e.shiftKey) previous();
2141
+ else next();
2142
+ }
2143
+ })]
1939
2144
  }),
1940
2145
  /* @__PURE__ */ jsx("span", {
1941
2146
  className: "ink-find-count",
@@ -2072,7 +2277,7 @@ function InkZoomSelect({ className, ...rest }) {
2072
2277
  value,
2073
2278
  onValueChange,
2074
2279
  children: [/* @__PURE__ */ jsxs(Select.Trigger, {
2075
- className: cx("ink-zoom-select ink-select-trigger", className),
2280
+ className: cx("ink-select ink-select-trigger ink-zoom-select", className),
2076
2281
  "data-ink-zoom-select": "",
2077
2282
  "aria-label": "Zoom",
2078
2283
  ...rest,
@@ -2340,7 +2545,11 @@ function InkColorMenu({ property, label, className, children, standalone = false
2340
2545
  }
2341
2546
  //#endregion
2342
2547
  //#region src/components/InkShapeMenu.tsx
2343
- function InkShapeMenu({ className, children, ...rest }) {
2548
+ /** Renders nothing while no `shape` tool is registered. */
2549
+ function InkShapeMenu(props) {
2550
+ return useRegisteredTool("shape") ? /* @__PURE__ */ jsx(ShapeMenu, { ...props }) : null;
2551
+ }
2552
+ function ShapeMenu({ className, children, ...rest }) {
2344
2553
  const { tool, setTool } = useTool();
2345
2554
  const [options, setOptions] = useToolOptions("shape");
2346
2555
  const [open, setOpen] = useState(false);
@@ -2447,7 +2656,11 @@ function InkShapeMenu({ className, children, ...rest }) {
2447
2656
  //#endregion
2448
2657
  //#region src/components/InkSignatureMenu.tsx
2449
2658
  const NO_TEMPLATES = [];
2450
- function InkSignatureMenu({ className, children, fonts, ...rest }) {
2659
+ /** Renders nothing while no `signature` tool is registered. */
2660
+ function InkSignatureMenu(props) {
2661
+ return useRegisteredTool("signature") ? /* @__PURE__ */ jsx(SignatureMenu, { ...props }) : null;
2662
+ }
2663
+ function SignatureMenu({ className, children, fonts, ...rest }) {
2451
2664
  const { tool, setTool } = useTool();
2452
2665
  const [options, setOptions] = useToolOptions("signature");
2453
2666
  const { signatures } = useInkContext("InkSignatureMenu");
@@ -2569,7 +2782,11 @@ function InkSignatureMenu({ className, children, fonts, ...rest }) {
2569
2782
  }
2570
2783
  //#endregion
2571
2784
  //#region src/components/InkHighlightMenu.tsx
2572
- function InkHighlightMenu({ className, children, ...rest }) {
2785
+ /** Renders nothing while no `highlight` tool is registered. */
2786
+ function InkHighlightMenu(props) {
2787
+ return useRegisteredTool("highlight") ? /* @__PURE__ */ jsx(HighlightMenu, { ...props }) : null;
2788
+ }
2789
+ function HighlightMenu({ className, children, ...rest }) {
2573
2790
  const { tool, setTool } = useTool();
2574
2791
  const [options, setOptions] = useToolOptions("highlight");
2575
2792
  const { document: doc } = useDocument();
@@ -3202,6 +3419,706 @@ function ThreadForm({ editingRoot, locked, deleteLabel, onSubmit, onRemove, onCl
3202
3419
  });
3203
3420
  }
3204
3421
  //#endregion
3205
- export { InkAnnotation, InkColorMenu, InkCommand, InkFindBar, InkHighlightMenu, InkIcon, InkLineStyleMenu, InkNextPage, InkPage, InkPageCount, InkPageInput, InkPrevPage, InkRedo, InkRoot, InkSearchButton, InkShapeMenu, InkSignatureDialog, InkSignatureMenu, InkTextStyleMenu, InkThreadPopover, InkToolButton, InkToolbar, InkToolbarSeparator, InkUndo, InkViewport, InkZoomIn, InkZoomOut, InkZoomSelect, useAnnotations, useCommand, useDocument, useEmbedded, useForm, useHistory, useInkContext, usePage, useReadable, useSearch, useSelection, useTool, useToolOptions, useToolStyle, useZoom };
3422
+ //#region src/dom-attrs.ts
3423
+ /**
3424
+ * Core's attributes (`rootAttrs()`, `triggerAttrs(id)`, `panelAttrs(id)` and the like) as React
3425
+ * props. Core names them as the DOM does; React wants `hidden` as a boolean and `tabindex` as
3426
+ * `tabIndex`. `aria-*`, `data-*`, `id`, `role`, `type` and `title` pass through as they are.
3427
+ * Every component that spreads core's attributes goes through this, so the mapping lives once.
3428
+ */
3429
+ function domAttrs(attrs) {
3430
+ const out = {};
3431
+ for (const [name, value] of Object.entries(attrs)) if (name === "hidden") out.hidden = true;
3432
+ else if (name === "tabindex") out.tabIndex = Number(value);
3433
+ else out[name] = value;
3434
+ return out;
3435
+ }
3436
+ //#endregion
3437
+ //#region src/components/sidebar-context.ts
3438
+ const SidebarContextObject = createContext(null);
3439
+ /** The sidebar context: throws a readable error outside `InkSidebar`. */
3440
+ function useSidebarContext(consumer) {
3441
+ const ctx = useContext(SidebarContextObject);
3442
+ if (!ctx) throw new Error(`[inkbar] ${consumer} must be rendered inside <InkSidebar>.`);
3443
+ return ctx;
3444
+ }
3445
+ /**
3446
+ * The controller's state, re-rendering as it changes. Unlike `useReadable` it reads the live
3447
+ * state on the server too: the sidebar works without a document, so its markup is known there.
3448
+ */
3449
+ function useSidebarState(controller) {
3450
+ const { state } = controller;
3451
+ const subscribe = useCallback((onChange) => state.subscribe(() => onChange()), [state]);
3452
+ const get = useCallback(() => state.get(), [state]);
3453
+ return useSyncExternalStore(subscribe, get, get);
3454
+ }
3455
+ //#endregion
3456
+ //#region src/components/SidebarTrigger.tsx
3457
+ /**
3458
+ * A sidebar trigger's content: `createSidebar`'s markup, so the theme styles both alike. The
3459
+ * rail shows the icon alone (the trigger's `title` carries the label); tabs and stack headers
3460
+ * show the label too. The badge is hidden from assistive technology: the trigger's name has it.
3461
+ */
3462
+ function SidebarTrigger({ panel, rail }) {
3463
+ const badge = sidebarBadge(panel.badge);
3464
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
3465
+ panel.icon ? /* @__PURE__ */ jsx(InkIcon, { icon: panel.icon }) : null,
3466
+ !panel.icon || !rail ? /* @__PURE__ */ jsx("span", {
3467
+ className: "ink-sidebar-label",
3468
+ children: panel.label
3469
+ }) : null,
3470
+ /* @__PURE__ */ jsx("span", {
3471
+ className: "ink-sidebar-badge",
3472
+ "aria-hidden": "true",
3473
+ hidden: badge === null,
3474
+ children: badge
3475
+ })
3476
+ ] });
3477
+ }
3478
+ //#endregion
3479
+ //#region src/components/InkSidebar.tsx
3480
+ /** Sorts the panels by where their elements sit in the DOM. */
3481
+ function inDomOrder(entries) {
3482
+ return [...entries].sort((a, b) => {
3483
+ const x = a.element();
3484
+ const y = b.element();
3485
+ if (!x || !y || x === y) return 0;
3486
+ return x.compareDocumentPosition(y) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
3487
+ });
3488
+ }
3489
+ /**
3490
+ * The panels' registry around the controller. Before the first mount, panels preregister as
3491
+ * they render, in render order, so the server and the browser's first render show the panel
3492
+ * core opens. From then on they register from layout effects, ordered by DOM position. A
3493
+ * removal is applied a microtask later: a panel remounted in the same commit (a new `key`) is
3494
+ * back by then, so its open state does not fall back. While the sidebar itself is unmounted
3495
+ * (on its way out, or StrictMode and `<Activity>` replaying effects) nothing is applied.
3496
+ */
3497
+ function createRegistry(controller) {
3498
+ let mounted = false;
3499
+ let started = false;
3500
+ const early = [];
3501
+ let entries = [];
3502
+ const flush = () => {
3503
+ if (!mounted) return;
3504
+ controller.update({ panels: inDomOrder(entries).map((e) => e.info) });
3505
+ };
3506
+ const context = {
3507
+ controller,
3508
+ preregister(info) {
3509
+ if (started) return;
3510
+ const at = early.findIndex((p) => p.id === info.id);
3511
+ if (at < 0) early.push(info);
3512
+ else early[at] = info;
3513
+ controller.update({ panels: [...early] });
3514
+ },
3515
+ register(entry) {
3516
+ entries = [...entries, entry];
3517
+ flush();
3518
+ return () => {
3519
+ entries = entries.filter((e) => e !== entry);
3520
+ queueMicrotask(flush);
3521
+ };
3522
+ },
3523
+ changed: flush
3524
+ };
3525
+ /** Binds the triggers and ends the controller's pending state; returns the unbind. */
3526
+ function mount(el) {
3527
+ mounted = true;
3528
+ started = true;
3529
+ const unbind = controller.bindTriggers(el);
3530
+ controller.update({
3531
+ panels: inDomOrder(entries).map((e) => e.info),
3532
+ pending: false
3533
+ });
3534
+ return () => {
3535
+ mounted = false;
3536
+ unbind();
3537
+ };
3538
+ }
3539
+ return {
3540
+ context,
3541
+ mount
3542
+ };
3543
+ }
3544
+ /**
3545
+ * A sidebar of `InkPanel`s: `createSidebar`'s markup on core's sidebar controller, which holds
3546
+ * the open state, the ARIA, the keys and the clicks. Rail and tabs render their triggers here;
3547
+ * in a stack each panel renders its own header. Panels take their place in DOM order, so a
3548
+ * panel rendered later slots in where it stands. A closed panel stays mounted and hidden, and
3549
+ * the panels' content stays in place across layouts, so its state survives switching.
3550
+ *
3551
+ * `open` with `onOpenChange` controls it; `defaultOpen` starts it uncontrolled; with neither,
3552
+ * the first panel starts open. It needs no document, so it works outside `InkRoot`. The server
3553
+ * renders the root, the panels (the open one shown) and the stack headers; rail and tab
3554
+ * buttons follow once mounted.
3555
+ */
3556
+ function InkSidebar({ side = "left", layout = "rail", visible = true, open, defaultOpen, onOpenChange, className, children, ...rest }) {
3557
+ const controlled = open !== void 0;
3558
+ const [inner, setInner] = useState(defaultOpen);
3559
+ const value = controlled ? open : inner;
3560
+ const latest = useLatest({
3561
+ controlled,
3562
+ onOpenChange
3563
+ });
3564
+ const idPrefix = useId();
3565
+ const [{ context, mount }] = useState(() => createRegistry(createSidebarController({
3566
+ layout,
3567
+ side,
3568
+ visible,
3569
+ panels: [],
3570
+ ...value !== void 0 && { open: value },
3571
+ pending: true,
3572
+ idPrefix,
3573
+ onOpenChange: (next) => {
3574
+ if (!latest.current.controlled) setInner(next);
3575
+ latest.current.onOpenChange?.(next);
3576
+ }
3577
+ })));
3578
+ const { controller } = context;
3579
+ const state = useSidebarState(controller);
3580
+ useLayoutEffect(() => {
3581
+ controller.update({
3582
+ layout,
3583
+ side,
3584
+ visible,
3585
+ ...value !== void 0 && { open: value }
3586
+ });
3587
+ }, [
3588
+ controller,
3589
+ layout,
3590
+ side,
3591
+ visible,
3592
+ value
3593
+ ]);
3594
+ const root = useRef(null);
3595
+ useLayoutEffect(() => mount(root.current), [mount]);
3596
+ const rail = state.layout === "rail";
3597
+ return /* @__PURE__ */ jsxs("div", {
3598
+ ...rest,
3599
+ ref: root,
3600
+ className: cx("ink-sidebar", className),
3601
+ ...domAttrs(controller.rootAttrs()),
3602
+ children: [state.layout !== "stack" ? /* @__PURE__ */ jsx("div", {
3603
+ className: rail ? "ink-sidebar-rail" : "ink-sidebar-tabs",
3604
+ ...domAttrs(controller.tablistAttrs()),
3605
+ children: state.panels.map((panel) => /* @__PURE__ */ jsx("button", {
3606
+ ...domAttrs(controller.triggerAttrs(panel.id)),
3607
+ children: /* @__PURE__ */ jsx(SidebarTrigger, {
3608
+ panel,
3609
+ rail
3610
+ })
3611
+ }, panel.id))
3612
+ }) : null, /* @__PURE__ */ jsx("div", {
3613
+ className: state.layout !== "stack" ? "ink-sidebar-panels" : void 0,
3614
+ children: /* @__PURE__ */ jsx(SidebarContextObject.Provider, {
3615
+ value: context,
3616
+ children
3617
+ })
3618
+ })]
3619
+ });
3620
+ }
3621
+ //#endregion
3622
+ //#region src/components/InkPanel.tsx
3623
+ /**
3624
+ * One panel of an `InkSidebar`, holding `children` as its content. Closed, the panel stays
3625
+ * mounted and hidden. In a stack it renders its own header above its content.
3626
+ */
3627
+ function InkPanel({ id, label, icon, badge, children }) {
3628
+ const ctx = useSidebarContext("InkPanel");
3629
+ const { controller } = ctx;
3630
+ const info = useMemo(() => ({
3631
+ id,
3632
+ label,
3633
+ ...icon !== void 0 && { icon },
3634
+ ...badge !== void 0 && { badge }
3635
+ }), [
3636
+ id,
3637
+ label,
3638
+ icon,
3639
+ badge
3640
+ ]);
3641
+ ctx.preregister(info);
3642
+ const state = useSidebarState(controller);
3643
+ const wrapper = useRef(null);
3644
+ const [entry] = useState(() => ({
3645
+ info,
3646
+ element: () => wrapper.current
3647
+ }));
3648
+ useLayoutEffect(() => ctx.register(entry), [ctx, entry]);
3649
+ useLayoutEffect(() => {
3650
+ if (entry.info === info) return;
3651
+ entry.info = info;
3652
+ ctx.changed();
3653
+ }, [
3654
+ ctx,
3655
+ entry,
3656
+ info
3657
+ ]);
3658
+ const stack = state.layout === "stack";
3659
+ return /* @__PURE__ */ jsxs("div", {
3660
+ ref: wrapper,
3661
+ className: cx("ink-sidebar-slot", stack && "ink-sidebar-section"),
3662
+ children: [stack ? /* @__PURE__ */ jsx("button", {
3663
+ className: "ink-sidebar-header",
3664
+ ...domAttrs(controller.triggerAttrs(id)),
3665
+ children: /* @__PURE__ */ jsx(SidebarTrigger, {
3666
+ panel: info,
3667
+ rail: false
3668
+ })
3669
+ }) : null, /* @__PURE__ */ jsxs("div", {
3670
+ className: "ink-sidebar-panel",
3671
+ ...domAttrs(controller.panelAttrs(id)),
3672
+ children: [/* @__PURE__ */ jsx("div", {
3673
+ className: "ink-sidebar-panel-title",
3674
+ "aria-hidden": "true",
3675
+ children: label
3676
+ }), /* @__PURE__ */ jsx("div", {
3677
+ className: "ink-sidebar-panel-body",
3678
+ children
3679
+ })]
3680
+ })]
3681
+ });
3682
+ }
3683
+ //#endregion
3684
+ //#region src/to-readable.ts
3685
+ /**
3686
+ * A core `Readable` that follows a React value, for the core controllers and painters that
3687
+ * components build on. The same readable for the component's life; it takes each new value in
3688
+ * a layout effect, so declare it before the effects that hand it to core.
3689
+ */
3690
+ function useToReadable(value) {
3691
+ const [store] = useState(() => writable(value));
3692
+ useLayoutEffect(() => {
3693
+ store.set(value);
3694
+ }, [store, value]);
3695
+ const [view] = useState(() => readonly(store));
3696
+ return view;
3697
+ }
3698
+ //#endregion
3699
+ //#region src/components/InkThumbnail.tsx
3700
+ /**
3701
+ * One page as a static image, with its live annotations and plugin marks: core's thumbnail
3702
+ * painter on this element, which sizes it, names it ("Page N" unless `label` says otherwise)
3703
+ * and draws it once it nears the visible area. It is not focusable and does nothing on click:
3704
+ * wrap it in the control you need. Inside `InkRoot`; it renders empty on the server.
3705
+ */
3706
+ function InkThumbnail({ page, width, height, annotations = true, marks = true, label, className, ...rest }) {
3707
+ const ctx = useInkContext("InkThumbnail");
3708
+ const document = useToReadable(ctx.document);
3709
+ const pluginHost = useToReadable(ctx.pluginHost);
3710
+ const options = useMemo(() => ({
3711
+ page,
3712
+ width,
3713
+ height,
3714
+ annotations,
3715
+ marks,
3716
+ label
3717
+ }), [
3718
+ page,
3719
+ width,
3720
+ height,
3721
+ annotations,
3722
+ marks,
3723
+ label
3724
+ ]);
3725
+ const latest = useLatest(options);
3726
+ const el = useRef(null);
3727
+ const painter = useRef(null);
3728
+ useLayoutEffect(() => {
3729
+ painter.current?.update(options);
3730
+ }, [options]);
3731
+ useLayoutEffect(() => {
3732
+ const p = createThumbnailPainter(el.current, {
3733
+ document,
3734
+ pluginHost
3735
+ }, latest.current);
3736
+ painter.current = p;
3737
+ return () => {
3738
+ p.destroy();
3739
+ painter.current = null;
3740
+ };
3741
+ }, [
3742
+ document,
3743
+ pluginHost,
3744
+ latest
3745
+ ]);
3746
+ return /* @__PURE__ */ jsx("div", {
3747
+ ...rest,
3748
+ ref: el,
3749
+ className: cx("ink-thumbnail", className)
3750
+ });
3751
+ }
3752
+ //#endregion
3753
+ //#region src/components/InkThumbnails.tsx
3754
+ /** Until the list mounts, and on the server: nothing listed. */
3755
+ const EMPTY = {
3756
+ pages: [],
3757
+ current: -1,
3758
+ window: {
3759
+ start: 0,
3760
+ end: 0
3761
+ },
3762
+ offsets: [0],
3763
+ width: THUMBNAIL_WIDTH
3764
+ };
3765
+ /**
3766
+ * The pages as a list of thumbnails: `createThumbnails`' markup on core's thumbnails
3767
+ * controller, which holds the current page, the keys, the clicks, the Tab stop and the window.
3768
+ * Only the items near the visible part are mounted, each placed at its offset over a spacer of
3769
+ * the full height, and each holds an `InkThumbnail`. An item is a button that scrolls the
3770
+ * viewport to its page; the current page's has `aria-current="page"`. Inside `InkRoot`; it
3771
+ * renders empty on the server.
3772
+ */
3773
+ function InkThumbnails({ pages, width, annotations = true, marks = true, renderItem, className, ...rest }) {
3774
+ const ctx = useInkContext("InkThumbnails");
3775
+ const document = useToReadable(ctx.document);
3776
+ const pluginHost = useToReadable(ctx.pluginHost);
3777
+ const viewport = useToReadable(ctx.viewport);
3778
+ const options = useMemo(() => ({
3779
+ pages,
3780
+ width,
3781
+ annotations,
3782
+ marks
3783
+ }), [
3784
+ pages,
3785
+ width,
3786
+ annotations,
3787
+ marks
3788
+ ]);
3789
+ const latest = useLatest(options);
3790
+ const scroller = useRef(null);
3791
+ const [controller, setController] = useState(null);
3792
+ useLayoutEffect(() => {
3793
+ controller?.update(options);
3794
+ }, [controller, options]);
3795
+ useLayoutEffect(() => {
3796
+ const c = createThumbnailsController({
3797
+ document,
3798
+ pluginHost,
3799
+ viewport
3800
+ }, latest.current);
3801
+ setController(c);
3802
+ c.attach(scroller.current);
3803
+ return () => {
3804
+ c.destroy();
3805
+ setController(null);
3806
+ };
3807
+ }, [
3808
+ document,
3809
+ pluginHost,
3810
+ viewport,
3811
+ latest
3812
+ ]);
3813
+ const state = useReadable(controller?.state, EMPTY);
3814
+ const { window: win, offsets } = state;
3815
+ const items = state.pages.slice(win.start, win.end).map((page, k) => {
3816
+ const top = offsets[win.start + k];
3817
+ const bottom = offsets[win.start + k + 1];
3818
+ const attrs = controller?.itemAttrs(win.start + k) ?? {};
3819
+ return {
3820
+ page,
3821
+ current: page === state.current,
3822
+ top,
3823
+ height: bottom - top,
3824
+ attrs
3825
+ };
3826
+ });
3827
+ return /* @__PURE__ */ jsxs("div", {
3828
+ ...rest,
3829
+ ref: scroller,
3830
+ className: cx("ink-thumbnails", className),
3831
+ "data-ink-thumbnails": "",
3832
+ role: "list",
3833
+ children: [/* @__PURE__ */ jsx("div", {
3834
+ className: "ink-thumbnails-spacer",
3835
+ "aria-hidden": "true",
3836
+ style: { height: `${offsets.at(-1) ?? 0}px` }
3837
+ }), items.map((item) => /* @__PURE__ */ jsx("div", {
3838
+ className: "ink-thumbnails-entry",
3839
+ ...domAttrs(item.attrs),
3840
+ style: {
3841
+ top: `${item.top}px`,
3842
+ height: `${item.height}px`
3843
+ },
3844
+ children: /* @__PURE__ */ jsxs("button", {
3845
+ type: "button",
3846
+ className: "ink-thumbnails-item",
3847
+ "data-ink-thumbnail-item": item.page,
3848
+ "aria-current": item.current ? "page" : void 0,
3849
+ children: [
3850
+ /* @__PURE__ */ jsx(InkThumbnail, {
3851
+ page: item.page,
3852
+ width: state.width,
3853
+ annotations,
3854
+ marks
3855
+ }),
3856
+ /* @__PURE__ */ jsx("span", {
3857
+ className: "ink-thumbnails-label",
3858
+ "aria-hidden": "true",
3859
+ children: item.page + 1
3860
+ }),
3861
+ renderItem?.({
3862
+ page: item.page,
3863
+ current: item.current
3864
+ })
3865
+ ]
3866
+ })
3867
+ }, item.page))]
3868
+ });
3869
+ }
3870
+ //#endregion
3871
+ //#region src/components/RecordCards.tsx
3872
+ const NO_ROWS = Object.freeze([]);
3873
+ const actionButtons = (row) => [...row.querySelectorAll(".ink-card-actions > button")];
3874
+ /**
3875
+ * A record list's markup on core's record list controller, shared by `InkRecordList` and
3876
+ * `InkSearchResults`: `createRecordList`'s `div.ink-records` holding `ul.ink-record-list` with
3877
+ * one `li.ink-record` card per row, the empty text and a polite live region. The controller
3878
+ * holds the rows, the count and the announcement, and handles the clicks, keys, Tab stop,
3879
+ * focus and scrolling. Empty, with no empty text, until there is a source.
3880
+ */
3881
+ function RecordCards({ source, emptyShown = true, empty, onCountChange, className, ...rest }) {
3882
+ const root = useRef(null);
3883
+ const [controller, setController] = useState(null);
3884
+ const latest = useLatest(onCountChange);
3885
+ /** The count last reported, across sources and StrictMode's replayed effects. */
3886
+ const reported = useRef(null);
3887
+ const held = useRef(null);
3888
+ useLayoutEffect(() => {
3889
+ const el = root.current;
3890
+ if (!source || !el) return;
3891
+ const c = createRecordListController(source);
3892
+ const report = (count) => {
3893
+ if (count === reported.current) return;
3894
+ reported.current = count;
3895
+ latest.current?.(count);
3896
+ };
3897
+ /**
3898
+ * Before the rows re-render under a focused action whose button goes (its card lost that
3899
+ * action), note where it was; the layout effect below focuses the same place in the card,
3900
+ * else the row. A row that goes is the controller's to re-home.
3901
+ */
3902
+ const holdActionFocus = () => {
3903
+ const active = el.ownerDocument.activeElement;
3904
+ if (!(active instanceof HTMLButtonElement) || !el.contains(active)) return;
3905
+ const row = active.closest("[data-ink-record]");
3906
+ const id = row?.dataset.inkRecord;
3907
+ if (!row || id === void 0) return;
3908
+ held.current = {
3909
+ button: active,
3910
+ id,
3911
+ index: actionButtons(row).indexOf(active)
3912
+ };
3913
+ };
3914
+ setController(c);
3915
+ report(c.count.get());
3916
+ const stops = [c.rows.subscribe(holdActionFocus), c.count.subscribe(report)];
3917
+ const detach = c.attach(el);
3918
+ return () => {
3919
+ for (const stop of stops) stop();
3920
+ detach();
3921
+ c.destroy();
3922
+ setController(null);
3923
+ };
3924
+ }, [source, latest]);
3925
+ const rows = useReadable(controller?.rows, NO_ROWS);
3926
+ const announcement = useReadable(controller?.announcement, "");
3927
+ const showEmpty = controller !== null && rows.length === 0 && emptyShown;
3928
+ useLayoutEffect(() => {
3929
+ const focus = held.current;
3930
+ const el = root.current;
3931
+ held.current = null;
3932
+ if (!focus || !el || focus.button.isConnected) return;
3933
+ const row = [...el.querySelectorAll("[data-ink-record]")].find((r) => r.dataset.inkRecord === focus.id);
3934
+ if (row) (actionButtons(row)[focus.index] ?? row).focus({ preventScroll: true });
3935
+ }, [rows]);
3936
+ return /* @__PURE__ */ jsxs("div", {
3937
+ ...rest,
3938
+ ref: root,
3939
+ className: cx("ink-records", className),
3940
+ "data-ink-records": "",
3941
+ tabIndex: -1,
3942
+ children: [
3943
+ /* @__PURE__ */ jsx("ul", {
3944
+ className: "ink-record-list",
3945
+ role: "list",
3946
+ hidden: rows.length === 0,
3947
+ children: rows.map(({ item, card, selected }) => /* @__PURE__ */ jsx("li", {
3948
+ className: "ink-record",
3949
+ "data-ink-record": item.id,
3950
+ "aria-current": selected ? "true" : void 0,
3951
+ "data-selected": selected ? "" : void 0,
3952
+ children: /* @__PURE__ */ jsx("div", {
3953
+ className: cx("ink-card", card?.className),
3954
+ children: card ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
3955
+ /* @__PURE__ */ jsx("p", {
3956
+ className: "ink-card-title",
3957
+ children: card.title
3958
+ }),
3959
+ card.meta !== void 0 ? /* @__PURE__ */ jsxs("p", {
3960
+ className: "ink-card-meta",
3961
+ children: [card.swatch !== void 0 ? /* @__PURE__ */ jsx("span", {
3962
+ className: "ink-card-swatch",
3963
+ style: { background: card.swatch }
3964
+ }) : null, card.meta]
3965
+ }) : null,
3966
+ card.body !== void 0 ? /* @__PURE__ */ jsx("p", {
3967
+ className: "ink-card-body",
3968
+ children: card.body
3969
+ }) : null,
3970
+ card.excerpt !== void 0 ? /* @__PURE__ */ jsxs("p", {
3971
+ className: "ink-card-excerpt",
3972
+ children: [
3973
+ card.excerpt.before,
3974
+ /* @__PURE__ */ jsx("mark", { children: card.excerpt.match }),
3975
+ card.excerpt.after
3976
+ ]
3977
+ }) : null,
3978
+ /* @__PURE__ */ jsx("div", {
3979
+ className: "ink-card-actions",
3980
+ children: Object.keys(card.actions).map((label) => /* @__PURE__ */ jsx("button", {
3981
+ type: "button",
3982
+ onClick: () => controller?.act(item.id, label),
3983
+ children: label
3984
+ }, label))
3985
+ })
3986
+ ] }) : null
3987
+ })
3988
+ }, item.id))
3989
+ }),
3990
+ /* @__PURE__ */ jsx("p", {
3991
+ className: "ink-record-list-empty",
3992
+ hidden: !showEmpty,
3993
+ children: empty === void 0 ? "Nothing here yet." : empty
3994
+ }),
3995
+ /* @__PURE__ */ jsx("div", {
3996
+ className: "ink-visually-hidden",
3997
+ "aria-live": "polite",
3998
+ "data-ink-record-status": "",
3999
+ children: announcement
4000
+ })
4001
+ ]
4002
+ });
4003
+ }
4004
+ //#endregion
4005
+ //#region src/components/InkRecordList.tsx
4006
+ function isDevelopment() {
4007
+ try {
4008
+ return process.env.NODE_ENV !== "production";
4009
+ } catch {
4010
+ return false;
4011
+ }
4012
+ }
4013
+ /**
4014
+ * The records a plugin keeps under `records` (`<plugin>.<name>`) as a list of cards, the same
4015
+ * cards their marks open, with the same actions: `createRecordList` on core's record list
4016
+ * controller. The list, the marks and `useRecordSelection` share one selection. `card`, `where`
4017
+ * and `sort` replace the marks' card, filter and reading order; a new function rebuilds the
4018
+ * list, so keep them stable (`useCallback`, or defined outside the component).
4019
+ * `onCountChange` gets the number listed whenever it changes; `empty` replaces "Nothing here
4020
+ * yet.". It fills its parent's height. Inside `InkRoot`; it renders empty on the server.
4021
+ *
4022
+ * In place of `records`, `source` takes a host's own `RecordListSource` and lists it with the
4023
+ * same controller and cards. Give one of the two: with both, `source` wins and a development
4024
+ * build warns.
4025
+ */
4026
+ function InkRecordList({ records, source, card, where, sort, onCountChange, empty, ...rest }) {
4027
+ const ctx = useInkContext("InkRecordList");
4028
+ const viewport = useToReadable(ctx.viewport);
4029
+ const { pluginHost } = ctx;
4030
+ const both = records !== void 0 && source !== void 0;
4031
+ useEffect(() => {
4032
+ if (both && isDevelopment()) console.warn("[inkbar] InkRecordList takes `records` or `source`, not both: it lists `source`.");
4033
+ }, [both]);
4034
+ const listed = useMemo(() => {
4035
+ if (source) return source;
4036
+ if (!pluginHost || records === void 0) return null;
4037
+ const opts = {};
4038
+ if (card) opts.card = card;
4039
+ if (where) opts.where = where;
4040
+ if (sort) opts.sort = sort;
4041
+ return recordsSource({
4042
+ pluginHost,
4043
+ viewport
4044
+ }, records, opts);
4045
+ }, [
4046
+ source,
4047
+ pluginHost,
4048
+ records,
4049
+ card,
4050
+ where,
4051
+ sort,
4052
+ viewport
4053
+ ]);
4054
+ return /* @__PURE__ */ jsx(RecordCards, {
4055
+ ...rest,
4056
+ source: listed,
4057
+ onCountChange,
4058
+ empty
4059
+ });
4060
+ }
4061
+ //#endregion
4062
+ //#region src/components/InkSearchResults.tsx
4063
+ /**
4064
+ * The search hits as a list of cards, for a sidebar panel: `createSearchResults` on core's
4065
+ * record list controller over the root's search. One card per hit, with its page and the text
4066
+ * around it. Choosing a card makes its hit current, as the find bar's next and previous do, and
4067
+ * the current hit is the selected card. Above the cards, a query field (`field={false}`
4068
+ * leaves it out when a find bar is on the page); under them, the find bar's match count.
4069
+ * Field and find bar share the search state, so they never disagree. It fills its parent's
4070
+ * height. Inside `InkRoot`; it renders empty on the server.
4071
+ */
4072
+ function InkSearchResults({ field = true, className, ...rest }) {
4073
+ const ctx = useInkContext("InkSearchResults");
4074
+ const search = useSearch();
4075
+ const { state } = search;
4076
+ const source = useMemo(() => ctx.search ? searchSource(ctx.search) : null, [ctx.search]);
4077
+ const input = useRef(null);
4078
+ useLayoutEffect(() => {
4079
+ const el = input.current;
4080
+ if (el && el.value !== state.query) el.value = state.query;
4081
+ }, [state.query, field]);
4082
+ return /* @__PURE__ */ jsxs("div", {
4083
+ ...rest,
4084
+ className: cx("ink-search-results", className),
4085
+ "data-ink-search-results": "",
4086
+ children: [
4087
+ field ? /* @__PURE__ */ jsx("div", {
4088
+ className: "ink-search-results-bar",
4089
+ role: "search",
4090
+ children: /* @__PURE__ */ jsxs("div", {
4091
+ className: "ink-input-group",
4092
+ children: [/* @__PURE__ */ jsx("span", {
4093
+ className: "ink-input-accessory",
4094
+ children: /* @__PURE__ */ jsx(InkIcon, { icon: Search })
4095
+ }), /* @__PURE__ */ jsx("input", {
4096
+ ref: input,
4097
+ type: "text",
4098
+ "aria-label": "Find in document",
4099
+ placeholder: "Search for a word or phrase",
4100
+ autoComplete: "off",
4101
+ spellCheck: false,
4102
+ defaultValue: state.query,
4103
+ onInput: (e) => search.setQuery(e.currentTarget.value),
4104
+ onKeyDown: (e) => handleResultsFieldKey(e.nativeEvent, search)
4105
+ })]
4106
+ })
4107
+ }) : null,
4108
+ /* @__PURE__ */ jsx(RecordCards, {
4109
+ source,
4110
+ emptyShown: isBlankQuery(state.query),
4111
+ empty: "Search to list every match."
4112
+ }),
4113
+ /* @__PURE__ */ jsx("div", {
4114
+ className: "ink-search-results-count",
4115
+ "data-ink-search-results-count": "",
4116
+ children: findCountText(state)
4117
+ })
4118
+ ]
4119
+ });
4120
+ }
4121
+ //#endregion
4122
+ export { InkAnnotation, InkColorMenu, InkCommand, InkDownloadButton, InkFindBar, InkHighlightMenu, InkIcon, InkLineStyleMenu, InkNextPage, InkPage, InkPageCount, InkPageInput, InkPanel, InkPrevPage, InkPrintButton, InkRecordList, InkRedo, InkRoot, InkSearchButton, InkSearchResults, InkShapeMenu, InkSidebar, InkSignatureDialog, InkSignatureMenu, InkTextStyleMenu, InkThreadPopover, InkThumbnail, InkThumbnails, InkToolButton, InkToolbar, InkToolbarButton, InkToolbarSeparator, InkUndo, InkViewport, InkZoomIn, InkZoomOut, InkZoomSelect, useAnnotations, useCommand, useDocument, useEmbedded, useForm, useHistory, useInkContext, usePage, useReadable, useRecordSelection, useRecords, useSearch, useSelection, useTool, useToolOptions, useToolStyle, useZoom };
3206
4123
 
3207
4124
  //# sourceMappingURL=index.mjs.map