@docmentis/udoc-viewer 0.6.22 → 0.6.23

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.
Files changed (130) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/UDocClient.d.ts +1 -1
  3. package/dist/src/UDocClient.js +1 -1
  4. package/dist/src/UDocViewer.d.ts +4 -3
  5. package/dist/src/UDocViewer.d.ts.map +1 -1
  6. package/dist/src/UDocViewer.js +38 -4
  7. package/dist/src/UDocViewer.js.map +1 -1
  8. package/dist/src/ui/viewer/actions.d.ts +23 -3
  9. package/dist/src/ui/viewer/actions.d.ts.map +1 -1
  10. package/dist/src/ui/viewer/annotation/ShapeRenderer.d.ts.map +1 -1
  11. package/dist/src/ui/viewer/annotation/ShapeRenderer.js +23 -16
  12. package/dist/src/ui/viewer/annotation/ShapeRenderer.js.map +1 -1
  13. package/dist/src/ui/viewer/annotation/index.d.ts +1 -1
  14. package/dist/src/ui/viewer/annotation/index.d.ts.map +1 -1
  15. package/dist/src/ui/viewer/annotation/index.js +1 -1
  16. package/dist/src/ui/viewer/annotation/index.js.map +1 -1
  17. package/dist/src/ui/viewer/annotation/render.d.ts.map +1 -1
  18. package/dist/src/ui/viewer/annotation/render.js +11 -4
  19. package/dist/src/ui/viewer/annotation/render.js.map +1 -1
  20. package/dist/src/ui/viewer/annotation/types.d.ts +9 -1
  21. package/dist/src/ui/viewer/annotation/types.d.ts.map +1 -1
  22. package/dist/src/ui/viewer/annotation/utils.d.ts +10 -1
  23. package/dist/src/ui/viewer/annotation/utils.d.ts.map +1 -1
  24. package/dist/src/ui/viewer/annotation/utils.js +114 -0
  25. package/dist/src/ui/viewer/annotation/utils.js.map +1 -1
  26. package/dist/src/ui/viewer/components/ColorSelect.d.ts +26 -0
  27. package/dist/src/ui/viewer/components/ColorSelect.d.ts.map +1 -0
  28. package/dist/src/ui/viewer/components/ColorSelect.js +120 -0
  29. package/dist/src/ui/viewer/components/ColorSelect.js.map +1 -0
  30. package/dist/src/ui/viewer/components/NumberInput.d.ts +32 -0
  31. package/dist/src/ui/viewer/components/NumberInput.d.ts.map +1 -0
  32. package/dist/src/ui/viewer/components/NumberInput.js +101 -0
  33. package/dist/src/ui/viewer/components/NumberInput.js.map +1 -0
  34. package/dist/src/ui/viewer/components/Spread.d.ts +2 -2
  35. package/dist/src/ui/viewer/components/Spread.d.ts.map +1 -1
  36. package/dist/src/ui/viewer/components/Spread.js.map +1 -1
  37. package/dist/src/ui/viewer/components/SubToolbar.d.ts.map +1 -1
  38. package/dist/src/ui/viewer/components/SubToolbar.js +385 -107
  39. package/dist/src/ui/viewer/components/SubToolbar.js.map +1 -1
  40. package/dist/src/ui/viewer/components/Toolbar.d.ts.map +1 -1
  41. package/dist/src/ui/viewer/components/Toolbar.js +6 -3
  42. package/dist/src/ui/viewer/components/Toolbar.js.map +1 -1
  43. package/dist/src/ui/viewer/components/Viewport.d.ts.map +1 -1
  44. package/dist/src/ui/viewer/components/Viewport.js +20 -0
  45. package/dist/src/ui/viewer/components/Viewport.js.map +1 -1
  46. package/dist/src/ui/viewer/i18n/ar.d.ts.map +1 -1
  47. package/dist/src/ui/viewer/i18n/ar.js +19 -4
  48. package/dist/src/ui/viewer/i18n/ar.js.map +1 -1
  49. package/dist/src/ui/viewer/i18n/de.d.ts.map +1 -1
  50. package/dist/src/ui/viewer/i18n/de.js +19 -4
  51. package/dist/src/ui/viewer/i18n/de.js.map +1 -1
  52. package/dist/src/ui/viewer/i18n/en.d.ts.map +1 -1
  53. package/dist/src/ui/viewer/i18n/en.js +15 -0
  54. package/dist/src/ui/viewer/i18n/en.js.map +1 -1
  55. package/dist/src/ui/viewer/i18n/es.d.ts.map +1 -1
  56. package/dist/src/ui/viewer/i18n/es.js +19 -4
  57. package/dist/src/ui/viewer/i18n/es.js.map +1 -1
  58. package/dist/src/ui/viewer/i18n/fr.d.ts.map +1 -1
  59. package/dist/src/ui/viewer/i18n/fr.js +19 -4
  60. package/dist/src/ui/viewer/i18n/fr.js.map +1 -1
  61. package/dist/src/ui/viewer/i18n/ja.d.ts.map +1 -1
  62. package/dist/src/ui/viewer/i18n/ja.js +19 -4
  63. package/dist/src/ui/viewer/i18n/ja.js.map +1 -1
  64. package/dist/src/ui/viewer/i18n/ko.d.ts.map +1 -1
  65. package/dist/src/ui/viewer/i18n/ko.js +19 -4
  66. package/dist/src/ui/viewer/i18n/ko.js.map +1 -1
  67. package/dist/src/ui/viewer/i18n/pt-BR.d.ts.map +1 -1
  68. package/dist/src/ui/viewer/i18n/pt-BR.js +19 -4
  69. package/dist/src/ui/viewer/i18n/pt-BR.js.map +1 -1
  70. package/dist/src/ui/viewer/i18n/ru.d.ts.map +1 -1
  71. package/dist/src/ui/viewer/i18n/ru.js +19 -4
  72. package/dist/src/ui/viewer/i18n/ru.js.map +1 -1
  73. package/dist/src/ui/viewer/i18n/types.d.ts +14 -0
  74. package/dist/src/ui/viewer/i18n/types.d.ts.map +1 -1
  75. package/dist/src/ui/viewer/i18n/zh-CN.d.ts.map +1 -1
  76. package/dist/src/ui/viewer/i18n/zh-CN.js +19 -4
  77. package/dist/src/ui/viewer/i18n/zh-CN.js.map +1 -1
  78. package/dist/src/ui/viewer/i18n/zh-TW.d.ts.map +1 -1
  79. package/dist/src/ui/viewer/i18n/zh-TW.js +19 -4
  80. package/dist/src/ui/viewer/i18n/zh-TW.js.map +1 -1
  81. package/dist/src/ui/viewer/icons.d.ts +13 -0
  82. package/dist/src/ui/viewer/icons.d.ts.map +1 -1
  83. package/dist/src/ui/viewer/icons.js +22 -0
  84. package/dist/src/ui/viewer/icons.js.map +1 -1
  85. package/dist/src/ui/viewer/reducer.d.ts.map +1 -1
  86. package/dist/src/ui/viewer/reducer.js +70 -3
  87. package/dist/src/ui/viewer/reducer.js.map +1 -1
  88. package/dist/src/ui/viewer/search/search.d.ts +4 -4
  89. package/dist/src/ui/viewer/search/search.d.ts.map +1 -1
  90. package/dist/src/ui/viewer/search/search.js +1 -1
  91. package/dist/src/ui/viewer/search/search.js.map +1 -1
  92. package/dist/src/ui/viewer/shell.d.ts +2 -2
  93. package/dist/src/ui/viewer/shell.d.ts.map +1 -1
  94. package/dist/src/ui/viewer/shell.js.map +1 -1
  95. package/dist/src/ui/viewer/state.d.ts +23 -6
  96. package/dist/src/ui/viewer/state.d.ts.map +1 -1
  97. package/dist/src/ui/viewer/state.js +9 -0
  98. package/dist/src/ui/viewer/state.js.map +1 -1
  99. package/dist/src/ui/viewer/styles-inline.d.ts +1 -1
  100. package/dist/src/ui/viewer/styles-inline.d.ts.map +1 -1
  101. package/dist/src/ui/viewer/styles-inline.js +468 -34
  102. package/dist/src/ui/viewer/styles-inline.js.map +1 -1
  103. package/dist/src/ui/viewer/text/render.d.ts +3 -3
  104. package/dist/src/ui/viewer/text/render.d.ts.map +1 -1
  105. package/dist/src/ui/viewer/text/render.js +2 -2
  106. package/dist/src/ui/viewer/text/render.js.map +1 -1
  107. package/dist/src/ui/viewer/tools/AnnotationDrawController.d.ts +21 -0
  108. package/dist/src/ui/viewer/tools/AnnotationDrawController.d.ts.map +1 -0
  109. package/dist/src/ui/viewer/tools/AnnotationDrawController.js +392 -0
  110. package/dist/src/ui/viewer/tools/AnnotationDrawController.js.map +1 -0
  111. package/dist/src/ui/viewer/tools/AnnotationSelectController.d.ts +22 -0
  112. package/dist/src/ui/viewer/tools/AnnotationSelectController.d.ts.map +1 -0
  113. package/dist/src/ui/viewer/tools/AnnotationSelectController.js +367 -0
  114. package/dist/src/ui/viewer/tools/AnnotationSelectController.js.map +1 -0
  115. package/dist/src/wasm/udoc.d.ts +399 -114
  116. package/dist/src/wasm/udoc.js +157 -81
  117. package/dist/src/wasm/udoc_bg.wasm +0 -0
  118. package/dist/src/wasm/udoc_bg.wasm.d.ts +8 -7
  119. package/dist/src/worker/WorkerClient.d.ts +17 -11
  120. package/dist/src/worker/WorkerClient.d.ts.map +1 -1
  121. package/dist/src/worker/WorkerClient.js +10 -2
  122. package/dist/src/worker/WorkerClient.js.map +1 -1
  123. package/dist/src/worker/index.d.ts +1 -1
  124. package/dist/src/worker/index.d.ts.map +1 -1
  125. package/dist/src/worker/worker-inline.js +1 -1
  126. package/dist/src/worker/worker.d.ts +26 -58
  127. package/dist/src/worker/worker.d.ts.map +1 -1
  128. package/dist/src/worker/worker.js +161 -80
  129. package/dist/src/worker/worker.js.map +1 -1
  130. package/package.json +1 -1
