@inkbar/react 0.7.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,7 +1,7 @@
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, createSearchController, createSignatureDraft, createToolOptions, cssVariableDefaults, findAnnouncer, findCountText, focusInitialMenuItem, fontFamilyValue, handleFindKey, handleMenuKeys, handleRadioKeys, handleTabKeys, highlightMenuModel, lineStyleMenuModel, menuItems, mountViewer, observeAnnotationElement, pageSelectionText, shapeMenuModel, shouldKeepOpenOnAnnotation, signatureMenuModel, signaturePreview, textStyleMenuModel, threadDeleteLabel, threadPopoverLabel, trackToolStyle, unknownCommand, viewerSearchOf } 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";
@@ -253,6 +253,30 @@ function useCommand(id) {
253
253
  };
254
254
  }
255
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
256
280
  //#region src/use-latest.ts
257
281
  /**
258
282
  * A ref that always holds the latest render's value, for callbacks that outlive a render
@@ -290,7 +314,7 @@ function isDevelopment() {
290
314
  function setTestHook(el, doc) {
291
315
  if (el && isDevelopment()) el.__inkDocument = doc;
292
316
  }
293
- 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 }) {
294
318
  const rootElRef = useRef(null);
295
319
  const [rootEl, setRootElState] = useState(null);
296
320
  const setRootEl = useCallback((el) => {
@@ -320,6 +344,9 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
320
344
  defaults,
321
345
  annotations,
322
346
  controlled: controlledProp,
347
+ fields,
348
+ onFieldsChange,
349
+ onFormAction,
323
350
  history,
324
351
  historyEnabled,
325
352
  onAnnotationsChange,
@@ -336,6 +363,16 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
336
363
  tool: internalTool,
337
364
  tools
338
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]);
339
376
  const snapshotHistory = useMemo(() => createSnapshotHistory({
340
377
  apply: (list) => documentRef.current?.annotations.replaceAll(list, "history"),
341
378
  enabled: () => latest.current.historyEnabled
@@ -431,6 +468,7 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
431
468
  /** The plugins told of `opened`, and the snapshot history's hold on its store. */
432
469
  let told = null;
433
470
  let detach = noop;
471
+ let stopFormErrors = noop;
434
472
  const run = async () => {
435
473
  try {
436
474
  const doc = await openDocument(source, {
@@ -450,6 +488,16 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
450
488
  });
451
489
  });
452
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
+ }
453
501
  } : { mode: "uncontrolled" }
454
502
  });
455
503
  if (cancelled) {
@@ -459,6 +507,13 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
459
507
  opened = doc;
460
508
  const current = latest.current.annotations;
461
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
+ });
462
517
  documentRef.current = doc;
463
518
  if (doc.annotations.mode === "controlled") detach = snapshotHistory.attach(doc.annotations);
464
519
  told = plugins;
@@ -467,8 +522,14 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
467
522
  setOverride(null);
468
523
  setTestHook(rootElRef.current, doc);
469
524
  const s = doc.status.get();
470
- if (s.state === "ready") latest.current.onReady?.(doc);
471
- 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
+ }
472
533
  } catch (cause) {
473
534
  if (cancelled) return;
474
535
  const error = cause instanceof InkError ? cause : new InkError("InvalidPdf", cause instanceof Error ? cause.message : String(cause), {}, cause);
@@ -486,6 +547,7 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
486
547
  cancelled = true;
487
548
  told?.document.set(null);
488
549
  detach();
550
+ stopFormErrors();
489
551
  opened?.destroy();
490
552
  documentRef.current = null;
491
553
  setDocument(null);
@@ -496,11 +558,15 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
496
558
  password,
497
559
  snapshotHistory,
498
560
  latest,
499
- plugins
561
+ plugins,
562
+ reportErrorFor
500
563
  ]);
501
564
  useLayoutEffect(() => {
502
565
  if (annotations && document?.annotations.mode === "controlled") document.annotations.setControlledValue(annotations);
503
566
  }, [annotations, document]);
567
+ useLayoutEffect(() => {
568
+ if (fields && document) document.form.setControlledValue(fields);
569
+ }, [fields, document]);
504
570
  useEffect(() => {
505
571
  if (tool) setInternalTool(tool);
506
572
  }, [tool]);
@@ -512,6 +578,16 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
512
578
  }, [latest]);
513
579
  const reportZoom = useCallback((z) => latest.current.onZoomChange?.(z), [latest]);
514
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]);
515
591
  const setViewport = useCallback((vp, tc) => {
516
592
  viewportRef.current = vp;
517
593
  setViewportState(vp);
@@ -545,6 +621,31 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
545
621
  pluginHistory
546
622
  ]);
547
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
+ ]);
548
649
  /** The document's tool style; it batches through `history`, whichever mode is on. */
549
650
  const styleTracker = useMemo(() => document ? trackToolStyle({
550
651
  store: document.annotations,
@@ -624,7 +725,10 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
624
725
  viewportEl: plugins.root,
625
726
  history: inkHistory,
626
727
  rootEl,
627
- setViewport
728
+ setViewport,
729
+ formHighlight,
730
+ reportFormAction,
731
+ reportError
628
732
  }), [
629
733
  assets,
630
734
  document,
@@ -654,7 +758,10 @@ function InkRoot({ src, assets, password, defaults, annotations, onAnnotationsCh
654
758
  plugins,
655
759
  inkHistory,
656
760
  rootEl,
657
- setViewport
761
+ setViewport,
762
+ formHighlight,
763
+ reportFormAction,
764
+ reportError
658
765
  ]);
659
766
  useEffect(() => {
660
767
  if (!rootEl || !search) return;
@@ -687,6 +794,77 @@ function cx(...parts) {
687
794
  return out === "" ? void 0 : out;
688
795
  }
689
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
690
868
  //#region src/components/InkAnnotation.tsx
691
869
  function readState(el) {
692
870
  const s = el.dataset.state;
@@ -782,245 +960,214 @@ function InkPage({ page, host, mount, error, customAnnotations = false, renderAn
782
960
  }), t.el, t.annotation.id)) : null] });
783
961
  }
784
962
  //#endregion
785
- //#region src/components/InkViewport.tsx
786
- function InkViewport({ mode = "continuous", rotation = 0, gap = 16, buffer = 2, loading, empty, error, renderAnnotation, renderPageError, className, ...rest }) {
787
- const ctx = useInkContext("InkViewport");
788
- const scroll = useRef(null);
789
- const custom = !!renderAnnotation;
790
- const latest = useLatest({
791
- ctx,
792
- mode,
793
- rotation,
794
- gap,
795
- buffer,
796
- 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
+ })] })
797
991
  });
