@inkbar/react 0.6.0 → 0.8.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
- import { InkError, createMemorySignatureStore, createSignatureLibrary, createSnapshotHistory, isAbandonedNote, openDocument, readonly, repliesTo, threadOf, threadRoot, writable } from "@inkbar/core";
4
- import { COLOR_COLUMNS, FONT_FAMILIES, MARKUP_ICONS, MARKUP_KINDS, SHAPE_COLUMNS, SHAPE_ICONS, SHAPE_KINDS, SIGNATURE_FONTS, SIGNATURE_LABELS, SIGNATURE_TABS, TOOL_LABELS, ZOOM_LEVELS, colorMenuModel, createFocusReturn, createPluginHost, createSignatureDraft, createToolOptions, cssVariableDefaults, focusInitialMenuItem, fontFamilyValue, handleMenuKeys, handleRadioKeys, handleTabKeys, highlightMenuModel, lineStyleMenuModel, menuItems, mountViewer, observeAnnotationElement, shapeMenuModel, shouldKeepOpenOnAnnotation, signatureMenuModel, signaturePreview, textStyleMenuModel, threadDeleteLabel, threadPopoverLabel, trackToolStyle, unknownCommand } from "@inkbar/core/dom";
3
+ import { InkError, createMemorySignatureStore, createSignatureLibrary, createSnapshotHistory, isAbandonedNote, openDocument, readonly, repliesTo, signField, threadOf, threadRoot, writable } from "@inkbar/core";
4
+ import { COLOR_COLUMNS, FONT_FAMILIES, FORM_LABELS, MARKUP_ICONS, MARKUP_KINDS, SHAPE_COLUMNS, SHAPE_ICONS, SHAPE_KINDS, SIGNATURE_FONTS, SIGNATURE_LABELS, SIGNATURE_TABS, TOOL_LABELS, ZOOM_LEVELS, attachHistoryKeys, colorMenuModel, createFocusReturn, createPluginHost, createSearchController, createSignatureDraft, createToolOptions, cssVariableDefaults, findAnnouncer, findCountText, focusInitialMenuItem, fontFamilyValue, handleFindKey, handleMenuKeys, handleRadioKeys, handleTabKeys, highlightMenuModel, keepHistoryFocus, lineStyleMenuModel, menuItems, mountViewer, observeAnnotationElement, pageSelectionText, shapeMenuModel, shouldKeepOpenOnAnnotation, signatureMenuModel, signaturePreview, textStyleMenuModel, threadDeleteLabel, threadPopoverLabel, trackToolStyle, unknownCommand, viewerSearchOf, watchDetached } from "@inkbar/core/dom";
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, Check, ChevronDown, ChevronLeft, ChevronRight, Highlighter, ICON_ATTRIBUTES, Italic, MessageSquare, Minus, PenLine, PenTool, Plus, Redo2, Signature, Square, SquareDashedMousePointer, TextCursor, Trash2, Type, Underline, Undo2, ZoomIn, ZoomOut } from "@inkbar/icons";
8
+ import { ALargeSmall, AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, CaseSensitive, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Highlighter, ICON_ATTRIBUTES, Italic, MessageSquare, Minus, PenLine, PenTool, Plus, Redo2, Search, Signature, Square, SquareDashedMousePointer, TextCursor, Trash2, Type, Underline, Undo2, X, ZoomIn, ZoomOut } from "@inkbar/icons";
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$1 = () => void 0;
22
+ const noop$2 = () => 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$1, [readable]);
29
+ const subscribe = useCallback((onChange) => readable ? readable.subscribe(() => onChange()) : noop$2, [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);
@@ -147,6 +147,35 @@ function useZoom() {
147
147
  ]);
148
148
  }
149
149
  //#endregion
150
+ //#region src/hooks/useSearch.ts
151
+ const CLOSED = {
152
+ open: false,
153
+ query: "",
154
+ matchCase: false,
155
+ hits: [],
156
+ current: -1,
157
+ scanning: false
158
+ };
159
+ const noop$1 = () => void 0;
160
+ /**
161
+ * The document search for hosts building their own control: the state, and the actions the
162
+ * find bar uses. Search is set up once InkRoot has mounted, so until then (and on the server)
163
+ * the state is closed and the actions do nothing. `open()` runs the search and draws the
164
+ * highlights even when no find bar is mounted.
165
+ */
166
+ function useSearch() {
167
+ const search = useInkContext("useSearch()").search;
168
+ return {
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
176
+ };
177
+ }
178
+ //#endregion
150
179
  //#region src/hooks/usePage.ts
151
180
  function usePage() {
152
181
  const { viewport, document } = useInkContext("usePage()");
@@ -224,6 +253,30 @@ function useCommand(id) {
224
253
  };
225
254
  }
226
255
  //#endregion
256
+ //#region src/hooks/useForm.ts
257
+ const NO_FIELDS = [];
258
+ const NO_VALUES = Object.freeze({});
259
+ /** The open document's form: its fields, their values, and `set` and `reset`. */
260
+ function useForm() {
261
+ const { document } = useInkContext("useForm()");
262
+ const form = document?.form;
263
+ const fields = useReadable(form?.fields, NO_FIELDS);
264
+ const values = useReadable(form?.values, NO_VALUES);
265
+ const actions = useMemo(() => ({
266
+ set: (v) => form?.set(v),
267
+ reset: (names) => form?.reset(names)
268
+ }), [form]);
269
+ return useMemo(() => ({
270
+ fields,
271
+ values,
272
+ ...actions
273
+ }), [
274
+ fields,
275
+ values,
276
+ actions
277
+ ]);
278
+ }
279
+ //#endregion
227
280
  //#region src/use-latest.ts
228
281
  /**
229
282
  * A ref that always holds the latest render's value, for callbacks that outlive a render
@@ -261,7 +314,7 @@ function isDevelopment() {
261
314
  function setTestHook(el, doc) {
262
315
  if (el && isDevelopment()) el.__inkDocument = doc;
263
316
  }
264
- function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsChange, onChange, 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 }) {
317
+ 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 }) {
265
318
  const rootElRef = useRef(null);
266
319
  const [rootEl, setRootElState] = useState(null);
267
320
  const setRootEl = useCallback((el) => {
@@ -291,6 +344,9 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
291
344
  defaults,
292
345
  annotations,
293
346
  controlled: controlledProp,
347
+ fields,
348
+ onFieldsChange,
349
+ onFormAction,
294
350
  history,
295
351
  historyEnabled,
296
352
  onAnnotationsChange,
@@ -307,6 +363,16 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
307
363
  tool: internalTool,
308
364
  tools
309
365
  });
366
+ /**
367
+ * Hands `e` to `onError` one microtask later, as `onFieldsChange` and `onFormAction` are: a
368
+ * script or a press reports from inside its own work. Dropped once `doc` is no longer open.
369
+ */
370
+ const reportErrorFor = useCallback((doc, e) => {
371
+ queueMicrotask(() => {
372
+ if (documentRef.current !== doc) return;
373
+ flushSync(() => latest.current.onError?.(e));
374
+ });
375
+ }, [latest]);
310
376
  const snapshotHistory = useMemo(() => createSnapshotHistory({
311
377
  apply: (list) => documentRef.current?.annotations.replaceAll(list, "history"),
312
378
  enabled: () => latest.current.historyEnabled
@@ -347,10 +413,15 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
347
413
  }, [snapshotHistory]);
348
414
  const hostRef = useRef(null);
349
415
  const [pluginHost, setPluginHost] = useState(null);
416
+ const [search, setSearch] = useState(null);
350
417
  const hostMounts = useRef(0);
351
418
  useLayoutEffect(() => {
352
419
  hostMounts.current += 1;
353
420
  if (!hostRef.current || hostRef.current.destroyed) {
421
+ const search = createSearchController({
422
+ document: readonly(plugins.document),
423
+ viewport: () => viewportRef.current
424
+ });
354
425
  const host = createPluginHost({
355
426
  plugins: latest.current.plugins ?? [],
356
427
  document: readonly(plugins.document),
@@ -361,13 +432,16 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
361
432
  },
362
433
  root: readonly(plugins.root),
363
434
  decorate: () => latest.current.decorateAnnotation,
364
- viewport: () => viewportRef.current
435
+ viewport: () => viewportRef.current,
436
+ search: viewerSearchOf(search)
365
437
  });
366
438
  hostRef.current = {
367
439
  host,
440
+ search,
368
441
  destroyed: false
369
442
  };
370
443
  setPluginHost(host);
444
+ setSearch(search);
371
445
  }
372
446
  return () => {
373
447
  hostMounts.current -= 1;
@@ -376,6 +450,7 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
376
450
  if (hostMounts.current > 0 || !current || current.destroyed) return;
377
451
  current.destroyed = true;
378
452
  current.host.destroy();
453
+ current.search.destroy();
379
454
  });
380
455
  };
381
456
  }, []);
@@ -393,6 +468,7 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
393
468
  /** The plugins told of `opened`, and the snapshot history's hold on its store. */
394
469
  let told = null;
395
470
  let detach = noop;
471
+ let stopFormErrors = noop;
396
472
  const run = async () => {
397
473
  try {
398
474
  const doc = await openDocument(source, {
@@ -412,6 +488,16 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
412
488
  });
413
489
  });
414
490
  }
491
+ } : { mode: "uncontrolled" },
492
+ fields: latest.current.fields !== void 0 ? {
493
+ mode: "controlled",
494
+ initial: latest.current.fields,
495
+ onChange: (next, change) => {
496
+ queueMicrotask(() => {
497
+ if (cancelled) return;
498
+ flushSync(() => latest.current.onFieldsChange?.(next, change));
499
+ });
500
+ }
415
501
  } : { mode: "uncontrolled" }
416
502
  });
417
503
  if (cancelled) {
@@ -421,6 +507,13 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
421
507
  opened = doc;
422
508
  const current = latest.current.annotations;
423
509
  if (doc.annotations.mode === "controlled" && current !== void 0) doc.annotations.setControlledValue(current);
510
+ const currentFields = latest.current.fields;
511
+ if (currentFields !== void 0) doc.form.setControlledValue(currentFields);
512
+ let held = [];
513
+ stopFormErrors = doc.form.on("error", (e) => {
514
+ if (held) held.push(e);
515
+ else reportErrorFor(doc, e);
516
+ });
424
517
  documentRef.current = doc;
425
518
  if (doc.annotations.mode === "controlled") detach = snapshotHistory.attach(doc.annotations);
426
519
  told = plugins;
@@ -429,8 +522,14 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
429
522
  setOverride(null);
430
523
  setTestHook(rootElRef.current, doc);
431
524
  const s = doc.status.get();
432
- if (s.state === "ready") latest.current.onReady?.(doc);
433
- else if (s.state === "error") latest.current.onError?.(s.error);
525
+ try {
526
+ if (s.state === "ready") latest.current.onReady?.(doc);
527
+ else if (s.state === "error") latest.current.onError?.(s.error);
528
+ } finally {
529
+ const early = held;
530
+ held = void 0;
531
+ for (const e of early) reportErrorFor(doc, e);
532
+ }
434
533
  } catch (cause) {
435
534
  if (cancelled) return;
436
535
  const error = cause instanceof InkError ? cause : new InkError("InvalidPdf", cause instanceof Error ? cause.message : String(cause), {}, cause);
@@ -448,6 +547,7 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
448
547
  cancelled = true;
449
548
  told?.document.set(null);
450
549
  detach();
550
+ stopFormErrors();
451
551
  opened?.destroy();
452
552
  documentRef.current = null;
453
553
  setDocument(null);
@@ -458,11 +558,15 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
458
558
  password,
459
559
  snapshotHistory,
460
560
  latest,
461
- plugins
561
+ plugins,
562
+ reportErrorFor
462
563
  ]);
463
564
  useLayoutEffect(() => {
464
565
  if (annotations && document?.annotations.mode === "controlled") document.annotations.setControlledValue(annotations);
465
566
  }, [annotations, document]);
567
+ useLayoutEffect(() => {
568
+ if (fields && document) document.form.setControlledValue(fields);
569
+ }, [fields, document]);
466
570
  useEffect(() => {
467
571
  if (tool) setInternalTool(tool);
468
572
  }, [tool]);
@@ -474,6 +578,16 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
474
578
  }, [latest]);
475
579
  const reportZoom = useCallback((z) => latest.current.onZoomChange?.(z), [latest]);
476
580
  const reportPage = useCallback((p) => latest.current.onPageChange?.(p), [latest]);
581
+ const reportFormAction = useCallback((a) => {
582
+ const doc = documentRef.current;
583
+ queueMicrotask(() => {
584
+ if (documentRef.current !== doc) return;
585
+ flushSync(() => latest.current.onFormAction?.(a));
586
+ });
587
+ }, [latest]);
588
+ const reportError = useCallback((e) => {
589
+ if (documentRef.current) reportErrorFor(documentRef.current, e);
590
+ }, [reportErrorFor]);
477
591
  const setViewport = useCallback((vp, tc) => {
478
592
  viewportRef.current = vp;
479
593
  setViewportState(vp);
@@ -507,6 +621,31 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
507
621
  pluginHistory
508
622
  ]);
509
623
  const latestHistory = useLatest(inkHistory);
624
+ useEffect(() => {
625
+ if (!rootEl) return;
626
+ const stopKeys = attachHistoryKeys(rootEl, () => {
627
+ const doc = documentRef.current;
628
+ if (!doc) return null;
629
+ const snap = doc.annotations.mode === "controlled";
630
+ const on = latest.current.historyEnabled;
631
+ return {
632
+ canUndo: snap ? on && snapshotHistory.canUndo.get() : doc.history.canUndo.get(),
633
+ canRedo: snap ? on && snapshotHistory.canRedo.get() : doc.history.canRedo.get(),
634
+ undo: () => latestHistory.current.undo(),
635
+ redo: () => latestHistory.current.redo()
636
+ };
637
+ });
638
+ const stopFocus = keepHistoryFocus(rootEl, () => rootEl.querySelector("[data-ink-scroll]"));
639
+ return () => {
640
+ stopKeys();
641
+ stopFocus();
642
+ };
643
+ }, [
644
+ rootEl,
645
+ latestHistory,
646
+ latest,
647
+ snapshotHistory
648
+ ]);
510
649
  /** The document's tool style; it batches through `history`, whichever mode is on. */
511
650
  const styleTracker = useMemo(() => document ? trackToolStyle({
512
651
  store: document.annotations,
@@ -582,10 +721,14 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
582
721
  activated,
583
722
  setActivated,
584
723
  pluginHost,
724
+ search,
585
725
  viewportEl: plugins.root,
586
726
  history: inkHistory,
587
727
  rootEl,
588
- setViewport
728
+ setViewport,
729
+ formHighlight,
730
+ reportFormAction,
731
+ reportError
589
732
  }), [
590
733
  assets,
591
734
  document,
@@ -611,11 +754,23 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
611
754
  activated,
612
755
  setActivated,
613
756
  pluginHost,
757
+ search,
614
758
  plugins,
615
759
  inkHistory,
616
760
  rootEl,
617
- setViewport
761
+ setViewport,
762
+ formHighlight,
763
+ reportFormAction,
764
+ reportError
618
765
  ]);
766
+ useEffect(() => {
767
+ if (!rootEl || !search) return;
768
+ const onKeyDown = (e) => {
769
+ handleFindKey(e, search, () => pageSelectionText(rootEl));
770
+ };
771
+ rootEl.addEventListener("keydown", onKeyDown);
772
+ return () => rootEl.removeEventListener("keydown", onKeyDown);
773
+ }, [rootEl, search]);
619
774
  const Comp = asChild ? Slot.Root : "div";