@@ -1,77 +1,163 @@
1
1
  import { subscribeSelector } from "../../framework/selectors";
2
2
  import { isToolSet, DEFAULT_TOOL_OPTIONS } from "../state";
3
- import { ICON_SUBTOOL_FREEHAND, ICON_SUBTOOL_LINE, ICON_SUBTOOL_ARROW, ICON_SUBTOOL_RECTANGLE, ICON_SUBTOOL_ELLIPSE, ICON_SUBTOOL_POLYGON, ICON_SUBTOOL_TEXTBOX, ICON_SUBTOOL_HIGHLIGHT, ICON_SUBTOOL_UNDERLINE, ICON_SUBTOOL_STRIKETHROUGH, ICON_SUBTOOL_SQUIGGLY, } from "../icons";
3
+ import { createNumberInput } from "./NumberInput";
4
+ import { createColorSelect } from "./ColorSelect";
5
+ import { ICON_SUBTOOL_SELECT, ICON_SUBTOOL_FREEHAND, ICON_SUBTOOL_LINE, ICON_SUBTOOL_ARROW, ICON_SUBTOOL_RECTANGLE, ICON_SUBTOOL_ELLIPSE, ICON_SUBTOOL_POLYGON, ICON_SUBTOOL_POLYLINE, ICON_SUBTOOL_HIGHLIGHT, ICON_SUBTOOL_UNDERLINE, ICON_SUBTOOL_STRIKETHROUGH, ICON_SUBTOOL_SQUIGGLY, ICON_LINE_SOLID, ICON_LINE_DASHED, ICON_LINE_DOTTED, ICON_ARROW_NONE, ICON_ARROW_OPEN, ICON_ARROW_CLOSED, ICON_STROKE_WIDTH, ICON_OPACITY, ICON_FONT_SIZE, ICON_DELETE, } from "../icons";
4
6
  const ANNOTATE_SUB_TOOLS = [
7
+ { id: "select", icon: ICON_SUBTOOL_SELECT, labelKey: "tools.select" },
5
8
  { id: "freehand", icon: ICON_SUBTOOL_FREEHAND, labelKey: "tools.freehand" },
6
9
  { id: "line", icon: ICON_SUBTOOL_LINE, labelKey: "tools.line" },
7
10
  { id: "arrow", icon: ICON_SUBTOOL_ARROW, labelKey: "tools.arrow" },
8
11
  { id: "rectangle", icon: ICON_SUBTOOL_RECTANGLE, labelKey: "tools.rectangle" },
9
12
  { id: "ellipse", icon: ICON_SUBTOOL_ELLIPSE, labelKey: "tools.ellipse" },
10
13
  { id: "polygon", icon: ICON_SUBTOOL_POLYGON, labelKey: "tools.polygon" },
11
- { id: "textbox", icon: ICON_SUBTOOL_TEXTBOX, labelKey: "tools.textbox" },
14
+ { id: "polyline", icon: ICON_SUBTOOL_POLYLINE, labelKey: "tools.polyline" },
12
15
  ];