798
- const engine = useRef(null);
799
- /** Page hosts, mounts and errors the engine reported; InkPage renders portals for them. */
800
- const [pages, setPages] = useState([]);
801
- /**
802
- * Numbered before each build starts, so the pages a build mounts carry its own number. A
803
- * rebuild gives each page a new host and mount, so pages from an earlier build must remount,
804
- * not be reused.
805
- */
806
- const buildCount = useRef(0);
807
- /** The construction options the engine was last built with, so a rebuild runs only on a change. */
808
- const builtWith = useRef(null);
809
- /** The zoom and page last reported to the host. */
810
- const lastReportedZoom = useRef(void 0);
811
- 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);
812
1013
  useEffect(() => {
813
- const { ctx: c, mode: m, rotation: r, gap: g, buffer: b, custom: cu } = latest.current;
814
- const doc = c.document;
815
- const el = scroll.current;
816
- lastReportedZoom.current = void 0;
817
- lastReportedPage.current = void 0;
818
- if (!doc || !el || c.status.state !== "ready") return;
819
- let first = true;
820
- let stops = [];
821
- buildCount.current += 1;
822
- builtWith.current = {
823
- gap: g,
824
- buffer: b,
825
- custom: cu
826
- };
827
- const e = mountViewer({
828
- container: el,
829
- document: doc,
830
- zoom: c.zoomModel ?? "fit-width",
831
- ...c.pageModel === void 0 ? {} : { page: c.pageModel },
832
- mode: m,
833
- rotation: r,
834
- gap: g,
835
- buffer: b,
836
- tool: c.tool,
837
- ...c.hostTools ? { tools: c.hostTools } : {},
838
- ...c.pluginHost ? { plugins: c.pluginHost } : {},
839
- ...c.search ? { search: c.search } : {},
840
- decorateAnnotation: (a) => latest.current.ctx.decorateAnnotation(a),
841
- ...c.toolStyle ? { toolStyle: c.toolStyle } : {},
842
- toolHistory: { batch: (fn) => latest.current.ctx.history.batch(fn) },
843
- toolOptions: c.toolOptions,
844
- fonts: [...c.fonts],
845
- signatureLibrary: () => latest.current.ctx.signatures,
846
- customAnnotations: cu,
847
- onActivate: (id) => latest.current.ctx.setActivated(id),
848
- onBuild: (vp, tc) => {
849
- latest.current.ctx.setViewport(vp, tc);
850
- if (first) {
851
- first = false;
852
- lastReportedZoom.current = vp.zoom.get();
853
- lastReportedPage.current = 0;
854
- }
855
- for (const stop of stops) stop();
856
- const reportZoom = (z) => {
857
- if (z === lastReportedZoom.current) return;
858
- lastReportedZoom.current = z;
859
- latest.current.ctx.reportZoom(z);
860
- };
861
- const reportPage = (p) => {
862
- if (p === lastReportedPage.current) return;
863
- lastReportedPage.current = p;
864
- latest.current.ctx.reportPage(p);
865
- };
866
- const followTool = (id) => {
867
- if (id !== latest.current.ctx.tool) latest.current.ctx.setTool(id);
868
- };
869
- stops = [
870
- vp.zoom.subscribe(reportZoom),
871
- vp.currentPage.subscribe(reportPage),
872
- tc.active.subscribe(followTool)
873
- ];
874
- reportZoom(vp.zoom.get());
875
- reportPage(vp.currentPage.get());
876
- },
877
- onPageMount: (page, mount, host) => {
878
- const build = buildCount.current;
879
- const custom = builtWith.current?.custom ?? false;
880
- setPages((prev) => [...prev, {
881
- build,
882
- page,
883
- host,
884
- mount,
885
- error: null,
886
- custom
887
- }]);
888
- },
889
- onPageUnmount: (page) => setPages((prev) => prev.filter((p) => p.page !== page)),
890
- onPageError: (page, err) => setPages((prev) => prev.map((p) => p.page === page ? {
891
- ...p,
892
- error: err
893
- } : p))
894
- });
895
- engine.current = e;
896
- return () => {
897
- for (const stop of stops) stop();
898
- e.destroy();
899
- engine.current = null;
900
- builtWith.current = null;
901
- c.setViewport(null, null);
902
- setPages((prev) => prev.length === 0 ? prev : []);
903
- };
904
- }, [
905
- ctx.document,
906
- ctx.status.state,
907
- ctx.pluginHost,
908
- ctx.search,
909
- latest
910
- ]);
911
- const { viewportEl } = ctx;
912
- useLayoutEffect(() => {
913
- const el = scroll.current;
914
- viewportEl.set(el);
1014
+ const host = padHost.current;
1015
+ const detach = host ? draft.attachPad(host) : void 0;
915
1016
  return () => {
916
- if (viewportEl.get() === el) viewportEl.set(null);
917
- };
918
- }, [viewportEl]);
919
- const { onDecorateAnnotationChange } = ctx;
920
- useEffect(() => onDecorateAnnotationChange(() => engine.current?.redecorate()), [onDecorateAnnotationChange]);
921
- useEffect(() => {
922
- const built = builtWith.current;
923
- if (!engine.current || !built) return;
924
- if (built.gap === gap && built.buffer === buffer && built.custom === custom) return;
925
- buildCount.current += 1;
926
- builtWith.current = {
927
- gap,
928
- buffer,
929
- custom
1017
+ detach?.();
1018
+ draft.destroy();
930
1019
  };
931
- engine.current.rebuild({
932
- gap,
933
- buffer,
934
- customAnnotations: custom
935
- });
936
- }, [
937
- gap,
938
- buffer,
939
- custom
940
- ]);
941
- useEffect(() => {
942
- const vp = engine.current?.viewport;
943
- const z = ctx.zoomModel;
944
- if (vp && z !== void 0 && z !== vp.zoom.get()) vp.setZoom(z);
945
- }, [ctx.zoomModel]);
946
- useEffect(() => {
947
- const vp = engine.current?.viewport;
948
- const p = ctx.pageModel;
949
- if (!vp || p === void 0 || p === lastReportedPage.current) return;
950
- if (p !== vp.currentPage.get()) vp.scrollToPage(p);
951
- }, [ctx.pageModel]);
952
- useEffect(() => {
953
- const tc = engine.current?.tools;
954
- if (tc && tc.active.get() !== ctx.tool) tc.set(ctx.tool);
955
- }, [ctx.tool]);
956
- useEffect(() => {
957
- const vp = engine.current?.viewport;
958
- if (vp && vp.mode.get() !== mode) vp.setMode(mode);
959
- }, [mode]);
960
- useEffect(() => {
961
- const vp = engine.current?.viewport;
962
- if (vp && vp.rotation.get() !== rotation) vp.setRotation(rotation);
963
- }, [rotation]);
964
- const state = ctx.status.state;
965
- const err = ctx.status.state === "error" ? ctx.status.error : null;
966
- return /* @__PURE__ */ jsxs("div", {
967
- ...rest,
968
- className: cx("ink-viewport-root", className),
969
- "data-ink-viewport-root": "",
970
- "data-state": state,
971
- children: [
972
- state === "loading" ? /* @__PURE__ */ jsx("div", {
973
- className: "ink-status",
974
- role: "status",
975
- "aria-live": "polite",
976
- children: loading === void 0 ? "Loading document…" : loading
977
- }) : err ? /* @__PURE__ */ jsx("div", {
978
- className: "ink-status ink-error",
979
- role: "alert",
980
- children: typeof error === "function" ? error({ error: err }) : error !== void 0 ? error : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", {
981
- className: "ink-error-title",
982
- children: "This document could not be opened."
983
- }), /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("code", {
984
- className: "ink-error-code",
985
- title: err.message,
986
- children: err.code
987
- }) })] })
988
- }) : state === "idle" ? /* @__PURE__ */ jsx("div", {
989
- className: "ink-status ink-empty",
990
- children: empty
991
- }) : null,
992
- /* @__PURE__ */ jsx("div", {
993
- ref: scroll,
994
- className: "ink-scroll",
995
- "data-ink-scroll": "",
996
- tabIndex: 0,
997
- role: "document",
998
- "aria-label": "Document"
999
- }),
1000
- pages.map((p) => /* @__PURE__ */ jsx(InkPage, {
1001
- page: p.page,
1002
- host: p.host,
1003
- mount: p.mount,
1004
- error: p.error,
1005
- customAnnotations: p.custom,
1006
- renderAnnotation,
1007
- renderPageError
1008
- }, `${p.build}:${p.page}`))
1009
- ]
1010
- });
1011
- }
1012
- //#endregion
1013
- //#region src/components/InkToolbar.tsx
1014
- function InkToolbar({ label = "Document tools", asChild = false, className, children, ...rest }) {
1015
- return /* @__PURE__ */ jsx(Toolbar.Root, {
1016
- asChild,
1017
- orientation: "horizontal",
1018
- "aria-label": label,
1019
- "data-ink-toolbar": "",
1020
- className: cx("ink-toolbar", className),
1021
- ...rest,
1022
- 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
1023
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
+ ] });
1024
1171
  }
1025
1172
  //#endregion
1026
1173
  //#region src/components/InkIcon.tsx
@@ -1041,1014 +1188,1270 @@ function InkIcon({ icon, size = 18 }) {
1041
1188
  });
1042
1189
  }
1043
1190
  //#endregion
1044
- //#region src/components/InkToolButton.tsx
1045
- const ICONS = {
1046
- "select-text": TextCursor,
1047
- select: SquareDashedMousePointer,
1048
- highlight: Highlighter,
1049
- "redact-area": Square,
1050
- sketch: PenTool,
1051
- draw: PenLine,
1052
- note: MessageSquare,
1053
- freetext: Type,
1054
- signature: Signature
1055
- };
1056
- function InkToolButton({ tool, label, asChild = false, className, children, onClick, ...rest }) {
1057
- const { hostTools, pluginHost } = useInkContext("InkToolButton");
1058
- const { tool: current, setTool } = useTool();
1059
- const active = current === tool;
1060
- /** A host or plugin tool by this id, for the name and icon core has none for. */
1061
- const own = [...hostTools ?? [], ...pluginHost?.tools ?? []].find((t) => t.id === tool);
1062
- const name = label ?? TOOL_LABELS[tool] ?? own?.label ?? tool;
1063
- const icon = ICONS[tool] ?? own?.icon;
1064
- return /* @__PURE__ */ jsx(Toolbar.Button, {
1065
- asChild,
1066
- className: cx("ink-tool-button", className),
1067
- "data-ink-tool-button": "",
1068
- "data-tool": tool,
1069
- "data-state": active ? "on" : "off",
1070
- "aria-pressed": active,
1071
- "aria-label": name,
1072
- title: name,
1073
- onClick: (e) => {
1074
- onClick?.(e);
1075
- if (!e.defaultPrevented) setTool(tool);
1076
- },
1077
- ...rest,
1078
- children: typeof children === "function" ? children({
1079
- active,
1080
- label: name
1081
- }) : children ?? (icon ? /* @__PURE__ */ jsx(InkIcon, { icon }) : null)
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
+ })
1082
1203
  });
1083
- }
1084
- //#endregion
1085
- //#region src/components/InkCommand.tsx
1086
- function InkCommand({ id, asChild = false, children, disabled, onClick, ...rest }) {
1087
- const { run, enabled, busy, status, label, icon } = useCommand(id);
1088
- return /* @__PURE__ */ jsxs("div", {
1089
- className: "ink-command",
1090
- children: [/* @__PURE__ */ jsx(Toolbar.Button, {
1091
- asChild,
1092
- type: "button",
1093
- "data-command": id,
1094
- "aria-label": label || void 0,
1095
- title: label || void 0,
1096
- "aria-hidden": label ? void 0 : true,
1097
- "aria-busy": busy,
1098
- "aria-disabled": busy || void 0,
1099
- ...rest,
1100
- disabled: disabled === true || !enabled && !busy,
1101
- onClick: (e) => {
1102
- onClick?.(e);
1103
- if (!e.defaultPrevented) run();
1104
- },
1105
- children: typeof children === "function" ? children({
1106
- label,
1107
- busy
1108
- }) : children ?? (icon ? /* @__PURE__ */ jsx(InkIcon, { icon }) : label)
1109
- }), /* @__PURE__ */ jsx("span", {
1110
- className: "ink-command-status",
1111
- role: "status",
1112
- children: status
1113
- })]
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
+ })
1114
1216
  });