620
775
  return /* @__PURE__ */ jsx(InkContextObject.Provider, {
621
776
  value: ctx,
@@ -639,6 +794,77 @@ function cx(...parts) {
639
794
  return out === "" ? void 0 : out;
640
795
  }
641
796
  //#endregion
797
+ //#region src/components/InkFormAlert.tsx
798
+ function InkFormAlert({ message, shown, fallback, onClose }) {
799
+ const uid = useId();
800
+ const panel = useRef(null);
801
+ const ok = useRef(null);
802
+ /** Where focus was when the alert opened. */
803
+ const returnTo = useRef(null);
804
+ const open = message !== null;
805
+ const wasOpen = useRef(false);
806
+ useLayoutEffect(() => {
807
+ if (!open) {
808
+ wasOpen.current = false;
809
+ return;
810
+ }
811
+ if (!wasOpen.current) {
812
+ wasOpen.current = true;
813
+ const active = document.activeElement;
814
+ returnTo.current = active instanceof HTMLElement && active !== document.body ? active : null;
815
+ }
816
+ ok.current?.focus({ preventScroll: true });
817
+ }, [open, shown]);
818
+ const close = () => {
819
+ const hadFocus = !!panel.current?.contains(document.activeElement);
820
+ onClose();
821
+ if (!hadFocus) return;
822
+ (returnTo.current?.isConnected === true ? returnTo.current : fallback())?.focus({ preventScroll: true });
823
+ };
824
+ if (!open) return null;
825
+ return createPortal(/* @__PURE__ */ jsxs("div", {
826
+ ref: panel,
827
+ role: "alertdialog",
828
+ "aria-labelledby": `${uid}-title`,
829
+ "aria-describedby": `${uid}-message`,
830
+ className: "ink-dialog ink-form-alert",
831
+ "data-ink-form-alert": "",
832
+ "data-state": "open",
833
+ tabIndex: -1,
834
+ onKeyDown: (e) => {
835
+ if (e.key !== "Escape" || e.defaultPrevented) return;
836
+ e.preventDefault();
837
+ e.stopPropagation();
838
+ close();
839
+ },
840
+ children: [
841
+ /* @__PURE__ */ jsx("h2", {
842
+ id: `${uid}-title`,
843
+ className: "ink-dialog-title",
844
+ children: FORM_LABELS.alert
845
+ }),
846
+ /* @__PURE__ */ jsx("p", {
847
+ id: `${uid}-message`,
848
+ className: "ink-form-alert-message",
849
+ "data-ink-form-alert-message": "",
850
+ children: message
851
+ }),
852
+ /* @__PURE__ */ jsx("div", {
853
+ className: "ink-dialog-actions",
854
+ children: /* @__PURE__ */ jsx("button", {
855
+ ref: ok,
856
+ type: "button",
857
+ className: "ink-button",
858
+ "data-variant": "primary",
859
+ "data-ink-form-alert-ok": "",
860
+ onClick: close,
861
+ children: FORM_LABELS.ok
862
+ })
863
+ })
864
+ ]
865
+ }), document.body);
866
+ }
867
+ //#endregion
642
868
  //#region src/components/InkAnnotation.tsx
643
869
  function readState(el) {
644
870
  const s = el.dataset.state;
@@ -734,243 +960,214 @@ function InkPage({ page, host, mount, error, customAnnotations = false, renderAn
734
960
  }), t.el, t.annotation.id)) : null] });
735
961
  }
736
962
  //#endregion
737
- //#region src/components/InkViewport.tsx
738
- function InkViewport({ mode = "continuous", rotation = 0, gap = 16, buffer = 2, loading, empty, error, renderAnnotation, renderPageError, className, ...rest }) {
739
- const ctx = useInkContext("InkViewport");
740
- const scroll = useRef(null);
741
- const custom = !!renderAnnotation;
742
- const latest = useLatest({
743
- ctx,
744
- mode,
745
- rotation,
746
- gap,
747
- buffer,
748
- custom
963
+ //#region src/components/InkSignatureDialog.tsx
964
+ function InkSignatureDialog({ open, onOpenChange, fonts, select = true, onSave, onCloseAutoFocus, className }) {
965
+ return /* @__PURE__ */ jsx(Dialog.Root, {
966
+ open,
967
+ onOpenChange,
968
+ children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [/* @__PURE__ */ jsx(Dialog.Overlay, {
969
+ className: "ink-dialog-overlay",
970
+ "data-ink-dialog-overlay": ""
971
+ }), /* @__PURE__ */ jsxs(Dialog.Content, {
972
+ className: cx("ink-dialog ink-signature-dialog", className),
973
+ "data-ink-signature-dialog": "",
974
+ "aria-modal": "true",
975
+ "aria-describedby": void 0,
976
+ onOpenAutoFocus: (e) => {
977
+ e.preventDefault();
978
+ e.currentTarget.querySelector("[role=\"tab\"][aria-selected=\"true\"]")?.focus({ preventScroll: true });
979
+ },
980
+ onCloseAutoFocus,
981
+ children: [/* @__PURE__ */ jsx(Dialog.Title, {
982
+ className: "ink-dialog-title",
983
+ children: SIGNATURE_LABELS.dialog
984
+ }), /* @__PURE__ */ jsx(SignatureForm, {
985
+ fonts,
986
+ select,
987
+ onSave,
988
+ onClose: () => onOpenChange(false)
989
+ })]
990
+ })] })
749
991
  });
750
- const engine = useRef(null);
751
- /** Page hosts, mounts and errors the engine reported; InkPage renders portals for them. */
752
- const [pages, setPages] = useState([]);
753
- /**
754
- * Numbered before each build starts, so the pages a build mounts carry its own number. A
755
- * rebuild gives each page a new host and mount, so pages from an earlier build must remount,
756
- * not be reused.
757
- */
758
- const buildCount = useRef(0);
759
- /** The construction options the engine was last built with, so a rebuild runs only on a change. */
760
- const builtWith = useRef(null);
761
- /** The zoom and page last reported to the host. */
762
- const lastReportedZoom = useRef(void 0);
763
- const lastReportedPage = useRef(void 0);
992
+ }
993
+ const IDLE = {
994
+ tab: "draw",
995
+ padEmpty: true,
996
+ name: "",
997
+ font: "",
998
+ image: void 0,
999
+ imageError: void 0,
1000
+ saveError: void 0,
1001
+ canSave: false,
1002
+ saving: false
1003
+ };
1004
+ /** The dialog's body, mounted while it is open: one draft per opening. */
1005
+ function SignatureForm({ fonts, select, onSave, onClose }) {
1006
+ const ctx = useInkContext("InkSignatureDialog");
1007
+ const { setTool } = useTool();
1008
+ const [, setOptions] = useToolOptions("signature");
1009
+ const [draft] = useState(() => createSignatureDraft({ fonts: fonts ?? ctx.signatureFonts }));
1010
+ const s = useReadable(draft.state, IDLE);
1011
+ const uid = useId();
1012
+ const padHost = useRef(null);
764
1013
  useEffect(() => {
765
- const { ctx: c, mode: m, rotation: r, gap: g, buffer: b, custom: cu } = latest.current;
766
- const doc = c.document;
767
- const el = scroll.current;
768
- lastReportedZoom.current = void 0;
769
- lastReportedPage.current = void 0;
770
- if (!doc || !el || c.status.state !== "ready") return;
771
- let first = true;
772
- let stops = [];
773
- buildCount.current += 1;
774
- builtWith.current = {
775
- gap: g,
776
- buffer: b,
777
- custom: cu
778
- };
779
- const e = mountViewer({
780
- container: el,
781
- document: doc,
782
- zoom: c.zoomModel ?? "fit-width",
783
- ...c.pageModel === void 0 ? {} : { page: c.pageModel },
784
- mode: m,
785
- rotation: r,
786
- gap: g,
787
- buffer: b,
788
- tool: c.tool,
789
- ...c.hostTools ? { tools: c.hostTools } : {},
790
- ...c.pluginHost ? { plugins: c.pluginHost } : {},
791
- decorateAnnotation: (a) => latest.current.ctx.decorateAnnotation(a),
792
- ...c.toolStyle ? { toolStyle: c.toolStyle } : {},
793
- toolHistory: { batch: (fn) => latest.current.ctx.history.batch(fn) },
794
- toolOptions: c.toolOptions,
795
- fonts: [...c.fonts],
796
- signatureLibrary: () => latest.current.ctx.signatures,
797
- customAnnotations: cu,
798
- onActivate: (id) => latest.current.ctx.setActivated(id),
799
- onBuild: (vp, tc) => {
800
- latest.current.ctx.setViewport(vp, tc);
801
- if (first) {
802
- first = false;
803
- lastReportedZoom.current = vp.zoom.get();
804
- lastReportedPage.current = 0;
805
- }
806
- for (const stop of stops) stop();
807
- const reportZoom = (z) => {
808
- if (z === lastReportedZoom.current) return;
809
- lastReportedZoom.current = z;
810
- latest.current.ctx.reportZoom(z);
811
- };
812
- const reportPage = (p) => {
813
- if (p === lastReportedPage.current) return;
814
- lastReportedPage.current = p;
815
- latest.current.ctx.reportPage(p);
816
- };
817
- const followTool = (id) => {
818
- if (id !== latest.current.ctx.tool) latest.current.ctx.setTool(id);
819
- };
820
- stops = [
821
- vp.zoom.subscribe(reportZoom),
822
- vp.currentPage.subscribe(reportPage),
823
- tc.active.subscribe(followTool)
824
- ];
825
- reportZoom(vp.zoom.get());
826
- reportPage(vp.currentPage.get());
827
- },
828
- onPageMount: (page, mount, host) => {
829
- const build = buildCount.current;
830
- const custom = builtWith.current?.custom ?? false;
831
- setPages((prev) => [...prev, {
832
- build,
833
- page,
834
- host,
835
- mount,
836
- error: null,
837
- custom
838
- }]);
839
- },
840
- onPageUnmount: (page) => setPages((prev) => prev.filter((p) => p.page !== page)),
841
- onPageError: (page, err) => setPages((prev) => prev.map((p) => p.page === page ? {
842
- ...p,
843
- error: err
844
- } : p))
845
- });
846
- engine.current = e;
847
- return () => {
848
- for (const stop of stops) stop();
849
- e.destroy();
850
- engine.current = null;
851
- builtWith.current = null;
852
- c.setViewport(null, null);
853
- setPages((prev) => prev.length === 0 ? prev : []);
854
- };
855
- }, [
856
- ctx.document,
857
- ctx.status.state,
858
- ctx.pluginHost,
859
- latest
860
- ]);
861
- const { viewportEl } = ctx;
862
- useLayoutEffect(() => {
863
- const el = scroll.current;
864
- viewportEl.set(el);
1014
+ const host = padHost.current;
1015
+ const detach = host ? draft.attachPad(host) : void 0;
865
1016
  return () => {
866
- if (viewportEl.get() === el) viewportEl.set(null);
1017
+ detach?.();
1018
+ draft.destroy();
867
1019
  };
868
- }, [viewportEl]);
869
- const { onDecorateAnnotationChange } = ctx;
870
- useEffect(() => onDecorateAnnotationChange(() => engine.current?.redecorate()), [onDecorateAnnotationChange]);
871
- useEffect(() => {
872
- const built = builtWith.current;
873
- if (!engine.current || !built) return;
874
- if (built.gap === gap && built.buffer === buffer && built.custom === custom) return;
875
- buildCount.current += 1;
876
- builtWith.current = {
877
- gap,
878
- buffer,
879
- custom
880
- };
881
- engine.current.rebuild({
882
- gap,
883
- buffer,
884
- customAnnotations: custom
885
- });
886
- }, [
887
- gap,
888
- buffer,
889
- custom
890
- ]);
891
- useEffect(() => {
892
- const vp = engine.current?.viewport;
893
- const z = ctx.zoomModel;
894
- if (vp && z !== void 0 && z !== vp.zoom.get()) vp.setZoom(z);
895
- }, [ctx.zoomModel]);
896
- useEffect(() => {
897
- const vp = engine.current?.viewport;
898
- const p = ctx.pageModel;
899
- if (!vp || p === void 0 || p === lastReportedPage.current) return;
900
- if (p !== vp.currentPage.get()) vp.scrollToPage(p);
901
- }, [ctx.pageModel]);
902
- useEffect(() => {
903
- const tc = engine.current?.tools;
904
- if (tc && tc.active.get() !== ctx.tool) tc.set(ctx.tool);
905
- }, [ctx.tool]);
906
- useEffect(() => {
907
- const vp = engine.current?.viewport;
908
- if (vp && vp.mode.get() !== mode) vp.setMode(mode);
909
- }, [mode]);
910
- useEffect(() => {
911
- const vp = engine.current?.viewport;
912
- if (vp && vp.rotation.get() !== rotation) vp.setRotation(rotation);
913
- }, [rotation]);
914
- const state = ctx.status.state;
915
- const err = ctx.status.state === "error" ? ctx.status.error : null;
916
- return /* @__PURE__ */ jsxs("div", {
917
- ...rest,
918
- className: cx("ink-viewport-root", className),
919
- "data-ink-viewport-root": "",
920
- "data-state": state,
921
- children: [
922
- state === "loading" ? /* @__PURE__ */ jsx("div", {
923
- className: "ink-status",
924
- role: "status",
925
- "aria-live": "polite",
926
- children: loading === void 0 ? "Loading document…" : loading
927
- }) : err ? /* @__PURE__ */ jsx("div", {
928
- className: "ink-status ink-error",
929
- role: "alert",
930
- children: typeof error === "function" ? error({ error: err }) : error !== void 0 ? error : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", {
931
- className: "ink-error-title",
932
- children: "This document could not be opened."
933
- }), /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("code", {
934
- className: "ink-error-code",
935
- title: err.message,
936
- children: err.code
937
- }) })] })
938
- }) : state === "idle" ? /* @__PURE__ */ jsx("div", {
939
- className: "ink-status ink-empty",
940
- children: empty
941
- }) : null,
942
- /* @__PURE__ */ jsx("div", {
943
- ref: scroll,
944
- className: "ink-scroll",
945
- "data-ink-scroll": "",
946
- tabIndex: 0,
947
- role: "document",
948
- "aria-label": "Document"
949
- }),
950
- pages.map((p) => /* @__PURE__ */ jsx(InkPage, {
951
- page: p.page,
952
- host: p.host,
953
- mount: p.mount,
954
- error: p.error,
955
- customAnnotations: p.custom,
956
- renderAnnotation,
957
- renderPageError
958
- }, `${p.build}:${p.page}`))
959
- ]
960
- });
961
- }
962
- //#endregion
963
- //#region src/components/InkToolbar.tsx
964
- function InkToolbar({ label = "Document tools", asChild = false, className, children, ...rest }) {
965
- return /* @__PURE__ */ jsx(Toolbar.Root, {
966
- asChild,
967
- orientation: "horizontal",
968
- "aria-label": label,
969
- "data-ink-toolbar": "",
970
- className: cx("ink-toolbar", className),
971
- ...rest,
972
- children
1020
+ }, [draft]);
1021
+ const save = () => {
1022
+ draft.save(ctx.signatures).then((t) => {
1023
+ if (!t) return;
1024
+ if (select) {
1025
+ setOptions({ templateId: t.id });
1026
+ setTool("signature");
1027
+ }
1028
+ onSave?.(t);
1029
+ onClose();
1030
+ }, () => {});
1031
+ };
1032
+ const tabId = (tab) => `${uid}-tab-${tab}`;
1033
+ const panelId = (tab) => `${uid}-panel-${tab}`;
1034
+ const panel = (tab) => ({
1035
+ role: "tabpanel",
1036
+ id: panelId(tab),
1037
+ "aria-labelledby": tabId(tab),
1038
+ className: "ink-tab-panel",
1039
+ "data-key": tab,
1040
+ hidden: s.tab !== tab
973
1041
  });
1042
+ const shown = s.name.trim() || SIGNATURE_LABELS.namePlaceholder;
1043
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
1044
+ /* @__PURE__ */ jsx("div", {
1045
+ role: "tablist",
1046
+ "aria-label": SIGNATURE_LABELS.tabs,
1047
+ className: "ink-tabs",
1048
+ onKeyDown: (e) => handleTabKeys(e.nativeEvent, e.currentTarget),
1049
+ children: SIGNATURE_TABS.map((tab) => /* @__PURE__ */ jsx("button", {
1050
+ type: "button",
1051
+ role: "tab",
1052
+ id: tabId(tab),
1053
+ "aria-controls": panelId(tab),
1054
+ "aria-selected": s.tab === tab,
1055
+ tabIndex: s.tab === tab ? 0 : -1,
1056
+ className: "ink-tab",
1057
+ "data-key": tab,
1058
+ "data-state": s.tab === tab ? "active" : "inactive",
1059
+ onClick: () => draft.setTab(tab),
1060
+ children: SIGNATURE_LABELS[tab]
1061
+ }, tab))
1062
+ }),
1063
+ /* @__PURE__ */ jsxs("div", {
1064
+ ...panel("draw"),
1065
+ children: [/* @__PURE__ */ jsx("div", {
1066
+ ref: padHost,
1067
+ className: "ink-signature-pad-host",
1068
+ "data-ink-signature-pad-host": "",
1069
+ role: "img",
1070
+ "aria-label": SIGNATURE_LABELS.pad
1071
+ }), /* @__PURE__ */ jsx("div", {
1072
+ className: "ink-dialog-row",
1073
+ children: /* @__PURE__ */ jsx("button", {
1074
+ type: "button",
1075
+ className: "ink-button",
1076
+ "data-ink-signature-clear": "",
1077
+ disabled: s.padEmpty,
1078
+ onClick: () => draft.clearPad(),
1079
+ children: SIGNATURE_LABELS.clear
1080
+ })
1081
+ })]
1082
+ }),
1083
+ /* @__PURE__ */ jsxs("div", {
1084
+ ...panel("type"),
1085
+ children: [/* @__PURE__ */ jsxs("label", {
1086
+ className: "ink-field",
1087
+ children: [/* @__PURE__ */ jsx("span", { children: SIGNATURE_LABELS.name }), /* @__PURE__ */ jsx("input", {
1088
+ type: "text",
1089
+ className: "ink-input",
1090
+ "data-ink-signature-name": "",
1091
+ autoComplete: "name",
1092
+ placeholder: SIGNATURE_LABELS.namePlaceholder,
1093
+ value: s.name,
1094
+ onChange: (e) => draft.setName(e.target.value)
1095
+ })]
1096
+ }), /* @__PURE__ */ jsx("div", {
1097
+ role: "radiogroup",
1098
+ "aria-label": SIGNATURE_LABELS.fonts,
1099
+ className: "ink-signature-fonts",
1100
+ onKeyDown: (e) => handleRadioKeys(e.nativeEvent, e.currentTarget),
1101
+ children: draft.fonts.map((font) => /* @__PURE__ */ jsx("button", {
1102
+ type: "button",
1103
+ role: "radio",
1104
+ "aria-checked": s.font === font,
1105
+ "aria-label": font,
1106
+ tabIndex: s.font === font ? 0 : -1,
1107
+ className: "ink-signature-font",
1108
+ "data-key": font,
1109
+ "data-state": s.font === font ? "checked" : "unchecked",
1110
+ onClick: () => draft.setFont(font),
1111
+ children: /* @__PURE__ */ jsx("span", {
1112
+ className: "ink-signature-font-sample",
1113
+ style: { fontFamily: fontFamilyValue(font) },
1114
+ children: shown
1115
+ })
1116
+ }, font))
1117
+ })]
1118
+ }),
1119
+ /* @__PURE__ */ jsxs("div", {
1120
+ ...panel("image"),
1121
+ children: [
1122
+ /* @__PURE__ */ jsxs("label", {
1123
+ className: "ink-field",
1124
+ children: [/* @__PURE__ */ jsx("span", { children: SIGNATURE_LABELS.file }), /* @__PURE__ */ jsx("input", {
1125
+ type: "file",
1126
+ accept: "image/png,image/jpeg",
1127
+ className: "ink-input",
1128
+ "data-ink-signature-file": "",
1129
+ onChange: (e) => void draft.chooseFile(e.target.files?.[0])
1130
+ })]
1131
+ }),
1132
+ s.image?.kind === "image" ? /* @__PURE__ */ jsx("img", {
1133
+ className: "ink-signature-image",
1134
+ src: s.image.imagedata,
1135
+ alt: ""
1136
+ }) : null,
1137
+ s.imageError ? /* @__PURE__ */ jsx("p", {
1138
+ className: "ink-field-error",
1139
+ role: "alert",
1140
+ children: s.imageError
1141
+ }) : null
1142
+ ]
1143
+ }),
1144
+ s.saveError ? /* @__PURE__ */ jsx("p", {
1145
+ className: "ink-field-error",
1146
+ role: "alert",
1147
+ "data-ink-signature-save-error": "",
1148
+ children: s.saveError
1149
+ }) : null,
1150
+ /* @__PURE__ */ jsxs("div", {
1151
+ className: "ink-dialog-actions",
1152
+ children: [/* @__PURE__ */ jsx(Dialog.Close, {
1153
+ asChild: true,
1154
+ children: /* @__PURE__ */ jsx("button", {
1155
+ type: "button",
1156
+ className: "ink-button",
1157
+ "data-ink-signature-cancel": "",
1158
+ children: SIGNATURE_LABELS.cancel
1159
+ })
1160
+ }), /* @__PURE__ */ jsx("button", {
1161
+ type: "button",
1162
+ className: "ink-button",
1163
+ "data-variant": "primary",
1164
+ "data-ink-signature-save": "",
1165
+ disabled: !s.canSave,
1166
+ onClick: save,
1167
+ children: SIGNATURE_LABELS.save
1168
+ })]
1169
+ })
1170
+ ] });
974
1171
  }
