@xom11/whiteboard 0.10.1 → 0.24.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.
Files changed (112) hide show
  1. package/README.md +67 -0
  2. package/dist/{ExcalidrawWithMenus-EAVPOPJZ.mjs → ExcalidrawWithMenus-KBLDWPM2.mjs} +2 -3
  3. package/dist/ExcalidrawWithMenus-KBLDWPM2.mjs.map +1 -0
  4. package/dist/catalog.json +57 -0
  5. package/dist/{chunk-PWIMZIB6.mjs → chunk-2SKXRBGS.mjs} +7 -8
  6. package/dist/chunk-2SKXRBGS.mjs.map +1 -0
  7. package/dist/chunk-33PEN2WC.mjs +57 -0
  8. package/dist/chunk-33PEN2WC.mjs.map +1 -0
  9. package/dist/chunk-3KBL77M6.mjs +127 -0
  10. package/dist/chunk-3KBL77M6.mjs.map +1 -0
  11. package/dist/chunk-5UTGXHLJ.mjs +57 -0
  12. package/dist/chunk-5UTGXHLJ.mjs.map +1 -0
  13. package/dist/chunk-6XUPIGVD.mjs +467 -0
  14. package/dist/chunk-6XUPIGVD.mjs.map +1 -0
  15. package/dist/chunk-7WG2KDRF.mjs +28 -0
  16. package/dist/chunk-7WG2KDRF.mjs.map +1 -0
  17. package/dist/chunk-FZY33J6Z.mjs +95 -0
  18. package/dist/chunk-FZY33J6Z.mjs.map +1 -0
  19. package/dist/chunk-HNQLZIEP.mjs +78 -0
  20. package/dist/chunk-HNQLZIEP.mjs.map +1 -0
  21. package/dist/chunk-NVJ7K3DK.mjs +29 -0
  22. package/dist/chunk-NVJ7K3DK.mjs.map +1 -0
  23. package/dist/chunk-O4WIZFRQ.mjs +11 -0
  24. package/dist/chunk-O4WIZFRQ.mjs.map +1 -0
  25. package/dist/{chunk-YVJP7NRG.mjs → chunk-O6QTYAKE.mjs} +7 -9
  26. package/dist/chunk-O6QTYAKE.mjs.map +1 -0
  27. package/dist/chunk-R5FL6S7L.mjs +22 -0
  28. package/dist/chunk-R5FL6S7L.mjs.map +1 -0
  29. package/dist/chunk-RBUILBX3.mjs +388 -0
  30. package/dist/chunk-RBUILBX3.mjs.map +1 -0
  31. package/dist/chunk-RD34F5PM.mjs +57 -0
  32. package/dist/chunk-RD34F5PM.mjs.map +1 -0
  33. package/dist/{chunk-7P7SQFOW.mjs → chunk-RXOFO64U.mjs} +3 -3
  34. package/dist/chunk-RXOFO64U.mjs.map +1 -0
  35. package/dist/chunk-TOOHCAWP.mjs +1167 -0
  36. package/dist/chunk-TOOHCAWP.mjs.map +1 -0
  37. package/dist/{chunk-C6SCVOMC.mjs → chunk-TQYQVXNW.mjs} +5 -41
  38. package/dist/chunk-TQYQVXNW.mjs.map +1 -0
  39. package/dist/chunk-VBJLUHCY.mjs +23 -0
  40. package/dist/chunk-VBJLUHCY.mjs.map +1 -0
  41. package/dist/chunk-VRWZILTG.mjs +205 -0
  42. package/dist/chunk-VRWZILTG.mjs.map +1 -0
  43. package/dist/chunk-XVSO7FBM.mjs +61 -0
  44. package/dist/chunk-XVSO7FBM.mjs.map +1 -0
  45. package/dist/geometry-2d.d.mts +3 -6
  46. package/dist/geometry-2d.d.ts +3 -6
  47. package/dist/geometry-2d.js +5069 -2651
  48. package/dist/geometry-2d.js.map +1 -1
  49. package/dist/geometry-2d.mjs +8 -4
  50. package/dist/geometry-3d.d.mts +4 -7
  51. package/dist/geometry-3d.d.ts +4 -7
  52. package/dist/geometry-3d.js +3053 -2150
  53. package/dist/geometry-3d.js.map +1 -1
  54. package/dist/geometry-3d.mjs +7 -4
  55. package/dist/graph-2d.d.mts +4 -7
  56. package/dist/graph-2d.d.ts +4 -7
  57. package/dist/graph-2d.js +3363 -1670
  58. package/dist/graph-2d.js.map +1 -1
  59. package/dist/graph-2d.mjs +10 -3
  60. package/dist/host-3N4E4KJH.mjs +1142 -0
  61. package/dist/host-3N4E4KJH.mjs.map +1 -0
  62. package/dist/{host-Z3TEJKZA.mjs → host-6SNSZ332.mjs} +4 -4
  63. package/dist/{host-Z3TEJKZA.mjs.map → host-6SNSZ332.mjs.map} +1 -1
  64. package/dist/host-EVJT3LIF.mjs +3198 -0
  65. package/dist/host-EVJT3LIF.mjs.map +1 -0
  66. package/dist/host-HN4X3TBC.mjs +2374 -0
  67. package/dist/host-HN4X3TBC.mjs.map +1 -0
  68. package/dist/index.css +4 -1
  69. package/dist/index.css.map +1 -1
  70. package/dist/index.d.mts +675 -19
  71. package/dist/index.d.ts +675 -19
  72. package/dist/index.js +11764 -9417
  73. package/dist/index.js.map +1 -1
  74. package/dist/index.mjs +1492 -335
  75. package/dist/index.mjs.map +1 -1
  76. package/dist/latex.d.mts +3 -4
  77. package/dist/latex.d.ts +3 -4
  78. package/dist/latex.js +33 -18
  79. package/dist/latex.js.map +1 -1
  80. package/dist/latex.mjs +2 -3
  81. package/dist/render-OCVGDKK6.mjs +8 -0
  82. package/dist/render-OCVGDKK6.mjs.map +1 -0
  83. package/dist/serialize-GKN6OVPM.mjs +6 -0
  84. package/dist/serialize-GKN6OVPM.mjs.map +1 -0
  85. package/dist/{types-CinstD7T.d.mts → types-rA4slL08.d.mts} +69 -4
  86. package/dist/{types-CinstD7T.d.ts → types-rA4slL08.d.ts} +69 -4
  87. package/package.json +24 -5
  88. package/dist/ExcalidrawWithMenus-EAVPOPJZ.mjs.map +0 -1
  89. package/dist/chunk-74VEEZBV.mjs +0 -619
  90. package/dist/chunk-74VEEZBV.mjs.map +0 -1
  91. package/dist/chunk-7P7SQFOW.mjs.map +0 -1
  92. package/dist/chunk-BJTO5JO5.mjs +0 -11
  93. package/dist/chunk-BJTO5JO5.mjs.map +0 -1
  94. package/dist/chunk-C6SCVOMC.mjs.map +0 -1
  95. package/dist/chunk-D257NCQW.mjs +0 -58
  96. package/dist/chunk-D257NCQW.mjs.map +0 -1
  97. package/dist/chunk-G7FR3AIV.mjs +0 -193
  98. package/dist/chunk-G7FR3AIV.mjs.map +0 -1
  99. package/dist/chunk-HTBLO5JO.mjs +0 -41
  100. package/dist/chunk-HTBLO5JO.mjs.map +0 -1
  101. package/dist/chunk-PWIMZIB6.mjs.map +0 -1
  102. package/dist/chunk-SBDMF4NQ.mjs +0 -212
  103. package/dist/chunk-SBDMF4NQ.mjs.map +0 -1
  104. package/dist/chunk-WQOABS6N.mjs +0 -197
  105. package/dist/chunk-WQOABS6N.mjs.map +0 -1
  106. package/dist/chunk-YVJP7NRG.mjs.map +0 -1
  107. package/dist/host-N6ACNJKI.mjs +0 -3226
  108. package/dist/host-N6ACNJKI.mjs.map +0 -1
  109. package/dist/host-NKGV6RF2.mjs +0 -1134
  110. package/dist/host-NKGV6RF2.mjs.map +0 -1
  111. package/dist/host-XVK7UCRE.mjs +0 -2908
  112. package/dist/host-XVK7UCRE.mjs.map +0 -1