1115
1217
  }
1116
- //#endregion
1117
- //#region src/components/InkToolbarSeparator.tsx
1118
- /**
1119
- * A divider between groups of controls in a horizontal toolbar. Not focusable. A plain div
1120
- * rather than Radix's separator, which reads as horizontal inside a horizontal toolbar.
1121
- */
1122
- function InkToolbarSeparator({ className, ...rest }) {
1123
- return /* @__PURE__ */ jsx("div", {
1124
- role: "separator",
1125
- "aria-orientation": "vertical",
1126
- className: cx("ink-toolbar-separator", className),
1127
- "data-ink-toolbar-separator": "",
1128
- ...rest
1129
- });
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
+ ] });
1130
1295
  }
1131
1296
  //#endregion
1132
- //#region src/components/InkUndo.tsx
1133
- function InkUndo({ asChild = false, className, children, disabled, onClick, ...rest }) {
1134
- const { canUndo, undo } = useHistory();
1135
- return /* @__PURE__ */ jsx(Toolbar.Button, {
1136
- asChild,
1137
- className: cx("ink-undo", className),
1138
- "data-ink-undo": "",
1139
- "aria-label": "Undo",
1140
- title: "Undo",
1141
- ...rest,
1142
- disabled: disabled === true || !canUndo,
1143
- onClick: (e) => {
1144
- onClick?.(e);
1145
- if (!e.defaultPrevented) undo();
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);
1146
1389
  },
1147
- children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Undo2 })
1148
- });
1149
- }
1150
- //#endregion
1151
- //#region src/components/InkRedo.tsx
1152
- function InkRedo({ asChild = false, className, children, disabled, onClick, ...rest }) {
1153
- const { canRedo, redo } = useHistory();
1154
- return /* @__PURE__ */ jsx(Toolbar.Button, {
1155
- asChild,
1156
- className: cx("ink-redo", className),
1157
- "data-ink-redo": "",
1158
- "aria-label": "Redo",
1159
- title: "Redo",
1160
- ...rest,
1161
- disabled: disabled === true || !canRedo,
1162
- onClick: (e) => {
1163
- onClick?.(e);
1164
- if (!e.defaultPrevented) redo();
1390
+ select: false,
1391
+ onSave: (t) => {
1392
+ if (creatingFor) sign(creatingFor, t);
1165
1393
  },
1166
- children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Redo2 })
1167
- });
1394
+ onCloseAutoFocus: (e) => {
1395
+ e.preventDefault();
1396
+ returnFocus(last.current);
1397
+ }
1398
+ })] });
1168
1399
  }
1169
1400
  //#endregion
1170
- //#region src/components/InkSearchButton.tsx
1171
- /**
1172
- * Opens the find bar; pressed while it is open. Pressing it again focuses the field. Place it
1173
- * in a toolbar next to `InkFindBar`, which it needs for the field to focus. Disabled until a
1174
- * document is open.
1175
- */
1176
- function InkSearchButton({ asChild = false, className, children, disabled, onClick, ...rest }) {
1177
- const { rootEl } = useInkContext("InkSearchButton");
1178
- const { state, open } = useSearch();
1179
- const { isReady } = useDocument();
1180
- return /* @__PURE__ */ jsx(Toolbar.Button, {
1181
- ...rest,
1182
- asChild,
1183
- className: cx("ink-search-button", className),
1184
- "data-ink-search-button": "",
1185
- "aria-label": "Find",
1186
- title: "Find",
1187
- "aria-pressed": state.open,
1188
- "data-state": state.open ? "on" : "off",
1189
- disabled: !isReady || !!disabled,
1190
- onClick: (e) => {
1191
- onClick?.(e);
1192
- if (e.defaultPrevented) return;
1193
- flushSync(() => open());
1194
- const field = rootEl?.querySelector("[data-ink-find-bar] input");
1195
- field?.focus();
1196
- field?.select();
1197
- },
1198
- children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Search })
1199
- });
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;
1200
1413
  }
1201
- //#endregion
1202
- //#region src/components/InkFindBar.tsx
1203
- /**
1204
- * The find bar: a field, the match count, previous and next, a match-case toggle and close,
1205
- * a view of the root's search state. Place it anywhere inside `InkRoot`: it portals itself
1206
- * into `[data-ink-viewport-root]`, the positioned ancestor the theme floats it from over the
1207
- * top-right of the pages (as `InkThreadPopover` anchors to the root's overlay rather than
1208
- * where it is written). It renders nothing until `InkViewport` has mounted, and is hidden
1209
- * while the search is closed. While it is mounted it registers with the search controller,
1210
- * which is what lets Ctrl/Cmd+F open it.
1211
- *
1212
- * Enter in the field steps to the next match, Shift+Enter to the previous, Escape closes.
1213
- * Closing returns focus to where it was before the bar opened, else to the document.
1214
- */
1215
- function InkFindBar({ className, ...rest }) {
1216
- const ctx = useInkContext("InkFindBar");
1217
- const { state, setQuery, setMatchCase, next, previous, close } = useSearch();
1218
- const scroll = useReadable(ctx.viewportEl, null);
1219
- /** The viewport root: the scroll element's parent, once InkViewport has mounted. */
1220
- const target = scroll?.parentElement ?? null;
1221
- const bar = useRef(null);
1222
- const field = useRef(null);
1223
- const live = useRef(null);
1224
- /** Where focus was when the bar opened, and whether it was open on the last pass. */
1225
- const before = useRef(null);
1226
- const wasOpen = useRef(false);
1227
- useLayoutEffect(() => {
1228
- const el = bar.current;
1229
- const input = field.current;
1230
- if (!el || !input) return;
1231
- if (state.open !== wasOpen.current) {
1232
- wasOpen.current = state.open;
1233
- const doc = el.ownerDocument;
1234
- if (state.open) {
1235
- const active = doc.activeElement;
1236
- before.current = active instanceof HTMLElement && active !== doc.body && !el.contains(active) ? active : null;
1237
- input.focus();
1238
- input.select();
1239
- } else {
1240
- if (el.contains(doc.activeElement) || doc.activeElement === doc.body) (before.current?.isConnected && !el.contains(before.current) ? before.current : scroll)?.focus({ preventScroll: true });
1241
- before.current = null;
1242
- }
1243
- }
1244
- if (input.value !== state.query) {
1245
- input.value = state.query;
1246
- if (el.ownerDocument.activeElement === input) input.select();
1247
- }
1248
- }, [
1249
- state.open,
1250
- state.query,
1251
- scroll
1252
- ]);
1253
- const { search } = ctx;
1254
- useEffect(() => {
1255
- if (!search || !target) return;
1256
- return search.attachBar();
1257
- }, [search, target]);
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
1425
+ });
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);
1258
1444
  useEffect(() => {
1259
- const el = live.current;
1260
- if (!search || !el) return;
1261
- const announce = findAnnouncer();
1262
- const say = (s) => {
1263
- const text = announce(s);
1264
- if (el.textContent !== text) el.textContent = text;
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
1265
1458
  };
1266
- say(search.state.get());
1267
- return search.state.subscribe(say);
1268
- }, [search, target]);
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]);
1269
1562
  useEffect(() => {
1270
- const el = bar.current;
1271
- if (!search || !el) return;
1272
- const onKeyDown = (e) => {
1273
- if (e.key !== "Escape" || e.defaultPrevented || e.isComposing) return;
1274
- e.preventDefault();
1275
- e.stopPropagation();
1276
- search.close();
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
1277
1571
  };
1278
- el.addEventListener("keydown", onKeyDown);
1279
- return () => el.removeEventListener("keydown", onKeyDown);
1280
- }, [search, target]);
1281
- if (!target) return null;
1282
- return createPortal(/* @__PURE__ */ jsxs("div", {
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", {
1283
1611
  ...rest,
1284
- ref: bar,
1285
- className: cx("ink-find-bar", className),
1286
- "data-ink-find-bar": "",
1287
- role: "search",
1288
- hidden: !state.open,
1612
+ className: cx("ink-viewport-root", className),
1613
+ "data-ink-viewport-root": "",
1614
+ "data-state": state,
1289
1615
  children: [
1290
- /* @__PURE__ */ jsx("input", {
1291
- ref: field,
1292
- type: "text",
1293
- "aria-label": "Find in document",
1294
- autoComplete: "off",
1295
- spellCheck: false,
1296
- onInput: (e) => setQuery(e.currentTarget.value),
1297
- onKeyDown: (e) => {
1298
- if (e.key !== "Enter" || e.nativeEvent.isComposing) return;
1299
- e.preventDefault();
1300
- if (e.shiftKey) previous();
1301
- else next();
1302
- }
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"
1303
1643
  }),
1304
- /* @__PURE__ */ jsx("span", {
1305
- className: "ink-find-count",
1306
- "data-ink-find-count": "",
1307
- children: findCountText(state)
1308
- }),
1309
- /* @__PURE__ */ jsx("button", {
1310
- type: "button",
1311
- "data-ink-find-previous": "",
1312
- "aria-label": "Previous match",
1313
- title: "Previous match",
1314
- onClick: () => previous(),
1315
- children: /* @__PURE__ */ jsx(InkIcon, { icon: ChevronUp })
1316
- }),
1317
- /* @__PURE__ */ jsx("button", {
1318
- type: "button",
1319
- "data-ink-find-next": "",
1320
- "aria-label": "Next match",
1321
- title: "Next match",
1322
- onClick: () => next(),
1323
- children: /* @__PURE__ */ jsx(InkIcon, { icon: ChevronDown })
1324
- }),
1325
- /* @__PURE__ */ jsx("button", {
1326
- type: "button",
1327
- "data-ink-find-match-case": "",
1328
- "aria-label": "Match case",
1329
- title: "Match case",
1330
- "aria-pressed": state.matchCase,
1331
- onClick: () => setMatchCase(!state.matchCase),
1332
- children: /* @__PURE__ */ jsx(InkIcon, { icon: CaseSensitive })
1333
- }),
1334
- /* @__PURE__ */ jsx("button", {
1335
- type: "button",
1336
- "data-ink-find-close": "",
1337
- "aria-label": "Close find",
1338
- title: "Close find",
1339
- onClick: () => close(),
1340
- children: /* @__PURE__ */ jsx(InkIcon, { icon: X })
1341
- }),
1342
- /* @__PURE__ */ jsx("div", {
1343
- ref: live,
1344
- className: "ink-visually-hidden",
1345
- "data-ink-find-status": "",
1346
- "aria-live": "polite"
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)
1347
1662
  })
1348
1663
  ]
1349
- }), target);
1664
+ });
1350
1665
  }
