@inkbar/react 0.9.0 → 0.11.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, unknownTool, viewerSearchOf, watchDetached, withHostTools } 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, ICON_ATTRIBUTES, Italic, Minus, Plus, Redo2, Search, Signature, SquareDashedMousePointer, TextCursor, Trash2, 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
@@ -349,7 +385,7 @@ function identity(src) {
349
385
  if ("key" in src) return src.key;
350
386
  return src;
351
387
  }
352
- function isDevelopment() {
388
+ function isDevelopment$1() {
353
389
  try {
354
390
  return process.env.NODE_ENV !== "production";
355
391
  } catch {
@@ -357,7 +393,7 @@ function isDevelopment() {
357
393
  }
358
394
  }
359
395
  function setTestHook(el, doc) {
360
- if (el && isDevelopment()) el.__inkDocument = doc;
396
+ if (el && isDevelopment$1()) el.__inkDocument = doc;
361
397
  }
362
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 }) {
363
399
  const rootElRef = useRef(null);
@@ -723,9 +759,11 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
723
759
  };
724
760
  });
725
761
  const stopFocus = keepHistoryFocus(rootEl, () => rootEl.querySelector("[data-ink-scroll]"));
762
+ const stopFileKeys = attachFileKeys(rootEl, () => documentRef.current);
726
763
  return () => {
727
764
  stopKeys();
728
765
  stopFocus();
766
+ stopFileKeys();
729
767
  };
730
768
  }, [
731
769
  rootEl,
@@ -1502,7 +1540,7 @@ function documentKey(doc) {
1502
1540
  }
1503
1541
  return key;
1504
1542
  }