975
1172
  //#endregion
976
1173
  //#region src/components/InkIcon.tsx
@@ -991,193 +1188,864 @@ function InkIcon({ icon, size = 18 }) {
991
1188
  });
992
1189
  }
993
1190
  //#endregion
994
- //#region src/components/InkToolButton.tsx
995
- const ICONS = {
996
- "select-text": TextCursor,
997
- select: SquareDashedMousePointer,
998
- highlight: Highlighter,
999
- "redact-area": Square,
1000
- sketch: PenTool,
1001
- draw: PenLine,
1002
- note: MessageSquare,
1003
- freetext: Type,
1004
- signature: Signature
1005
- };
1006
- function InkToolButton({ tool, label, asChild = false, className, children, onClick, ...rest }) {
1007
- const { hostTools, pluginHost } = useInkContext("InkToolButton");
1008
- const { tool: current, setTool } = useTool();
1009
- const active = current === tool;
1010
- /** A host or plugin tool by this id, for the name and icon core has none for. */
1011
- const own = [...hostTools ?? [], ...pluginHost?.tools ?? []].find((t) => t.id === tool);
1012
- const name = label ?? TOOL_LABELS[tool] ?? own?.label ?? tool;
1013
- const icon = ICONS[tool] ?? own?.icon;
1014
- return /* @__PURE__ */ jsx(Toolbar.Button, {
1015
- asChild,
1016
- className: cx("ink-tool-button", className),
1017
- "data-ink-tool-button": "",
1018
- "data-tool": tool,
1019
- "data-state": active ? "on" : "off",
1020
- "aria-pressed": active,
1021
- "aria-label": name,
1022
- title: name,
1023
- onClick: (e) => {
1024
- onClick?.(e);
1025
- if (!e.defaultPrevented) setTool(tool);
1026
- },
1027
- ...rest,
1028
- children: typeof children === "function" ? children({
1029
- active,
1030
- label: name
1031
- }) : children ?? (icon ? /* @__PURE__ */ jsx(InkIcon, { icon }) : null)
1032
- });
1033
- }
1034
- //#endregion
1035
- //#region src/components/InkCommand.tsx
1036
- function InkCommand({ id, asChild = false, children, disabled, onClick, ...rest }) {
1037
- const { run, enabled, busy, status, label, icon } = useCommand(id);
1038
- return /* @__PURE__ */ jsxs("div", {
1039
- className: "ink-command",
1040
- children: [/* @__PURE__ */ jsx(Toolbar.Button, {
1041
- asChild,
1042
- type: "button",
1043
- "data-command": id,
1044
- "aria-label": label || void 0,
1045
- title: label || void 0,
1046
- "aria-hidden": label ? void 0 : true,
1047
- "aria-busy": busy,
1048
- "aria-disabled": busy || void 0,
1049
- ...rest,
1050
- disabled: disabled === true || !enabled && !busy,
1051
- onClick: (e) => {
1052
- onClick?.(e);
1053
- if (!e.defaultPrevented) run();
1054
- },
1055
- children: typeof children === "function" ? children({
1056
- label,
1057
- busy
1058
- }) : children ?? (icon ? /* @__PURE__ */ jsx(InkIcon, { icon }) : label)
1059
- }), /* @__PURE__ */ jsx("span", {
1060
- className: "ink-command-status",
1061
- role: "status",
1062
- children: status
1063
- })]
1191
+ //#region src/components/SignatureMenuPanel.tsx
1192
+ const NO_TEMPLATES$2 = [];
1193
+ /** A saved signature drawn small: ink as filled outlines, an image as itself. */
1194
+ function SignaturePreview({ template }) {
1195
+ if (template.kind === "image") return /* @__PURE__ */ jsx("span", {
1196
+ className: "ink-signature-preview",
1197
+ "data-kind": "image",
1198
+ children: /* @__PURE__ */ jsx("img", {
1199
+ src: template.imagedata,
1200
+ alt: "",
1201
+ draggable: false
1202
+ })
1064
1203
  });
1065
- }
1066
- //#endregion
1067
- //#region src/components/InkToolbarSeparator.tsx
1068
- /**
1069
- * A divider between groups of controls in a horizontal toolbar. Not focusable. A plain div
1070
- * rather than Radix's separator, which reads as horizontal inside a horizontal toolbar.
1071
- */
1072
- function InkToolbarSeparator({ className, ...rest }) {
1073
- return /* @__PURE__ */ jsx("div", {
1074
- role: "separator",
1075
- "aria-orientation": "vertical",
1076
- className: cx("ink-toolbar-separator", className),
1077
- "data-ink-toolbar-separator": "",
1078
- ...rest
1204
+ const { viewBox, paths } = signaturePreview(template);
1205
+ return /* @__PURE__ */ jsx("span", {
1206
+ className: "ink-signature-preview",
1207
+ "data-kind": "ink",
1208
+ children: /* @__PURE__ */ jsx("svg", {
1209
+ viewBox,
1210
+ "aria-hidden": "true",
1211
+ children: paths.map((d, i) => /* @__PURE__ */ jsx("path", {
1212
+ d,
1213
+ fill: "currentColor"
1214
+ }, i))
1215
+ })
1079
1216
  });
1080
1217
  }
1081
- //#endregion
1082
- //#region src/components/InkUndo.tsx
1083
- function InkUndo({ asChild = false, className, children, disabled, onClick, ...rest }) {
1084
- const { canUndo, undo } = useHistory();
1085
- return /* @__PURE__ */ jsx(Toolbar.Button, {
1086
- asChild,
1087
- className: cx("ink-undo", className),
1088
- "data-ink-undo": "",
1089
- "aria-label": "Undo",
1090
- title: "Undo",
1091
- ...rest,
1092
- disabled: disabled === true || !canUndo,
1093
- onClick: (e) => {
1094
- onClick?.(e);
1095
- if (!e.defaultPrevented) undo();
1096
- },
1097
- children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Undo2 })
1098
- });
1218
+ function SignatureMenuPanel({ items, menu, onChoose, onCreate }) {
1219
+ const { signatures } = useInkContext("SignatureMenuPanel");
1220
+ const templates = useReadable(signatures.templates, NO_TEMPLATES$2);
1221
+ const removeError = useReadable(signatures.removeError, void 0);
1222
+ /** The row of a removed signature, until focus lands on what took its place. */
1223
+ const removedRow = useRef(null);
1224
+ useEffect(() => {
1225
+ const row = removedRow.current;
1226
+ const panel = menu.current;
1227
+ if (row === null || !panel) return;
1228
+ removedRow.current = null;
1229
+ const rows = panel.querySelectorAll(".ink-signature-item");
1230
+ (rows[Math.min(row, rows.length - 1)] ?? panel.querySelector("[data-ink-signature-create]"))?.focus({ preventScroll: true });
1231
+ }, [templates, menu]);
1232
+ const remove = (id, row) => {
1233
+ removedRow.current = row;
1234
+ signatures.remove(id).catch(() => {
1235
+ removedRow.current = null;
1236
+ });
1237
+ };
1238
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
1239
+ items.length > 0 ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
1240
+ role: "group",
1241
+ "aria-label": SIGNATURE_LABELS.saved,
1242
+ className: "ink-signature-list",
1243
+ "data-ink-menu-columns": 2,
1244
+ children: items.map((item, row) => [/* @__PURE__ */ jsx("button", {
1245
+ type: "button",
1246
+ role: "menuitemradio",
1247
+ "aria-checked": item.checked,
1248
+ "aria-label": item.label,
1249
+ tabIndex: -1,
1250
+ className: "ink-select-item ink-menu-item ink-signature-item",
1251
+ "data-ink-select-item": "",
1252
+ "data-key": item.id,
1253
+ "data-state": item.checked ? "checked" : "unchecked",
1254
+ onClick: () => onChoose(item.template),
1255
+ children: /* @__PURE__ */ jsx(SignaturePreview, { template: item.template })
1256
+ }, `${item.id}-choose`), /* @__PURE__ */ jsx("button", {
1257
+ type: "button",
1258
+ role: "menuitem",
1259
+ "aria-label": item.removeLabel,
1260
+ title: item.removeLabel,
1261
+ tabIndex: -1,
1262
+ className: "ink-menu-item ink-signature-remove",
1263
+ "data-ink-signature-remove": "",
1264
+ "data-key": item.id,
1265
+ onClick: () => remove(item.id, row),
1266
+ children: /* @__PURE__ */ jsx(InkIcon, {
1267
+ icon: Trash2,
1268
+ size: 16
1269
+ })
1270
+ }, `${item.id}-remove`)])
1271
+ }), /* @__PURE__ */ jsx("div", {
1272
+ role: "separator",
1273
+ className: "ink-select-separator"
1274
+ })] }) : null,
1275
+ /* @__PURE__ */ jsxs("button", {
1276
+ type: "button",
1277
+ role: "menuitem",
1278
+ tabIndex: -1,
1279
+ className: "ink-select-item ink-menu-item ink-signature-create",
1280
+ "data-ink-select-item": "",
1281
+ "data-ink-signature-create": "",
1282
+ onClick: onCreate,
1283
+ children: [/* @__PURE__ */ jsx(InkIcon, {
1284
+ icon: Plus,
1285
+ size: 16
1286
+ }), SIGNATURE_LABELS.create]
1287
+ }),
1288
+ removeError ? /* @__PURE__ */ jsx("p", {
1289
+ className: "ink-field-error ink-signature-remove-error",
1290
+ role: "alert",
1291
+ "data-ink-signature-remove-error": "",
1292
+ children: removeError
1293
+ }) : null
1294
+ ] });
1099
1295
  }
1100
1296
  //#endregion
