@xom11/whiteboard 0.6.5 → 0.9.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/README.md +87 -1
- package/dist/chunk-74VEEZBV.mjs +619 -0
- package/dist/chunk-74VEEZBV.mjs.map +1 -0
- package/dist/chunk-7P7SQFOW.mjs +39 -0
- package/dist/chunk-7P7SQFOW.mjs.map +1 -0
- package/dist/chunk-C6SCVOMC.mjs +111 -0
- package/dist/chunk-C6SCVOMC.mjs.map +1 -0
- package/dist/chunk-DU2NFHRR.mjs +103 -0
- package/dist/chunk-DU2NFHRR.mjs.map +1 -0
- package/dist/chunk-DU3RHKT5.mjs +44 -0
- package/dist/chunk-DU3RHKT5.mjs.map +1 -0
- package/dist/chunk-HTBLO5JO.mjs +41 -0
- package/dist/chunk-HTBLO5JO.mjs.map +1 -0
- package/dist/chunk-IUVV52HO.mjs +144 -0
- package/dist/chunk-IUVV52HO.mjs.map +1 -0
- package/dist/chunk-KEYZ5EZT.mjs +154 -0
- package/dist/chunk-KEYZ5EZT.mjs.map +1 -0
- package/dist/chunk-P2AOIF7S.mjs +40 -0
- package/dist/chunk-P2AOIF7S.mjs.map +1 -0
- package/dist/chunk-SBDMF4NQ.mjs +212 -0
- package/dist/chunk-SBDMF4NQ.mjs.map +1 -0
- package/dist/chunk-X5R72SSJ.mjs +52 -0
- package/dist/chunk-X5R72SSJ.mjs.map +1 -0
- package/dist/chunk-ZVN356JZ.mjs +58 -0
- package/dist/chunk-ZVN356JZ.mjs.map +1 -0
- package/dist/geometry-2d.d.mts +16 -0
- package/dist/geometry-2d.d.ts +16 -0
- package/dist/geometry-2d.js +3581 -0
- package/dist/geometry-2d.js.map +1 -0
- package/dist/geometry-2d.mjs +7 -0
- package/dist/geometry-2d.mjs.map +1 -0
- package/dist/geometry-3d.d.mts +16 -0
- package/dist/geometry-3d.d.ts +16 -0
- package/dist/geometry-3d.js +4105 -0
- package/dist/geometry-3d.js.map +1 -0
- package/dist/geometry-3d.mjs +7 -0
- package/dist/geometry-3d.mjs.map +1 -0
- package/dist/graph-2d.d.mts +16 -0
- package/dist/graph-2d.d.ts +16 -0
- package/dist/graph-2d.js +2019 -0
- package/dist/graph-2d.js.map +1 -0
- package/dist/graph-2d.mjs +6 -0
- package/dist/graph-2d.mjs.map +1 -0
- package/dist/host-LZH2FZ2N.mjs +1066 -0
- package/dist/host-LZH2FZ2N.mjs.map +1 -0
- package/dist/host-PIIDSMVE.mjs +3187 -0
- package/dist/host-PIIDSMVE.mjs.map +1 -0
- package/dist/host-VDNAJMLC.mjs +2864 -0
- package/dist/host-VDNAJMLC.mjs.map +1 -0
- package/dist/host-Z3TEJKZA.mjs +466 -0
- package/dist/host-Z3TEJKZA.mjs.map +1 -0
- package/dist/index.d.mts +30 -148
- package/dist/index.d.ts +30 -148
- package/dist/index.js +8370 -5614
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +395 -7294
- package/dist/index.mjs.map +1 -1
- package/dist/latex.d.mts +15 -0
- package/dist/latex.d.ts +15 -0
- package/dist/latex.js +750 -0
- package/dist/latex.js.map +1 -0
- package/dist/latex.mjs +6 -0
- package/dist/latex.mjs.map +1 -0
- package/dist/types-CinstD7T.d.mts +110 -0
- package/dist/types-CinstD7T.d.ts +110 -0
- package/package.json +26 -7
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { renderLatexToSvg, isLatexCustomData } from './chunk-X5R72SSJ.mjs';
|
|
3
|
+
import { useIsMobile } from './chunk-P2AOIF7S.mjs';
|
|
4
|
+
import { insertStampImage } from './chunk-C6SCVOMC.mjs';
|
|
5
|
+
import './chunk-BJTO5JO5.mjs';
|
|
6
|
+
import { forwardRef, useState, useRef, useEffect, useCallback, useImperativeHandle, useMemo } from 'react';
|
|
7
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
8
|
+
|
|
9
|
+
function Shell({ title, icon, onClose, children, isMobile, drawerOpen, onDrawerClose }) {
|
|
10
|
+
const mobileAttrs = isMobile ? {
|
|
11
|
+
"data-mobile-drawer": "true",
|
|
12
|
+
"data-drawer-state": drawerOpen ? "open" : "closed"
|
|
13
|
+
} : {};
|
|
14
|
+
const handleHeaderClose = () => {
|
|
15
|
+
if (isMobile) onDrawerClose?.();
|
|
16
|
+
else onClose();
|
|
17
|
+
};
|
|
18
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
19
|
+
isMobile && drawerOpen && /* @__PURE__ */ jsx(
|
|
20
|
+
"div",
|
|
21
|
+
{
|
|
22
|
+
className: "stamp-drawer-backdrop",
|
|
23
|
+
onPointerDown: onDrawerClose,
|
|
24
|
+
"aria-hidden": "true"
|
|
25
|
+
}
|
|
26
|
+
),
|
|
27
|
+
/* @__PURE__ */ jsxs(
|
|
28
|
+
"aside",
|
|
29
|
+
{
|
|
30
|
+
role: "complementary",
|
|
31
|
+
"aria-label": title,
|
|
32
|
+
"aria-hidden": isMobile && !drawerOpen ? "true" : void 0,
|
|
33
|
+
"data-testid": "stamp-left-panel",
|
|
34
|
+
"data-stamp-area": "true",
|
|
35
|
+
...mobileAttrs,
|
|
36
|
+
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",
|
|
37
|
+
children: [
|
|
38
|
+
/* @__PURE__ */ jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-3 py-2", children: [
|
|
39
|
+
/* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
|
|
40
|
+
/* @__PURE__ */ jsx("span", { className: "text-base leading-none", children: icon }),
|
|
41
|
+
title
|
|
42
|
+
] }),
|
|
43
|
+
/* @__PURE__ */ jsx(
|
|
44
|
+
"button",
|
|
45
|
+
{
|
|
46
|
+
onClick: handleHeaderClose,
|
|
47
|
+
"aria-label": isMobile ? "\u0110\xF3ng ng\u0103n c\xF4ng c\u1EE5" : "\u0110\xF3ng",
|
|
48
|
+
className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
49
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
50
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
51
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
52
|
+
] })
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
] }),
|
|
56
|
+
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-4", children })
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
] });
|
|
61
|
+
}
|
|
62
|
+
function Section({ label, children }) {
|
|
63
|
+
return /* @__PURE__ */ jsxs("section", { children: [
|
|
64
|
+
/* @__PURE__ */ jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: label }),
|
|
65
|
+
children
|
|
66
|
+
] });
|
|
67
|
+
}
|
|
68
|
+
var SNIPPETS = [
|
|
69
|
+
{
|
|
70
|
+
group: "Ph\xE2n s\u1ED1 & lu\u1EF9 th\u1EEBa",
|
|
71
|
+
items: [
|
|
72
|
+
{ label: "Ph\xE2n s\u1ED1", preview: "a\u2044b", snippet: "\\frac{a}{b}" },
|
|
73
|
+
{ label: "Lu\u1EF9 th\u1EEBa", preview: "x\xB2", snippet: "^{2}" },
|
|
74
|
+
{ label: "Ch\u1EC9 s\u1ED1", preview: "x\u2081", snippet: "_{1}" },
|
|
75
|
+
{ label: "C\u0103n", preview: "\u221Ax", snippet: "\\sqrt{x}" },
|
|
76
|
+
{ label: "C\u0103n n", preview: "\u207F\u221Ax", snippet: "\\sqrt[n]{x}" }
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
group: "T\u1ED5ng & t\xEDch ph\xE2n",
|
|
81
|
+
items: [
|
|
82
|
+
{ label: "T\u1ED5ng", preview: "\u03A3", snippet: "\\sum_{i=1}^{n}" },
|
|
83
|
+
{ label: "T\xEDch", preview: "\u03A0", snippet: "\\prod_{i=1}^{n}" },
|
|
84
|
+
{ label: "T\xEDch ph\xE2n", preview: "\u222B", snippet: "\\int_{a}^{b}" },
|
|
85
|
+
{ label: "Gi\u1EDBi h\u1EA1n", preview: "lim", snippet: "\\lim_{x \\to 0}" }
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
group: "K\xFD hi\u1EC7u",
|
|
90
|
+
items: [
|
|
91
|
+
{ label: "\u03B1", preview: "\u03B1", snippet: "\\alpha" },
|
|
92
|
+
{ label: "\u03B2", preview: "\u03B2", snippet: "\\beta" },
|
|
93
|
+
{ label: "\u03C0", preview: "\u03C0", snippet: "\\pi" },
|
|
94
|
+
{ label: "\u03B8", preview: "\u03B8", snippet: "\\theta" },
|
|
95
|
+
{ label: "\u2260", preview: "\u2260", snippet: "\\neq" },
|
|
96
|
+
{ label: "\u2264", preview: "\u2264", snippet: "\\leq" },
|
|
97
|
+
{ label: "\u2265", preview: "\u2265", snippet: "\\geq" },
|
|
98
|
+
{ label: "\u221E", preview: "\u221E", snippet: "\\infty" },
|
|
99
|
+
{ label: "\u2192", preview: "\u2192", snippet: "\\to" }
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
];
|
|
103
|
+
function LeftPanel({
|
|
104
|
+
displayMode,
|
|
105
|
+
onDisplayModeChange,
|
|
106
|
+
onInsertSnippet,
|
|
107
|
+
onClose,
|
|
108
|
+
isMobile,
|
|
109
|
+
drawerOpen,
|
|
110
|
+
onDrawerClose
|
|
111
|
+
}) {
|
|
112
|
+
return /* @__PURE__ */ jsxs(
|
|
113
|
+
Shell,
|
|
114
|
+
{
|
|
115
|
+
title: "C\xF4ng th\u1EE9c LaTeX",
|
|
116
|
+
icon: "\u2211",
|
|
117
|
+
onClose,
|
|
118
|
+
isMobile,
|
|
119
|
+
drawerOpen,
|
|
120
|
+
onDrawerClose,
|
|
121
|
+
children: [
|
|
122
|
+
/* @__PURE__ */ jsx(Section, { label: "Ch\u1EBF \u0111\u1ED9 hi\u1EC3n th\u1ECB", children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-1.5", children: [
|
|
123
|
+
/* @__PURE__ */ jsxs(
|
|
124
|
+
"button",
|
|
125
|
+
{
|
|
126
|
+
type: "button",
|
|
127
|
+
onClick: () => onDisplayModeChange(false),
|
|
128
|
+
"aria-pressed": !displayMode,
|
|
129
|
+
className: [
|
|
130
|
+
"rounded-md border px-2 py-1.5 text-xs transition",
|
|
131
|
+
!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"
|
|
132
|
+
].join(" "),
|
|
133
|
+
children: [
|
|
134
|
+
/* @__PURE__ */ jsx("span", { className: "block font-medium", children: "Inline" }),
|
|
135
|
+
/* @__PURE__ */ jsx("span", { className: "block text-[10px] text-slate-500", children: "$ ... $" })
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
),
|
|
139
|
+
/* @__PURE__ */ jsxs(
|
|
140
|
+
"button",
|
|
141
|
+
{
|
|
142
|
+
type: "button",
|
|
143
|
+
onClick: () => onDisplayModeChange(true),
|
|
144
|
+
"aria-pressed": displayMode,
|
|
145
|
+
className: [
|
|
146
|
+
"rounded-md border px-2 py-1.5 text-xs transition",
|
|
147
|
+
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"
|
|
148
|
+
].join(" "),
|
|
149
|
+
children: [
|
|
150
|
+
/* @__PURE__ */ jsx("span", { className: "block font-medium", children: "Block" }),
|
|
151
|
+
/* @__PURE__ */ jsx("span", { className: "block text-[10px] text-slate-500", children: "$$ ... $$" })
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
] }) }),
|
|
156
|
+
SNIPPETS.map((group) => /* @__PURE__ */ jsx(Section, { label: group.group, children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: group.items.map((s) => /* @__PURE__ */ jsx(
|
|
157
|
+
"button",
|
|
158
|
+
{
|
|
159
|
+
type: "button",
|
|
160
|
+
"data-snippet": s.snippet,
|
|
161
|
+
onClick: () => onInsertSnippet(s.snippet),
|
|
162
|
+
title: s.snippet,
|
|
163
|
+
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",
|
|
164
|
+
children: s.preview
|
|
165
|
+
},
|
|
166
|
+
s.snippet
|
|
167
|
+
)) }) }, group.group)),
|
|
168
|
+
/* @__PURE__ */ jsx(Section, { label: "Ph\xEDm t\u1EAFt", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2 text-[11px] text-slate-600", children: [
|
|
169
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
|
|
170
|
+
/* @__PURE__ */ jsx("kbd", { className: "rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 font-mono", children: "Enter" }),
|
|
171
|
+
"ch\xE8n"
|
|
172
|
+
] }),
|
|
173
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
|
|
174
|
+
/* @__PURE__ */ jsx("kbd", { className: "rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 font-mono", children: "Esc" }),
|
|
175
|
+
"\u0111\xF3ng"
|
|
176
|
+
] })
|
|
177
|
+
] }) })
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
var DEBOUNCE_MS = 100;
|
|
183
|
+
var EditorPopover = forwardRef(function EditorPopover2({
|
|
184
|
+
x,
|
|
185
|
+
y,
|
|
186
|
+
initialValue,
|
|
187
|
+
onInsert,
|
|
188
|
+
onClose,
|
|
189
|
+
displayMode: controlledDisplayMode,
|
|
190
|
+
onDisplayModeChange,
|
|
191
|
+
withLeftPanel = false,
|
|
192
|
+
isMobile = false,
|
|
193
|
+
onOpenDrawer
|
|
194
|
+
}, ref) {
|
|
195
|
+
const [value, setValue] = useState(initialValue);
|
|
196
|
+
const [internalDisplayMode] = useState(false);
|
|
197
|
+
const displayMode = controlledDisplayMode ?? internalDisplayMode;
|
|
198
|
+
const [previewSvg, setPreviewSvg] = useState(null);
|
|
199
|
+
const [error, setError] = useState(null);
|
|
200
|
+
const debounceRef = useRef(null);
|
|
201
|
+
const inputRef = useRef(null);
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
204
|
+
debounceRef.current = setTimeout(async () => {
|
|
205
|
+
try {
|
|
206
|
+
const svg = await renderLatexToSvg(value, displayMode);
|
|
207
|
+
setPreviewSvg(svg);
|
|
208
|
+
setError(null);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
setPreviewSvg(null);
|
|
211
|
+
setError(err.message);
|
|
212
|
+
}
|
|
213
|
+
}, DEBOUNCE_MS);
|
|
214
|
+
return () => {
|
|
215
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
216
|
+
};
|
|
217
|
+
}, [value, displayMode]);
|
|
218
|
+
const handleInsert = useCallback(() => {
|
|
219
|
+
if (!previewSvg) return;
|
|
220
|
+
onInsert(previewSvg, value, displayMode);
|
|
221
|
+
}, [previewSvg, value, displayMode, onInsert]);
|
|
222
|
+
const handleKeyDown = useCallback(
|
|
223
|
+
(e) => {
|
|
224
|
+
if (e.key === "Escape") onClose();
|
|
225
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
226
|
+
e.preventDefault();
|
|
227
|
+
handleInsert();
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
[onClose, handleInsert]
|
|
231
|
+
);
|
|
232
|
+
useImperativeHandle(
|
|
233
|
+
ref,
|
|
234
|
+
() => ({
|
|
235
|
+
insertAtCursor: (snippet) => {
|
|
236
|
+
const el = inputRef.current;
|
|
237
|
+
if (!el) {
|
|
238
|
+
setValue((v) => v + snippet);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const start = el.selectionStart ?? value.length;
|
|
242
|
+
const end = el.selectionEnd ?? value.length;
|
|
243
|
+
const next = value.slice(0, start) + snippet + value.slice(end);
|
|
244
|
+
setValue(next);
|
|
245
|
+
requestAnimationFrame(() => {
|
|
246
|
+
el.focus();
|
|
247
|
+
const pos = start + snippet.length;
|
|
248
|
+
try {
|
|
249
|
+
el.setSelectionRange(pos, pos);
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
hasContent: () => value.trim().length > 0 && !!previewSvg && !error,
|
|
255
|
+
tryInsert: () => {
|
|
256
|
+
if (!previewSvg || error || !value.trim()) return false;
|
|
257
|
+
onInsert(previewSvg, value, displayMode);
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
}),
|
|
261
|
+
[value, previewSvg, error, displayMode, onInsert]
|
|
262
|
+
);
|
|
263
|
+
const isLegacyPosition = x > 0 || y > 0;
|
|
264
|
+
const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 50 } : isLegacyPosition ? { position: "absolute", top: y, left: x, zIndex: 50 } : {
|
|
265
|
+
position: "absolute",
|
|
266
|
+
top: "50%",
|
|
267
|
+
left: withLeftPanel ? "calc(50% + 120px)" : "50%",
|
|
268
|
+
transform: "translate(-50%, -50%)",
|
|
269
|
+
zIndex: 50
|
|
270
|
+
};
|
|
271
|
+
return /* @__PURE__ */ jsxs(
|
|
272
|
+
"div",
|
|
273
|
+
{
|
|
274
|
+
style: wrapperStyle,
|
|
275
|
+
"data-stamp-area": "true",
|
|
276
|
+
"data-mobile-editor": isMobile ? "true" : void 0,
|
|
277
|
+
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",
|
|
278
|
+
role: "dialog",
|
|
279
|
+
"aria-label": "Nh\u1EADp c\xF4ng th\u1EE9c LaTeX",
|
|
280
|
+
children: [
|
|
281
|
+
/* @__PURE__ */ 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: [
|
|
282
|
+
isMobile && /* @__PURE__ */ jsx(
|
|
283
|
+
"button",
|
|
284
|
+
{
|
|
285
|
+
type: "button",
|
|
286
|
+
onClick: onOpenDrawer,
|
|
287
|
+
"aria-label": "M\u1EDF ng\u0103n snippet",
|
|
288
|
+
className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
|
|
289
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
290
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
|
|
291
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
|
|
292
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
|
|
293
|
+
] })
|
|
294
|
+
}
|
|
295
|
+
),
|
|
296
|
+
/* @__PURE__ */ jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
|
|
297
|
+
/* @__PURE__ */ jsx("span", { className: "text-base leading-none", children: "\u2211" }),
|
|
298
|
+
"C\xF4ng th\u1EE9c LaTeX"
|
|
299
|
+
] }),
|
|
300
|
+
isMobile && /* @__PURE__ */ jsx(
|
|
301
|
+
"button",
|
|
302
|
+
{
|
|
303
|
+
type: "button",
|
|
304
|
+
onClick: handleInsert,
|
|
305
|
+
disabled: !previewSvg || !!error,
|
|
306
|
+
"data-testid": "latex-insert-btn-mobile",
|
|
307
|
+
className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
|
|
308
|
+
children: "Ch\xE8n"
|
|
309
|
+
}
|
|
310
|
+
),
|
|
311
|
+
/* @__PURE__ */ jsx(
|
|
312
|
+
"button",
|
|
313
|
+
{
|
|
314
|
+
onClick: onClose,
|
|
315
|
+
"aria-label": "\u0110\xF3ng",
|
|
316
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15",
|
|
317
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
318
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
319
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
320
|
+
] })
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
] }),
|
|
324
|
+
/* @__PURE__ */ jsxs("div", { className: `space-y-2 p-3${isMobile ? " flex min-h-0 flex-1 flex-col" : ""}`, children: [
|
|
325
|
+
/* @__PURE__ */ jsx(
|
|
326
|
+
"input",
|
|
327
|
+
{
|
|
328
|
+
ref: inputRef,
|
|
329
|
+
type: "text",
|
|
330
|
+
role: "textbox",
|
|
331
|
+
value,
|
|
332
|
+
onChange: (e) => setValue(e.target.value),
|
|
333
|
+
onKeyDown: handleKeyDown,
|
|
334
|
+
placeholder: "Vd: \\frac{a^2+b^2}{c}",
|
|
335
|
+
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"}`,
|
|
336
|
+
autoFocus: true
|
|
337
|
+
}
|
|
338
|
+
),
|
|
339
|
+
/* @__PURE__ */ jsx(
|
|
340
|
+
"div",
|
|
341
|
+
{
|
|
342
|
+
className: [
|
|
343
|
+
"flex items-center justify-center rounded border p-3 text-center",
|
|
344
|
+
isMobile ? "min-h-0 flex-1 overflow-auto" : "min-h-[64px]",
|
|
345
|
+
error ? "border-rose-300 bg-rose-50 text-rose-700" : "border-slate-200 bg-slate-50"
|
|
346
|
+
].join(" "),
|
|
347
|
+
children: error ? /* @__PURE__ */ jsxs("span", { className: "text-xs", children: [
|
|
348
|
+
"L\u1ED7i: ",
|
|
349
|
+
error.slice(0, 80)
|
|
350
|
+
] }) : previewSvg ? /* @__PURE__ */ jsx("span", { dangerouslySetInnerHTML: { __html: previewSvg } }) : /* @__PURE__ */ jsx("span", { className: "text-xs text-slate-400", children: "(xem tr\u01B0\u1EDBc)" })
|
|
351
|
+
}
|
|
352
|
+
),
|
|
353
|
+
!isMobile && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
354
|
+
/* @__PURE__ */ jsxs("span", { className: "text-[11px] text-slate-500", children: [
|
|
355
|
+
displayMode ? "Block" : "Inline",
|
|
356
|
+
" \xB7 Enter \u0111\u1EC3 ch\xE8n"
|
|
357
|
+
] }),
|
|
358
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
359
|
+
/* @__PURE__ */ jsx(
|
|
360
|
+
"button",
|
|
361
|
+
{
|
|
362
|
+
onClick: onClose,
|
|
363
|
+
className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
|
|
364
|
+
children: "Hu\u1EF7"
|
|
365
|
+
}
|
|
366
|
+
),
|
|
367
|
+
/* @__PURE__ */ jsx(
|
|
368
|
+
"button",
|
|
369
|
+
{
|
|
370
|
+
onClick: handleInsert,
|
|
371
|
+
disabled: !previewSvg || !!error,
|
|
372
|
+
className: "rounded bg-indigo-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-indigo-700 disabled:opacity-50",
|
|
373
|
+
children: "Ch\xE8n"
|
|
374
|
+
}
|
|
375
|
+
)
|
|
376
|
+
] })
|
|
377
|
+
] }),
|
|
378
|
+
isMobile && /* @__PURE__ */ jsxs("div", { className: "text-center text-[11px] text-slate-500", children: [
|
|
379
|
+
displayMode ? "Block" : "Inline",
|
|
380
|
+
" \xB7 B\u1EA5m Ch\xE8n \u1EDF thanh tr\xEAn"
|
|
381
|
+
] })
|
|
382
|
+
] })
|
|
383
|
+
]
|
|
384
|
+
}
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
var LatexStampHost = forwardRef(
|
|
388
|
+
function LatexStampHost2({ api, editingElement, onClose }, ref) {
|
|
389
|
+
const editorRef = useRef(null);
|
|
390
|
+
const { isMobile } = useIsMobile();
|
|
391
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
392
|
+
const initial = useMemo(() => {
|
|
393
|
+
if (editingElement && isLatexCustomData(editingElement.customData)) {
|
|
394
|
+
return {
|
|
395
|
+
initialValue: editingElement.customData.src,
|
|
396
|
+
displayMode: !!editingElement.customData.displayMode
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return { initialValue: "", displayMode: false };
|
|
400
|
+
}, [editingElement]);
|
|
401
|
+
const [displayMode, setDisplayMode] = useState(initial.displayMode);
|
|
402
|
+
const handleInsert = useCallback(
|
|
403
|
+
async (svgString, src, dm) => {
|
|
404
|
+
if (!api) return;
|
|
405
|
+
try {
|
|
406
|
+
await insertStampImage(api, {
|
|
407
|
+
svgString,
|
|
408
|
+
makeCustomData: () => ({
|
|
409
|
+
kind: "latex",
|
|
410
|
+
version: 1,
|
|
411
|
+
src,
|
|
412
|
+
displayMode: dm
|
|
413
|
+
}),
|
|
414
|
+
editingElementId: editingElement?.id ?? null
|
|
415
|
+
});
|
|
416
|
+
} catch (err) {
|
|
417
|
+
console.error("Latex insert failed:", err);
|
|
418
|
+
}
|
|
419
|
+
onClose();
|
|
420
|
+
},
|
|
421
|
+
[api, editingElement?.id, onClose]
|
|
422
|
+
);
|
|
423
|
+
useImperativeHandle(
|
|
424
|
+
ref,
|
|
425
|
+
() => ({
|
|
426
|
+
tryInsert: () => editorRef.current?.tryInsert() ?? false,
|
|
427
|
+
hasContent: () => editorRef.current?.hasContent() ?? false
|
|
428
|
+
}),
|
|
429
|
+
[]
|
|
430
|
+
);
|
|
431
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
432
|
+
/* @__PURE__ */ jsx(
|
|
433
|
+
LeftPanel,
|
|
434
|
+
{
|
|
435
|
+
displayMode,
|
|
436
|
+
onDisplayModeChange: setDisplayMode,
|
|
437
|
+
onInsertSnippet: (s) => editorRef.current?.insertAtCursor(s),
|
|
438
|
+
onClose,
|
|
439
|
+
isMobile,
|
|
440
|
+
drawerOpen,
|
|
441
|
+
onDrawerClose: () => setDrawerOpen(false)
|
|
442
|
+
}
|
|
443
|
+
),
|
|
444
|
+
/* @__PURE__ */ jsx(
|
|
445
|
+
EditorPopover,
|
|
446
|
+
{
|
|
447
|
+
ref: editorRef,
|
|
448
|
+
x: 0,
|
|
449
|
+
y: 0,
|
|
450
|
+
initialValue: initial.initialValue,
|
|
451
|
+
displayMode,
|
|
452
|
+
onDisplayModeChange: setDisplayMode,
|
|
453
|
+
onInsert: handleInsert,
|
|
454
|
+
onClose,
|
|
455
|
+
withLeftPanel: !isMobile,
|
|
456
|
+
isMobile,
|
|
457
|
+
onOpenDrawer: () => setDrawerOpen(true)
|
|
458
|
+
}
|
|
459
|
+
)
|
|
460
|
+
] });
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
export { LatexStampHost };
|
|
465
|
+
//# sourceMappingURL=host-Z3TEJKZA.mjs.map
|
|
466
|
+
//# sourceMappingURL=host-Z3TEJKZA.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stamps/latex/editor/LeftPanel.tsx","../src/stamps/latex/editor/EditorPopover.tsx","../src/stamps/latex/host.tsx"],"names":["EditorPopover","jsxs","jsx","forwardRef","LatexStampHost","useRef","useState","useCallback","useImperativeHandle","Fragment"],"mappings":"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","file":"host-Z3TEJKZA.mjs","sourcesContent":["'use client';\n\nimport React from 'react';\n\n// ---------- Shared shell (latex copy — geometry copy stays in src/stamp/StampLeftPanel.tsx) ----------\n\ninterface ShellProps {\n title: string;\n icon: React.ReactNode;\n onClose: () => void;\n children: React.ReactNode;\n isMobile?: boolean;\n drawerOpen?: boolean;\n onDrawerClose?: () => void;\n}\n\nfunction Shell({ title, icon, onClose, children, isMobile, drawerOpen, onDrawerClose }: ShellProps) {\n const mobileAttrs = isMobile\n ? {\n 'data-mobile-drawer': 'true',\n 'data-drawer-state': drawerOpen ? 'open' : 'closed',\n }\n : {};\n const handleHeaderClose = () => {\n if (isMobile) onDrawerClose?.();\n else onClose();\n };\n return (\n <>\n {isMobile && drawerOpen && (\n <div\n className=\"stamp-drawer-backdrop\"\n onPointerDown={onDrawerClose}\n aria-hidden=\"true\"\n />\n )}\n <aside\n role=\"complementary\"\n aria-label={title}\n aria-hidden={isMobile && !drawerOpen ? 'true' : undefined}\n data-testid=\"stamp-left-panel\"\n data-stamp-area=\"true\"\n {...mobileAttrs}\n className={\n isMobile\n ? 'stamp-drawer-mobile flex flex-col border-r border-slate-200 bg-white shadow-md'\n : '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'\n }\n >\n <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\">\n <h3 className=\"flex items-center gap-2 text-sm font-semibold text-slate-800\">\n <span className=\"text-base leading-none\">{icon}</span>\n {title}\n </h3>\n <button\n onClick={handleHeaderClose}\n aria-label={isMobile ? 'Đóng ngăn công cụ' : 'Đóng'}\n className=\"rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n </svg>\n </button>\n </header>\n <div className=\"min-h-0 flex-1 overflow-y-auto p-3 space-y-4\">{children}</div>\n </aside>\n </>\n );\n}\n\nfunction Section({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <section>\n <h4 className=\"mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500\">\n {label}\n </h4>\n {children}\n </section>\n );\n}\n\n// ---------- LaTeX left panel ----------\n\ninterface LatexLeftPanelProps {\n displayMode: boolean;\n onDisplayModeChange: (b: boolean) => void;\n onInsertSnippet: (snippet: string) => void;\n onClose: () => void;\n isMobile?: boolean;\n drawerOpen?: boolean;\n onDrawerClose?: () => void;\n}\n\ninterface SnippetDef {\n label: string;\n preview: string;\n snippet: string;\n}\n\nconst SNIPPETS: { group: string; items: SnippetDef[] }[] = [\n {\n group: 'Phân số & luỹ thừa',\n items: [\n { label: 'Phân số', preview: 'a⁄b', snippet: '\\\\frac{a}{b}' },\n { label: 'Luỹ thừa', preview: 'x²', snippet: '^{2}' },\n { label: 'Chỉ số', preview: 'x₁', snippet: '_{1}' },\n { label: 'Căn', preview: '√x', snippet: '\\\\sqrt{x}' },\n { label: 'Căn n', preview: 'ⁿ√x', snippet: '\\\\sqrt[n]{x}' },\n ],\n },\n {\n group: 'Tổng & tích phân',\n items: [\n { label: 'Tổng', preview: 'Σ', snippet: '\\\\sum_{i=1}^{n}' },\n { label: 'Tích', preview: 'Π', snippet: '\\\\prod_{i=1}^{n}' },\n { label: 'Tích phân', preview: '∫', snippet: '\\\\int_{a}^{b}' },\n { label: 'Giới hạn', preview: 'lim', snippet: '\\\\lim_{x \\\\to 0}' },\n ],\n },\n {\n group: 'Ký hiệu',\n items: [\n { label: 'α', preview: 'α', snippet: '\\\\alpha' },\n { label: 'β', preview: 'β', snippet: '\\\\beta' },\n { label: 'π', preview: 'π', snippet: '\\\\pi' },\n { label: 'θ', preview: 'θ', snippet: '\\\\theta' },\n { label: '≠', preview: '≠', snippet: '\\\\neq' },\n { label: '≤', preview: '≤', snippet: '\\\\leq' },\n { label: '≥', preview: '≥', snippet: '\\\\geq' },\n { label: '∞', preview: '∞', snippet: '\\\\infty' },\n { label: '→', preview: '→', snippet: '\\\\to' },\n ],\n },\n];\n\nexport function LeftPanel({\n displayMode,\n onDisplayModeChange,\n onInsertSnippet,\n onClose,\n isMobile,\n drawerOpen,\n onDrawerClose,\n}: LatexLeftPanelProps) {\n return (\n <Shell\n title=\"Công thức LaTeX\"\n icon=\"∑\"\n onClose={onClose}\n isMobile={isMobile}\n drawerOpen={drawerOpen}\n onDrawerClose={onDrawerClose}\n >\n <Section label=\"Chế độ hiển thị\">\n <div className=\"grid grid-cols-2 gap-1.5\">\n <button\n type=\"button\"\n onClick={() => onDisplayModeChange(false)}\n aria-pressed={!displayMode}\n className={[\n 'rounded-md border px-2 py-1.5 text-xs transition',\n !displayMode\n ? 'border-emerald-500 bg-emerald-50 text-emerald-700 ring-1 ring-emerald-300'\n : 'border-slate-200 bg-white text-slate-700 hover:border-slate-300 hover:bg-slate-50',\n ].join(' ')}\n >\n <span className=\"block font-medium\">Inline</span>\n <span className=\"block text-[10px] text-slate-500\">$ ... $</span>\n </button>\n <button\n type=\"button\"\n onClick={() => onDisplayModeChange(true)}\n aria-pressed={displayMode}\n className={[\n 'rounded-md border px-2 py-1.5 text-xs transition',\n displayMode\n ? 'border-emerald-500 bg-emerald-50 text-emerald-700 ring-1 ring-emerald-300'\n : 'border-slate-200 bg-white text-slate-700 hover:border-slate-300 hover:bg-slate-50',\n ].join(' ')}\n >\n <span className=\"block font-medium\">Block</span>\n <span className=\"block text-[10px] text-slate-500\">$$ ... $$</span>\n </button>\n </div>\n </Section>\n\n {SNIPPETS.map((group) => (\n <Section key={group.group} label={group.group}>\n <div className=\"flex flex-wrap gap-1\">\n {group.items.map((s) => (\n <button\n key={s.snippet}\n type=\"button\"\n data-snippet={s.snippet}\n onClick={() => onInsertSnippet(s.snippet)}\n title={s.snippet}\n 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\"\n >\n {s.preview}\n </button>\n ))}\n </div>\n </Section>\n ))}\n\n <Section label=\"Phím tắt\">\n <div className=\"flex flex-wrap gap-2 text-[11px] text-slate-600\">\n <span className=\"inline-flex items-center gap-1\">\n <kbd className=\"rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 font-mono\">Enter</kbd>\n chèn\n </span>\n <span className=\"inline-flex items-center gap-1\">\n <kbd className=\"rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 font-mono\">Esc</kbd>\n đóng\n </span>\n </div>\n </Section>\n </Shell>\n );\n}\n\n// Back-compat alias\nexport { LeftPanel as LatexLeftPanel };\n","'use client';\nimport React, {\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from 'react';\nimport { renderLatexToSvg } from '../render';\n\ninterface Props {\n /**\n * Legacy: vị trí absolute x/y nếu cần (test). Khi cả 2 = 0 và `centered` !== false,\n * popover sẽ tự center floating ở giữa khu vực bảng. Khi `withLeftPanel` = true,\n * vị trí center được offset 120px để chừa chỗ panel trái.\n */\n x: number;\n y: number;\n initialValue: string;\n onInsert: (svgString: string, src: string, displayMode: boolean) => void;\n onClose: () => void;\n /** Khi controlled từ parent (StampLeftPanel), parent set giá trị này. */\n displayMode?: boolean;\n onDisplayModeChange?: (b: boolean) => void;\n /** Khi true, position center offset cho panel trái. */\n withLeftPanel?: boolean;\n /** Mobile mode: full-screen + hamburger header. */\n isMobile?: boolean;\n /** Trigger mở snippet drawer trên mobile. */\n onOpenDrawer?: () => void;\n}\n\nexport interface EditorPopoverHandle {\n /** Chèn snippet vào vị trí con trỏ trong textbox. */\n insertAtCursor: (snippet: string) => void;\n /** Có content hợp lệ để chèn không (input không rỗng + preview ok). */\n hasContent: () => boolean;\n /** Trigger insert programmatically — return true nếu chèn thành công. */\n tryInsert: () => boolean;\n}\n\nconst DEBOUNCE_MS = 100;\n\nexport const EditorPopover = forwardRef<EditorPopoverHandle, Props>(function EditorPopover(\n {\n x,\n y,\n initialValue,\n onInsert,\n onClose,\n displayMode: controlledDisplayMode,\n onDisplayModeChange,\n withLeftPanel = false,\n isMobile = false,\n onOpenDrawer,\n },\n ref,\n) {\n const [value, setValue] = useState(initialValue);\n const [internalDisplayMode] = useState(false);\n const displayMode = controlledDisplayMode ?? internalDisplayMode;\n // onDisplayModeChange chỉ dùng khi controlled từ parent — không cần local setter\n void onDisplayModeChange;\n\n const [previewSvg, setPreviewSvg] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n debounceRef.current = setTimeout(async () => {\n try {\n const svg = await renderLatexToSvg(value, displayMode);\n setPreviewSvg(svg);\n setError(null);\n } catch (err) {\n setPreviewSvg(null);\n setError((err as Error).message);\n }\n }, DEBOUNCE_MS);\n return () => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n };\n }, [value, displayMode]);\n\n const handleInsert = useCallback(() => {\n if (!previewSvg) return;\n onInsert(previewSvg, value, displayMode);\n }, [previewSvg, value, displayMode, onInsert]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === 'Escape') onClose();\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleInsert();\n }\n },\n [onClose, handleInsert],\n );\n\n // Imperative API: snippet button trong panel trái gọi vào, click-outside auto-insert.\n useImperativeHandle(\n ref,\n () => ({\n insertAtCursor: (snippet: string) => {\n const el = inputRef.current;\n if (!el) {\n setValue((v) => v + snippet);\n return;\n }\n const start = el.selectionStart ?? value.length;\n const end = el.selectionEnd ?? value.length;\n const next = value.slice(0, start) + snippet + value.slice(end);\n setValue(next);\n requestAnimationFrame(() => {\n el.focus();\n const pos = start + snippet.length;\n try {\n el.setSelectionRange(pos, pos);\n } catch {\n /* ignore */\n }\n });\n },\n hasContent: () => value.trim().length > 0 && !!previewSvg && !error,\n tryInsert: () => {\n if (!previewSvg || error || !value.trim()) return false;\n onInsert(previewSvg, value, displayMode);\n return true;\n },\n }),\n [value, previewSvg, error, displayMode, onInsert],\n );\n\n // Position: nếu x/y > 0 → dùng legacy absolute (cho tests cũ). Còn không thì center floating.\n const isLegacyPosition = x > 0 || y > 0;\n const wrapperStyle: React.CSSProperties = isMobile\n ? { position: 'fixed', inset: 0, zIndex: 50 }\n : isLegacyPosition\n ? { position: 'absolute', top: y, left: x, zIndex: 50 }\n : {\n position: 'absolute',\n top: '50%',\n left: withLeftPanel ? 'calc(50% + 120px)' : '50%',\n transform: 'translate(-50%, -50%)',\n zIndex: 50,\n };\n\n return (\n <div\n style={wrapperStyle}\n data-stamp-area=\"true\"\n data-mobile-editor={isMobile ? 'true' : undefined}\n className={\n isMobile\n ? 'flex h-full w-full flex-col bg-white'\n : 'w-[420px] max-w-[calc(100vw-280px)] rounded-lg border border-slate-300 bg-white shadow-2xl ring-1 ring-black/5'\n }\n role=\"dialog\"\n aria-label=\"Nhập công thức LaTeX\"\n >\n <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'}`}>\n {isMobile && (\n <button\n type=\"button\"\n onClick={onOpenDrawer}\n aria-label=\"Mở ngăn snippet\"\n className=\"-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <line x1=\"4\" y1=\"6\" x2=\"20\" y2=\"6\" />\n <line x1=\"4\" y1=\"12\" x2=\"20\" y2=\"12\" />\n <line x1=\"4\" y1=\"18\" x2=\"20\" y2=\"18\" />\n </svg>\n </button>\n )}\n <h3 className=\"flex flex-1 items-center gap-2 text-sm font-semibold\">\n <span className=\"text-base leading-none\">∑</span>\n Công thức LaTeX\n </h3>\n {isMobile && (\n <button\n type=\"button\"\n onClick={handleInsert}\n disabled={!previewSvg || !!error}\n data-testid=\"latex-insert-btn-mobile\"\n className=\"rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50\"\n >\n Chèn\n </button>\n )}\n <button\n onClick={onClose}\n aria-label=\"Đóng\"\n className=\"inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15\"\n >\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n </svg>\n </button>\n </header>\n <div className={`space-y-2 p-3${isMobile ? ' flex min-h-0 flex-1 flex-col' : ''}`}>\n <input\n ref={inputRef}\n type=\"text\"\n role=\"textbox\"\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder=\"Vd: \\frac{a^2+b^2}{c}\"\n 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${\n isMobile ? ' min-h-[44px] text-base' : ' text-sm'\n }`}\n autoFocus\n />\n <div\n className={[\n 'flex items-center justify-center rounded border p-3 text-center',\n isMobile ? 'min-h-0 flex-1 overflow-auto' : 'min-h-[64px]',\n error ? 'border-rose-300 bg-rose-50 text-rose-700' : 'border-slate-200 bg-slate-50',\n ].join(' ')}\n >\n {error ? (\n <span className=\"text-xs\">Lỗi: {error.slice(0, 80)}</span>\n ) : previewSvg ? (\n <span dangerouslySetInnerHTML={{ __html: previewSvg }} />\n ) : (\n <span className=\"text-xs text-slate-400\">(xem trước)</span>\n )}\n </div>\n {!isMobile && (\n <div className=\"flex items-center justify-between\">\n <span className=\"text-[11px] text-slate-500\">\n {displayMode ? 'Block' : 'Inline'} · Enter để chèn\n </span>\n <div className=\"flex gap-2\">\n <button\n onClick={onClose}\n className=\"rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100\"\n >\n Huỷ\n </button>\n <button\n onClick={handleInsert}\n disabled={!previewSvg || !!error}\n className=\"rounded bg-indigo-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-indigo-700 disabled:opacity-50\"\n >\n Chèn\n </button>\n </div>\n </div>\n )}\n {isMobile && (\n <div className=\"text-center text-[11px] text-slate-500\">\n {displayMode ? 'Block' : 'Inline'} · Bấm Chèn ở thanh trên\n </div>\n )}\n </div>\n </div>\n );\n});\n\n// Back-compat aliases\nexport { EditorPopover as LatexEditorPopover };\nexport type { EditorPopoverHandle as LatexEditorHandle };\n","'use client';\n\nimport {\n forwardRef,\n useCallback,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { LeftPanel as LatexLeftPanel } from './editor/LeftPanel';\nimport {\n EditorPopover as LatexEditorPopover,\n type EditorPopoverHandle as LatexEditorHandle,\n} from './editor/EditorPopover';\nimport { insertStampImage } from '../shared/insertImage';\nimport { useIsMobile } from '../shared/useIsMobile';\nimport { isLatexCustomData, type LatexCustomData } from './types';\nimport type { StampHostProps, StampHostHandle } from '../shared/types';\n\nexport const LatexStampHost = forwardRef<StampHostHandle, StampHostProps>(\n function LatexStampHost({ api, editingElement, onClose }, ref) {\n const editorRef = useRef<LatexEditorHandle | null>(null);\n const { isMobile } = useIsMobile();\n const [drawerOpen, setDrawerOpen] = useState(false);\n\n const initial = useMemo(() => {\n if (editingElement && isLatexCustomData(editingElement.customData)) {\n return {\n initialValue: editingElement.customData.src,\n displayMode: !!editingElement.customData.displayMode,\n };\n }\n return { initialValue: '', displayMode: false };\n }, [editingElement]);\n\n const [displayMode, setDisplayMode] = useState(initial.displayMode);\n\n const handleInsert = useCallback(\n async (svgString: string, src: string, dm: boolean) => {\n if (!api) return;\n try {\n await insertStampImage(api, {\n svgString,\n makeCustomData: (): LatexCustomData => ({\n kind: 'latex',\n version: 1,\n src,\n displayMode: dm,\n }),\n editingElementId: editingElement?.id ?? null,\n });\n } catch (err) {\n console.error('Latex insert failed:', err);\n }\n onClose();\n },\n [api, editingElement?.id, onClose],\n );\n\n useImperativeHandle(\n ref,\n () => ({\n tryInsert: () => editorRef.current?.tryInsert() ?? false,\n hasContent: () => editorRef.current?.hasContent() ?? false,\n }),\n [],\n );\n\n return (\n <>\n <LatexLeftPanel\n displayMode={displayMode}\n onDisplayModeChange={setDisplayMode}\n onInsertSnippet={(s) => editorRef.current?.insertAtCursor(s)}\n onClose={onClose}\n isMobile={isMobile}\n drawerOpen={drawerOpen}\n onDrawerClose={() => setDrawerOpen(false)}\n />\n <LatexEditorPopover\n ref={editorRef}\n x={0}\n y={0}\n initialValue={initial.initialValue}\n displayMode={displayMode}\n onDisplayModeChange={setDisplayMode}\n onInsert={handleInsert}\n onClose={onClose}\n withLeftPanel={!isMobile}\n isMobile={isMobile}\n onOpenDrawer={() => setDrawerOpen(true)}\n />\n </>\n );\n },\n);\n"]}
|