1351
1666
  //#endregion
1352
- //#region src/components/InkZoomIn.tsx
1353
- function InkZoomIn({ asChild = false, className, children, onClick, ...rest }) {
1354
- const { zoomIn } = useZoom();
1355
- return /* @__PURE__ */ jsx(Toolbar.Button, {
1667
+ //#region src/components/InkToolbar.tsx
1668
+ function InkToolbar({ label = "Document tools", asChild = false, className, children, ...rest }) {
1669
+ return /* @__PURE__ */ jsx(Toolbar.Root, {
1356
1670
  asChild,
1357
- className: cx("ink-zoom-in", className),
1358
- "data-ink-zoom-in": "",
1359
- "aria-label": "Zoom in",
1360
- title: "Zoom in",
1671
+ orientation: "horizontal",
1672
+ "aria-label": label,
1673
+ "data-ink-toolbar": "",
1674
+ className: cx("ink-toolbar", className),
1361
1675
  ...rest,
1362
- onClick: (e) => {
1363
- onClick?.(e);
1364
- if (!e.defaultPrevented) zoomIn();
1365
- },
1366
- children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ZoomIn })
1676
+ children
1367
1677
  });
1368
1678
  }
1369
1679
  //#endregion
1370
- //#region src/components/InkZoomOut.tsx
1371
- function InkZoomOut({ asChild = false, className, children, onClick, ...rest }) {
1372
- const { zoomOut } = useZoom();
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;
1373
1700
  return /* @__PURE__ */ jsx(Toolbar.Button, {
1374
1701
  asChild,
1375
- className: cx("ink-zoom-out", className),
1376
- "data-ink-zoom-out": "",
1377
- "aria-label": "Zoom out",
1378
- title: "Zoom out",
1379
- ...rest,
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,
1380
1709
  onClick: (e) => {
1381
1710
  onClick?.(e);
1382
- if (!e.defaultPrevented) zoomOut();
1711
+ if (!e.defaultPrevented) setTool(tool);
1383
1712
  },
1384
- children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ZoomOut })
1713
+ ...rest,
1714
+ children: typeof children === "function" ? children({
1715
+ active,
1716
+ label: name
1717
+ }) : children ?? (icon ? /* @__PURE__ */ jsx(InkIcon, { icon }) : null)
1385
1718
  });
1386
1719
  }
1387
1720
  //#endregion
1388
- //#region src/components/InkZoomSelect.tsx
1389
- const CUSTOM = "custom:";
1390
- const level = (z) => ZOOM_LEVELS.find((l) => Math.abs(l - z) < .001);
1391
- const percent = (z) => `${Math.round(z * 100)}%`;
1392
- const FITS = [{
1393
- value: "fit-width",
1394
- label: "Fit width"
1395
- }, {
1396
- value: "fit-page",
1397
- label: "Fit page"
1398
- }];
1399
- function Item$1({ value, label }) {
1400
- return /* @__PURE__ */ jsxs(Select.Item, {
1401
- value,
1402
- className: "ink-select-item",
1403
- "data-ink-select-item": "",
1404
- children: [/* @__PURE__ */ jsx(Select.ItemText, { children: label }), /* @__PURE__ */ jsx(Select.ItemIndicator, {
1405
- className: "ink-select-indicator",
1406
- children: /* @__PURE__ */ jsx(InkIcon, {
1407
- icon: Check,
1408
- size: 16
1409
- })
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
1410
1749
  })]
1411
1750
  });
1412
1751
  }
1413
- function InkZoomSelect({ className, ...rest }) {
1414
- const { zoom, scale, setZoom } = useZoom();
1415
- /** A numeric zoom that is not one of the levels (zoom in and out multiply). */
1416
- const custom = typeof zoom === "number" && level(zoom) === void 0 ? zoom : null;
1417
- const value = typeof zoom !== "number" ? zoom : custom === null ? String(level(zoom)) : `${CUSTOM}${zoom}`;
1418
- /** The levels, with a custom zoom in its place among them. */
1419
- const levels = ZOOM_LEVELS.map((l) => ({
1420
- z: l,
1421
- value: String(l),
1422
- label: percent(l)
1423
- }));
1424
- if (custom !== null) levels.push({
1425
- z: custom,
1426
- value: `${CUSTOM}${custom}`,
1427
- label: percent(scale)
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
1428
1765
  });
1429
- levels.sort((a, b) => a.z - b.z);
1430
- function onValueChange(v) {
1431
- if (v === value) return;
1432
- const next = v === "fit-width" || v === "fit-page" ? v : Number(v.replace(CUSTOM, ""));
1433
- setZoom(next);
1434
- }
1435
- return /* @__PURE__ */ jsxs(Select.Root, {
1436
- value,
1437
- onValueChange,
1438
- children: [/* @__PURE__ */ jsxs(Select.Trigger, {
1439
- className: cx("ink-zoom-select ink-select-trigger", className),
1440
- "data-ink-zoom-select": "",
1441
- "aria-label": "Zoom",
1442
- ...rest,
1443
- children: [/* @__PURE__ */ jsx(Select.Value, { className: "ink-select-value" }), /* @__PURE__ */ jsx(Select.Icon, {
1444
- className: "ink-select-icon",
1445
- children: /* @__PURE__ */ jsx(InkIcon, {
1446
- icon: ChevronDown,
1447
- size: 16
1448
- })
1449
- })]
1450
- }), /* @__PURE__ */ jsx(Select.Portal, { children: /* @__PURE__ */ jsx(Select.Content, {
1451
- className: "ink-select-content",
1452
- "data-ink-select-content": "",
1453
- position: "popper",
1454
- side: "top",
1455
- sideOffset: 4,
1456
- children: /* @__PURE__ */ jsxs(Select.Viewport, { children: [
1457
- FITS.map((o) => /* @__PURE__ */ jsx(Item$1, {
1458
- value: o.value,
1459
- label: o.label
1460
- }, o.value)),
1461
- /* @__PURE__ */ jsx(Select.Separator, { className: "ink-select-separator" }),
1462
- levels.map((o) => /* @__PURE__ */ jsx(Item$1, {
1463
- value: o.value,
1464
- label: o.label
1465
- }, o.value))
1466
- ] })
1467
- }) })]
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 })
1468
1784
  });
1469
1785
  }
1470
1786
  //#endregion
