@xom11/whiteboard 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2264 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var excalidraw = require('@excalidraw/excalidraw');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var dynamic = require('next/dynamic');
7
+ var react = require('react');
8
+ var reactDom = require('react-dom');
9
+ require('@excalidraw/excalidraw/index.css');
10
+
11
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
+
13
+ var dynamic__default = /*#__PURE__*/_interopDefault(dynamic);
14
+
15
+ var __defProp = Object.defineProperty;
16
+ var __getOwnPropNames = Object.getOwnPropertyNames;
17
+ var __esm = (fn, res) => function __init() {
18
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
19
+ };
20
+ var __export = (target, all) => {
21
+ for (var name in all)
22
+ __defProp(target, name, { get: all[name], enumerable: true });
23
+ };
24
+
25
+ // src/ExcalidrawWithMenus.tsx
26
+ var ExcalidrawWithMenus_exports = {};
27
+ __export(ExcalidrawWithMenus_exports, {
28
+ ExcalidrawWithMenus: () => ExcalidrawWithMenus
29
+ });
30
+ function ExcalidrawWithMenus(props) {
31
+ const { children, ...rest } = props;
32
+ return /* @__PURE__ */ jsxRuntime.jsxs(excalidraw.Excalidraw, { ...rest, children: [
33
+ /* @__PURE__ */ jsxRuntime.jsxs(excalidraw.MainMenu, { children: [
34
+ /* @__PURE__ */ jsxRuntime.jsx(excalidraw.MainMenu.DefaultItems.LoadScene, {}),
35
+ /* @__PURE__ */ jsxRuntime.jsx(excalidraw.MainMenu.DefaultItems.SaveAsImage, {}),
36
+ /* @__PURE__ */ jsxRuntime.jsx(excalidraw.MainMenu.DefaultItems.ClearCanvas, {}),
37
+ /* @__PURE__ */ jsxRuntime.jsx(excalidraw.MainMenu.DefaultItems.ToggleTheme, {})
38
+ ] }),
39
+ /* @__PURE__ */ jsxRuntime.jsx(excalidraw.Footer, { children: /* @__PURE__ */ jsxRuntime.jsx("span", {}) }),
40
+ /* @__PURE__ */ jsxRuntime.jsx(excalidraw.WelcomeScreen, { children: /* @__PURE__ */ jsxRuntime.jsx("span", {}) }),
41
+ children
42
+ ] });
43
+ }
44
+ var init_ExcalidrawWithMenus = __esm({
45
+ "src/ExcalidrawWithMenus.tsx"() {
46
+ "use client";
47
+ }
48
+ });
49
+
50
+ // src/serialize.ts
51
+ function pickSyncableAppState(s) {
52
+ return {
53
+ viewBackgroundColor: s.viewBackgroundColor,
54
+ zoom: s.zoom,
55
+ scrollX: s.scrollX,
56
+ scrollY: s.scrollY,
57
+ gridSize: s.gridSize ?? null,
58
+ theme: s.theme
59
+ };
60
+ }
61
+ var WRAPPER_ID = "stamp-toolbar-portal-wrapper";
62
+ function ToolbarStampInjector({
63
+ enabled,
64
+ activeStamp,
65
+ onToggleGeometry,
66
+ onToggleLatex
67
+ }) {
68
+ const [mountNode, setMountNode] = react.useState(null);
69
+ react.useEffect(() => {
70
+ if (!enabled) {
71
+ setMountNode(null);
72
+ return;
73
+ }
74
+ let cancelled = false;
75
+ let attempts = 0;
76
+ let observer = null;
77
+ let timer = null;
78
+ const tryMount = () => {
79
+ if (cancelled) return;
80
+ const container = document.querySelector(".excalidraw .App-toolbar .Stack_horizontal") ?? document.querySelector(".App-toolbar .Stack_horizontal");
81
+ if (!container) {
82
+ if (attempts++ < 20) {
83
+ timer = setTimeout(tryMount, 100);
84
+ }
85
+ return;
86
+ }
87
+ let wrapper = container.querySelector("#" + WRAPPER_ID);
88
+ if (!wrapper) {
89
+ wrapper = document.createElement("div");
90
+ wrapper.id = WRAPPER_ID;
91
+ wrapper.className = "Stamp-toolbar-injector";
92
+ wrapper.setAttribute("data-stamp-area", "true");
93
+ wrapper.style.display = "inline-flex";
94
+ wrapper.style.alignItems = "center";
95
+ wrapper.style.gap = "4px";
96
+ wrapper.style.marginInlineStart = "6px";
97
+ wrapper.style.paddingInlineStart = "6px";
98
+ wrapper.style.borderInlineStart = "1px solid var(--default-border-color, rgba(0,0,0,0.1))";
99
+ const moreTools = container.querySelector(".App-toolbar__extra-tools-dropdown") ?? container.querySelector('button[aria-label*="More tools" i]');
100
+ if (moreTools && moreTools.parentElement === container) {
101
+ container.insertBefore(wrapper, moreTools);
102
+ } else {
103
+ container.appendChild(wrapper);
104
+ }
105
+ }
106
+ setMountNode(wrapper);
107
+ };
108
+ tryMount();
109
+ const root = document.querySelector(".excalidraw") ?? document.body;
110
+ observer = new MutationObserver(() => {
111
+ if (cancelled) return;
112
+ const stillThere = document.getElementById(WRAPPER_ID);
113
+ if (!stillThere) {
114
+ attempts = 0;
115
+ tryMount();
116
+ }
117
+ });
118
+ observer.observe(root, { childList: true, subtree: true });
119
+ return () => {
120
+ cancelled = true;
121
+ if (timer) clearTimeout(timer);
122
+ observer?.disconnect();
123
+ document.getElementById(WRAPPER_ID)?.remove();
124
+ };
125
+ }, [enabled]);
126
+ if (!enabled || !mountNode) return null;
127
+ return reactDom.createPortal(
128
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
129
+ /* @__PURE__ */ jsxRuntime.jsx(
130
+ StampToolButton,
131
+ {
132
+ icon: GeometryIcon,
133
+ keybind: "G",
134
+ label: "Ch\xE8n h\xECnh h\u1ECDc (G)",
135
+ active: activeStamp === "geometry",
136
+ onClick: onToggleGeometry,
137
+ dataTestId: "stamp-toolbar-geometry"
138
+ }
139
+ ),
140
+ /* @__PURE__ */ jsxRuntime.jsx(
141
+ StampToolButton,
142
+ {
143
+ icon: LatexIcon,
144
+ keybind: "L",
145
+ label: "Ch\xE8n c\xF4ng th\u1EE9c LaTeX (L)",
146
+ active: activeStamp === "latex",
147
+ onClick: onToggleLatex,
148
+ dataTestId: "stamp-toolbar-latex"
149
+ }
150
+ )
151
+ ] }),
152
+ mountNode
153
+ );
154
+ }
155
+ var GeometryIcon = /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
156
+ /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "4,20 20,20 12,5" }),
157
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "20", r: "1.4", fill: "currentColor", stroke: "none" }),
158
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "20", cy: "20", r: "1.4", fill: "currentColor", stroke: "none" }),
159
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "5", r: "1.4", fill: "currentColor", stroke: "none" })
160
+ ] });
161
+ var LatexIcon = /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M17 5 H7 L13 12 L7 19 H17" }) });
162
+ function StampToolButton({ icon, keybind, label, active, onClick, dataTestId }) {
163
+ return /* @__PURE__ */ jsxRuntime.jsxs(
164
+ "button",
165
+ {
166
+ type: "button",
167
+ title: label,
168
+ "aria-label": label,
169
+ "aria-pressed": active,
170
+ onClick,
171
+ "data-testid": dataTestId,
172
+ className: "ToolIcon Shape",
173
+ style: {
174
+ position: "relative",
175
+ display: "flex",
176
+ alignItems: "center",
177
+ justifyContent: "center",
178
+ width: "var(--lg-button-size, 2.25rem)",
179
+ height: "var(--lg-button-size, 2.25rem)",
180
+ padding: 0,
181
+ margin: 0,
182
+ background: active ? "var(--color-primary-light, #e0e7ff)" : "transparent",
183
+ border: 0,
184
+ borderRadius: "var(--space-factor, 0.25rem)",
185
+ color: active ? "var(--color-primary, #6965db)" : "var(--icon-fill-color, #1b1b1f)",
186
+ cursor: "pointer",
187
+ transition: "background 0.15s"
188
+ },
189
+ onMouseEnter: (e) => {
190
+ if (!active) e.currentTarget.style.background = "var(--button-hover-bg, rgba(0,0,0,0.06))";
191
+ },
192
+ onMouseLeave: (e) => {
193
+ if (!active) e.currentTarget.style.background = "transparent";
194
+ },
195
+ children: [
196
+ /* @__PURE__ */ jsxRuntime.jsx(
197
+ "div",
198
+ {
199
+ "aria-hidden": "true",
200
+ style: { display: "flex", alignItems: "center", justifyContent: "center" },
201
+ children: icon
202
+ }
203
+ ),
204
+ /* @__PURE__ */ jsxRuntime.jsx(
205
+ "span",
206
+ {
207
+ "aria-hidden": "true",
208
+ style: {
209
+ position: "absolute",
210
+ right: "3px",
211
+ bottom: "2px",
212
+ fontSize: "0.5625rem",
213
+ color: "var(--keybinding-color, #6b7280)",
214
+ fontFamily: "var(--ui-font, system-ui)",
215
+ fontWeight: 400,
216
+ pointerEvents: "none"
217
+ },
218
+ children: keybind
219
+ }
220
+ )
221
+ ]
222
+ }
223
+ );
224
+ }
225
+ var Icon = {
226
+ cursor: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 4 L20 12 L13 13 L11 20 Z" }) }),
227
+ point: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "4", fill: "currentColor" }) }),
228
+ midpoint: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
229
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
230
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "2.5", fill: "currentColor", stroke: "none" }),
231
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "12", r: "1.6", fill: "currentColor", stroke: "none" }),
232
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "20", cy: "12", r: "1.6", fill: "currentColor", stroke: "none" })
233
+ ] }),
234
+ segment: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
235
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "5", y1: "18", x2: "19", y2: "6" }),
236
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "5", cy: "18", r: "1.7", fill: "currentColor" }),
237
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "19", cy: "6", r: "1.7", fill: "currentColor" })
238
+ ] }),
239
+ line: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
240
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "2", y1: "20", x2: "22", y2: "4" }),
241
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "8", cy: "16", r: "1.6", fill: "currentColor" }),
242
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "16", cy: "8", r: "1.6", fill: "currentColor" })
243
+ ] }),
244
+ ray: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
245
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "5", y1: "19", x2: "22", y2: "2" }),
246
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "5", cy: "19", r: "1.7", fill: "currentColor" }),
247
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "1.5", fill: "currentColor" })
248
+ ] }),
249
+ vector: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
250
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "20", x2: "20", y2: "4" }),
251
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "14,4 20,4 20,10" })
252
+ ] }),
253
+ perpendicular: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
254
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "18", x2: "21", y2: "18" }),
255
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "18", x2: "12", y2: "4" }),
256
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "12", y: "14", width: "4", height: "4", fill: "none" })
257
+ ] }),
258
+ parallel: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
259
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "9", x2: "21", y2: "5" }),
260
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "19", x2: "21", y2: "15" })
261
+ ] }),
262
+ perpBisector: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
263
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" }),
264
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "4", x2: "12", y2: "22", strokeDasharray: "3 2" }),
265
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "6", cy: "18", r: "1.5", fill: "currentColor" }),
266
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "18", cy: "18", r: "1.5", fill: "currentColor" })
267
+ ] }),
268
+ bisector: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
269
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "20", x2: "20", y2: "4" }),
270
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "20", x2: "20", y2: "20" }),
271
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "20", x2: "22", y2: "12", strokeDasharray: "3 2" })
272
+ ] }),
273
+ polygon: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "6,6 18,6 22,14 12,22 4,14" }) }),
274
+ circleCenter: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
275
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "8" }),
276
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "1.6", fill: "currentColor" })
277
+ ] }),
278
+ circle3: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
279
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "8" }),
280
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "4", r: "1.5", fill: "currentColor" }),
281
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "20", cy: "14", r: "1.5", fill: "currentColor" }),
282
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "5", cy: "16", r: "1.5", fill: "currentColor" })
283
+ ] }),
284
+ tangent: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
285
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "11", cy: "13", r: "6" }),
286
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "2", y1: "20", x2: "22", y2: "2" })
287
+ ] }),
288
+ angle: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
289
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "20", x2: "20", y2: "20" }),
290
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "20", x2: "20", y2: "6" }),
291
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 20 A 10 10 0 0 0 11 13" })
292
+ ] }),
293
+ distance: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
294
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
295
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "8", x2: "4", y2: "16" }),
296
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "20", y1: "8", x2: "20", y2: "16" })
297
+ ] }),
298
+ area: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "5,6 19,6 21,14 13,21 3,15", fill: "currentColor", fillOpacity: "0.2" }) }),
299
+ toggleLabel: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
300
+ /* @__PURE__ */ jsxRuntime.jsx("text", { x: "3", y: "18", fontSize: "16", fontFamily: "serif", fontWeight: "700", fill: "currentColor", stroke: "none", children: "A" }),
301
+ /* @__PURE__ */ jsxRuntime.jsx("text", { x: "13", y: "14", fontSize: "11", fontFamily: "serif", fontWeight: "700", fill: "currentColor", stroke: "none", children: "A" })
302
+ ] }),
303
+ toggleVisible: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
304
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "3.5", fill: "currentColor", fillOpacity: "0.4" }),
305
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "3.5" }),
306
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "20", cy: "6", r: "1.5", fill: "currentColor" })
307
+ ] }),
308
+ trash: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
309
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "3,6 5,6 21,6" }),
310
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 6 l-1 14 a 2 2 0 0 1 -2 2 H 8 a 2 2 0 0 1 -2 -2 l-1 -14" }),
311
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "10", y1: "11", x2: "10", y2: "17" }),
312
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "14", y1: "11", x2: "14", y2: "17" })
313
+ ] })
314
+ };
315
+ var TOOLS = [
316
+ { key: "move", label: "Di chuy\u1EC3n", hint: "K\xE9o \u0111i\u1EC3m ho\u1EB7c xoay n\u1EC1n", icon: Icon.cursor, group: "move", needs: 0 },
317
+ { key: "point", label: "\u0110i\u1EC3m m\u1EDBi", hint: "Click \u0111\u1EC3 th\xEAm \u0111i\u1EC3m", icon: Icon.point, group: "point", needs: 1 },
318
+ { key: "midpoint", label: "Trung \u0111i\u1EC3m", hint: "Click 2 \u0111i\u1EC3m c\xF3 s\u1EB5n", icon: Icon.midpoint, group: "point", needs: 2, accepts: ["point", "point"] },
319
+ { key: "segment", label: "\u0110o\u1EA1n th\u1EB3ng", hint: "Click 2 \u0111i\u1EC3m", icon: Icon.segment, group: "line", needs: 2 },
320
+ { key: "line", label: "\u0110\u01B0\u1EDDng th\u1EB3ng qua 2 \u0111i\u1EC3m", hint: "Click 2 \u0111i\u1EC3m", icon: Icon.line, group: "line", needs: 2 },
321
+ { key: "ray", label: "Tia qua 2 \u0111i\u1EC3m", hint: "Click 2 \u0111i\u1EC3m", icon: Icon.ray, group: "line", needs: 2 },
322
+ { key: "vector", label: "Vector", hint: "Click 2 \u0111i\u1EC3m", icon: Icon.vector, group: "line", needs: 2 },
323
+ { key: "perpendicular", label: "\u0110\u01B0\u1EDDng vu\xF4ng g\xF3c", hint: "Click 1 \u0111i\u1EC3m + 1 \u0111\u01B0\u1EDDng c\xF3 s\u1EB5n", icon: Icon.perpendicular, group: "construct", needs: 2, accepts: ["point", "line"] },
324
+ { key: "parallel", label: "\u0110\u01B0\u1EDDng song song", hint: "Click 1 \u0111i\u1EC3m + 1 \u0111\u01B0\u1EDDng c\xF3 s\u1EB5n", icon: Icon.parallel, group: "construct", needs: 2, accepts: ["point", "line"] },
325
+ { key: "perpBisector", label: "\u0110\u01B0\u1EDDng trung tr\u1EF1c", hint: "Click 2 \u0111i\u1EC3m c\xF3 s\u1EB5n", icon: Icon.perpBisector, group: "construct", needs: 2, accepts: ["point", "point"] },
326
+ { key: "angleBisector", label: "\u0110\u01B0\u1EDDng ph\xE2n gi\xE1c", hint: "Click 3 \u0111i\u1EC3m c\xF3 s\u1EB5n (\u0111\u1EC9nh \u1EDF gi\u1EEFa)", icon: Icon.bisector, group: "construct", needs: 3, accepts: ["point", "point", "point"] },
327
+ { key: "polygon", label: "\u0110a gi\xE1c", hint: "Click c\xE1c \u0111i\u1EC3m, click l\u1EA1i \u0111i\u1EC3m \u0111\u1EA7u \u0111\u1EC3 \u0111\xF3ng", icon: Icon.polygon, group: "polygon", needs: -1 },
328
+ { key: "circleCenter", label: "\u0110\u01B0\u1EDDng tr\xF2n (t\xE2m + \u0111i\u1EC3m)", hint: "Click t\xE2m r\u1ED3i 1 \u0111i\u1EC3m tr\xEAn \u0111\u01B0\u1EDDng tr\xF2n", icon: Icon.circleCenter, group: "circle", needs: 2 },
329
+ { key: "circle3", label: "\u0110\u01B0\u1EDDng tr\xF2n qua 3 \u0111i\u1EC3m", hint: "Click 3 \u0111i\u1EC3m", icon: Icon.circle3, group: "circle", needs: 3 },
330
+ { key: "tangent", label: "Ti\u1EBFp tuy\u1EBFn", hint: "Click 1 \u0111i\u1EC3m + 1 \u0111\u01B0\u1EDDng tr\xF2n c\xF3 s\u1EB5n", icon: Icon.tangent, group: "circle", needs: 2, accepts: ["point", "circle"] },
331
+ { key: "angle", label: "G\xF3c", hint: "Click 3 \u0111i\u1EC3m c\xF3 s\u1EB5n (\u0111\u1EC9nh \u1EDF gi\u1EEFa)", icon: Icon.angle, group: "measure", needs: 3, accepts: ["point", "point", "point"] },
332
+ { key: "distance", label: "Kho\u1EA3ng c\xE1ch", hint: "Click 2 \u0111i\u1EC3m c\xF3 s\u1EB5n", icon: Icon.distance, group: "measure", needs: 2, accepts: ["point", "point"] },
333
+ { key: "area", label: "Di\u1EC7n t\xEDch", hint: "Click c\xE1c \u0111\u1EC9nh, click l\u1EA1i \u0111i\u1EC3m \u0111\u1EA7u \u0111\u1EC3 \u0111\xF3ng", icon: Icon.area, group: "measure", needs: -1 },
334
+ { key: "toggleLabel", label: "Hi\u1EC7n/\u1EA9n t\xEAn", hint: "Click v\xE0o \u0111\u1ED1i t\u01B0\u1EE3ng", icon: Icon.toggleLabel, group: "edit", needs: 1, accepts: ["any"] },
335
+ { key: "toggleVisible", label: "Hi\u1EC7n/\u1EA9n \u0111\u1ED1i t\u01B0\u1EE3ng", hint: "Click v\xE0o \u0111\u1ED1i t\u01B0\u1EE3ng", icon: Icon.toggleVisible, group: "edit", needs: 1, accepts: ["any"] },
336
+ { key: "delete", label: "Xo\xE1", hint: "Click v\xE0o \u0111\u1ED1i t\u01B0\u1EE3ng", icon: Icon.trash, group: "edit", needs: 1, accepts: ["any"] }
337
+ ];
338
+ var GROUP_LABELS = {
339
+ move: "C\u01A1 b\u1EA3n",
340
+ point: "\u0110i\u1EC3m",
341
+ line: "\u0110\u01B0\u1EDDng",
342
+ construct: "D\u1EF1ng h\xECnh",
343
+ polygon: "\u0110a gi\xE1c",
344
+ circle: "\u0110\u01B0\u1EDDng tr\xF2n",
345
+ measure: "\u0110o l\u01B0\u1EDDng",
346
+ edit: "Ch\u1EC9nh s\u1EEDa"
347
+ };
348
+ function objKind(obj) {
349
+ if (!obj) return "other";
350
+ const e = (obj.elType || obj.type || "").toString().toLowerCase();
351
+ if (e === "point" || e === "glider" || e === "midpoint") return "point";
352
+ if (e === "line" || e === "segment" || e === "arrow" || e === "axis" || e === "normal" || e === "parallel" || e === "perpendicular" || e === "tangent" || e === "bisector" || e === "perpendicularsegment") return "line";
353
+ if (e === "circle" || e === "circumcircle") return "circle";
354
+ return "other";
355
+ }
356
+ var JSXGraphMiniBoard = ({ onReady, initialState }) => {
357
+ const containerId = react.useId().replace(/:/g, "_") + "_jxgmini";
358
+ const containerRef = react.useRef(null);
359
+ const boardRef = react.useRef(null);
360
+ const jxgRef = react.useRef(null);
361
+ const axisObjsRef = react.useRef({});
362
+ const creationLogRef = react.useRef([]);
363
+ const [tool, setTool] = react.useState("move");
364
+ const toolRef = react.useRef("move");
365
+ toolRef.current = tool;
366
+ const [showAxis, setShowAxis] = react.useState(initialState?.showAxis ?? false);
367
+ const [showGrid, setShowGrid] = react.useState(initialState?.showGrid ?? false);
368
+ const showAxisRef = react.useRef(showAxis);
369
+ showAxisRef.current = showAxis;
370
+ const showGridRef = react.useRef(showGrid);
371
+ showGridRef.current = showGrid;
372
+ const objMapRef = react.useRef(/* @__PURE__ */ new Map());
373
+ const pendingRef = react.useRef([]);
374
+ const [, setPendingCount] = react.useState(0);
375
+ const previewSegRef = react.useRef([]);
376
+ const [historyTick, setHistoryTick] = react.useState(0);
377
+ const [, setWarn] = react.useState(null);
378
+ const warnTimerRef = react.useRef(null);
379
+ const flashWarn = react.useCallback((msg) => {
380
+ if (warnTimerRef.current) clearTimeout(warnTimerRef.current);
381
+ setWarn(msg);
382
+ warnTimerRef.current = setTimeout(() => setWarn(null), 1800);
383
+ }, []);
384
+ react.useEffect(() => () => {
385
+ if (warnTimerRef.current) clearTimeout(warnTimerRef.current);
386
+ }, []);
387
+ const labelIdxRef = react.useRef(0);
388
+ const nextLabel = react.useCallback(() => {
389
+ const idx = labelIdxRef.current;
390
+ const suffix = idx >= 26 ? String(Math.floor(idx / 26)) : "";
391
+ const code = "A".charCodeAt(0) + idx % 26;
392
+ labelIdxRef.current = idx + 1;
393
+ return String.fromCharCode(code) + suffix;
394
+ }, []);
395
+ const nextLocalId = react.useCallback(() => "j" + creationLogRef.current.length, []);
396
+ const resolveArgs = react.useCallback((args) => {
397
+ return args.map((a) => {
398
+ if (typeof a === "string" && objMapRef.current.has(a)) {
399
+ return objMapRef.current.get(a);
400
+ }
401
+ return a;
402
+ });
403
+ }, []);
404
+ const pushLog = react.useCallback(
405
+ (id, type, args, attrs, obj) => {
406
+ creationLogRef.current.push({ id, type, args, attrs });
407
+ objMapRef.current.set(id, obj);
408
+ setHistoryTick((t) => t + 1);
409
+ },
410
+ []
411
+ );
412
+ const create = react.useCallback(
413
+ (type, args, attrs = {}) => {
414
+ if (!boardRef.current) return null;
415
+ const id = nextLocalId();
416
+ const resolved = resolveArgs(args);
417
+ const obj = boardRef.current.create(type, resolved, { ...attrs });
418
+ pushLog(id, type, args, attrs, obj);
419
+ return obj;
420
+ },
421
+ [nextLocalId, resolveArgs, pushLog]
422
+ );
423
+ const localIdOf = react.useCallback((obj) => {
424
+ for (const [id, o] of objMapRef.current.entries()) {
425
+ if (o === obj) return id;
426
+ }
427
+ return null;
428
+ }, []);
429
+ const clearPreviewSegs = react.useCallback(() => {
430
+ const b = boardRef.current;
431
+ if (!b) return;
432
+ for (const s of previewSegRef.current) {
433
+ try {
434
+ b.removeObject(s);
435
+ } catch {
436
+ }
437
+ }
438
+ previewSegRef.current = [];
439
+ }, []);
440
+ const clearPending = react.useCallback(() => {
441
+ clearPreviewSegs();
442
+ pendingRef.current = [];
443
+ setPendingCount(0);
444
+ }, [clearPreviewSegs]);
445
+ const finalize = react.useCallback((toolDef, picks) => {
446
+ if (!boardRef.current) return;
447
+ const labels = picks.map(localIdOf).filter(Boolean);
448
+ const stroke = { strokeColor: "#0f172a", strokeWidth: 2 };
449
+ const strokeOnly = { ...stroke, fillColor: "none", fillOpacity: 0 };
450
+ const lblName = nextLabel();
451
+ switch (toolDef.key) {
452
+ case "midpoint":
453
+ create("midpoint", labels, { name: lblName, color: "#000", size: 3 });
454
+ break;
455
+ case "segment":
456
+ create("segment", labels, stroke);
457
+ break;
458
+ case "line":
459
+ create("line", labels, stroke);
460
+ break;
461
+ case "ray": {
462
+ create("line", labels, { ...stroke, straightFirst: false, straightLast: true });
463
+ break;
464
+ }
465
+ case "vector":
466
+ create("arrow", labels, stroke);
467
+ break;
468
+ case "perpendicular": {
469
+ const [p, l] = picks[0] && objKind(picks[0]) === "point" ? [labels[0], labels[1]] : [labels[1], labels[0]];
470
+ create("perpendicular", [l, p], stroke);
471
+ break;
472
+ }
473
+ case "parallel": {
474
+ const [p, l] = picks[0] && objKind(picks[0]) === "point" ? [labels[0], labels[1]] : [labels[1], labels[0]];
475
+ create("parallel", [l, p], stroke);
476
+ break;
477
+ }
478
+ case "perpBisector": {
479
+ const mid = create("midpoint", labels, { visible: false, withLabel: false, name: "" });
480
+ const seg = create("segment", labels, { visible: false, withLabel: false });
481
+ const midId = localIdOf(mid);
482
+ const segId = localIdOf(seg);
483
+ if (midId && segId) create("perpendicular", [segId, midId], stroke);
484
+ break;
485
+ }
486
+ case "angleBisector":
487
+ create("bisector", labels, stroke);
488
+ break;
489
+ case "circleCenter":
490
+ create("circle", labels, strokeOnly);
491
+ break;
492
+ case "circle3":
493
+ create("circumcircle", labels, strokeOnly);
494
+ break;
495
+ case "tangent": {
496
+ const firstIsPoint = picks[0] && objKind(picks[0]) === "point";
497
+ const pointPick = firstIsPoint ? picks[0] : picks[1];
498
+ const circleLabel = firstIsPoint ? labels[1] : labels[0];
499
+ if (!pointPick || !circleLabel) break;
500
+ const px = typeof pointPick.X === "function" ? pointPick.X() : 0;
501
+ const py = typeof pointPick.Y === "function" ? pointPick.Y() : 0;
502
+ const glider = create("glider", [px, py, circleLabel], { name: "", size: 2, strokeColor: "#666", visible: false });
503
+ const gid = localIdOf(glider);
504
+ if (gid) create("tangent", [gid], stroke);
505
+ break;
506
+ }
507
+ case "angle":
508
+ create("angle", labels, { radius: 1, fillColor: "#22c55e", fillOpacity: 0.25, strokeColor: "#16a34a", strokeWidth: 1.5, name: lblName });
509
+ break;
510
+ case "distance": {
511
+ const pA = picks[0], pB = picks[1];
512
+ const dist = Math.hypot(pA.X() - pB.X(), pA.Y() - pB.Y());
513
+ const midX = (pA.X() + pB.X()) / 2;
514
+ const midY = (pA.Y() + pB.Y()) / 2;
515
+ create("text", [midX, midY, `d = ${dist.toFixed(2)}`], { fontSize: 14, color: "#dc2626" });
516
+ break;
517
+ }
518
+ case "polygon": {
519
+ create("polygon", labels, { fillColor: "#1e3a8a", fillOpacity: 0.1, borders: { strokeColor: "#0f172a", strokeWidth: 2 } });
520
+ break;
521
+ }
522
+ case "area": {
523
+ create("polygon", labels, { fillColor: "#3b82f6", fillOpacity: 0.18, borders: { strokeColor: "#1d4ed8", strokeWidth: 2 } });
524
+ break;
525
+ }
526
+ case "toggleLabel": {
527
+ const obj = picks[0];
528
+ try {
529
+ if (obj.label) {
530
+ const visible = obj.label.visProp.visible !== false;
531
+ obj.label.setAttribute({ visible: !visible });
532
+ } else if (obj.setAttribute) {
533
+ const cur = obj.visProp.withlabel !== false;
534
+ obj.setAttribute({ withLabel: !cur });
535
+ }
536
+ boardRef.current.update();
537
+ } catch {
538
+ }
539
+ break;
540
+ }
541
+ case "toggleVisible": {
542
+ const obj = picks[0];
543
+ try {
544
+ const visible = obj.visProp.visible !== false;
545
+ obj.setAttribute({ visible: !visible });
546
+ boardRef.current.update();
547
+ } catch {
548
+ }
549
+ break;
550
+ }
551
+ case "delete": {
552
+ const obj = picks[0];
553
+ try {
554
+ boardRef.current.removeObject(obj);
555
+ const id = localIdOf(obj);
556
+ if (id) {
557
+ creationLogRef.current = creationLogRef.current.filter((e) => e.id !== id);
558
+ objMapRef.current.delete(id);
559
+ setHistoryTick((t) => t + 1);
560
+ }
561
+ } catch {
562
+ }
563
+ break;
564
+ }
565
+ }
566
+ }, [create, localIdOf, nextLabel]);
567
+ const undoLast = react.useCallback(() => {
568
+ const b = boardRef.current;
569
+ if (!b) return;
570
+ while (creationLogRef.current.length > 0) {
571
+ const last = creationLogRef.current.pop();
572
+ if (!last) break;
573
+ const obj = objMapRef.current.get(last.id);
574
+ objMapRef.current.delete(last.id);
575
+ if (obj) {
576
+ try {
577
+ b.removeObject(obj);
578
+ } catch {
579
+ }
580
+ clearPending();
581
+ setHistoryTick((t) => t + 1);
582
+ try {
583
+ b.update();
584
+ } catch {
585
+ }
586
+ return;
587
+ }
588
+ }
589
+ setHistoryTick((t) => t + 1);
590
+ }, [clearPending]);
591
+ react.useEffect(() => {
592
+ const onKey = (e) => {
593
+ if (!((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z" && !e.shiftKey)) return;
594
+ const ae = document.activeElement;
595
+ if (ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable)) return;
596
+ e.preventDefault();
597
+ e.stopPropagation();
598
+ undoLast();
599
+ };
600
+ window.addEventListener("keydown", onKey, { capture: true });
601
+ return () => window.removeEventListener("keydown", onKey, { capture: true });
602
+ }, [undoLast]);
603
+ const screenCoordsOf = react.useCallback((evt) => {
604
+ const b = boardRef.current;
605
+ if (!b) return null;
606
+ try {
607
+ const mp = b.getMousePosition ? b.getMousePosition(evt) : null;
608
+ if (mp && mp.length >= 2) return [mp[0], mp[1]];
609
+ } catch {
610
+ }
611
+ if (containerRef.current) {
612
+ const rect = containerRef.current.getBoundingClientRect();
613
+ const cx = evt.clientX ?? evt.touches?.[0]?.clientX ?? 0;
614
+ const cy = evt.clientY ?? evt.touches?.[0]?.clientY ?? 0;
615
+ return [cx - rect.left, cy - rect.top];
616
+ }
617
+ return null;
618
+ }, []);
619
+ const objectsAt = react.useCallback((evt) => {
620
+ const b = boardRef.current;
621
+ if (!b) return [];
622
+ const sc = screenCoordsOf(evt);
623
+ if (!sc) return [];
624
+ const [sx, sy] = sc;
625
+ const list = [];
626
+ try {
627
+ const objs = b.objectsList || [];
628
+ for (const o of objs) {
629
+ try {
630
+ if (o.hasPoint && o.hasPoint(sx, sy)) list.push(o);
631
+ } catch {
632
+ }
633
+ }
634
+ } catch {
635
+ }
636
+ return list;
637
+ }, [screenCoordsOf]);
638
+ const findNearestPoint = react.useCallback((evt, tolPx = 12) => {
639
+ const b = boardRef.current;
640
+ if (!b) return null;
641
+ const sc = screenCoordsOf(evt);
642
+ if (!sc) return null;
643
+ const [sx, sy] = sc;
644
+ const tol2 = tolPx * tolPx;
645
+ let best = null;
646
+ try {
647
+ const objs = b.objectsList || [];
648
+ for (const o of objs) {
649
+ try {
650
+ if (objKind(o) !== "point") continue;
651
+ const pc = o.coords?.scrCoords;
652
+ if (!pc) continue;
653
+ const dx = pc[1] - sx;
654
+ const dy = pc[2] - sy;
655
+ const d2 = dx * dx + dy * dy;
656
+ if (d2 <= tol2 && (!best || d2 < best.d2)) best = { obj: o, d2 };
657
+ } catch {
658
+ }
659
+ }
660
+ } catch {
661
+ }
662
+ return best ? best.obj : null;
663
+ }, [screenCoordsOf]);
664
+ react.useEffect(() => {
665
+ if (typeof window === "undefined" || !containerRef.current) return;
666
+ let cancelled = false;
667
+ (async () => {
668
+ const JXG = (await import('jsxgraph')).default;
669
+ if (cancelled || !containerRef.current) return;
670
+ jxgRef.current = JXG;
671
+ try {
672
+ const opts = JXG.Options;
673
+ if (opts) {
674
+ opts.text = opts.text || {};
675
+ opts.text.display = "internal";
676
+ opts.text.useASCIIMathML = false;
677
+ opts.text.useMathJax = false;
678
+ opts.text.useKatex = false;
679
+ opts.label = opts.label || {};
680
+ opts.label.display = "internal";
681
+ }
682
+ } catch {
683
+ }
684
+ const board = JXG.JSXGraph.initBoard(containerId, {
685
+ boundingbox: initialState?.bbox ?? [-10, 10, 10, -10],
686
+ axis: false,
687
+ // We manage axis manually via toggle for clean default
688
+ grid: false,
689
+ showCopyright: false,
690
+ showNavigation: true,
691
+ // Keep 1:1 user→pixel ratio so circles stay circular regardless of the
692
+ // container aspect ratio (Excalidraw panel is taller than wide and
693
+ // without this circles became ellipses after reload).
694
+ keepAspectRatio: true,
695
+ pan: { enabled: true, needShift: false },
696
+ zoom: { wheel: true },
697
+ // Looser hit-test radius so clicking on a thin segment/line/circle
698
+ // actually registers without pixel-perfect aim. `precision` is a real
699
+ // JSXGraph option (Options.precision) but isn't in the d.ts file.
700
+ ...{ precision: { hasPoint: 8, mouse: 4, touch: 16 } }
701
+ });
702
+ boardRef.current = board;
703
+ if (initialState && initialState.elements.length > 0) {
704
+ const idMap = objMapRef.current;
705
+ for (const el of initialState.elements) {
706
+ const resolved = el.args.map((a) => typeof a === "string" && idMap.has(a) ? idMap.get(a) : a);
707
+ try {
708
+ const obj = board.create(el.type, resolved, { ...el.attrs });
709
+ idMap.set(el.id, obj);
710
+ } catch (err) {
711
+ console.warn("Replay failed for", el.type, err);
712
+ }
713
+ }
714
+ creationLogRef.current = [...initialState.elements];
715
+ labelIdxRef.current = initialState.elements.filter((e) => e.type === "point").length;
716
+ }
717
+ if (showAxisRef.current) {
718
+ try {
719
+ axisObjsRef.current.x = board.create("axis", [[0, 0], [1, 0]], { strokeColor: "#94a3b8", name: "", withLabel: false });
720
+ axisObjsRef.current.y = board.create("axis", [[0, 0], [0, 1]], { strokeColor: "#94a3b8", name: "", withLabel: false });
721
+ } catch {
722
+ }
723
+ }
724
+ if (showGridRef.current) {
725
+ try {
726
+ board.create("grid", [], { strokeColor: "#e2e8f0", strokeOpacity: 1 });
727
+ } catch {
728
+ }
729
+ }
730
+ board.on("down", (e) => {
731
+ if (!boardRef.current) return;
732
+ const t = toolRef.current;
733
+ if (t === "move") return;
734
+ const toolDef = TOOLS.find((td) => td.key === t);
735
+ if (!toolDef) return;
736
+ const coords = boardRef.current.getUsrCoordsOfMouse(e);
737
+ const x = coords[0], y = coords[1];
738
+ const hits = objectsAt(e).filter((o) => o !== axisObjsRef.current.x && o !== axisObjsRef.current.y);
739
+ const bestHit = hits.find((o) => objKind(o) === "point") ?? hits[0] ?? null;
740
+ const snapPointForPointSlot = () => bestHit && objKind(bestHit) === "point" ? bestHit : findNearestPoint(e, 12);
741
+ if (t === "point") {
742
+ const name = nextLabel();
743
+ create("point", [x, y], { name, color: "#0f172a", size: 3, fillColor: "#0f172a", strokeColor: "#0f172a" });
744
+ return;
745
+ }
746
+ if (toolDef.needs === 1 && toolDef.accepts) {
747
+ const hit = bestHit ?? findNearestPoint(e, 12);
748
+ if (hit) finalize(toolDef, [hit]);
749
+ else flashWarn("Click v\xE0o m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng \u0111\u1EC3 \xE1p d\u1EE5ng");
750
+ return;
751
+ }
752
+ if (toolDef.needs === -1) {
753
+ const snappedPoint = snapPointForPointSlot();
754
+ if (pendingRef.current.length >= 3 && snappedPoint && snappedPoint === pendingRef.current[0]) {
755
+ clearPreviewSegs();
756
+ finalize(toolDef, pendingRef.current);
757
+ clearPending();
758
+ return;
759
+ }
760
+ const pick2 = snappedPoint ?? (() => {
761
+ const name = nextLabel();
762
+ return create("point", [x, y], { name, color: "#0f172a", size: 3 });
763
+ })();
764
+ if (pendingRef.current.length > 0 && boardRef.current) {
765
+ const prev = pendingRef.current[pendingRef.current.length - 1];
766
+ try {
767
+ const seg = boardRef.current.create("segment", [prev, pick2], {
768
+ strokeColor: "#3b82f6",
769
+ strokeWidth: 1.5,
770
+ strokeOpacity: 0.75,
771
+ fixed: true,
772
+ highlight: false,
773
+ withLabel: false
774
+ });
775
+ previewSegRef.current.push(seg);
776
+ } catch {
777
+ }
778
+ }
779
+ pendingRef.current.push(pick2);
780
+ setPendingCount(pendingRef.current.length);
781
+ return;
782
+ }
783
+ let pick = null;
784
+ if (toolDef.accepts) {
785
+ const usedKinds = pendingRef.current.map((p) => objKind(p));
786
+ const remaining = [...toolDef.accepts];
787
+ for (const u of usedKinds) {
788
+ if (u === "other") continue;
789
+ const i = remaining.indexOf(u);
790
+ if (i >= 0) remaining.splice(i, 1);
791
+ }
792
+ const strictPoint = hits.find((o) => objKind(o) === "point") ?? null;
793
+ const lineHit = hits.find((o) => objKind(o) === "line") ?? null;
794
+ const circleHit = hits.find((o) => objKind(o) === "circle") ?? null;
795
+ if (remaining.includes("point") && strictPoint) pick = strictPoint;
796
+ else if (remaining.includes("line") && lineHit) pick = lineHit;
797
+ else if (remaining.includes("circle") && circleHit) pick = circleHit;
798
+ else if (remaining.includes("point")) {
799
+ const near = findNearestPoint(e, 12);
800
+ if (near) pick = near;
801
+ } else if (remaining.includes("any")) {
802
+ pick = strictPoint ?? lineHit ?? circleHit ?? null;
803
+ }
804
+ if (!pick) {
805
+ const needs = remaining.map(
806
+ (k) => k === "point" ? "m\u1ED9t \u0111i\u1EC3m" : k === "line" ? "m\u1ED9t \u0111\u01B0\u1EDDng/\u0111o\u1EA1n" : k === "circle" ? "m\u1ED9t \u0111\u01B0\u1EDDng tr\xF2n" : "m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng"
807
+ );
808
+ flashWarn(`C\xF2n c\u1EA7n click v\xE0o ${needs.join(" + ")} c\xF3 s\u1EB5n`);
809
+ return;
810
+ }
811
+ } else {
812
+ const snapped = snapPointForPointSlot();
813
+ if (snapped) pick = snapped;
814
+ else {
815
+ const name = nextLabel();
816
+ pick = create("point", [x, y], { name, color: "#0f172a", size: 3, fillColor: "#0f172a", strokeColor: "#0f172a" });
817
+ }
818
+ }
819
+ if (!pick) return;
820
+ pendingRef.current.push(pick);
821
+ setPendingCount(pendingRef.current.length);
822
+ if (pendingRef.current.length >= toolDef.needs) {
823
+ finalize(toolDef, pendingRef.current);
824
+ clearPending();
825
+ }
826
+ });
827
+ onReady({
828
+ getContainer: () => containerRef.current,
829
+ getCreationLog: () => [...creationLogRef.current],
830
+ getBbox: () => boardRef.current ? boardRef.current.getBoundingBox() : [-10, 10, 10, -10],
831
+ getShowAxis: () => showAxisRef.current,
832
+ getShowGrid: () => showGridRef.current,
833
+ setTool: (t) => handleToolChangeRef.current(t),
834
+ getTool: () => toolRef.current,
835
+ setShowAxis: (b) => setShowAxisRef.current(b),
836
+ setShowGrid: (b) => setShowGridRef.current(b),
837
+ undo: () => undoLastRef.current(),
838
+ canUndo: () => creationLogRef.current.length > 0,
839
+ subscribe: (cb) => {
840
+ subscribersRef.current.add(cb);
841
+ return () => {
842
+ subscribersRef.current.delete(cb);
843
+ };
844
+ }
845
+ });
846
+ })();
847
+ return () => {
848
+ cancelled = true;
849
+ if (boardRef.current && jxgRef.current) {
850
+ try {
851
+ jxgRef.current.JSXGraph.freeBoard(boardRef.current);
852
+ } catch {
853
+ }
854
+ boardRef.current = null;
855
+ }
856
+ };
857
+ }, [containerId]);
858
+ react.useEffect(() => {
859
+ const b = boardRef.current;
860
+ if (!b) return;
861
+ try {
862
+ if (axisObjsRef.current.x) {
863
+ try {
864
+ b.removeObject(axisObjsRef.current.x);
865
+ } catch {
866
+ }
867
+ axisObjsRef.current.x = void 0;
868
+ }
869
+ if (axisObjsRef.current.y) {
870
+ try {
871
+ b.removeObject(axisObjsRef.current.y);
872
+ } catch {
873
+ }
874
+ axisObjsRef.current.y = void 0;
875
+ }
876
+ if (showAxis) {
877
+ axisObjsRef.current.x = b.create("axis", [[0, 0], [1, 0]], { strokeColor: "#94a3b8", name: "", withLabel: false });
878
+ axisObjsRef.current.y = b.create("axis", [[0, 0], [0, 1]], { strokeColor: "#94a3b8", name: "", withLabel: false });
879
+ }
880
+ b.update();
881
+ } catch {
882
+ }
883
+ }, [showAxis]);
884
+ react.useEffect(() => {
885
+ const b = boardRef.current;
886
+ if (!b) return;
887
+ try {
888
+ const objs = Object.values(b.objects || {});
889
+ for (const o of objs) {
890
+ if (o && (o.elType === "grid" || o.type === "grid" || o.visProp && o.visProp.type === "grid")) {
891
+ try {
892
+ b.removeObject(o);
893
+ } catch {
894
+ }
895
+ }
896
+ }
897
+ if (showGrid) {
898
+ b.create("grid", [], { strokeColor: "#e2e8f0", strokeOpacity: 1 });
899
+ }
900
+ b.update();
901
+ } catch {
902
+ }
903
+ }, [showGrid]);
904
+ const handleToolChange = react.useCallback((t) => {
905
+ clearPending();
906
+ toolRef.current = t;
907
+ setTool(t);
908
+ }, [clearPending]);
909
+ const handleToolChangeRef = react.useRef(handleToolChange);
910
+ handleToolChangeRef.current = handleToolChange;
911
+ const subscribersRef = react.useRef(/* @__PURE__ */ new Set());
912
+ const notifySubscribers = react.useCallback(() => {
913
+ subscribersRef.current.forEach((cb) => {
914
+ try {
915
+ cb();
916
+ } catch {
917
+ }
918
+ });
919
+ }, []);
920
+ react.useEffect(() => {
921
+ notifySubscribers();
922
+ }, [tool, showAxis, showGrid, historyTick, notifySubscribers]);
923
+ const undoLastRef = react.useRef(undoLast);
924
+ undoLastRef.current = undoLast;
925
+ const setShowAxisRef = react.useRef(setShowAxis);
926
+ setShowAxisRef.current = setShowAxis;
927
+ const setShowGridRef = react.useRef(setShowGrid);
928
+ setShowGridRef.current = setShowGrid;
929
+ return /* @__PURE__ */ jsxRuntime.jsx(
930
+ "div",
931
+ {
932
+ ref: containerRef,
933
+ id: containerId,
934
+ "data-testid": "jxgmini-container",
935
+ className: "h-full min-h-0 bg-white",
936
+ style: { touchAction: "none" }
937
+ }
938
+ );
939
+ };
940
+ var TOOLTIP_DELAY_MS = 400;
941
+ function Shell({ title, icon, onClose, children }) {
942
+ return /* @__PURE__ */ jsxRuntime.jsxs(
943
+ "aside",
944
+ {
945
+ role: "complementary",
946
+ "aria-label": title,
947
+ "data-testid": "stamp-left-panel",
948
+ "data-stamp-area": "true",
949
+ className: "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",
950
+ children: [
951
+ /* @__PURE__ */ jsxRuntime.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: [
952
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
953
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base leading-none", children: icon }),
954
+ title
955
+ ] }),
956
+ /* @__PURE__ */ jsxRuntime.jsx(
957
+ "button",
958
+ {
959
+ onClick: onClose,
960
+ "aria-label": "\u0110\xF3ng",
961
+ className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
962
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
963
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
964
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
965
+ ] })
966
+ }
967
+ )
968
+ ] }),
969
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-4", children })
970
+ ]
971
+ }
972
+ );
973
+ }
974
+ function Section({ label, children }) {
975
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
976
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: label }),
977
+ children
978
+ ] });
979
+ }
980
+ var GeometryIconHeader = /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
981
+ /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "4,20 20,20 12,5" }),
982
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "20", r: "1.5", fill: "currentColor", stroke: "none" }),
983
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "20", cy: "20", r: "1.5", fill: "currentColor", stroke: "none" }),
984
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "5", r: "1.5", fill: "currentColor", stroke: "none" })
985
+ ] });
986
+ function GeometryLeftPanel({
987
+ activeTool,
988
+ onToolChange,
989
+ showAxis,
990
+ showGrid,
991
+ onShowAxisChange,
992
+ onShowGridChange,
993
+ onUndo,
994
+ canUndo,
995
+ onClose
996
+ }) {
997
+ const grouped = TOOLS.reduce((acc, t) => {
998
+ var _a;
999
+ (acc[_a = t.group] ?? (acc[_a] = [])).push(t);
1000
+ return acc;
1001
+ }, {});
1002
+ const groupKeys = Object.keys(grouped);
1003
+ const [hover, setHover] = react.useState(null);
1004
+ const [portalReady, setPortalReady] = react.useState(false);
1005
+ const hoverTimerRef = react.useRef(null);
1006
+ react.useEffect(() => {
1007
+ setPortalReady(true);
1008
+ return () => {
1009
+ if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
1010
+ };
1011
+ }, []);
1012
+ const showHover = react.useCallback((el, t) => {
1013
+ if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
1014
+ hoverTimerRef.current = setTimeout(() => {
1015
+ const r = el.getBoundingClientRect();
1016
+ setHover({ label: t.label, hint: t.hint, x: r.right, y: r.top + r.height / 2 });
1017
+ }, TOOLTIP_DELAY_MS);
1018
+ }, []);
1019
+ const hideHover = react.useCallback(() => {
1020
+ if (hoverTimerRef.current) {
1021
+ clearTimeout(hoverTimerRef.current);
1022
+ hoverTimerRef.current = null;
1023
+ }
1024
+ setHover(null);
1025
+ }, []);
1026
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1027
+ /* @__PURE__ */ jsxRuntime.jsxs(Shell, { title: "H\xECnh h\u1ECDc", icon: GeometryIconHeader, onClose, children: [
1028
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { label: "B\u1ED1 c\u1EE5c", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 text-[11px] text-slate-700", children: [
1029
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
1030
+ /* @__PURE__ */ jsxRuntime.jsx(
1031
+ "input",
1032
+ {
1033
+ type: "checkbox",
1034
+ checked: showAxis,
1035
+ onChange: (e) => onShowAxisChange(e.target.checked),
1036
+ "data-testid": "toggle-axis"
1037
+ }
1038
+ ),
1039
+ "Tr\u1EE5c to\u1EA1 \u0111\u1ED9"
1040
+ ] }),
1041
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
1042
+ /* @__PURE__ */ jsxRuntime.jsx(
1043
+ "input",
1044
+ {
1045
+ type: "checkbox",
1046
+ checked: showGrid,
1047
+ onChange: (e) => onShowGridChange(e.target.checked),
1048
+ "data-testid": "toggle-grid"
1049
+ }
1050
+ ),
1051
+ "L\u01B0\u1EDBi"
1052
+ ] }),
1053
+ /* @__PURE__ */ jsxRuntime.jsx(
1054
+ "button",
1055
+ {
1056
+ type: "button",
1057
+ onClick: onUndo,
1058
+ disabled: !canUndo,
1059
+ title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
1060
+ "aria-label": "Ho\xE0n t\xE1c",
1061
+ className: "ml-auto 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",
1062
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1063
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "3 7 3 13 9 13" }),
1064
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
1065
+ ] })
1066
+ }
1067
+ )
1068
+ ] }) }),
1069
+ groupKeys.map((group) => /* @__PURE__ */ jsxRuntime.jsx(Section, { label: GROUP_LABELS[group], children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-4 gap-1", children: grouped[group].map((t) => {
1070
+ const active = activeTool === t.key;
1071
+ return /* @__PURE__ */ jsxRuntime.jsx(
1072
+ "button",
1073
+ {
1074
+ type: "button",
1075
+ "aria-label": t.label,
1076
+ "aria-pressed": active,
1077
+ "data-tool": t.key,
1078
+ onClick: () => onToolChange(t.key),
1079
+ onMouseEnter: (e) => showHover(e.currentTarget, t),
1080
+ onMouseLeave: hideHover,
1081
+ onFocus: (e) => showHover(e.currentTarget, t),
1082
+ onBlur: hideHover,
1083
+ className: [
1084
+ "flex h-8 items-center justify-center rounded-md transition",
1085
+ active ? "bg-emerald-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
1086
+ ].join(" "),
1087
+ children: t.icon
1088
+ },
1089
+ t.key
1090
+ );
1091
+ }) }) }, group))
1092
+ ] }),
1093
+ portalReady && hover && typeof document !== "undefined" ? reactDom.createPortal(
1094
+ /* @__PURE__ */ jsxRuntime.jsxs(
1095
+ "div",
1096
+ {
1097
+ role: "tooltip",
1098
+ 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",
1099
+ style: {
1100
+ left: hover.x + 8,
1101
+ top: hover.y,
1102
+ transform: "translate(0, -50%)",
1103
+ zIndex: 2147483600
1104
+ },
1105
+ children: [
1106
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block font-medium", children: hover.label }),
1107
+ hover.hint && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-0.5 block text-slate-300", children: hover.hint })
1108
+ ]
1109
+ }
1110
+ ),
1111
+ document.body
1112
+ ) : null
1113
+ ] });
1114
+ }
1115
+ var SNIPPETS = [
1116
+ {
1117
+ group: "Ph\xE2n s\u1ED1 & lu\u1EF9 th\u1EEBa",
1118
+ items: [
1119
+ { label: "Ph\xE2n s\u1ED1", preview: "a\u2044b", snippet: "\\frac{a}{b}" },
1120
+ { label: "Lu\u1EF9 th\u1EEBa", preview: "x\xB2", snippet: "^{2}" },
1121
+ { label: "Ch\u1EC9 s\u1ED1", preview: "x\u2081", snippet: "_{1}" },
1122
+ { label: "C\u0103n", preview: "\u221Ax", snippet: "\\sqrt{x}" },
1123
+ { label: "C\u0103n n", preview: "\u207F\u221Ax", snippet: "\\sqrt[n]{x}" }
1124
+ ]
1125
+ },
1126
+ {
1127
+ group: "T\u1ED5ng & t\xEDch ph\xE2n",
1128
+ items: [
1129
+ { label: "T\u1ED5ng", preview: "\u03A3", snippet: "\\sum_{i=1}^{n}" },
1130
+ { label: "T\xEDch", preview: "\u03A0", snippet: "\\prod_{i=1}^{n}" },
1131
+ { label: "T\xEDch ph\xE2n", preview: "\u222B", snippet: "\\int_{a}^{b}" },
1132
+ { label: "Gi\u1EDBi h\u1EA1n", preview: "lim", snippet: "\\lim_{x \\to 0}" }
1133
+ ]
1134
+ },
1135
+ {
1136
+ group: "K\xFD hi\u1EC7u",
1137
+ items: [
1138
+ { label: "\u03B1", preview: "\u03B1", snippet: "\\alpha" },
1139
+ { label: "\u03B2", preview: "\u03B2", snippet: "\\beta" },
1140
+ { label: "\u03C0", preview: "\u03C0", snippet: "\\pi" },
1141
+ { label: "\u03B8", preview: "\u03B8", snippet: "\\theta" },
1142
+ { label: "\u2260", preview: "\u2260", snippet: "\\neq" },
1143
+ { label: "\u2264", preview: "\u2264", snippet: "\\leq" },
1144
+ { label: "\u2265", preview: "\u2265", snippet: "\\geq" },
1145
+ { label: "\u221E", preview: "\u221E", snippet: "\\infty" },
1146
+ { label: "\u2192", preview: "\u2192", snippet: "\\to" }
1147
+ ]
1148
+ }
1149
+ ];
1150
+ function LatexLeftPanel({
1151
+ displayMode,
1152
+ onDisplayModeChange,
1153
+ onInsertSnippet,
1154
+ onClose
1155
+ }) {
1156
+ return /* @__PURE__ */ jsxRuntime.jsxs(Shell, { title: "C\xF4ng th\u1EE9c LaTeX", icon: "\u2211", onClose, children: [
1157
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { label: "Ch\u1EBF \u0111\u1ED9 hi\u1EC3n th\u1ECB", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-1.5", children: [
1158
+ /* @__PURE__ */ jsxRuntime.jsxs(
1159
+ "button",
1160
+ {
1161
+ type: "button",
1162
+ onClick: () => onDisplayModeChange(false),
1163
+ "aria-pressed": !displayMode,
1164
+ className: [
1165
+ "rounded-md border px-2 py-1.5 text-xs transition",
1166
+ !displayMode ? "border-emerald-500 bg-emerald-50 text-emerald-700 ring-1 ring-emerald-300" : "border-slate-200 bg-white text-slate-700 hover:border-slate-300 hover:bg-slate-50"
1167
+ ].join(" "),
1168
+ children: [
1169
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block font-medium", children: "Inline" }),
1170
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-[10px] text-slate-500", children: "$ ... $" })
1171
+ ]
1172
+ }
1173
+ ),
1174
+ /* @__PURE__ */ jsxRuntime.jsxs(
1175
+ "button",
1176
+ {
1177
+ type: "button",
1178
+ onClick: () => onDisplayModeChange(true),
1179
+ "aria-pressed": displayMode,
1180
+ className: [
1181
+ "rounded-md border px-2 py-1.5 text-xs transition",
1182
+ displayMode ? "border-emerald-500 bg-emerald-50 text-emerald-700 ring-1 ring-emerald-300" : "border-slate-200 bg-white text-slate-700 hover:border-slate-300 hover:bg-slate-50"
1183
+ ].join(" "),
1184
+ children: [
1185
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block font-medium", children: "Block" }),
1186
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-[10px] text-slate-500", children: "$$ ... $$" })
1187
+ ]
1188
+ }
1189
+ )
1190
+ ] }) }),
1191
+ SNIPPETS.map((group) => /* @__PURE__ */ jsxRuntime.jsx(Section, { label: group.group, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1", children: group.items.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
1192
+ "button",
1193
+ {
1194
+ type: "button",
1195
+ onClick: () => onInsertSnippet(s.snippet),
1196
+ title: s.snippet,
1197
+ className: "rounded border border-slate-200 bg-white px-2 py-1 text-xs text-slate-700 transition hover:border-emerald-300 hover:bg-emerald-50 hover:text-emerald-700",
1198
+ children: s.preview
1199
+ },
1200
+ s.snippet
1201
+ )) }) }, group.group)),
1202
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { label: "Ph\xEDm t\u1EAFt", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2 text-[11px] text-slate-600", children: [
1203
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1", children: [
1204
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 font-mono", children: "Enter" }),
1205
+ "ch\xE8n"
1206
+ ] }),
1207
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1", children: [
1208
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 font-mono", children: "Esc" }),
1209
+ "\u0111\xF3ng"
1210
+ ] })
1211
+ ] }) })
1212
+ ] });
1213
+ }
1214
+
1215
+ // src/stamp/renderLatexToSvg.ts
1216
+ var cachedCss = null;
1217
+ function absoluteOrigin() {
1218
+ if (typeof window !== "undefined" && window.location) return window.location.origin;
1219
+ return "";
1220
+ }
1221
+ async function loadKatexCss() {
1222
+ if (cachedCss !== null) return cachedCss;
1223
+ try {
1224
+ if (typeof fetch === "function") {
1225
+ const res = await fetch("/katex.min.css");
1226
+ if (res.ok) {
1227
+ let css = await res.text();
1228
+ const origin = absoluteOrigin();
1229
+ if (origin) {
1230
+ css = css.replace(/url\((['"]?)(fonts\/)/g, `url($1${origin}/$2`);
1231
+ }
1232
+ cachedCss = css;
1233
+ return css;
1234
+ }
1235
+ }
1236
+ } catch {
1237
+ }
1238
+ cachedCss = "";
1239
+ return "";
1240
+ }
1241
+ async function renderLatexToSvg(src, displayMode) {
1242
+ const katex = await import('katex');
1243
+ const html = katex.default.renderToString(src, { displayMode, throwOnError: true, output: "html" });
1244
+ const measureDiv = document.createElement("div");
1245
+ measureDiv.style.cssText = "position:absolute;top:-9999px;left:-9999px;visibility:hidden;display:inline-block;";
1246
+ measureDiv.innerHTML = html;
1247
+ document.body.appendChild(measureDiv);
1248
+ const rect = measureDiv.getBoundingClientRect();
1249
+ const width = Math.ceil(rect.width) || 50;
1250
+ const height = Math.ceil(rect.height) || 20;
1251
+ document.body.removeChild(measureDiv);
1252
+ const cssText = await loadKatexCss();
1253
+ return '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="' + height + '" viewBox="0 0 ' + width + " " + height + '"><foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:16px;line-height:1.2;"><style>' + cssText + "</style>" + html + "</div></foreignObject></svg>";
1254
+ }
1255
+ var DEBOUNCE_MS = 100;
1256
+ var LatexEditorPopover = react.forwardRef(function LatexEditorPopover2({
1257
+ x,
1258
+ y,
1259
+ initialValue,
1260
+ onInsert,
1261
+ onClose,
1262
+ displayMode: controlledDisplayMode,
1263
+ onDisplayModeChange,
1264
+ withLeftPanel = false
1265
+ }, ref) {
1266
+ const [value, setValue] = react.useState(initialValue);
1267
+ const [internalDisplayMode] = react.useState(false);
1268
+ const displayMode = controlledDisplayMode ?? internalDisplayMode;
1269
+ const [previewSvg, setPreviewSvg] = react.useState(null);
1270
+ const [error, setError] = react.useState(null);
1271
+ const debounceRef = react.useRef(null);
1272
+ const inputRef = react.useRef(null);
1273
+ react.useEffect(() => {
1274
+ if (debounceRef.current) clearTimeout(debounceRef.current);
1275
+ debounceRef.current = setTimeout(async () => {
1276
+ try {
1277
+ const svg = await renderLatexToSvg(value, displayMode);
1278
+ setPreviewSvg(svg);
1279
+ setError(null);
1280
+ } catch (err) {
1281
+ setPreviewSvg(null);
1282
+ setError(err.message);
1283
+ }
1284
+ }, DEBOUNCE_MS);
1285
+ return () => {
1286
+ if (debounceRef.current) clearTimeout(debounceRef.current);
1287
+ };
1288
+ }, [value, displayMode]);
1289
+ const handleInsert = react.useCallback(() => {
1290
+ if (!previewSvg) return;
1291
+ onInsert(previewSvg, value, displayMode);
1292
+ }, [previewSvg, value, displayMode, onInsert]);
1293
+ const handleKeyDown = react.useCallback(
1294
+ (e) => {
1295
+ if (e.key === "Escape") onClose();
1296
+ if (e.key === "Enter" && !e.shiftKey) {
1297
+ e.preventDefault();
1298
+ handleInsert();
1299
+ }
1300
+ },
1301
+ [onClose, handleInsert]
1302
+ );
1303
+ react.useImperativeHandle(
1304
+ ref,
1305
+ () => ({
1306
+ insertAtCursor: (snippet) => {
1307
+ const el = inputRef.current;
1308
+ if (!el) {
1309
+ setValue((v) => v + snippet);
1310
+ return;
1311
+ }
1312
+ const start = el.selectionStart ?? value.length;
1313
+ const end = el.selectionEnd ?? value.length;
1314
+ const next = value.slice(0, start) + snippet + value.slice(end);
1315
+ setValue(next);
1316
+ requestAnimationFrame(() => {
1317
+ el.focus();
1318
+ const pos = start + snippet.length;
1319
+ try {
1320
+ el.setSelectionRange(pos, pos);
1321
+ } catch {
1322
+ }
1323
+ });
1324
+ },
1325
+ hasContent: () => value.trim().length > 0 && !!previewSvg && !error,
1326
+ tryInsert: () => {
1327
+ if (!previewSvg || error || !value.trim()) return false;
1328
+ onInsert(previewSvg, value, displayMode);
1329
+ return true;
1330
+ }
1331
+ }),
1332
+ [value, previewSvg, error, displayMode, onInsert]
1333
+ );
1334
+ const isLegacyPosition = x > 0 || y > 0;
1335
+ const wrapperStyle = isLegacyPosition ? { position: "absolute", top: y, left: x, zIndex: 50 } : {
1336
+ position: "absolute",
1337
+ top: "50%",
1338
+ left: withLeftPanel ? "calc(50% + 120px)" : "50%",
1339
+ transform: "translate(-50%, -50%)",
1340
+ zIndex: 50
1341
+ };
1342
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1343
+ "div",
1344
+ {
1345
+ style: wrapperStyle,
1346
+ "data-stamp-area": "true",
1347
+ className: "w-[420px] max-w-[calc(100vw-280px)] rounded-lg border border-slate-300 bg-white shadow-2xl ring-1 ring-black/5",
1348
+ role: "dialog",
1349
+ "aria-label": "Nh\u1EADp c\xF4ng th\u1EE9c LaTeX",
1350
+ children: [
1351
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center justify-between rounded-t-lg border-b border-slate-200 bg-gradient-to-r from-indigo-600 to-purple-600 px-3 py-2 text-white", children: [
1352
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold", children: [
1353
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base leading-none", children: "\u2211" }),
1354
+ "C\xF4ng th\u1EE9c LaTeX"
1355
+ ] }),
1356
+ /* @__PURE__ */ jsxRuntime.jsx(
1357
+ "button",
1358
+ {
1359
+ onClick: onClose,
1360
+ "aria-label": "\u0110\xF3ng",
1361
+ className: "rounded p-1 transition hover:bg-white/15",
1362
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1363
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
1364
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
1365
+ ] })
1366
+ }
1367
+ )
1368
+ ] }),
1369
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2 p-3", children: [
1370
+ /* @__PURE__ */ jsxRuntime.jsx(
1371
+ "input",
1372
+ {
1373
+ ref: inputRef,
1374
+ type: "text",
1375
+ role: "textbox",
1376
+ value,
1377
+ onChange: (e) => setValue(e.target.value),
1378
+ onKeyDown: handleKeyDown,
1379
+ placeholder: "Vd: \\frac{a^2+b^2}{c}",
1380
+ className: "w-full rounded border border-slate-300 px-2 py-1.5 font-mono text-sm outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-200",
1381
+ autoFocus: true
1382
+ }
1383
+ ),
1384
+ /* @__PURE__ */ jsxRuntime.jsx(
1385
+ "div",
1386
+ {
1387
+ className: [
1388
+ "flex min-h-[64px] items-center justify-center rounded border p-3 text-center",
1389
+ error ? "border-rose-300 bg-rose-50 text-rose-700" : "border-slate-200 bg-slate-50"
1390
+ ].join(" "),
1391
+ children: error ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs", children: [
1392
+ "L\u1ED7i: ",
1393
+ error.slice(0, 80)
1394
+ ] }) : previewSvg ? /* @__PURE__ */ jsxRuntime.jsx("span", { dangerouslySetInnerHTML: { __html: previewSvg } }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-slate-400", children: "(xem tr\u01B0\u1EDBc)" })
1395
+ }
1396
+ ),
1397
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
1398
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-[11px] text-slate-500", children: [
1399
+ displayMode ? "Block" : "Inline",
1400
+ " \xB7 Enter \u0111\u1EC3 ch\xE8n"
1401
+ ] }),
1402
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
1403
+ /* @__PURE__ */ jsxRuntime.jsx(
1404
+ "button",
1405
+ {
1406
+ onClick: onClose,
1407
+ className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
1408
+ children: "Hu\u1EF7"
1409
+ }
1410
+ ),
1411
+ /* @__PURE__ */ jsxRuntime.jsx(
1412
+ "button",
1413
+ {
1414
+ onClick: handleInsert,
1415
+ disabled: !previewSvg || !!error,
1416
+ className: "rounded bg-indigo-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-indigo-700 disabled:opacity-50",
1417
+ children: "Ch\xE8n"
1418
+ }
1419
+ )
1420
+ ] })
1421
+ ] })
1422
+ ] })
1423
+ ]
1424
+ }
1425
+ );
1426
+ });
1427
+
1428
+ // src/stamp/serializeBoard.ts
1429
+ function serializeBoard(board, log, options = {}) {
1430
+ return {
1431
+ bbox: board.getBoundingBox(),
1432
+ elements: log.map((e) => ({ type: e.type, args: e.args, attrs: e.attrs, id: e.id })),
1433
+ showAxis: !!options.showAxis,
1434
+ showGrid: !!options.showGrid
1435
+ };
1436
+ }
1437
+ function deserializeIntoBoard(board, serialized) {
1438
+ const idMap = /* @__PURE__ */ new Map();
1439
+ for (const el of serialized.elements) {
1440
+ const resolvedArgs = el.args.map((a) => {
1441
+ if (typeof a === "string" && idMap.has(a)) return idMap.get(a);
1442
+ return a;
1443
+ });
1444
+ const created = board.create(el.type, resolvedArgs, { ...el.attrs });
1445
+ idMap.set(el.id, created);
1446
+ }
1447
+ }
1448
+
1449
+ // src/stamp/renderGeometryToSvg.ts
1450
+ function renderGeometryToSvg(boardContainer) {
1451
+ const svgEl = boardContainer.querySelector("svg");
1452
+ if (!svgEl) throw new Error("renderGeometryToSvg: no SVG found in board container");
1453
+ const clone = svgEl.cloneNode(true);
1454
+ if (!clone.getAttribute("xmlns")) {
1455
+ clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
1456
+ }
1457
+ return new XMLSerializer().serializeToString(clone);
1458
+ }
1459
+ var GeometryEditorPanel = react.forwardRef(
1460
+ function GeometryEditorPanel2({ initialState, onInsert, onClose, withLeftPanel = false, onStateChange }, ref) {
1461
+ const handleRef = react.useRef(null);
1462
+ const [ready, setReady] = react.useState(false);
1463
+ const onStateChangeRef = react.useRef(onStateChange);
1464
+ react.useEffect(() => {
1465
+ onStateChangeRef.current = onStateChange;
1466
+ }, [onStateChange]);
1467
+ const emitState = react.useCallback(() => {
1468
+ const h = handleRef.current;
1469
+ const cb = onStateChangeRef.current;
1470
+ if (!h || !cb) return;
1471
+ cb({
1472
+ tool: h.getTool(),
1473
+ showAxis: h.getShowAxis(),
1474
+ showGrid: h.getShowGrid(),
1475
+ canUndo: h.canUndo()
1476
+ });
1477
+ }, []);
1478
+ const handleReady = react.useCallback((h) => {
1479
+ handleRef.current = h;
1480
+ setReady(true);
1481
+ emitState();
1482
+ h.subscribe(emitState);
1483
+ }, [emitState]);
1484
+ const performInsert = react.useCallback(() => {
1485
+ if (!handleRef.current) return false;
1486
+ const container = handleRef.current.getContainer();
1487
+ if (!container) return false;
1488
+ const log = handleRef.current.getCreationLog();
1489
+ if (log.length === 0) return false;
1490
+ try {
1491
+ const svgString = renderGeometryToSvg(container);
1492
+ const bbox = handleRef.current.getBbox();
1493
+ const showAxis = handleRef.current.getShowAxis();
1494
+ const showGrid = handleRef.current.getShowGrid();
1495
+ const serialized = serializeBoard(
1496
+ { getBoundingBox: () => bbox, create: () => void 0 },
1497
+ log,
1498
+ { showAxis, showGrid }
1499
+ );
1500
+ onInsert(JSON.stringify(serialized), svgString);
1501
+ return true;
1502
+ } catch (err) {
1503
+ console.error("Geometry insert failed:", err);
1504
+ return false;
1505
+ }
1506
+ }, [onInsert]);
1507
+ const handleInsert = react.useCallback(() => {
1508
+ performInsert();
1509
+ }, [performInsert]);
1510
+ react.useImperativeHandle(ref, () => ({
1511
+ setTool: (t) => handleRef.current?.setTool(t),
1512
+ setShowAxis: (b) => handleRef.current?.setShowAxis(b),
1513
+ setShowGrid: (b) => handleRef.current?.setShowGrid(b),
1514
+ undo: () => handleRef.current?.undo(),
1515
+ insert: performInsert,
1516
+ hasContent: () => (handleRef.current?.getCreationLog().length ?? 0) > 0
1517
+ }), [performInsert]);
1518
+ const wrapperStyle = {
1519
+ position: "absolute",
1520
+ top: "50%",
1521
+ left: withLeftPanel ? "calc(50% + 120px)" : "50%",
1522
+ transform: "translate(-50%, -50%)",
1523
+ zIndex: 40
1524
+ };
1525
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1526
+ "div",
1527
+ {
1528
+ role: "dialog",
1529
+ "aria-label": "D\u1EF1ng h\xECnh h\u1ECDc",
1530
+ "data-testid": "geometry-editor-panel",
1531
+ "data-stamp-area": "true",
1532
+ style: wrapperStyle,
1533
+ className: "flex h-[540px] max-h-[85vh] w-[640px] max-w-[calc(100vw-280px)] flex-col overflow-hidden rounded-lg border border-slate-300 bg-white shadow-2xl ring-1 ring-black/5",
1534
+ children: [
1535
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-emerald-600 to-teal-600 px-3 py-2 text-white", children: [
1536
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold", children: [
1537
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1538
+ /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "3,18 12,3 21,18" }),
1539
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "3", r: "1.5", fill: "currentColor" }),
1540
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "3", cy: "18", r: "1.5", fill: "currentColor" }),
1541
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "21", cy: "18", r: "1.5", fill: "currentColor" })
1542
+ ] }),
1543
+ "D\u1EF1ng h\xECnh h\u1ECDc"
1544
+ ] }),
1545
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onClose, "aria-label": "\u0110\xF3ng", className: "rounded p-1 transition hover:bg-white/15", children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1546
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
1547
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
1548
+ ] }) })
1549
+ ] }),
1550
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1", style: { height: "420px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
1551
+ JSXGraphMiniBoard,
1552
+ {
1553
+ onReady: handleReady,
1554
+ initialState
1555
+ }
1556
+ ) }),
1557
+ /* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
1558
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-slate-500", children: "Ch\u1ECDn c\xF4ng c\u1EE5 b\xEAn tr\xE1i, click tr\xEAn b\u1EA3ng \u0111\u1EC3 d\u1EF1ng h\xECnh." }),
1559
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
1560
+ /* @__PURE__ */ jsxRuntime.jsx(
1561
+ "button",
1562
+ {
1563
+ onClick: onClose,
1564
+ className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
1565
+ children: "Hu\u1EF7"
1566
+ }
1567
+ ),
1568
+ /* @__PURE__ */ jsxRuntime.jsx(
1569
+ "button",
1570
+ {
1571
+ onClick: handleInsert,
1572
+ disabled: !ready,
1573
+ "data-testid": "geometry-insert-btn",
1574
+ className: "rounded bg-emerald-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-emerald-700 disabled:opacity-50",
1575
+ children: "Ch\xE8n"
1576
+ }
1577
+ )
1578
+ ] })
1579
+ ] })
1580
+ ]
1581
+ }
1582
+ );
1583
+ }
1584
+ );
1585
+ function isEditableTarget(t) {
1586
+ if (!t || !(t instanceof HTMLElement)) return false;
1587
+ if (t.isContentEditable) return true;
1588
+ const tag = t.tagName;
1589
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
1590
+ }
1591
+ function useStampShortcuts({ onGeometry, onLatex, enabled }) {
1592
+ react.useEffect(() => {
1593
+ if (!enabled) return;
1594
+ const handler = (e) => {
1595
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
1596
+ if (isEditableTarget(e.target)) return;
1597
+ const key = e.key.toLowerCase();
1598
+ if (key !== "g" && key !== "l") return;
1599
+ e.preventDefault();
1600
+ e.stopPropagation();
1601
+ if (key === "g") onGeometry();
1602
+ else onLatex();
1603
+ };
1604
+ window.addEventListener("keydown", handler, { capture: true });
1605
+ return () => window.removeEventListener("keydown", handler, { capture: true });
1606
+ }, [enabled, onGeometry, onLatex]);
1607
+ }
1608
+
1609
+ // src/stamp/types.ts
1610
+ function isMathStamp(element) {
1611
+ const c = element.customData;
1612
+ if (!c) return false;
1613
+ if (c.version !== 1) return false;
1614
+ return c.kind === "geometry" || c.kind === "latex";
1615
+ }
1616
+
1617
+ // src/stamp/svgToImageElement.ts
1618
+ async function hashString(input) {
1619
+ if (typeof crypto !== "undefined" && crypto.subtle) {
1620
+ const buf = new TextEncoder().encode(input);
1621
+ const digest = await crypto.subtle.digest("SHA-256", buf);
1622
+ return Array.from(new Uint8Array(digest)).slice(0, 16).map((b) => b.toString(16).padStart(2, "0")).join("");
1623
+ }
1624
+ let h1 = 2166136261;
1625
+ let h2 = 3421674724;
1626
+ for (let i = 0; i < input.length; i++) {
1627
+ const c = input.charCodeAt(i);
1628
+ h1 ^= c;
1629
+ h1 = Math.imul(h1, 16777619);
1630
+ h2 ^= c + i;
1631
+ h2 = Math.imul(h2, 1099511628211 & 4294967295);
1632
+ }
1633
+ return (h1 >>> 0).toString(16).padStart(8, "0") + (h2 >>> 0).toString(16).padStart(8, "0");
1634
+ }
1635
+ function parseSize(svg, attr) {
1636
+ const re = new RegExp(`<svg[^>]*\\s${attr}="(\\d+(?:\\.\\d+)?)`, "i");
1637
+ const m = svg.match(re);
1638
+ if (m) return Math.max(1, Math.round(parseFloat(m[1])));
1639
+ const vb = svg.match(/viewBox="([\d.\s-]+)"/i);
1640
+ if (vb) {
1641
+ const parts = vb[1].trim().split(/\s+/).map(parseFloat);
1642
+ if (parts.length === 4) return Math.max(1, Math.round(attr === "width" ? parts[2] : parts[3]));
1643
+ }
1644
+ return attr === "width" ? 200 : 100;
1645
+ }
1646
+ async function svgToImageElement(svg) {
1647
+ const width = parseSize(svg, "width");
1648
+ const height = parseSize(svg, "height");
1649
+ const utf8 = unescape(encodeURIComponent(svg));
1650
+ const dataURL = "data:image/svg+xml;base64," + btoa(utf8);
1651
+ const fileId = await hashString(dataURL);
1652
+ return { dataURL, fileId, width, height, mimeType: "image/svg+xml" };
1653
+ }
1654
+
1655
+ // src/stamp/restoreMathStampFiles.ts
1656
+ function svgToDataURL(svg) {
1657
+ const utf8 = unescape(encodeURIComponent(svg));
1658
+ return "data:image/svg+xml;base64," + btoa(utf8);
1659
+ }
1660
+ async function renderGeometrySvgFromState(jsonState) {
1661
+ const parsed = JSON.parse(jsonState);
1662
+ const JXG = (await import('jsxgraph')).default;
1663
+ try {
1664
+ const opts = JXG.Options;
1665
+ if (opts) {
1666
+ opts.text = opts.text || {};
1667
+ opts.text.display = "internal";
1668
+ opts.text.useASCIIMathML = false;
1669
+ opts.text.useMathJax = false;
1670
+ opts.text.useKatex = false;
1671
+ opts.label = opts.label || {};
1672
+ opts.label.display = "internal";
1673
+ }
1674
+ } catch {
1675
+ }
1676
+ const container = document.createElement("div");
1677
+ const containerId = "jxg_offscreen_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
1678
+ container.id = containerId;
1679
+ container.style.cssText = "position:absolute;top:-99999px;left:-99999px;width:400px;height:300px;visibility:hidden;pointer-events:none;";
1680
+ document.body.appendChild(container);
1681
+ let board = null;
1682
+ try {
1683
+ board = JXG.JSXGraph.initBoard(containerId, {
1684
+ boundingbox: parsed.bbox,
1685
+ axis: !!parsed.showAxis,
1686
+ grid: !!parsed.showGrid,
1687
+ showCopyright: false,
1688
+ showNavigation: false,
1689
+ keepAspectRatio: false
1690
+ });
1691
+ deserializeIntoBoard(board, parsed);
1692
+ board.update();
1693
+ return renderGeometryToSvg(container);
1694
+ } finally {
1695
+ try {
1696
+ if (board) JXG.JSXGraph.freeBoard(board);
1697
+ } catch {
1698
+ }
1699
+ if (container.parentNode) container.parentNode.removeChild(container);
1700
+ }
1701
+ }
1702
+ async function buildFileForStamp(fileId, customData) {
1703
+ try {
1704
+ let svg;
1705
+ if (customData.kind === "latex") {
1706
+ svg = await renderLatexToSvg(customData.src, customData.displayMode);
1707
+ } else if (customData.kind === "geometry") {
1708
+ svg = await renderGeometrySvgFromState(customData.jsonState);
1709
+ } else {
1710
+ return null;
1711
+ }
1712
+ return { id: fileId, dataURL: svgToDataURL(svg), mimeType: "image/svg+xml", created: Date.now() };
1713
+ } catch (err) {
1714
+ console.warn("Math-stamp restore failed for", fileId, err);
1715
+ return null;
1716
+ }
1717
+ }
1718
+ async function restoreMissingMathStampFiles(api, elements) {
1719
+ if (!api) return;
1720
+ const existing = typeof api.getFiles === "function" ? api.getFiles() : {};
1721
+ const targets = [];
1722
+ const seen = /* @__PURE__ */ new Set();
1723
+ for (const el of elements) {
1724
+ if (el.type !== "image") continue;
1725
+ if (!el.fileId) continue;
1726
+ if (existing && existing[el.fileId]) continue;
1727
+ if (seen.has(el.fileId)) continue;
1728
+ if (!isMathStamp(el)) continue;
1729
+ seen.add(el.fileId);
1730
+ targets.push({ fileId: el.fileId, customData: el.customData });
1731
+ }
1732
+ if (targets.length === 0) return;
1733
+ const built = await Promise.all(targets.map((t) => buildFileForStamp(t.fileId, t.customData)));
1734
+ const files = built.filter((f) => !!f);
1735
+ if (files.length > 0) {
1736
+ try {
1737
+ api.addFiles(files);
1738
+ } catch (err) {
1739
+ console.warn("addFiles failed:", err);
1740
+ }
1741
+ }
1742
+ }
1743
+ var Excalidraw2 = dynamic__default.default(
1744
+ async () => (await Promise.resolve().then(() => (init_ExcalidrawWithMenus(), ExcalidrawWithMenus_exports))).ExcalidrawWithMenus,
1745
+ {
1746
+ ssr: false,
1747
+ loading: () => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center text-sm text-gray-500", children: "\u0110ang t\u1EA3i b\u1EA3ng\u2026" })
1748
+ }
1749
+ );
1750
+ var SYNC_THROTTLE_MS = 200;
1751
+ var DOUBLE_CLICK_MS = 400;
1752
+ var INITIAL_GEOM_STATE = {
1753
+ tool: "move",
1754
+ showAxis: false,
1755
+ showGrid: false,
1756
+ canUndo: false
1757
+ };
1758
+ function ExcalidrawWhiteboardView({
1759
+ role,
1760
+ initialScene,
1761
+ remoteScene,
1762
+ remoteFiles,
1763
+ onSceneChange,
1764
+ onFilesChange,
1765
+ langCode = "vi-VN"
1766
+ }) {
1767
+ const [api, setApi] = react.useState(null);
1768
+ const [isDarkTheme, setIsDarkTheme] = react.useState(false);
1769
+ const knownFileIdsRef = react.useRef(/* @__PURE__ */ new Set());
1770
+ const lastElementsHashRef = react.useRef("");
1771
+ const throttleTimerRef = react.useRef(null);
1772
+ const [activeStamp, setActiveStamp] = react.useState(null);
1773
+ const activeStampRef = react.useRef(activeStamp);
1774
+ activeStampRef.current = activeStamp;
1775
+ const [geometryEditing, setGeometryEditing] = react.useState({ editingElementId: null, initialState: null });
1776
+ const [geomState, setGeomState] = react.useState(INITIAL_GEOM_STATE);
1777
+ const geomPanelRef = react.useRef(null);
1778
+ const [latexEditing, setLatexEditing] = react.useState({ editingElementId: null, initialValue: "", x: 0, y: 0 });
1779
+ const [latexDisplayMode, setLatexDisplayMode] = react.useState(false);
1780
+ const latexEditorRef = react.useRef(null);
1781
+ const latexInsertableRef = react.useRef({
1782
+ insert: () => false,
1783
+ hasContent: () => false
1784
+ });
1785
+ const lastClickRef = react.useRef({
1786
+ time: 0,
1787
+ elementId: null
1788
+ });
1789
+ const handledCropIdRef = react.useRef(null);
1790
+ const prevExcalidrawToolRef = react.useRef("selection");
1791
+ const isTeacher = role === "teacher";
1792
+ const openGeometry = react.useCallback(() => {
1793
+ if (!isTeacher) return;
1794
+ setGeometryEditing({ editingElementId: null, initialState: null });
1795
+ setGeomState(INITIAL_GEOM_STATE);
1796
+ setActiveStamp("geometry");
1797
+ }, [isTeacher]);
1798
+ const openLatex = react.useCallback(() => {
1799
+ if (!isTeacher) return;
1800
+ setLatexEditing({ editingElementId: null, initialValue: "", x: 0, y: 0 });
1801
+ setActiveStamp("latex");
1802
+ }, [isTeacher]);
1803
+ const closeStamp = react.useCallback(() => {
1804
+ setActiveStamp(null);
1805
+ setGeometryEditing({ editingElementId: null, initialState: null });
1806
+ setLatexEditing({ editingElementId: null, initialValue: "", x: 0, y: 0 });
1807
+ }, []);
1808
+ const toggleGeometry = react.useCallback(() => {
1809
+ if (activeStamp === "geometry") closeStamp();
1810
+ else openGeometry();
1811
+ }, [activeStamp, closeStamp, openGeometry]);
1812
+ const toggleLatex = react.useCallback(() => {
1813
+ if (activeStamp === "latex") closeStamp();
1814
+ else openLatex();
1815
+ }, [activeStamp, closeStamp, openLatex]);
1816
+ const handleChange = react.useCallback(
1817
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1818
+ (elements, appState, files) => {
1819
+ const nextDark = appState?.theme === "dark";
1820
+ setIsDarkTheme((prev) => prev === nextDark ? prev : nextDark);
1821
+ if (!isTeacher) return;
1822
+ const cropId = appState?.croppingElementId;
1823
+ if (cropId && cropId !== handledCropIdRef.current && api) {
1824
+ const el = elements.find((e) => e.id === cropId);
1825
+ if (el && isMathStamp(el)) {
1826
+ handledCropIdRef.current = cropId;
1827
+ api.updateScene({
1828
+ appState: { ...appState, croppingElementId: null, selectedElementIds: {} }
1829
+ });
1830
+ if (el.customData.kind === "geometry") {
1831
+ try {
1832
+ const parsed = JSON.parse(el.customData.jsonState);
1833
+ setGeometryEditing({ editingElementId: el.id, initialState: parsed });
1834
+ setActiveStamp("geometry");
1835
+ } catch {
1836
+ console.warn("customData jsonState corrupted; skipping reopen");
1837
+ }
1838
+ } else {
1839
+ const elAny = el;
1840
+ setLatexEditing({
1841
+ editingElementId: el.id,
1842
+ initialValue: el.customData.src,
1843
+ x: elAny.x ?? 0,
1844
+ y: elAny.y ?? 0
1845
+ });
1846
+ setLatexDisplayMode(!!el.customData.displayMode);
1847
+ setActiveStamp("latex");
1848
+ }
1849
+ return;
1850
+ }
1851
+ }
1852
+ if (!cropId) {
1853
+ handledCropIdRef.current = null;
1854
+ }
1855
+ const fileIds = Object.keys(files);
1856
+ const newIds = fileIds.filter((id) => !knownFileIdsRef.current.has(id));
1857
+ if (newIds.length > 0) {
1858
+ newIds.forEach((id) => knownFileIdsRef.current.add(id));
1859
+ onFilesChange(files, newIds);
1860
+ }
1861
+ if (throttleTimerRef.current) return;
1862
+ throttleTimerRef.current = setTimeout(async () => {
1863
+ throttleTimerRef.current = null;
1864
+ const mod = await import('@excalidraw/excalidraw');
1865
+ const hash = mod.hashElementsVersion(elements);
1866
+ if (hash === lastElementsHashRef.current) return;
1867
+ lastElementsHashRef.current = hash;
1868
+ onSceneChange({
1869
+ elements: elements.filter((e) => !e.isDeleted),
1870
+ appState: pickSyncableAppState(appState)
1871
+ });
1872
+ }, SYNC_THROTTLE_MS);
1873
+ },
1874
+ [isTeacher, api, onSceneChange, onFilesChange]
1875
+ );
1876
+ react.useEffect(() => {
1877
+ if (isTeacher || !api || !remoteScene) return;
1878
+ api.updateScene({
1879
+ elements: remoteScene.elements,
1880
+ appState: remoteScene.appState
1881
+ });
1882
+ }, [isTeacher, api, remoteScene]);
1883
+ react.useEffect(() => {
1884
+ if (isTeacher || !api || !remoteFiles) return;
1885
+ const entries = Object.entries(remoteFiles);
1886
+ if (entries.length === 0) return;
1887
+ api.addFiles(
1888
+ entries.map(([id, f]) => ({
1889
+ id,
1890
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1891
+ dataURL: f.dataURL,
1892
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1893
+ mimeType: f.mimeType,
1894
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1895
+ created: f.created ?? Date.now()
1896
+ }))
1897
+ );
1898
+ }, [isTeacher, api, remoteFiles]);
1899
+ react.useEffect(() => {
1900
+ if (!api) return;
1901
+ let cancelled = false;
1902
+ const run = async () => {
1903
+ try {
1904
+ const elements = api.getSceneElements();
1905
+ if (!elements || elements.length === 0) return;
1906
+ if (cancelled) return;
1907
+ await restoreMissingMathStampFiles(api, elements);
1908
+ } catch (err) {
1909
+ console.warn("Math stamp restore pass failed:", err);
1910
+ }
1911
+ };
1912
+ run();
1913
+ const t = setTimeout(run, 400);
1914
+ return () => {
1915
+ cancelled = true;
1916
+ clearTimeout(t);
1917
+ };
1918
+ }, [api, initialScene, remoteScene]);
1919
+ react.useEffect(
1920
+ () => () => {
1921
+ if (throttleTimerRef.current) clearTimeout(throttleTimerRef.current);
1922
+ },
1923
+ []
1924
+ );
1925
+ const buildStampImageElement = react.useCallback(
1926
+ (fileId, width, height, customData, x, y) => {
1927
+ const appState = api?.getAppState() ?? { scrollX: 0, scrollY: 0, width: 800, height: 600, zoom: { value: 1 } };
1928
+ const cx = x ?? appState.scrollX + (appState.width ?? 800) / 2 / (appState.zoom?.value ?? 1) - width / 2;
1929
+ const cy = y ?? appState.scrollY + (appState.height ?? 600) / 2 / (appState.zoom?.value ?? 1) - height / 2;
1930
+ return {
1931
+ type: "image",
1932
+ id: "stamp_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8),
1933
+ x: cx,
1934
+ y: cy,
1935
+ width,
1936
+ height,
1937
+ fileId,
1938
+ customData,
1939
+ angle: 0,
1940
+ strokeColor: "transparent",
1941
+ backgroundColor: "transparent",
1942
+ fillStyle: "solid",
1943
+ strokeWidth: 1,
1944
+ strokeStyle: "solid",
1945
+ roughness: 0,
1946
+ opacity: 100,
1947
+ groupIds: [],
1948
+ roundness: null,
1949
+ seed: Math.floor(Math.random() * 1e9),
1950
+ versionNonce: 0,
1951
+ version: 1,
1952
+ isDeleted: false,
1953
+ boundElements: null,
1954
+ updated: Date.now(),
1955
+ link: null,
1956
+ locked: false,
1957
+ status: "saved",
1958
+ scale: [1, 1]
1959
+ };
1960
+ },
1961
+ [api]
1962
+ );
1963
+ const clearAppStateAfterInsert = () => ({
1964
+ selectedElementIds: {},
1965
+ croppingElementId: null
1966
+ });
1967
+ const handleGeometryInsert = react.useCallback(
1968
+ async (jsonState, svgString) => {
1969
+ if (!api) return;
1970
+ try {
1971
+ const { dataURL, fileId, width, height, mimeType } = await svgToImageElement(svgString);
1972
+ api.addFiles([{ id: fileId, dataURL, mimeType, created: Date.now() }]);
1973
+ const customData = {
1974
+ kind: "geometry",
1975
+ version: 1,
1976
+ jsonState,
1977
+ svgWidth: width,
1978
+ svgHeight: height
1979
+ };
1980
+ const elements = api.getSceneElements();
1981
+ const editingId = geometryEditing.editingElementId;
1982
+ if (editingId) {
1983
+ const updated = elements.map(
1984
+ (e) => e.id === editingId ? { ...e, fileId, customData, width, height } : e
1985
+ );
1986
+ api.updateScene({ elements: updated, appState: clearAppStateAfterInsert() });
1987
+ } else {
1988
+ const newElement = buildStampImageElement(fileId, width, height, customData);
1989
+ api.updateScene({
1990
+ elements: [...elements, newElement],
1991
+ appState: clearAppStateAfterInsert()
1992
+ });
1993
+ }
1994
+ } catch (err) {
1995
+ console.error("Geometry stamp insert failed:", err);
1996
+ }
1997
+ closeStamp();
1998
+ },
1999
+ [api, geometryEditing.editingElementId, buildStampImageElement, closeStamp]
2000
+ );
2001
+ const handleLatexInsert = react.useCallback(
2002
+ async (svgString, src, displayMode) => {
2003
+ if (!api) return;
2004
+ try {
2005
+ const { dataURL, fileId, width, height, mimeType } = await svgToImageElement(svgString);
2006
+ api.addFiles([{ id: fileId, dataURL, mimeType, created: Date.now() }]);
2007
+ const customData = {
2008
+ kind: "latex",
2009
+ version: 1,
2010
+ src,
2011
+ displayMode
2012
+ };
2013
+ const elements = api.getSceneElements();
2014
+ const editingId = latexEditing.editingElementId;
2015
+ if (editingId) {
2016
+ const updated = elements.map(
2017
+ (e) => e.id === editingId ? { ...e, fileId, customData, width, height } : e
2018
+ );
2019
+ api.updateScene({ elements: updated, appState: clearAppStateAfterInsert() });
2020
+ } else {
2021
+ const newElement = buildStampImageElement(
2022
+ fileId,
2023
+ width,
2024
+ height,
2025
+ customData,
2026
+ latexEditing.x || void 0,
2027
+ latexEditing.y || void 0
2028
+ );
2029
+ api.updateScene({
2030
+ elements: [...elements, newElement],
2031
+ appState: clearAppStateAfterInsert()
2032
+ });
2033
+ }
2034
+ } catch (err) {
2035
+ console.error("LaTeX stamp insert failed:", err);
2036
+ }
2037
+ closeStamp();
2038
+ },
2039
+ [api, latexEditing.editingElementId, latexEditing.x, latexEditing.y, buildStampImageElement, closeStamp]
2040
+ );
2041
+ const handlePointerDown = react.useCallback(
2042
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2043
+ (_activeTool, pointerDownState) => {
2044
+ if (!isTeacher) return;
2045
+ const hitElement = pointerDownState?.hit?.element;
2046
+ if (!hitElement || hitElement.type !== "image") return;
2047
+ if (!isMathStamp(hitElement)) return;
2048
+ const now = Date.now();
2049
+ const isDouble = lastClickRef.current.elementId === hitElement.id && now - lastClickRef.current.time < DOUBLE_CLICK_MS;
2050
+ lastClickRef.current = { time: now, elementId: hitElement.id };
2051
+ if (!isDouble) return;
2052
+ if (hitElement.customData.kind === "geometry") {
2053
+ try {
2054
+ const parsed = JSON.parse(hitElement.customData.jsonState);
2055
+ setGeometryEditing({ editingElementId: hitElement.id, initialState: parsed });
2056
+ setActiveStamp("geometry");
2057
+ } catch {
2058
+ console.warn("customData jsonState corrupted; skipping reopen");
2059
+ }
2060
+ } else {
2061
+ setLatexEditing({
2062
+ editingElementId: hitElement.id,
2063
+ initialValue: hitElement.customData.src,
2064
+ x: hitElement.x ?? 0,
2065
+ y: hitElement.y ?? 0
2066
+ });
2067
+ setLatexDisplayMode(!!hitElement.customData.displayMode);
2068
+ setActiveStamp("latex");
2069
+ }
2070
+ },
2071
+ [isTeacher]
2072
+ );
2073
+ useStampShortcuts({
2074
+ enabled: isTeacher,
2075
+ onGeometry: toggleGeometry,
2076
+ onLatex: toggleLatex
2077
+ });
2078
+ react.useEffect(() => {
2079
+ if (!api) return;
2080
+ if (activeStamp) {
2081
+ try {
2082
+ const cur = api.getAppState?.()?.activeTool?.type ?? "selection";
2083
+ if (cur && cur !== "hand") prevExcalidrawToolRef.current = cur;
2084
+ api.setActiveTool?.({ type: "hand" });
2085
+ } catch {
2086
+ }
2087
+ } else {
2088
+ try {
2089
+ api.setActiveTool?.({ type: prevExcalidrawToolRef.current });
2090
+ } catch {
2091
+ }
2092
+ }
2093
+ }, [activeStamp, api]);
2094
+ react.useEffect(() => {
2095
+ if (!activeStamp) return;
2096
+ const ALLOWED_KEYS = /* @__PURE__ */ new Set([
2097
+ "Tab",
2098
+ "ArrowUp",
2099
+ "ArrowDown",
2100
+ "ArrowLeft",
2101
+ "ArrowRight",
2102
+ "Shift",
2103
+ "Control",
2104
+ "Alt",
2105
+ "Meta",
2106
+ "CapsLock",
2107
+ "Home",
2108
+ "End",
2109
+ "PageUp",
2110
+ "PageDown"
2111
+ ]);
2112
+ const isEditable = (el) => {
2113
+ if (!(el instanceof HTMLElement)) return false;
2114
+ if (el.isContentEditable) return true;
2115
+ const tag = el.tagName;
2116
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
2117
+ };
2118
+ const blocker = (e) => {
2119
+ if (isEditable(e.target)) return;
2120
+ if (e.ctrlKey || e.metaKey) return;
2121
+ if (ALLOWED_KEYS.has(e.key)) return;
2122
+ if (e.key === "Escape") return;
2123
+ const k = e.key.toLowerCase();
2124
+ if (k === "g" || k === "l") return;
2125
+ e.preventDefault();
2126
+ e.stopPropagation();
2127
+ };
2128
+ window.addEventListener("keydown", blocker, { capture: true });
2129
+ return () => window.removeEventListener("keydown", blocker, { capture: true });
2130
+ }, [activeStamp]);
2131
+ react.useEffect(() => {
2132
+ if (!activeStamp) return;
2133
+ const onKey = (e) => {
2134
+ if (e.key !== "Escape") return;
2135
+ const ae = document.activeElement;
2136
+ if (ae && (ae.tagName === "TEXTAREA" || ae.isContentEditable)) {
2137
+ return;
2138
+ }
2139
+ e.preventDefault();
2140
+ e.stopPropagation();
2141
+ closeStamp();
2142
+ };
2143
+ window.addEventListener("keydown", onKey, { capture: true });
2144
+ return () => window.removeEventListener("keydown", onKey, { capture: true });
2145
+ }, [activeStamp, closeStamp]);
2146
+ react.useEffect(() => {
2147
+ if (!activeStamp) return;
2148
+ let lastFireTime = 0;
2149
+ const handler = (e) => {
2150
+ const target = e.target;
2151
+ if (!target) return;
2152
+ if (target.closest('[data-stamp-area="true"]')) return;
2153
+ const now = Date.now();
2154
+ if (now - lastFireTime < 50) return;
2155
+ lastFireTime = now;
2156
+ const stampType = activeStampRef.current;
2157
+ if (stampType === "geometry") {
2158
+ geomPanelRef.current?.insert();
2159
+ } else if (stampType === "latex") {
2160
+ latexInsertableRef.current.insert();
2161
+ }
2162
+ closeStamp();
2163
+ };
2164
+ window.addEventListener("pointerdown", handler, { capture: true });
2165
+ window.addEventListener("mousedown", handler, { capture: true });
2166
+ return () => {
2167
+ window.removeEventListener("pointerdown", handler, { capture: true });
2168
+ window.removeEventListener("mousedown", handler, { capture: true });
2169
+ };
2170
+ }, [activeStamp, closeStamp]);
2171
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative h-full w-full${isDarkTheme ? " theme--dark" : ""}`, children: [
2172
+ /* @__PURE__ */ jsxRuntime.jsx(
2173
+ Excalidraw2,
2174
+ {
2175
+ excalidrawAPI: (a) => setApi(a),
2176
+ langCode,
2177
+ viewModeEnabled: !isTeacher,
2178
+ initialData: initialScene ? {
2179
+ elements: initialScene.elements,
2180
+ appState: {
2181
+ ...initialScene.appState,
2182
+ gridSize: initialScene.appState.gridSize ?? void 0
2183
+ }
2184
+ } : { appState: { viewBackgroundColor: "#ffffff" } },
2185
+ onChange: handleChange,
2186
+ onPointerDown: handlePointerDown
2187
+ }
2188
+ ),
2189
+ /* @__PURE__ */ jsxRuntime.jsx(
2190
+ ToolbarStampInjector,
2191
+ {
2192
+ enabled: isTeacher,
2193
+ activeStamp,
2194
+ onToggleGeometry: toggleGeometry,
2195
+ onToggleLatex: toggleLatex
2196
+ }
2197
+ ),
2198
+ activeStamp === "geometry" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2199
+ /* @__PURE__ */ jsxRuntime.jsx(
2200
+ GeometryLeftPanel,
2201
+ {
2202
+ activeTool: geomState.tool,
2203
+ onToolChange: (t) => geomPanelRef.current?.setTool(t),
2204
+ showAxis: geomState.showAxis,
2205
+ showGrid: geomState.showGrid,
2206
+ onShowAxisChange: (b) => geomPanelRef.current?.setShowAxis(b),
2207
+ onShowGridChange: (b) => geomPanelRef.current?.setShowGrid(b),
2208
+ onUndo: () => geomPanelRef.current?.undo(),
2209
+ canUndo: geomState.canUndo,
2210
+ onClose: closeStamp
2211
+ }
2212
+ ),
2213
+ /* @__PURE__ */ jsxRuntime.jsx(
2214
+ GeometryEditorPanel,
2215
+ {
2216
+ ref: geomPanelRef,
2217
+ initialState: geometryEditing.initialState,
2218
+ onInsert: handleGeometryInsert,
2219
+ onClose: closeStamp,
2220
+ onStateChange: setGeomState,
2221
+ withLeftPanel: true
2222
+ }
2223
+ )
2224
+ ] }),
2225
+ activeStamp === "latex" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2226
+ /* @__PURE__ */ jsxRuntime.jsx(
2227
+ LatexLeftPanel,
2228
+ {
2229
+ displayMode: latexDisplayMode,
2230
+ onDisplayModeChange: setLatexDisplayMode,
2231
+ onInsertSnippet: (s) => latexEditorRef.current?.insertAtCursor(s),
2232
+ onClose: closeStamp
2233
+ }
2234
+ ),
2235
+ /* @__PURE__ */ jsxRuntime.jsx(
2236
+ LatexEditorPopover,
2237
+ {
2238
+ ref: (node) => {
2239
+ latexEditorRef.current = node;
2240
+ if (node) {
2241
+ latexInsertableRef.current = {
2242
+ insert: () => node.tryInsert(),
2243
+ hasContent: () => node.hasContent()
2244
+ };
2245
+ }
2246
+ },
2247
+ x: 0,
2248
+ y: 0,
2249
+ initialValue: latexEditing.initialValue,
2250
+ displayMode: latexDisplayMode,
2251
+ onDisplayModeChange: setLatexDisplayMode,
2252
+ onInsert: handleLatexInsert,
2253
+ onClose: closeStamp,
2254
+ withLeftPanel: true
2255
+ }
2256
+ )
2257
+ ] })
2258
+ ] });
2259
+ }
2260
+
2261
+ exports.ExcalidrawWhiteboardView = ExcalidrawWhiteboardView;
2262
+ exports.pickSyncableAppState = pickSyncableAppState;
2263
+ //# sourceMappingURL=index.js.map
2264
+ //# sourceMappingURL=index.js.map