1505
- function InkViewport({ mode = "continuous", rotation = 0, gap = 16, buffer = 2, loading, empty, error, renderAnnotation, renderPageError, className, ...rest }) {
1543
+ function InkViewport({ mode = "continuous", rotation = 0, gap = 16, buffer = 2, pinchZoom = true, loading, empty, error, renderAnnotation, renderPageError, className, ...rest }) {
1506
1544
  const ctx = useInkContext("InkViewport");
1507
1545
  const scroll = useRef(null);
1508
1546
  const custom = !!renderAnnotation;
@@ -1512,6 +1550,7 @@ function InkViewport({ mode = "continuous", rotation = 0, gap = 16, buffer = 2,
1512
1550
  rotation,
1513
1551
  gap,
1514
1552
  buffer,
1553
+ pinchZoom,
1515
1554
  custom
1516
1555
  });
1517
1556
  const engine = useRef(null);
@@ -1533,7 +1572,7 @@ function InkViewport({ mode = "continuous", rotation = 0, gap = 16, buffer = 2,
1533
1572
  /** The form script's message on show, and how many have been shown. */
1534
1573
  const [alert, setAlert] = useState(null);
1535
1574
  useEffect(() => {
1536
- const { ctx: c, mode: m, rotation: r, gap: g, buffer: b, custom: cu } = latest.current;
1575
+ const { ctx: c, mode: m, rotation: r, gap: g, buffer: b, pinchZoom: pz, custom: cu } = latest.current;
1537
1576
  const doc = c.document;
1538
1577
  const el = scroll.current;
1539
1578
  lastReportedZoom.current = void 0;
@@ -1545,6 +1584,7 @@ function InkViewport({ mode = "continuous", rotation = 0, gap = 16, buffer = 2,
1545
1584
  builtWith.current = {
1546
1585
  gap: g,
1547
1586
  buffer: b,
1587
+ pinchZoom: pz,
1548
1588
  custom: cu
1549
1589
  };
1550
1590
  c.setOpenedToolIds(toolIdsOf(c.hostTools, c.pluginHost));
@@ -1557,6 +1597,7 @@ function InkViewport({ mode = "continuous", rotation = 0, gap = 16, buffer = 2,
1557
1597
  rotation: r,
1558
1598
  gap: g,
1559
1599
  buffer: b,
1600
+ pinchZoom: pz,
1560
1601
  tool: c.tool,
1561
1602
  ...c.hostTools ? { tools: c.hostTools } : {},
1562
1603
  ...c.pluginHost ? { plugins: c.pluginHost } : {},
@@ -1655,21 +1696,24 @@ function InkViewport({ mode = "continuous", rotation = 0, gap = 16, buffer = 2,
1655
1696
  useEffect(() => {
1656
1697
  const built = builtWith.current;
1657
1698
  if (!engine.current || !built) return;
1658
- if (built.gap === gap && built.buffer === buffer && built.custom === custom) return;
1699
+ if (built.gap === gap && built.buffer === buffer && built.pinchZoom === pinchZoom && built.custom === custom) return;
1659
1700
  buildCount.current += 1;
1660
1701
  builtWith.current = {
1661
1702
  gap,
1662
1703
  buffer,
1704
+ pinchZoom,
1663
1705
  custom
1664
1706
  };
1665
1707
  engine.current.rebuild({
1666
1708
  gap,
1667
1709
  buffer,
1710
+ pinchZoom,
1668
1711
  customAnnotations: custom
1669
1712
  });
1670
1713
  }, [
1671
1714
  gap,
1672
1715
  buffer,
1716
+ pinchZoom,
1673
1717
  custom
1674
1718
  ]);
1675
1719
  useEffect(() => {
@@ -1895,6 +1939,56 @@ function InkUndo({ asChild = false, className, children, disabled, onClick, ...r
1895
1939
  });
1896
1940
  }
1897
1941
  //#endregion
1942
+ //#region src/components/InkDownloadButton.tsx
1943
+ /**
1944
+ * Saves the open document's file through the browser's download, under its `fileName` or
1945
+ * `filename`. Disabled until the document is ready. Today that is the file as it was opened,
1946
+ * without this session's annotations.
1947
+ */
1948
+ function InkDownloadButton({ asChild = false, filename, className, children, disabled, onClick, ...rest }) {
1949
+ const { document: doc, status } = useInkContext("InkDownloadButton");
1950
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
1951
+ asChild,
1952
+ className: cx("ink-download", className),
1953
+ "data-ink-download": "",
1954
+ "aria-label": "Download",
1955
+ title: "Download",
1956
+ ...rest,
1957
+ disabled: disabled === true || doc === null || status.state !== "ready",
1958
+ onClick: (e) => {
1959
+ onClick?.(e);
1960
+ if (e.defaultPrevented || !doc) return;
1961
+ downloadDocument(doc, filename === void 0 ? {} : { filename }).catch(() => void 0);
1962
+ },
1963
+ children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Download })
1964
+ });
1965
+ }
1966
+ //#endregion
1967
+ //#region src/components/InkPrintButton.tsx
1968
+ /**
1969
+ * Prints the open document's file with the browser's own PDF viewer (on iOS, in a new tab).
1970
+ * Disabled until the document is ready. Today that is the file as it was opened, without this
1971
+ * session's annotations.
1972
+ */
1973
+ function InkPrintButton({ asChild = false, className, children, disabled, onClick, ...rest }) {
1974
+ const { document: doc, status } = useInkContext("InkPrintButton");
1975
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
1976
+ asChild,
1977
+ className: cx("ink-print", className),
1978
+ "data-ink-print": "",
1979
+ "aria-label": "Print",
1980
+ title: "Print",
1981
+ ...rest,
1982
+ disabled: disabled === true || doc === null || status.state !== "ready",
1983
+ onClick: (e) => {
1984
+ onClick?.(e);
1985
+ if (e.defaultPrevented || !doc) return;
1986
+ printDocument(doc).catch(() => void 0);
1987
+ },
1988
+ children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Printer })
1989
+ });
1990
+ }
1991
+ //#endregion
1898
1992
  //#region src/components/InkRedo.tsx
1899
1993
  function InkRedo({ asChild = false, className, children, disabled, onClick, ...rest }) {
1900
1994
  const { canRedo, redo } = useHistory();
@@ -2034,19 +2128,25 @@ function InkFindBar({ className, ...rest }) {
2034
2128
  role: "search",
2035
2129
  hidden: !state.open,
2036
2130
  children: [
2037
- /* @__PURE__ */ jsx("input", {
2038
- ref: field,
2039
- type: "text",
2040
- "aria-label": "Find in document",
2041
- autoComplete: "off",
2042
- spellCheck: false,
2043
- onInput: (e) => setQuery(e.currentTarget.value),
2044
- onKeyDown: (e) => {
2045
- if (e.key !== "Enter" || e.nativeEvent.isComposing) return;
2046
- e.preventDefault();
2047
- if (e.shiftKey) previous();
2048
- else next();
2049
- }
2131
+ /* @__PURE__ */ jsxs("div", {
2132
+ className: "ink-input-group",
2133
+ children: [/* @__PURE__ */ jsx("span", {
2134
+ className: "ink-input-accessory",
2135
+ children: /* @__PURE__ */ jsx(InkIcon, { icon: Search })
2136
+ }), /* @__PURE__ */ jsx("input", {
2137
+ ref: field,
2138
+ type: "text",
2139
+ "aria-label": "Find in document",
2140
+ autoComplete: "off",
2141
+ spellCheck: false,
2142
+ onInput: (e) => setQuery(e.currentTarget.value),
2143
+ onKeyDown: (e) => {
2144
+ if (e.key !== "Enter" || e.nativeEvent.isComposing) return;
2145
+ e.preventDefault();
2146
+ if (e.shiftKey) previous();
2147
+ else next();
2148
+ }
2149
+ })]
2050
2150
  }),
2051
2151
  /* @__PURE__ */ jsx("span", {
2052
2152
  className: "ink-find-count",
@@ -2183,7 +2283,7 @@ function InkZoomSelect({ className, ...rest }) {
2183
2283
  value,
2184
2284
  onValueChange,
2185
2285
  children: [/* @__PURE__ */ jsxs(Select.Trigger, {
2186
- className: cx("ink-zoom-select ink-select-trigger", className),
2286
+ className: cx("ink-select ink-select-trigger ink-zoom-select", className),
2187
2287
  "data-ink-zoom-select": "",
2188
2288
  "aria-label": "Zoom",
2189
2289
  ...rest,
@@ -3325,6 +3425,706 @@ function ThreadForm({ editingRoot, locked, deleteLabel, onSubmit, onRemove, onCl
3325
3425
  });
3326
3426
  }
3327
3427
  //#endregion
3328
- export { InkAnnotation, InkColorMenu, InkCommand, InkFindBar, InkHighlightMenu, InkIcon, InkLineStyleMenu, InkNextPage, InkPage, InkPageCount, InkPageInput, InkPrevPage, InkRedo, InkRoot, InkSearchButton, InkShapeMenu, InkSignatureDialog, InkSignatureMenu, InkTextStyleMenu, InkThreadPopover, InkToolButton, InkToolbar, InkToolbarButton, InkToolbarSeparator, InkUndo, InkViewport, InkZoomIn, InkZoomOut, InkZoomSelect, useAnnotations, useCommand, useDocument, useEmbedded, useForm, useHistory, useInkContext, usePage, useReadable, useSearch, useSelection, useTool, useToolOptions, useToolStyle, useZoom };
3428
+ //#region src/dom-attrs.ts
3429
+ /**
3430
+ * Core's attributes (`rootAttrs()`, `triggerAttrs(id)`, `panelAttrs(id)` and the like) as React
3431
+ * props. Core names them as the DOM does; React wants `hidden` as a boolean and `tabindex` as
3432
+ * `tabIndex`. `aria-*`, `data-*`, `id`, `role`, `type` and `title` pass through as they are.
3433
+ * Every component that spreads core's attributes goes through this, so the mapping lives once.
3434
+ */
3435
+ function domAttrs(attrs) {
3436
+ const out = {};
3437
+ for (const [name, value] of Object.entries(attrs)) if (name === "hidden") out.hidden = true;
3438
+ else if (name === "tabindex") out.tabIndex = Number(value);
3439
+ else out[name] = value;
3440
+ return out;
3441
+ }
3442
+ //#endregion
3443
+ //#region src/components/sidebar-context.ts
3444
+ const SidebarContextObject = createContext(null);
3445
+ /** The sidebar context: throws a readable error outside `InkSidebar`. */
3446
+ function useSidebarContext(consumer) {
3447
+ const ctx = useContext(SidebarContextObject);
3448
+ if (!ctx) throw new Error(`[inkbar] ${consumer} must be rendered inside <InkSidebar>.`);
3449
+ return ctx;
3450
+ }
3451
+ /**
3452
+ * The controller's state, re-rendering as it changes. Unlike `useReadable` it reads the live
3453
+ * state on the server too: the sidebar works without a document, so its markup is known there.
3454
+ */
3455
+ function useSidebarState(controller) {
3456
+ const { state } = controller;
3457
+ const subscribe = useCallback((onChange) => state.subscribe(() => onChange()), [state]);
3458
+ const get = useCallback(() => state.get(), [state]);
3459
+ return useSyncExternalStore(subscribe, get, get);
3460
+ }
3461
+ //#endregion
3462
+ //#region src/components/SidebarTrigger.tsx
3463
+ /**
3464
+ * A sidebar trigger's content: `createSidebar`'s markup, so the theme styles both alike. The
3465
+ * rail shows the icon alone (the trigger's `title` carries the label); tabs and stack headers
3466
+ * show the label too. The badge is hidden from assistive technology: the trigger's name has it.
3467
+ */
3468
+ function SidebarTrigger({ panel, rail }) {
3469
+ const badge = sidebarBadge(panel.badge);
3470
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
3471
+ panel.icon ? /* @__PURE__ */ jsx(InkIcon, { icon: panel.icon }) : null,
3472
+ !panel.icon || !rail ? /* @__PURE__ */ jsx("span", {
3473
+ className: "ink-sidebar-label",
3474
+ children: panel.label
3475
+ }) : null,
3476
+ /* @__PURE__ */ jsx("span", {
3477
+ className: "ink-sidebar-badge",
3478
+ "aria-hidden": "true",
3479
+ hidden: badge === null,
3480
+ children: badge
3481
+ })
3482
+ ] });
3483
+ }
3484
+ //#endregion
3485
+ //#region src/components/InkSidebar.tsx
3486
+ /** Sorts the panels by where their elements sit in the DOM. */
3487
+ function inDomOrder(entries) {
3488
+ return [...entries].sort((a, b) => {
3489
+ const x = a.element();
3490
+ const y = b.element();
3491
+ if (!x || !y || x === y) return 0;
3492
+ return x.compareDocumentPosition(y) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
3493
+ });
3494
+ }
3495
+ /**
3496
+ * The panels' registry around the controller. Before the first mount, panels preregister as
3497
+ * they render, in render order, so the server and the browser's first render show the panel
3498
+ * core opens. From then on they register from layout effects, ordered by DOM position. A
3499
+ * removal is applied a microtask later: a panel remounted in the same commit (a new `key`) is
3500
+ * back by then, so its open state does not fall back. While the sidebar itself is unmounted
3501
+ * (on its way out, or StrictMode and `<Activity>` replaying effects) nothing is applied.
3502
+ */
3503
+ function createRegistry(controller) {
3504
+ let mounted = false;
3505
+ let started = false;
3506
+ const early = [];
3507
+ let entries = [];
3508
+ const flush = () => {
3509
+ if (!mounted) return;
3510
+ controller.update({ panels: inDomOrder(entries).map((e) => e.info) });
3511
+ };
3512
+ const context = {
3513
+ controller,
3514
+ preregister(info) {
3515
+ if (started) return;
3516
+ const at = early.findIndex((p) => p.id === info.id);
3517
+ if (at < 0) early.push(info);
3518
+ else early[at] = info;
3519
+ controller.update({ panels: [...early] });
3520
+ },
3521
+ register(entry) {
3522
+ entries = [...entries, entry];
3523
+ flush();
3524
+ return () => {
3525
+ entries = entries.filter((e) => e !== entry);
3526
+ queueMicrotask(flush);
3527
+ };
3528
+ },
3529
+ changed: flush
3530
+ };
3531
+ /** Binds the triggers and ends the controller's pending state; returns the unbind. */
3532
+ function mount(el) {
3533
+ mounted = true;
3534
+ started = true;
3535
+ const unbind = controller.bindTriggers(el);
3536
+ controller.update({
3537
+ panels: inDomOrder(entries).map((e) => e.info),
3538
+ pending: false
3539
+ });
3540
+ return () => {
3541
+ mounted = false;
3542
+ unbind();
3543
+ };
3544
+ }
3545
+ return {
3546
+ context,
3547
+ mount
3548
+ };
3549
+ }
3550
+ /**
3551
+ * A sidebar of `InkPanel`s: `createSidebar`'s markup on core's sidebar controller, which holds
3552
+ * the open state, the ARIA, the keys and the clicks. Rail and tabs render their triggers here;
3553
+ * in a stack each panel renders its own header. Panels take their place in DOM order, so a
3554
+ * panel rendered later slots in where it stands. A closed panel stays mounted and hidden, and
3555
+ * the panels' content stays in place across layouts, so its state survives switching.
3556
+ *
3557
+ * `open` with `onOpenChange` controls it; `defaultOpen` starts it uncontrolled; with neither,
3558
+ * the first panel starts open. It needs no document, so it works outside `InkRoot`. The server
3559
+ * renders the root, the panels (the open one shown) and the stack headers; rail and tab
3560
+ * buttons follow once mounted.
3561
+ */
3562
+ function InkSidebar({ side = "left", layout = "rail", visible = true, open, defaultOpen, onOpenChange, className, children, ...rest }) {
3563
+ const controlled = open !== void 0;
3564
+ const [inner, setInner] = useState(defaultOpen);
3565
+ const value = controlled ? open : inner;
3566
+ const latest = useLatest({
3567
+ controlled,
3568
+ onOpenChange
3569
+ });
3570
+ const idPrefix = useId();
3571
+ const [{ context, mount }] = useState(() => createRegistry(createSidebarController({
3572
+ layout,
3573
+ side,
3574
+ visible,
3575
+ panels: [],
3576
+ ...value !== void 0 && { open: value },
3577
+ pending: true,
3578
+ idPrefix,
3579
+ onOpenChange: (next) => {
3580
+ if (!latest.current.controlled) setInner(next);
3581
+ latest.current.onOpenChange?.(next);
3582
+ }
3583
+ })));
3584
+ const { controller } = context;
3585
+ const state = useSidebarState(controller);
3586
+ useLayoutEffect(() => {
3587
+ controller.update({
3588
+ layout,
3589
+ side,
3590
+ visible,
3591
+ ...value !== void 0 && { open: value }
3592
+ });
3593
+ }, [
3594
+ controller,
3595
+ layout,
3596
+ side,
3597
+ visible,
3598
+ value
3599
+ ]);
3600
+ const root = useRef(null);
3601
+ useLayoutEffect(() => mount(root.current), [mount]);
3602
+ const rail = state.layout === "rail";
3603
+ return /* @__PURE__ */ jsxs("div", {
3604
+ ...rest,
3605
+ ref: root,
3606
+ className: cx("ink-sidebar", className),
3607
+ ...domAttrs(controller.rootAttrs()),
3608
+ children: [state.layout !== "stack" ? /* @__PURE__ */ jsx("div", {
3609
+ className: rail ? "ink-sidebar-rail" : "ink-sidebar-tabs",
3610
+ ...domAttrs(controller.tablistAttrs()),
3611
+ children: state.panels.map((panel) => /* @__PURE__ */ jsx("button", {
3612
+ ...domAttrs(controller.triggerAttrs(panel.id)),
3613
+ children: /* @__PURE__ */ jsx(SidebarTrigger, {
3614
+ panel,
3615
+ rail
3616
+ })
3617
+ }, panel.id))
3618
+ }) : null, /* @__PURE__ */ jsx("div", {
3619
+ className: state.layout !== "stack" ? "ink-sidebar-panels" : void 0,
3620
+ children: /* @__PURE__ */ jsx(SidebarContextObject.Provider, {
3621
+ value: context,
3622
+ children
3623
+ })
3624
+ })]
3625
+ });
3626
+ }
3627
+ //#endregion
3628
+ //#region src/components/InkPanel.tsx
3629
+ /**
3630
+ * One panel of an `InkSidebar`, holding `children` as its content. Closed, the panel stays
3631
+ * mounted and hidden. In a stack it renders its own header above its content.
3632
+ */
3633
+ function InkPanel({ id, label, icon, badge, children }) {
3634
+ const ctx = useSidebarContext("InkPanel");
3635
+ const { controller } = ctx;
3636
+ const info = useMemo(() => ({
3637
+ id,
3638
+ label,
3639
+ ...icon !== void 0 && { icon },
3640
+ ...badge !== void 0 && { badge }
3641
+ }), [
3642
+ id,
3643
+ label,
3644
+ icon,
3645
+ badge
3646
+ ]);
3647
+ ctx.preregister(info);
3648
+ const state = useSidebarState(controller);
3649
+ const wrapper = useRef(null);
3650
+ const [entry] = useState(() => ({
3651
+ info,
3652
+ element: () => wrapper.current
3653
+ }));
3654
+ useLayoutEffect(() => ctx.register(entry), [ctx, entry]);
3655
+ useLayoutEffect(() => {
3656
+ if (entry.info === info) return;
3657
+ entry.info = info;
3658
+ ctx.changed();
3659
+ }, [
3660
+ ctx,
3661
+ entry,
3662
+ info
3663
+ ]);
3664
+ const stack = state.layout === "stack";
3665
+ return /* @__PURE__ */ jsxs("div", {
3666
+ ref: wrapper,
3667
+ className: cx("ink-sidebar-slot", stack && "ink-sidebar-section"),
3668
+ children: [stack ? /* @__PURE__ */ jsx("button", {
3669
+ className: "ink-sidebar-header",
3670
+ ...domAttrs(controller.triggerAttrs(id)),
3671
+ children: /* @__PURE__ */ jsx(SidebarTrigger, {
3672
+ panel: info,
3673
+ rail: false
3674
+ })
3675
+ }) : null, /* @__PURE__ */ jsxs("div", {
3676
+ className: "ink-sidebar-panel",
3677
+ ...domAttrs(controller.panelAttrs(id)),
3678
+ children: [/* @__PURE__ */ jsx("div", {
3679
+ className: "ink-sidebar-panel-title",
3680
+ "aria-hidden": "true",
3681
+ children: label
3682
+ }), /* @__PURE__ */ jsx("div", {
3683
+ className: "ink-sidebar-panel-body",
3684
+ children
3685
+ })]
3686
+ })]
3687
+ });
3688
+ }
3689
+ //#endregion
3690
+ //#region src/to-readable.ts
3691
+ /**
3692
+ * A core `Readable` that follows a React value, for the core controllers and painters that
3693
+ * components build on. The same readable for the component's life; it takes each new value in
3694
+ * a layout effect, so declare it before the effects that hand it to core.
3695
+ */
3696
+ function useToReadable(value) {
3697
+ const [store] = useState(() => writable(value));
3698
+ useLayoutEffect(() => {
3699
+ store.set(value);
3700
+ }, [store, value]);
3701
+ const [view] = useState(() => readonly(store));
3702
+ return view;
3703
+ }
3704
+ //#endregion
3705
+ //#region src/components/InkThumbnail.tsx
3706
+ /**
3707
+ * One page as a static image, with its live annotations and plugin marks: core's thumbnail
3708
+ * painter on this element, which sizes it, names it ("Page N" unless `label` says otherwise)
3709
+ * and draws it once it nears the visible area. It is not focusable and does nothing on click:
3710
+ * wrap it in the control you need. Inside `InkRoot`; it renders empty on the server.
3711
+ */
3712
+ function InkThumbnail({ page, width, height, annotations = true, marks = true, label, className, ...rest }) {
3713
+ const ctx = useInkContext("InkThumbnail");
3714
+ const document = useToReadable(ctx.document);
3715
+ const pluginHost = useToReadable(ctx.pluginHost);
3716
+ const options = useMemo(() => ({
3717
+ page,
3718
+ width,
3719
+ height,
3720
+ annotations,
3721
+ marks,
3722
+ label
3723
+ }), [
3724
+ page,
3725
+ width,
3726
+ height,
3727
+ annotations,
3728
+ marks,
3729
+ label
3730
+ ]);
3731
+ const latest = useLatest(options);
3732
+ const el = useRef(null);
3733
+ const painter = useRef(null);
3734
+ useLayoutEffect(() => {
3735
+ painter.current?.update(options);
3736
+ }, [options]);
3737
+ useLayoutEffect(() => {
3738
+ const p = createThumbnailPainter(el.current, {
3739
+ document,
3740
+ pluginHost
3741
+ }, latest.current);
3742
+ painter.current = p;
3743
+ return () => {
3744
+ p.destroy();
3745
+ painter.current = null;
3746
+ };
3747
+ }, [
3748
+ document,
3749
+ pluginHost,
3750
+ latest
3751
+ ]);
3752
+ return /* @__PURE__ */ jsx("div", {
3753
+ ...rest,
3754
+ ref: el,
3755
+ className: cx("ink-thumbnail", className)
3756
+ });
3757
+ }
3758
+ //#endregion
3759
+ //#region src/components/InkThumbnails.tsx
3760
+ /** Until the list mounts, and on the server: nothing listed. */
3761
+ const EMPTY = {
3762
+ pages: [],
3763
+ current: -1,
3764
+ window: {
3765
+ start: 0,
3766
+ end: 0
3767
+ },
3768
+ offsets: [0],
3769
+ width: THUMBNAIL_WIDTH
3770
+ };
3771
+ /**
3772
+ * The pages as a list of thumbnails: `createThumbnails`' markup on core's thumbnails
3773
+ * controller, which holds the current page, the keys, the clicks, the Tab stop and the window.
3774
+ * Only the items near the visible part are mounted, each placed at its offset over a spacer of
3775
+ * the full height, and each holds an `InkThumbnail`. An item is a button that scrolls the
3776
+ * viewport to its page; the current page's has `aria-current="page"`. Inside `InkRoot`; it
3777
+ * renders empty on the server.
3778
+ */
3779
+ function InkThumbnails({ pages, width, annotations = true, marks = true, renderItem, className, ...rest }) {
3780
+ const ctx = useInkContext("InkThumbnails");
3781
+ const document = useToReadable(ctx.document);
3782
+ const pluginHost = useToReadable(ctx.pluginHost);
3783
+ const viewport = useToReadable(ctx.viewport);
3784
+ const options = useMemo(() => ({
3785
+ pages,
3786
+ width,
3787
+ annotations,
3788
+ marks
3789
+ }), [
3790
+ pages,
3791
+ width,
3792
+ annotations,
3793
+ marks
3794
+ ]);
3795
+ const latest = useLatest(options);
3796
+ const scroller = useRef(null);
3797
+ const [controller, setController] = useState(null);
3798
+ useLayoutEffect(() => {
3799
+ controller?.update(options);
3800
+ }, [controller, options]);
3801
+ useLayoutEffect(() => {
3802
+ const c = createThumbnailsController({
3803
+ document,
3804
+ pluginHost,
3805
+ viewport
3806
+ }, latest.current);
3807
+ setController(c);
3808
+ c.attach(scroller.current);
3809
+ return () => {
3810
+ c.destroy();
3811
+ setController(null);
3812
+ };
3813
+ }, [
3814
+ document,
3815
+ pluginHost,
3816
+ viewport,
3817
+ latest
3818
+ ]);
3819
+ const state = useReadable(controller?.state, EMPTY);
3820
+ const { window: win, offsets } = state;
3821
+ const items = state.pages.slice(win.start, win.end).map((page, k) => {
3822
+ const top = offsets[win.start + k];
3823
+ const bottom = offsets[win.start + k + 1];
3824
+ const attrs = controller?.itemAttrs(win.start + k) ?? {};
3825
+ return {
3826
+ page,
3827
+ current: page === state.current,
3828
+ top,
3829
+ height: bottom - top,
3830
+ attrs
3831
+ };
3832
+ });
3833
+ return /* @__PURE__ */ jsxs("div", {
3834
+ ...rest,
3835
+ ref: scroller,
3836
+ className: cx("ink-thumbnails", className),
3837
+ "data-ink-thumbnails": "",
3838
+ role: "list",
3839
+ children: [/* @__PURE__ */ jsx("div", {
3840
+ className: "ink-thumbnails-spacer",
3841
+ "aria-hidden": "true",
3842
+ style: { height: `${offsets.at(-1) ?? 0}px` }
3843
+ }), items.map((item) => /* @__PURE__ */ jsx("div", {
3844
+ className: "ink-thumbnails-entry",
3845
+ ...domAttrs(item.attrs),
3846
+ style: {
3847
+ top: `${item.top}px`,
3848
+ height: `${item.height}px`
3849
+ },
3850
+ children: /* @__PURE__ */ jsxs("button", {
3851
+ type: "button",
3852
+ className: "ink-thumbnails-item",
3853
+ "data-ink-thumbnail-item": item.page,
3854
+ "aria-current": item.current ? "page" : void 0,
3855
+ children: [
3856
+ /* @__PURE__ */ jsx(InkThumbnail, {
3857
+ page: item.page,
3858
+ width: state.width,
3859
+ annotations,
3860
+ marks
3861
+ }),
3862
+ /* @__PURE__ */ jsx("span", {
3863
+ className: "ink-thumbnails-label",
3864
+ "aria-hidden": "true",
3865
+ children: item.page + 1
3866
+ }),
3867
+ renderItem?.({
3868
+ page: item.page,
3869
+ current: item.current
3870
+ })
3871
+ ]
3872
+ })
3873
+ }, item.page))]
3874
+ });
3875
+ }
3876
+ //#endregion
3877
+ //#region src/components/RecordCards.tsx
3878
+ const NO_ROWS = Object.freeze([]);
3879
+ const actionButtons = (row) => [...row.querySelectorAll(".ink-card-actions > button")];
3880
+ /**
3881
+ * A record list's markup on core's record list controller, shared by `InkRecordList` and
3882
+ * `InkSearchResults`: `createRecordList`'s `div.ink-records` holding `ul.ink-record-list` with
3883
+ * one `li.ink-record` card per row, the empty text and a polite live region. The controller
3884
+ * holds the rows, the count and the announcement, and handles the clicks, keys, Tab stop,
3885
+ * focus and scrolling. Empty, with no empty text, until there is a source.
3886
+ */
3887
+ function RecordCards({ source, emptyShown = true, empty, onCountChange, className, ...rest }) {
3888
+ const root = useRef(null);
3889
+ const [controller, setController] = useState(null);
3890
+ const latest = useLatest(onCountChange);
3891
+ /** The count last reported, across sources and StrictMode's replayed effects. */
3892
+ const reported = useRef(null);
3893
+ const held = useRef(null);
3894
+ useLayoutEffect(() => {
3895
+ const el = root.current;
3896
+ if (!source || !el) return;
3897
+ const c = createRecordListController(source);
3898
+ const report = (count) => {
3899
+ if (count === reported.current) return;
3900
+ reported.current = count;
3901
+ latest.current?.(count);
3902
+ };
3903
+ /**
3904
+ * Before the rows re-render under a focused action whose button goes (its card lost that
3905
+ * action), note where it was; the layout effect below focuses the same place in the card,
3906
+ * else the row. A row that goes is the controller's to re-home.
3907
+ */
3908
+ const holdActionFocus = () => {
3909
+ const active = el.ownerDocument.activeElement;
3910
+ if (!(active instanceof HTMLButtonElement) || !el.contains(active)) return;
3911
+ const row = active.closest("[data-ink-record]");
3912
+ const id = row?.dataset.inkRecord;
3913
+ if (!row || id === void 0) return;
3914
+ held.current = {
3915
+ button: active,
3916
+ id,
3917
+ index: actionButtons(row).indexOf(active)
3918
+ };
3919
+ };
3920
+ setController(c);
3921
+ report(c.count.get());
3922
+ const stops = [c.rows.subscribe(holdActionFocus), c.count.subscribe(report)];
3923
+ const detach = c.attach(el);
3924
+ return () => {
3925
+ for (const stop of stops) stop();
3926
+ detach();
3927
+ c.destroy();
3928
+ setController(null);
3929
+ };
3930
+ }, [source, latest]);
3931
+ const rows = useReadable(controller?.rows, NO_ROWS);
3932
+ const announcement = useReadable(controller?.announcement, "");
3933
+ const showEmpty = controller !== null && rows.length === 0 && emptyShown;
3934
+ useLayoutEffect(() => {
3935
+ const focus = held.current;
3936
+ const el = root.current;
3937
+ held.current = null;
3938
+ if (!focus || !el || focus.button.isConnected) return;
3939
+ const row = [...el.querySelectorAll("[data-ink-record]")].find((r) => r.dataset.inkRecord === focus.id);
3940
+ if (row) (actionButtons(row)[focus.index] ?? row).focus({ preventScroll: true });
3941
+ }, [rows]);
3942
+ return /* @__PURE__ */ jsxs("div", {
3943
+ ...rest,
3944
+ ref: root,
3945
+ className: cx("ink-records", className),
3946
+ "data-ink-records": "",
3947
+ tabIndex: -1,
3948
+ children: [
3949
+ /* @__PURE__ */ jsx("ul", {
3950
+ className: "ink-record-list",
3951
+ role: "list",
3952
+ hidden: rows.length === 0,
3953
+ children: rows.map(({ item, card, selected }) => /* @__PURE__ */ jsx("li", {
3954
+ className: "ink-record",
3955
+ "data-ink-record": item.id,
3956
+ "aria-current": selected ? "true" : void 0,
3957
+ "data-selected": selected ? "" : void 0,
3958
+ children: /* @__PURE__ */ jsx("div", {
3959
+ className: cx("ink-card", card?.className),
3960
+ children: card ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
3961
+ /* @__PURE__ */ jsx("p", {
3962
+ className: "ink-card-title",
3963
+ children: card.title
3964
+ }),
3965
+ card.meta !== void 0 ? /* @__PURE__ */ jsxs("p", {
3966
+ className: "ink-card-meta",
3967
+ children: [card.swatch !== void 0 ? /* @__PURE__ */ jsx("span", {
3968
+ className: "ink-card-swatch",
3969
+ style: { background: card.swatch }
3970
+ }) : null, card.meta]
3971
+ }) : null,
3972
+ card.body !== void 0 ? /* @__PURE__ */ jsx("p", {
3973
+ className: "ink-card-body",
3974
+ children: card.body
3975
+ }) : null,
3976
+ card.excerpt !== void 0 ? /* @__PURE__ */ jsxs("p", {
3977
+ className: "ink-card-excerpt",
3978
+ children: [
3979
+ card.excerpt.before,
3980
+ /* @__PURE__ */ jsx("mark", { children: card.excerpt.match }),
3981
+ card.excerpt.after
3982
+ ]
3983
+ }) : null,
3984
+ /* @__PURE__ */ jsx("div", {
3985
+ className: "ink-card-actions",
3986
+ children: Object.keys(card.actions).map((label) => /* @__PURE__ */ jsx("button", {
3987
+ type: "button",
3988
+ onClick: () => controller?.act(item.id, label),
3989
+ children: label
3990
+ }, label))
3991
+ })
3992
+ ] }) : null
3993
+ })
3994
+ }, item.id))
3995
+ }),
3996
+ /* @__PURE__ */ jsx("p", {
3997
+ className: "ink-record-list-empty",
3998
+ hidden: !showEmpty,
3999
+ children: empty === void 0 ? "Nothing here yet." : empty
4000
+ }),
4001
+ /* @__PURE__ */ jsx("div", {
4002
+ className: "ink-visually-hidden",
4003
+ "aria-live": "polite",
4004
+ "data-ink-record-status": "",
4005
+ children: announcement
4006
+ })
4007
+ ]
4008
+ });
4009
+ }
4010
+ //#endregion
4011
+ //#region src/components/InkRecordList.tsx
4012
+ function isDevelopment() {
4013
+ try {
4014
+ return process.env.NODE_ENV !== "production";
4015
+ } catch {
4016
+ return false;
4017
+ }
4018
+ }
4019
+ /**
4020
+ * The records a plugin keeps under `records` (`<plugin>.<name>`) as a list of cards, the same
4021
+ * cards their marks open, with the same actions: `createRecordList` on core's record list
4022
+ * controller. The list, the marks and `useRecordSelection` share one selection. `card`, `where`
4023
+ * and `sort` replace the marks' card, filter and reading order; a new function rebuilds the
4024
+ * list, so keep them stable (`useCallback`, or defined outside the component).
4025
+ * `onCountChange` gets the number listed whenever it changes; `empty` replaces "Nothing here
4026
+ * yet.". It fills its parent's height. Inside `InkRoot`; it renders empty on the server.
4027
+ *
4028
+ * In place of `records`, `source` takes a host's own `RecordListSource` and lists it with the
4029
+ * same controller and cards. Give one of the two: with both, `source` wins and a development
4030
+ * build warns.
4031
+ */
4032
+ function InkRecordList({ records, source, card, where, sort, onCountChange, empty, ...rest }) {
4033
+ const ctx = useInkContext("InkRecordList");
4034
+ const viewport = useToReadable(ctx.viewport);
4035
+ const { pluginHost } = ctx;
4036
+ const both = records !== void 0 && source !== void 0;
4037
+ useEffect(() => {
4038
+ if (both && isDevelopment()) console.warn("[inkbar] InkRecordList takes `records` or `source`, not both: it lists `source`.");
4039
+ }, [both]);
4040
+ const listed = useMemo(() => {
4041
+ if (source) return source;
4042
+ if (!pluginHost || records === void 0) return null;
4043
+ const opts = {};
4044
+ if (card) opts.card = card;
4045
+ if (where) opts.where = where;
4046
+ if (sort) opts.sort = sort;
4047
+ return recordsSource({
4048
+ pluginHost,
4049
+ viewport
4050
+ }, records, opts);
4051
+ }, [
4052
+ source,
4053
+ pluginHost,
4054
+ records,
4055
+ card,
4056
+ where,
4057
+ sort,
4058
+ viewport
4059
+ ]);
4060
+ return /* @__PURE__ */ jsx(RecordCards, {
4061
+ ...rest,
4062
+ source: listed,
4063
+ onCountChange,
4064
+ empty
4065
+ });
4066
+ }
4067
+ //#endregion
4068
+ //#region src/components/InkSearchResults.tsx
4069
+ /**
4070
+ * The search hits as a list of cards, for a sidebar panel: `createSearchResults` on core's
4071
+ * record list controller over the root's search. One card per hit, with its page and the text
4072
+ * around it. Choosing a card makes its hit current, as the find bar's next and previous do, and
4073
+ * the current hit is the selected card. Above the cards, a query field (`field={false}`
4074
+ * leaves it out when a find bar is on the page); under them, the find bar's match count.
4075
+ * Field and find bar share the search state, so they never disagree. It fills its parent's
4076
+ * height. Inside `InkRoot`; it renders empty on the server.
4077
+ */
4078
+ function InkSearchResults({ field = true, className, ...rest }) {
4079
+ const ctx = useInkContext("InkSearchResults");
4080
+ const search = useSearch();
4081
+ const { state } = search;
4082
+ const source = useMemo(() => ctx.search ? searchSource(ctx.search) : null, [ctx.search]);
4083
+ const input = useRef(null);
4084
+ useLayoutEffect(() => {
4085
+ const el = input.current;
4086
+ if (el && el.value !== state.query) el.value = state.query;
4087
+ }, [state.query, field]);
4088
+ return /* @__PURE__ */ jsxs("div", {
4089
+ ...rest,
4090
+ className: cx("ink-search-results", className),
4091
+ "data-ink-search-results": "",
4092
+ children: [
4093
+ field ? /* @__PURE__ */ jsx("div", {
4094
+ className: "ink-search-results-bar",
4095
+ role: "search",
4096
+ children: /* @__PURE__ */ jsxs("div", {
4097
+ className: "ink-input-group",
4098
+ children: [/* @__PURE__ */ jsx("span", {
4099
+ className: "ink-input-accessory",
4100
+ children: /* @__PURE__ */ jsx(InkIcon, { icon: Search })
4101
+ }), /* @__PURE__ */ jsx("input", {
4102
+ ref: input,
4103
+ type: "text",
4104
+ "aria-label": "Find in document",
4105
+ placeholder: "Search for a word or phrase",
4106
+ autoComplete: "off",
4107
+ spellCheck: false,
4108
+ defaultValue: state.query,
4109
+ onInput: (e) => search.setQuery(e.currentTarget.value),
4110
+ onKeyDown: (e) => handleResultsFieldKey(e.nativeEvent, search)
4111
+ })]
4112
+ })
4113
+ }) : null,
4114
+ /* @__PURE__ */ jsx(RecordCards, {
4115
+ source,
4116
+ emptyShown: isBlankQuery(state.query),
4117
+ empty: "Search to list every match."
4118
+ }),
4119
+ /* @__PURE__ */ jsx("div", {
4120
+ className: "ink-search-results-count",
4121
+ "data-ink-search-results-count": "",
4122
+ children: findCountText(state)
4123
+ })
4124
+ ]
4125
+ });
4126
+ }
4127
+ //#endregion
4128
+ 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 };
3329
4129
 
3330
4130
  //# sourceMappingURL=index.mjs.map