1471
- //#region src/components/InkPageInput.tsx
1472
- function InkPageInput({ className, onKeyDown: onKeyDownProp, ...rest }) {
1473
- const { current, count, goTo } = usePage();
1474
- const [draft, setDraft] = useState(String(current + 1));
1475
- useEffect(() => {
1476
- setDraft(String(current + 1));
1477
- }, [current]);
1478
- const input = useRef(null);
1479
- const latest = useLatest({
1480
- current,
1481
- count,
1482
- goTo
1483
- });
1484
- /** 1-based in the input, 0-based in the viewport. Out of range resets to the current page. */
1485
- const commit = useCallback((value) => {
1486
- const { current: page, count: total, goTo: go } = latest.current;
1487
- const n = Number(value);
1488
- if (Number.isInteger(n) && n >= 1 && n <= total) {
1489
- if (n - 1 !== page) go(n - 1);
1490
- } else setDraft(String(page + 1));
1491
- }, [latest]);
1492
- useEffect(() => {
1493
- const el = input.current;
1494
- if (!el) return;
1495
- const onNativeChange = () => commit(el.value);
1496
- el.addEventListener("change", onNativeChange);
1497
- return () => el.removeEventListener("change", onNativeChange);
1498
- }, [commit]);
1499
- function onKeyDown(e) {
1500
- onKeyDownProp?.(e);
1501
- if (e.defaultPrevented || e.key !== "Enter") return;
1502
- e.preventDefault();
1503
- commit(draft);
1504
- }
1505
- return /* @__PURE__ */ jsx("input", {
1506
- ref: input,
1507
- className: cx("ink-page-input", className),
1508
- "data-ink-page-input": "",
1509
- type: "number",
1510
- inputMode: "numeric",
1511
- "aria-label": "Page",
1512
- min: 1,
1513
- max: count,
1514
- value: draft,
1515
- onChange: (e) => setDraft(e.target.value),
1516
- onKeyDown,
1517
- ...rest
1518
- });
1519
- }
1520
- //#endregion
1521
- //#region src/components/InkPageCount.tsx
1522
- function InkPageCount({ className, ...rest }) {
1523
- const { count } = usePage();
1524
- return /* @__PURE__ */ jsx("span", {
1525
- className: cx("ink-page-count", className),
1526
- "data-ink-page-count": "",
1527
- "aria-live": "polite",
1528
- ...rest,
1529
- children: `of ${count}`
1530
- });
1531
- }
1532
- //#endregion
1533
- //#region src/components/InkPrevPage.tsx
1534
- function InkPrevPage({ asChild = false, className, children, disabled, onClick, ...rest }) {
1535
- const { current, previous } = usePage();
1787
+ //#region src/components/InkRedo.tsx
1788
+ function InkRedo({ asChild = false, className, children, disabled, onClick, ...rest }) {
1789
+ const { canRedo, redo } = useHistory();
1536
1790
  return /* @__PURE__ */ jsx(Toolbar.Button, {
1537
1791
  asChild,
1538
- className: cx("ink-prev-page", className),
1539
- "data-ink-prev-page": "",
1540
- "aria-label": "Previous page",
1541
- title: "Previous page",
1542
- disabled: disabled === true || current <= 0,
1792
+ className: cx("ink-redo", className),
1793
+ "data-ink-redo": "",
1794
+ "aria-label": "Redo",
1795
+ title: "Redo",
1543
1796
  ...rest,
1797
+ disabled: disabled === true || !canRedo,
1544
1798
  onClick: (e) => {
1545
1799
  onClick?.(e);
1546
- if (!e.defaultPrevented) previous();
1800
+ if (!e.defaultPrevented) redo();
1547
1801
  },
1548
- children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ChevronLeft })
1802
+ children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Redo2 })
1549
1803
  });
1550
1804
  }
1551
1805
  //#endregion
1552
- //#region src/components/InkNextPage.tsx
1553
- function InkNextPage({ asChild = false, className, children, disabled, onClick, ...rest }) {
1554
- const { current, count, next } = usePage();
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();
1555
1816
  return /* @__PURE__ */ jsx(Toolbar.Button, {
1556
- asChild,
1557
- className: cx("ink-next-page", className),
1558
- "data-ink-next-page": "",
1559
- "aria-label": "Next page",
1560
- title: "Next page",
1561
- disabled: disabled === true || current >= count - 1,
1562
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,
1563
1826
  onClick: (e) => {
1564
1827
  onClick?.(e);
1565
- if (!e.defaultPrevented) next();
1828
+ if (e.defaultPrevented) return;
1829
+ flushSync(() => open());
1830
+ const field = rootEl?.querySelector("[data-ink-find-bar] input");
1831
+ field?.focus();
1832
+ field?.select();
1566
1833
  },
1567
- children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ChevronRight })
1834
+ children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: Search })
1568
1835
  });
1569
1836
  }
1570
1837
  //#endregion
1571
- //#region src/components/InkColorMenu.tsx
1572
- function InkColorMenu({ property, label, className, children, standalone = false, group, fallback, disabled, ...rest }) {
1573
- const Wrap = standalone ? Fragment : Toolbar.Button;
1574
- const { current, supported, set } = useToolStyle(group);
1575
- const [open, setOpen] = useState(false);
1576
- const model = colorMenuModel(property, current, supported, fallback);
1577
- const name = label ?? model.label;
1578
- /** ArrowDown opens on the first item; a click opens on the checked one. */
1579
- const focusFirst = useRef(false);
1580
- const input = useRef(null);
1581
- const apply = useRef((_hex) => {});
1582
- apply.current = (hex) => set(model.custom(hex));
1583
- const bindInput = useCallback((el) => {
1584
- input.current = el;
1585
- el?.addEventListener("change", () => apply.current(el.value));
1586
- }, []);
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;
1587
1890
  useEffect(() => {
1588
- if (input.current) input.current.value = model.customValue.toLowerCase();
1589
- }, [model.customValue]);
1891
+ if (!search || !target) return;
1892
+ return search.attachBar();
1893
+ }, [search, target]);
1590
1894
  useEffect(() => {
1591
- if (!model.enabled) setOpen(false);
1592
- }, [model.enabled]);
1593
- const trigger = model.trigger;
1594
- const swatchStyle = trigger.kind === "color" ? { "--ink-swatch": trigger.color } : void 0;
1595
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Popover.Root, {
1596
- open,
1597
- onOpenChange: setOpen,
1598
- children: [/* @__PURE__ */ jsx(Wrap, {
1599
- ...standalone ? {} : { asChild: true },
1600
- children: /* @__PURE__ */ jsx(Popover.Trigger, {
1601
- asChild: true,
1602
- children: /* @__PURE__ */ jsx("button", {
1603
- type: "button",
1604
- className: cx("ink-menu-trigger ink-color-menu", className),
1605
- "data-ink-color-menu": property,
1606
- "data-state": open ? "open" : "closed",
1607
- "data-mixed": model.mixed ? "" : void 0,
1608
- "aria-label": name,
1609
- title: name,
1610
- "aria-haspopup": "menu",
1611
- disabled: disabled === true || !model.enabled,
1612
- ...rest,
1613
- onKeyDown: (e) => {
1614
- rest.onKeyDown?.(e);
1615
- if (e.key !== "ArrowDown" || e.defaultPrevented || open || !model.enabled) return;
1616
- e.preventDefault();
1617
- focusFirst.current = true;
1618
- setOpen(true);
1619
- },
1620
- children: children ?? /* @__PURE__ */ jsx("span", {
1621
- className: "ink-swatch",
1622
- "data-ink-swatch": "",
1623
- "data-property": property,
1624
- "data-kind": trigger.kind,
1625
- "data-color": trigger.kind === "color" ? trigger.color : void 0,
1626
- style: swatchStyle,
1627
- "aria-hidden": "true"
1628
- })
1629
- })
1630
- })
1631
- }), /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsxs(Popover.Content, {
1632
- role: "menu",
1633
- "aria-label": name,
1634
- className: "ink-select-content ink-menu-content",
1635
- "data-ink-select-content": "",
1636
- "data-ink-menu": `color-${property}`,
1637
- side: "bottom",
1638
- align: "start",
1639
- sideOffset: 4,
1640
- collisionPadding: 8,
1641
- onOpenAutoFocus: (e) => {
1642
- e.preventDefault();
1643
- const menu = e.currentTarget;
1644
- if (focusFirst.current) menuItems(menu)[0]?.focus({ preventScroll: true });
1645
- else focusInitialMenuItem(menu);
1646
- focusFirst.current = false;
1647
- },
1648
- onKeyDown: (e) => handleMenuKeys(e.nativeEvent, e.currentTarget),
1649
- children: [
1650
- model.noFill ? /* @__PURE__ */ jsxs("button", {
1651
- type: "button",
1652
- role: "menuitemradio",
1653
- "aria-checked": model.noFill.checked,
1654
- tabIndex: -1,
1655
- className: "ink-select-item ink-menu-item",
1656
- "data-ink-select-item": "",
1657
- "data-state": model.noFill.checked ? "checked" : "unchecked",
1658
- onClick: () => set(model.noFill.patch),
1659
- children: ["No fill", model.noFill.checked ? /* @__PURE__ */ jsx("span", {
1660
- className: "ink-select-indicator",
1661
- children: /* @__PURE__ */ jsx(InkIcon, {
1662
- icon: Check,
1663
- size: 16
1664
- })
1665
- }) : null]
1666
- }) : null,
1667
- /* @__PURE__ */ jsx("div", {
1668
- role: "group",
1669
- "aria-label": "Colours",
1670
- className: "ink-color-grid",
1671
- "data-ink-menu-columns": COLOR_COLUMNS,
1672
- children: model.swatches.map((sw) => /* @__PURE__ */ jsx("button", {
1673
- type: "button",
1674
- role: "menuitemradio",
1675
- "aria-label": sw.label,
1676
- "aria-checked": sw.checked,
1677
- tabIndex: -1,
1678
- className: "ink-swatch-item",
1679
- "data-state": sw.checked ? "checked" : "unchecked",
1680
- style: { "--ink-swatch": sw.color },
1681
- onClick: () => set(sw.patch)
1682
- }, sw.key))
1683
- }),
1684
- /* @__PURE__ */ jsx("button", {
1685
- type: "button",
1686
- role: "menuitem",
1687
- tabIndex: -1,
1688
- className: "ink-select-item ink-menu-item",
1689
- "data-ink-select-item": "",
1690
- onClick: () => input.current?.click(),
1691
- children: "Custom…"
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
+ }
2049
+ function InkZoomSelect({ className, ...rest }) {
2050
+ const { zoom, scale, setZoom } = useZoom();
2051
+ /** A numeric zoom that is not one of the levels (zoom in and out multiply). */
2052
+ const custom = typeof zoom === "number" && level(zoom) === void 0 ? zoom : null;
2053
+ const value = typeof zoom !== "number" ? zoom : custom === null ? String(level(zoom)) : `${CUSTOM}${zoom}`;
2054
+ /** The levels, with a custom zoom in its place among them. */
2055
+ const levels = ZOOM_LEVELS.map((l) => ({
2056
+ z: l,
2057
+ value: String(l),
2058
+ label: percent(l)
2059
+ }));
2060
+ if (custom !== null) levels.push({
2061
+ z: custom,
2062
+ value: `${CUSTOM}${custom}`,
2063
+ label: percent(scale)
2064
+ });
2065
+ levels.sort((a, b) => a.z - b.z);
2066
+ function onValueChange(v) {
2067
+ if (v === value) return;
2068
+ const next = v === "fit-width" || v === "fit-page" ? v : Number(v.replace(CUSTOM, ""));
2069
+ setZoom(next);
2070
+ }
2071
+ return /* @__PURE__ */ jsxs(Select.Root, {
2072
+ value,
2073
+ onValueChange,
2074
+ children: [/* @__PURE__ */ jsxs(Select.Trigger, {
2075
+ className: cx("ink-zoom-select ink-select-trigger", className),
2076
+ "data-ink-zoom-select": "",
2077
+ "aria-label": "Zoom",
2078
+ ...rest,
2079
+ children: [/* @__PURE__ */ jsx(Select.Value, { className: "ink-select-value" }), /* @__PURE__ */ jsx(Select.Icon, {
2080
+ className: "ink-select-icon",
2081
+ children: /* @__PURE__ */ jsx(InkIcon, {
2082
+ icon: ChevronDown,
2083
+ size: 16
1692
2084
  })
1693
- ]
2085
+ })]
2086
+ }), /* @__PURE__ */ jsx(Select.Portal, { children: /* @__PURE__ */ jsx(Select.Content, {
2087
+ className: "ink-select-content",
2088
+ "data-ink-select-content": "",
2089
+ position: "popper",
2090
+ side: "top",
2091
+ sideOffset: 4,
2092
+ children: /* @__PURE__ */ jsxs(Select.Viewport, { children: [
2093
+ FITS.map((o) => /* @__PURE__ */ jsx(Item$1, {
2094
+ value: o.value,
2095
+ label: o.label
2096
+ }, o.value)),
2097
+ /* @__PURE__ */ jsx(Select.Separator, { className: "ink-select-separator" }),
2098
+ levels.map((o) => /* @__PURE__ */ jsx(Item$1, {
2099
+ value: o.value,
2100
+ label: o.label
2101
+ }, o.value))
2102
+ ] })
1694
2103
  }) })]
1695
- }), /* @__PURE__ */ jsx("input", {
1696
- ref: bindInput,
1697
- type: "color",
1698
- className: "ink-color-input",
1699
- "aria-label": "Custom colour",
1700
- "data-ink-color-input": property,
1701
- tabIndex: -1,
1702
- defaultValue: model.customValue.toLowerCase()
1703
- })] });
2104
+ });
1704
2105
  }