13
16
  const MARKUP_SUB_TOOLS = [
17
+ { id: "select", icon: ICON_SUBTOOL_SELECT, labelKey: "tools.select" },
14
18
  { id: "highlight", icon: ICON_SUBTOOL_HIGHLIGHT, labelKey: "tools.highlight" },
15
19
  { id: "underline", icon: ICON_SUBTOOL_UNDERLINE, labelKey: "tools.underline" },
16
20
  { id: "strikethrough", icon: ICON_SUBTOOL_STRIKETHROUGH, labelKey: "tools.strikethrough" },
17
21
  { id: "squiggly", icon: ICON_SUBTOOL_SQUIGGLY, labelKey: "tools.squiggly" },
18
22
  ];
19
- const SUB_TOOLS_MAP = {
20
- annotate: ANNOTATE_SUB_TOOLS,
21
- markup: MARKUP_SUB_TOOLS,
22
- };
23
- /** Which options each sub-tool supports */
23
+ const SUB_TOOLS_MAP = { annotate: ANNOTATE_SUB_TOOLS, markup: MARKUP_SUB_TOOLS };
24
24
  const TOOL_OPTIONS_CONFIG = {
25
25
  freehand: ["strokeColor", "strokeWidth", "opacity"],
26
- line: ["strokeColor", "strokeWidth", "opacity"],
27
- arrow: ["strokeColor", "strokeWidth", "opacity"],
28
- rectangle: ["strokeColor", "fillColor", "strokeWidth", "opacity"],
29
- ellipse: ["strokeColor", "fillColor", "strokeWidth", "opacity"],
30
- polygon: ["strokeColor", "fillColor", "strokeWidth", "opacity"],
31
- textbox: ["strokeColor", "opacity"],
26
+ line: ["strokeColor", "strokeWidth", "opacity", "lineStyle"],
27
+ arrow: ["strokeColor", "strokeWidth", "opacity", "lineStyle", "arrowHead"],
28
+ rectangle: ["strokeColor", "fillColor", "strokeWidth", "opacity", "lineStyle"],
29
+ ellipse: ["strokeColor", "fillColor", "strokeWidth", "opacity", "lineStyle"],
30
+ polygon: ["strokeColor", "fillColor", "strokeWidth", "opacity", "lineStyle"],
31
+ polyline: ["strokeColor", "strokeWidth", "opacity", "lineStyle"],
32
32
  highlight: ["strokeColor", "opacity"],
33
33
  underline: ["strokeColor"],
34
34
  strikethrough: ["strokeColor"],
35
35
  squiggly: ["strokeColor"],
36
36
  };
