@inkbar/core 0.1.1 → 0.3.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/dom.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { B as mergeLineRects, F as confidenceBucket, H as readonly, K as invertMatrix, L as cssVariableDefaults, Q as rectFromPoints, U as writable, W as applyMatrix, a as acceptSuggestion, at as InkError, et as rectsIntersect, i as threadRoot, nt as translateRect, o as openDocument, ot as isInkError, r as threadOf, t as isAbandonedNote, tt as translateQuad } from "./threads-BtvtrAhL.mjs";
1
+ import { $ as rectFromPoints, G as applyMatrix, I as confidenceBucket, R as cssVariableDefaults, U as readonly, V as mergeLineRects, W as writable, a as acceptSuggestion, i as threadRoot, nt as translateQuad, o as openDocument, ot as InkError, q as invertMatrix, r as threadOf, rt as translateRect, st as isInkError, t as isAbandonedNote, tt as rectsIntersect } from "./threads-2USvyGpX.mjs";
2
+ import { ChevronLeft, ChevronRight, Hand, Highlighter, ICON_ATTRIBUTES, MessageSquare, MousePointer2, Redo2, Square, Undo2, ZoomIn, ZoomOut } from "@inkbar/icons";
2
3
  //#region src/dom/transform.ts
3
4
  /** Folds any integer multiple of 90 onto 0, 90, 180 or 270. Throws `RangeError` otherwise. */
4
5
  function normalizeRotation(deg) {
@@ -924,7 +925,7 @@ function translated(a, dx, dy) {
924
925
  case "polygon":
925
926
  patch.vertices = a.vertices.map((p) => shift(p, dx, dy));
926
927
  break;
927
- case "ink": patch.paths = a.paths.map((path) => path.map((p) => shift(p, dx, dy)));
928
+ case "ink": patch.inklist = a.inklist.map((path) => path.map((p) => shift(p, dx, dy)));
928
929
  }
929
930
  return patch;
930
931
  }