1705
2106
  //#endregion
1706
- //#region src/components/InkShapeMenu.tsx
1707
- function InkShapeMenu({ className, children, ...rest }) {
1708
- const { tool, setTool } = useTool();
1709
- const [options, setOptions] = useToolOptions("shape");
1710
- const [open, setOpen] = useState(false);
1711
- const model = shapeMenuModel(options, SHAPE_KINDS);
1712
- const active = tool === "shape";
1713
- /** ArrowDown opens on the first item; a click opens on the checked one. */
1714
- const focusFirst = useRef(false);
1715
- return /* @__PURE__ */ jsx(Popover.Root, {
1716
- open,
1717
- onOpenChange: setOpen,
1718
- children: /* @__PURE__ */ jsx(Popover.Anchor, {
1719
- asChild: true,
1720
- children: /* @__PURE__ */ jsxs("div", {
1721
- role: "group",
1722
- "aria-label": "Shape",
1723
- className: cx("ink-split-button ink-shape-menu", className),
1724
- "data-ink-shape-menu": "",
1725
- ...rest,
1726
- children: [
1727
- /* @__PURE__ */ jsx(Toolbar.Button, {
1728
- className: "ink-tool-button ink-shape-button",
1729
- "data-ink-tool-button": "",
1730
- "data-ink-shape-button": "",
1731
- "data-tool": "shape",
1732
- "data-kind": model.kind,
1733
- "data-state": active ? "on" : "off",
1734
- "aria-pressed": active,
1735
- "aria-label": model.label,
1736
- title: model.label,
1737
- onClick: () => setTool("shape"),
1738
- children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: SHAPE_ICONS[model.kind] })
1739
- }),
1740
- /* @__PURE__ */ jsx(Toolbar.Button, {
1741
- asChild: true,
1742
- children: /* @__PURE__ */ jsx(Popover.Trigger, {
1743
- asChild: true,
1744
- children: /* @__PURE__ */ jsx("button", {
1745
- type: "button",
1746
- className: "ink-menu-trigger ink-shape-menu-trigger",
1747
- "data-ink-shape-menu-trigger": "",
1748
- "data-state": open ? "open" : "closed",
1749
- "aria-label": "Choose shape",
1750
- title: "Choose shape",
1751
- "aria-haspopup": "menu",
1752
- onKeyDown: (e) => {
1753
- if (e.key !== "ArrowDown" || e.defaultPrevented || open) return;
1754
- e.preventDefault();
1755
- focusFirst.current = true;
1756
- setOpen(true);
1757
- },
1758
- children: /* @__PURE__ */ jsx(InkIcon, { icon: ChevronDown })
1759
- })
1760
- })
1761
- }),
1762
- /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Content, {
1763
- role: "menu",
1764
- "aria-label": "Shapes",
1765
- className: "ink-select-content ink-menu-content",
1766
- "data-ink-select-content": "",
1767
- "data-ink-menu": "shape",
1768
- side: "bottom",
1769
- align: "start",
1770
- sideOffset: 4,
1771
- collisionPadding: 8,
1772
- onOpenAutoFocus: (e) => {
1773
- e.preventDefault();
1774
- const menu = e.currentTarget;
1775
- if (focusFirst.current) menuItems(menu)[0]?.focus({ preventScroll: true });
1776
- else focusInitialMenuItem(menu);
1777
- focusFirst.current = false;
1778
- },
1779
- onKeyDown: (e) => handleMenuKeys(e.nativeEvent, e.currentTarget),
1780
- children: /* @__PURE__ */ jsx("div", {
1781
- role: "group",
1782
- "aria-label": "Shapes",
1783
- className: "ink-shape-grid",
1784
- "data-ink-menu-columns": SHAPE_COLUMNS,
1785
- children: model.items.map((item) => /* @__PURE__ */ jsxs("button", {
1786
- type: "button",
1787
- role: "menuitemradio",
1788
- "aria-checked": item.checked,
1789
- tabIndex: -1,
1790
- className: "ink-select-item ink-menu-item ink-shape-item",
1791
- "data-ink-select-item": "",
1792
- "data-key": item.kind,
1793
- "data-state": item.checked ? "checked" : "unchecked",
1794
- onClick: () => {
1795
- setOptions({ kind: item.kind });
1796
- setTool("shape");
1797
- setOpen(false);
1798
- },
1799
- children: [/* @__PURE__ */ jsx(InkIcon, {
1800
- icon: SHAPE_ICONS[item.kind],
1801
- size: 16
1802
- }), item.label]
1803
- }, item.kind))
1804
- })
1805
- }) })
1806
- ]
1807
- })
1808
- })
2107
+ //#region src/components/InkPageInput.tsx
2108
+ function InkPageInput({ className, onKeyDown: onKeyDownProp, ...rest }) {
2109
+ const { current, count, goTo } = usePage();
2110
+ const [draft, setDraft] = useState(String(current + 1));
2111
+ useEffect(() => {
2112
+ setDraft(String(current + 1));
2113
+ }, [current]);
2114
+ const input = useRef(null);
2115
+ const latest = useLatest({
2116
+ current,
2117
+ count,
2118
+ goTo
2119
+ });
2120
+ /** 1-based in the input, 0-based in the viewport. Out of range resets to the current page. */
2121
+ const commit = useCallback((value) => {
2122
+ const { current: page, count: total, goTo: go } = latest.current;
2123
+ const n = Number(value);
2124
+ if (Number.isInteger(n) && n >= 1 && n <= total) {
2125
+ if (n - 1 !== page) go(n - 1);
2126
+ } else setDraft(String(page + 1));
2127
+ }, [latest]);
2128
+ useEffect(() => {
2129
+ const el = input.current;
2130
+ if (!el) return;
2131
+ const onNativeChange = () => commit(el.value);
2132
+ el.addEventListener("change", onNativeChange);
2133
+ return () => el.removeEventListener("change", onNativeChange);
2134
+ }, [commit]);
2135
+ function onKeyDown(e) {
2136
+ onKeyDownProp?.(e);
2137
+ if (e.defaultPrevented || e.key !== "Enter") return;
2138
+ e.preventDefault();
2139
+ commit(draft);
2140
+ }
2141
+ return /* @__PURE__ */ jsx("input", {
2142
+ ref: input,
2143
+ className: cx("ink-page-input", className),
2144
+ "data-ink-page-input": "",
2145
+ type: "number",
2146
+ inputMode: "numeric",
2147
+ "aria-label": "Page",
2148
+ min: 1,
2149
+ max: count,
2150
+ value: draft,
2151
+ onChange: (e) => setDraft(e.target.value),
2152
+ onKeyDown,
2153
+ ...rest
2154
+ });
2155
+ }
2156
+ //#endregion
2157
+ //#region src/components/InkPageCount.tsx
2158
+ function InkPageCount({ className, ...rest }) {
2159
+ const { count } = usePage();
2160
+ return /* @__PURE__ */ jsx("span", {
2161
+ className: cx("ink-page-count", className),
2162
+ "data-ink-page-count": "",
2163
+ "aria-live": "polite",
2164
+ ...rest,
2165
+ children: `of ${count}`
2166
+ });
2167
+ }
2168
+ //#endregion
2169
+ //#region src/components/InkPrevPage.tsx
2170
+ function InkPrevPage({ asChild = false, className, children, disabled, onClick, ...rest }) {
2171
+ const { current, previous } = usePage();
2172
+ return /* @__PURE__ */ jsx(Toolbar.Button, {
2173
+ asChild,
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,
2179
+ ...rest,
2180
+ onClick: (e) => {
2181
+ onClick?.(e);
2182
+ if (!e.defaultPrevented) previous();
2183
+ },
2184
+ children: children ?? /* @__PURE__ */ jsx(InkIcon, { icon: ChevronLeft })
1809
2185
  });
1810
2186
  }
