@xom11/whiteboard 0.6.4 → 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.
- package/README.md +36 -0
- package/dist/chunk-3SSQKRRO.mjs +58 -0
- package/dist/chunk-3SSQKRRO.mjs.map +1 -0
- package/dist/chunk-7P7SQFOW.mjs +39 -0
- package/dist/chunk-7P7SQFOW.mjs.map +1 -0
- package/dist/chunk-BJX4YNA5.mjs +137 -0
- package/dist/chunk-BJX4YNA5.mjs.map +1 -0
- package/dist/chunk-C6SCVOMC.mjs +111 -0
- package/dist/chunk-C6SCVOMC.mjs.map +1 -0
- package/dist/chunk-DJTBZEAR.mjs +25 -0
- package/dist/chunk-DJTBZEAR.mjs.map +1 -0
- package/dist/chunk-HM7RIXJE.mjs +331 -0
- package/dist/chunk-HM7RIXJE.mjs.map +1 -0
- package/dist/chunk-HTBLO5JO.mjs +41 -0
- package/dist/chunk-HTBLO5JO.mjs.map +1 -0
- package/dist/chunk-HYXFHEDJ.mjs +129 -0
- package/dist/chunk-HYXFHEDJ.mjs.map +1 -0
- package/dist/chunk-LPM4MM45.mjs +211 -0
- package/dist/chunk-LPM4MM45.mjs.map +1 -0
- package/dist/chunk-P2AOIF7S.mjs +40 -0
- package/dist/chunk-P2AOIF7S.mjs.map +1 -0
- package/dist/chunk-SHFOGORM.mjs +44 -0
- package/dist/chunk-SHFOGORM.mjs.map +1 -0
- package/dist/chunk-X5R72SSJ.mjs +52 -0
- package/dist/chunk-X5R72SSJ.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 +3549 -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 +2030 -0
- package/dist/geometry-3d.js.map +1 -0
- package/dist/geometry-3d.mjs +6 -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 +1725 -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-2QGKMGCT.mjs +1066 -0
- package/dist/host-2QGKMGCT.mjs.map +1 -0
- package/dist/host-T2W6R6SO.mjs +2859 -0
- package/dist/host-T2W6R6SO.mjs.map +1 -0
- package/dist/host-XUFON6CQ.mjs +1422 -0
- package/dist/host-XUFON6CQ.mjs.map +1 -0
- package/dist/host-Z3TEJKZA.mjs +466 -0
- package/dist/host-Z3TEJKZA.mjs.map +1 -0
- package/dist/index.d.mts +27 -146
- package/dist/index.d.ts +27 -146
- package/dist/index.js +4694 -4482
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +136 -7179
- 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 +24 -2
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
3
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/stamps/shared/useChordShortcut.ts
|
|
6
|
+
var A_CODE = "a".charCodeAt(0);
|
|
7
|
+
function isFieldFocused() {
|
|
8
|
+
const ae = typeof document !== "undefined" ? document.activeElement : null;
|
|
9
|
+
return !!(ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable));
|
|
10
|
+
}
|
|
11
|
+
function useChordShortcut(args) {
|
|
12
|
+
const { groupOrder, tools, onSelect, enabled } = args;
|
|
13
|
+
const [chordGroup, setChordGroup] = useState(null);
|
|
14
|
+
const groupOrderRef = useRef(groupOrder);
|
|
15
|
+
const toolsRef = useRef(tools);
|
|
16
|
+
const onSelectRef = useRef(onSelect);
|
|
17
|
+
const chordGroupRef = useRef(null);
|
|
18
|
+
groupOrderRef.current = groupOrder;
|
|
19
|
+
toolsRef.current = tools;
|
|
20
|
+
onSelectRef.current = onSelect;
|
|
21
|
+
const cancel = useCallback(() => {
|
|
22
|
+
chordGroupRef.current = null;
|
|
23
|
+
setChordGroup(null);
|
|
24
|
+
}, []);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!enabled) return;
|
|
27
|
+
const setChord = (next) => {
|
|
28
|
+
chordGroupRef.current = next;
|
|
29
|
+
setChordGroup(next);
|
|
30
|
+
};
|
|
31
|
+
const onKey = (e) => {
|
|
32
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
33
|
+
if (isFieldFocused()) return;
|
|
34
|
+
const key = e.key;
|
|
35
|
+
const lower = key.length === 1 ? key.toLowerCase() : key;
|
|
36
|
+
if (key === "Escape") {
|
|
37
|
+
if (chordGroupRef.current !== null) {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
e.stopPropagation();
|
|
40
|
+
setChord(null);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (lower.length === 1 && lower >= "a" && lower <= "z") {
|
|
45
|
+
const idx = lower.charCodeAt(0) - A_CODE;
|
|
46
|
+
if (idx < groupOrderRef.current.length) {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
e.stopPropagation();
|
|
49
|
+
setChord(groupOrderRef.current[idx]);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (key >= "1" && key <= "9") {
|
|
54
|
+
const active = chordGroupRef.current;
|
|
55
|
+
if (active === null) return;
|
|
56
|
+
const n = key.charCodeAt(0) - "1".charCodeAt(0);
|
|
57
|
+
const toolsInGroup = toolsRef.current.filter(
|
|
58
|
+
(t) => t.group === active
|
|
59
|
+
);
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
e.stopPropagation();
|
|
62
|
+
if (n < toolsInGroup.length) {
|
|
63
|
+
onSelectRef.current(toolsInGroup[n].key);
|
|
64
|
+
}
|
|
65
|
+
setChord(null);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
window.addEventListener("keydown", onKey, { capture: true });
|
|
70
|
+
return () => {
|
|
71
|
+
window.removeEventListener("keydown", onKey, { capture: true });
|
|
72
|
+
};
|
|
73
|
+
}, [enabled]);
|
|
74
|
+
return { chordGroup, cancel };
|
|
75
|
+
}
|
|
76
|
+
function MobileToolDrawer({
|
|
77
|
+
title,
|
|
78
|
+
headerIcon,
|
|
79
|
+
chips,
|
|
80
|
+
actions,
|
|
81
|
+
groups,
|
|
82
|
+
activeTool,
|
|
83
|
+
onToolSelect,
|
|
84
|
+
drawerOpen,
|
|
85
|
+
onDrawerClose,
|
|
86
|
+
isDark,
|
|
87
|
+
testId
|
|
88
|
+
}) {
|
|
89
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
90
|
+
drawerOpen && /* @__PURE__ */ jsx(
|
|
91
|
+
"div",
|
|
92
|
+
{
|
|
93
|
+
className: "stamp-drawer-backdrop",
|
|
94
|
+
onPointerDown: onDrawerClose,
|
|
95
|
+
"aria-hidden": "true"
|
|
96
|
+
}
|
|
97
|
+
),
|
|
98
|
+
/* @__PURE__ */ jsxs(
|
|
99
|
+
"aside",
|
|
100
|
+
{
|
|
101
|
+
role: "complementary",
|
|
102
|
+
"aria-label": title,
|
|
103
|
+
"aria-hidden": !drawerOpen ? "true" : void 0,
|
|
104
|
+
"data-testid": testId,
|
|
105
|
+
"data-stamp-area": "true",
|
|
106
|
+
"data-mobile-drawer": "true",
|
|
107
|
+
"data-geo-mobile": "true",
|
|
108
|
+
"data-drawer-state": drawerOpen ? "open" : "closed",
|
|
109
|
+
className: [
|
|
110
|
+
isDark ? "theme--dark " : "",
|
|
111
|
+
"stamp-drawer-mobile flex flex-col border-r border-slate-200 bg-white shadow-md"
|
|
112
|
+
].join(""),
|
|
113
|
+
children: [
|
|
114
|
+
/* @__PURE__ */ jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-4 py-3", children: [
|
|
115
|
+
/* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 text-base font-semibold text-slate-800", children: [
|
|
116
|
+
/* @__PURE__ */ jsx("span", { className: "inline-flex h-7 w-7 items-center justify-center rounded-lg bg-emerald-50 text-emerald-700", children: headerIcon }),
|
|
117
|
+
title
|
|
118
|
+
] }),
|
|
119
|
+
/* @__PURE__ */ jsx(
|
|
120
|
+
"button",
|
|
121
|
+
{
|
|
122
|
+
type: "button",
|
|
123
|
+
onClick: onDrawerClose,
|
|
124
|
+
"aria-label": "\u0110\xF3ng ng\u0103n c\xF4ng c\u1EE5",
|
|
125
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
126
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
127
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
128
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
129
|
+
] })
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
] }),
|
|
133
|
+
/* @__PURE__ */ jsxs("div", { className: "sticky top-0 z-10 flex items-center gap-2 border-b border-slate-200 bg-white/95 px-3 py-2 backdrop-blur", children: [
|
|
134
|
+
chips.map((c) => /* @__PURE__ */ jsxs(
|
|
135
|
+
"button",
|
|
136
|
+
{
|
|
137
|
+
type: "button",
|
|
138
|
+
role: "switch",
|
|
139
|
+
"aria-pressed": c.pressed,
|
|
140
|
+
"aria-label": c.label,
|
|
141
|
+
"data-testid": c.testId,
|
|
142
|
+
onClick: () => c.onToggle(!c.pressed),
|
|
143
|
+
className: "geo-mobile-chip",
|
|
144
|
+
children: [
|
|
145
|
+
c.icon,
|
|
146
|
+
c.label
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
c.label
|
|
150
|
+
)),
|
|
151
|
+
actions.length > 0 && /* @__PURE__ */ jsx("div", { className: "ml-auto flex items-center gap-1", children: actions.map((a) => /* @__PURE__ */ jsx(
|
|
152
|
+
"button",
|
|
153
|
+
{
|
|
154
|
+
type: "button",
|
|
155
|
+
onClick: a.onClick,
|
|
156
|
+
disabled: a.disabled,
|
|
157
|
+
"aria-label": a.label,
|
|
158
|
+
title: a.title ?? a.label,
|
|
159
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
|
|
160
|
+
children: a.icon
|
|
161
|
+
},
|
|
162
|
+
a.label
|
|
163
|
+
)) })
|
|
164
|
+
] }),
|
|
165
|
+
/* @__PURE__ */ jsx(
|
|
166
|
+
"div",
|
|
167
|
+
{
|
|
168
|
+
className: "min-h-0 flex-1 overflow-y-auto",
|
|
169
|
+
style: { paddingBottom: "calc(0.75rem + env(safe-area-inset-bottom))" },
|
|
170
|
+
children: groups.map((g) => /* @__PURE__ */ jsxs("section", { className: "px-3 pt-3 pb-1", children: [
|
|
171
|
+
/* @__PURE__ */ jsxs("h4", { className: "mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: [
|
|
172
|
+
/* @__PURE__ */ jsx("span", { className: "h-1 w-1 rounded-full bg-emerald-500" }),
|
|
173
|
+
g.groupLabel
|
|
174
|
+
] }),
|
|
175
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-2", children: g.tools.map((t) => {
|
|
176
|
+
const active = activeTool === t.key;
|
|
177
|
+
return /* @__PURE__ */ jsxs(
|
|
178
|
+
"button",
|
|
179
|
+
{
|
|
180
|
+
type: "button",
|
|
181
|
+
"aria-label": t.label,
|
|
182
|
+
"aria-pressed": active,
|
|
183
|
+
"data-tool": t.key,
|
|
184
|
+
onClick: () => {
|
|
185
|
+
onToolSelect(t.key);
|
|
186
|
+
onDrawerClose();
|
|
187
|
+
},
|
|
188
|
+
className: [
|
|
189
|
+
"flex flex-col items-center justify-center gap-1.5 rounded-2xl px-2 py-3 transition active:scale-95",
|
|
190
|
+
active ? "geo-mobile-tool-active" : "bg-slate-50 text-slate-700 hover:bg-slate-100"
|
|
191
|
+
].join(" "),
|
|
192
|
+
children: [
|
|
193
|
+
/* @__PURE__ */ jsx("span", { className: "flex h-6 w-6 items-center justify-center", children: t.icon }),
|
|
194
|
+
/* @__PURE__ */ jsx("span", { className: "text-center text-[11px] font-medium leading-tight line-clamp-2", children: t.label })
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
t.key
|
|
198
|
+
);
|
|
199
|
+
}) })
|
|
200
|
+
] }, g.group))
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
] });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export { MobileToolDrawer, useChordShortcut };
|
|
210
|
+
//# sourceMappingURL=chunk-LPM4MM45.mjs.map
|
|
211
|
+
//# sourceMappingURL=chunk-LPM4MM45.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stamps/shared/useChordShortcut.ts","../src/stamps/shared/MobileToolDrawer.tsx"],"names":[],"mappings":"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","file":"chunk-LPM4MM45.mjs","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\ninterface UseChordShortcutArgs<G extends string> {\n groupOrder: readonly G[];\n tools: ReadonlyArray<{ key: string; group: G }>;\n onSelect: (toolKey: string) => void;\n enabled: boolean;\n}\n\ninterface UseChordShortcutResult<G extends string> {\n chordGroup: G | null;\n cancel: () => void;\n}\n\nconst A_CODE = 'a'.charCodeAt(0);\n\nfunction isFieldFocused(): boolean {\n const ae = (typeof document !== 'undefined'\n ? (document.activeElement as HTMLElement | null)\n : null);\n return !!(\n ae &&\n (ae.tagName === 'INPUT' ||\n ae.tagName === 'TEXTAREA' ||\n ae.isContentEditable)\n );\n}\n\nexport function useChordShortcut<G extends string>(\n args: UseChordShortcutArgs<G>,\n): UseChordShortcutResult<G> {\n const { groupOrder, tools, onSelect, enabled } = args;\n\n const [chordGroup, setChordGroup] = useState<G | null>(null);\n\n const groupOrderRef = useRef(groupOrder);\n const toolsRef = useRef(tools);\n const onSelectRef = useRef(onSelect);\n const chordGroupRef = useRef<G | null>(null);\n\n groupOrderRef.current = groupOrder;\n toolsRef.current = tools;\n onSelectRef.current = onSelect;\n // chordGroupRef được sync ngay trong handler (xem `setChord` dưới đây)\n // thay vì ghi từ render body — nếu ghi ở body sẽ bị React batch hoá khi\n // hai event xảy ra trong cùng một act() (event sau đọc giá trị cũ).\n\n const cancel = useCallback(() => {\n chordGroupRef.current = null;\n setChordGroup(null);\n }, []);\n\n useEffect(() => {\n if (!enabled) return;\n\n const setChord = (next: G | null) => {\n chordGroupRef.current = next;\n setChordGroup(next);\n };\n\n const onKey = (e: KeyboardEvent) => {\n if (e.metaKey || e.ctrlKey || e.altKey) return;\n if (isFieldFocused()) return;\n\n const key = e.key;\n const lower = key.length === 1 ? key.toLowerCase() : key;\n\n if (key === 'Escape') {\n if (chordGroupRef.current !== null) {\n e.preventDefault();\n e.stopPropagation();\n setChord(null);\n }\n return;\n }\n\n if (lower.length === 1 && lower >= 'a' && lower <= 'z') {\n const idx = lower.charCodeAt(0) - A_CODE;\n if (idx < groupOrderRef.current.length) {\n e.preventDefault();\n e.stopPropagation();\n setChord(groupOrderRef.current[idx]);\n }\n return;\n }\n\n if (key >= '1' && key <= '9') {\n const active = chordGroupRef.current;\n if (active === null) return;\n const n = key.charCodeAt(0) - '1'.charCodeAt(0); // 0-indexed\n const toolsInGroup = toolsRef.current.filter(\n (t) => t.group === active,\n );\n e.preventDefault();\n e.stopPropagation();\n if (n < toolsInGroup.length) {\n onSelectRef.current(toolsInGroup[n].key);\n }\n setChord(null);\n return;\n }\n };\n\n window.addEventListener('keydown', onKey, { capture: true });\n return () => {\n window.removeEventListener('keydown', onKey, { capture: true });\n };\n }, [enabled]);\n\n return { chordGroup, cancel };\n}\n","'use client';\n\nimport React from 'react';\n\n/**\n * Generic mobile tool drawer dùng chung cho geometry-2d + geometry-3d.\n *\n * Layout:\n * - Header: icon + title + close\n * - Sticky toolbar: chip switches (Trục/Lưới) + icon-actions (Reset, Undo)\n * - Body: section dọc, mỗi section là 1 nhóm tools, grid 3-col card có nhãn\n *\n * Style: soft-modern, emerald accent, khớp các class trong shared/stamp.css.\n */\n\nexport interface MobileChip {\n /** Label hiển thị + aria-label */\n label: string;\n icon: React.ReactNode;\n pressed: boolean;\n onToggle: (next: boolean) => void;\n /** data-testid optional (để test cũ chạy được) */\n testId?: string;\n}\n\nexport interface MobileActionButton {\n label: string;\n icon: React.ReactNode;\n onClick: () => void;\n disabled?: boolean;\n title?: string;\n}\n\nexport interface MobileTool<TKey extends string> {\n key: TKey;\n label: string;\n icon: React.ReactNode;\n}\n\nexport interface MobileToolGroup<TKey extends string, TGroup extends string> {\n group: TGroup;\n groupLabel: string;\n tools: MobileTool<TKey>[];\n}\n\ninterface MobileToolDrawerProps<TKey extends string, TGroup extends string> {\n title: string;\n headerIcon: React.ReactNode;\n chips: MobileChip[];\n actions: MobileActionButton[];\n groups: MobileToolGroup<TKey, TGroup>[];\n activeTool: TKey;\n onToolSelect: (key: TKey) => void;\n drawerOpen: boolean;\n onDrawerClose: () => void;\n isDark?: boolean;\n /** data-testid trên <aside> — giữ để test cũ tìm được panel */\n testId?: string;\n}\n\nexport function MobileToolDrawer<TKey extends string, TGroup extends string>({\n title,\n headerIcon,\n chips,\n actions,\n groups,\n activeTool,\n onToolSelect,\n drawerOpen,\n onDrawerClose,\n isDark,\n testId,\n}: MobileToolDrawerProps<TKey, TGroup>) {\n return (\n <>\n {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={!drawerOpen ? 'true' : undefined}\n data-testid={testId}\n data-stamp-area=\"true\"\n data-mobile-drawer=\"true\"\n data-geo-mobile=\"true\"\n data-drawer-state={drawerOpen ? 'open' : 'closed'}\n className={[\n isDark ? 'theme--dark ' : '',\n 'stamp-drawer-mobile flex flex-col border-r border-slate-200 bg-white shadow-md',\n ].join('')}\n >\n {/* Header */}\n <header className=\"flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-4 py-3\">\n <h3 className=\"flex items-center gap-2 text-base font-semibold text-slate-800\">\n <span className=\"inline-flex h-7 w-7 items-center justify-center rounded-lg bg-emerald-50 text-emerald-700\">\n {headerIcon}\n </span>\n {title}\n </h3>\n <button\n type=\"button\"\n onClick={onDrawerClose}\n aria-label=\"Đóng ngăn công cụ\"\n className=\"inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-500 transition hover:bg-slate-100 hover:text-slate-800\"\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\n {/* Sticky toolbar: chips + actions */}\n <div className=\"sticky top-0 z-10 flex items-center gap-2 border-b border-slate-200 bg-white/95 px-3 py-2 backdrop-blur\">\n {chips.map((c) => (\n <button\n key={c.label}\n type=\"button\"\n role=\"switch\"\n aria-pressed={c.pressed}\n aria-label={c.label}\n data-testid={c.testId}\n onClick={() => c.onToggle(!c.pressed)}\n className=\"geo-mobile-chip\"\n >\n {c.icon}\n {c.label}\n </button>\n ))}\n {actions.length > 0 && <div className=\"ml-auto flex items-center gap-1\">\n {actions.map((a) => (\n <button\n key={a.label}\n type=\"button\"\n onClick={a.onClick}\n disabled={a.disabled}\n aria-label={a.label}\n title={a.title ?? a.label}\n className=\"inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent\"\n >\n {a.icon}\n </button>\n ))}\n </div>}\n </div>\n\n {/* Body: groups xếp dọc */}\n <div\n className=\"min-h-0 flex-1 overflow-y-auto\"\n style={{ paddingBottom: 'calc(0.75rem + env(safe-area-inset-bottom))' }}\n >\n {groups.map((g) => (\n <section key={g.group} className=\"px-3 pt-3 pb-1\">\n <h4 className=\"mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500\">\n <span className=\"h-1 w-1 rounded-full bg-emerald-500\" />\n {g.groupLabel}\n </h4>\n <div className=\"grid grid-cols-3 gap-2\">\n {g.tools.map((t) => {\n const active = activeTool === t.key;\n return (\n <button\n key={t.key}\n type=\"button\"\n aria-label={t.label}\n aria-pressed={active}\n data-tool={t.key}\n onClick={() => {\n onToolSelect(t.key);\n onDrawerClose();\n }}\n className={[\n 'flex flex-col items-center justify-center gap-1.5 rounded-2xl px-2 py-3 transition active:scale-95',\n active\n ? 'geo-mobile-tool-active'\n : 'bg-slate-50 text-slate-700 hover:bg-slate-100',\n ].join(' ')}\n >\n <span className=\"flex h-6 w-6 items-center justify-center\">{t.icon}</span>\n <span className=\"text-center text-[11px] font-medium leading-tight line-clamp-2\">\n {t.label}\n </span>\n </button>\n );\n })}\n </div>\n </section>\n ))}\n </div>\n </aside>\n </>\n );\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
// src/stamps/shared/useIsMobile.ts
|
|
5
|
+
var MOBILE_QUERY = "(max-width: 768px)";
|
|
6
|
+
var NO_HOVER_QUERY = "(hover: none)";
|
|
7
|
+
function readMatch(query) {
|
|
8
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
9
|
+
try {
|
|
10
|
+
return window.matchMedia(query).matches;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function useIsMobile() {
|
|
16
|
+
const [state, setState] = useState(() => ({
|
|
17
|
+
isMobile: readMatch(MOBILE_QUERY),
|
|
18
|
+
isTouchOnly: readMatch(NO_HOVER_QUERY)
|
|
19
|
+
}));
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
22
|
+
const mql = window.matchMedia(MOBILE_QUERY);
|
|
23
|
+
const tql = window.matchMedia(NO_HOVER_QUERY);
|
|
24
|
+
const update = () => {
|
|
25
|
+
setState({ isMobile: mql.matches, isTouchOnly: tql.matches });
|
|
26
|
+
};
|
|
27
|
+
update();
|
|
28
|
+
mql.addEventListener("change", update);
|
|
29
|
+
tql.addEventListener("change", update);
|
|
30
|
+
return () => {
|
|
31
|
+
mql.removeEventListener("change", update);
|
|
32
|
+
tql.removeEventListener("change", update);
|
|
33
|
+
};
|
|
34
|
+
}, []);
|
|
35
|
+
return state;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { useIsMobile };
|
|
39
|
+
//# sourceMappingURL=chunk-P2AOIF7S.mjs.map
|
|
40
|
+
//# sourceMappingURL=chunk-P2AOIF7S.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stamps/shared/useIsMobile.ts"],"names":[],"mappings":";;;AAIA,IAAM,YAAA,GAAe,oBAAA;AACrB,IAAM,cAAA,GAAiB,eAAA;AASvB,SAAS,UAAU,KAAA,EAAwB;AACzC,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,CAAC,MAAA,CAAO,YAAY,OAAO,KAAA;AAChE,EAAA,IAAI;AACF,IAAA,OAAO,MAAA,CAAO,UAAA,CAAW,KAAK,CAAA,CAAE,OAAA;AAAA,EAClC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAMO,SAAS,WAAA,GAA2B;AACzC,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAAsB,OAAO;AAAA,IACrD,QAAA,EAAU,UAAU,YAAY,CAAA;AAAA,IAChC,WAAA,EAAa,UAAU,cAAc;AAAA,GACvC,CAAE,CAAA;AAEF,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,CAAC,OAAO,UAAA,EAAY;AACzD,IAAA,MAAM,GAAA,GAAM,MAAA,CAAO,UAAA,CAAW,YAAY,CAAA;AAC1C,IAAA,MAAM,GAAA,GAAM,MAAA,CAAO,UAAA,CAAW,cAAc,CAAA;AAC5C,IAAA,MAAM,SAAS,MAAM;AACnB,MAAA,QAAA,CAAS,EAAE,QAAA,EAAU,GAAA,CAAI,SAAS,WAAA,EAAa,GAAA,CAAI,SAAS,CAAA;AAAA,IAC9D,CAAA;AACA,IAAA,MAAA,EAAO;AACP,IAAA,GAAA,CAAI,gBAAA,CAAiB,UAAU,MAAM,CAAA;AACrC,IAAA,GAAA,CAAI,gBAAA,CAAiB,UAAU,MAAM,CAAA;AACrC,IAAA,OAAO,MAAM;AACX,MAAA,GAAA,CAAI,mBAAA,CAAoB,UAAU,MAAM,CAAA;AACxC,MAAA,GAAA,CAAI,mBAAA,CAAoB,UAAU,MAAM,CAAA;AAAA,IAC1C,CAAA;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,OAAO,KAAA;AACT","file":"chunk-P2AOIF7S.mjs","sourcesContent":["'use client';\n\nimport { useEffect, useState } from 'react';\n\nconst MOBILE_QUERY = '(max-width: 768px)';\nconst NO_HOVER_QUERY = '(hover: none)';\n\nexport interface MobileState {\n /** Viewport ≤ 768px — dùng để chuyển full-screen modal + drawer. */\n isMobile: boolean;\n /** Device không có hover (touch-only) — dùng để ẩn hover tooltips. */\n isTouchOnly: boolean;\n}\n\nfunction readMatch(query: string): boolean {\n if (typeof window === 'undefined' || !window.matchMedia) return false;\n try {\n return window.matchMedia(query).matches;\n } catch {\n return false;\n }\n}\n\n/**\n * SSR-safe mobile detection qua matchMedia. Trả về `{ isMobile, isTouchOnly }`.\n * Re-render khi viewport resize hoặc input modality đổi (e.g., gắn Bluetooth mouse).\n */\nexport function useIsMobile(): MobileState {\n const [state, setState] = useState<MobileState>(() => ({\n isMobile: readMatch(MOBILE_QUERY),\n isTouchOnly: readMatch(NO_HOVER_QUERY),\n }));\n\n useEffect(() => {\n if (typeof window === 'undefined' || !window.matchMedia) return;\n const mql = window.matchMedia(MOBILE_QUERY);\n const tql = window.matchMedia(NO_HOVER_QUERY);\n const update = () => {\n setState({ isMobile: mql.matches, isTouchOnly: tql.matches });\n };\n update();\n mql.addEventListener('change', update);\n tql.addEventListener('change', update);\n return () => {\n mql.removeEventListener('change', update);\n tql.removeEventListener('change', update);\n };\n }, []);\n\n return state;\n}\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { isGeometryCustomData, renderGeometrySvgFromState } from './chunk-BJX4YNA5.mjs';
|
|
3
|
+
import { lazy } from 'react';
|
|
4
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
var GeometryStampHost = lazy(
|
|
7
|
+
() => import('./host-T2W6R6SO.mjs').then((m) => ({ default: m.GeometryStampHost }))
|
|
8
|
+
);
|
|
9
|
+
var GeometryIcon = /* @__PURE__ */ 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: [
|
|
10
|
+
/* @__PURE__ */ jsx("polygon", { points: "4,20 20,20 12,5" }),
|
|
11
|
+
/* @__PURE__ */ jsx("circle", { cx: "4", cy: "20", r: "1.4", fill: "currentColor", stroke: "none" }),
|
|
12
|
+
/* @__PURE__ */ jsx("circle", { cx: "20", cy: "20", r: "1.4", fill: "currentColor", stroke: "none" }),
|
|
13
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "5", r: "1.4", fill: "currentColor", stroke: "none" })
|
|
14
|
+
] });
|
|
15
|
+
var geometryStamp = {
|
|
16
|
+
kind: "geometry",
|
|
17
|
+
shortcutKey: "g",
|
|
18
|
+
toolbarLabel: "G",
|
|
19
|
+
toolbarTitle: "Ch\xE8n h\xECnh h\u1ECDc (G)",
|
|
20
|
+
toolbarIcon: GeometryIcon,
|
|
21
|
+
toolbarTestId: "stamp-toolbar-geometry",
|
|
22
|
+
matchesCustomData: isGeometryCustomData,
|
|
23
|
+
async renderSvgFromCustomData(data) {
|
|
24
|
+
if (!isGeometryCustomData(data)) {
|
|
25
|
+
throw new Error("geometryStamp.renderSvgFromCustomData: customData kh\xF4ng ph\u1EA3i geometry");
|
|
26
|
+
}
|
|
27
|
+
return renderGeometrySvgFromState(data.jsonState);
|
|
28
|
+
},
|
|
29
|
+
async restoreFileFromCustomData(element) {
|
|
30
|
+
const data = element.customData;
|
|
31
|
+
const fileId = element.fileId;
|
|
32
|
+
if (!data || !fileId) return null;
|
|
33
|
+
if (!isGeometryCustomData(data)) return null;
|
|
34
|
+
const svgString = await renderGeometrySvgFromState(data.jsonState);
|
|
35
|
+
const utf8 = unescape(encodeURIComponent(svgString));
|
|
36
|
+
const dataURL = "data:image/svg+xml;base64," + (typeof btoa !== "undefined" ? btoa(utf8) : Buffer.from(utf8).toString("base64"));
|
|
37
|
+
return { fileId, dataURL, mimeType: "image/svg+xml" };
|
|
38
|
+
},
|
|
39
|
+
Host: GeometryStampHost
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export { geometryStamp };
|
|
43
|
+
//# sourceMappingURL=chunk-SHFOGORM.mjs.map
|
|
44
|
+
//# sourceMappingURL=chunk-SHFOGORM.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stamps/geometry-2d/index.tsx"],"names":[],"mappings":";;;;AAgBA,IAAM,iBAAA,GAAoB,IAAA;AAAA,EAAK,MAC7B,OAAO,qBAAQ,CAAA,CAAE,IAAA,CAAK,CAAC,CAAA,MAAO,EAAE,OAAA,EAAS,CAAA,CAAE,iBAAA,EAAkB,CAAE;AACjE,CAAA;AAEA,IAAM,YAAA,wBACH,KAAA,EAAA,EAAI,KAAA,EAAM,MAAK,MAAA,EAAO,IAAA,EAAK,SAAQ,WAAA,EAAY,IAAA,EAAK,QAAO,MAAA,EAAO,cAAA,EAAe,aAAY,KAAA,EAAM,aAAA,EAAc,SAAQ,cAAA,EAAe,OAAA,EAAQ,eAAY,MAAA,EAC3J,QAAA,EAAA;AAAA,kBAAA,GAAA,CAAC,SAAA,EAAA,EAAQ,QAAO,iBAAA,EAAkB,CAAA;AAAA,kBAClC,GAAA,CAAC,QAAA,EAAA,EAAO,EAAA,EAAG,GAAA,EAAI,EAAA,EAAG,IAAA,EAAK,CAAA,EAAE,KAAA,EAAM,IAAA,EAAK,cAAA,EAAe,MAAA,EAAO,MAAA,EAAO,CAAA;AAAA,kBACjE,GAAA,CAAC,QAAA,EAAA,EAAO,EAAA,EAAG,IAAA,EAAK,EAAA,EAAG,IAAA,EAAK,CAAA,EAAE,KAAA,EAAM,IAAA,EAAK,cAAA,EAAe,MAAA,EAAO,MAAA,EAAO,CAAA;AAAA,kBAClE,GAAA,CAAC,QAAA,EAAA,EAAO,EAAA,EAAG,IAAA,EAAK,EAAA,EAAG,GAAA,EAAI,CAAA,EAAE,KAAA,EAAM,IAAA,EAAK,cAAA,EAAe,MAAA,EAAO,MAAA,EAAO;AAAA,CAAA,EACnE,CAAA;AAGK,IAAM,aAAA,GAA2B;AAAA,EACtC,IAAA,EAAM,UAAA;AAAA,EACN,WAAA,EAAa,GAAA;AAAA,EACb,YAAA,EAAc,GAAA;AAAA,EACd,YAAA,EAAc,8BAAA;AAAA,EACd,WAAA,EAAa,YAAA;AAAA,EACb,aAAA,EAAe,wBAAA;AAAA,EACf,iBAAA,EAAmB,oBAAA;AAAA,EACnB,MAAM,wBAAwB,IAAA,EAAM;AAClC,IAAA,IAAI,CAAC,oBAAA,CAAqB,IAAI,CAAA,EAAG;AAC/B,MAAA,MAAM,IAAI,MAAM,+EAAuE,CAAA;AAAA,IACzF;AACA,IAAA,OAAO,0BAAA,CAA2B,KAAK,SAAS,CAAA;AAAA,EAClD,CAAA;AAAA,EACA,MAAM,0BAA0B,OAAA,EAA4C;AAC1E,IAAA,MAAM,OAAO,OAAA,CAAQ,UAAA;AACrB,IAAA,MAAM,SAAU,OAAA,CAAuC,MAAA;AACvD,IAAA,IAAI,CAAC,IAAA,IAAQ,CAAC,MAAA,EAAQ,OAAO,IAAA;AAC7B,IAAA,IAAI,CAAC,oBAAA,CAAqB,IAAI,CAAA,EAAG,OAAO,IAAA;AACxC,IAAA,MAAM,SAAA,GAAY,MAAM,0BAAA,CAA2B,IAAA,CAAK,SAAS,CAAA;AACjE,IAAA,MAAM,IAAA,GAAO,QAAA,CAAS,kBAAA,CAAmB,SAAS,CAAC,CAAA;AACnD,IAAA,MAAM,OAAA,GAAU,4BAAA,IACd,OAAO,IAAA,KAAS,WAAA,GAAc,IAAA,CAAK,IAAI,CAAA,GAAI,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,CAAE,SAAS,QAAQ,CAAA,CAAA;AAEhF,IAAA,OAAO,EAAE,MAAA,EAAQ,OAAA,EAAS,QAAA,EAAU,eAAA,EAAgB;AAAA,EACtD,CAAA;AAAA,EACA,IAAA,EAAM;AACR","file":"chunk-SHFOGORM.mjs","sourcesContent":["'use client';\n\nimport { lazy, type ReactNode } from 'react';\nimport { renderGeometrySvgFromState } from './render';\nimport type {\n RestoredStampFile,\n StampType,\n} from '../shared/types';\nimport {\n isGeometryCustomData,\n type GeometryCustomData,\n} from './types';\n\nexport { isGeometryCustomData };\nexport type { GeometryCustomData };\n\nconst GeometryStampHost = lazy(() =>\n import('./host').then((m) => ({ default: m.GeometryStampHost })),\n);\n\nconst GeometryIcon: ReactNode = (\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <polygon points=\"4,20 20,20 12,5\" />\n <circle cx=\"4\" cy=\"20\" r=\"1.4\" fill=\"currentColor\" stroke=\"none\" />\n <circle cx=\"20\" cy=\"20\" r=\"1.4\" fill=\"currentColor\" stroke=\"none\" />\n <circle cx=\"12\" cy=\"5\" r=\"1.4\" fill=\"currentColor\" stroke=\"none\" />\n </svg>\n);\n\nexport const geometryStamp: StampType = {\n kind: 'geometry',\n shortcutKey: 'g',\n toolbarLabel: 'G',\n toolbarTitle: 'Chèn hình học (G)',\n toolbarIcon: GeometryIcon,\n toolbarTestId: 'stamp-toolbar-geometry',\n matchesCustomData: isGeometryCustomData,\n async renderSvgFromCustomData(data) {\n if (!isGeometryCustomData(data)) {\n throw new Error('geometryStamp.renderSvgFromCustomData: customData không phải geometry');\n }\n return renderGeometrySvgFromState(data.jsonState);\n },\n async restoreFileFromCustomData(element): Promise<RestoredStampFile | null> {\n const data = element.customData as GeometryCustomData | undefined;\n const fileId = (element as { fileId?: string | null }).fileId;\n if (!data || !fileId) return null;\n if (!isGeometryCustomData(data)) return null;\n const svgString = await renderGeometrySvgFromState(data.jsonState);\n const utf8 = unescape(encodeURIComponent(svgString));\n const dataURL = 'data:image/svg+xml;base64,' + (\n typeof btoa !== 'undefined' ? btoa(utf8) : Buffer.from(utf8).toString('base64')\n );\n return { fileId, dataURL, mimeType: 'image/svg+xml' };\n },\n Host: GeometryStampHost,\n};\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// src/stamps/latex/render.ts
|
|
3
|
+
var cachedCss = null;
|
|
4
|
+
function absoluteOrigin() {
|
|
5
|
+
if (typeof window !== "undefined" && window.location) return window.location.origin;
|
|
6
|
+
return "";
|
|
7
|
+
}
|
|
8
|
+
async function loadKatexCss() {
|
|
9
|
+
if (cachedCss !== null) return cachedCss;
|
|
10
|
+
try {
|
|
11
|
+
if (typeof fetch === "function") {
|
|
12
|
+
const res = await fetch("/katex.min.css");
|
|
13
|
+
if (res.ok) {
|
|
14
|
+
let css = await res.text();
|
|
15
|
+
const origin = absoluteOrigin();
|
|
16
|
+
if (origin) {
|
|
17
|
+
css = css.replace(/url\((['"]?)(fonts\/)/g, `url($1${origin}/$2`);
|
|
18
|
+
}
|
|
19
|
+
cachedCss = css;
|
|
20
|
+
return css;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
cachedCss = "";
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
async function renderLatexToSvg(src, displayMode) {
|
|
29
|
+
const katex = await import('katex');
|
|
30
|
+
const html = katex.default.renderToString(src, { displayMode, throwOnError: true, output: "html" });
|
|
31
|
+
const measureDiv = document.createElement("div");
|
|
32
|
+
measureDiv.style.cssText = "position:absolute;top:-9999px;left:-9999px;visibility:hidden;display:inline-block;";
|
|
33
|
+
measureDiv.innerHTML = html;
|
|
34
|
+
document.body.appendChild(measureDiv);
|
|
35
|
+
const rect = measureDiv.getBoundingClientRect();
|
|
36
|
+
const width = Math.ceil(rect.width) || 50;
|
|
37
|
+
const height = Math.ceil(rect.height) || 20;
|
|
38
|
+
document.body.removeChild(measureDiv);
|
|
39
|
+
const cssText = await loadKatexCss();
|
|
40
|
+
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>";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/stamps/latex/types.ts
|
|
44
|
+
function isLatexCustomData(data) {
|
|
45
|
+
if (!data || typeof data !== "object") return false;
|
|
46
|
+
const d = data;
|
|
47
|
+
return d.kind === "latex" && d.version === 1 && typeof d.src === "string";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { isLatexCustomData, renderLatexToSvg };
|
|
51
|
+
//# sourceMappingURL=chunk-X5R72SSJ.mjs.map
|
|
52
|
+
//# sourceMappingURL=chunk-X5R72SSJ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stamps/latex/render.ts","../src/stamps/latex/types.ts"],"names":[],"mappings":";AAAA,IAAI,SAAA,GAA2B,IAAA;AAI/B,SAAS,cAAA,GAAyB;AAChC,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,QAAA,EAAU,OAAO,OAAO,QAAA,CAAS,MAAA;AAC7E,EAAA,OAAO,EAAA;AACT;AAEA,eAAe,YAAA,GAAgC;AAC7C,EAAA,IAAI,SAAA,KAAc,MAAM,OAAO,SAAA;AAC/B,EAAA,IAAI;AACF,IAAA,IAAI,OAAO,UAAU,UAAA,EAAY;AAC/B,MAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,gBAAgB,CAAA;AACxC,MAAA,IAAI,IAAI,EAAA,EAAI;AACV,QAAA,IAAI,GAAA,GAAM,MAAM,GAAA,CAAI,IAAA,EAAK;AAKzB,QAAA,MAAM,SAAS,cAAA,EAAe;AAC9B,QAAA,IAAI,MAAA,EAAQ;AACV,UAAA,GAAA,GAAM,GAAA,CAAI,OAAA,CAAQ,wBAAA,EAA0B,CAAA,MAAA,EAAS,MAAM,CAAA,GAAA,CAAK,CAAA;AAAA,QAClE;AACA,QAAA,SAAA,GAAY,GAAA;AACZ,QAAA,OAAO,GAAA;AAAA,MACT;AAAA,IACF;AAAA,EACF,CAAA,CAAA,MAAQ;AAAA,EAER;AACA,EAAA,SAAA,GAAY,EAAA;AACZ,EAAA,OAAO,EAAA;AACT;AAEA,eAAsB,gBAAA,CAAiB,KAAa,WAAA,EAAuC;AACzF,EAAA,MAAM,KAAA,GAAQ,MAAM,OAAO,OAAO,CAAA;AAClC,EAAA,MAAM,IAAA,GAAO,KAAA,CAAM,OAAA,CAAQ,cAAA,CAAe,GAAA,EAAK,EAAE,WAAA,EAAa,YAAA,EAAc,IAAA,EAAM,MAAA,EAAQ,MAAA,EAAQ,CAAA;AAElG,EAAA,MAAM,UAAA,GAAa,QAAA,CAAS,aAAA,CAAc,KAAK,CAAA;AAC/C,EAAA,UAAA,CAAW,MAAM,OAAA,GAAU,oFAAA;AAC3B,EAAA,UAAA,CAAW,SAAA,GAAY,IAAA;AACvB,EAAA,QAAA,CAAS,IAAA,CAAK,YAAY,UAAU,CAAA;AACpC,EAAA,MAAM,IAAA,GAAO,WAAW,qBAAA,EAAsB;AAC9C,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,KAAK,CAAA,IAAK,EAAA;AACvC,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,MAAM,CAAA,IAAK,EAAA;AACzC,EAAA,QAAA,CAAS,IAAA,CAAK,YAAY,UAAU,CAAA;AAEpC,EAAA,MAAM,OAAA,GAAU,MAAM,YAAA,EAAa;AAKnC,EAAA,OAAO,iDAAA,GAAoD,KAAA,GAAQ,YAAA,GAAe,MAAA,GAAS,iBAAA,GAAoB,KAAA,GAAQ,GAAA,GAAM,MAAA,GAAS,uIAAA,GAGxH,OAAA,GAAU,UAAA,GACtB,IAAA,GACA,8BAAA;AAGJ;;;ACpDO,SAAS,kBAAkB,IAAA,EAAwC;AACxE,EAAA,IAAI,CAAC,IAAA,IAAQ,OAAO,IAAA,KAAS,UAAU,OAAO,KAAA;AAC9C,EAAA,MAAM,CAAA,GAAI,IAAA;AACV,EAAA,OAAO,CAAA,CAAE,SAAS,OAAA,IAAW,CAAA,CAAE,YAAY,CAAA,IAAK,OAAO,EAAE,GAAA,KAAQ,QAAA;AACnE","file":"chunk-X5R72SSJ.mjs","sourcesContent":["let cachedCss: string | null = null;\n\n// Absolute origin để inlined CSS có thể load fonts khi SVG render trong\n// Excalidraw / image context (relative paths fail trong nested URL contexts).\nfunction absoluteOrigin(): string {\n if (typeof window !== 'undefined' && window.location) return window.location.origin;\n return '';\n}\n\nasync function loadKatexCss(): Promise<string> {\n if (cachedCss !== null) return cachedCss;\n try {\n if (typeof fetch === 'function') {\n const res = await fetch('/katex.min.css');\n if (res.ok) {\n let css = await res.text();\n // Rewrite relative font URLs → absolute origin URLs.\n // KaTeX CSS uses url(fonts/...) — relative to /katex.min.css → /fonts/...\n // Trong SVG <foreignObject> được render thành image, relative resolves\n // tới page URL (/room/...) thay vì root, gây 404.\n const origin = absoluteOrigin();\n if (origin) {\n css = css.replace(/url\\((['\"]?)(fonts\\/)/g, `url($1${origin}/$2`);\n }\n cachedCss = css;\n return css;\n }\n }\n } catch {\n /* ignore */\n }\n cachedCss = '';\n return '';\n}\n\nexport async function renderLatexToSvg(src: string, displayMode: boolean): Promise<string> {\n const katex = await import('katex');\n const html = katex.default.renderToString(src, { displayMode, throwOnError: true, output: 'html' });\n\n const measureDiv = document.createElement('div');\n measureDiv.style.cssText = 'position:absolute;top:-9999px;left:-9999px;visibility:hidden;display:inline-block;';\n measureDiv.innerHTML = html;\n document.body.appendChild(measureDiv);\n const rect = measureDiv.getBoundingClientRect();\n const width = Math.ceil(rect.width) || 50;\n const height = Math.ceil(rect.height) || 20;\n document.body.removeChild(measureDiv);\n\n const cssText = await loadKatexCss();\n\n // KaTeX render với text màu đen mặc định. Excalidraw apply CSS filter\n // invert+hue-rotate trên canvas khi dark mode → text tự thành sáng. Đừng\n // override color ở đây vì sẽ đánh nhau với filter.\n return '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"' + width + '\" height=\"' + height + '\" viewBox=\"0 0 ' + width + ' ' + height + '\">' +\n '<foreignObject width=\"100%\" height=\"100%\">' +\n '<div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"font-size:16px;line-height:1.2;\">' +\n '<style>' + cssText + '</style>' +\n html +\n '</div>' +\n '</foreignObject>' +\n '</svg>';\n}\n","import type { BaseStampCustomData } from '../shared/types';\n\nexport interface LatexCustomData extends BaseStampCustomData {\n kind: 'latex';\n version: 1;\n src: string;\n displayMode: boolean;\n}\n\nexport function isLatexCustomData(data: unknown): data is LatexCustomData {\n if (!data || typeof data !== 'object') return false;\n const d = data as Partial<LatexCustomData>;\n return d.kind === 'latex' && d.version === 1 && typeof d.src === 'string';\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { B as BaseStampCustomData, S as StampType } from './types-CinstD7T.mjs';
|
|
2
|
+
import 'react';
|
|
3
|
+
import '@excalidraw/excalidraw/element/types';
|
|
4
|
+
|
|
5
|
+
interface GeometryCustomData extends BaseStampCustomData {
|
|
6
|
+
kind: 'geometry';
|
|
7
|
+
version: 1;
|
|
8
|
+
jsonState: string;
|
|
9
|
+
svgWidth: number;
|
|
10
|
+
svgHeight: number;
|
|
11
|
+
}
|
|
12
|
+
declare function isGeometryCustomData(data: unknown): data is GeometryCustomData;
|
|
13
|
+
|
|
14
|
+
declare const geometryStamp: StampType;
|
|
15
|
+
|
|
16
|
+
export { type GeometryCustomData, geometryStamp, isGeometryCustomData };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { B as BaseStampCustomData, S as StampType } from './types-CinstD7T.js';
|
|
2
|
+
import 'react';
|
|
3
|
+
import '@excalidraw/excalidraw/element/types';
|
|
4
|
+
|
|
5
|
+
interface GeometryCustomData extends BaseStampCustomData {
|
|
6
|
+
kind: 'geometry';
|
|
7
|
+
version: 1;
|
|
8
|
+
jsonState: string;
|
|
9
|
+
svgWidth: number;
|
|
10
|
+
svgHeight: number;
|
|
11
|
+
}
|
|
12
|
+
declare function isGeometryCustomData(data: unknown): data is GeometryCustomData;
|
|
13
|
+
|
|
14
|
+
declare const geometryStamp: StampType;
|
|
15
|
+
|
|
16
|
+
export { type GeometryCustomData, geometryStamp, isGeometryCustomData };
|