@@ -936,7 +937,7 @@ function translated(a, dx, dy) {
936
937
  function hasRectGeometry(a) {
937
938
  switch (a.type) {
938
939
  case "redact": return a.quads === void 0 || a.quads.length === 0;
939
- case "freeText":
940
+ case "freetext":
940
941
  case "square":
941
942
  case "circle": return true;
942
943
  default: return false;
@@ -970,10 +971,10 @@ const SVG$1 = "http://www.w3.org/2000/svg";
970
971
  * `toCssRect(a.rect)`.
971
972
  */
972
973
  function annotationBox(a, t) {
973
- const points = a.type === "line" ? [a.start, a.end] : a.type === "polyline" || a.type === "polygon" ? a.vertices : a.type === "ink" ? a.paths.flat() : void 0;
974
- if (!points || points.length === 0 || !("strokeWidth" in a)) return t.toCssRect(a.rect);
974
+ const points = a.type === "line" ? [a.start, a.end] : a.type === "polyline" || a.type === "polygon" ? a.vertices : a.type === "ink" ? a.inklist.flat() : void 0;
975
+ if (!points || points.length === 0 || !("width" in a)) return t.toCssRect(a.rect);
975
976
  const box = t.toCssRect(pointBounds(points));
976
- const pad = Math.max(t.toCssLength(a.strokeWidth), 1) / 2;
977
+ const pad = Math.max(t.toCssLength(a.width), 1) / 2;
977
978
  return {
978
979
  left: box.left - pad,
979
980
  top: box.top - pad,
@@ -1011,7 +1012,7 @@ function renderAnnotationContent(el, a, t) {
1011
1012
  for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, String(v));
1012
1013
  return n;
1013
1014
  };
1014
- if (a.type === "note") {
1015
+ if (a.type === "text") {
1015
1016
  const svg = boxSvg(doc, box.width, box.height);
1016
1017
  const hit = node("rect", {
1017
1018
  x: 0,
@@ -1030,10 +1031,10 @@ function renderAnnotationContent(el, a, t) {
1030
1031
  el.append(svg, icon);
1031
1032
  return;
1032
1033
  }
1033
- if (a.type === "freeText") {
1034
+ if (a.type === "freetext") {
1034
1035
  const text = doc.createElement("div");
1035
1036
  text.className = "ink-free-text";
1036
- text.textContent = a.text;
1037
+ text.textContent = a.contents;
1037
1038
  text.dataset.align = a.align;
1038
1039
  text.style.setProperty("--ink-font-size", `${t.toCssLength(a.fontSize)}px`);
1039
1040
  text.style.setProperty("--ink-text-color", a.color);
@@ -1075,7 +1076,7 @@ function renderAnnotationContent(el, a, t) {
1075
1076
  height: box.height
1076
1077
  })];
1077
1078
  for (const s of shapes) {
1078
- s.setAttribute("fill", a.fill);
1079
+ s.setAttribute("fill", a.interiorColor);
1079
1080
  s.setAttribute("fill-opacity", "0.15");
1080
1081
  s.setAttribute("stroke", a.color);
1081
1082
  s.setAttribute("stroke-width", String(t.toCssLength(1)));
@@ -1086,7 +1087,7 @@ function renderAnnotationContent(el, a, t) {
1086
1087
  }
1087
1088
  case "square":
1088
1089
  case "circle": {
1089
- const sw = t.toCssLength(a.strokeWidth);
1090
+ const sw = t.toCssLength(a.width);
1090
1091
  const shape = a.type === "square" ? node("rect", {
1091
1092
  x: sw / 2,
1092
1093
  y: sw / 2,
@@ -1098,7 +1099,7 @@ function renderAnnotationContent(el, a, t) {
1098
1099
  rx: Math.max(0, box.width / 2 - sw / 2),
1099
1100
  ry: Math.max(0, box.height / 2 - sw / 2)
1100
1101
  });
1101
- shape.setAttribute("fill", a.fill ?? "none");
1102
+ shape.setAttribute("fill", a.interiorColor ?? "none");
1102
1103
  shape.setAttribute("stroke", a.color);
1103
1104
  shape.setAttribute("stroke-width", String(sw));
1104
1105
  svg.append(shape);
@@ -1111,20 +1112,20 @@ function renderAnnotationContent(el, a, t) {
1111
1112
  const pts = a.type === "line" ? [a.start, a.end] : a.vertices;
1112
1113
  svg.append(node(a.type === "polygon" ? "polygon" : "polyline", {
1113
1114
  points: pts.map(local).join(" "),
1114
- fill: a.type === "polygon" ? a.fill ?? "none" : "none",
1115
+ fill: a.type === "polygon" ? a.interiorColor ?? "none" : "none",
1115
1116
  stroke: a.color,
1116
- "stroke-width": t.toCssLength(a.strokeWidth),
1117
+ "stroke-width": t.toCssLength(a.width),
1117
1118
  "stroke-linejoin": "round"
1118
1119
  }));
1119
1120
  svg.setAttribute("opacity", String(a.opacity));
1120
1121
  break;
1121
1122
  }
1122
1123
  case "ink":
1123
- for (const path of a.paths) svg.append(node("polyline", {
1124
+ for (const path of a.inklist) svg.append(node("polyline", {
1124
1125
  points: path.map(local).join(" "),
1125
1126
  fill: "none",
1126
1127
  stroke: a.color,
1127
- "stroke-width": t.toCssLength(a.strokeWidth),
1128
+ "stroke-width": t.toCssLength(a.width),
1128
1129
  "stroke-linecap": "round",
1129
1130
  "stroke-linejoin": "round"
1130
1131
  }));
@@ -1257,8 +1258,8 @@ const LABELS = {
1257
1258
  underline: "Underline",
1258
1259
  strikeout: "Strikeout",
1259
1260
  squiggly: "Squiggly underline",
1260
- note: "Note",
1261
- freeText: "Text box",
1261
+ text: "Note",
1262
+ freetext: "Text box",
1262
1263
  square: "Rectangle",
1263
1264
  circle: "Ellipse",
1264
1265
  line: "Line",
@@ -1789,7 +1790,7 @@ function noteTool() {
1789
1790
  let down;
1790
1791
  const place = (c, page, at) => {
1791
1792
  const a = c.store.add({
1792
- type: "note",
1793
+ type: "text",
1793
1794
  page,
1794
1795
  rect: {
1795
1796
  x: at.x - SIZE / 2,
@@ -2497,7 +2498,7 @@ function h(tag, attrs = {}, ...children) {
2497
2498
  el.append(...children);
2498
2499
  return el;
2499
2500
  }
2500
- function button(label, onClick) {
2501
+ function button$1(label, onClick) {
2501
2502
  const b = h("button", { type: "button" }, label);
2502
2503
  b.addEventListener("click", onClick);
2503
2504
  return b;
@@ -2654,13 +2655,13 @@ function createNotePopover(opts) {
2654
2655
  const dropped = /* @__PURE__ */ new Set();
2655
2656
  function shown() {
2656
2657
  const a = current ? store.get(current) : void 0;
2657
- return a?.type === "note" ? a : null;
2658
+ return a?.type === "text" ? a : null;
2658
2659
  }
2659
2660
  function target() {
2660
2661
  const list = store.list.get();
2661
2662
  const id = activated.get();
2662
2663
  const a = id === null ? void 0 : list.find((x) => x.id === id);
2663
- return a?.type === "note" ? threadRoot(list, a) : null;
2664
+ return a?.type === "text" ? threadRoot(list, a) : null;
2664
2665
  }
2665
2666
  const editingRoot = (n) => !n.locked && !n.contents?.trim();
2666
2667
  function isEmptyRoot(n) {
@@ -2716,7 +2717,7 @@ function createNotePopover(opts) {
2716
2717
  written.add(n.id);
2717
2718
  if (editingRoot(n)) store.update(n.id, { contents: text });
2718
2719
  else store.add({
2719
- type: "note",
2720
+ type: "text",
2720
2721
  page: n.page,
2721
2722
  rect: n.rect,
2722
2723
  contents: text,
@@ -2757,11 +2758,11 @@ function threadItem(a) {
2757
2758
  function createThreadForm(n, actions) {
2758
2759
  const textarea = h("textarea", { rows: "2" });
2759
2760
  const send = h("button", { type: "submit" });
2760
- const remove = button("Delete", actions.remove);
2761
+ const remove = button$1("Delete", actions.remove);
2761
2762
  const element = h("form", {
2762
2763
  class: "ink-thread-reply",
2763
2764
  "data-ink-thread-reply": ""
2764
- }, textarea, h("div", { class: "ink-thread-actions" }, send, remove, button("Close", actions.close)));
2765
+ }, textarea, h("div", { class: "ink-thread-actions" }, send, remove, button$1("Close", actions.close)));
2765
2766
  const syncSend = () => {
2766
2767
  send.disabled = !textarea.value.trim();
2767
2768
  };
@@ -2835,7 +2836,7 @@ function createSuggestionCard(opts) {
2835
2836
  popover.panel.replaceChildren(h("p", { class: "ink-suggestion-reason" }, h("strong", {}, s?.reason ?? "Suggested redaction"), ...bucket ? [h("span", {
2836
2837
  class: "ink-suggestion-confidence",
2837
2838
  "data-confidence": bucket
2838
- }, `${bucket} confidence`)] : []), ...s?.explanation ? [h("p", { class: "ink-suggestion-explanation" }, s.explanation)] : [], h("div", { class: "ink-suggestion-actions" }, button("Accept", accept), ...a.locked ? [] : [button("Dismiss", dismiss)]));
2839
+ }, `${bucket} confidence`)] : []), ...s?.explanation ? [h("p", { class: "ink-suggestion-explanation" }, s.explanation)] : [], h("div", { class: "ink-suggestion-actions" }, button$1("Accept", accept), ...a.locked ? [] : [button$1("Dismiss", dismiss)]));
2839
2840
  }
2840
2841
  function close() {
2841
2842
  activated.set(null);
@@ -2864,315 +2865,14 @@ function createSuggestionCard(opts) {
2864
2865
  } };
2865
2866
  }
2866
2867
  //#endregion
2867
- //#region src/dom/viewer/icons.ts
2868
- const ICONS = {
2869
- hand: [
2870
- ["path", { d: "M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2" }],
2871
- ["path", { d: "M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2" }],
2872
- ["path", { d: "M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8" }],
2873
- ["path", { d: "M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15" }]
2874
- ],
2875
- "mouse-pointer-2": [["path", { d: "M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z" }]],
2876
- highlighter: [["path", { d: "m9 11-6 6v3h9l3-3" }], ["path", { d: "m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" }]],
2877
- square: [["rect", {
2878
- width: "18",
2879
- height: "18",
2880
- x: "3",
2881
- y: "3",
2882
- rx: "2"
2883
- }]],
2884
- "message-square": [["path", { d: "M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" }]],
2885
- "zoom-in": [
2886
- ["circle", {
2887
- cx: "11",
2888
- cy: "11",
2889
- r: "8"
2890
- }],
2891
- ["line", {
2892
- x1: "21",
2893
- x2: "16.65",
2894
- y1: "21",
2895
- y2: "16.65"
2896
- }],
2897
- ["line", {
2898
- x1: "11",
2899
- x2: "11",
2900
- y1: "8",
2901
- y2: "14"
2902
- }],
2903
- ["line", {
2904
- x1: "8",
2905
- x2: "14",
2906
- y1: "11",
2907
- y2: "11"
2908
- }]
2909
- ],
2910
- "zoom-out": [
2911
- ["circle", {
2912
- cx: "11",
2913
- cy: "11",
2914
- r: "8"
2915
- }],
2916
- ["line", {
2917
- x1: "21",
2918
- x2: "16.65",
2919
- y1: "21",
2920
- y2: "16.65"
2921
- }],
2922
- ["line", {
2923
- x1: "8",
2924
- x2: "14",
2925
- y1: "11",
2926
- y2: "11"
2927
- }]
2928
- ]
2929
- };
2930
- const SVG = "http://www.w3.org/2000/svg";
2931
- /** An 18px Lucide icon, hidden from assistive technology: its button carries the name. */
2932
- function icon(name) {
2933
- const svg = document.createElementNS(SVG, "svg");
2934
- const attrs = {
2935
- xmlns: SVG,
2936
- width: "18",
2937
- height: "18",
2938
- viewBox: "0 0 24 24",
2939
- fill: "none",
2940
- stroke: "currentColor",
2941
- "stroke-width": "2",
2942
- "stroke-linecap": "round",
2943
- "stroke-linejoin": "round",
2944
- "aria-hidden": "true"
2945
- };
2946
- for (const [k, v] of Object.entries(attrs)) svg.setAttribute(k, v);
2947
- for (const [tag, shape] of ICONS[name]) {
2948
- const child = document.createElementNS(SVG, tag);
2949
- for (const [k, v] of Object.entries(shape)) child.setAttribute(k, v);
2950
- svg.append(child);
2951
- }
2952
- return svg;
2953
- }
2954
- //#endregion
2955
- //#region src/dom/viewer/toolbar.ts
2956
- const TOOLS = [
2957
- ["view", "hand"],
2958
- ["select", "mouse-pointer-2"],
2959
- ["highlight", "highlighter"],
2960
- ["redact-area", "square"],
2961
- ["note", "message-square"]
2962
- ];
2963
- /** A toolbar button with an icon; the label is its accessible name and tooltip. */
2964
- function iconButton(label, attrs, name, onClick) {
2965
- const b = h("button", {
2966
- type: "button",
2967
- ...attrs,
2968
- "aria-label": label,
2969
- title: label
2970
- });
2971
- b.append(icon(name));
2972
- b.addEventListener("click", onClick);
2973
- return b;
2974
- }
2975
- const group = (name, label, ...children) => h("div", {
2976
- class: "ink-toolbar-group",
2977
- "data-ink-toolbar-group": name,
2978
- role: "group",
2979
- "aria-label": label
2980
- }, ...children);
2981
- function createToolbar() {
2982
- let stops = [];
2983
- let bound = null;
2984
- const watch = (r, fn) => {
2985
- fn(r.get());
2986
- stops.push(r.subscribe(fn));
2987
- };
2988
- const toolButtons = TOOLS.map(([id, name]) => iconButton(TOOL_LABELS[id], {
2989
- class: "ink-tool-button",
2990
- "data-ink-tool-button": "",
2991
- "data-tool": id
2992
- }, name, () => bound?.tools.set(id)));
2993
- const showTool = (active) => {
2994
- for (const b of toolButtons) {
2995
- const on = b.dataset.tool === active;
2996
- b.dataset.state = on ? "on" : "off";
2997
- b.setAttribute("aria-pressed", String(on));
2998
- }
2999
- };
3000
- const zoomOut = iconButton("Zoom out", {
3001
- class: "ink-zoom-out",
3002
- "data-ink-zoom-out": ""
3003
- }, "zoom-out", () => bound?.viewport.zoomBy(.8));
3004
- const zoomIn = iconButton("Zoom in", {
3005
- class: "ink-zoom-in",
3006
- "data-ink-zoom-in": ""
3007
- }, "zoom-in", () => bound?.viewport.zoomBy(1.25));
3008
- const custom = h("option", { value: "custom" });
3009
- const zoomSelect = h("select", {
3010
- class: "ink-zoom-select",
3011
- "data-ink-zoom-select": "",
3012
- "aria-label": "Zoom"
3013
- }, h("option", { value: "fit-width" }, "Fit width"), h("option", { value: "fit-page" }, "Fit page"), ...ZOOM_LEVELS.map((l) => h("option", { value: String(l) }, `${Math.round(l * 100)}%`)));
3014
- zoomSelect.addEventListener("change", () => {
3015
- const v = zoomSelect.value;
3016
- if (v === "fit-width" || v === "fit-page") bound?.viewport.setZoom(v);
3017
- else if (v !== "custom") bound?.viewport.setZoom(Number(v));
3018
- });
3019
- const pageInput = h("input", {
3020
- class: "ink-page-input",
3021
- "data-ink-page-input": "",
3022
- type: "number",
3023
- inputmode: "numeric",
3024
- "aria-label": "Page",
3025
- min: "1"
3026
- });
3027
- const commitPage = () => {
3028
- if (!bound) return;
3029
- const { viewport } = bound;
3030
- const n = Number(pageInput.value);
3031
- const current = viewport.currentPage.get();
3032
- if (Number.isInteger(n) && n >= 1 && n <= bound.doc.pages.length) {
3033
- if (n - 1 !== current) viewport.scrollToPage(n - 1);
3034
- } else pageInput.value = String(current + 1);
3035
- };
3036
- pageInput.addEventListener("change", commitPage);
3037
- pageInput.addEventListener("keydown", (e) => {
3038
- if (e.key !== "Enter" || e.defaultPrevented) return;
3039
- e.preventDefault();
3040
- commitPage();
3041
- });
3042
- const pageCount = h("span", {
3043
- class: "ink-page-count",
3044
- "data-ink-page-count": "",
3045
- "aria-live": "polite"
3046
- });
3047
- const controls = [
3048
- ...toolButtons,
3049
- zoomOut,
3050
- zoomSelect,
3051
- zoomIn,
3052
- pageInput
3053
- ];
3054
- const element = h("div", {
3055
- class: "ink-toolbar ink-default-toolbar",
3056
- "data-ink-toolbar": "",
3057
- "data-ink-default-toolbar": "",
3058
- role: "toolbar",
3059
- "aria-label": "Document tools",
3060
- "aria-orientation": "horizontal"
3061
- }, group("tools", "Tools", ...toolButtons), group("zoom", "Zoom", zoomOut, zoomSelect, zoomIn), group("page", "Page", pageInput, pageCount));
3062
- const roving = rovingTabindex(element);
3063
- function stop() {
3064
- for (const s of stops) s();
3065
- stops = [];
3066
- bound = null;
3067
- }
3068
- function unbind() {
3069
- stop();
3070
- for (const c of controls) c.disabled = true;
3071
- showTool(null);
3072
- custom.remove();
3073
- zoomSelect.value = "fit-width";
3074
- pageInput.value = "";
3075
- pageCount.textContent = "";
3076
- roving.sync();
3077
- }
3078
- unbind();
3079
- return {
3080
- element,
3081
- bind(doc, viewport, tools) {
3082
- stop();
3083
- bound = {
3084
- doc,
3085
- viewport,
3086
- tools
3087
- };
3088
- for (const c of controls) c.disabled = false;
3089
- watch(tools.active, showTool);
3090
- watch(viewport.zoom, (z) => {
3091
- const level = typeof z === "number" ? ZOOM_LEVELS.find((l) => Math.abs(l - z) < .001) : void 0;
3092
- if (typeof z === "number" && level === void 0) {
3093
- custom.textContent = `${Math.round(z * 100)}%`;
3094
- zoomSelect.append(custom);
3095
- zoomSelect.value = "custom";
3096
- } else {
3097
- custom.remove();
3098
- zoomSelect.value = typeof z === "number" ? String(level) : z;
3099
- }
3100
- });
3101
- const count = doc.pages.length;
3102
- pageInput.max = String(count);
3103
- pageCount.textContent = `of ${count}`;
3104
- watch(viewport.currentPage, (p) => {
3105
- pageInput.value = String(p + 1);
3106
- });
3107
- roving.sync();
3108
- },
3109
- unbind,
3110
- destroy() {
3111
- stop();
3112
- roving.destroy();
3113
- element.remove();
3114
- }
3115
- };
3116
- }
3117
- /** Roving items, as in the bindings' Radix toolbar: buttons only. */
3118
- const ITEMS = "button";
3119
- /**
3120
- * One Tab stop across the enabled buttons in `bar`: the current one has `tabindex="0"`, the
3121
- * rest `-1`. ArrowLeft and ArrowRight move between them and wrap, Home and End go to the ends.
3122
- * Inputs, selects and textareas, built in or added by the host, keep their own tabindex (a
3123
- * Tab stop of their own by default) and their own arrow, Home and End keys.
3124
- */
3125
- function rovingTabindex(bar) {
3126
- let current = null;
3127
- const items = () => [...bar.querySelectorAll(ITEMS)].filter((el) => !el.disabled && !el.closest("[hidden]"));
3128
- const sync = () => {
3129
- const list = items();
3130
- if (!current || !list.includes(current)) current = list[0] ?? null;
3131
- for (const el of bar.querySelectorAll(ITEMS)) el.tabIndex = el === current ? 0 : -1;
3132
- };
3133
- const onFocusIn = (e) => {
3134
- const target = e.target;
3135
- if (!items().includes(target)) return;
3136
- current = target;
3137
- sync();
3138
- };
3139
- const onKeyDown = (e) => {
3140
- if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey) return;
3141
- const list = items();
3142
- const i = list.indexOf(e.target);
3143
- if (i < 0) return;
3144
- const last = list.length - 1;
3145
- const next = e.key === "ArrowRight" ? i === last ? 0 : i + 1 : e.key === "ArrowLeft" ? i === 0 ? last : i - 1 : e.key === "Home" ? 0 : e.key === "End" ? last : -1;
3146
- if (next < 0) return;
3147
- e.preventDefault();
3148
- list[next].focus();
3149
- };
3150
- const observer = new MutationObserver(sync);
3151
- observer.observe(bar, {
3152
- childList: true,
3153
- subtree: true,
3154
- attributes: true,
3155
- attributeFilter: ["disabled", "hidden"]
3156
- });
3157
- bar.addEventListener("focusin", onFocusIn);
3158
- bar.addEventListener("keydown", onKeyDown);
3159
- sync();
3160
- return {
3161
- sync,
3162
- destroy() {
3163
- observer.disconnect();
3164
- bar.removeEventListener("focusin", onFocusIn);
3165
- bar.removeEventListener("keydown", onKeyDown);
3166
- }
3167
- };
3168
- }
3169
- //#endregion
3170
2868
  //#region src/dom/viewer/create-viewer.ts
3171
2869
  function createViewer(el, opts) {
3172
- const { source: initial, assets, password, defaults, history, toolbar: withToolbar = true, notes: withNotes = true, suggestions: withSuggestions = true, onReady, onError, onActivate, onBuild, onPageMount, onPageUnmount, onPageError, ...view } = opts;
2870
+ const { source: initial, assets, password, defaults, history, notes: withNotes = true, suggestions: withSuggestions = true, onReady, onError, onActivate, onBuild, onPageMount, onPageUnmount, onPageError, ...view } = opts;
3173
2871
  const status = writable({ state: "idle" });
3174
2872
  const current = writable(null);
3175
2873
  const engine = writable(null);
2874
+ const viewport = writable(null);
2875
+ const tools = writable(null);
3176
2876
  /** The activated annotation: the note thread and the suggestion card open from it. */
3177
2877
  const activated = writable(null);
3178
2878
  const root = h("div", {
@@ -3191,8 +2891,6 @@ function createViewer(el, opts) {
3191
2891
  "data-ink-viewport-root": "",
3192
2892
  "data-state": "idle"
3193
2893
  }, scroll);
3194
- const toolbar = withToolbar ? createToolbar() : null;
3195
- if (toolbar) root.append(toolbar.element);
3196
2894
  root.append(viewportRoot);
3197
2895
  el.append(root);
3198
2896
  const focusReturn = createFocusReturn({
@@ -3255,9 +2953,10 @@ function createViewer(el, opts) {
3255
2953
  activated.set(id);
3256
2954
  onActivate?.(id);
3257
2955
  },
3258
- onBuild: (viewport, tools) => {
3259
- toolbar?.bind(doc, viewport, tools);
3260
- onBuild?.(viewport, tools);
2956
+ onBuild: (vp, tc) => {
2957
+ tools.set(tc);
2958
+ viewport.set(vp);
2959
+ onBuild?.(vp, tc);
3261
2960
  },
3262
2961
  onPageMount: (page, mount, host) => {
3263
2962
  hosts.set(page, host);
@@ -3290,7 +2989,8 @@ function createViewer(el, opts) {
3290
2989
  activated.set(null);
3291
2990
  engine.set(null);
3292
2991
  mounted.destroy();
3293
- toolbar?.unbind();
2992
+ viewport.set(null);
2993
+ tools.set(null);
3294
2994
  };
3295
2995
  }
3296
2996
  let loads = 0;
@@ -3346,19 +3046,319 @@ function createViewer(el, opts) {
3346
3046
  status: readonly(status),
3347
3047
  document: readonly(current),
3348
3048
  engine: readonly(engine),
3349
- toolbar: toolbar?.element ?? null,
3049
+ viewport: readonly(viewport),
3050
+ tools: readonly(tools),
3350
3051
  open,
3351
3052
  destroy() {
3352
3053
  if (destroyed) return;
3353
3054
  destroyed = true;
3354
3055
  loads++;
3355
3056
  close();
3356
- toolbar?.destroy();
3357
3057
  root.remove();
3358
3058
  }
3359
3059
  };
3360
3060
  }
3361
3061
  //#endregion
3362
- export { TOOL_LABELS, ZOOM_LEVELS, annotationElementFor, attachViewportKeys, builtinTools, computeCssLayout, confidenceAttr, createFocusReturn, createOverlay, createPageMount, createPageRenderer, createPageTransform, createTextLayer, createToolController, createViewer, createViewport, cssRectsToUserRects, cssVariableDefaults, fitZoomCss, getSelectionQuads, highlightTool, hitFor, mostVisiblePageCss, mountViewer, normalizeRotation, noteTool, observeAnnotationElement, redactAreaTool, renderAnnotationContent, selectTool, shouldKeepOpenOnAnnotation, viewTool, visibleRangeCss };
3062
+ //#region src/dom/viewer/controls.ts
3063
+ /**
3064
+ * Runs `connect` for every value `source` takes while it is set, undoing the previous run's
3065
+ * subscriptions first, and disables `control` while it is `null`.
3066
+ */
3067
+ function follow(source, control, connect, reset) {
3068
+ let stops = [];
3069
+ const watch = (r, fn) => {
3070
+ fn(r.get());
3071
+ stops.push(r.subscribe(fn));
3072
+ };
3073
+ const run = (value) => {
3074
+ for (const s of stops) s();
3075
+ stops = [];
3076
+ control.disabled = value === null;
3077
+ if (value === null) reset?.();
3078
+ else connect(value, watch);
3079
+ };
3080
+ run(source.get());
3081
+ source.subscribe(run);
3082
+ }
3083
+ const SVG = "http://www.w3.org/2000/svg";
3084
+ const TOOL_ICONS = {
3085
+ view: Hand,
3086
+ select: MousePointer2,
3087
+ highlight: Highlighter,
3088
+ "redact-area": Square,
3089
+ note: MessageSquare
3090
+ };
3091
+ /**
3092
+ * An `<svg>` drawing `icon` from `@inkbar/icons`, hidden from assistive technology: the
3093
+ * control it sits in carries the name. The theme sizes it with `--ink-icon-size`.
3094
+ */
3095
+ function createIcon(icon) {
3096
+ const svg = document.createElementNS(SVG, "svg");
3097
+ const attrs = {
3098
+ ...ICON_ATTRIBUTES,
3099
+ width: "18",
3100
+ height: "18",
3101
+ class: "ink-icon",
3102
+ "aria-hidden": "true"
3103
+ };
3104
+ for (const [k, v] of Object.entries(attrs)) svg.setAttribute(k, v);
3105
+ for (const [tag, shape] of icon) {
3106
+ const child = document.createElementNS(SVG, tag);
3107
+ for (const [k, v] of Object.entries(shape)) child.setAttribute(k, v);
3108
+ svg.append(child);
3109
+ }
3110
+ return svg;
3111
+ }
3112
+ function button(label, attrs, icon) {
3113
+ const b = h("button", {
3114
+ type: "button",
3115
+ ...attrs,
3116
+ "aria-label": label,
3117
+ title: label
3118
+ });
3119
+ b.append(createIcon(icon));
3120
+ return b;
3121
+ }
3122
+ /**
3123
+ * A toolbar following the WAI-ARIA toolbar pattern: its buttons share one Tab stop, with
3124
+ * arrow keys, Home and End to move between them. Fields and selects are Tab stops of their
3125
+ * own and keep their keys. Buttons added later join in.
3126
+ */
3127
+ function createToolbar(opts = {}) {
3128
+ const bar = h("div", {
3129
+ class: "ink-toolbar",
3130
+ "data-ink-toolbar": "",
3131
+ role: "toolbar",
3132
+ "aria-label": opts.label ?? "Document tools",
3133
+ "aria-orientation": "horizontal"
3134
+ });
3135
+ rovingTabindex(bar);
3136
+ return bar;
3137
+ }
3138
+ /** A divider between groups of controls in a horizontal toolbar. Not focusable. */
3139
+ function createToolbarSeparator() {
3140
+ return h("div", {
3141
+ role: "separator",
3142
+ "aria-orientation": "vertical",
3143
+ class: "ink-toolbar-separator",
3144
+ "data-ink-toolbar-separator": ""
3145
+ });
3146
+ }
3147
+ /** Undoes the last change to the document's annotations; disabled when there is none. */
3148
+ function createUndo(viewer) {
3149
+ return historyButton(viewer, "undo");
3150
+ }
3151
+ /** Redoes the last undone change; disabled when there is none. */
3152
+ function createRedo(viewer) {
3153
+ return historyButton(viewer, "redo");
3154
+ }
3155
+ function historyButton(viewer, action) {
3156
+ const b = button(action === "undo" ? "Undo" : "Redo", {
3157
+ class: `ink-${action}`,
3158
+ [`data-ink-${action}`]: ""
3159
+ }, action === "undo" ? Undo2 : Redo2);
3160
+ b.addEventListener("click", () => viewer.document.get()?.history[action]());
3161
+ follow(viewer.document, b, (doc, watch) => watch(action === "undo" ? doc.history.canUndo : doc.history.canRedo, (can) => {
3162
+ b.disabled = !can;
3163
+ }));
3164
+ return b;
3165
+ }
3166
+ /** Sets the tool; pressed while it is the active one. */
3167
+ function createToolButton(viewer, tool, opts = {}) {
3168
+ const b = button(opts.label ?? TOOL_LABELS[tool], {
3169
+ class: "ink-tool-button",
3170
+ "data-ink-tool-button": "",
3171
+ "data-tool": tool
3172
+ }, TOOL_ICONS[tool]);
3173
+ const show = (active) => {
3174
+ const on = active === tool;
3175
+ b.dataset.state = on ? "on" : "off";
3176
+ b.setAttribute("aria-pressed", String(on));
3177
+ };
3178
+ b.addEventListener("click", () => viewer.tools.get()?.set(tool));
3179
+ follow(viewer.tools, b, (tools, watch) => watch(tools.active, show), () => show(null));
3180
+ return b;
3181
+ }
3182
+ function createZoomIn(viewer) {
3183
+ const b = button("Zoom in", {
3184
+ class: "ink-zoom-in",
3185
+ "data-ink-zoom-in": ""
3186
+ }, ZoomIn);
3187
+ b.addEventListener("click", () => viewer.viewport.get()?.zoomBy(1.25));
3188
+ follow(viewer.viewport, b, () => void 0);
3189
+ return b;
3190
+ }
3191
+ function createZoomOut(viewer) {
3192
+ const b = button("Zoom out", {
3193
+ class: "ink-zoom-out",
3194
+ "data-ink-zoom-out": ""
3195
+ }, ZoomOut);
3196
+ b.addEventListener("click", () => viewer.viewport.get()?.zoomBy(.8));
3197
+ follow(viewer.viewport, b, () => void 0);
3198
+ return b;
3199
+ }
3200
+ /** Fit width, fit page and the zoom levels; a zoom between levels gets an option of its own. */
3201
+ function createZoomSelect(viewer) {
3202
+ const custom = h("option", { value: "custom" });
3203
+ const select = h("select", {
3204
+ class: "ink-zoom-select",
3205
+ "data-ink-zoom-select": "",
3206
+ "aria-label": "Zoom"
3207
+ }, h("option", { value: "fit-width" }, "Fit width"), h("option", { value: "fit-page" }, "Fit page"), ...ZOOM_LEVELS.map((l) => h("option", { value: String(l) }, `${Math.round(l * 100)}%`)));
3208
+ select.addEventListener("change", () => {
3209
+ const v = select.value;
3210
+ const viewport = viewer.viewport.get();
3211
+ if (v === "fit-width" || v === "fit-page") viewport?.setZoom(v);
3212
+ else if (v !== "custom") viewport?.setZoom(Number(v));
3213
+ });
3214
+ follow(viewer.viewport, select, (viewport, watch) => watch(viewport.zoom, (z) => {
3215
+ const level = typeof z === "number" ? ZOOM_LEVELS.find((l) => Math.abs(l - z) < .001) : void 0;
3216
+ if (typeof z === "number" && level === void 0) {
3217
+ custom.textContent = `${Math.round(z * 100)}%`;
3218
+ const after = [...select.options].find((o) => o !== custom && Number(o.value) > z);
3219
+ select.insertBefore(custom, after ?? null);
3220
+ select.value = "custom";
3221
+ } else {
3222
+ custom.remove();
3223
+ select.value = typeof z === "number" ? String(level) : z;
3224
+ }
3225
+ }), () => {
3226
+ custom.remove();
3227
+ select.value = "fit-width";
3228
+ });
3229
+ return select;
3230
+ }
3231
+ /**
3232
+ * The current page, 1-based. Enter or leaving the field goes to the page typed; a number out
3233
+ * of range puts the current page back.
3234
+ */
3235
+ function createPageInput(viewer) {
3236
+ const input = h("input", {
3237
+ class: "ink-page-input",
3238
+ "data-ink-page-input": "",
3239
+ type: "number",
3240
+ inputmode: "numeric",
3241
+ "aria-label": "Page",
3242
+ min: "1"
3243
+ });
3244
+ const commit = () => {
3245
+ const viewport = viewer.viewport.get();
3246
+ const doc = viewer.document.get();
3247
+ if (!viewport || !doc) return;
3248
+ const n = Number(input.value);
3249
+ const current = viewport.currentPage.get();
3250
+ if (Number.isInteger(n) && n >= 1 && n <= doc.pages.length) {
3251
+ if (n - 1 !== current) viewport.scrollToPage(n - 1);
3252
+ } else input.value = String(current + 1);
3253
+ };
3254
+ input.addEventListener("change", commit);
3255
+ input.addEventListener("keydown", (e) => {
3256
+ if (e.key !== "Enter" || e.defaultPrevented) return;
3257
+ e.preventDefault();
3258
+ commit();
3259
+ });
3260
+ follow(viewer.viewport, input, (viewport, watch) => {
3261
+ input.max = String(viewer.document.get()?.pages.length ?? 1);
3262
+ watch(viewport.currentPage, (p) => input.value = String(p + 1));
3263
+ }, () => input.value = "");
3264
+ return input;
3265
+ }
3266
+ /** "of 12": the page count, announced when it changes. */
3267
+ function createPageCount(viewer) {
3268
+ const span = h("span", {
3269
+ class: "ink-page-count",
3270
+ "data-ink-page-count": "",
3271
+ "aria-live": "polite"
3272
+ });
3273
+ const show = () => {
3274
+ const doc = viewer.document.get();
3275
+ span.textContent = doc && viewer.viewport.get() ? `of ${doc.pages.length}` : "";
3276
+ };
3277
+ show();
3278
+ viewer.viewport.subscribe(show);
3279
+ return span;
3280
+ }
3281
+ /** Disabled on the first page. */
3282
+ function createPrevPage(viewer) {
3283
+ return pageButton(viewer, -1);
3284
+ }
3285
+ /** Disabled on the last page. */
3286
+ function createNextPage(viewer) {
3287
+ return pageButton(viewer, 1);
3288
+ }
3289
+ function pageButton(viewer, step) {
3290
+ const b = step < 0 ? button("Previous page", {
3291
+ class: "ink-prev-page",
3292
+ "data-ink-prev-page": ""
3293
+ }, ChevronLeft) : button("Next page", {
3294
+ class: "ink-next-page",
3295
+ "data-ink-next-page": ""
3296
+ }, ChevronRight);
3297
+ b.addEventListener("click", () => {
3298
+ const viewport = viewer.viewport.get();
3299
+ viewport?.scrollToPage(viewport.currentPage.get() + step);
3300
+ });
3301
+ follow(viewer.viewport, b, (viewport, watch) => {
3302
+ const last = (viewer.document.get()?.pages.length ?? 1) - 1;
3303
+ watch(viewport.currentPage, (p) => {
3304
+ b.disabled = step < 0 ? p <= 0 : p >= last;
3305
+ });
3306
+ });
3307
+ return b;
3308
+ }
3309
+ /** Roving items, as in the bindings' Radix toolbar: buttons only. */
3310
+ const ITEMS = "button";
3311
+ /**
3312
+ * One Tab stop across the enabled buttons in `bar`: the current one has `tabindex="0"`, the
3313
+ * rest `-1`. ArrowLeft and ArrowRight move between them and wrap, Home and End go to the ends.
3314
+ * Inputs, selects and textareas, built in or added by the host, keep their own tabindex (a
3315
+ * Tab stop of their own by default) and their own arrow, Home and End keys.
3316
+ */
3317
+ function rovingTabindex(bar) {
3318
+ let current = null;
3319
+ const items = () => [...bar.querySelectorAll(ITEMS)].filter((el) => !el.disabled && !el.closest("[hidden]"));
3320
+ const sync = () => {
3321
+ const list = items();
3322
+ if (!current || !list.includes(current)) current = list[0] ?? null;
3323
+ for (const el of bar.querySelectorAll(ITEMS)) el.tabIndex = el === current ? 0 : -1;
3324
+ };
3325
+ const onFocusIn = (e) => {
3326
+ const target = e.target;
3327
+ if (!items().includes(target)) return;
3328
+ current = target;
3329
+ sync();
3330
+ };
3331
+ const onKeyDown = (e) => {
3332
+ if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey) return;
3333
+ const list = items();
3334
+ const i = list.indexOf(e.target);
3335
+ if (i < 0) return;
3336
+ const last = list.length - 1;
3337
+ const next = e.key === "ArrowRight" ? i === last ? 0 : i + 1 : e.key === "ArrowLeft" ? i === 0 ? last : i - 1 : e.key === "Home" ? 0 : e.key === "End" ? last : -1;
3338
+ if (next < 0) return;
3339
+ e.preventDefault();
3340
+ list[next].focus();
3341
+ };
3342
+ const observer = new MutationObserver(sync);
3343
+ observer.observe(bar, {
3344
+ childList: true,
3345
+ subtree: true,
3346
+ attributes: true,
3347
+ attributeFilter: ["disabled", "hidden"]
3348
+ });
3349
+ bar.addEventListener("focusin", onFocusIn);
3350
+ bar.addEventListener("keydown", onKeyDown);
3351
+ sync();
3352
+ return {
3353
+ sync,
3354
+ destroy() {
3355
+ observer.disconnect();
3356
+ bar.removeEventListener("focusin", onFocusIn);
3357
+ bar.removeEventListener("keydown", onKeyDown);
3358
+ }
3359
+ };
3360
+ }
3361
+ //#endregion
3362
+ export { TOOL_LABELS, ZOOM_LEVELS, annotationElementFor, attachViewportKeys, builtinTools, computeCssLayout, confidenceAttr, createFocusReturn, createIcon, createNextPage, createOverlay, createPageCount, createPageInput, createPageMount, createPageRenderer, createPageTransform, createPrevPage, createRedo, createTextLayer, createToolButton, createToolController, createToolbar, createToolbarSeparator, createUndo, createViewer, createViewport, createZoomIn, createZoomOut, createZoomSelect, cssRectsToUserRects, cssVariableDefaults, fitZoomCss, getSelectionQuads, highlightTool, hitFor, mostVisiblePageCss, mountViewer, normalizeRotation, noteTool, observeAnnotationElement, redactAreaTool, renderAnnotationContent, selectTool, shouldKeepOpenOnAnnotation, viewTool, visibleRangeCss };
3363
3363
 
3364
3364
  //# sourceMappingURL=dom.mjs.map