1811
2187
  //#endregion
1812
- //#region src/components/InkSignatureDialog.tsx
1813
- function InkSignatureDialog({ open, onOpenChange, fonts, onSave, onCloseAutoFocus, className }) {
1814
- return /* @__PURE__ */ jsx(Dialog.Root, {
1815
- open,
1816
- onOpenChange,
1817
- children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [/* @__PURE__ */ jsx(Dialog.Overlay, {
1818
- className: "ink-dialog-overlay",
1819
- "data-ink-dialog-overlay": ""
1820
- }), /* @__PURE__ */ jsxs(Dialog.Content, {
1821
- className: cx("ink-dialog ink-signature-dialog", className),
1822
- "data-ink-signature-dialog": "",
1823
- "aria-modal": "true",
1824
- "aria-describedby": void 0,
1825
- onOpenAutoFocus: (e) => {
1826
- e.preventDefault();
1827
- e.currentTarget.querySelector("[role=\"tab\"][aria-selected=\"true\"]")?.focus({ preventScroll: true });
1828
- },
1829
- onCloseAutoFocus,
1830
- children: [/* @__PURE__ */ jsx(Dialog.Title, {
1831
- className: "ink-dialog-title",
1832
- children: SIGNATURE_LABELS.dialog
1833
- }), /* @__PURE__ */ jsx(SignatureForm, {
1834
- fonts,
1835
- onSave,
1836
- onClose: () => onOpenChange(false)
1837
- })]
1838
- })] })
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 })
1839
2204
  });
1840
2205
  }
1841
- const IDLE = {
1842
- tab: "draw",
1843
- padEmpty: true,
1844
- name: "",
1845
- font: "",
1846
- image: void 0,
1847
- imageError: void 0,
1848
- saveError: void 0,
1849
- canSave: false,
1850
- saving: false
1851
- };
1852
- /** The dialog's body, mounted while it is open: one draft per opening. */
1853
- function SignatureForm({ fonts, onSave, onClose }) {
1854
- const ctx = useInkContext("InkSignatureDialog");
1855
- const { setTool } = useTool();
1856
- const [, setOptions] = useToolOptions("signature");
1857
- const [draft] = useState(() => createSignatureDraft({ fonts: fonts ?? ctx.signatureFonts }));
1858
- const s = useReadable(draft.state, IDLE);
1859
- const uid = useId();
1860
- 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
+ }, []);
1861
2223
  useEffect(() => {
1862
- const host = padHost.current;
1863
- const detach = host ? draft.attachPad(host) : void 0;
1864
- return () => {
1865
- detach?.();
1866
- draft.destroy();
1867
- };
1868
- }, [draft]);
1869
- const save = () => {
1870
- draft.save(ctx.signatures).then((t) => {
1871
- if (!t) return;
1872
- setOptions({ templateId: t.id });
1873
- setTool("signature");
1874
- onSave?.(t);
1875
- onClose();
1876
- }, () => {});
1877
- };
1878
- const tabId = (tab) => `${uid}-tab-${tab}`;
1879
- const panelId = (tab) => `${uid}-panel-${tab}`;
1880
- const panel = (tab) => ({
1881
- role: "tabpanel",
1882
- id: panelId(tab),
1883
- "aria-labelledby": tabId(tab),
1884
- className: "ink-tab-panel",
1885
- "data-key": tab,
1886
- hidden: s.tab !== tab
1887
- });
1888
- const shown = s.name.trim() || SIGNATURE_LABELS.namePlaceholder;
1889
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [
1890
- /* @__PURE__ */ jsx("div", {
1891
- role: "tablist",
1892
- "aria-label": SIGNATURE_LABELS.tabs,
1893
- className: "ink-tabs",
1894
- onKeyDown: (e) => handleTabKeys(e.nativeEvent, e.currentTarget),
1895
- children: SIGNATURE_TABS.map((tab) => /* @__PURE__ */ jsx("button", {
1896
- type: "button",
1897
- role: "tab",
1898
- id: tabId(tab),
1899
- "aria-controls": panelId(tab),
1900
- "aria-selected": s.tab === tab,
1901
- tabIndex: s.tab === tab ? 0 : -1,
1902
- className: "ink-tab",
1903
- "data-key": tab,
1904
- "data-state": s.tab === tab ? "active" : "inactive",
1905
- onClick: () => draft.setTab(tab),
1906
- children: SIGNATURE_LABELS[tab]
1907
- }, tab))
1908
- }),
1909
- /* @__PURE__ */ jsxs("div", {
1910
- ...panel("draw"),
1911
- children: [/* @__PURE__ */ jsx("div", {
1912
- ref: padHost,
1913
- className: "ink-signature-pad-host",
1914
- "data-ink-signature-pad-host": "",
1915
- role: "img",
1916
- "aria-label": SIGNATURE_LABELS.pad
1917
- }), /* @__PURE__ */ jsx("div", {
1918
- 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,
1919
2238
  children: /* @__PURE__ */ jsx("button", {
1920
2239
  type: "button",
1921
- className: "ink-button",
1922
- "data-ink-signature-clear": "",
1923
- disabled: s.padEmpty,
1924
- onClick: () => draft.clearPad(),
1925
- children: SIGNATURE_LABELS.clear
1926
- })
1927
- })]
1928
- }),
1929
- /* @__PURE__ */ jsxs("div", {
1930
- ...panel("type"),
1931
- children: [/* @__PURE__ */ jsxs("label", {
1932
- className: "ink-field",
1933
- children: [/* @__PURE__ */ jsx("span", { children: SIGNATURE_LABELS.name }), /* @__PURE__ */ jsx("input", {
1934
- type: "text",
1935
- className: "ink-input",
1936
- "data-ink-signature-name": "",
1937
- autoComplete: "name",
1938
- placeholder: SIGNATURE_LABELS.namePlaceholder,
1939
- value: s.name,
1940
- onChange: (e) => draft.setName(e.target.value)
1941
- })]
1942
- }), /* @__PURE__ */ jsx("div", {
1943
- role: "radiogroup",
1944
- "aria-label": SIGNATURE_LABELS.fonts,
1945
- className: "ink-signature-fonts",
1946
- onKeyDown: (e) => handleRadioKeys(e.nativeEvent, e.currentTarget),
1947
- children: draft.fonts.map((font) => /* @__PURE__ */ jsx("button", {
1948
- type: "button",
1949
- role: "radio",
1950
- "aria-checked": s.font === font,
1951
- "aria-label": font,
1952
- tabIndex: s.font === font ? 0 : -1,
1953
- className: "ink-signature-font",
1954
- "data-key": font,
1955
- "data-state": s.font === font ? "checked" : "unchecked",
1956
- onClick: () => draft.setFont(font),
1957
- children: /* @__PURE__ */ jsx("span", {
1958
- className: "ink-signature-font-sample",
1959
- style: { fontFamily: fontFamilyValue(font) },
1960
- 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"
1961
2264
  })
1962
- }, font))
1963
- })]
1964
- }),
1965
- /* @__PURE__ */ jsxs("div", {
1966
- ...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),
1967
2285
  children: [
1968
- /* @__PURE__ */ jsxs("label", {
1969
- className: "ink-field",
1970
- children: [/* @__PURE__ */ jsx("span", { children: SIGNATURE_LABELS.file }), /* @__PURE__ */ jsx("input", {
1971
- type: "file",
1972
- accept: "image/png,image/jpeg",
1973
- className: "ink-input",
1974
- "data-ink-signature-file": "",
1975
- onChange: (e) => void draft.chooseFile(e.target.files?.[0])
1976
- })]
1977
- }),
1978
- s.image?.kind === "image" ? /* @__PURE__ */ jsx("img", {
1979
- className: "ink-signature-image",
1980
- src: s.image.imagedata,
1981
- 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]
1982
2302
  }) : null,
1983
- s.imageError ? /* @__PURE__ */ jsx("p", {
1984
- className: "ink-field-error",
1985
- role: "alert",
1986
- children: s.imageError
1987
- }) : null
1988
- ]
1989
- }),
1990
- s.saveError ? /* @__PURE__ */ jsx("p", {
1991
- className: "ink-field-error",
1992
- role: "alert",
1993
- "data-ink-signature-save-error": "",
1994
- children: s.saveError
1995
- }) : null,
1996
- /* @__PURE__ */ jsxs("div", {
1997
- className: "ink-dialog-actions",
1998
- children: [/* @__PURE__ */ jsx(Dialog.Close, {
1999
- asChild: true,
2000
- 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", {
2001
2321
  type: "button",
2002
- className: "ink-button",
2003
- "data-ink-signature-cancel": "",
2004
- 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…"
2005
2328
  })
2006
- }), /* @__PURE__ */ jsx("button", {
2007
- type: "button",
2008
- className: "ink-button",
2009
- "data-variant": "primary",
2010
- "data-ink-signature-save": "",
2011
- disabled: !s.canSave,
2012
- onClick: save,
2013
- children: SIGNATURE_LABELS.save
2014
- })]
2015
- })
2016
- ] });
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
+ })] });
2017
2340
  }
