@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/README.md +64 -3
- package/dist/index.d.mts +295 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +835 -35
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
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$
|
|
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$
|
|
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$
|
|
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$
|
|
171
|
-
setQuery: search ? (query) => search.setQuery(query) : noop$
|
|
172
|
-
setMatchCase: search ? (on) => search.setMatchCase(on) : noop$
|
|
173
|
-
next: search ? () => search.next() : noop$
|
|
174
|
-
previous: search ? () => search.previous() : noop$
|
|
175
|
-
|
|
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__ */
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
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-
|
|
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
|
-
|
|
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
|