1101
- //#region src/components/InkRedo.tsx
1102
- function InkRedo({ asChild = false, className, children, disabled, onClick, ...rest }) {
1103
- const { canRedo, redo } = useHistory();
1104
- return /* @__PURE__ */ jsx(Toolbar.Button, {
1105
- asChild,
1106
- className: cx("ink-redo", className),
1107
- "data-ink-redo": "",
1108
- "aria-label": "Redo",
1109
- title: "Redo",
1110
- ...rest,
1111
- disabled: disabled === true || !canRedo,
1112
- onClick: (e) => {
1113
- onClick?.(e);
1114
- if (!e.defaultPrevented) redo();
1297
+ //#region src/components/InkSignFieldPopover.tsx
1298
+ const NO_TEMPLATES$1 = [];
1299
+ /** The field's own button, which focus returns to. */
1300
+ const control = (req) => req?.anchor.querySelector("button.ink-form-control") ?? null;
1301
+ function InkSignFieldPopover({ request, onClose }) {
1302
+ const ctx = useInkContext("InkSignFieldPopover");
1303
+ const { signatures, reportError } = ctx;
1304
+ const templates = useReadable(signatures.templates, NO_TEMPLATES$1);
1305
+ const model = useMemo(() => signatureMenuModel(templates, {}), [templates]);
1306
+ /** The field the dialog signs once its signature is saved. */
1307
+ const [creatingFor, setCreatingFor] = useState(null);
1308
+ const menu = useRef(null);
1309
+ /**
1310
+ * The request the menu was last open for, kept to return focus once it closes. A request
1311
+ * that replaces the open one takes over, so focus returns to the field asked for last.
1312
+ */
1313
+ const last = useRef(null);
1314
+ /** Set while the menu closes for the dialog, which takes focus instead of the field. */
1315
+ const toDialog = useRef(false);
1316
+ const anchor = useMemo(() => ({ current: request?.anchor ?? null }), [request]);
1317
+ const doc = ctx.document;
1318
+ useEffect(() => {
1319
+ if (request) signatures.clearRemoveError();
1320
+ }, [request, signatures]);
1321
+ const close = useLatest(onClose);
1322
+ useEffect(() => {
1323
+ if (!request) return;
1324
+ last.current = request;
1325
+ return watchDetached(request.anchor, () => close.current());
1326
+ }, [request, close]);
1327
+ /** Focus goes back to the field's button, or to the scroll element once the field's box has gone. */
1328
+ const returnFocus = (req) => {
1329
+ const c = control(req);
1330
+ (c?.isConnected ? c : ctx.viewportEl.get())?.focus({ preventScroll: true });
1331
+ };
1332
+ /** Signs the request's field with `t`; a refusal (signed, read-only by now) goes to `onError`. */
1333
+ const sign = (req, t) => {
1334
+ if (!doc) return;
1335
+ try {
1336
+ signField(doc, req.field, req.widget, t);
1337
+ } catch (e) {
1338
+ if (!(e instanceof InkError)) throw e;
1339
+ reportError(e);
1340
+ }
1341
+ };
1342
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Popover.Root, {
1343
+ open: request !== null,
1344
+ onOpenChange: (next) => !next && onClose(),
1345
+ children: [/* @__PURE__ */ jsx(Popover.Anchor, { virtualRef: anchor }), /* @__PURE__ */ jsx(Popover.Portal, { children: request ? /* @__PURE__ */ jsx(Popover.Content, {
1346
+ ref: menu,
1347
+ role: "menu",
1348
+ "aria-label": SIGNATURE_LABELS.menu,
1349
+ className: "ink-select-content ink-menu-content",
1350
+ "data-ink-select-content": "",
1351
+ "data-ink-menu": "signature",
1352
+ "data-ink-sign-field-menu": request.field,
1353
+ side: "bottom",
1354
+ align: "start",
1355
+ sideOffset: 4,
1356
+ collisionPadding: 8,
1357
+ onOpenAutoFocus: (e) => {
1358
+ e.preventDefault();
1359
+ focusInitialMenuItem(e.currentTarget);
1360
+ },
1361
+ onCloseAutoFocus: (e) => {
1362
+ e.preventDefault();
1363
+ if (!toDialog.current) returnFocus(last.current);
1364
+ toDialog.current = false;
1365
+ },
1366
+ onInteractOutside: (e) => {
1367
+ const target = e.detail.originalEvent.target;
1368
+ if (target instanceof Node && request.anchor.contains(target)) e.preventDefault();
1369
+ },
1370
+ onKeyDown: (e) => handleMenuKeys(e.nativeEvent, e.currentTarget),
1371
+ children: /* @__PURE__ */ jsx(SignatureMenuPanel, {
1372
+ items: model.items,
1373
+ menu,
1374
+ onChoose: (t) => {
1375
+ onClose();
1376
+ sign(request, t);
1377
+ },
1378
+ onCreate: () => {
1379
+ toDialog.current = true;
1380
+ setCreatingFor(request);
1381
+ onClose();
1382
+ }
1383
+ })
1384
+ }) : null })]
1385
+ }), /* @__PURE__ */ jsx(InkSignatureDialog, {
1386
+ open: creatingFor !== null,
1387
+ onOpenChange: (next) => {
1388
+ if (!next) setCreatingFor(null);
1115
1389
  },
1116
- children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Redo2 })
1117
- });
1118
- }
1119
- //#endregion
1120
- //#region src/components/InkZoomIn.tsx
1121
- function InkZoomIn({ asChild = false, className, children, onClick, ...rest }) {
1122
- const { zoomIn } = useZoom();
1123
- return /* @__PURE__ */ jsx(Toolbar.Button, {
1124
- asChild,
1125
- className: cx("ink-zoom-in", className),
1126
- "data-ink-zoom-in": "",
1127
- "aria-label": "Zoom in",
1128
- title: "Zoom in",
1129
- ...rest,
1130
- onClick: (e) => {
1131
- onClick?.(e);
1132
- if (!e.defaultPrevented) zoomIn();
1390
+ select: false,
1391
+ onSave: (t) => {
1392
+ if (creatingFor) sign(creatingFor, t);
1133
1393
  },
1134
- children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ZoomIn })
1135
- });
1394
+ onCloseAutoFocus: (e) => {
1395
+ e.preventDefault();
1396
+ returnFocus(last.current);
1397
+ }
1398
+ })] });
1136
1399
  }
1137
1400
  //#endregion
1138
- //#region src/components/InkZoomOut.tsx
1139
- function InkZoomOut({ asChild = false, className, children, onClick, ...rest }) {
1140
- const { zoomOut } = useZoom();
1141
- return /* @__PURE__ */ jsx(Toolbar.Button, {
1142
- asChild,
1143
- className: cx("ink-zoom-out", className),
1144
- "data-ink-zoom-out": "",
1145
- "aria-label": "Zoom out",
1146
- title: "Zoom out",
1147
- ...rest,
1148
- onClick: (e) => {
1149
- onClick?.(e);
1150
- if (!e.defaultPrevented) zoomOut();
1151
- },
1152
- children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ZoomOut })
1153
- });
1401
+ //#region src/components/InkViewport.tsx
1402
+ /** A key per document, so the field menu and its dialog start afresh for each one. */
1403
+ const documentKeys = /* @__PURE__ */ new WeakMap();
1404
+ let documentKeyCount = 0;
1405
+ function documentKey(doc) {
1406
+ if (!doc) return 0;
1407
+ let key = documentKeys.get(doc);
1408
+ if (key === void 0) {
1409
+ key = ++documentKeyCount;
1410
+ documentKeys.set(doc, key);
1411
+ }
1412
+ return key;
1154
1413
  }