2018
2341
  //#endregion
2019
- //#region src/components/InkSignatureMenu.tsx
2020
- const NO_TEMPLATES = [];
2021
- /** A saved signature drawn small: ink as filled outlines, an image as itself. */
2022
- function SignaturePreview({ template }) {
2023
- if (template.kind === "image") return /* @__PURE__ */ jsx("span", {
2024
- className: "ink-signature-preview",
2025
- "data-kind": "image",
2026
- children: /* @__PURE__ */ jsx("img", {
2027
- src: template.imagedata,
2028
- alt: "",
2029
- draggable: false
2030
- })
2031
- });
2032
- const { viewBox, paths } = signaturePreview(template);
2033
- return /* @__PURE__ */ jsx("span", {
2034
- className: "ink-signature-preview",
2035
- "data-kind": "ink",
2036
- children: /* @__PURE__ */ jsx("svg", {
2037
- viewBox,
2038
- "aria-hidden": "true",
2039
- children: paths.map((d, i) => /* @__PURE__ */ jsx("path", {
2040
- d,
2041
- fill: "currentColor"
2042
- }, 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
+ })
2043
2444
  })
2044
2445
  });
2045
2446
  }
2447
+ //#endregion
2448
+ //#region src/components/InkSignatureMenu.tsx
2449
+ const NO_TEMPLATES = [];
2046
2450
  function InkSignatureMenu({ className, children, fonts, ...rest }) {
2047
2451
  const { tool, setTool } = useTool();
2048
2452
  const [options, setOptions] = useToolOptions("signature");
2049
2453
  const { signatures } = useInkContext("InkSignatureMenu");
2050
2454
  const templates = useReadable(signatures.templates, NO_TEMPLATES);
2051
- const removeError = useReadable(signatures.removeError, void 0);
2052
2455
  const model = signatureMenuModel(templates, options);
2053
2456
  const [open, setOpen] = useState(false);
2054
2457
  const [creating, setCreating] = useState(false);
@@ -2059,25 +2462,9 @@ function InkSignatureMenu({ className, children, fonts, ...rest }) {
2059
2462
  const focusFirst = useRef(false);
2060
2463
  /** Set while the menu closes for the dialog, which takes focus instead of the trigger. */
2061
2464
  const toDialog = useRef(false);
2062
- /** The row of a removed signature, until focus lands on what took its place. */
2063
- const removedRow = useRef(null);
2064
2465
  useEffect(() => {
2065
2466
  if (open) signatures.clearRemoveError();
2066
2467
  }, [open, signatures]);
2067
- useEffect(() => {
2068
- const row = removedRow.current;
2069
- const panel = menu.current;
2070
- if (row === null || !panel) return;
2071
- removedRow.current = null;
2072
- const rows = panel.querySelectorAll(".ink-signature-item");
2073
- (rows[Math.min(row, rows.length - 1)] ?? panel.querySelector("[data-ink-signature-create]"))?.focus({ preventScroll: true });
2074
- }, [templates]);
2075
- const remove = (id, row) => {
2076
- removedRow.current = row;
2077
- signatures.remove(id).catch(() => {
2078
- removedRow.current = null;
2079
- });
2080
- };
2081
2468
  return /* @__PURE__ */ jsx(Popover.Root, {
2082
2469
  open,
2083
2470
  onOpenChange: setOpen,
@@ -2128,7 +2515,7 @@ function InkSignatureMenu({ className, children, fonts, ...rest }) {
2128
2515
  })
2129
2516
  })
2130
2517
  }),
2131
- /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsxs(Popover.Content, {
2518
+ /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Content, {
2132
2519
  ref: menu,
2133
2520
  role: "menu",
2134
2521
  "aria-label": SIGNATURE_LABELS.menu,
@@ -2151,71 +2538,20 @@ function InkSignatureMenu({ className, children, fonts, ...rest }) {
2151
2538
  toDialog.current = false;
2152
2539
  },
2153
2540
  onKeyDown: (e) => handleMenuKeys(e.nativeEvent, e.currentTarget),
2154
- children: [
2155
- model.items.length > 0 ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
2156
- role: "group",
2157
- "aria-label": SIGNATURE_LABELS.saved,
2158
- className: "ink-signature-list",
2159
- "data-ink-menu-columns": 2,
2160
- children: model.items.map((item, row) => [/* @__PURE__ */ jsx("button", {
2161
- type: "button",
2162
- role: "menuitemradio",
2163
- "aria-checked": item.checked,
2164
- "aria-label": item.label,
2165
- tabIndex: -1,
2166
- className: "ink-select-item ink-menu-item ink-signature-item",
2167
- "data-ink-select-item": "",
2168
- "data-key": item.id,
2169
- "data-state": item.checked ? "checked" : "unchecked",
2170
- onClick: () => {
2171
- setOptions({ templateId: item.id });
2172
- setTool("signature");
2173
- setOpen(false);
2174
- },
2175
- children: /* @__PURE__ */ jsx(SignaturePreview, { template: item.template })
2176
- }, `${item.id}-choose`), /* @__PURE__ */ jsx("button", {
2177
- type: "button",
2178
- role: "menuitem",
2179
- "aria-label": item.removeLabel,
2180
- title: item.removeLabel,
2181
- tabIndex: -1,
2182
- className: "ink-menu-item ink-signature-remove",
2183
- "data-ink-signature-remove": "",
2184
- "data-key": item.id,
2185
- onClick: () => remove(item.id, row),
2186
- children: /* @__PURE__ */ jsx(InkIcon, {
2187
- icon: Trash2,
2188
- size: 16
2189
- })
2190
- }, `${item.id}-remove`)])
2191
- }), /* @__PURE__ */ jsx("div", {
2192
- role: "separator",
2193
- className: "ink-select-separator"
2194
- })] }) : null,
2195
- /* @__PURE__ */ jsxs("button", {
2196
- type: "button",
2197
- role: "menuitem",
2198
- tabIndex: -1,
2199
- className: "ink-select-item ink-menu-item ink-signature-create",
2200
- "data-ink-select-item": "",
2201
- "data-ink-signature-create": "",
2202
- onClick: () => {
2203
- toDialog.current = true;
2204
- setOpen(false);
2205
- setCreating(true);
2206
- },
2207
- children: [/* @__PURE__ */ jsx(InkIcon, {
2208
- icon: Plus,
2209
- size: 16
2210
- }), SIGNATURE_LABELS.create]
2211
- }),
2212
- removeError ? /* @__PURE__ */ jsx("p", {
2213
- className: "ink-field-error ink-signature-remove-error",
2214
- role: "alert",
2215
- "data-ink-signature-remove-error": "",
2216
- children: removeError
2217
- }) : null
2218
- ]
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
+ })
2219
2555
  }) }),
2220
2556
  /* @__PURE__ */ jsx(InkSignatureDialog, {
2221
2557
  open: creating,
@@ -2866,6 +3202,6 @@ function ThreadForm({ editingRoot, locked, deleteLabel, onSubmit, onRemove, onCl
2866
3202
  });
2867
3203
  }
2868
3204
  //#endregion
2869
- 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, useHistory, useInkContext, usePage, useReadable, useSearch, 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 };
2870
3206
 
2871
3207
  //# sourceMappingURL=index.mjs.map