@xom11/whiteboard 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +36 -0
  2. package/dist/chunk-3SSQKRRO.mjs +58 -0
  3. package/dist/chunk-3SSQKRRO.mjs.map +1 -0
  4. package/dist/chunk-7P7SQFOW.mjs +39 -0
  5. package/dist/chunk-7P7SQFOW.mjs.map +1 -0
  6. package/dist/chunk-BJX4YNA5.mjs +137 -0
  7. package/dist/chunk-BJX4YNA5.mjs.map +1 -0
  8. package/dist/chunk-C6SCVOMC.mjs +111 -0
  9. package/dist/chunk-C6SCVOMC.mjs.map +1 -0
  10. package/dist/chunk-DJTBZEAR.mjs +25 -0
  11. package/dist/chunk-DJTBZEAR.mjs.map +1 -0
  12. package/dist/chunk-HM7RIXJE.mjs +331 -0
  13. package/dist/chunk-HM7RIXJE.mjs.map +1 -0
  14. package/dist/chunk-HTBLO5JO.mjs +41 -0
  15. package/dist/chunk-HTBLO5JO.mjs.map +1 -0
  16. package/dist/chunk-HYXFHEDJ.mjs +129 -0
  17. package/dist/chunk-HYXFHEDJ.mjs.map +1 -0
  18. package/dist/chunk-LPM4MM45.mjs +211 -0
  19. package/dist/chunk-LPM4MM45.mjs.map +1 -0
  20. package/dist/chunk-P2AOIF7S.mjs +40 -0
  21. package/dist/chunk-P2AOIF7S.mjs.map +1 -0
  22. package/dist/chunk-SHFOGORM.mjs +44 -0
  23. package/dist/chunk-SHFOGORM.mjs.map +1 -0
  24. package/dist/chunk-X5R72SSJ.mjs +52 -0
  25. package/dist/chunk-X5R72SSJ.mjs.map +1 -0
  26. package/dist/geometry-2d.d.mts +16 -0
  27. package/dist/geometry-2d.d.ts +16 -0
  28. package/dist/geometry-2d.js +3549 -0
  29. package/dist/geometry-2d.js.map +1 -0
  30. package/dist/geometry-2d.mjs +7 -0
  31. package/dist/geometry-2d.mjs.map +1 -0
  32. package/dist/geometry-3d.d.mts +16 -0
  33. package/dist/geometry-3d.d.ts +16 -0
  34. package/dist/geometry-3d.js +2030 -0
  35. package/dist/geometry-3d.js.map +1 -0
  36. package/dist/geometry-3d.mjs +6 -0
  37. package/dist/geometry-3d.mjs.map +1 -0
  38. package/dist/graph-2d.d.mts +16 -0
  39. package/dist/graph-2d.d.ts +16 -0
  40. package/dist/graph-2d.js +1725 -0
  41. package/dist/graph-2d.js.map +1 -0
  42. package/dist/graph-2d.mjs +6 -0
  43. package/dist/graph-2d.mjs.map +1 -0
  44. package/dist/host-2QGKMGCT.mjs +1066 -0
  45. package/dist/host-2QGKMGCT.mjs.map +1 -0
  46. package/dist/host-T2W6R6SO.mjs +2859 -0
  47. package/dist/host-T2W6R6SO.mjs.map +1 -0
  48. package/dist/host-XUFON6CQ.mjs +1422 -0
  49. package/dist/host-XUFON6CQ.mjs.map +1 -0
  50. package/dist/host-Z3TEJKZA.mjs +466 -0
  51. package/dist/host-Z3TEJKZA.mjs.map +1 -0
  52. package/dist/index.d.mts +27 -146
  53. package/dist/index.d.ts +27 -146
  54. package/dist/index.js +4694 -4482
  55. package/dist/index.js.map +1 -1
  56. package/dist/index.mjs +136 -7179
  57. package/dist/index.mjs.map +1 -1
  58. package/dist/latex.d.mts +15 -0
  59. package/dist/latex.d.ts +15 -0
  60. package/dist/latex.js +750 -0
  61. package/dist/latex.js.map +1 -0
  62. package/dist/latex.mjs +6 -0
  63. package/dist/latex.mjs.map +1 -0
  64. package/dist/types-CinstD7T.d.mts +110 -0
  65. package/dist/types-CinstD7T.d.ts +110 -0
  66. package/package.json +21 -2