1155
- //#endregion
1156
- //#region src/components/InkZoomSelect.tsx
1157
- const CUSTOM = "custom:";
1158
- const level = (z) => ZOOM_LEVELS.find((l) => Math.abs(l - z) < .001);
1159
- const percent = (z) => `${Math.round(z * 100)}%`;
1160
- const FITS = [{
1161
- value: "fit-width",
1162
- label: "Fit width"
1163
- }, {
1164
- value: "fit-page",
1165
- label: "Fit page"
1166
- }];
1167
- function Item$1({ value, label }) {
1168
- return /* @__PURE__ */ jsxs(Select.Item, {
1169
- value,
1170
- className: "ink-select-item",
1171
- "data-ink-select-item": "",
1172
- children: [/* @__PURE__ */ jsx(Select.ItemText, { children: label }), /* @__PURE__ */ jsx(Select.ItemIndicator, {
1173
- className: "ink-select-indicator",
1174
- children: /* @__PURE__ */ jsx(InkIcon, {
1175
- icon: Check,
1176
- size: 16
1177
- })
1178
- })]
1414
+ function InkViewport({ mode = "continuous", rotation = 0, gap = 16, buffer = 2, loading, empty, error, renderAnnotation, renderPageError, className, ...rest }) {
1415
+ const ctx = useInkContext("InkViewport");
1416
+ const scroll = useRef(null);
1417
+ const custom = !!renderAnnotation;
1418
+ const latest = useLatest({
1419
+ ctx,
1420
+ mode,
1421
+ rotation,
1422
+ gap,
1423
+ buffer,
1424
+ custom
1179
1425
  });
1180
- }
1426
+ const engine = useRef(null);
1427
+ /** Page hosts, mounts and errors the engine reported; InkPage renders portals for them. */
1428
+ const [pages, setPages] = useState([]);
1429
+ /**
1430
+ * Numbered before each build starts, so the pages a build mounts carry its own number. A
1431
+ * rebuild gives each page a new host and mount, so pages from an earlier build must remount,
1432
+ * not be reused.
1433
+ */
1434
+ const buildCount = useRef(0);
1435
+ /** The construction options the engine was last built with, so a rebuild runs only on a change. */
1436
+ const builtWith = useRef(null);
1437
+ /** The zoom and page last reported to the host. */
1438
+ const lastReportedZoom = useRef(void 0);
1439
+ const lastReportedPage = useRef(void 0);
1440
+ /** The signature field whose menu is open. */
1441
+ const [signRequest, setSignRequest] = useState(null);
1442
+ /** The form script's message on show, and how many have been shown. */
1443
+ const [alert, setAlert] = useState(null);
1444
+ useEffect(() => {
1445
+ const { ctx: c, mode: m, rotation: r, gap: g, buffer: b, custom: cu } = latest.current;
1446
+ const doc = c.document;
1447
+ const el = scroll.current;
1448
+ lastReportedZoom.current = void 0;
1449
+ lastReportedPage.current = void 0;
1450
+ if (!doc || !el || c.status.state !== "ready") return;
1451
+ let first = true;
1452
+ let stops = [];
1453
+ buildCount.current += 1;
1454
+ builtWith.current = {
1455
+ gap: g,
1456
+ buffer: b,
1457
+ custom: cu
1458
+ };
1459
+ const e = mountViewer({
1460
+ container: el,
1461
+ document: doc,
1462
+ zoom: c.zoomModel ?? "fit-width",
1463
+ ...c.pageModel === void 0 ? {} : { page: c.pageModel },
1464
+ mode: m,
1465
+ rotation: r,
1466
+ gap: g,
1467
+ buffer: b,
1468
+ tool: c.tool,
1469
+ ...c.hostTools ? { tools: c.hostTools } : {},
1470
+ ...c.pluginHost ? { plugins: c.pluginHost } : {},
1471
+ ...c.search ? { search: c.search } : {},
1472
+ decorateAnnotation: (a) => latest.current.ctx.decorateAnnotation(a),
1473
+ ...c.toolStyle ? { toolStyle: c.toolStyle } : {},
1474
+ toolHistory: { batch: (fn) => latest.current.ctx.history.batch(fn) },
1475
+ toolOptions: c.toolOptions,
1476
+ fonts: [...c.fonts],
1477
+ signatureLibrary: () => latest.current.ctx.signatures,
1478
+ customAnnotations: cu,
1479
+ ...c.formHighlight === void 0 ? {} : { formHighlight: c.formHighlight },
1480
+ onSignField: (req) => setSignRequest((open) => open?.widget === req.widget ? null : req),
1481
+ onFormAction: (a) => latest.current.ctx.reportFormAction(a),
1482
+ onFormAlert: (message) => setAlert((prev) => ({
1483
+ message,
1484
+ shown: (prev?.shown ?? 0) + 1
1485
+ })),
1486
+ onActivate: (id) => latest.current.ctx.setActivated(id),
1487
+ onBuild: (vp, tc) => {
1488
+ latest.current.ctx.setViewport(vp, tc);
1489
+ if (first) {
1490
+ first = false;
1491
+ lastReportedZoom.current = vp.zoom.get();
1492
+ lastReportedPage.current = 0;
1493
+ }
1494
+ for (const stop of stops) stop();
1495
+ const reportZoom = (z) => {
1496
+ if (z === lastReportedZoom.current) return;
1497
+ lastReportedZoom.current = z;
1498
+ latest.current.ctx.reportZoom(z);
1499
+ };
1500
+ const reportPage = (p) => {
1501
+ if (p === lastReportedPage.current) return;
1502
+ lastReportedPage.current = p;
1503
+ latest.current.ctx.reportPage(p);
1504
+ };
1505
+ const followTool = (id) => {
1506
+ if (id !== latest.current.ctx.tool) latest.current.ctx.setTool(id);
1507
+ };
1508
+ stops = [
1509
+ vp.zoom.subscribe(reportZoom),
1510
+ vp.currentPage.subscribe(reportPage),
1511
+ tc.active.subscribe(followTool)
1512
+ ];
1513
+ reportZoom(vp.zoom.get());
1514
+ reportPage(vp.currentPage.get());
1515
+ },
1516
+ onPageMount: (page, mount, host) => {
1517
+ const build = buildCount.current;
1518
+ const custom = builtWith.current?.custom ?? false;
1519
+ setPages((prev) => [...prev, {
1520
+ build,
1521
+ page,
1522
+ host,
1523
+ mount,
1524
+ error: null,
1525
+ custom
1526
+ }]);
1527
+ },
1528
+ onPageUnmount: (page) => setPages((prev) => prev.filter((p) => p.page !== page)),
1529
+ onPageError: (page, err) => setPages((prev) => prev.map((p) => p.page === page ? {
1530
+ ...p,
1531
+ error: err
1532
+ } : p))
1533
+ });
1534
+ engine.current = e;
1535
+ return () => {
1536
+ for (const stop of stops) stop();
1537
+ e.destroy();
1538
+ engine.current = null;
1539
+ builtWith.current = null;
1540
+ c.setViewport(null, null);
1541
+ setPages((prev) => prev.length === 0 ? prev : []);
1542
+ setSignRequest(null);
1543
+ setAlert(null);
1544
+ };
1545
+ }, [
1546
+ ctx.document,
1547
+ ctx.status.state,
1548
+ ctx.pluginHost,
1549
+ ctx.search,
1550
+ latest
1551
+ ]);
1552
+ const { viewportEl } = ctx;
1553
+ useLayoutEffect(() => {
1554
+ const el = scroll.current;
1555
+ viewportEl.set(el);
1556
+ return () => {
1557
+ if (viewportEl.get() === el) viewportEl.set(null);
1558
+ };
1559
+ }, [viewportEl]);
1560
+ const { onDecorateAnnotationChange } = ctx;
1561
+ useEffect(() => onDecorateAnnotationChange(() => engine.current?.redecorate()), [onDecorateAnnotationChange]);
1562
+ useEffect(() => {
1563
+ const built = builtWith.current;
1564
+ if (!engine.current || !built) return;
1565
+ if (built.gap === gap && built.buffer === buffer && built.custom === custom) return;
1566
+ buildCount.current += 1;
1567
+ builtWith.current = {
1568
+ gap,
1569
+ buffer,
1570
+ custom
1571
+ };
1572
+ engine.current.rebuild({
1573
+ gap,
1574
+ buffer,
1575
+ customAnnotations: custom
1576
+ });
1577
+ }, [
1578
+ gap,
1579
+ buffer,
1580
+ custom
1581
+ ]);
1582
+ useEffect(() => {
1583
+ const vp = engine.current?.viewport;
1584
+ const z = ctx.zoomModel;
1585
+ if (vp && z !== void 0 && z !== vp.zoom.get()) vp.setZoom(z);
1586
+ }, [ctx.zoomModel]);
1587
+ useEffect(() => {
1588
+ const vp = engine.current?.viewport;
1589
+ const p = ctx.pageModel;
1590
+ if (!vp || p === void 0 || p === lastReportedPage.current) return;
1591
+ if (p !== vp.currentPage.get()) vp.scrollToPage(p);
1592
+ }, [ctx.pageModel]);
1593
+ useEffect(() => {
1594
+ const tc = engine.current?.tools;
1595
+ if (tc && tc.active.get() !== ctx.tool) tc.set(ctx.tool);
1596
+ }, [ctx.tool]);
1597
+ useEffect(() => {
1598
+ engine.current?.setFormHighlight(ctx.formHighlight ?? true);
1599
+ }, [ctx.formHighlight]);
1600
+ useEffect(() => {
1601
+ const vp = engine.current?.viewport;
1602
+ if (vp && vp.mode.get() !== mode) vp.setMode(mode);
1603
+ }, [mode]);
1604
+ useEffect(() => {
1605
+ const vp = engine.current?.viewport;
1606
+ if (vp && vp.rotation.get() !== rotation) vp.setRotation(rotation);
1607
+ }, [rotation]);
1608
+ const state = ctx.status.state;
1609
+ const err = ctx.status.state === "error" ? ctx.status.error : null;
1610
+ return /* @__PURE__ */ jsxs("div", {
1611
+ ...rest,
1612
+ className: cx("ink-viewport-root", className),
1613
+ "data-ink-viewport-root": "",
1614
+ "data-state": state,
1615
+ children: [
1616
+ state === "loading" ? /* @__PURE__ */ jsx("div", {
1617
+ className: "ink-status",
1618
+ role: "status",
1619
+ "aria-live": "polite",
1620
+ children: loading === void 0 ? "Loading document…" : loading
1621
+ }) : err ? /* @__PURE__ */ jsx("div", {
1622
+ className: "ink-status ink-error",
1623
+ role: "alert",
1624
+ children: typeof error === "function" ? error({ error: err }) : error !== void 0 ? error : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", {
1625
+ className: "ink-error-title",
1626
+ children: "This document could not be opened."
1627
+ }), /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("code", {
1628
+ className: "ink-error-code",
1629
+ title: err.message,
1630
+ children: err.code
1631
+ }) })] })
1632
+ }) : state === "idle" ? /* @__PURE__ */ jsx("div", {
1633
+ className: "ink-status ink-empty",
1634
+ children: empty
1635
+ }) : null,
1636
+ /* @__PURE__ */ jsx("div", {
1637
+ ref: scroll,
1638
+ className: "ink-scroll",
1639
+ "data-ink-scroll": "",
1640
+ tabIndex: 0,
1641
+ role: "document",
1642
+ "aria-label": "Document"
1643
+ }),
1644
+ pages.map((p) => /* @__PURE__ */ jsx(InkPage, {
1645
+ page: p.page,
1646
+ host: p.host,
1647
+ mount: p.mount,
1648
+ error: p.error,
1649
+ customAnnotations: p.custom,
1650
+ renderAnnotation,
1651
+ renderPageError
1652
+ }, `${p.build}:${p.page}`)),
1653
+ /* @__PURE__ */ jsx(InkSignFieldPopover, {
1654
+ request: signRequest,
1655
+ onClose: () => setSignRequest(null)
1656
+ }, documentKey(ctx.document)),
1657
+ /* @__PURE__ */ jsx(InkFormAlert, {
1658
+ message: alert?.message ?? null,
1659
+ shown: alert?.shown ?? 0,
1660
+ fallback: () => scroll.current,
1661
+ onClose: () => setAlert(null)
1662
+ })
1663
+ ]
1664
+ });
1665
+ }
1666
+ //#endregion
1667
+ //#region src/components/InkToolbar.tsx
1668
+ function InkToolbar({ label = "Document tools", asChild = false, className, children, ...rest }) {
1669
+ return /* @__PURE__ */ jsx(Toolbar.Root, {
1670
+ asChild,
1671
+ orientation: "horizontal",
1672
+ "aria-label": label,
1673
+ "data-ink-toolbar": "",
1674
+ className: cx("ink-toolbar", className),
1675
+ ...rest,
1676
+ children
1677
+ });
1678
+ }
1679
+ //#endregion
1680
+ //#region src/components/InkToolButton.tsx
1681
+ const ICONS = {
1682
+ "select-text": TextCursor,
1683
+ select: SquareDashedMousePointer,
1684
+ highlight: Highlighter,
1685
+ "redact-area": Square,
1686
+ sketch: PenTool,
1687
+ draw: PenLine,
1688
+ note: MessageSquare,
1689
+ freetext: Type,
1690
+ signature: Signature
1691
+ };
1692
+ function InkToolButton({ tool, label, asChild = false, className, children, onClick, ...rest }) {
1693
+ const { hostTools, pluginHost } = useInkContext("InkToolButton");
1694
+ const { tool: current, setTool } = useTool();
1695
+ const active = current === tool;
1696
+ /** A host or plugin tool by this id, for the name and icon core has none for. */
1697
+ const own = [...hostTools ?? [], ...pluginHost?.tools ?? []].find((t) => t.id === tool);
1698
+ const name = label ?? TOOL_LABELS[tool] ?? own?.label ?? tool;
1699
+ const icon = ICONS[tool] ?? own?.icon;
1700
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
1701
+ asChild,
1702
+ className: cx("ink-tool-button", className),
1703
+ "data-ink-tool-button": "",
1704
+ "data-tool": tool,
1705
+ "data-state": active ? "on" : "off",
1706
+ "aria-pressed": active,
1707
+ "aria-label": name,
1708
+ title: name,
1709
+ onClick: (e) => {
1710
+ onClick?.(e);
1711
+ if (!e.defaultPrevented) setTool(tool);
1712
+ },
1713
+ ...rest,
1714
+ children: typeof children === "function" ? children({
1715
+ active,
1716
+ label: name
1717
+ }) : children ?? (icon ? /* @__PURE__ */ jsx(InkIcon, { icon }) : null)
1718
+ });
1719
+ }
1720
+ //#endregion
1721
+ //#region src/components/InkCommand.tsx
1722
+ function InkCommand({ id, asChild = false, children, disabled, onClick, ...rest }) {
1723
+ const { run, enabled, busy, status, label, icon } = useCommand(id);
1724
+ return /* @__PURE__ */ jsxs("div", {
1725
+ className: "ink-command",
1726
+ children: [/* @__PURE__ */ jsx(Toolbar.Button, {
1727
+ asChild,
1728
+ type: "button",
1729
+ "data-command": id,
1730
+ "aria-label": label || void 0,
1731
+ title: label || void 0,
1732
+ "aria-hidden": label ? void 0 : true,
1733
+ "aria-busy": busy,
1734
+ "aria-disabled": busy || void 0,
1735
+ ...rest,
1736
+ disabled: disabled === true || !enabled && !busy,
1737
+ onClick: (e) => {
1738
+ onClick?.(e);
1739
+ if (!e.defaultPrevented) run();
1740
+ },
1741
+ children: typeof children === "function" ? children({
1742
+ label,
1743
+ busy
1744
+ }) : children ?? (icon ? /* @__PURE__ */ jsx(InkIcon, { icon }) : label)
1745
+ }), /* @__PURE__ */ jsx("span", {
1746
+ className: "ink-command-status",
1747
+ role: "status",
1748
+ children: status
1749
+ })]
1750
+ });
1751
+ }
1752
+ //#endregion
1753
+ //#region src/components/InkToolbarSeparator.tsx
1754
+ /**
1755
+ * A divider between groups of controls in a horizontal toolbar. Not focusable. A plain div
1756
+ * rather than Radix's separator, which reads as horizontal inside a horizontal toolbar.
1757
+ */
1758
+ function InkToolbarSeparator({ className, ...rest }) {
1759
+ return /* @__PURE__ */ jsx("div", {
1760
+ role: "separator",
1761
+ "aria-orientation": "vertical",
1762
+ className: cx("ink-toolbar-separator", className),
1763
+ "data-ink-toolbar-separator": "",
1764
+ ...rest
1765
+ });
1766
+ }
1767
+ //#endregion
1768
+ //#region src/components/InkUndo.tsx
1769
+ function InkUndo({ asChild = false, className, children, disabled, onClick, ...rest }) {
1770
+ const { canUndo, undo } = useHistory();
1771
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
1772
+ asChild,
1773
+ className: cx("ink-undo", className),
1774
+ "data-ink-undo": "",
1775
+ "aria-label": "Undo",
1776
+ title: "Undo",
1777
+ ...rest,
1778
+ disabled: disabled === true || !canUndo,
1779
+ onClick: (e) => {
1780
+ onClick?.(e);
1781
+ if (!e.defaultPrevented) undo();
1782
+ },
1783
+ children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Undo2 })
1784
+ });
1785
+ }
1786
+ //#endregion
1787
+ //#region src/components/InkRedo.tsx
1788
+ function InkRedo({ asChild = false, className, children, disabled, onClick, ...rest }) {
1789
+ const { canRedo, redo } = useHistory();
1790
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
1791
+ asChild,
1792
+ className: cx("ink-redo", className),
1793
+ "data-ink-redo": "",
1794
+ "aria-label": "Redo",
1795
+ title: "Redo",
1796
+ ...rest,
1797
+ disabled: disabled === true || !canRedo,
1798
+ onClick: (e) => {
1799
+ onClick?.(e);
1800
+ if (!e.defaultPrevented) redo();
1801
+ },
1802
+ children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Redo2 })
1803
+ });
1804
+ }
1805
+ //#endregion
1806
+ //#region src/components/InkSearchButton.tsx
1807
+ /**
1808
+ * Opens the find bar; pressed while it is open. Pressing it again focuses the field. Place it
1809
+ * in a toolbar next to `InkFindBar`, which it needs for the field to focus. Disabled until a
1810
+ * document is open.
1811
+ */
1812
+ function InkSearchButton({ asChild = false, className, children, disabled, onClick, ...rest }) {
1813
+ const { rootEl } = useInkContext("InkSearchButton");
1814
+ const { state, open } = useSearch();
1815
+ const { isReady } = useDocument();
1816
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
1817
+ ...rest,
1818
+ asChild,
1819
+ className: cx("ink-search-button", className),
1820
+ "data-ink-search-button": "",
1821
+ "aria-label": "Find",
1822
+ title: "Find",
1823
+ "aria-pressed": state.open,
1824
+ "data-state": state.open ? "on" : "off",
1825
+ disabled: !isReady || !!disabled,
1826
+ onClick: (e) => {
1827
+ onClick?.(e);
1828
+ if (e.defaultPrevented) return;
1829
+ flushSync(() => open());
1830
+ const field = rootEl?.querySelector("[data-ink-find-bar] input");
1831
+ field?.focus();
1832
+ field?.select();
1833
+ },
1834
+ children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Search })
1835
+ });
1836
+ }
1837
+ //#endregion
1838
+ //#region src/components/InkFindBar.tsx
1839
+ /**
1840
+ * The find bar: a field, the match count, previous and next, a match-case toggle and close,
1841
+ * a view of the root's search state. Place it anywhere inside `InkRoot`: it portals itself
1842
+ * into `[data-ink-viewport-root]`, the positioned ancestor the theme floats it from over the
1843
+ * top-right of the pages (as `InkThreadPopover` anchors to the root's overlay rather than
1844
+ * where it is written). It renders nothing until `InkViewport` has mounted, and is hidden
1845
+ * while the search is closed. While it is mounted it registers with the search controller,
1846
+ * which is what lets Ctrl/Cmd+F open it.
1847
+ *
1848
+ * Enter in the field steps to the next match, Shift+Enter to the previous, Escape closes.
1849
+ * Closing returns focus to where it was before the bar opened, else to the document.
1850
+ */
1851
+ function InkFindBar({ className, ...rest }) {
1852
+ const ctx = useInkContext("InkFindBar");
1853
+ const { state, setQuery, setMatchCase, next, previous, close } = useSearch();
1854
+ const scroll = useReadable(ctx.viewportEl, null);
1855
+ /** The viewport root: the scroll element's parent, once InkViewport has mounted. */
1856
+ const target = scroll?.parentElement ?? null;
1857
+ const bar = useRef(null);
1858
+ const field = useRef(null);
1859
+ const live = useRef(null);
1860
+ /** Where focus was when the bar opened, and whether it was open on the last pass. */
1861
+ const before = useRef(null);
1862
+ const wasOpen = useRef(false);
1863
+ useLayoutEffect(() => {
1864
+ const el = bar.current;
1865
+ const input = field.current;
1866
+ if (!el || !input) return;
1867
+ if (state.open !== wasOpen.current) {
1868
+ wasOpen.current = state.open;
1869
+ const doc = el.ownerDocument;
1870
+ if (state.open) {
1871
+ const active = doc.activeElement;
1872
+ before.current = active instanceof HTMLElement && active !== doc.body && !el.contains(active) ? active : null;
1873
+ input.focus();
1874
+ input.select();
1875
+ } else {
1876
+ if (el.contains(doc.activeElement) || doc.activeElement === doc.body) (before.current?.isConnected && !el.contains(before.current) ? before.current : scroll)?.focus({ preventScroll: true });
1877
+ before.current = null;
1878
+ }
1879
+ }
1880
+ if (input.value !== state.query) {
1881
+ input.value = state.query;
1882
+ if (el.ownerDocument.activeElement === input) input.select();
1883
+ }
1884
+ }, [
1885
+ state.open,
1886
+ state.query,
1887
+ scroll
1888
+ ]);
1889
+ const { search } = ctx;
1890
+ useEffect(() => {
1891
+ if (!search || !target) return;
1892
+ return search.attachBar();
1893
+ }, [search, target]);
1894
+ useEffect(() => {
1895
+ const el = live.current;
1896
+ if (!search || !el) return;
1897
+ const announce = findAnnouncer();
1898
+ const say = (s) => {
1899
+ const text = announce(s);
1900
+ if (el.textContent !== text) el.textContent = text;
1901
+ };
1902
+ say(search.state.get());
1903
+ return search.state.subscribe(say);
1904
+ }, [search, target]);
1905
+ useEffect(() => {
1906
+ const el = bar.current;
1907
+ if (!search || !el) return;
1908
+ const onKeyDown = (e) => {
1909
+ if (e.key !== "Escape" || e.defaultPrevented || e.isComposing) return;
1910
+ e.preventDefault();
1911
+ e.stopPropagation();
1912
+ search.close();
1913
+ };
1914
+ el.addEventListener("keydown", onKeyDown);
1915
+ return () => el.removeEventListener("keydown", onKeyDown);
1916
+ }, [search, target]);
1917
+ if (!target) return null;
1918
+ return createPortal(/* @__PURE__ */ jsxs("div", {
1919
+ ...rest,
1920
+ ref: bar,
1921
+ className: cx("ink-find-bar", className),
1922
+ "data-ink-find-bar": "",
1923
+ role: "search",
1924
+ hidden: !state.open,
1925
+ children: [
1926
+ /* @__PURE__ */ jsx("input", {
1927
+ ref: field,
1928
+ type: "text",
1929
+ "aria-label": "Find in document",
1930
+ autoComplete: "off",
1931
+ spellCheck: false,
1932
+ onInput: (e) => setQuery(e.currentTarget.value),
1933
+ onKeyDown: (e) => {
1934
+ if (e.key !== "Enter" || e.nativeEvent.isComposing) return;
1935
+ e.preventDefault();
1936
+ if (e.shiftKey) previous();
1937
+ else next();
1938
+ }
1939
+ }),
1940
+ /* @__PURE__ */ jsx("span", {
1941
+ className: "ink-find-count",
1942
+ "data-ink-find-count": "",
1943
+ children: findCountText(state)
1944
+ }),
1945
+ /* @__PURE__ */ jsx("button", {
1946
+ type: "button",
1947
+ "data-ink-find-previous": "",
1948
+ "aria-label": "Previous match",
1949
+ title: "Previous match",
1950
+ onClick: () => previous(),
1951
+ children: /* @__PURE__ */ jsx(InkIcon, { icon: ChevronUp })
1952
+ }),
1953
+ /* @__PURE__ */ jsx("button", {
1954
+ type: "button",
1955
+ "data-ink-find-next": "",
1956
+ "aria-label": "Next match",
1957
+ title: "Next match",
1958
+ onClick: () => next(),
1959
+ children: /* @__PURE__ */ jsx(InkIcon, { icon: ChevronDown })
1960
+ }),
1961
+ /* @__PURE__ */ jsx("button", {
1962
+ type: "button",
1963
+ "data-ink-find-match-case": "",
1964
+ "aria-label": "Match case",
1965
+ title: "Match case",
1966
+ "aria-pressed": state.matchCase,
1967
+ onClick: () => setMatchCase(!state.matchCase),
1968
+ children: /* @__PURE__ */ jsx(InkIcon, { icon: CaseSensitive })
1969
+ }),
1970
+ /* @__PURE__ */ jsx("button", {
1971
+ type: "button",
1972
+ "data-ink-find-close": "",
1973
+ "aria-label": "Close find",
1974
+ title: "Close find",
1975
+ onClick: () => close(),
1976
+ children: /* @__PURE__ */ jsx(InkIcon, { icon: X })
1977
+ }),
1978
+ /* @__PURE__ */ jsx("div", {
1979
+ ref: live,
1980
+ className: "ink-visually-hidden",
1981
+ "data-ink-find-status": "",
1982
+ "aria-live": "polite"
1983
+ })
1984
+ ]
1985
+ }), target);
1986
+ }
1987
+ //#endregion
1988
+ //#region src/components/InkZoomIn.tsx
1989
+ function InkZoomIn({ asChild = false, className, children, onClick, ...rest }) {
1990
+ const { zoomIn } = useZoom();
1991
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
1992
+ asChild,
1993
+ className: cx("ink-zoom-in", className),
1994
+ "data-ink-zoom-in": "",
1995
+ "aria-label": "Zoom in",
1996
+ title: "Zoom in",
1997
+ ...rest,
1998
+ onClick: (e) => {
1999
+ onClick?.(e);
2000
+ if (!e.defaultPrevented) zoomIn();
2001
+ },
2002
+ children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ZoomIn })
2003
+ });
2004
+ }
2005
+ //#endregion
2006
+ //#region src/components/InkZoomOut.tsx
2007
+ function InkZoomOut({ asChild = false, className, children, onClick, ...rest }) {
2008
+ const { zoomOut } = useZoom();
2009
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
2010
+ asChild,
2011
+ className: cx("ink-zoom-out", className),
2012
+ "data-ink-zoom-out": "",
2013
+ "aria-label": "Zoom out",
2014
+ title: "Zoom out",
2015
+ ...rest,
2016
+ onClick: (e) => {
2017
+ onClick?.(e);
2018
+ if (!e.defaultPrevented) zoomOut();
2019
+ },
2020
+ children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ZoomOut })
2021
+ });
2022
+ }
2023
+ //#endregion
2024
+ //#region src/components/InkZoomSelect.tsx
2025
+ const CUSTOM = "custom:";
2026
+ const level = (z) => ZOOM_LEVELS.find((l) => Math.abs(l - z) < .001);
2027
+ const percent = (z) => `${Math.round(z * 100)}%`;
2028
+ const FITS = [{
2029
+ value: "fit-width",
2030
+ label: "Fit width"
2031
+ }, {
2032
+ value: "fit-page",
2033
+ label: "Fit page"
2034
+ }];
2035
+ function Item$1({ value, label }) {
2036
+ return /* @__PURE__ */ jsxs(Select.Item, {
2037
+ value,
2038
+ className: "ink-select-item",
2039
+ "data-ink-select-item": "",
2040
+ children: [/* @__PURE__ */ jsx(Select.ItemText, { children: label }), /* @__PURE__ */ jsx(Select.ItemIndicator, {
2041
+ className: "ink-select-indicator",
2042
+ children: /* @__PURE__ */ jsx(InkIcon, {
2043
+ icon: Check,
2044
+ size: 16
2045
+ })
2046
+ })]
2047
+ });
2048
+ }
1181
2049
  function InkZoomSelect({ className, ...rest }) {
1182
2050
  const { zoom, scale, setZoom } = useZoom();
1183
2051
  /** A numeric zoom that is not one of the levels (zoom in and out multiply). */
@@ -1299,524 +2167,291 @@ function InkPageCount({ className, ...rest }) {
1299
2167
  }
1300
2168
  //#endregion
1301
2169
  //#region src/components/InkPrevPage.tsx
1302
- function InkPrevPage({ asChild = false, className, children, disabled, onClick, ...rest }) {
1303
- const { current, previous } = usePage();
1304
- return /* @__PURE__ */ jsx(Toolbar.Button, {
1305
- asChild,
1306
- className: cx("ink-prev-page", className),
1307
- "data-ink-prev-page": "",
1308
- "aria-label": "Previous page",
1309
- title: "Previous page",
1310
- disabled: disabled === true || current <= 0,
1311
- ...rest,
1312
- onClick: (e) => {
1313
- onClick?.(e);
1314
- if (!e.defaultPrevented) previous();
1315
- },
1316
- children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ChevronLeft })
1317
- });
1318
- }
1319
- //#endregion
1320
- //#region src/components/InkNextPage.tsx
1321
- function InkNextPage({ asChild = false, className, children, disabled, onClick, ...rest }) {
1322
- const { current, count, next } = usePage();
2170
+ function InkPrevPage({ asChild = false, className, children, disabled, onClick, ...rest }) {
2171
+ const { current, previous } = usePage();
1323
2172
  return /* @__PURE__ */ jsx(Toolbar.Button, {
1324
2173
  asChild,
1325
- className: cx("ink-next-page", className),
1326
- "data-ink-next-page": "",
1327
- "aria-label": "Next page",
1328
- title: "Next page",
1329
- disabled: disabled === true || current >= count - 1,
2174
+ className: cx("ink-prev-page", className),
2175
+ "data-ink-prev-page": "",
2176
+ "aria-label": "Previous page",
2177
+ title: "Previous page",
2178
+ disabled: disabled === true || current <= 0,
1330
2179
  ...rest,
1331
2180
  onClick: (e) => {
1332
2181
  onClick?.(e);
1333
- if (!e.defaultPrevented) next();
1334
- },
1335
- children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ChevronRight })
1336
- });
1337
- }
1338
- //#endregion
1339
- //#region src/components/InkColorMenu.tsx
1340
- function InkColorMenu({ property, label, className, children, standalone = false, group, fallback, disabled, ...rest }) {
1341
- const Wrap = standalone ? Fragment : Toolbar.Button;
1342
- const { current, supported, set } = useToolStyle(group);
1343
- const [open, setOpen] = useState(false);
1344
- const model = colorMenuModel(property, current, supported, fallback);
1345
- const name = label ?? model.label;
1346
- /** ArrowDown opens on the first item; a click opens on the checked one. */
1347
- const focusFirst = useRef(false);
1348
- const input = useRef(null);
1349
- const apply = useRef((_hex) => {});
1350
- apply.current = (hex) => set(model.custom(hex));
1351
- const bindInput = useCallback((el) => {
1352
- input.current = el;
1353
- el?.addEventListener("change", () => apply.current(el.value));
1354
- }, []);
1355
- useEffect(() => {
1356
- if (input.current) input.current.value = model.customValue.toLowerCase();
1357
- }, [model.customValue]);
1358
- useEffect(() => {
1359
- if (!model.enabled) setOpen(false);
1360
- }, [model.enabled]);
1361
- const trigger = model.trigger;
1362
- const swatchStyle = trigger.kind === "color" ? { "--ink-swatch": trigger.color } : void 0;
1363
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Popover.Root, {
1364
- open,
1365
- onOpenChange: setOpen,
1366
- children: [/* @__PURE__ */ jsx(Wrap, {
1367
- ...standalone ? {} : { asChild: true },
1368
- children: /* @__PURE__ */ jsx(Popover.Trigger, {
1369
- asChild: true,
1370
- children: /* @__PURE__ */ jsx("button", {
1371
- type: "button",
1372
- className: cx("ink-menu-trigger ink-color-menu", className),
1373
- "data-ink-color-menu": property,
1374
- "data-state": open ? "open" : "closed",
1375
- "data-mixed": model.mixed ? "" : void 0,
1376
- "aria-label": name,
1377
- title: name,
1378
- "aria-haspopup": "menu",
1379
- disabled: disabled === true || !model.enabled,
1380
- ...rest,
1381
- onKeyDown: (e) => {
1382
- rest.onKeyDown?.(e);
1383
- if (e.key !== "ArrowDown" || e.defaultPrevented || open || !model.enabled) return;
1384
- e.preventDefault();
1385
- focusFirst.current = true;
1386
- setOpen(true);
1387
- },
1388
- children: children ?? /* @__PURE__ */ jsx("span", {
1389
- className: "ink-swatch",
1390
- "data-ink-swatch": "",
1391
- "data-property": property,
1392
- "data-kind": trigger.kind,
1393
- "data-color": trigger.kind === "color" ? trigger.color : void 0,
1394
- style: swatchStyle,
1395
- "aria-hidden": "true"
1396
- })
1397
- })
1398
- })
1399
- }), /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsxs(Popover.Content, {
1400
- role: "menu",
1401
- "aria-label": name,
1402
- className: "ink-select-content ink-menu-content",
1403
- "data-ink-select-content": "",
1404
- "data-ink-menu": `color-${property}`,
1405
- side: "bottom",
1406
- align: "start",
1407
- sideOffset: 4,
1408
- collisionPadding: 8,
1409
- onOpenAutoFocus: (e) => {
1410
- e.preventDefault();
1411
- const menu = e.currentTarget;
1412
- if (focusFirst.current) menuItems(menu)[0]?.focus({ preventScroll: true });
1413
- else focusInitialMenuItem(menu);
1414
- focusFirst.current = false;
1415
- },
1416
- onKeyDown: (e) => handleMenuKeys(e.nativeEvent, e.currentTarget),
1417
- children: [
1418
- model.noFill ? /* @__PURE__ */ jsxs("button", {
1419
- type: "button",
1420
- role: "menuitemradio",
1421
- "aria-checked": model.noFill.checked,
1422
- tabIndex: -1,
1423
- className: "ink-select-item ink-menu-item",
1424
- "data-ink-select-item": "",
1425
- "data-state": model.noFill.checked ? "checked" : "unchecked",
1426
- onClick: () => set(model.noFill.patch),
1427
- children: ["No fill", model.noFill.checked ? /* @__PURE__ */ jsx("span", {
1428
- className: "ink-select-indicator",
1429
- children: /* @__PURE__ */ jsx(InkIcon, {
1430
- icon: Check,
1431
- size: 16
1432
- })
1433
- }) : null]
1434
- }) : null,
1435
- /* @__PURE__ */ jsx("div", {
1436
- role: "group",
1437
- "aria-label": "Colours",
1438
- className: "ink-color-grid",
1439
- "data-ink-menu-columns": COLOR_COLUMNS,
1440
- children: model.swatches.map((sw) => /* @__PURE__ */ jsx("button", {
1441
- type: "button",
1442
- role: "menuitemradio",
1443
- "aria-label": sw.label,
1444
- "aria-checked": sw.checked,
1445
- tabIndex: -1,
1446
- className: "ink-swatch-item",
1447
- "data-state": sw.checked ? "checked" : "unchecked",
1448
- style: { "--ink-swatch": sw.color },
1449
- onClick: () => set(sw.patch)
1450
- }, sw.key))
1451
- }),
1452
- /* @__PURE__ */ jsx("button", {
1453
- type: "button",
1454
- role: "menuitem",
1455
- tabIndex: -1,
1456
- className: "ink-select-item ink-menu-item",
1457
- "data-ink-select-item": "",
1458
- onClick: () => input.current?.click(),
1459
- children: "Custom…"
1460
- })
1461
- ]
1462
- }) })]
1463
- }), /* @__PURE__ */ jsx("input", {
1464
- ref: bindInput,
1465
- type: "color",
1466
- className: "ink-color-input",
1467
- "aria-label": "Custom colour",
1468
- "data-ink-color-input": property,
1469
- tabIndex: -1,
1470
- defaultValue: model.customValue.toLowerCase()
1471
- })] });
1472
- }
1473
- //#endregion
1474
- //#region src/components/InkShapeMenu.tsx
1475
- function InkShapeMenu({ className, children, ...rest }) {
1476
- const { tool, setTool } = useTool();
1477
- const [options, setOptions] = useToolOptions("shape");
1478
- const [open, setOpen] = useState(false);
1479
- const model = shapeMenuModel(options, SHAPE_KINDS);
1480
- const active = tool === "shape";
1481
- /** ArrowDown opens on the first item; a click opens on the checked one. */
1482
- const focusFirst = useRef(false);
1483
- return /* @__PURE__ */ jsx(Popover.Root, {
1484
- open,
1485
- onOpenChange: setOpen,
1486
- children: /* @__PURE__ */ jsx(Popover.Anchor, {
1487
- asChild: true,
1488
- children: /* @__PURE__ */ jsxs("div", {
1489
- role: "group",
1490
- "aria-label": "Shape",
1491
- className: cx("ink-split-button ink-shape-menu", className),
1492
- "data-ink-shape-menu": "",
1493
- ...rest,
1494
- children: [
1495
- /* @__PURE__ */ jsx(Toolbar.Button, {
1496
- className: "ink-tool-button ink-shape-button",
1497
- "data-ink-tool-button": "",
1498
- "data-ink-shape-button": "",
1499
- "data-tool": "shape",
1500
- "data-kind": model.kind,
1501
- "data-state": active ? "on" : "off",
1502
- "aria-pressed": active,
1503
- "aria-label": model.label,
1504
- title: model.label,
1505
- onClick: () => setTool("shape"),
1506
- children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: SHAPE_ICONS[model.kind] })
1507
- }),
1508
- /* @__PURE__ */ jsx(Toolbar.Button, {
1509
- asChild: true,
1510
- children: /* @__PURE__ */ jsx(Popover.Trigger, {
1511
- asChild: true,
1512
- children: /* @__PURE__ */ jsx("button", {
1513
- type: "button",
1514
- className: "ink-menu-trigger ink-shape-menu-trigger",
1515
- "data-ink-shape-menu-trigger": "",
1516
- "data-state": open ? "open" : "closed",
1517
- "aria-label": "Choose shape",
1518
- title: "Choose shape",
1519
- "aria-haspopup": "menu",
1520
- onKeyDown: (e) => {
1521
- if (e.key !== "ArrowDown" || e.defaultPrevented || open) return;
1522
- e.preventDefault();
1523
- focusFirst.current = true;
1524
- setOpen(true);
1525
- },
1526
- children: /* @__PURE__ */ jsx(InkIcon, { icon: ChevronDown })
1527
- })
1528
- })
1529
- }),
1530
- /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Content, {
1531
- role: "menu",
1532
- "aria-label": "Shapes",
1533
- className: "ink-select-content ink-menu-content",
1534
- "data-ink-select-content": "",
1535
- "data-ink-menu": "shape",
1536
- side: "bottom",
1537
- align: "start",
1538
- sideOffset: 4,
1539
- collisionPadding: 8,
1540
- onOpenAutoFocus: (e) => {
1541
- e.preventDefault();
1542
- const menu = e.currentTarget;
1543
- if (focusFirst.current) menuItems(menu)[0]?.focus({ preventScroll: true });
1544
- else focusInitialMenuItem(menu);
1545
- focusFirst.current = false;
1546
- },
1547
- onKeyDown: (e) => handleMenuKeys(e.nativeEvent, e.currentTarget),
1548
- children: /* @__PURE__ */ jsx("div", {
1549
- role: "group",
1550
- "aria-label": "Shapes",
1551
- className: "ink-shape-grid",
1552
- "data-ink-menu-columns": SHAPE_COLUMNS,
1553
- children: model.items.map((item) => /* @__PURE__ */ jsxs("button", {
1554
- type: "button",
1555
- role: "menuitemradio",
1556
- "aria-checked": item.checked,
1557
- tabIndex: -1,
1558
- className: "ink-select-item ink-menu-item ink-shape-item",
1559
- "data-ink-select-item": "",
1560
- "data-key": item.kind,
1561
- "data-state": item.checked ? "checked" : "unchecked",
1562
- onClick: () => {
1563
- setOptions({ kind: item.kind });
1564
- setTool("shape");
1565
- setOpen(false);
1566
- },
1567
- children: [/* @__PURE__ */ jsx(InkIcon, {
1568
- icon: SHAPE_ICONS[item.kind],
1569
- size: 16
1570
- }), item.label]
1571
- }, item.kind))
1572
- })
1573
- }) })
1574
- ]
1575
- })
1576
- })
2182
+ if (!e.defaultPrevented) previous();
2183
+ },
2184
+ children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ChevronLeft })
1577
2185
  });