37
- const STROKE_WIDTHS = [1, 2, 4, 8];
38
- const COLOR_PRESETS = ["#ff0000", "#ff8800", "#ffcc00", "#00cc00", "#0088ff", "#8800ff", "#000000", "#ffffff"];
39
- function selectSlice(state) {
37
+ const LINE_STYLE_DEFS = [
38
+ { id: "solid", icon: ICON_LINE_SOLID, labelKey: "tools.lineStyleSolid" },
39
+ { id: "dashed", icon: ICON_LINE_DASHED, labelKey: "tools.lineStyleDashed" },
40
+ { id: "dotted", icon: ICON_LINE_DOTTED, labelKey: "tools.lineStyleDotted" },
41
+ ];
42
+ const ARROW_HEAD_DEFS = [
43
+ { id: "none", icon: ICON_ARROW_NONE, labelKey: "tools.arrowHeadNone" },
44
+ { id: "open", icon: ICON_ARROW_OPEN, labelKey: "tools.arrowHeadOpen" },
45
+ { id: "closed", icon: ICON_ARROW_CLOSED, labelKey: "tools.arrowHeadClosed" },
46
+ ];
47
+ function selectSlice(s) {
40
48
  return {
41
- activeTool: state.activeTool,
42
- activeSubTool: state.activeSubTool,
43
- toolOptions: state.toolOptions,
49
+ activeTool: s.activeTool,
50
+ activeSubTool: s.activeSubTool,
51
+ toolOptions: s.toolOptions,
52
+ selectedAnnotation: s.selectedAnnotation,
44
53
  };
45
54
  }
46
55
  function sliceEqual(a, b) {
47
- return a.activeTool === b.activeTool && a.activeSubTool === b.activeSubTool && a.toolOptions === b.toolOptions;
56
+ return (a.activeTool === b.activeTool &&
57
+ a.activeSubTool === b.activeSubTool &&
58
+ a.toolOptions === b.toolOptions &&
59
+ a.selectedAnnotation === b.selectedAnnotation);
48
60
  }
61
+ // ====================================================================================
62
+ // createSubToolbar
63
+ // ====================================================================================
49
64
  export function createSubToolbar() {
50
65
  const el = document.createElement("div");
51
66
  el.className = "udoc-subtoolbar";
52
67
  el.setAttribute("role", "toolbar");
53
68
  el.style.display = "none";
54
- // Left: sub-tool buttons
55
69
  const toolsSection = document.createElement("div");
56
70
  toolsSection.className = "udoc-subtoolbar__tools";
57
- // Divider
58
- const divider = document.createElement("div");
59
- divider.className = "udoc-subtoolbar__divider";
60
- // Right: options
71
+ const dividerEl = document.createElement("div");
72
+ dividerEl.className = "udoc-subtoolbar__divider";
61
73
  const optionsSection = document.createElement("div");
62
74
  optionsSection.className = "udoc-subtoolbar__options";
63
- el.append(toolsSection, divider, optionsSection);
75
+ el.append(toolsSection, dividerEl, optionsSection);
76
+ // ---- Mutable state ----
77
+ let containerRef = null;
64
78
  let unsubRender = null;
65
79
  const currentToolBtns = new Map();
66
80
  let currentToolSet = null;
81
+ let builtSubTool = null;
82
+ let activeSubTool = null;
83
+ let openPanel = null;
84
+ // ---- Lazy-created option controls ----
85
+ let strokeColorSelect = null;
86
+ let fillColorSelect = null;
87
+ let strokeWidthInput = null;
88
+ let opacityInput = null;
89
+ let fontSizeInput = null;
90
+ // Delete button for select tool
91
+ let deleteBtn = null;
92
+ // Line style & arrow head (panels + triggers)
93
+ const lineStylePanel = document.createElement("div");
94
+ lineStylePanel.className = "udoc-linestyle-panel";
95
+ lineStylePanel.addEventListener("click", (e) => e.stopPropagation());
96
+ const arrowHeadPanel = document.createElement("div");
97
+ arrowHeadPanel.className = "udoc-arrowhead-panel";
98
+ arrowHeadPanel.addEventListener("click", (e) => e.stopPropagation());
99
+ let lineStyleTrigger = null;
100
+ let arrowHeadTrigger = null;
101
+ // ---- Panel management ----
102
+ function closeAllPanels() {
103
+ openPanel = null;
104
+ strokeColorSelect?.close();
105
+ fillColorSelect?.close();
106
+ lineStylePanel.classList.remove("udoc-linestyle-panel--open");
107
+ arrowHeadPanel.classList.remove("udoc-arrowhead-panel--open");
108
+ strokeWidthInput?.close();
109
+ opacityInput?.close();
110
+ fontSizeInput?.close();
111
+ }
112
+ const onDocumentClick = () => closeAllPanels();
113
+ function openPanelAt(key, trigger) {
114
+ const wasOpen = openPanel === key;
115
+ closeAllPanels();
116
+ if (wasOpen)
117
+ return;
118
+ const rect = trigger.getBoundingClientRect();
119
+ const cRect = containerRef.getBoundingClientRect();
120
+ const left = `${rect.left - cRect.left}px`;
121
+ if (key === "strokeColor")
122
+ strokeColorSelect?.open(rect, cRect);
123
+ else if (key === "fillColor")
124
+ fillColorSelect?.open(rect, cRect);
125
+ else if (key === "strokeWidth")
126
+ strokeWidthInput?.open(rect, cRect);
127
+ else if (key === "opacity")
128
+ opacityInput?.open(rect, cRect);
129
+ else if (key === "fontSize")
130
+ fontSizeInput?.open(rect, cRect);
131
+ else if (key === "lineStyle") {
132
+ lineStylePanel.style.left = left;
133
+ lineStylePanel.classList.add("udoc-linestyle-panel--open");
134
+ }
135
+ else if (key === "arrowHead") {
136
+ arrowHeadPanel.style.left = left;
137
+ arrowHeadPanel.classList.add("udoc-arrowhead-panel--open");
138
+ }
139
+ openPanel = key;
140
+ }
141
+ // ---- Dispatch helper ----
142
+ function dispatchOpt(store, key, value) {
143
+ if (activeSubTool)
144
+ store.dispatch({ type: "SET_TOOL_OPTION", subTool: activeSubTool, key, value });
145
+ }
146
+ // ---- Mount ----
67
147
  function mount(container, store, i18n) {
148
+ containerRef = container;
68
149
  container.appendChild(el);
150
+ container.appendChild(lineStylePanel);
151
+ container.appendChild(arrowHeadPanel);
69
152
  el.setAttribute("aria-label", i18n.t("tools.subtoolbar"));
153
+ document.addEventListener("click", onDocumentClick);
70
154
  const applyState = (slice) => {
71
155
  const visible = isToolSet(slice.activeTool);
72
156
  el.style.display = visible ? "flex" : "none";
73
- if (!visible)
157
+ if (!visible) {
158
+ closeAllPanels();
74
159
  return;
160
+ }
75
161
  const toolSet = slice.activeTool;
76
162
  const subTools = SUB_TOOLS_MAP[toolSet];
77
163
  if (!subTools)
@@ -87,115 +173,307 @@ export function createSubToolbar() {
87
173
  btn.innerHTML = def.icon;
88
174
  btn.title = i18n.t(def.labelKey);
89
175
  btn.setAttribute("aria-label", i18n.t(def.labelKey));
90
- btn.addEventListener("click", () => {
91
- store.dispatch({ type: "SET_SUB_TOOL", subTool: def.id });
92
- });
176
+ btn.addEventListener("click", () => store.dispatch({ type: "SET_SUB_TOOL", subTool: def.id }));
93
177
  toolsSection.appendChild(btn);
94
178
  currentToolBtns.set(def.id, btn);
95
179
  }
96
180
  }
97
- // Update active state on buttons
98
181
  for (const [id, btn] of currentToolBtns) {
99
182
  btn.classList.toggle("udoc-subtoolbar__btn--active", id === slice.activeSubTool);
100
183
  }
101
- // Build options for the active sub-tool
102
- buildOptions(slice, store, i18n);
184
+ if (!slice.activeSubTool) {
185
+ optionsSection.innerHTML = "";
186
+ builtSubTool = null;
187
+ return;
188
+ }
189
+ activeSubTool = slice.activeSubTool;
190
+ const opts = slice.toolOptions[slice.activeSubTool] ?? { ...DEFAULT_TOOL_OPTIONS };
191
+ // If sub-tool changed, rebuild the set of option controls in the toolbar
192
+ if (builtSubTool !== slice.activeSubTool) {
193
+ closeAllPanels();
194
+ buildControls(slice.activeSubTool, opts, store, i18n);
195
+ builtSubTool = slice.activeSubTool;
196
+ }
197
+ // Always update values in-place
198
+ updateValues(opts, store, i18n);
199
+ // Update delete button state for select tool
200
+ if (deleteBtn) {
201
+ deleteBtn.disabled = !slice.selectedAnnotation;
202
+ }
103
203
  };
104
204
  applyState(selectSlice(store.getState()));
105
- unsubRender = subscribeSelector(store, selectSlice, applyState, {
106
- equality: sliceEqual,
107
- });
205
+ unsubRender = subscribeSelector(store, selectSlice, applyState, { equality: sliceEqual });
108
206
  }
109
- function buildOptions(slice, store, i18n) {
207
+ // ====================================================================
208
+ // buildControls — called ONLY when active sub-tool changes
209
+ // ====================================================================
210
+ function buildControls(subTool, opts, store, i18n) {
110
211
  optionsSection.innerHTML = "";
111
- if (!slice.activeSubTool)
212
+ lineStyleTrigger = null;
213
+ arrowHeadTrigger = null;
214
+ deleteBtn = null;
215
+ // Select tool shows a delete button instead of drawing options
216
+ if (subTool === "select") {
217
+ deleteBtn = document.createElement("button");
218
+ deleteBtn.className = "udoc-subtoolbar__btn udoc-subtoolbar__btn--delete";
219
+ deleteBtn.innerHTML = ICON_DELETE;
220
+ deleteBtn.title = i18n.t("tools.deleteAnnotation");
221
+ deleteBtn.setAttribute("aria-label", i18n.t("tools.deleteAnnotation"));
222
+ deleteBtn.disabled = true;
223
+ deleteBtn.addEventListener("click", () => {
224
+ const sel = store.getState().selectedAnnotation;
225
+ if (sel) {
226
+ store.dispatch({
227
+ type: "REMOVE_ANNOTATION",
228
+ pageIndex: sel.pageIndex,
229
+ annotationIndex: sel.annotationIndex,
230
+ });
231
+ }
232
+ });
233
+ optionsSection.appendChild(deleteBtn);
112
234
  return;
113
- const supportedOptions = TOOL_OPTIONS_CONFIG[slice.activeSubTool];
114
- if (!supportedOptions || supportedOptions.length === 0)
235
+ }
236
+ const supported = TOOL_OPTIONS_CONFIG[subTool];
237
+ if (!supported || supported.length === 0)
115
238
  return;
116
- const opts = slice.toolOptions[slice.activeSubTool] ?? { ...DEFAULT_TOOL_OPTIONS };
117
- const subTool = slice.activeSubTool;
118
- for (const optKey of supportedOptions) {
119
- if (optKey === "strokeColor" || optKey === "fillColor") {
120
- const colorGroup = document.createElement("div");
121
- colorGroup.className = "udoc-subtoolbar__option-group";
122
- const label = document.createElement("span");
123
- label.className = "udoc-subtoolbar__option-label";
124
- label.textContent = i18n.t(optKey === "strokeColor" ? "tools.strokeColor" : "tools.fillColor");
125
- colorGroup.appendChild(label);
126
- const currentColor = optKey === "strokeColor" ? opts.strokeColor : opts.fillColor;
127
- for (const color of COLOR_PRESETS) {
128
- const swatch = document.createElement("button");
129
- swatch.className = "udoc-subtoolbar__color-swatch";
130
- if (currentColor === color) {
131
- swatch.classList.add("udoc-subtoolbar__color-swatch--active");
132
- }
133
- swatch.style.setProperty("--swatch-color", color);
134
- swatch.title = color;
135
- swatch.setAttribute("aria-label", `${i18n.t(optKey === "strokeColor" ? "tools.strokeColor" : "tools.fillColor")} ${color}`);
136
- swatch.addEventListener("click", () => {
137
- store.dispatch({ type: "SET_TOOL_OPTION", subTool, key: optKey, value: color });
239
+ for (const optKey of supported) {
240
+ if (optKey === "strokeColor") {
241
+ if (!strokeColorSelect) {
242
+ strokeColorSelect = createColorSelect({
243
+ variant: "stroke",
244
+ label: i18n.t("tools.strokeColor"),
245
+ noneLabel: i18n.t("tools.noFill"),
246
+ onSelect: () => closeAllPanels(),
247
+ onChange: (color) => dispatchOpt(store, "strokeColor", color),
248
+ });
249
+ containerRef.appendChild(strokeColorSelect.panel);
250
+ strokeColorSelect.el.addEventListener("click", (e) => {
251
+ e.stopPropagation();
252
+ openPanelAt("strokeColor", strokeColorSelect.el);
253
+ });
254
+ }
255
+ optionsSection.appendChild(strokeColorSelect.el);
256
+ }
257
+ else if (optKey === "fillColor") {
258
+ if (!fillColorSelect) {
259
+ fillColorSelect = createColorSelect({
260
+ variant: "fill",
261
+ label: i18n.t("tools.fillColor"),
262
+ noneLabel: i18n.t("tools.noFill"),
263
+ onSelect: () => closeAllPanels(),
264
+ onChange: (color) => dispatchOpt(store, "fillColor", color),
265
+ });
266
+ containerRef.appendChild(fillColorSelect.panel);
267
+ fillColorSelect.el.addEventListener("click", (e) => {
268
+ e.stopPropagation();
269
+ openPanelAt("fillColor", fillColorSelect.el);
138
270
  });
139
- colorGroup.appendChild(swatch);
140
271
  }
141
- optionsSection.appendChild(colorGroup);
272
+ optionsSection.appendChild(fillColorSelect.el);
142
273
  }
143
274
  else if (optKey === "strokeWidth") {
144
- const widthGroup = document.createElement("div");
145
- widthGroup.className = "udoc-subtoolbar__option-group";
146
- const label = document.createElement("span");
147
- label.className = "udoc-subtoolbar__option-label";
148
- label.textContent = i18n.t("tools.strokeWidth");
149
- widthGroup.appendChild(label);
150
- for (const w of STROKE_WIDTHS) {
151
- const btn = document.createElement("button");
152
- btn.className = "udoc-subtoolbar__width-btn";
153
- if (opts.strokeWidth === w) {
154
- btn.classList.add("udoc-subtoolbar__width-btn--active");
155
- }
156
- // Visual indicator: a horizontal line of the given width
157
- const line = document.createElement("span");
158
- line.className = "udoc-subtoolbar__width-line";
159
- line.style.height = `${w}px`;
160
- btn.appendChild(line);
161
- btn.title = `${w}px`;
162
- btn.setAttribute("aria-label", `${i18n.t("tools.strokeWidth")} ${w}px`);
163
- btn.addEventListener("click", () => {
164
- store.dispatch({ type: "SET_TOOL_OPTION", subTool, key: "strokeWidth", value: w });
275
+ if (!strokeWidthInput) {
276
+ strokeWidthInput = createNumberInput({
277
+ min: 0.5,
278
+ max: 12,
279
+ step: 0.5,
280
+ value: opts.strokeWidth,
281
+ icon: ICON_STROKE_WIDTH,
282
+ formatValue: (v) => `${v}pt`,
283
+ parseValue: (s) => {
284
+ const n = parseFloat(s.replace(/pt$/i, ""));
285
+ return isNaN(n) ? null : n;
286
+ },
287
+ label: i18n.t("tools.strokeWidth"),
288
+ onChange: (v) => dispatchOpt(store, "strokeWidth", v),
289
+ });
290
+ containerRef.appendChild(strokeWidthInput.panel);
291
+ strokeWidthInput.el.addEventListener("click", (e) => {
292
+ e.stopPropagation();
293
+ openPanelAt("strokeWidth", strokeWidthInput.el);
165
294
  });
166
- widthGroup.appendChild(btn);
167
295
  }
168
- optionsSection.appendChild(widthGroup);
296
+ optionsSection.appendChild(strokeWidthInput.el);
169
297
  }
170
298
  else if (optKey === "opacity") {
171
- const opacityGroup = document.createElement("div");
172
- opacityGroup.className = "udoc-subtoolbar__option-group";
173
- const label = document.createElement("span");
174
- label.className = "udoc-subtoolbar__option-label";
175
- label.textContent = i18n.t("tools.opacity");
176
- opacityGroup.appendChild(label);
177
- const slider = document.createElement("input");
178
- slider.type = "range";
179
- slider.className = "udoc-subtoolbar__opacity-slider";
180
- slider.min = "0.1";
181
- slider.max = "1";
182
- slider.step = "0.1";
183
- slider.value = String(opts.opacity);
184
- slider.title = `${Math.round(opts.opacity * 100)}%`;
185
- slider.setAttribute("aria-label", i18n.t("tools.opacity"));
186
- slider.addEventListener("input", () => {
187
- const value = parseFloat(slider.value);
188
- store.dispatch({ type: "SET_TOOL_OPTION", subTool, key: "opacity", value });
299
+ if (!opacityInput) {
300
+ opacityInput = createNumberInput({
301
+ min: 10,
302
+ max: 100,
303
+ step: 10,
304
+ value: Math.round(opts.opacity * 100),
305
+ icon: ICON_OPACITY,
306
+ formatValue: (v) => `${Math.round(v)}%`,
307
+ parseValue: (s) => {
308
+ const n = parseFloat(s.replace(/%$/i, ""));
309
+ return isNaN(n) ? null : n;
310
+ },
311
+ label: i18n.t("tools.opacity"),
312
+ onChange: (v) => dispatchOpt(store, "opacity", v / 100),
313
+ });
314
+ containerRef.appendChild(opacityInput.panel);
315
+ opacityInput.el.addEventListener("click", (e) => {
316
+ e.stopPropagation();
317
+ openPanelAt("opacity", opacityInput.el);
318
+ });
319
+ }
320
+ optionsSection.appendChild(opacityInput.el);
321
+ }
322
+ else if (optKey === "fontSize") {
323
+ if (!fontSizeInput) {
324
+ fontSizeInput = createNumberInput({
325
+ min: 8,
326
+ max: 144,
327
+ step: 1,
328
+ value: opts.fontSize,
329
+ icon: ICON_FONT_SIZE,
330
+ formatValue: (v) => `${v}pt`,
331
+ parseValue: (s) => {
332
+ const n = parseFloat(s.replace(/pt$/i, ""));
333
+ return isNaN(n) ? null : n;
334
+ },
335
+ label: i18n.t("tools.fontSize"),
336
+ onChange: (v) => dispatchOpt(store, "fontSize", v),
337
+ });
338
+ containerRef.appendChild(fontSizeInput.panel);
339
+ fontSizeInput.el.addEventListener("click", (e) => {
340
+ e.stopPropagation();
341
+ openPanelAt("fontSize", fontSizeInput.el);
342
+ });
343
+ }
344
+ optionsSection.appendChild(fontSizeInput.el);
345
+ }
346
+ else if (optKey === "lineStyle") {
347
+ lineStyleTrigger = document.createElement("button");
348
+ lineStyleTrigger.className =
349
+ "udoc-subtoolbar__dropdown-trigger udoc-subtoolbar__dropdown-trigger--icon";
350
+ lineStyleTrigger.title = i18n.t("tools.lineStyle");
351
+ const arrow = document.createElement("span");
352
+ arrow.className = "udoc-subtoolbar__dropdown-arrow";
353
+ arrow.textContent = "\u25BE";
354
+ lineStyleTrigger.appendChild(arrow);
355
+ lineStyleTrigger.addEventListener("click", (e) => {
356
+ e.stopPropagation();
357
+ openPanelAt("lineStyle", lineStyleTrigger);
358
+ });
359
+ optionsSection.appendChild(lineStyleTrigger);
360
+ }
361
+ else if (optKey === "arrowHead") {
362
+ arrowHeadTrigger = document.createElement("button");
363
+ arrowHeadTrigger.className =
364
+ "udoc-subtoolbar__dropdown-trigger udoc-subtoolbar__dropdown-trigger--icon";
365
+ arrowHeadTrigger.title = i18n.t("tools.arrowHeadEnd");
366
+ const arrow = document.createElement("span");
367
+ arrow.className = "udoc-subtoolbar__dropdown-arrow";
368
+ arrow.textContent = "\u25BE";
369
+ arrowHeadTrigger.appendChild(arrow);
370
+ arrowHeadTrigger.addEventListener("click", (e) => {
371
+ e.stopPropagation();
372
+ openPanelAt("arrowHead", arrowHeadTrigger);
373
+ });
374
+ optionsSection.appendChild(arrowHeadTrigger);
375
+ }
376
+ }
377
+ }
378
+ // ====================================================================
379
+ // updateValues — called on EVERY state change, updates in-place
380
+ // ====================================================================
381
+ function updateValues(opts, store, i18n) {
382
+ // Color selects
383
+ strokeColorSelect?.update(opts.strokeColor);
384
+ fillColorSelect?.update(opts.fillColor);
385
+ // Number inputs
386
+ strokeWidthInput?.update(opts.strokeWidth);
387
+ opacityInput?.update(Math.round(opts.opacity * 100));
388
+ fontSizeInput?.update(opts.fontSize);
389
+ // Line style trigger + panel
390
+ if (lineStyleTrigger) {
391
+ const def = LINE_STYLE_DEFS.find((s) => s.id === opts.lineStyle) ?? LINE_STYLE_DEFS[0];
392
+ const arrowEl = lineStyleTrigger.lastElementChild;
393
+ lineStyleTrigger.innerHTML = def.icon;
394
+ if (arrowEl)
395
+ lineStyleTrigger.appendChild(arrowEl);
396
+ lineStyleTrigger.setAttribute("aria-label", `${i18n.t("tools.lineStyle")}: ${i18n.t(def.labelKey)}`);
397
+ }
398
+ rebuildLineStylePanel(opts.lineStyle, store, i18n);
399
+ // Arrow head trigger + panel
400
+ if (arrowHeadTrigger) {
401
+ const endDef = ARROW_HEAD_DEFS.find((a) => a.id === opts.arrowHeadEnd) ?? ARROW_HEAD_DEFS[0];
402
+ const arrowEl = arrowHeadTrigger.lastElementChild;
403
+ arrowHeadTrigger.innerHTML = endDef.icon;
404
+ if (arrowEl)
405
+ arrowHeadTrigger.appendChild(arrowEl);
406
+ }
407
+ rebuildArrowHeadPanel(opts.arrowHeadStart, opts.arrowHeadEnd, store, i18n);
408
+ }
409
+ // ---- Line style panel ----
410
+ function rebuildLineStylePanel(current, store, i18n) {
411
+ lineStylePanel.innerHTML = "";
412
+ for (const def of LINE_STYLE_DEFS) {
413
+ const item = document.createElement("button");
414
+ item.className = "udoc-linestyle-panel__item";
415
+ if (current === def.id)
416
+ item.classList.add("udoc-linestyle-panel__item--active");
417
+ const icon = document.createElement("span");
418
+ icon.className = "udoc-linestyle-panel__icon";
419
+ icon.innerHTML = def.icon;
420
+ const label = document.createElement("span");
421
+ label.className = "udoc-linestyle-panel__label";
422
+ label.textContent = i18n.t(def.labelKey);
423
+ item.append(icon, label);
424
+ item.title = i18n.t(def.labelKey);
425
+ item.addEventListener("click", (e) => {
426
+ e.stopPropagation();
427
+ closeAllPanels();
428
+ dispatchOpt(store, "lineStyle", def.id);
429
+ });
430
+ lineStylePanel.appendChild(item);
431
+ }
432
+ }
433
+ // ---- Arrow head panel ----
434
+ function rebuildArrowHeadPanel(startVal, endVal, store, i18n) {
435
+ arrowHeadPanel.innerHTML = "";
436
+ for (const [rowKey, rowLabel, currentVal] of [
437
+ ["arrowHeadStart", i18n.t("tools.arrowHeadStart"), startVal],
438
+ ["arrowHeadEnd", i18n.t("tools.arrowHeadEnd"), endVal],
439
+ ]) {
440
+ const row = document.createElement("div");
441
+ row.className = "udoc-arrowhead-panel__row";
442
+ const lbl = document.createElement("span");
443
+ lbl.className = "udoc-arrowhead-panel__row-label";
444
+ lbl.textContent = rowLabel;
445
+ row.appendChild(lbl);
446
+ for (const def of ARROW_HEAD_DEFS) {
447
+ const btn = document.createElement("button");
448
+ btn.className = "udoc-arrowhead-panel__btn";
449
+ if (currentVal === def.id)
450
+ btn.classList.add("udoc-arrowhead-panel__btn--active");
451
+ btn.innerHTML = def.icon;
452
+ btn.title = i18n.t(def.labelKey);
453
+ if (rowKey === "arrowHeadStart")
454
+ btn.style.transform = "scaleX(-1)";
455
+ btn.addEventListener("click", (e) => {
456
+ e.stopPropagation();
457
+ dispatchOpt(store, rowKey, def.id);
189
458
  });
190
- opacityGroup.appendChild(slider);
191
- optionsSection.appendChild(opacityGroup);
459
+ row.appendChild(btn);
192
460
  }
461
+ arrowHeadPanel.appendChild(row);
193
462
  }
194
463
  }
464
+ // ---- Destroy ----
195
465
  function destroy() {
196
466
  if (unsubRender)
197
467
  unsubRender();
468
+ document.removeEventListener("click", onDocumentClick);
198
469
  currentToolBtns.clear();
470
+ strokeColorSelect?.destroy();
471
+ fillColorSelect?.destroy();
472
+ strokeWidthInput?.destroy();
473
+ opacityInput?.destroy();
474
+ fontSizeInput?.destroy();
475
+ lineStylePanel.remove();
476
+ arrowHeadPanel.remove();
199
477
  el.remove();
200
478
  }
201
479
  return { el, mount, destroy };