package/dist/latex.js ADDED
@@ -0,0 +1,750 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var react = require('react');
6
+
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+
17
+ // src/stamps/latex/render.ts
18
+ function absoluteOrigin() {
19
+ if (typeof window !== "undefined" && window.location) return window.location.origin;
20
+ return "";
21
+ }
22
+ async function loadKatexCss() {
23
+ if (cachedCss !== null) return cachedCss;
24
+ try {
25
+ if (typeof fetch === "function") {
26
+ const res = await fetch("/katex.min.css");
27
+ if (res.ok) {
28
+ let css = await res.text();
29
+ const origin = absoluteOrigin();
30
+ if (origin) {
31
+ css = css.replace(/url\((['"]?)(fonts\/)/g, `url($1${origin}/$2`);
32
+ }
33
+ cachedCss = css;
34
+ return css;
35
+ }
36
+ }
37
+ } catch {
38
+ }
39
+ cachedCss = "";
40
+ return "";
41
+ }
42
+ async function renderLatexToSvg(src, displayMode) {
43
+ const katex = await import('katex');
44
+ const html = katex.default.renderToString(src, { displayMode, throwOnError: true, output: "html" });
45
+ const measureDiv = document.createElement("div");
46
+ measureDiv.style.cssText = "position:absolute;top:-9999px;left:-9999px;visibility:hidden;display:inline-block;";
47
+ measureDiv.innerHTML = html;
48
+ document.body.appendChild(measureDiv);
49
+ const rect = measureDiv.getBoundingClientRect();
50
+ const width = Math.ceil(rect.width) || 50;
51
+ const height = Math.ceil(rect.height) || 20;
52
+ document.body.removeChild(measureDiv);
53
+ const cssText = await loadKatexCss();
54
+ 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>";
55
+ }
56
+ var cachedCss;
57
+ var init_render = __esm({
58
+ "src/stamps/latex/render.ts"() {
59
+ cachedCss = null;
60
+ }
61
+ });
62
+
63
+ // src/stamps/latex/types.ts
64
+ function isLatexCustomData(data) {
65
+ if (!data || typeof data !== "object") return false;
66
+ const d = data;
67
+ return d.kind === "latex" && d.version === 1 && typeof d.src === "string";
68
+ }
69
+ var init_types = __esm({
70
+ "src/stamps/latex/types.ts"() {
71
+ }
72
+ });
73
+ function Shell({ title, icon, onClose, children, isMobile, drawerOpen, onDrawerClose }) {
74
+ const mobileAttrs = isMobile ? {
75
+ "data-mobile-drawer": "true",
76
+ "data-drawer-state": drawerOpen ? "open" : "closed"
77
+ } : {};
78
+ const handleHeaderClose = () => {
79
+ if (isMobile) onDrawerClose?.();
80
+ else onClose();
81
+ };
82
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
83
+ isMobile && drawerOpen && /* @__PURE__ */ jsxRuntime.jsx(
84
+ "div",
85
+ {
86
+ className: "stamp-drawer-backdrop",
87
+ onPointerDown: onDrawerClose,
88
+ "aria-hidden": "true"
89
+ }
90
+ ),
91
+ /* @__PURE__ */ jsxRuntime.jsxs(
92
+ "aside",
93
+ {
94
+ role: "complementary",
95
+ "aria-label": title,
96
+ "aria-hidden": isMobile && !drawerOpen ? "true" : void 0,
97
+ "data-testid": "stamp-left-panel",
98
+ "data-stamp-area": "true",
99
+ ...mobileAttrs,
100
+ className: isMobile ? "stamp-drawer-mobile flex flex-col border-r border-slate-200 bg-white shadow-md" : "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",
101
+ children: [
102
+ /* @__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: [
103
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
104
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base leading-none", children: icon }),
105
+ title
106
+ ] }),
107
+ /* @__PURE__ */ jsxRuntime.jsx(
108
+ "button",
109
+ {
110
+ onClick: handleHeaderClose,
111
+ "aria-label": isMobile ? "\u0110\xF3ng ng\u0103n c\xF4ng c\u1EE5" : "\u0110\xF3ng",
112
+ className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
113
+ 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: [
114
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
115
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
116
+ ] })
117
+ }
118
+ )
119
+ ] }),
120
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-4", children })
121
+ ]
122
+ }
123
+ )
124
+ ] });
125
+ }
126
+ function Section({ label, children }) {
127
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
128
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: label }),
129
+ children
130
+ ] });
131
+ }
132
+ function LeftPanel({
133
+ displayMode,
134
+ onDisplayModeChange,
135
+ onInsertSnippet,
136
+ onClose,
137
+ isMobile,
138
+ drawerOpen,
139
+ onDrawerClose
140
+ }) {
141
+ return /* @__PURE__ */ jsxRuntime.jsxs(
142
+ Shell,
143
+ {
144
+ title: "C\xF4ng th\u1EE9c LaTeX",
145
+ icon: "\u2211",
146
+ onClose,
147
+ isMobile,
148
+ drawerOpen,
149
+ onDrawerClose,
150
+ children: [
151
+ /* @__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: [
152
+ /* @__PURE__ */ jsxRuntime.jsxs(
153
+ "button",
154
+ {
155
+ type: "button",
156
+ onClick: () => onDisplayModeChange(false),
157
+ "aria-pressed": !displayMode,
158
+ className: [
159
+ "rounded-md border px-2 py-1.5 text-xs transition",
160
+ !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"
161
+ ].join(" "),
162
+ children: [
163
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block font-medium", children: "Inline" }),
164
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-[10px] text-slate-500", children: "$ ... $" })
165
+ ]
166
+ }
167
+ ),
168
+ /* @__PURE__ */ jsxRuntime.jsxs(
169
+ "button",
170
+ {
171
+ type: "button",
172
+ onClick: () => onDisplayModeChange(true),
173
+ "aria-pressed": displayMode,
174
+ className: [
175
+ "rounded-md border px-2 py-1.5 text-xs transition",
176
+ 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"
177
+ ].join(" "),
178
+ children: [
179
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block font-medium", children: "Block" }),
180
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-[10px] text-slate-500", children: "$$ ... $$" })
181
+ ]
182
+ }
183
+ )
184
+ ] }) }),
185
+ 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(
186
+ "button",
187
+ {
188
+ type: "button",
189
+ "data-snippet": s.snippet,
190
+ onClick: () => onInsertSnippet(s.snippet),
191
+ title: s.snippet,
192
+ 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",
193
+ children: s.preview
194
+ },
195
+ s.snippet
196
+ )) }) }, group.group)),
197
+ /* @__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: [
198
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1", children: [
199
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 font-mono", children: "Enter" }),
200
+ "ch\xE8n"
201
+ ] }),
202
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1", children: [
203
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 font-mono", children: "Esc" }),
204
+ "\u0111\xF3ng"
205
+ ] })
206
+ ] }) })
207
+ ]
208
+ }
209
+ );
210
+ }
211
+ var SNIPPETS;
212
+ var init_LeftPanel = __esm({
213
+ "src/stamps/latex/editor/LeftPanel.tsx"() {
214
+ "use client";
215
+ SNIPPETS = [
216
+ {
217
+ group: "Ph\xE2n s\u1ED1 & lu\u1EF9 th\u1EEBa",
218
+ items: [
219
+ { label: "Ph\xE2n s\u1ED1", preview: "a\u2044b", snippet: "\\frac{a}{b}" },
220
+ { label: "Lu\u1EF9 th\u1EEBa", preview: "x\xB2", snippet: "^{2}" },
221
+ { label: "Ch\u1EC9 s\u1ED1", preview: "x\u2081", snippet: "_{1}" },
222
+ { label: "C\u0103n", preview: "\u221Ax", snippet: "\\sqrt{x}" },
223
+ { label: "C\u0103n n", preview: "\u207F\u221Ax", snippet: "\\sqrt[n]{x}" }
224
+ ]
225
+ },
226
+ {
227
+ group: "T\u1ED5ng & t\xEDch ph\xE2n",
228
+ items: [
229
+ { label: "T\u1ED5ng", preview: "\u03A3", snippet: "\\sum_{i=1}^{n}" },
230
+ { label: "T\xEDch", preview: "\u03A0", snippet: "\\prod_{i=1}^{n}" },
231
+ { label: "T\xEDch ph\xE2n", preview: "\u222B", snippet: "\\int_{a}^{b}" },
232
+ { label: "Gi\u1EDBi h\u1EA1n", preview: "lim", snippet: "\\lim_{x \\to 0}" }
233
+ ]
234
+ },
235
+ {
236
+ group: "K\xFD hi\u1EC7u",
237
+ items: [
238
+ { label: "\u03B1", preview: "\u03B1", snippet: "\\alpha" },
239
+ { label: "\u03B2", preview: "\u03B2", snippet: "\\beta" },
240
+ { label: "\u03C0", preview: "\u03C0", snippet: "\\pi" },
241
+ { label: "\u03B8", preview: "\u03B8", snippet: "\\theta" },
242
+ { label: "\u2260", preview: "\u2260", snippet: "\\neq" },
243
+ { label: "\u2264", preview: "\u2264", snippet: "\\leq" },
244
+ { label: "\u2265", preview: "\u2265", snippet: "\\geq" },
245
+ { label: "\u221E", preview: "\u221E", snippet: "\\infty" },
246
+ { label: "\u2192", preview: "\u2192", snippet: "\\to" }
247
+ ]
248
+ }
249
+ ];
250
+ }
251
+ });
252
+ var DEBOUNCE_MS, EditorPopover;
253
+ var init_EditorPopover = __esm({
254
+ "src/stamps/latex/editor/EditorPopover.tsx"() {
255
+ "use client";
256
+ init_render();
257
+ DEBOUNCE_MS = 100;
258
+ EditorPopover = react.forwardRef(function EditorPopover2({
259
+ x,
260
+ y,
261
+ initialValue,
262
+ onInsert,
263
+ onClose,
264
+ displayMode: controlledDisplayMode,
265
+ onDisplayModeChange,
266
+ withLeftPanel = false,
267
+ isMobile = false,
268
+ onOpenDrawer
269
+ }, ref) {
270
+ const [value, setValue] = react.useState(initialValue);
271
+ const [internalDisplayMode] = react.useState(false);
272
+ const displayMode = controlledDisplayMode ?? internalDisplayMode;
273
+ const [previewSvg, setPreviewSvg] = react.useState(null);
274
+ const [error, setError] = react.useState(null);
275
+ const debounceRef = react.useRef(null);
276
+ const inputRef = react.useRef(null);
277
+ react.useEffect(() => {
278
+ if (debounceRef.current) clearTimeout(debounceRef.current);
279
+ debounceRef.current = setTimeout(async () => {
280
+ try {
281
+ const svg = await renderLatexToSvg(value, displayMode);
282
+ setPreviewSvg(svg);
283
+ setError(null);
284
+ } catch (err) {
285
+ setPreviewSvg(null);
286
+ setError(err.message);
287
+ }
288
+ }, DEBOUNCE_MS);
289
+ return () => {
290
+ if (debounceRef.current) clearTimeout(debounceRef.current);
291
+ };
292
+ }, [value, displayMode]);
293
+ const handleInsert = react.useCallback(() => {
294
+ if (!previewSvg) return;
295
+ onInsert(previewSvg, value, displayMode);
296
+ }, [previewSvg, value, displayMode, onInsert]);
297
+ const handleKeyDown = react.useCallback(
298
+ (e) => {
299
+ if (e.key === "Escape") onClose();
300
+ if (e.key === "Enter" && !e.shiftKey) {
301
+ e.preventDefault();
302
+ handleInsert();
303
+ }
304
+ },
305
+ [onClose, handleInsert]
306
+ );
307
+ react.useImperativeHandle(
308
+ ref,
309
+ () => ({
310
+ insertAtCursor: (snippet) => {
311
+ const el = inputRef.current;
312
+ if (!el) {
313
+ setValue((v) => v + snippet);
314
+ return;
315
+ }
316
+ const start = el.selectionStart ?? value.length;
317
+ const end = el.selectionEnd ?? value.length;
318
+ const next = value.slice(0, start) + snippet + value.slice(end);
319
+ setValue(next);
320
+ requestAnimationFrame(() => {
321
+ el.focus();
322
+ const pos = start + snippet.length;
323
+ try {
324
+ el.setSelectionRange(pos, pos);
325
+ } catch {
326
+ }
327
+ });
328
+ },
329
+ hasContent: () => value.trim().length > 0 && !!previewSvg && !error,
330
+ tryInsert: () => {
331
+ if (!previewSvg || error || !value.trim()) return false;
332
+ onInsert(previewSvg, value, displayMode);
333
+ return true;
334
+ }
335
+ }),
336
+ [value, previewSvg, error, displayMode, onInsert]
337
+ );
338
+ const isLegacyPosition = x > 0 || y > 0;
339
+ const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 50 } : isLegacyPosition ? { position: "absolute", top: y, left: x, zIndex: 50 } : {
340
+ position: "absolute",
341
+ top: "50%",
342
+ left: withLeftPanel ? "calc(50% + 120px)" : "50%",
343
+ transform: "translate(-50%, -50%)",
344
+ zIndex: 50
345
+ };
346
+ return /* @__PURE__ */ jsxRuntime.jsxs(
347
+ "div",
348
+ {
349
+ style: wrapperStyle,
350
+ "data-stamp-area": "true",
351
+ "data-mobile-editor": isMobile ? "true" : void 0,
352
+ className: isMobile ? "flex h-full w-full flex-col bg-white" : "w-[420px] max-w-[calc(100vw-280px)] rounded-lg border border-slate-300 bg-white shadow-2xl ring-1 ring-black/5",
353
+ role: "dialog",
354
+ "aria-label": "Nh\u1EADp c\xF4ng th\u1EE9c LaTeX",
355
+ children: [
356
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: `flex items-center gap-2 border-b border-slate-200 bg-gradient-to-r from-indigo-600 to-purple-600 px-3 py-2 text-white${isMobile ? "" : " rounded-t-lg"}`, children: [
357
+ isMobile && /* @__PURE__ */ jsxRuntime.jsx(
358
+ "button",
359
+ {
360
+ type: "button",
361
+ onClick: onOpenDrawer,
362
+ "aria-label": "M\u1EDF ng\u0103n snippet",
363
+ className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
364
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
365
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
366
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
367
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
368
+ ] })
369
+ }
370
+ ),
371
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
372
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base leading-none", children: "\u2211" }),
373
+ "C\xF4ng th\u1EE9c LaTeX"
374
+ ] }),
375
+ isMobile && /* @__PURE__ */ jsxRuntime.jsx(
376
+ "button",
377
+ {
378
+ type: "button",
379
+ onClick: handleInsert,
380
+ disabled: !previewSvg || !!error,
381
+ "data-testid": "latex-insert-btn-mobile",
382
+ className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
383
+ children: "Ch\xE8n"
384
+ }
385
+ ),
386
+ /* @__PURE__ */ jsxRuntime.jsx(
387
+ "button",
388
+ {
389
+ onClick: onClose,
390
+ "aria-label": "\u0110\xF3ng",
391
+ className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15",
392
+ 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: [
393
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
394
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
395
+ ] })
396
+ }
397
+ )
398
+ ] }),
399
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `space-y-2 p-3${isMobile ? " flex min-h-0 flex-1 flex-col" : ""}`, children: [
400
+ /* @__PURE__ */ jsxRuntime.jsx(
401
+ "input",
402
+ {
403
+ ref: inputRef,
404
+ type: "text",
405
+ role: "textbox",
406
+ value,
407
+ onChange: (e) => setValue(e.target.value),
408
+ onKeyDown: handleKeyDown,
409
+ placeholder: "Vd: \\frac{a^2+b^2}{c}",
410
+ className: `w-full rounded border border-slate-300 px-2 py-1.5 font-mono outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-200${isMobile ? " min-h-[44px] text-base" : " text-sm"}`,
411
+ autoFocus: true
412
+ }
413
+ ),
414
+ /* @__PURE__ */ jsxRuntime.jsx(
415
+ "div",
416
+ {
417
+ className: [
418
+ "flex items-center justify-center rounded border p-3 text-center",
419
+ isMobile ? "min-h-0 flex-1 overflow-auto" : "min-h-[64px]",
420
+ error ? "border-rose-300 bg-rose-50 text-rose-700" : "border-slate-200 bg-slate-50"
421
+ ].join(" "),
422
+ children: error ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs", children: [
423
+ "L\u1ED7i: ",
424
+ error.slice(0, 80)
425
+ ] }) : previewSvg ? /* @__PURE__ */ jsxRuntime.jsx("span", { dangerouslySetInnerHTML: { __html: previewSvg } }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-slate-400", children: "(xem tr\u01B0\u1EDBc)" })
426
+ }
427
+ ),
428
+ !isMobile && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
429
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-[11px] text-slate-500", children: [
430
+ displayMode ? "Block" : "Inline",
431
+ " \xB7 Enter \u0111\u1EC3 ch\xE8n"
432
+ ] }),
433
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
434
+ /* @__PURE__ */ jsxRuntime.jsx(
435
+ "button",
436
+ {
437
+ onClick: onClose,
438
+ className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
439
+ children: "Hu\u1EF7"
440
+ }
441
+ ),
442
+ /* @__PURE__ */ jsxRuntime.jsx(
443
+ "button",
444
+ {
445
+ onClick: handleInsert,
446
+ disabled: !previewSvg || !!error,
447
+ className: "rounded bg-indigo-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-indigo-700 disabled:opacity-50",
448
+ children: "Ch\xE8n"
449
+ }
450
+ )
451
+ ] })
452
+ ] }),
453
+ isMobile && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center text-[11px] text-slate-500", children: [
454
+ displayMode ? "Block" : "Inline",
455
+ " \xB7 B\u1EA5m Ch\xE8n \u1EDF thanh tr\xEAn"
456
+ ] })
457
+ ] })
458
+ ]
459
+ }
460
+ );
461
+ });
462
+ }
463
+ });
464
+
465
+ // src/stamps/shared/svgToImage.ts
466
+ async function hashString(input) {
467
+ if (typeof crypto !== "undefined" && crypto.subtle) {
468
+ const buf = new TextEncoder().encode(input);
469
+ const digest = await crypto.subtle.digest("SHA-256", buf);
470
+ return Array.from(new Uint8Array(digest)).slice(0, 16).map((b) => b.toString(16).padStart(2, "0")).join("");
471
+ }
472
+ let h1 = 2166136261;
473
+ let h2 = 3421674724;
474
+ for (let i = 0; i < input.length; i++) {
475
+ const c = input.charCodeAt(i);
476
+ h1 ^= c;
477
+ h1 = Math.imul(h1, 16777619);
478
+ h2 ^= c + i;
479
+ h2 = Math.imul(h2, 1099511628211 & 4294967295);
480
+ }
481
+ return (h1 >>> 0).toString(16).padStart(8, "0") + (h2 >>> 0).toString(16).padStart(8, "0");
482
+ }
483
+ function parseSize(svg, attr) {
484
+ const re = new RegExp(`<svg[^>]*\\s${attr}="(\\d+(?:\\.\\d+)?)`, "i");
485
+ const m = svg.match(re);
486
+ if (m) return Math.max(1, Math.round(parseFloat(m[1])));
487
+ const vb = svg.match(/viewBox="([\d.\s-]+)"/i);
488
+ if (vb) {
489
+ const parts = vb[1].trim().split(/\s+/).map(parseFloat);
490
+ if (parts.length === 4) return Math.max(1, Math.round(attr === "width" ? parts[2] : parts[3]));
491
+ }
492
+ return attr === "width" ? 200 : 100;
493
+ }
494
+ async function svgToImageElement(svg) {
495
+ const width = parseSize(svg, "width");
496
+ const height = parseSize(svg, "height");
497
+ const utf8 = unescape(encodeURIComponent(svg));
498
+ const dataURL = "data:image/svg+xml;base64," + btoa(utf8);
499
+ const fileId = await hashString(dataURL);
500
+ return { dataURL, fileId, width, height, mimeType: "image/svg+xml" };
501
+ }
502
+ var init_svgToImage = __esm({
503
+ "src/stamps/shared/svgToImage.ts"() {
504
+ }
505
+ });
506
+
507
+ // src/stamps/shared/insertImage.ts
508
+ function buildStampImageElement(api, fileId, width, height, customData, x, y) {
509
+ const appState = api?.getAppState() ?? { scrollX: 0, scrollY: 0, width: 800, height: 600, zoom: { value: 1 } };
510
+ const cx = x ?? appState.scrollX + (appState.width ?? 800) / 2 / (appState.zoom?.value ?? 1) - width / 2;
511
+ const cy = y ?? appState.scrollY + (appState.height ?? 600) / 2 / (appState.zoom?.value ?? 1) - height / 2;
512
+ return {
513
+ type: "image",
514
+ id: "stamp_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8),
515
+ x: cx,
516
+ y: cy,
517
+ width,
518
+ height,
519
+ fileId,
520
+ customData,
521
+ angle: 0,
522
+ strokeColor: "transparent",
523
+ backgroundColor: "transparent",
524
+ fillStyle: "solid",
525
+ strokeWidth: 1,
526
+ strokeStyle: "solid",
527
+ roughness: 0,
528
+ opacity: 100,
529
+ groupIds: [],
530
+ roundness: null,
531
+ seed: Math.floor(Math.random() * 1e9),
532
+ versionNonce: 0,
533
+ version: 1,
534
+ isDeleted: false,
535
+ boundElements: null,
536
+ updated: Date.now(),
537
+ link: null,
538
+ locked: false,
539
+ status: "saved",
540
+ scale: [1, 1]
541
+ };
542
+ }
543
+ async function insertStampImage(api, opts) {
544
+ const { dataURL, fileId, width, height, mimeType } = await svgToImageElement(opts.svgString);
545
+ api.addFiles([{ id: fileId, dataURL, mimeType, created: Date.now() }]);
546
+ const customData = opts.makeCustomData(width, height);
547
+ const elements = api.getSceneElements();
548
+ const editingId = opts.editingElementId ?? null;
549
+ if (editingId) {
550
+ const updated = elements.map(
551
+ (e) => e.id === editingId ? { ...e, fileId, customData, width, height } : e
552
+ );
553
+ api.updateScene({ elements: updated, appState: clearAppStateAfterInsert() });
554
+ return { fileId, width, height, elementId: editingId };
555
+ }
556
+ const newElement = buildStampImageElement(
557
+ api,
558
+ fileId,
559
+ width,
560
+ height,
561
+ customData,
562
+ opts.position?.x,
563
+ opts.position?.y
564
+ );
565
+ api.updateScene({
566
+ elements: [...elements, newElement],
567
+ appState: clearAppStateAfterInsert()
568
+ });
569
+ return { fileId, width, height, elementId: newElement.id };
570
+ }
571
+ var clearAppStateAfterInsert;
572
+ var init_insertImage = __esm({
573
+ "src/stamps/shared/insertImage.ts"() {
574
+ init_svgToImage();
575
+ clearAppStateAfterInsert = () => ({
576
+ selectedElementIds: {},
577
+ croppingElementId: null
578
+ });
579
+ }
580
+ });
581
+ function readMatch(query) {
582
+ if (typeof window === "undefined" || !window.matchMedia) return false;
583
+ try {
584
+ return window.matchMedia(query).matches;
585
+ } catch {
586
+ return false;
587
+ }
588
+ }
589
+ function useIsMobile() {
590
+ const [state, setState] = react.useState(() => ({
591
+ isMobile: readMatch(MOBILE_QUERY),
592
+ isTouchOnly: readMatch(NO_HOVER_QUERY)
593
+ }));
594
+ react.useEffect(() => {
595
+ if (typeof window === "undefined" || !window.matchMedia) return;
596
+ const mql = window.matchMedia(MOBILE_QUERY);
597
+ const tql = window.matchMedia(NO_HOVER_QUERY);
598
+ const update = () => {
599
+ setState({ isMobile: mql.matches, isTouchOnly: tql.matches });
600
+ };
601
+ update();
602
+ mql.addEventListener("change", update);
603
+ tql.addEventListener("change", update);
604
+ return () => {
605
+ mql.removeEventListener("change", update);
606
+ tql.removeEventListener("change", update);
607
+ };
608
+ }, []);
609
+ return state;
610
+ }
611
+ var MOBILE_QUERY, NO_HOVER_QUERY;
612
+ var init_useIsMobile = __esm({
613
+ "src/stamps/shared/useIsMobile.ts"() {
614
+ "use client";
615
+ MOBILE_QUERY = "(max-width: 768px)";
616
+ NO_HOVER_QUERY = "(hover: none)";
617
+ }
618
+ });
619
+
620
+ // src/stamps/latex/host.tsx
621
+ var host_exports = {};
622
+ __export(host_exports, {
623
+ LatexStampHost: () => LatexStampHost
624
+ });
625
+ var LatexStampHost;
626
+ var init_host = __esm({
627
+ "src/stamps/latex/host.tsx"() {
628
+ "use client";
629
+ init_LeftPanel();
630
+ init_EditorPopover();
631
+ init_insertImage();
632
+ init_useIsMobile();
633
+ init_types();
634
+ LatexStampHost = react.forwardRef(
635
+ function LatexStampHost2({ api, editingElement, onClose }, ref) {
636
+ const editorRef = react.useRef(null);
637
+ const { isMobile } = useIsMobile();
638
+ const [drawerOpen, setDrawerOpen] = react.useState(false);
639
+ const initial = react.useMemo(() => {
640
+ if (editingElement && isLatexCustomData(editingElement.customData)) {
641
+ return {
642
+ initialValue: editingElement.customData.src,
643
+ displayMode: !!editingElement.customData.displayMode
644
+ };
645
+ }
646
+ return { initialValue: "", displayMode: false };
647
+ }, [editingElement]);
648
+ const [displayMode, setDisplayMode] = react.useState(initial.displayMode);
649
+ const handleInsert = react.useCallback(
650
+ async (svgString, src, dm) => {
651
+ if (!api) return;
652
+ try {
653
+ await insertStampImage(api, {
654
+ svgString,
655
+ makeCustomData: () => ({
656
+ kind: "latex",
657
+ version: 1,
658
+ src,
659
+ displayMode: dm
660
+ }),
661
+ editingElementId: editingElement?.id ?? null
662
+ });
663
+ } catch (err) {
664
+ console.error("Latex insert failed:", err);
665
+ }
666
+ onClose();
667
+ },
668
+ [api, editingElement?.id, onClose]
669
+ );
670
+ react.useImperativeHandle(
671
+ ref,
672
+ () => ({
673
+ tryInsert: () => editorRef.current?.tryInsert() ?? false,
674
+ hasContent: () => editorRef.current?.hasContent() ?? false
675
+ }),
676
+ []
677
+ );
678
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
679
+ /* @__PURE__ */ jsxRuntime.jsx(
680
+ LeftPanel,
681
+ {
682
+ displayMode,
683
+ onDisplayModeChange: setDisplayMode,
684
+ onInsertSnippet: (s) => editorRef.current?.insertAtCursor(s),
685
+ onClose,
686
+ isMobile,
687
+ drawerOpen,
688
+ onDrawerClose: () => setDrawerOpen(false)
689
+ }
690
+ ),
691
+ /* @__PURE__ */ jsxRuntime.jsx(
692
+ EditorPopover,
693
+ {
694
+ ref: editorRef,
695
+ x: 0,
696
+ y: 0,
697
+ initialValue: initial.initialValue,
698
+ displayMode,
699
+ onDisplayModeChange: setDisplayMode,
700
+ onInsert: handleInsert,
701
+ onClose,
702
+ withLeftPanel: !isMobile,
703
+ isMobile,
704
+ onOpenDrawer: () => setDrawerOpen(true)
705
+ }
706
+ )
707
+ ] });
708
+ }
709
+ );
710
+ }
711
+ });
712
+
713
+ // src/stamps/latex/index.tsx
714
+ init_render();
715
+ init_types();
716
+ var LatexStampHost3 = react.lazy(
717
+ () => Promise.resolve().then(() => (init_host(), host_exports)).then((m) => ({ default: m.LatexStampHost }))
718
+ );
719
+ 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" }) });
720
+ var latexStamp = {
721
+ kind: "latex",
722
+ shortcutKey: "l",
723
+ toolbarLabel: "L",
724
+ toolbarTitle: "Ch\xE8n c\xF4ng th\u1EE9c LaTeX (L)",
725
+ toolbarIcon: LatexIcon,
726
+ toolbarTestId: "stamp-toolbar-latex",
727
+ matchesCustomData: isLatexCustomData,
728
+ async renderSvgFromCustomData(data) {
729
+ if (!isLatexCustomData(data)) {
730
+ throw new Error("latexStamp.renderSvgFromCustomData: customData kh\xF4ng ph\u1EA3i latex");
731
+ }
732
+ return renderLatexToSvg(data.src, data.displayMode);
733
+ },
734
+ async restoreFileFromCustomData(element) {
735
+ const data = element.customData;
736
+ const fileId = element.fileId;
737
+ if (!data || !fileId) return null;
738
+ if (!isLatexCustomData(data)) return null;
739
+ const svgString = await renderLatexToSvg(data.src, data.displayMode);
740
+ const utf8 = unescape(encodeURIComponent(svgString));
741
+ const dataURL = "data:image/svg+xml;base64," + (typeof btoa !== "undefined" ? btoa(utf8) : Buffer.from(utf8).toString("base64"));
742
+ return { fileId, dataURL, mimeType: "image/svg+xml" };
743
+ },
744
+ Host: LatexStampHost3
745
+ };
746
+
747
+ exports.isLatexCustomData = isLatexCustomData;
748
+ exports.latexStamp = latexStamp;
749
+ //# sourceMappingURL=latex.js.map
750
+ //# sourceMappingURL=latex.js.map