1578
2186
  }
1579
2187
  //#endregion
1580
- //#region src/components/InkSignatureDialog.tsx
1581
- function InkSignatureDialog({ open, onOpenChange, fonts, onSave, onCloseAutoFocus, className }) {
1582
- return /* @__PURE__ */ jsx(Dialog.Root, {
1583
- open,
1584
- onOpenChange,
1585
- children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [/* @__PURE__ */ jsx(Dialog.Overlay, {
1586
- className: "ink-dialog-overlay",
1587
- "data-ink-dialog-overlay": ""
1588
- }), /* @__PURE__ */ jsxs(Dialog.Content, {
1589
- className: cx("ink-dialog ink-signature-dialog", className),
1590
- "data-ink-signature-dialog": "",
1591
- "aria-modal": "true",
1592
- "aria-describedby": void 0,
1593
- onOpenAutoFocus: (e) => {
1594
- e.preventDefault();
1595
- e.currentTarget.querySelector("[role=\"tab\"][aria-selected=\"true\"]")?.focus({ preventScroll: true });
1596
- },
1597
- onCloseAutoFocus,
1598
- children: [/* @__PURE__ */ jsx(Dialog.Title, {
1599
- className: "ink-dialog-title",
1600
- children: SIGNATURE_LABELS.dialog
1601
- }), /* @__PURE__ */ jsx(SignatureForm, {
1602
- fonts,
1603
- onSave,
1604
- onClose: () => onOpenChange(false)
1605
- })]
1606
- })] })
2188
+ //#region src/components/InkNextPage.tsx
2189
+ function InkNextPage({ asChild = false, className, children, disabled, onClick, ...rest }) {
2190
+ const { current, count, next } = usePage();
2191
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
2192
+ asChild,
2193
+ className: cx("ink-next-page", className),
2194
+ "data-ink-next-page": "",
2195
+ "aria-label": "Next page",
2196
+ title: "Next page",
2197
+ disabled: disabled === true || current >= count - 1,
2198
+ ...rest,
2199
+ onClick: (e) => {
2200
+ onClick?.(e);
2201
+ if (!e.defaultPrevented) next();
2202
+ },
2203
+ children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ChevronRight })
1607
2204
  });