@@ -0,0 +1,1167 @@
1
+ "use client";
2
+ import { createEmptyState, listObjects } from './chunk-3KBL77M6.mjs';
3
+ import { createStore, getKind } from './chunk-VRWZILTG.mjs';
4
+ import * as React2 from 'react';
5
+ import React2__default, { createContext, useRef, useReducer, useCallback, useEffect, useMemo, useContext, useState } from 'react';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
+ import { createPortal } from 'react-dom';
8
+
9
+ function CloseIcon() {
10
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
11
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
12
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
13
+ ] });
14
+ }
15
+ function LeftPanelShell(props) {
16
+ const { title, icon, onClose, isDark, tabs, activeTab, onTabChange, testId, children } = props;
17
+ const showTabs = !!tabs && tabs.length >= 2;
18
+ return /* @__PURE__ */ jsxs(
19
+ "aside",
20
+ {
21
+ role: "complementary",
22
+ "aria-label": title,
23
+ "data-testid": testId ?? "left-panel",
24
+ "data-stamp-area": "true",
25
+ className: [
26
+ isDark ? "theme--dark " : "",
27
+ "absolute left-0 top-0 z-30 flex h-full w-60 flex-col border-r border-slate-200 bg-white shadow-md animate-in slide-in-from-left duration-200"
28
+ ].join(""),
29
+ children: [
30
+ /* @__PURE__ */ jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-3 py-2", children: [
31
+ /* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
32
+ /* @__PURE__ */ jsx("span", { className: "text-base leading-none", children: icon }),
33
+ title
34
+ ] }),
35
+ /* @__PURE__ */ jsx(
36
+ "button",
37
+ {
38
+ type: "button",
39
+ onClick: onClose,
40
+ "aria-label": "\u0110\xF3ng",
41
+ className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
42
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
43
+ }
44
+ )
45
+ ] }),
46
+ showTabs && /* @__PURE__ */ jsx("div", { role: "tablist", className: "flex gap-1 rounded-md bg-slate-100 p-0.5 mx-3 mt-3", children: tabs.map((t) => /* @__PURE__ */ jsx(
47
+ TabPill,
48
+ {
49
+ active: t.key === activeTab,
50
+ onClick: () => onTabChange?.(t.key),
51
+ testId: t.testId,
52
+ children: t.label
53
+ },
54
+ t.key
55
+ )) }),
56
+ /* @__PURE__ */ jsx(
57
+ "div",
58
+ {
59
+ ...showTabs ? { role: "tabpanel" } : {},
60
+ className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-3",
61
+ children
62
+ }
63
+ )
64
+ ]
65
+ }
66
+ );
67
+ }
68
+ function TabPill(props) {
69
+ const { active, onClick, testId, children } = props;
70
+ return /* @__PURE__ */ jsx(
71
+ "button",
72
+ {
73
+ type: "button",
74
+ role: "tab",
75
+ "aria-selected": active,
76
+ onClick,
77
+ "data-testid": testId,
78
+ className: [
79
+ "flex-1 rounded px-2 py-1 text-[11px] font-medium transition",
80
+ active ? "bg-white text-slate-900 shadow-sm ring-1 ring-slate-200" : "text-slate-500 hover:text-slate-800"
81
+ ].join(" "),
82
+ children
83
+ }
84
+ );
85
+ }
86
+ function Section(props) {
87
+ return /* @__PURE__ */ jsxs("section", { children: [
88
+ /* @__PURE__ */ jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: props.label }),
89
+ props.children
90
+ ] });
91
+ }
92
+
93
+ // src/core/scene/ui/kindMeta.ts
94
+ var POINT_COLOR = "#1e40af";
95
+ var CURVE_COLOR = "#0f172a";
96
+ var PLANE_COLOR = "#60a5fa";
97
+ var KIND_UI_META = {
98
+ // 2D
99
+ point: { displayName: "\u0110i\u1EC3m", icon: "\xB7", defaultColor: POINT_COLOR },
100
+ segment: { displayName: "\u0110o\u1EA1n th\u1EB3ng", icon: "\u2014", defaultColor: CURVE_COLOR },
101
+ line: { displayName: "\u0110\u01B0\u1EDDng th\u1EB3ng", icon: "/", defaultColor: CURVE_COLOR },
102
+ ray: { displayName: "Tia", icon: "\u2192", defaultColor: CURVE_COLOR },
103
+ vector: { displayName: "Vector", icon: "\u2197", defaultColor: CURVE_COLOR },
104
+ circle: { displayName: "\u0110\u01B0\u1EDDng tr\xF2n", icon: "\u25CB", defaultColor: CURVE_COLOR },
105
+ polygon: { displayName: "\u0110a gi\xE1c", icon: "\u25C7", defaultColor: CURVE_COLOR },
106
+ intersection: { displayName: "Giao \u0111i\u1EC3m", icon: "\u2715", defaultColor: POINT_COLOR },
107
+ angle: { displayName: "G\xF3c", icon: "\u2220", defaultColor: "#16a34a" },
108
+ distance: { displayName: "Kho\u1EA3ng c\xE1ch", icon: "\u2194", defaultColor: "#dc2626" },
109
+ // 3D
110
+ point3d: { displayName: "\u0110i\u1EC3m", icon: "\xB7", defaultColor: POINT_COLOR },
111
+ segment3d: { displayName: "\u0110o\u1EA1n th\u1EB3ng", icon: "\u2014", defaultColor: CURVE_COLOR },
112
+ line3d: { displayName: "\u0110\u01B0\u1EDDng th\u1EB3ng", icon: "/", defaultColor: CURVE_COLOR },
113
+ ray3d: { displayName: "Tia", icon: "\u2192", defaultColor: CURVE_COLOR },
114
+ vector3d: { displayName: "Vector", icon: "\u2197", defaultColor: CURVE_COLOR },
115
+ plane3d: { displayName: "M\u1EB7t ph\u1EB3ng", icon: "\u25B1", defaultColor: PLANE_COLOR },
116
+ polygon3d: { displayName: "\u0110a gi\xE1c", icon: "\u25C7", defaultColor: CURVE_COLOR },
117
+ sphere3d: { displayName: "M\u1EB7t c\u1EA7u", icon: "\u25EF", defaultColor: PLANE_COLOR },
118
+ polyhedron3d: { displayName: "\u0110a di\u1EC7n", icon: "\u2B22", defaultColor: PLANE_COLOR },
119
+ cylinder3d: { displayName: "H\xECnh tr\u1EE5", icon: "\u232D", defaultColor: PLANE_COLOR },
120
+ cone3d: { displayName: "H\xECnh n\xF3n", icon: "\u25B2", defaultColor: PLANE_COLOR },
121
+ // Graph 2D
122
+ function2d: { displayName: "H\xE0m s\u1ED1", icon: "\u0192", defaultColor: CURVE_COLOR },
123
+ parameter: { displayName: "Tham s\u1ED1", icon: "\u03B1", defaultColor: "#7c3aed" },
124
+ pointOnCurve: { displayName: "\u0110i\u1EC3m tr\xEAn \u0111\u1ED3 th\u1ECB", icon: "\u25C9", defaultColor: POINT_COLOR },
125
+ tangent2d: { displayName: "Ti\u1EBFp tuy\u1EBFn", icon: "\u2571", defaultColor: CURVE_COLOR },
126
+ extremum2d: { displayName: "C\u1EF1c tr\u1ECB", icon: "\u2227", defaultColor: POINT_COLOR },
127
+ root2d: { displayName: "Nghi\u1EC7m", icon: "0", defaultColor: POINT_COLOR },
128
+ slope2d: { displayName: "H\u1EC7 s\u1ED1 g\xF3c", icon: "\u25B3", defaultColor: "#dc2626" }
129
+ };
130
+ function getKindUiMeta(kind) {
131
+ return KIND_UI_META[kind] ?? { displayName: kind, icon: "?", defaultColor: "#888888" };
132
+ }
133
+ function ObjectRowMenu(props) {
134
+ const { locked, onToggleLocked, onRename, onChangeColor, onDelete } = props;
135
+ const [open, setOpen] = React2.useState(false);
136
+ return /* @__PURE__ */ jsxs("div", { className: "relative inline-block", children: [
137
+ /* @__PURE__ */ jsx(
138
+ "button",
139
+ {
140
+ type: "button",
141
+ "aria-label": "Row menu",
142
+ onClick: (e) => {
143
+ e.stopPropagation();
144
+ setOpen((v) => !v);
145
+ },
146
+ className: "rounded px-1.5 text-black",
147
+ children: "\u22EE"
148
+ }
149
+ ),
150
+ open ? /* @__PURE__ */ jsxs(
151
+ "div",
152
+ {
153
+ role: "menu",
154
+ className: "absolute right-0 z-10 mt-1 w-40 rounded-md border border-zinc-200 bg-white py-1 text-xs shadow-lg dark:border-zinc-700 dark:bg-zinc-900",
155
+ onClick: (e) => e.stopPropagation(),
156
+ children: [
157
+ /* @__PURE__ */ jsx(MenuItem, { onClick: () => {
158
+ setOpen(false);
159
+ onRename();
160
+ }, children: "\u0110\u1ED5i t\xEAn" }),
161
+ /* @__PURE__ */ jsx(MenuItem, { onClick: () => {
162
+ setOpen(false);
163
+ onChangeColor();
164
+ }, children: "\u0110\u1ED5i m\xE0u" }),
165
+ /* @__PURE__ */ jsx(MenuItem, { onClick: () => {
166
+ setOpen(false);
167
+ onToggleLocked();
168
+ }, children: locked ? "M\u1EDF kho\xE1" : "Kho\xE1" }),
169
+ /* @__PURE__ */ jsx(
170
+ MenuItem,
171
+ {
172
+ onClick: () => {
173
+ setOpen(false);
174
+ onDelete();
175
+ },
176
+ className: "text-red-600 dark:text-red-400",
177
+ children: "Xo\xE1"
178
+ }
179
+ )
180
+ ]
181
+ }
182
+ ) : null
183
+ ] });
184
+ }
185
+ function MenuItem({
186
+ children,
187
+ onClick,
188
+ className
189
+ }) {
190
+ return /* @__PURE__ */ jsx(
191
+ "button",
192
+ {
193
+ type: "button",
194
+ role: "menuitem",
195
+ onClick,
196
+ className: `block w-full px-3 py-1 text-left text-black ${className ?? ""}`,
197
+ children
198
+ }
199
+ );
200
+ }
201
+ function formatMeasure(items) {
202
+ return items.map((it) => `${it.label} = ${it.value.toFixed(2)}`).join(", ");
203
+ }
204
+ function ObjectRow(props) {
205
+ const { obj, state, selected, onSelect, onToggleVisible, onToggleLocked, onRename, onChangeColor, onDelete } = props;
206
+ const meta = getKindUiMeta(obj.kind);
207
+ let title = "";
208
+ try {
209
+ title = getKind(obj.kind).describe(obj, state);
210
+ } catch {
211
+ title = `${meta.displayName} ${obj.label}`;
212
+ }
213
+ let measureText = null;
214
+ if (selected) {
215
+ try {
216
+ const m = getKind(obj.kind).measure?.(obj, state);
217
+ if (m && m.length > 0) measureText = formatMeasure(m);
218
+ } catch {
219
+ measureText = null;
220
+ }
221
+ }
222
+ const color = obj.attrs.color ?? meta.defaultColor;
223
+ return /* @__PURE__ */ jsxs(
224
+ "li",
225
+ {
226
+ "data-testid": `object-row-${obj.id}`,
227
+ "aria-selected": selected,
228
+ onClick: () => onSelect(obj.id),
229
+ className: "flex flex-col border-b border-zinc-100 cursor-pointer dark:border-zinc-800 " + (selected ? "bg-slate-200" : ""),
230
+ children: [
231
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 text-xs", children: [
232
+ /* @__PURE__ */ jsx(
233
+ "button",
234
+ {
235
+ type: "button",
236
+ "aria-label": "Toggle visibility",
237
+ "aria-pressed": !obj.visible,
238
+ onClick: (e) => {
239
+ e.stopPropagation();
240
+ onToggleVisible(obj.id);
241
+ },
242
+ className: "h-4 w-4 shrink-0 rounded-full border-2 transition",
243
+ style: {
244
+ backgroundColor: obj.visible ? color : "transparent",
245
+ borderColor: color
246
+ }
247
+ }
248
+ ),
249
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate text-black", children: title }),
250
+ /* @__PURE__ */ jsx(
251
+ ObjectRowMenu,
252
+ {
253
+ locked: obj.locked,
254
+ onToggleLocked: () => onToggleLocked(obj.id),
255
+ onRename: () => onRename(obj.id),
256
+ onChangeColor: () => onChangeColor(obj.id),
257
+ onDelete: () => onDelete(obj.id)
258
+ }
259
+ )
260
+ ] }),
261
+ selected && measureText && /* @__PURE__ */ jsx(
262
+ "div",
263
+ {
264
+ "data-testid": `object-row-detail-${obj.id}`,
265
+ className: "pl-9 pr-3 pb-1.5 text-[11px] text-black",
266
+ children: measureText
267
+ }
268
+ )
269
+ ]
270
+ }
271
+ );
272
+ }
273
+ function ObjectListPanel(props) {
274
+ const { store, selectedId, onSelect, renderRow } = props;
275
+ const subscribe = React2.useCallback(
276
+ (cb) => store.subscribe(() => cb()),
277
+ [store]
278
+ );
279
+ const state = React2.useSyncExternalStore(subscribe, store.getState, store.getState);
280
+ const objects = listObjects(state);
281
+ function handleSelect(id) {
282
+ onSelect?.(id === selectedId ? null : id);
283
+ }
284
+ function handleToggleVisible(id) {
285
+ const obj = state.objects[id];
286
+ if (!obj) return;
287
+ store.dispatch({ type: "UPDATE", payload: { id, patch: { visible: !obj.visible } } });
288
+ }
289
+ function handleToggleLocked(id) {
290
+ const obj = state.objects[id];
291
+ if (!obj) return;
292
+ store.dispatch({ type: "UPDATE", payload: { id, patch: { locked: !obj.locked } } });
293
+ }
294
+ function handleDelete(id) {
295
+ store.dispatch({ type: "DELETE", payload: { id } });
296
+ }
297
+ function noop() {
298
+ }
299
+ return /* @__PURE__ */ jsx(
300
+ "ul",
301
+ {
302
+ "data-testid": "object-list-panel",
303
+ className: "flex max-h-[calc(100vh-200px)] flex-col overflow-y-auto",
304
+ children: objects.length === 0 ? /* @__PURE__ */ jsx("li", { className: "px-3 py-4 text-center text-xs text-zinc-500", children: "Ch\u01B0a c\xF3 \u0111\u1ED1i t\u01B0\u1EE3ng n\xE0o" }) : objects.map((obj) => {
305
+ const selected = obj.id === selectedId;
306
+ const onClick = () => handleSelect(obj.id);
307
+ if (renderRow) {
308
+ const custom = renderRow(obj, { selected, onClick });
309
+ if (custom != null) {
310
+ return /* @__PURE__ */ jsx(React2.Fragment, { children: custom }, obj.id);
311
+ }
312
+ }
313
+ return /* @__PURE__ */ jsx(
314
+ ObjectRow,
315
+ {
316
+ obj,
317
+ state,
318
+ selected,
319
+ onSelect: handleSelect,
320
+ onToggleVisible: handleToggleVisible,
321
+ onToggleLocked: handleToggleLocked,
322
+ onRename: noop,
323
+ onChangeColor: noop,
324
+ onDelete: handleDelete
325
+ },
326
+ obj.id
327
+ );
328
+ })
329
+ }
330
+ );
331
+ }
332
+ function UndoIcon() {
333
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
334
+ /* @__PURE__ */ jsx("path", { d: "M3 10 L8 5 L8 8 L15 8 A5 5 0 0 1 20 13 L20 16" }),
335
+ /* @__PURE__ */ jsx("path", { d: "M3 10 L8 15 L8 12" })
336
+ ] });
337
+ }
338
+ function RedoIcon() {
339
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
340
+ /* @__PURE__ */ jsx("path", { d: "M21 10 L16 5 L16 8 L9 8 A5 5 0 0 0 4 13 L4 16" }),
341
+ /* @__PURE__ */ jsx("path", { d: "M21 10 L16 15 L16 12" })
342
+ ] });
343
+ }
344
+ function AxisGridSection(props) {
345
+ const { view, history } = props;
346
+ if (!view && !history) return null;
347
+ const sectionLabel = view?.sectionLabel ?? "B\u1ED1 c\u1EE5c";
348
+ const axisLabel = view?.axisLabel ?? "Tr\u1EE5c";
349
+ const gridLabel = view?.gridLabel ?? "L\u01B0\u1EDBi";
350
+ return /* @__PURE__ */ jsx(Section, { label: sectionLabel, children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 text-[11px] text-slate-700", children: [
351
+ view && /* @__PURE__ */ jsxs(Fragment, { children: [
352
+ /* @__PURE__ */ jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
353
+ /* @__PURE__ */ jsx(
354
+ "input",
355
+ {
356
+ type: "checkbox",
357
+ checked: view.showAxis,
358
+ onChange: (e) => view.onShowAxisChange(e.target.checked),
359
+ "data-testid": "toggle-axis"
360
+ }
361
+ ),
362
+ axisLabel
363
+ ] }),
364
+ /* @__PURE__ */ jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
365
+ /* @__PURE__ */ jsx(
366
+ "input",
367
+ {
368
+ type: "checkbox",
369
+ checked: view.showGrid,
370
+ onChange: (e) => view.onShowGridChange(e.target.checked),
371
+ "data-testid": "toggle-grid"
372
+ }
373
+ ),
374
+ gridLabel
375
+ ] })
376
+ ] }),
377
+ history && /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-0.5", children: [
378
+ /* @__PURE__ */ jsx(
379
+ "button",
380
+ {
381
+ type: "button",
382
+ onClick: history.onUndo,
383
+ disabled: !history.canUndo,
384
+ title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
385
+ "aria-label": "Ho\xE0n t\xE1c",
386
+ "data-testid": "undo-btn",
387
+ className: "inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
388
+ children: /* @__PURE__ */ jsx(UndoIcon, {})
389
+ }
390
+ ),
391
+ /* @__PURE__ */ jsx(
392
+ "button",
393
+ {
394
+ type: "button",
395
+ onClick: history.onRedo,
396
+ disabled: !history.canRedo,
397
+ title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
398
+ "aria-label": "L\xE0m l\u1EA1i",
399
+ "data-testid": "redo-btn",
400
+ className: "inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
401
+ children: /* @__PURE__ */ jsx(RedoIcon, {})
402
+ }
403
+ )
404
+ ] })
405
+ ] }) });
406
+ }
407
+
408
+ // src/stamps/shared/StampLeftPanel/types.ts
409
+ var TOOLTIP_DELAY_MS = 400;
410
+
411
+ // src/stamps/shared/StampLeftPanel/useToolHoverTooltip.ts
412
+ function useToolHoverTooltip() {
413
+ const [hover, setHover] = useState(null);
414
+ const [portalReady, setPortalReady] = useState(false);
415
+ const hoverTimerRef = useRef(null);
416
+ useEffect(() => {
417
+ setPortalReady(true);
418
+ return () => {
419
+ if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
420
+ };
421
+ }, []);
422
+ const showHover = useCallback((el, t) => {
423
+ if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
424
+ hoverTimerRef.current = setTimeout(() => {
425
+ const r = el.getBoundingClientRect();
426
+ setHover({ label: t.label, hint: t.hint, x: r.right, y: r.top + r.height / 2 });
427
+ }, TOOLTIP_DELAY_MS);
428
+ }, []);
429
+ const hideHover = useCallback(() => {
430
+ if (hoverTimerRef.current) {
431
+ clearTimeout(hoverTimerRef.current);
432
+ hoverTimerRef.current = null;
433
+ }
434
+ setHover(null);
435
+ }, []);
436
+ return { hover, portalReady, showHover, hideHover };
437
+ }
438
+ function ToolGrid(props) {
439
+ const { tools, groupOrder, groupLabels, activeTool, onToolChange, chord } = props;
440
+ const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip();
441
+ const grouped = useMemo(() => {
442
+ var _a;
443
+ const acc = {};
444
+ for (const t of tools) {
445
+ (acc[_a = t.group] ?? (acc[_a] = [])).push(t);
446
+ }
447
+ return acc;
448
+ }, [tools]);
449
+ const groupKeys = useMemo(
450
+ () => groupOrder.filter((g) => grouped[g]),
451
+ [grouped, groupOrder]
452
+ );
453
+ const activeGroupTools = chord?.activeGroup ? grouped[chord.activeGroup] ?? null : null;
454
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
455
+ groupKeys.map((group) => {
456
+ const isChordActive = chord?.activeGroup === group;
457
+ const dimmed = chord?.activeGroup != null && !isChordActive;
458
+ return /* @__PURE__ */ jsxs(
459
+ "section",
460
+ {
461
+ "data-chord-group": group,
462
+ "data-chord-active": isChordActive ? "true" : "false",
463
+ className: [
464
+ "rounded-md transition",
465
+ isChordActive ? "bg-emerald-50 ring-1 ring-emerald-400 p-1" : "p-0",
466
+ dimmed ? "opacity-55" : "opacity-100"
467
+ ].join(" "),
468
+ children: [
469
+ /* @__PURE__ */ jsxs("h4", { className: "mb-1.5 flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: [
470
+ /* @__PURE__ */ jsx("span", { children: groupLabels[group] }),
471
+ chord && /* @__PURE__ */ jsx(
472
+ "span",
473
+ {
474
+ "data-testid": `chord-letter-${group}`,
475
+ className: [
476
+ "font-mono text-[10px] leading-none transition",
477
+ isChordActive ? "text-emerald-700 font-bold" : "text-slate-400"
478
+ ].join(" "),
479
+ children: chord.letterForGroup(group)
480
+ }
481
+ )
482
+ ] }),
483
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 gap-1", children: grouped[group].map((t, i) => {
484
+ const active = activeTool === t.key;
485
+ return /* @__PURE__ */ jsxs(
486
+ "button",
487
+ {
488
+ type: "button",
489
+ "aria-label": t.label,
490
+ "aria-pressed": active,
491
+ "data-tool": t.key,
492
+ title: t.label + (t.shortcut ? ` (${t.shortcut})` : ""),
493
+ onClick: () => onToolChange(t.key),
494
+ onMouseEnter: (e) => showHover(e.currentTarget, t),
495
+ onMouseLeave: hideHover,
496
+ onFocus: (e) => showHover(e.currentTarget, t),
497
+ onBlur: hideHover,
498
+ className: [
499
+ "relative flex h-10 items-center justify-center rounded-md transition",
500
+ active ? "bg-emerald-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
501
+ ].join(" "),
502
+ children: [
503
+ t.icon,
504
+ chord && /* @__PURE__ */ jsx(
505
+ "span",
506
+ {
507
+ "data-testid": `chord-num-${t.key}`,
508
+ className: [
509
+ "pointer-events-none absolute bottom-0 right-0.5 font-mono text-[9px] leading-none transition",
510
+ active ? "text-white/70" : isChordActive ? "text-emerald-700 font-bold" : "text-slate-400"
511
+ ].join(" "),
512
+ children: i + 1
513
+ }
514
+ )
515
+ ]
516
+ },
517
+ t.key
518
+ );
519
+ }) })
520
+ ]
521
+ },
522
+ group
523
+ );
524
+ }),
525
+ chord?.activeGroup && activeGroupTools && /* @__PURE__ */ jsxs(
526
+ "div",
527
+ {
528
+ "data-testid": "chord-hint",
529
+ className: "mt-1 rounded border border-emerald-200 bg-emerald-50/60 px-2 py-1 text-[11px] leading-snug text-slate-600",
530
+ children: [
531
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-semibold text-emerald-700", children: chord.letterForGroup(chord.activeGroup) }),
532
+ /* @__PURE__ */ jsx("span", { className: "mx-1 text-slate-400", children: "\u2192" }),
533
+ activeGroupTools.map((t, i) => /* @__PURE__ */ jsxs("span", { className: "mr-2 inline-block", children: [
534
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-semibold text-emerald-700", children: i + 1 }),
535
+ /* @__PURE__ */ jsx("span", { className: "ml-1", children: t.label })
536
+ ] }, t.key)),
537
+ /* @__PURE__ */ jsx("span", { className: "text-slate-400", children: "Esc hu\u1EF7" })
538
+ ]
539
+ }
540
+ ),
541
+ portalReady && hover && typeof document !== "undefined" ? createPortal(
542
+ /* @__PURE__ */ jsxs(
543
+ "div",
544
+ {
545
+ role: "tooltip",
546
+ className: "pointer-events-none fixed w-max max-w-[220px] rounded-md bg-slate-900 px-2 py-1 text-left text-[11px] leading-tight text-white shadow-lg",
547
+ style: {
548
+ left: hover.x + 8,
549
+ top: hover.y,
550
+ transform: "translate(0, -50%)",
551
+ zIndex: 2147483600
552
+ },
553
+ children: [
554
+ /* @__PURE__ */ jsx("span", { className: "block font-medium", children: hover.label }),
555
+ hover.hint && /* @__PURE__ */ jsx("span", { className: "mt-0.5 block text-slate-300", children: hover.hint })
556
+ ]
557
+ }
558
+ ),
559
+ document.body
560
+ ) : null
561
+ ] });
562
+ }
563
+ function StampLeftPanelDesktop(props) {
564
+ const {
565
+ title,
566
+ icon,
567
+ onClose,
568
+ isDark,
569
+ testId,
570
+ tools,
571
+ groupOrder,
572
+ groupLabels,
573
+ activeTool,
574
+ onToolChange,
575
+ view,
576
+ history,
577
+ chord,
578
+ objects,
579
+ tabs
580
+ } = props;
581
+ const [tab, setTab] = useState("tools");
582
+ const hasObjects = !!objects;
583
+ useEffect(() => {
584
+ if (!hasObjects && tab === "objects") setTab("tools");
585
+ }, [hasObjects, tab]);
586
+ const tabSpecs = hasObjects ? [
587
+ { key: "tools", label: tabs?.toolsLabel ?? "\u{1F9F0} C\xF4ng c\u1EE5", testId: "tab-tools" },
588
+ { key: "objects", label: tabs?.objectsLabel ?? "\u{1F4D0} \u0110\u1ED1i t\u01B0\u1EE3ng", testId: "tab-objects" }
589
+ ] : void 0;
590
+ return /* @__PURE__ */ jsx(
591
+ LeftPanelShell,
592
+ {
593
+ title,
594
+ icon,
595
+ onClose,
596
+ isDark,
597
+ testId: testId ?? "stamp-left-panel",
598
+ tabs: tabSpecs,
599
+ activeTab: hasObjects ? tab : void 0,
600
+ onTabChange: hasObjects ? setTab : void 0,
601
+ children: !hasObjects || tab === "tools" ? /* @__PURE__ */ jsxs(Fragment, { children: [
602
+ /* @__PURE__ */ jsx(AxisGridSection, { view, history }),
603
+ /* @__PURE__ */ jsx(
604
+ ToolGrid,
605
+ {
606
+ tools,
607
+ groupOrder,
608
+ groupLabels,
609
+ activeTool,
610
+ onToolChange,
611
+ chord
612
+ }
613
+ )
614
+ ] }) : /* @__PURE__ */ jsxs("section", { "data-testid": "objects-panel", className: "flex flex-col gap-2", children: [
615
+ objects.addButtons && objects.addButtons.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex gap-1", children: objects.addButtons.map((b) => /* @__PURE__ */ jsx(
616
+ "button",
617
+ {
618
+ type: "button",
619
+ "data-testid": b.testId,
620
+ onClick: b.onClick,
621
+ className: "flex-1 rounded border border-slate-300 bg-slate-50 px-2 py-1 text-[11px] font-medium text-slate-700 transition hover:bg-slate-100",
622
+ children: b.label
623
+ },
624
+ b.label
625
+ )) }),
626
+ /* @__PURE__ */ jsx(
627
+ ObjectListPanel,
628
+ {
629
+ store: objects.store,
630
+ selectedId: objects.selectedObjectId,
631
+ onSelect: objects.onObjectSelect,
632
+ renderRow: objects.renderRow
633
+ }
634
+ )
635
+ ] })
636
+ }
637
+ );
638
+ }
639
+ function MobileToolDrawer({
640
+ title,
641
+ headerIcon,
642
+ chips,
643
+ actions,
644
+ groups,
645
+ activeTool,
646
+ onToolSelect,
647
+ drawerOpen,
648
+ onDrawerClose,
649
+ isDark,
650
+ testId,
651
+ objectsTab
652
+ }) {
653
+ const [mobileTab, setMobileTab] = React2__default.useState("tools");
654
+ const prevOpenRef = React2__default.useRef(drawerOpen);
655
+ React2__default.useEffect(() => {
656
+ if (!prevOpenRef.current && drawerOpen) setMobileTab("tools");
657
+ prevOpenRef.current = drawerOpen;
658
+ }, [drawerOpen]);
659
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
660
+ drawerOpen && /* @__PURE__ */ jsx(
661
+ "div",
662
+ {
663
+ className: "stamp-drawer-backdrop",
664
+ onPointerDown: onDrawerClose,
665
+ "aria-hidden": "true"
666
+ }
667
+ ),
668
+ /* @__PURE__ */ jsxs(
669
+ "aside",
670
+ {
671
+ role: "complementary",
672
+ "aria-label": title,
673
+ "aria-hidden": !drawerOpen ? "true" : void 0,
674
+ "data-testid": testId,
675
+ "data-stamp-area": "true",
676
+ "data-mobile-drawer": "true",
677
+ "data-geo-mobile": "true",
678
+ "data-drawer-state": drawerOpen ? "open" : "closed",
679
+ className: [
680
+ isDark ? "theme--dark " : "",
681
+ "stamp-drawer-mobile flex flex-col border-r border-slate-200 bg-white shadow-md"
682
+ ].join(""),
683
+ children: [
684
+ /* @__PURE__ */ jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-4 py-3", children: [
685
+ /* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 text-base font-semibold text-slate-800", children: [
686
+ /* @__PURE__ */ jsx("span", { className: "inline-flex h-7 w-7 items-center justify-center rounded-lg bg-emerald-50 text-emerald-700", children: headerIcon }),
687
+ title
688
+ ] }),
689
+ /* @__PURE__ */ jsx(
690
+ "button",
691
+ {
692
+ type: "button",
693
+ onClick: onDrawerClose,
694
+ "aria-label": "\u0110\xF3ng ng\u0103n c\xF4ng c\u1EE5",
695
+ className: "inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
696
+ children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
697
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
698
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
699
+ ] })
700
+ }
701
+ )
702
+ ] }),
703
+ /* @__PURE__ */ jsxs("div", { className: "sticky top-0 z-10 flex items-center gap-2 border-b border-slate-200 bg-white/95 px-3 py-2 backdrop-blur", children: [
704
+ chips.map((c) => /* @__PURE__ */ jsxs(
705
+ "button",
706
+ {
707
+ type: "button",
708
+ role: "switch",
709
+ "aria-pressed": c.pressed,
710
+ "aria-label": c.label,
711
+ "data-testid": c.testId,
712
+ onClick: () => c.onToggle(!c.pressed),
713
+ className: "geo-mobile-chip",
714
+ children: [
715
+ c.icon,
716
+ c.label
717
+ ]
718
+ },
719
+ c.label
720
+ )),
721
+ actions.length > 0 && /* @__PURE__ */ jsx("div", { className: "ml-auto flex items-center gap-1", children: actions.map((a) => /* @__PURE__ */ jsx(
722
+ "button",
723
+ {
724
+ type: "button",
725
+ onClick: a.onClick,
726
+ disabled: a.disabled,
727
+ "aria-label": a.label,
728
+ title: a.title ?? a.label,
729
+ "data-testid": a.testId,
730
+ className: "inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
731
+ children: a.icon
732
+ },
733
+ a.label
734
+ )) })
735
+ ] }),
736
+ objectsTab && /* @__PURE__ */ jsxs("div", { role: "tablist", className: "flex gap-1 rounded-md bg-slate-100 p-0.5 mx-3 mt-2", children: [
737
+ /* @__PURE__ */ jsx(
738
+ "button",
739
+ {
740
+ type: "button",
741
+ role: "tab",
742
+ "aria-selected": mobileTab === "tools",
743
+ onClick: () => setMobileTab("tools"),
744
+ className: [
745
+ "flex-1 rounded px-2 py-1 text-[11px] font-medium transition",
746
+ mobileTab === "tools" ? "bg-white text-slate-900 shadow-sm ring-1 ring-slate-200" : "text-slate-500 hover:text-slate-800"
747
+ ].join(" "),
748
+ children: "\u{1F9F0} C\xF4ng c\u1EE5"
749
+ }
750
+ ),
751
+ /* @__PURE__ */ jsx(
752
+ "button",
753
+ {
754
+ type: "button",
755
+ role: "tab",
756
+ "aria-selected": mobileTab === "objects",
757
+ onClick: () => setMobileTab("objects"),
758
+ className: [
759
+ "flex-1 rounded px-2 py-1 text-[11px] font-medium transition",
760
+ mobileTab === "objects" ? "bg-white text-slate-900 shadow-sm ring-1 ring-slate-200" : "text-slate-500 hover:text-slate-800"
761
+ ].join(" "),
762
+ children: objectsTab.label
763
+ }
764
+ )
765
+ ] }),
766
+ /* @__PURE__ */ jsx(
767
+ "div",
768
+ {
769
+ className: "min-h-0 flex-1 overflow-y-auto",
770
+ style: { paddingBottom: "calc(0.75rem + env(safe-area-inset-bottom))" },
771
+ children: objectsTab && mobileTab === "objects" ? /* @__PURE__ */ jsx("div", { className: "px-3 pt-3", children: objectsTab.render() }) : groups.map((g) => /* @__PURE__ */ jsxs("section", { className: "px-3 pt-3 pb-1", children: [
772
+ /* @__PURE__ */ jsxs("h4", { className: "mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: [
773
+ /* @__PURE__ */ jsx("span", { className: "h-1 w-1 rounded-full bg-emerald-500" }),
774
+ g.groupLabel
775
+ ] }),
776
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-2", children: g.tools.map((t) => {
777
+ const active = activeTool === t.key;
778
+ return /* @__PURE__ */ jsxs(
779
+ "button",
780
+ {
781
+ type: "button",
782
+ "aria-label": t.label,
783
+ "aria-pressed": active,
784
+ "data-tool": t.key,
785
+ onClick: () => {
786
+ onToolSelect(t.key);
787
+ onDrawerClose();
788
+ },
789
+ className: [
790
+ "flex flex-col items-center justify-center gap-1.5 rounded-2xl px-2 py-3 transition active:scale-95",
791
+ active ? "geo-mobile-tool-active" : "bg-slate-50 text-slate-700 hover:bg-slate-100"
792
+ ].join(" "),
793
+ children: [
794
+ /* @__PURE__ */ jsx("span", { className: "flex h-8 w-8 items-center justify-center", children: t.icon }),
795
+ /* @__PURE__ */ jsx("span", { className: "text-center text-[11px] font-medium leading-tight line-clamp-2", children: t.label })
796
+ ]
797
+ },
798
+ t.key
799
+ );
800
+ }) })
801
+ ] }, g.group))
802
+ }
803
+ )
804
+ ]
805
+ }
806
+ )
807
+ ] });
808
+ }
809
+ function AxisIcon() {
810
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
811
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "20", y2: "20" }),
812
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "4", y2: "4" }),
813
+ /* @__PURE__ */ jsx("polyline", { points: "2 6 4 4 6 6" }),
814
+ /* @__PURE__ */ jsx("polyline", { points: "18 18 20 20 18 22" })
815
+ ] });
816
+ }
817
+ function GridIcon() {
818
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
819
+ /* @__PURE__ */ jsx("rect", { x: "4", y: "4", width: "16", height: "16", rx: "1" }),
820
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "10", x2: "20", y2: "10" }),
821
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "16", x2: "20", y2: "16" }),
822
+ /* @__PURE__ */ jsx("line", { x1: "10", y1: "4", x2: "10", y2: "20" }),
823
+ /* @__PURE__ */ jsx("line", { x1: "16", y1: "4", x2: "16", y2: "20" })
824
+ ] });
825
+ }
826
+ function UndoIcon2() {
827
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
828
+ /* @__PURE__ */ jsx("path", { d: "M3 10 L8 5 L8 8 L15 8 A5 5 0 0 1 20 13 L20 16" }),
829
+ /* @__PURE__ */ jsx("path", { d: "M3 10 L8 15 L8 12" })
830
+ ] });
831
+ }
832
+ function RedoIcon2() {
833
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
834
+ /* @__PURE__ */ jsx("path", { d: "M21 10 L16 5 L16 8 L9 8 A5 5 0 0 0 4 13 L4 16" }),
835
+ /* @__PURE__ */ jsx("path", { d: "M21 10 L16 15 L16 12" })
836
+ ] });
837
+ }
838
+ function StampLeftPanelMobile(props) {
839
+ const {
840
+ title,
841
+ icon,
842
+ isDark,
843
+ testId,
844
+ tools,
845
+ groupOrder,
846
+ groupLabels,
847
+ activeTool,
848
+ onToolChange,
849
+ view,
850
+ history,
851
+ objects,
852
+ tabs,
853
+ drawerOpen,
854
+ onDrawerClose
855
+ } = props;
856
+ const groups = useMemo(() => {
857
+ const acc = /* @__PURE__ */ new Map();
858
+ for (const t of tools) {
859
+ if (!acc.has(t.group)) acc.set(t.group, []);
860
+ acc.get(t.group).push(t);
861
+ }
862
+ return groupOrder.filter((g) => acc.has(g)).map((group) => ({
863
+ group,
864
+ groupLabel: groupLabels[group],
865
+ tools: acc.get(group).map((t) => ({ key: t.key, label: t.label, icon: t.icon }))
866
+ }));
867
+ }, [tools, groupOrder, groupLabels]);
868
+ const chips = view ? [
869
+ {
870
+ label: view.axisLabel ?? "Tr\u1EE5c",
871
+ icon: /* @__PURE__ */ jsx(AxisIcon, {}),
872
+ pressed: view.showAxis,
873
+ onToggle: view.onShowAxisChange,
874
+ testId: "toggle-axis"
875
+ },
876
+ {
877
+ label: view.gridLabel ?? "L\u01B0\u1EDBi",
878
+ icon: /* @__PURE__ */ jsx(GridIcon, {}),
879
+ pressed: view.showGrid,
880
+ onToggle: view.onShowGridChange,
881
+ testId: "toggle-grid"
882
+ }
883
+ ] : [];
884
+ const actions = history ? [
885
+ {
886
+ label: "Ho\xE0n t\xE1c",
887
+ title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
888
+ icon: /* @__PURE__ */ jsx(UndoIcon2, {}),
889
+ onClick: history.onUndo,
890
+ disabled: !history.canUndo,
891
+ testId: "undo-btn"
892
+ },
893
+ {
894
+ label: "L\xE0m l\u1EA1i",
895
+ title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
896
+ icon: /* @__PURE__ */ jsx(RedoIcon2, {}),
897
+ onClick: history.onRedo,
898
+ disabled: !history.canRedo,
899
+ testId: "redo-btn"
900
+ }
901
+ ] : [];
902
+ return /* @__PURE__ */ jsx(
903
+ MobileToolDrawer,
904
+ {
905
+ title,
906
+ headerIcon: icon,
907
+ testId: testId ?? "stamp-left-panel",
908
+ isDark,
909
+ drawerOpen: !!drawerOpen,
910
+ onDrawerClose: () => onDrawerClose?.(),
911
+ chips,
912
+ actions,
913
+ groups,
914
+ activeTool,
915
+ onToolSelect: onToolChange,
916
+ objectsTab: objects ? {
917
+ label: tabs?.objectsLabel ?? "\u{1F4D0} \u0110\u1ED1i t\u01B0\u1EE3ng",
918
+ render: () => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 px-3", children: [
919
+ objects.addButtons && objects.addButtons.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex gap-1 pt-3", children: objects.addButtons.map((b) => /* @__PURE__ */ jsx(
920
+ "button",
921
+ {
922
+ type: "button",
923
+ "data-testid": b.testId,
924
+ onClick: b.onClick,
925
+ className: "flex-1 rounded border border-slate-300 bg-slate-50 px-2 py-1 text-[11px] font-medium text-slate-700 transition hover:bg-slate-100",
926
+ children: b.label
927
+ },
928
+ b.label
929
+ )) }),
930
+ /* @__PURE__ */ jsx(
931
+ ObjectListPanel,
932
+ {
933
+ store: objects.store,
934
+ selectedId: objects.selectedObjectId,
935
+ onSelect: objects.onObjectSelect,
936
+ renderRow: objects.renderRow
937
+ }
938
+ )
939
+ ] })
940
+ } : void 0
941
+ }
942
+ );
943
+ }
944
+ function StampLeftPanel(props) {
945
+ if (props.isMobile) return /* @__PURE__ */ jsx(StampLeftPanelMobile, { ...props });
946
+ return /* @__PURE__ */ jsx(StampLeftPanelDesktop, { ...props });
947
+ }
948
+ function useStampStore(domain, editingElement, parseInitial) {
949
+ const ref = useRef(null);
950
+ if (!ref.current) {
951
+ const initial = editingElement?.customData ? parseInitial(editingElement.customData) ?? createEmptyState(domain) : createEmptyState(domain);
952
+ ref.current = createStore(initial);
953
+ }
954
+ return ref.current;
955
+ }
956
+
957
+ // src/stamps/shared/StampLeftPanel/constants.ts
958
+ var STAMP_PANEL_DESKTOP = "h-[700px] w-[880px] max-h-[85vh] max-w-[calc(100vw-280px)]";
959
+ var ToastContext = createContext(null);
960
+ function reducer(state, action) {
961
+ switch (action.type) {
962
+ case "PUSH": {
963
+ const next = [...state, action.item];
964
+ return next.length > action.maxVisible ? next.slice(next.length - action.maxVisible) : next;
965
+ }
966
+ case "REPLACE":
967
+ return state.map((it) => it.id === action.item.id ? action.item : it);
968
+ case "DISMISS":
969
+ return state.filter((it) => it.id !== action.id);
970
+ }
971
+ }
972
+ var autoIdCounter = 0;
973
+ function ToastProvider({ children, maxVisible = 3 }) {
974
+ const [items, dispatch] = useReducer(reducer, []);
975
+ const timersRef = useRef(/* @__PURE__ */ new Map());
976
+ const itemsRef = useRef(items);
977
+ itemsRef.current = items;
978
+ const clearTimer = useCallback((id) => {
979
+ const t = timersRef.current.get(id);
980
+ if (t) {
981
+ clearTimeout(t);
982
+ timersRef.current.delete(id);
983
+ }
984
+ }, []);
985
+ const dismiss = useCallback((id) => {
986
+ clearTimer(id);
987
+ dispatch({ type: "DISMISS", id });
988
+ }, [clearTimer]);
989
+ const scheduleAutoDismiss = useCallback((id, duration) => {
990
+ if (duration <= 0) return;
991
+ const t = setTimeout(() => dismiss(id), duration);
992
+ timersRef.current.set(id, t);
993
+ }, [dismiss]);
994
+ const showToast = useCallback((message, opts = {}) => {
995
+ const variant = opts.variant ?? "info";
996
+ const duration = opts.duration ?? 3e3;
997
+ const id = opts.id ?? `toast-${++autoIdCounter}`;
998
+ const item = { id, message, variant, duration };
999
+ const existing = itemsRef.current.find((it) => it.id === id);
1000
+ if (existing) {
1001
+ clearTimer(id);
1002
+ dispatch({ type: "REPLACE", item });
1003
+ } else {
1004
+ dispatch({ type: "PUSH", item, maxVisible });
1005
+ }
1006
+ scheduleAutoDismiss(id, duration);
1007
+ }, [clearTimer, maxVisible, scheduleAutoDismiss]);
1008
+ useEffect(() => () => {
1009
+ timersRef.current.forEach((t) => clearTimeout(t));
1010
+ timersRef.current.clear();
1011
+ }, []);
1012
+ const value = useMemo(() => ({ items, showToast, dismiss }), [items, showToast, dismiss]);
1013
+ return /* @__PURE__ */ jsx(ToastContext.Provider, { value, children });
1014
+ }
1015
+ function useToast() {
1016
+ const ctx = useContext(ToastContext);
1017
+ if (!ctx) {
1018
+ throw new Error("useToast must be used inside <ToastProvider>");
1019
+ }
1020
+ return ctx;
1021
+ }
1022
+ var VARIANT_CLASS = {
1023
+ info: "border-l-sky-500",
1024
+ warning: "border-l-amber-500",
1025
+ error: "border-l-rose-500"
1026
+ };
1027
+ var VARIANT_ICON = {
1028
+ info: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
1029
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
1030
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
1031
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
1032
+ ] }),
1033
+ warning: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
1034
+ /* @__PURE__ */ jsx("path", { d: "M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" }),
1035
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "9", x2: "12", y2: "13" }),
1036
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
1037
+ ] }),
1038
+ error: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
1039
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
1040
+ /* @__PURE__ */ jsx("line", { x1: "15", y1: "9", x2: "9", y2: "15" }),
1041
+ /* @__PURE__ */ jsx("line", { x1: "9", y1: "9", x2: "15", y2: "15" })
1042
+ ] })
1043
+ };
1044
+ function Toast({ id, message, variant, onDismiss }) {
1045
+ return /* @__PURE__ */ jsxs(
1046
+ "div",
1047
+ {
1048
+ role: "status",
1049
+ className: [
1050
+ "pointer-events-auto flex max-w-sm items-start gap-2 rounded-lg border-l-4 bg-white px-3 py-2 text-sm text-slate-800 shadow-md ring-1 ring-black/5",
1051
+ VARIANT_CLASS[variant]
1052
+ ].join(" "),
1053
+ children: [
1054
+ /* @__PURE__ */ jsx("span", { className: "mt-0.5 shrink-0 text-slate-500", children: VARIANT_ICON[variant] }),
1055
+ /* @__PURE__ */ jsx("span", { className: "flex-1 leading-snug", children: message }),
1056
+ /* @__PURE__ */ jsx(
1057
+ "button",
1058
+ {
1059
+ type: "button",
1060
+ "aria-label": "\u0110\xF3ng th\xF4ng b\xE1o",
1061
+ onClick: () => onDismiss(id),
1062
+ className: "-mr-1 ml-1 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded text-slate-400 hover:bg-slate-100 hover:text-slate-700",
1063
+ children: "\xD7"
1064
+ }
1065
+ )
1066
+ ]
1067
+ }
1068
+ );
1069
+ }
1070
+ function ToastHost() {
1071
+ const { items, dismiss } = useToast();
1072
+ if (items.length === 0) return null;
1073
+ return /* @__PURE__ */ jsx(
1074
+ "div",
1075
+ {
1076
+ "aria-live": "polite",
1077
+ className: "pointer-events-none absolute inset-x-0 bottom-3 z-50 flex flex-col items-center gap-2",
1078
+ children: items.map((it) => /* @__PURE__ */ jsx(Toast, { id: it.id, message: it.message, variant: it.variant, onDismiss: dismiss }, it.id))
1079
+ }
1080
+ );
1081
+ }
1082
+
1083
+ // src/stamps/shared/safeJsx.ts
1084
+ var isDev = (() => {
1085
+ try {
1086
+ return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
1087
+ } catch {
1088
+ return false;
1089
+ }
1090
+ })();
1091
+ function safeJsx(label, fn, fallback) {
1092
+ try {
1093
+ return fn();
1094
+ } catch (err) {
1095
+ if (isDev) {
1096
+ console.warn("[whiteboard:jsxgraph]", label, err);
1097
+ }
1098
+ return fallback;
1099
+ }
1100
+ }
1101
+
1102
+ // src/stamps/shared/attachJxgWheelZoom.ts
1103
+ function attachJxgWheelZoom(target, board, label = "wheelZoom") {
1104
+ const onWheel = (e) => {
1105
+ if (!e.ctrlKey && !e.metaKey) return;
1106
+ e.preventDefault();
1107
+ e.stopPropagation();
1108
+ let cx;
1109
+ let cy;
1110
+ safeJsx(`${label}.coords`, () => {
1111
+ const usr = board.getUsrCoordsOfMouse?.(e);
1112
+ if (Array.isArray(usr) && usr.length >= 2 && Number.isFinite(usr[0]) && Number.isFinite(usr[1])) {
1113
+ cx = usr[0];
1114
+ cy = usr[1];
1115
+ }
1116
+ });
1117
+ if (e.deltaY < 0) safeJsx(`${label}.in`, () => board.zoomIn(cx, cy));
1118
+ else if (e.deltaY > 0) safeJsx(`${label}.out`, () => board.zoomOut(cx, cy));
1119
+ };
1120
+ target.addEventListener("wheel", onWheel, { passive: false });
1121
+ return () => {
1122
+ target.removeEventListener("wheel", onWheel);
1123
+ };
1124
+ }
1125
+
1126
+ // src/stamps/shared/initJxgBoard.ts
1127
+ async function initJxgBoard(target, config) {
1128
+ const label = config.label ?? "JxgBoard";
1129
+ const JXG = (await import('jsxgraph')).default;
1130
+ const {
1131
+ textDisplayInternal = true,
1132
+ disableTextEngines = true,
1133
+ labelDisplayInternal = true,
1134
+ disableElementHighlight = false
1135
+ } = config.defaults ?? {};
1136
+ safeJsx(`${label}.applyOptions`, () => {
1137
+ const opts = JXG.Options;
1138
+ if (!opts) return;
1139
+ if (textDisplayInternal || disableTextEngines) {
1140
+ opts.text = opts.text ?? {};
1141
+ if (textDisplayInternal) opts.text.display = "internal";
1142
+ if (disableTextEngines) {
1143
+ opts.text.useASCIIMathML = false;
1144
+ opts.text.useMathJax = false;
1145
+ opts.text.useKatex = false;
1146
+ }
1147
+ }
1148
+ if (labelDisplayInternal) {
1149
+ opts.label = opts.label ?? {};
1150
+ opts.label.display = "internal";
1151
+ }
1152
+ if (disableElementHighlight) {
1153
+ opts.elements = opts.elements ?? {};
1154
+ opts.elements.highlight = false;
1155
+ }
1156
+ config.extraOptionTweaks?.(opts);
1157
+ });
1158
+ const board = JXG.JSXGraph.initBoard(target, config.boardOptions);
1159
+ const cleanup = () => {
1160
+ safeJsx(`${label}.freeBoard`, () => JXG.JSXGraph.freeBoard(board));
1161
+ };
1162
+ return { JXG, board, cleanup };
1163
+ }
1164
+
1165
+ export { STAMP_PANEL_DESKTOP, StampLeftPanel, ToastHost, ToastProvider, attachJxgWheelZoom, initJxgBoard, safeJsx, useStampStore, useToast };
1166
+ //# sourceMappingURL=chunk-TOOHCAWP.mjs.map
1167
+ //# sourceMappingURL=chunk-TOOHCAWP.mjs.map