@inkbar/react 0.9.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, 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,
@@ -1895,6 +1933,56 @@ function InkUndo({ asChild = false, className, children, disabled, onClick, ...r
1895
1933
  });
1896
1934
  }
1897
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
1898
1986
  //#region src/components/InkRedo.tsx
1899
1987
  function InkRedo({ asChild = false, className, children, disabled, onClick, ...rest }) {
1900
1988
  const { canRedo, redo } = useHistory();
@@ -2034,19 +2122,25 @@ function InkFindBar({ className, ...rest }) {
2034
2122
  role: "search",
2035
2123
  hidden: !state.open,
2036
2124
  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
- }
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
+ })]
2050
2144
  }),
2051
2145
  /* @__PURE__ */ jsx("span", {
2052
2146
  className: "ink-find-count",
@@ -2183,7 +2277,7 @@ function InkZoomSelect({ className, ...rest }) {
2183
2277
  value,
2184
2278
  onValueChange,
2185
2279
  children: [/* @__PURE__ */ jsxs(Select.Trigger, {
2186
- className: cx("ink-zoom-select ink-select-trigger", className),
2280
+ className: cx("ink-select ink-select-trigger ink-zoom-select", className),
2187
2281
  "data-ink-zoom-select": "",
2188
2282
  "aria-label": "Zoom",
2189
2283
  ...rest,
@@ -3325,6 +3419,706 @@ function ThreadForm({ editingRoot, locked, deleteLabel, onSubmit, onRemove, onCl
3325
3419
  });
3326
3420
  }
3327
3421
  //#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 };
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 };
3329
4123
 
3330
4124
  //# sourceMappingURL=index.mjs.map