1608
2205
  }
1609
- const IDLE = {
1610
- tab: "draw",
1611
- padEmpty: true,
1612
- name: "",
1613
- font: "",
1614
- image: void 0,
1615
- imageError: void 0,
1616
- saveError: void 0,
1617
- canSave: false,
1618
- saving: false
1619
- };
1620
- /** The dialog's body, mounted while it is open: one draft per opening. */
1621
- function SignatureForm({ fonts, onSave, onClose }) {
1622
- const ctx = useInkContext("InkSignatureDialog");
1623
- const { setTool } = useTool();
1624
- const [, setOptions] = useToolOptions("signature");
1625
- const [draft] = useState(() => createSignatureDraft({ fonts: fonts ?? ctx.signatureFonts }));
1626
- const s = useReadable(draft.state, IDLE);
1627
- const uid = useId();
1628
- const padHost = useRef(null);
2206
+ //#endregion
2207
+ //#region src/components/InkColorMenu.tsx
2208
+ function InkColorMenu({ property, label, className, children, standalone = false, group, fallback, disabled, ...rest }) {
2209
+ const Wrap = standalone ? Fragment : Toolbar.Button;
2210
+ const { current, supported, set } = useToolStyle(group);
2211
+ const [open, setOpen] = useState(false);
2212
+ const model = colorMenuModel(property, current, supported, fallback);
2213
+ const name = label ?? model.label;
2214
+ /** ArrowDown opens on the first item; a click opens on the checked one. */
2215
+ const focusFirst = useRef(false);
2216
+ const input = useRef(null);
2217
+ const apply = useRef((_hex) => {});
2218
+ apply.current = (hex) => set(model.custom(hex));
2219
+ const bindInput = useCallback((el) => {
2220
+ input.current = el;
2221
+ el?.addEventListener("change", () => apply.current(el.value));
2222
+ }, []);
1629
2223
  useEffect(() => {
1630
- const host = padHost.current;
1631
- const detach = host ? draft.attachPad(host) : void 0;
1632
- return () => {
1633
- detach?.();
1634
- draft.destroy();
1635
- };
1636
- }, [draft]);
1637
- const save = () => {
1638
- draft.save(ctx.signatures).then((t) => {
1639
- if (!t) return;
1640
- setOptions({ templateId: t.id });
1641
- setTool("signature");
1642
- onSave?.(t);
1643
- onClose();
1644
- }, () => {});
1645
- };
1646
- const tabId = (tab) => `${uid}-tab-${tab}`;
1647
- const panelId = (tab) => `${uid}-panel-${tab}`;
1648
- const panel = (tab) => ({
1649
- role: "tabpanel",
1650
- id: panelId(tab),
1651
- "aria-labelledby": tabId(tab),
1652
- className: "ink-tab-panel",
1653
- "data-key": tab,
1654
- hidden: s.tab !== tab
1655
- });
1656
- const shown = s.name.trim() || SIGNATURE_LABELS.namePlaceholder;
1657
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [
1658
- /* @__PURE__ */ jsx("div", {
1659
- role: "tablist",
1660
- "aria-label": SIGNATURE_LABELS.tabs,
1661
- className: "ink-tabs",
1662
- onKeyDown: (e) => handleTabKeys(e.nativeEvent, e.currentTarget),
1663
- children: SIGNATURE_TABS.map((tab) => /* @__PURE__ */ jsx("button", {
1664
- type: "button",
1665
- role: "tab",
1666
- id: tabId(tab),
1667
- "aria-controls": panelId(tab),
1668
- "aria-selected": s.tab === tab,
1669
- tabIndex: s.tab === tab ? 0 : -1,
1670
- className: "ink-tab",
1671
- "data-key": tab,
1672
- "data-state": s.tab === tab ? "active" : "inactive",
1673
- onClick: () => draft.setTab(tab),
1674
- children: SIGNATURE_LABELS[tab]
1675
- }, tab))
1676
- }),
1677
- /* @__PURE__ */ jsxs("div", {
1678
- ...panel("draw"),
1679
- children: [/* @__PURE__ */ jsx("div", {
1680
- ref: padHost,
1681
- className: "ink-signature-pad-host",
1682
- "data-ink-signature-pad-host": "",
1683
- role: "img",
1684
- "aria-label": SIGNATURE_LABELS.pad
1685
- }), /* @__PURE__ */ jsx("div", {
1686
- className: "ink-dialog-row",
2224
+ if (input.current) input.current.value = model.customValue.toLowerCase();
2225
+ }, [model.customValue]);
2226
+ useEffect(() => {
2227
+ if (!model.enabled) setOpen(false);
2228
+ }, [model.enabled]);
2229
+ const trigger = model.trigger;
2230
+ const swatchStyle = trigger.kind === "color" ? { "--ink-swatch": trigger.color } : void 0;
2231
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Popover.Root, {
2232
+ open,
2233
+ onOpenChange: setOpen,
2234
+ children: [/* @__PURE__ */ jsx(Wrap, {
2235
+ ...standalone ? {} : { asChild: true },
2236
+ children: /* @__PURE__ */ jsx(Popover.Trigger, {
2237
+ asChild: true,
1687
2238
  children: /* @__PURE__ */ jsx("button", {
1688
2239
  type: "button",
1689
- className: "ink-button",
1690
- "data-ink-signature-clear": "",
1691
- disabled: s.padEmpty,
1692
- onClick: () => draft.clearPad(),
1693
- children: SIGNATURE_LABELS.clear
1694
- })
1695
- })]
1696
- }),
1697
- /* @__PURE__ */ jsxs("div", {
1698
- ...panel("type"),
1699
- children: [/* @__PURE__ */ jsxs("label", {
1700
- className: "ink-field",
1701
- children: [/* @__PURE__ */ jsx("span", { children: SIGNATURE_LABELS.name }), /* @__PURE__ */ jsx("input", {
1702
- type: "text",
1703
- className: "ink-input",
1704
- "data-ink-signature-name": "",
1705
- autoComplete: "name",
1706
- placeholder: SIGNATURE_LABELS.namePlaceholder,
1707
- value: s.name,
1708
- onChange: (e) => draft.setName(e.target.value)
1709
- })]
1710
- }), /* @__PURE__ */ jsx("div", {
1711
- role: "radiogroup",
1712
- "aria-label": SIGNATURE_LABELS.fonts,
1713
- className: "ink-signature-fonts",
1714
- onKeyDown: (e) => handleRadioKeys(e.nativeEvent, e.currentTarget),
1715
- children: draft.fonts.map((font) => /* @__PURE__ */ jsx("button", {
1716
- type: "button",
1717
- role: "radio",
1718
- "aria-checked": s.font === font,
1719
- "aria-label": font,
1720
- tabIndex: s.font === font ? 0 : -1,
1721
- className: "ink-signature-font",
1722
- "data-key": font,
1723
- "data-state": s.font === font ? "checked" : "unchecked",
1724
- onClick: () => draft.setFont(font),
1725
- children: /* @__PURE__ */ jsx("span", {
1726
- className: "ink-signature-font-sample",
1727
- style: { fontFamily: fontFamilyValue(font) },
1728
- children: shown
2240
+ className: cx("ink-menu-trigger ink-color-menu", className),
2241
+ "data-ink-color-menu": property,
2242
+ "data-state": open ? "open" : "closed",
2243
+ "data-mixed": model.mixed ? "" : void 0,
2244
+ "aria-label": name,
2245
+ title: name,
2246
+ "aria-haspopup": "menu",
2247
+ disabled: disabled === true || !model.enabled,
2248
+ ...rest,
2249
+ onKeyDown: (e) => {
2250
+ rest.onKeyDown?.(e);
2251
+ if (e.key !== "ArrowDown" || e.defaultPrevented || open || !model.enabled) return;
2252
+ e.preventDefault();
2253
+ focusFirst.current = true;
2254
+ setOpen(true);
2255
+ },
2256
+ children: children ?? /* @__PURE__ */ jsx("span", {
2257
+ className: "ink-swatch",
2258
+ "data-ink-swatch": "",
2259
+ "data-property": property,
2260
+ "data-kind": trigger.kind,
2261
+ "data-color": trigger.kind === "color" ? trigger.color : void 0,
2262
+ style: swatchStyle,
2263
+ "aria-hidden": "true"
1729
2264
  })
1730
- }, font))
1731
- })]
1732
- }),
1733
- /* @__PURE__ */ jsxs("div", {
1734
- ...panel("image"),
2265
+ })
2266
+ })
2267
+ }), /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsxs(Popover.Content, {
2268
+ role: "menu",
2269
+ "aria-label": name,
2270
+ className: "ink-select-content ink-menu-content",
2271
+ "data-ink-select-content": "",
2272
+ "data-ink-menu": `color-${property}`,
2273
+ side: "bottom",
2274
+ align: "start",
2275
+ sideOffset: 4,
2276
+ collisionPadding: 8,
2277
+ onOpenAutoFocus: (e) => {
2278
+ e.preventDefault();
2279
+ const menu = e.currentTarget;
2280
+ if (focusFirst.current) menuItems(menu)[0]?.focus({ preventScroll: true });
2281
+ else focusInitialMenuItem(menu);
2282
+ focusFirst.current = false;
2283
+ },
2284
+ onKeyDown: (e) => handleMenuKeys(e.nativeEvent, e.currentTarget),
1735
2285
  children: [
1736
- /* @__PURE__ */ jsxs("label", {
1737
- className: "ink-field",
1738
- children: [/* @__PURE__ */ jsx("span", { children: SIGNATURE_LABELS.file }), /* @__PURE__ */ jsx("input", {
1739
- type: "file",
1740
- accept: "image/png,image/jpeg",
1741
- className: "ink-input",
1742
- "data-ink-signature-file": "",
1743
- onChange: (e) => void draft.chooseFile(e.target.files?.[0])
1744
- })]
1745
- }),
1746
- s.image?.kind === "image" ? /* @__PURE__ */ jsx("img", {
1747
- className: "ink-signature-image",
1748
- src: s.image.imagedata,
1749
- alt: ""
2286
+ model.noFill ? /* @__PURE__ */ jsxs("button", {
2287
+ type: "button",
2288
+ role: "menuitemradio",
2289
+ "aria-checked": model.noFill.checked,
2290
+ tabIndex: -1,
2291
+ className: "ink-select-item ink-menu-item",
2292
+ "data-ink-select-item": "",
2293
+ "data-state": model.noFill.checked ? "checked" : "unchecked",
2294
+ onClick: () => set(model.noFill.patch),
2295
+ children: ["No fill", model.noFill.checked ? /* @__PURE__ */ jsx("span", {
2296
+ className: "ink-select-indicator",
2297
+ children: /* @__PURE__ */ jsx(InkIcon, {
2298
+ icon: Check,
2299
+ size: 16
2300
+ })
2301
+ }) : null]
1750
2302
  }) : null,
1751
- s.imageError ? /* @__PURE__ */ jsx("p", {
1752
- className: "ink-field-error",
1753
- role: "alert",
1754
- children: s.imageError
1755
- }) : null
1756
- ]
1757
- }),
1758
- s.saveError ? /* @__PURE__ */ jsx("p", {
1759
- className: "ink-field-error",
1760
- role: "alert",
1761
- "data-ink-signature-save-error": "",
1762
- children: s.saveError
1763
- }) : null,
1764
- /* @__PURE__ */ jsxs("div", {
1765
- className: "ink-dialog-actions",
1766
- children: [/* @__PURE__ */ jsx(Dialog.Close, {
1767
- asChild: true,
1768
- children: /* @__PURE__ */ jsx("button", {
2303
+ /* @__PURE__ */ jsx("div", {
2304
+ role: "group",
2305
+ "aria-label": "Colours",
2306
+ className: "ink-color-grid",
2307
+ "data-ink-menu-columns": COLOR_COLUMNS,
2308
+ children: model.swatches.map((sw) => /* @__PURE__ */ jsx("button", {
2309
+ type: "button",
2310
+ role: "menuitemradio",
2311
+ "aria-label": sw.label,
2312
+ "aria-checked": sw.checked,
2313
+ tabIndex: -1,
2314
+ className: "ink-swatch-item",
2315
+ "data-state": sw.checked ? "checked" : "unchecked",
2316
+ style: { "--ink-swatch": sw.color },
2317
+ onClick: () => set(sw.patch)
2318
+ }, sw.key))
2319
+ }),
2320
+ /* @__PURE__ */ jsx("button", {
1769
2321
  type: "button",
1770
- className: "ink-button",
1771
- "data-ink-signature-cancel": "",
1772
- children: SIGNATURE_LABELS.cancel
2322
+ role: "menuitem",
2323
+ tabIndex: -1,
2324
+ className: "ink-select-item ink-menu-item",
2325
+ "data-ink-select-item": "",
2326
+ onClick: () => input.current?.click(),
2327
+ children: "Custom…"
1773
2328
  })
1774
- }), /* @__PURE__ */ jsx("button", {
1775
- type: "button",
1776
- className: "ink-button",
1777
- "data-variant": "primary",
1778
- "data-ink-signature-save": "",
1779
- disabled: !s.canSave,
1780
- onClick: save,
1781
- children: SIGNATURE_LABELS.save
1782
- })]
1783
- })
1784
- ] });
2329
+ ]
2330
+ }) })]
2331
+ }), /* @__PURE__ */ jsx("input", {
2332
+ ref: bindInput,
2333
+ type: "color",
2334
+ className: "ink-color-input",
2335
+ "aria-label": "Custom colour",
2336
+ "data-ink-color-input": property,
2337
+ tabIndex: -1,
2338
+ defaultValue: model.customValue.toLowerCase()
2339
+ })] });
1785
2340
  }
1786
2341
  //#endregion
1787
- //#region src/components/InkSignatureMenu.tsx
1788
- const NO_TEMPLATES = [];
1789
- /** A saved signature drawn small: ink as filled outlines, an image as itself. */
1790
- function SignaturePreview({ template }) {
1791
- if (template.kind === "image") return /* @__PURE__ */ jsx("span", {
1792
- className: "ink-signature-preview",
1793
- "data-kind": "image",
1794
- children: /* @__PURE__ */ jsx("img", {
1795
- src: template.imagedata,
1796
- alt: "",
1797
- draggable: false
1798
- })
1799
- });
1800
- const { viewBox, paths } = signaturePreview(template);
1801
- return /* @__PURE__ */ jsx("span", {
1802
- className: "ink-signature-preview",
1803
- "data-kind": "ink",
1804
- children: /* @__PURE__ */ jsx("svg", {
1805
- viewBox,
1806
- "aria-hidden": "true",
1807
- children: paths.map((d, i) => /* @__PURE__ */ jsx("path", {
1808
- d,
1809
- fill: "currentColor"
1810
- }, i))
2342
+ //#region src/components/InkShapeMenu.tsx
2343
+ function InkShapeMenu({ className, children, ...rest }) {
2344
+ const { tool, setTool } = useTool();
2345
+ const [options, setOptions] = useToolOptions("shape");
2346
+ const [open, setOpen] = useState(false);
2347
+ const model = shapeMenuModel(options, SHAPE_KINDS);
2348
+ const active = tool === "shape";
2349
+ /** ArrowDown opens on the first item; a click opens on the checked one. */
2350
+ const focusFirst = useRef(false);
2351
+ return /* @__PURE__ */ jsx(Popover.Root, {
2352
+ open,
2353
+ onOpenChange: setOpen,
2354
+ children: /* @__PURE__ */ jsx(Popover.Anchor, {
2355
+ asChild: true,
2356
+ children: /* @__PURE__ */ jsxs("div", {
2357
+ role: "group",
2358
+ "aria-label": "Shape",
2359
+ className: cx("ink-split-button ink-shape-menu", className),
2360
+ "data-ink-shape-menu": "",
2361
+ ...rest,
2362
+ children: [
2363
+ /* @__PURE__ */ jsx(Toolbar.Button, {
2364
+ className: "ink-tool-button ink-shape-button",
2365
+ "data-ink-tool-button": "",
2366
+ "data-ink-shape-button": "",
2367
+ "data-tool": "shape",
2368
+ "data-kind": model.kind,
2369
+ "data-state": active ? "on" : "off",
2370
+ "aria-pressed": active,
2371
+ "aria-label": model.label,
2372
+ title: model.label,
2373
+ onClick: () => setTool("shape"),
2374
+ children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: SHAPE_ICONS[model.kind] })
2375
+ }),
2376
+ /* @__PURE__ */ jsx(Toolbar.Button, {
2377
+ asChild: true,
2378
+ children: /* @__PURE__ */ jsx(Popover.Trigger, {
2379
+ asChild: true,
2380
+ children: /* @__PURE__ */ jsx("button", {
2381
+ type: "button",
2382
+ className: "ink-menu-trigger ink-shape-menu-trigger",
2383
+ "data-ink-shape-menu-trigger": "",
2384
+ "data-state": open ? "open" : "closed",
2385
+ "aria-label": "Choose shape",
2386
+ title: "Choose shape",
2387
+ "aria-haspopup": "menu",
2388
+ onKeyDown: (e) => {
2389
+ if (e.key !== "ArrowDown" || e.defaultPrevented || open) return;
2390
+ e.preventDefault();
2391
+ focusFirst.current = true;
2392
+ setOpen(true);
2393
+ },
2394
+ children: /* @__PURE__ */ jsx(InkIcon, { icon: ChevronDown })
2395
+ })
2396
+ })
2397
+ }),
2398
+ /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Content, {
2399
+ role: "menu",
2400
+ "aria-label": "Shapes",
2401
+ className: "ink-select-content ink-menu-content",
2402
+ "data-ink-select-content": "",
2403
+ "data-ink-menu": "shape",
2404
+ side: "bottom",
2405
+ align: "start",
2406
+ sideOffset: 4,
2407
+ collisionPadding: 8,
2408
+ onOpenAutoFocus: (e) => {
2409
+ e.preventDefault();
2410
+ const menu = e.currentTarget;
2411
+ if (focusFirst.current) menuItems(menu)[0]?.focus({ preventScroll: true });
2412
+ else focusInitialMenuItem(menu);
2413
+ focusFirst.current = false;
2414
+ },
2415
+ onKeyDown: (e) => handleMenuKeys(e.nativeEvent, e.currentTarget),
2416
+ children: /* @__PURE__ */ jsx("div", {
2417
+ role: "group",
2418
+ "aria-label": "Shapes",
2419
+ className: "ink-shape-grid",
2420
+ "data-ink-menu-columns": SHAPE_COLUMNS,
2421
+ children: model.items.map((item) => /* @__PURE__ */ jsxs("button", {
2422
+ type: "button",
2423
+ role: "menuitemradio",
2424
+ "aria-checked": item.checked,
2425
+ tabIndex: -1,
2426
+ className: "ink-select-item ink-menu-item ink-shape-item",
2427
+ "data-ink-select-item": "",
2428
+ "data-key": item.kind,
2429
+ "data-state": item.checked ? "checked" : "unchecked",
2430
+ onClick: () => {
2431
+ setOptions({ kind: item.kind });
2432
+ setTool("shape");
2433
+ setOpen(false);
2434
+ },
2435
+ children: [/* @__PURE__ */ jsx(InkIcon, {
2436
+ icon: SHAPE_ICONS[item.kind],
2437
+ size: 16
2438
+ }), item.label]
2439
+ }, item.kind))
2440
+ })
2441
+ }) })
2442
+ ]
2443
+ })
1811
2444
  })
1812
2445
  });
1813
2446
  }
2447
+ //#endregion
2448
+ //#region src/components/InkSignatureMenu.tsx
2449
+ const NO_TEMPLATES = [];
1814
2450
  function InkSignatureMenu({ className, children, fonts, ...rest }) {
1815
2451
  const { tool, setTool } = useTool();
1816
2452
  const [options, setOptions] = useToolOptions("signature");
1817
2453
  const { signatures } = useInkContext("InkSignatureMenu");
1818
2454
  const templates = useReadable(signatures.templates, NO_TEMPLATES);
1819
- const removeError = useReadable(signatures.removeError, void 0);
1820
2455
  const model = signatureMenuModel(templates, options);
1821
2456
  const [open, setOpen] = useState(false);
1822
2457
  const [creating, setCreating] = useState(false);
@@ -1827,25 +2462,9 @@ function InkSignatureMenu({ className, children, fonts, ...rest }) {
1827
2462
  const focusFirst = useRef(false);
1828
2463
  /** Set while the menu closes for the dialog, which takes focus instead of the trigger. */
1829
2464
  const toDialog = useRef(false);
1830
- /** The row of a removed signature, until focus lands on what took its place. */
1831
- const removedRow = useRef(null);
1832
2465
  useEffect(() => {
1833
2466
  if (open) signatures.clearRemoveError();
1834
2467
  }, [open, signatures]);
1835
- useEffect(() => {
1836
- const row = removedRow.current;
1837
- const panel = menu.current;
1838
- if (row === null || !panel) return;
1839
- removedRow.current = null;
1840
- const rows = panel.querySelectorAll(".ink-signature-item");
1841
- (rows[Math.min(row, rows.length - 1)] ?? panel.querySelector("[data-ink-signature-create]"))?.focus({ preventScroll: true });
1842
- }, [templates]);
1843
- const remove = (id, row) => {
1844
- removedRow.current = row;
1845
- signatures.remove(id).catch(() => {
1846
- removedRow.current = null;
1847
- });
1848
- };
1849
2468
  return /* @__PURE__ */ jsx(Popover.Root, {
1850
2469
  open,
1851
2470
  onOpenChange: setOpen,
@@ -1896,7 +2515,7 @@ function InkSignatureMenu({ className, children, fonts, ...rest }) {
1896
2515
  })
1897
2516
  })
1898
2517
  }),
1899
- /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsxs(Popover.Content, {
2518
+ /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Content, {
1900
2519
  ref: menu,
1901
2520
  role: "menu",
1902
2521
  "aria-label": SIGNATURE_LABELS.menu,
@@ -1919,71 +2538,20 @@ function InkSignatureMenu({ className, children, fonts, ...rest }) {
1919
2538
  toDialog.current = false;
1920
2539
  },
1921
2540
  onKeyDown: (e) => handleMenuKeys(e.nativeEvent, e.currentTarget),
1922
- children: [
1923
- model.items.length > 0 ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
1924
- role: "group",
1925
- "aria-label": SIGNATURE_LABELS.saved,
1926
- className: "ink-signature-list",
1927
- "data-ink-menu-columns": 2,
1928
- children: model.items.map((item, row) => [/* @__PURE__ */ jsx("button", {
1929
- type: "button",
1930
- role: "menuitemradio",
1931
- "aria-checked": item.checked,
1932
- "aria-label": item.label,
1933
- tabIndex: -1,
1934
- className: "ink-select-item ink-menu-item ink-signature-item",
1935
- "data-ink-select-item": "",
1936
- "data-key": item.id,
1937
- "data-state": item.checked ? "checked" : "unchecked",
1938
- onClick: () => {
1939
- setOptions({ templateId: item.id });
1940
- setTool("signature");
1941
- setOpen(false);
1942
- },
1943
- children: /* @__PURE__ */ jsx(SignaturePreview, { template: item.template })
1944
- }, `${item.id}-choose`), /* @__PURE__ */ jsx("button", {
1945
- type: "button",
1946
- role: "menuitem",
1947
- "aria-label": item.removeLabel,
1948
- title: item.removeLabel,
1949
- tabIndex: -1,
1950
- className: "ink-menu-item ink-signature-remove",
1951
- "data-ink-signature-remove": "",
1952
- "data-key": item.id,
1953
- onClick: () => remove(item.id, row),
1954
- children: /* @__PURE__ */ jsx(InkIcon, {
1955
- icon: Trash2,
1956
- size: 16
1957
- })
1958
- }, `${item.id}-remove`)])
1959
- }), /* @__PURE__ */ jsx("div", {
1960
- role: "separator",
1961
- className: "ink-select-separator"
1962
- })] }) : null,
1963
- /* @__PURE__ */ jsxs("button", {
1964
- type: "button",
1965
- role: "menuitem",
1966
- tabIndex: -1,
1967
- className: "ink-select-item ink-menu-item ink-signature-create",
1968
- "data-ink-select-item": "",
1969
- "data-ink-signature-create": "",
1970
- onClick: () => {
1971
- toDialog.current = true;
1972
- setOpen(false);
1973
- setCreating(true);
1974
- },
1975
- children: [/* @__PURE__ */ jsx(InkIcon, {
1976
- icon: Plus,
1977
- size: 16
1978
- }), SIGNATURE_LABELS.create]
1979
- }),
1980
- removeError ? /* @__PURE__ */ jsx("p", {
1981
- className: "ink-field-error ink-signature-remove-error",
1982
- role: "alert",
1983
- "data-ink-signature-remove-error": "",
1984
- children: removeError
1985
- }) : null
1986
- ]
2541
+ children: /* @__PURE__ */ jsx(SignatureMenuPanel, {
2542
+ items: model.items,
2543
+ menu,
2544
+ onChoose: (t) => {
2545
+ setOptions({ templateId: t.id });
2546
+ setTool("signature");
2547
+ setOpen(false);
2548
+ },
2549
+ onCreate: () => {
2550
+ toDialog.current = true;
2551
+ setOpen(false);
2552
+ setCreating(true);
2553
+ }
2554
+ })
1987
2555
  }) }),
1988
2556
  /* @__PURE__ */ jsx(InkSignatureDialog, {
1989
2557
  open: creating,
@@ -2634,6 +3202,6 @@ function ThreadForm({ editingRoot, locked, deleteLabel, onSubmit, onRemove, onCl
2634
3202
  });
2635
3203
  }
2636
3204
  //#endregion
2637
- export { InkAnnotation, InkColorMenu, InkCommand, InkHighlightMenu, InkIcon, InkLineStyleMenu, InkNextPage, InkPage, InkPageCount, InkPageInput, InkPrevPage, InkRedo, InkRoot, InkShapeMenu, InkSignatureDialog, InkSignatureMenu, InkTextStyleMenu, InkThreadPopover, InkToolButton, InkToolbar, InkToolbarSeparator, InkUndo, InkViewport, InkZoomIn, InkZoomOut, InkZoomSelect, useAnnotations, useCommand, useDocument, useEmbedded, useHistory, useInkContext, usePage, useReadable, useSelection, useTool, useToolOptions, useToolStyle, useZoom };
3205
+ export { InkAnnotation, InkColorMenu, InkCommand, InkFindBar, InkHighlightMenu, InkIcon, InkLineStyleMenu, InkNextPage, InkPage, InkPageCount, InkPageInput, InkPrevPage, InkRedo, InkRoot, InkSearchButton, InkShapeMenu, InkSignatureDialog, InkSignatureMenu, InkTextStyleMenu, InkThreadPopover, InkToolButton, InkToolbar, InkToolbarSeparator, InkUndo, InkViewport, InkZoomIn, InkZoomOut, InkZoomSelect, useAnnotations, useCommand, useDocument, useEmbedded, useForm, useHistory, useInkContext, usePage, useReadable, useSearch, useSelection, useTool, useToolOptions, useToolStyle, useZoom };
2638
3206
 
2639
3207
  //# sourceMappingURL=index.mjs.map