@xom11/whiteboard 0.10.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +67 -0
  2. package/dist/{ExcalidrawWithMenus-EAVPOPJZ.mjs → ExcalidrawWithMenus-KBLDWPM2.mjs} +2 -3
  3. package/dist/ExcalidrawWithMenus-KBLDWPM2.mjs.map +1 -0
  4. package/dist/catalog.json +57 -0
  5. package/dist/{chunk-PWIMZIB6.mjs → chunk-2SKXRBGS.mjs} +7 -8
  6. package/dist/chunk-2SKXRBGS.mjs.map +1 -0
  7. package/dist/chunk-33PEN2WC.mjs +57 -0
  8. package/dist/chunk-33PEN2WC.mjs.map +1 -0
  9. package/dist/chunk-3KBL77M6.mjs +127 -0
  10. package/dist/chunk-3KBL77M6.mjs.map +1 -0
  11. package/dist/chunk-5UTGXHLJ.mjs +57 -0
  12. package/dist/chunk-5UTGXHLJ.mjs.map +1 -0
  13. package/dist/chunk-6XUPIGVD.mjs +467 -0
  14. package/dist/chunk-6XUPIGVD.mjs.map +1 -0
  15. package/dist/chunk-7WG2KDRF.mjs +28 -0
  16. package/dist/chunk-7WG2KDRF.mjs.map +1 -0
  17. package/dist/chunk-FZY33J6Z.mjs +95 -0
  18. package/dist/chunk-FZY33J6Z.mjs.map +1 -0
  19. package/dist/chunk-HNQLZIEP.mjs +78 -0
  20. package/dist/chunk-HNQLZIEP.mjs.map +1 -0
  21. package/dist/chunk-NVJ7K3DK.mjs +29 -0
  22. package/dist/chunk-NVJ7K3DK.mjs.map +1 -0
  23. package/dist/chunk-O4WIZFRQ.mjs +11 -0
  24. package/dist/chunk-O4WIZFRQ.mjs.map +1 -0
  25. package/dist/{chunk-YVJP7NRG.mjs → chunk-O6QTYAKE.mjs} +7 -9
  26. package/dist/chunk-O6QTYAKE.mjs.map +1 -0
  27. package/dist/chunk-R5FL6S7L.mjs +22 -0
  28. package/dist/chunk-R5FL6S7L.mjs.map +1 -0
  29. package/dist/chunk-RBUILBX3.mjs +388 -0
  30. package/dist/chunk-RBUILBX3.mjs.map +1 -0
  31. package/dist/chunk-RD34F5PM.mjs +57 -0
  32. package/dist/chunk-RD34F5PM.mjs.map +1 -0
  33. package/dist/{chunk-7P7SQFOW.mjs → chunk-RXOFO64U.mjs} +3 -3
  34. package/dist/chunk-RXOFO64U.mjs.map +1 -0
  35. package/dist/chunk-TOOHCAWP.mjs +1167 -0
  36. package/dist/chunk-TOOHCAWP.mjs.map +1 -0
  37. package/dist/{chunk-C6SCVOMC.mjs → chunk-TQYQVXNW.mjs} +5 -41
  38. package/dist/chunk-TQYQVXNW.mjs.map +1 -0
  39. package/dist/chunk-VBJLUHCY.mjs +23 -0
  40. package/dist/chunk-VBJLUHCY.mjs.map +1 -0
  41. package/dist/chunk-VRWZILTG.mjs +205 -0
  42. package/dist/chunk-VRWZILTG.mjs.map +1 -0
  43. package/dist/chunk-XVSO7FBM.mjs +61 -0
  44. package/dist/chunk-XVSO7FBM.mjs.map +1 -0
  45. package/dist/geometry-2d.d.mts +3 -6
  46. package/dist/geometry-2d.d.ts +3 -6
  47. package/dist/geometry-2d.js +5069 -2651
  48. package/dist/geometry-2d.js.map +1 -1
  49. package/dist/geometry-2d.mjs +8 -4
  50. package/dist/geometry-3d.d.mts +4 -7
  51. package/dist/geometry-3d.d.ts +4 -7
  52. package/dist/geometry-3d.js +3053 -2150
  53. package/dist/geometry-3d.js.map +1 -1
  54. package/dist/geometry-3d.mjs +7 -4
  55. package/dist/graph-2d.d.mts +4 -7
  56. package/dist/graph-2d.d.ts +4 -7
  57. package/dist/graph-2d.js +3363 -1670
  58. package/dist/graph-2d.js.map +1 -1
  59. package/dist/graph-2d.mjs +10 -3
  60. package/dist/host-3N4E4KJH.mjs +1142 -0
  61. package/dist/host-3N4E4KJH.mjs.map +1 -0
  62. package/dist/{host-Z3TEJKZA.mjs → host-6SNSZ332.mjs} +4 -4
  63. package/dist/{host-Z3TEJKZA.mjs.map → host-6SNSZ332.mjs.map} +1 -1
  64. package/dist/host-EVJT3LIF.mjs +3198 -0
  65. package/dist/host-EVJT3LIF.mjs.map +1 -0
  66. package/dist/host-HN4X3TBC.mjs +2374 -0
  67. package/dist/host-HN4X3TBC.mjs.map +1 -0
  68. package/dist/index.css +4 -1
  69. package/dist/index.css.map +1 -1
  70. package/dist/index.d.mts +675 -19
  71. package/dist/index.d.ts +675 -19
  72. package/dist/index.js +11764 -9417
  73. package/dist/index.js.map +1 -1
  74. package/dist/index.mjs +1492 -335
  75. package/dist/index.mjs.map +1 -1
  76. package/dist/latex.d.mts +3 -4
  77. package/dist/latex.d.ts +3 -4
  78. package/dist/latex.js +33 -18
  79. package/dist/latex.js.map +1 -1
  80. package/dist/latex.mjs +2 -3
  81. package/dist/render-OCVGDKK6.mjs +8 -0
  82. package/dist/render-OCVGDKK6.mjs.map +1 -0
  83. package/dist/serialize-GKN6OVPM.mjs +6 -0
  84. package/dist/serialize-GKN6OVPM.mjs.map +1 -0
  85. package/dist/{types-CinstD7T.d.mts → types-rA4slL08.d.mts} +69 -4
  86. package/dist/{types-CinstD7T.d.ts → types-rA4slL08.d.ts} +69 -4
  87. package/package.json +24 -5
  88. package/dist/ExcalidrawWithMenus-EAVPOPJZ.mjs.map +0 -1
  89. package/dist/chunk-74VEEZBV.mjs +0 -619
  90. package/dist/chunk-74VEEZBV.mjs.map +0 -1
  91. package/dist/chunk-7P7SQFOW.mjs.map +0 -1
  92. package/dist/chunk-BJTO5JO5.mjs +0 -11
  93. package/dist/chunk-BJTO5JO5.mjs.map +0 -1
  94. package/dist/chunk-C6SCVOMC.mjs.map +0 -1
  95. package/dist/chunk-D257NCQW.mjs +0 -58
  96. package/dist/chunk-D257NCQW.mjs.map +0 -1
  97. package/dist/chunk-G7FR3AIV.mjs +0 -193
  98. package/dist/chunk-G7FR3AIV.mjs.map +0 -1
  99. package/dist/chunk-HTBLO5JO.mjs +0 -41
  100. package/dist/chunk-HTBLO5JO.mjs.map +0 -1
  101. package/dist/chunk-PWIMZIB6.mjs.map +0 -1
  102. package/dist/chunk-SBDMF4NQ.mjs +0 -212
  103. package/dist/chunk-SBDMF4NQ.mjs.map +0 -1
  104. package/dist/chunk-WQOABS6N.mjs +0 -197
  105. package/dist/chunk-WQOABS6N.mjs.map +0 -1
  106. package/dist/chunk-YVJP7NRG.mjs.map +0 -1
  107. package/dist/host-N6ACNJKI.mjs +0 -3226
  108. package/dist/host-N6ACNJKI.mjs.map +0 -1
  109. package/dist/host-NKGV6RF2.mjs +0 -1134
  110. package/dist/host-NKGV6RF2.mjs.map +0 -1
  111. package/dist/host-XVK7UCRE.mjs +0 -2908
  112. package/dist/host-XVK7UCRE.mjs.map +0 -1
@@ -1,1134 +0,0 @@
1
- "use client";
2
- import { EMPTY_GRAPH, addPointOnCurve, addIntersection, validate, stringifySerializedGraph, renderGraph2dSvgFromState, isGraph2DCustomData, parseSerializedGraph, compile, numericalDerivative } from './chunk-74VEEZBV.mjs';
3
- import { useIsMobile } from './chunk-P2AOIF7S.mjs';
4
- import { insertStampImage } from './chunk-C6SCVOMC.mjs';
5
- import { __require } from './chunk-BJTO5JO5.mjs';
6
- import { forwardRef, useRef, useState, useCallback, useEffect, useImperativeHandle, useMemo } from 'react';
7
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
8
-
9
- // src/stamps/graph-2d/editor/tools.ts
10
- var GRAPH_TOOLS = [
11
- { id: "move", label: "Di chuy\u1EC3n", title: "Di chuy\u1EC3n / ch\u1ECDn" },
12
- { id: "point-on-curve", label: "\u0110i\u1EC3m tr\xEAn curve", title: "T\u1EA1o \u0111i\u1EC3m c\u1ED1 \u0111\u1ECBnh tr\xEAn \u0111\u1ED3 th\u1ECB" },
13
- { id: "intersect", label: "Giao \u0111i\u1EC3m", title: "\u0110\xE1nh d\u1EA5u giao \u0111i\u1EC3m 2 \u0111\u1ED3 th\u1ECB" },
14
- { id: "tangent", label: "Ti\u1EBFp tuy\u1EBFn", title: "V\u1EBD ti\u1EBFp tuy\u1EBFn t\u1EA1i \u0111i\u1EC3m tr\xEAn \u0111\u1ED3 th\u1ECB" }
15
- ];
16
- function FunctionRow(props) {
17
- const { id, name, expression, color, visible, error } = props;
18
- const [draft, setDraft] = useState(expression);
19
- useEffect(() => {
20
- setDraft(expression);
21
- }, [expression]);
22
- const commit = () => {
23
- if (draft !== expression) props.onExpressionCommit(draft);
24
- };
25
- const handleKeyDown = (e) => {
26
- if (e.key === "Enter") {
27
- e.preventDefault();
28
- commit();
29
- e.target.blur();
30
- } else if (e.key === "Escape") {
31
- setDraft(expression);
32
- e.target.blur();
33
- }
34
- };
35
- const handleBlur = (_) => commit();
36
- return /* @__PURE__ */ jsxs("div", { className: `graph-function-row${error ? " is-error" : ""}`, "data-testid": `graph-function-row-${id}`, children: [
37
- /* @__PURE__ */ jsx(
38
- "span",
39
- {
40
- className: "graph-function-color",
41
- style: { backgroundColor: color },
42
- "aria-hidden": "true"
43
- }
44
- ),
45
- /* @__PURE__ */ jsxs("span", { className: "graph-function-name", "data-testid": `graph-function-name-${id}`, children: [
46
- name,
47
- "(x) ="
48
- ] }),
49
- /* @__PURE__ */ jsx(
50
- "input",
51
- {
52
- "aria-label": "Bi\u1EC3u th\u1EE9c",
53
- className: "graph-function-input",
54
- type: "text",
55
- value: draft,
56
- onChange: (e) => setDraft(e.target.value),
57
- onKeyDown: handleKeyDown,
58
- onBlur: handleBlur,
59
- spellCheck: false,
60
- autoCorrect: "off",
61
- autoCapitalize: "off"
62
- }
63
- ),
64
- /* @__PURE__ */ jsx(
65
- "button",
66
- {
67
- type: "button",
68
- "aria-label": "\u1EA8n/hi\u1EC7n \u0111\u1ED3 th\u1ECB",
69
- className: `graph-function-eye${visible ? "" : " is-hidden"}`,
70
- onClick: props.onToggleVisible,
71
- children: visible ? "\u{1F441}" : "\u2298"
72
- }
73
- ),
74
- /* @__PURE__ */ jsx(
75
- "button",
76
- {
77
- type: "button",
78
- "aria-label": "Xo\xE1 \u0111\u1ED3 th\u1ECB",
79
- className: "graph-function-remove",
80
- onClick: props.onRemove,
81
- children: "\u2715"
82
- }
83
- ),
84
- error ? /* @__PURE__ */ jsx("div", { className: "graph-function-error", children: error }) : null
85
- ] });
86
- }
87
- function SliderRow(props) {
88
- const { name, value, min, max, step } = props;
89
- return /* @__PURE__ */ jsxs("div", { className: "graph-slider-row", "data-testid": `graph-slider-row-${name}`, children: [
90
- /* @__PURE__ */ jsxs("div", { className: "graph-slider-header", children: [
91
- /* @__PURE__ */ jsx("span", { className: "graph-slider-name", children: name }),
92
- /* @__PURE__ */ jsxs("span", { className: "graph-slider-value", children: [
93
- "= ",
94
- value.toFixed(2)
95
- ] }),
96
- /* @__PURE__ */ jsx(
97
- "button",
98
- {
99
- type: "button",
100
- "aria-label": `Xo\xE1 tham s\u1ED1 ${name}`,
101
- className: "graph-slider-remove",
102
- onClick: props.onRemove,
103
- children: "\u2715"
104
- }
105
- )
106
- ] }),
107
- /* @__PURE__ */ jsx(
108
- "input",
109
- {
110
- type: "range",
111
- "aria-label": `Slider ${name}`,
112
- min,
113
- max,
114
- step,
115
- value,
116
- onChange: (e) => props.onChange(parseFloat(e.target.value)),
117
- className: "graph-slider-input"
118
- }
119
- ),
120
- /* @__PURE__ */ jsxs("div", { className: "graph-slider-range", children: [
121
- /* @__PURE__ */ jsx("span", { children: min }),
122
- /* @__PURE__ */ jsx("span", { children: max })
123
- ] })
124
- ] });
125
- }
126
-
127
- // src/stamps/graph-2d/colors.ts
128
- var GRAPH_PALETTE = [
129
- "#2563eb",
130
- // blue
131
- "#dc2626",
132
- // red
133
- "#16a34a",
134
- // green
135
- "#9333ea",
136
- // purple
137
- "#ea580c",
138
- // orange
139
- "#0891b2",
140
- // cyan
141
- "#db2777",
142
- // pink
143
- "#65a30d"
144
- // lime
145
- ];
146
- var FUNCTION_NAMES = ["f", "g", "h", "i", "j", "k", "l", "m"];
147
- var MAX_FUNCTIONS = 8;
148
- var MAX_PARAMETERS = 8;
149
- function nextColor(usedColors) {
150
- for (const c of GRAPH_PALETTE) {
151
- if (!usedColors.includes(c)) return c;
152
- }
153
- return GRAPH_PALETTE[usedColors.length % GRAPH_PALETTE.length];
154
- }
155
- function nextFunctionName(usedNames) {
156
- for (const n of FUNCTION_NAMES) {
157
- if (!usedNames.includes(n)) return n;
158
- }
159
- return FUNCTION_NAMES[usedNames.length % FUNCTION_NAMES.length];
160
- }
161
- function AlgebraView(props) {
162
- const { graph, errors } = props;
163
- const atMax = graph.functions.length >= MAX_FUNCTIONS;
164
- return /* @__PURE__ */ jsxs("div", { className: "graph-algebra-view", children: [
165
- /* @__PURE__ */ jsxs("div", { className: "graph-algebra-section", children: [
166
- graph.functions.map((f) => /* @__PURE__ */ jsx(
167
- FunctionRow,
168
- {
169
- id: f.id,
170
- name: f.name,
171
- expression: f.expression,
172
- color: f.color,
173
- visible: f.visible,
174
- error: errors[f.id] ?? null,
175
- onExpressionCommit: (expr) => props.onCommitFunctionExpr(f.id, expr),
176
- onToggleVisible: () => props.onToggleFunctionVisible(f.id),
177
- onRemove: () => props.onRemoveFunction(f.id)
178
- },
179
- f.id
180
- )),
181
- /* @__PURE__ */ jsx(
182
- "button",
183
- {
184
- type: "button",
185
- "aria-label": "Th\xEAm h\xE0m s\u1ED1",
186
- className: "graph-algebra-add",
187
- onClick: props.onAddFunctionDraft,
188
- disabled: atMax,
189
- children: "+ Th\xEAm h\xE0m"
190
- }
191
- )
192
- ] }),
193
- graph.parameters.length > 0 ? /* @__PURE__ */ jsx("div", { className: "graph-algebra-section graph-algebra-parameters", children: graph.parameters.map((p) => /* @__PURE__ */ jsx(
194
- SliderRow,
195
- {
196
- name: p.name,
197
- value: p.value,
198
- min: p.min,
199
- max: p.max,
200
- step: p.step,
201
- onChange: (v) => props.onParameterChange(p.name, v),
202
- onRangeChange: (min, max, step) => props.onParameterRangeChange(p.name, min, max, step),
203
- onRemove: () => props.onRemoveParameter(p.name)
204
- },
205
- p.name
206
- )) }) : null
207
- ] });
208
- }
209
- var GraphIconHeader = /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
210
- /* @__PURE__ */ jsx("path", { d: "M3 21 V3" }),
211
- /* @__PURE__ */ jsx("path", { d: "M3 21 H21" }),
212
- /* @__PURE__ */ jsx("path", { d: "M5 19 C8 5, 14 5, 19 17" })
213
- ] });
214
- function CloseIcon() {
215
- return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
216
- /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
217
- /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
218
- ] });
219
- }
220
- function UndoIcon() {
221
- return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
222
- /* @__PURE__ */ jsx("path", { d: "M3 10 L8 5 L8 8 L15 8 A5 5 0 0 1 20 13 L20 16" }),
223
- /* @__PURE__ */ jsx("path", { d: "M3 10 L8 15 L8 12" })
224
- ] });
225
- }
226
- function RedoIcon() {
227
- return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
228
- /* @__PURE__ */ jsx("path", { d: "M21 10 L16 5 L16 8 L9 8 A5 5 0 0 0 4 13 L4 16" }),
229
- /* @__PURE__ */ jsx("path", { d: "M21 10 L16 15 L16 12" })
230
- ] });
231
- }
232
- function ResetViewIcon() {
233
- return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
234
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
235
- /* @__PURE__ */ jsx("line", { x1: "12", y1: "3", x2: "12", y2: "21" }),
236
- /* @__PURE__ */ jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12" })
237
- ] });
238
- }
239
- function MoveIcon() {
240
- return /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "M3 4 L9 4 L9 9 L4 9 Z" }) });
241
- }
242
- function PointOnCurveIcon() {
243
- return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
244
- /* @__PURE__ */ jsx("path", { d: "M3 17 C7 8, 14 8, 21 14" }),
245
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "11", r: "2.2", fill: "currentColor", stroke: "none" })
246
- ] });
247
- }
248
- function IntersectIcon() {
249
- return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
250
- /* @__PURE__ */ jsx("path", { d: "M3 17 C8 5, 14 5, 21 17" }),
251
- /* @__PURE__ */ jsx("path", { d: "M3 5 C8 17, 14 17, 21 5" }),
252
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "11", r: "1.6", fill: "currentColor", stroke: "none" })
253
- ] });
254
- }
255
- function TangentIcon() {
256
- return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
257
- /* @__PURE__ */ jsx("path", { d: "M3 17 C8 7, 14 7, 21 16" }),
258
- /* @__PURE__ */ jsx("line", { x1: "4", y1: "14", x2: "20", y2: "6" }),
259
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "10", r: "1.8", fill: "currentColor", stroke: "none" })
260
- ] });
261
- }
262
- var TOOL_ICONS = {
263
- move: /* @__PURE__ */ jsx(MoveIcon, {}),
264
- "point-on-curve": /* @__PURE__ */ jsx(PointOnCurveIcon, {}),
265
- intersect: /* @__PURE__ */ jsx(IntersectIcon, {}),
266
- tangent: /* @__PURE__ */ jsx(TangentIcon, {})
267
- };
268
- function Section({ label, children }) {
269
- return /* @__PURE__ */ jsxs("section", { children: [
270
- /* @__PURE__ */ jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: label }),
271
- children
272
- ] });
273
- }
274
- function PanelBody(props) {
275
- return /* @__PURE__ */ jsxs(Fragment, { children: [
276
- /* @__PURE__ */ jsx(Section, { label: "B\u1ED1 c\u1EE5c", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-wrap text-[11px] text-slate-700", children: [
277
- /* @__PURE__ */ jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
278
- /* @__PURE__ */ jsx(
279
- "input",
280
- {
281
- type: "checkbox",
282
- checked: props.showAxis,
283
- onChange: (e) => props.onShowAxisChange(e.target.checked),
284
- "data-testid": "toggle-axis"
285
- }
286
- ),
287
- "Tr\u1EE5c"
288
- ] }),
289
- /* @__PURE__ */ jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
290
- /* @__PURE__ */ jsx(
291
- "input",
292
- {
293
- type: "checkbox",
294
- checked: props.showGrid,
295
- onChange: (e) => props.onShowGridChange(e.target.checked),
296
- "data-testid": "toggle-grid"
297
- }
298
- ),
299
- "L\u01B0\u1EDBi"
300
- ] }),
301
- /* @__PURE__ */ jsx(
302
- "button",
303
- {
304
- type: "button",
305
- onClick: props.onResetView,
306
- title: "\u0110\u1EB7t l\u1EA1i t\u1EA7m nh\xECn",
307
- "aria-label": "\u0110\u1EB7t l\u1EA1i t\u1EA7m nh\xECn",
308
- className: "ml-auto inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900",
309
- children: /* @__PURE__ */ jsx(ResetViewIcon, {})
310
- }
311
- ),
312
- /* @__PURE__ */ jsx(
313
- "button",
314
- {
315
- type: "button",
316
- onClick: props.onUndo,
317
- disabled: !props.canUndo,
318
- title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
319
- "aria-label": "Ho\xE0n t\xE1c",
320
- "data-testid": "undo-btn",
321
- className: "inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
322
- children: /* @__PURE__ */ jsx(UndoIcon, {})
323
- }
324
- ),
325
- /* @__PURE__ */ jsx(
326
- "button",
327
- {
328
- type: "button",
329
- onClick: props.onRedo,
330
- disabled: !props.canRedo,
331
- title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
332
- "aria-label": "L\xE0m l\u1EA1i",
333
- "data-testid": "redo-btn",
334
- className: "inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
335
- children: /* @__PURE__ */ jsx(RedoIcon, {})
336
- }
337
- )
338
- ] }) }),
339
- /* @__PURE__ */ jsx(Section, { label: "C\xF4ng c\u1EE5", children: /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 gap-1", children: GRAPH_TOOLS.map((t) => {
340
- const isActive = props.activeTool === t.id;
341
- return /* @__PURE__ */ jsx(
342
- "button",
343
- {
344
- type: "button",
345
- "aria-label": t.title,
346
- title: t.title,
347
- "aria-pressed": isActive,
348
- onClick: () => props.onToolChange(t.id),
349
- "data-testid": `graph-tool-${t.id}`,
350
- className: [
351
- "flex h-8 items-center justify-center rounded-md transition",
352
- isActive ? "bg-orange-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
353
- ].join(" "),
354
- children: TOOL_ICONS[t.id]
355
- },
356
- t.id
357
- );
358
- }) }) }),
359
- /* @__PURE__ */ jsx(Section, { label: "H\xE0m s\u1ED1", children: /* @__PURE__ */ jsx(
360
- AlgebraView,
361
- {
362
- graph: props.graph,
363
- errors: props.errors,
364
- onAddFunctionDraft: props.onAddFunctionDraft,
365
- onCommitFunctionExpr: props.onCommitFunctionExpr,
366
- onToggleFunctionVisible: props.onToggleFunctionVisible,
367
- onRemoveFunction: props.onRemoveFunction,
368
- onParameterChange: props.onParameterChange,
369
- onParameterRangeChange: props.onParameterRangeChange,
370
- onRemoveParameter: props.onRemoveParameter
371
- }
372
- ) })
373
- ] });
374
- }
375
- function GraphLeftPanel(props) {
376
- const { isMobile, drawerOpen, isDark, onClose, onDrawerClose } = props;
377
- if (isMobile && !drawerOpen) return null;
378
- const handleClose = isMobile ? onDrawerClose : onClose;
379
- return /* @__PURE__ */ jsxs(
380
- "aside",
381
- {
382
- role: "complementary",
383
- "aria-label": "\u0110\u1ED3 th\u1ECB 2D",
384
- "data-testid": "graph-left-panel",
385
- "data-stamp-area": "true",
386
- className: [
387
- isDark ? "theme--dark " : "",
388
- isMobile ? "fixed inset-y-0 left-0 z-50 flex w-72 max-w-[85vw] flex-col bg-white shadow-2xl animate-in slide-in-from-left duration-200" : "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"
389
- ].join(" "),
390
- children: [
391
- /* @__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: [
392
- /* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
393
- /* @__PURE__ */ jsx("span", { className: "text-base leading-none", children: GraphIconHeader }),
394
- "\u0110\u1ED3 th\u1ECB 2D"
395
- ] }),
396
- /* @__PURE__ */ jsx(
397
- "button",
398
- {
399
- onClick: handleClose,
400
- "aria-label": "\u0110\xF3ng",
401
- className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
402
- children: /* @__PURE__ */ jsx(CloseIcon, {})
403
- }
404
- )
405
- ] }),
406
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-4", children: /* @__PURE__ */ jsx(PanelBody, { ...props }) })
407
- ]
408
- }
409
- );
410
- }
411
- function MiniBoard({ graph, activeTool, isDark, onBoardEvent }) {
412
- const containerRef = useRef(null);
413
- const boardRef = useRef(null);
414
- const curvesRef = useRef(/* @__PURE__ */ new Map());
415
- useEffect(() => {
416
- let cancelled = false;
417
- let createdBoard = null;
418
- const containerEl = containerRef.current;
419
- if (!containerEl) return;
420
- const containerId = `jxg_graph2d_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
421
- containerEl.id = containerId;
422
- (async () => {
423
- const JXG = (await import('jsxgraph')).default;
424
- if (cancelled) return;
425
- const opts = JXG.Options;
426
- if (opts) {
427
- opts.text = opts.text || {};
428
- opts.text.display = "internal";
429
- opts.label = opts.label || {};
430
- opts.label.display = "internal";
431
- }
432
- const board = JXG.JSXGraph.initBoard(containerId, {
433
- boundingbox: [graph.view.xMin, graph.view.yMax, graph.view.xMax, graph.view.yMin],
434
- axis: graph.view.showAxis,
435
- grid: graph.view.showGrid,
436
- showCopyright: false,
437
- showNavigation: true,
438
- pan: { enabled: true, needShift: false },
439
- zoom: { wheel: true, needShift: false },
440
- keepAspectRatio: false
441
- });
442
- boardRef.current = board;
443
- createdBoard = board;
444
- syncObjects(board, graph, curvesRef.current);
445
- board.on("boundingbox", () => {
446
- const bb = board.getBoundingBox();
447
- onBoardEvent({
448
- type: "view-change",
449
- view: {
450
- xMin: bb[0],
451
- xMax: bb[2],
452
- yMax: bb[1],
453
- yMin: bb[3],
454
- showAxis: graph.view.showAxis,
455
- showGrid: graph.view.showGrid
456
- }
457
- });
458
- });
459
- board.on("down", (ev) => {
460
- const usrCoords = board.getUsrCoordsOfMouse?.(ev);
461
- const x = usrCoords?.[0] ?? 0;
462
- const y = usrCoords?.[1] ?? 0;
463
- let functionId;
464
- for (const [id, ref] of curvesRef.current) {
465
- const obj = ref.obj;
466
- if (obj?.hasPoint && obj.hasPoint(ev.clientX ?? 0, ev.clientY ?? 0)) {
467
- functionId = id;
468
- break;
469
- }
470
- }
471
- if (functionId) onBoardEvent({ type: "click-curve", functionId, x, y });
472
- else onBoardEvent({ type: "click-empty", x, y });
473
- });
474
- })().catch((err) => console.error("MiniBoard init failed:", err));
475
- return () => {
476
- cancelled = true;
477
- try {
478
- if (createdBoard) __require("jsxgraph").default.JSXGraph.freeBoard(createdBoard);
479
- } catch {
480
- }
481
- boardRef.current = null;
482
- curvesRef.current.clear();
483
- };
484
- }, []);
485
- useEffect(() => {
486
- if (!boardRef.current) return;
487
- syncObjects(boardRef.current, graph, curvesRef.current);
488
- }, [graph]);
489
- useEffect(() => {
490
- const el = containerRef.current;
491
- if (!el) return;
492
- el.style.cursor = activeTool === "move" ? "" : "crosshair";
493
- }, [activeTool]);
494
- return /* @__PURE__ */ jsx(
495
- "div",
496
- {
497
- ref: containerRef,
498
- className: "graph-miniboard",
499
- style: { width: "100%", height: "100%", minHeight: "300px" },
500
- "data-testid": "graph-miniboard"
501
- }
502
- );
503
- }
504
- function paramSig(graph) {
505
- return graph.parameters.map((p) => `${p.name}=${p.value}`).join(",");
506
- }
507
- function syncObjects(board, graph, curves) {
508
- const sig = paramSig(graph);
509
- const paramMap = {};
510
- for (const p of graph.parameters) paramMap[p.name] = p.value;
511
- const wantedIds = new Set(graph.functions.map((f) => f.id));
512
- for (const [id, ref] of curves) {
513
- if (!wantedIds.has(id)) {
514
- try {
515
- board.removeObject(ref.obj);
516
- } catch {
517
- }
518
- curves.delete(id);
519
- }
520
- }
521
- for (const f of graph.functions) {
522
- const existing = curves.get(f.id);
523
- const needsRecreate = !existing || existing.expression !== f.expression || existing.color !== f.color || existing.visible !== f.visible || existing.paramSignature !== sig;
524
- if (!needsRecreate) continue;
525
- if (existing) {
526
- try {
527
- board.removeObject(existing.obj);
528
- } catch {
529
- }
530
- }
531
- if (!f.visible) {
532
- curves.delete(f.id);
533
- continue;
534
- }
535
- const compiled = compile(f.expression, paramMap);
536
- if (typeof compiled !== "function") continue;
537
- const domain = f.domain ?? { min: graph.view.xMin, max: graph.view.xMax };
538
- const obj = board.create("functiongraph", [compiled, domain.min, domain.max], {
539
- strokeColor: f.color,
540
- strokeWidth: 2,
541
- name: f.name,
542
- withLabel: false,
543
- highlight: false
544
- });
545
- curves.set(f.id, {
546
- obj,
547
- expression: f.expression,
548
- color: f.color,
549
- visible: f.visible,
550
- paramSignature: sig
551
- });
552
- }
553
- for (const point of graph.points) {
554
- const fn = graph.functions.find((f) => f.id === point.functionId);
555
- if (!fn || !fn.visible) continue;
556
- const compiled = compile(fn.expression, paramMap);
557
- if (typeof compiled !== "function") continue;
558
- const y = compiled(point.x);
559
- board.create("point", [point.x, y], {
560
- name: point.label ?? "",
561
- size: 3,
562
- fillColor: fn.color,
563
- strokeColor: fn.color,
564
- withLabel: !!point.label
565
- });
566
- }
567
- for (const inter of graph.intersections) {
568
- const fa = graph.functions.find((f) => f.id === inter.functionIdA);
569
- const fb = graph.functions.find((f) => f.id === inter.functionIdB);
570
- if (!fa || !fb || !fa.visible || !fb.visible) continue;
571
- const cfa = compile(fa.expression, paramMap);
572
- const cfb = compile(fb.expression, paramMap);
573
- if (typeof cfa !== "function" || typeof cfb !== "function") continue;
574
- const roots = scanRoots((x) => cfa(x) - cfb(x), graph.view.xMin, graph.view.xMax);
575
- for (const x of roots) {
576
- board.create("point", [x, cfa(x)], {
577
- size: 3,
578
- fillColor: "#000",
579
- strokeColor: "#000"
580
- });
581
- }
582
- }
583
- for (const tan of graph.tangents) {
584
- const pt = graph.points.find((p) => p.id === tan.pointId);
585
- if (!pt) continue;
586
- const fn = graph.functions.find((f) => f.id === pt.functionId);
587
- if (!fn || !fn.visible) continue;
588
- const slope = numericalDerivative(fn.expression, paramMap, pt.x);
589
- const cfn = compile(fn.expression, paramMap);
590
- if (typeof cfn !== "function" || !Number.isFinite(slope)) continue;
591
- const y0 = cfn(pt.x);
592
- const x1 = graph.view.xMin;
593
- const x2 = graph.view.xMax;
594
- board.create(
595
- "line",
596
- [
597
- [x1, slope * (x1 - pt.x) + y0],
598
- [x2, slope * (x2 - pt.x) + y0]
599
- ],
600
- {
601
- strokeColor: fn.color,
602
- strokeWidth: 1,
603
- dash: 2,
604
- straightFirst: false,
605
- straightLast: false
606
- }
607
- );
608
- }
609
- board.update();
610
- }
611
- function scanRoots(fn, xMin, xMax, samples = 200) {
612
- const roots = [];
613
- const step = (xMax - xMin) / samples;
614
- let prevX = xMin;
615
- let prevY = fn(prevX);
616
- for (let i = 1; i <= samples; i++) {
617
- const x = xMin + i * step;
618
- const y = fn(x);
619
- if (Number.isFinite(prevY) && Number.isFinite(y) && prevY * y < 0) {
620
- let a = prevX;
621
- let b = x;
622
- let ya = prevY;
623
- for (let j = 0; j < 30; j++) {
624
- const m = (a + b) / 2;
625
- const ym = fn(m);
626
- if (Math.abs(ym) < 1e-6) {
627
- a = b = m;
628
- break;
629
- }
630
- if (ya * ym < 0) {
631
- b = m;
632
- } else {
633
- a = m;
634
- ya = ym;
635
- }
636
- }
637
- roots.push((a + b) / 2);
638
- }
639
- prevX = x;
640
- prevY = y;
641
- }
642
- return roots;
643
- }
644
- var GraphEditorPanel = forwardRef(function GraphEditorPanel2(props, ref) {
645
- const initialGraph = props.initialState ?? EMPTY_GRAPH;
646
- const graphRef = useRef(initialGraph);
647
- const [, forceUpdate] = useState(0);
648
- const [errors, setErrors] = useState({});
649
- const [tool, setToolState] = useState("move");
650
- const undoStackRef = useRef([]);
651
- const redoStackRef = useRef([]);
652
- const idCounterRef = useRef(1);
653
- const toolRef = useRef(tool);
654
- toolRef.current = tool;
655
- const intersectFirstRef = useRef(null);
656
- const propsRef = useRef(props);
657
- propsRef.current = props;
658
- const initialGraphNotifiedRef = useRef(false);
659
- const pushUndo = useCallback((g) => {
660
- undoStackRef.current.push(g);
661
- if (undoStackRef.current.length > 30) undoStackRef.current.shift();
662
- redoStackRef.current = [];
663
- }, []);
664
- const setErrorsWithNotify = useCallback(
665
- (updater) => {
666
- setErrors((prev) => {
667
- const next = updater(prev);
668
- propsRef.current.onErrorsChange?.(next);
669
- return next;
670
- });
671
- },
672
- []
673
- );
674
- const notifyStateChange = useCallback((g, t) => {
675
- propsRef.current.onStateChange({
676
- tool: t,
677
- showAxis: g.view.showAxis,
678
- showGrid: g.view.showGrid,
679
- canUndo: undoStackRef.current.length > 0,
680
- canRedo: redoStackRef.current.length > 0
681
- });
682
- }, []);
683
- const updateGraph = useCallback(
684
- (mutator) => {
685
- const prev = graphRef.current;
686
- pushUndo(prev);
687
- const next = mutator(prev);
688
- graphRef.current = next;
689
- notifyStateChange(next, toolRef.current);
690
- forceUpdate((n) => n + 1);
691
- propsRef.current.onGraphChange?.(next);
692
- },
693
- [pushUndo, notifyStateChange]
694
- );
695
- const doUndo = useCallback(() => {
696
- const prev = undoStackRef.current.pop();
697
- if (!prev) return;
698
- redoStackRef.current.push(graphRef.current);
699
- if (redoStackRef.current.length > 30) redoStackRef.current.shift();
700
- graphRef.current = prev;
701
- forceUpdate((n) => n + 1);
702
- propsRef.current.onStateChange({
703
- tool: toolRef.current,
704
- showAxis: prev.view.showAxis,
705
- showGrid: prev.view.showGrid,
706
- canUndo: undoStackRef.current.length > 0,
707
- canRedo: redoStackRef.current.length > 0
708
- });
709
- propsRef.current.onGraphChange?.(prev);
710
- }, []);
711
- const doRedo = useCallback(() => {
712
- const next = redoStackRef.current.pop();
713
- if (!next) return;
714
- undoStackRef.current.push(graphRef.current);
715
- if (undoStackRef.current.length > 30) undoStackRef.current.shift();
716
- graphRef.current = next;
717
- forceUpdate((n) => n + 1);
718
- propsRef.current.onStateChange({
719
- tool: toolRef.current,
720
- showAxis: next.view.showAxis,
721
- showGrid: next.view.showGrid,
722
- canUndo: undoStackRef.current.length > 0,
723
- canRedo: redoStackRef.current.length > 0
724
- });
725
- propsRef.current.onGraphChange?.(next);
726
- }, []);
727
- useEffect(() => {
728
- const onKey = (e) => {
729
- const ae = document.activeElement;
730
- const inField = !!(ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable));
731
- if (inField) return;
732
- if (!(e.metaKey || e.ctrlKey)) return;
733
- const key = e.key.toLowerCase();
734
- if (key === "z" && !e.shiftKey) {
735
- e.preventDefault();
736
- e.stopPropagation();
737
- doUndo();
738
- } else if (key === "z" && e.shiftKey || key === "y" && !e.shiftKey) {
739
- e.preventDefault();
740
- e.stopPropagation();
741
- doRedo();
742
- }
743
- };
744
- window.addEventListener("keydown", onKey, { capture: true });
745
- return () => window.removeEventListener("keydown", onKey, { capture: true });
746
- }, [doUndo, doRedo]);
747
- const onBoardEvent = useCallback((ev) => {
748
- const currentTool = toolRef.current;
749
- if (currentTool === "point-on-curve" && ev.type === "click-curve" && ev.functionId && ev.x !== void 0) {
750
- updateGraph(
751
- (g) => addPointOnCurve(
752
- g,
753
- { x: ev.x, y: ev.y ?? 0, functionId: ev.functionId },
754
- () => `p${idCounterRef.current++}`
755
- )
756
- );
757
- setToolState("move");
758
- } else if (currentTool === "intersect" && ev.type === "click-curve" && ev.functionId) {
759
- if (!intersectFirstRef.current) {
760
- intersectFirstRef.current = ev.functionId;
761
- } else {
762
- const a = intersectFirstRef.current;
763
- const b = ev.functionId;
764
- intersectFirstRef.current = null;
765
- updateGraph(
766
- (g) => addIntersection(g, a, b, () => `i${idCounterRef.current++}`)
767
- );
768
- setToolState("move");
769
- }
770
- } else if (currentTool === "tangent" && ev.type === "click-curve" && ev.functionId && ev.x !== void 0) {
771
- const pointId = `p${idCounterRef.current++}`;
772
- const tangentId = `t${idCounterRef.current++}`;
773
- updateGraph((g) => ({
774
- ...g,
775
- points: [...g.points, { id: pointId, functionId: ev.functionId, x: ev.x }],
776
- tangents: [...g.tangents, { id: tangentId, pointId }]
777
- }));
778
- setToolState("move");
779
- }
780
- }, [updateGraph]);
781
- useImperativeHandle(
782
- ref,
783
- () => ({
784
- insert: () => {
785
- const g = graphRef.current;
786
- if (g.functions.length === 0) return false;
787
- const jsonState = stringifySerializedGraph(g);
788
- renderGraph2dSvgFromState(jsonState).then((svg) => propsRef.current.onInsert(jsonState, svg)).catch((err) => console.error("Graph2D insert render failed:", err));
789
- return true;
790
- },
791
- hasContent: () => graphRef.current.functions.length > 0,
792
- setTool: (t) => {
793
- setToolState(t);
794
- const g = graphRef.current;
795
- propsRef.current.onStateChange({
796
- tool: t,
797
- showAxis: g.view.showAxis,
798
- showGrid: g.view.showGrid,
799
- canUndo: undoStackRef.current.length > 0,
800
- canRedo: redoStackRef.current.length > 0
801
- });
802
- },
803
- setShowAxis: (b) => updateGraph((g) => ({ ...g, view: { ...g.view, showAxis: b } })),
804
- setShowGrid: (b) => updateGraph((g) => ({ ...g, view: { ...g.view, showGrid: b } })),
805
- resetView: () => updateGraph((g) => ({
806
- ...g,
807
- view: { ...g.view, xMin: -10, xMax: 10, yMin: -10, yMax: 10 }
808
- })),
809
- undo: doUndo,
810
- redo: doRedo,
811
- addFunction: (expr) => {
812
- const g = graphRef.current;
813
- if (g.functions.length >= MAX_FUNCTIONS) {
814
- return { ok: false, error: `T\u1ED1i \u0111a ${MAX_FUNCTIONS} h\xE0m` };
815
- }
816
- const v = validate(expr);
817
- if (!v.ok) return { ok: false, error: v.error ?? "Invalid" };
818
- const id = `f${idCounterRef.current++}`;
819
- const usedNames = g.functions.map((f) => f.name);
820
- const usedColors = g.functions.map((f) => f.color);
821
- const newFn = {
822
- id,
823
- name: nextFunctionName(usedNames),
824
- expression: expr,
825
- color: nextColor(usedColors),
826
- visible: true
827
- };
828
- const usedParamNames = new Set(g.parameters.map((p) => p.name));
829
- const newParams = [];
830
- for (const varName of v.freeVars) {
831
- if (usedParamNames.has(varName)) continue;
832
- if (g.parameters.length + newParams.length >= MAX_PARAMETERS) break;
833
- newParams.push({ name: varName, value: 1, min: -5, max: 5, step: 0.1 });
834
- }
835
- updateGraph((prev) => ({
836
- ...prev,
837
- functions: [...prev.functions, newFn],
838
- parameters: [...prev.parameters, ...newParams]
839
- }));
840
- setErrorsWithNotify((e) => ({ ...e, [id]: null }));
841
- return { ok: true, id };
842
- },
843
- commitFunctionExpression: (id, expr) => {
844
- const g = graphRef.current;
845
- const v = validate(expr);
846
- if (!v.ok) {
847
- setErrorsWithNotify((e) => ({ ...e, [id]: v.error ?? "Invalid" }));
848
- return;
849
- }
850
- const usedParamNames = new Set(g.parameters.map((p) => p.name));
851
- const newParams = [];
852
- for (const varName of v.freeVars) {
853
- if (usedParamNames.has(varName)) continue;
854
- if (g.parameters.length + newParams.length >= MAX_PARAMETERS) break;
855
- newParams.push({ name: varName, value: 1, min: -5, max: 5, step: 0.1 });
856
- }
857
- updateGraph((prev) => ({
858
- ...prev,
859
- functions: prev.functions.map(
860
- (f) => f.id === id ? { ...f, expression: expr } : f
861
- ),
862
- parameters: [...prev.parameters, ...newParams]
863
- }));
864
- setErrorsWithNotify((e) => ({ ...e, [id]: null }));
865
- },
866
- toggleFunctionVisible: (id) => updateGraph((g) => ({
867
- ...g,
868
- functions: g.functions.map(
869
- (f) => f.id === id ? { ...f, visible: !f.visible } : f
870
- )
871
- })),
872
- removeFunction: (id) => updateGraph((g) => ({
873
- ...g,
874
- functions: g.functions.filter((f) => f.id !== id)
875
- })),
876
- // setParameter does NOT push undo — would flood the stack (slider drag)
877
- setParameter: (name, value) => {
878
- const next = {
879
- ...graphRef.current,
880
- parameters: graphRef.current.parameters.map(
881
- (p) => p.name === name ? { ...p, value } : p
882
- )
883
- };
884
- graphRef.current = next;
885
- forceUpdate((n) => n + 1);
886
- propsRef.current.onGraphChange?.(next);
887
- },
888
- setParameterRange: (name, min, max, step) => updateGraph((g) => ({
889
- ...g,
890
- parameters: g.parameters.map(
891
- (p) => p.name === name ? { ...p, min, max, step, value: Math.min(max, Math.max(min, p.value)) } : p
892
- )
893
- })),
894
- removeParameter: (name) => updateGraph((g) => ({
895
- ...g,
896
- parameters: g.parameters.filter((p) => p.name !== name)
897
- })),
898
- getGraph: () => graphRef.current,
899
- getErrors: () => errors
900
- }),
901
- // deps: updateGraph stable; errors changes when function errors change; setErrorsWithNotify stable
902
- // eslint-disable-next-line react-hooks/exhaustive-deps
903
- [updateGraph, errors, setErrorsWithNotify, doUndo, doRedo]
904
- );
905
- useEffect(() => {
906
- if (!initialGraphNotifiedRef.current) {
907
- initialGraphNotifiedRef.current = true;
908
- propsRef.current.onGraphChange?.(graphRef.current);
909
- }
910
- }, []);
911
- const graph = graphRef.current;
912
- const hasContent = graph.functions.length > 0;
913
- const handleInsert = () => {
914
- const g = graphRef.current;
915
- if (g.functions.length === 0) return;
916
- const jsonState = stringifySerializedGraph(g);
917
- renderGraph2dSvgFromState(jsonState).then((svg) => propsRef.current.onInsert(jsonState, svg)).catch((err) => console.error("Graph2D insert render failed:", err));
918
- };
919
- const { isMobile, isDark, withLeftPanel } = props;
920
- const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
921
- position: "absolute",
922
- top: "50%",
923
- left: withLeftPanel ? "calc(50% + 120px)" : "50%",
924
- transform: "translate(-50%, -50%)",
925
- zIndex: 40
926
- };
927
- return /* @__PURE__ */ jsxs(
928
- "div",
929
- {
930
- role: "dialog",
931
- "aria-label": "\u0110\u1ED3 th\u1ECB 2D",
932
- "data-testid": "graph-editor-panel",
933
- "data-stamp-area": "true",
934
- "data-mobile-editor": isMobile ? "true" : void 0,
935
- style: wrapperStyle,
936
- className: [
937
- isDark ? "theme--dark " : "",
938
- "flex flex-col overflow-hidden bg-white",
939
- isMobile ? "h-full w-full" : "h-[540px] max-h-[85vh] w-[640px] max-w-[calc(100vw-280px)] rounded-lg border border-slate-300 shadow-2xl ring-1 ring-black/5"
940
- ].join(" "),
941
- children: [
942
- /* @__PURE__ */ jsxs("header", { className: "flex items-center gap-2 border-b border-slate-200 bg-gradient-to-r from-orange-500 to-amber-600 px-3 py-2 text-white", children: [
943
- isMobile && /* @__PURE__ */ jsx(
944
- "button",
945
- {
946
- type: "button",
947
- onClick: props.onOpenDrawer,
948
- "aria-label": "M\u1EDF b\u1EA3ng \u0111\u1EA1i s\u1ED1",
949
- "data-testid": "graph-drawer-toggle",
950
- className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
951
- children: /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
952
- /* @__PURE__ */ jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
953
- /* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
954
- /* @__PURE__ */ jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
955
- ] })
956
- }
957
- ),
958
- /* @__PURE__ */ jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
959
- /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
960
- /* @__PURE__ */ jsx("path", { d: "M3 21 V3" }),
961
- /* @__PURE__ */ jsx("path", { d: "M3 21 H21" }),
962
- /* @__PURE__ */ jsx("path", { d: "M5 19 C8 5, 14 5, 19 17" })
963
- ] }),
964
- "\u0110\u1ED3 th\u1ECB 2D"
965
- ] }),
966
- isMobile && /* @__PURE__ */ jsx(
967
- "button",
968
- {
969
- type: "button",
970
- onClick: handleInsert,
971
- disabled: !hasContent,
972
- "data-testid": "graph-insert-btn-mobile",
973
- className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
974
- children: "Ch\xE8n"
975
- }
976
- ),
977
- /* @__PURE__ */ jsx(
978
- "button",
979
- {
980
- onClick: props.onClose,
981
- "aria-label": "\u0110\xF3ng",
982
- className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15",
983
- children: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
984
- /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
985
- /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
986
- ] })
987
- }
988
- )
989
- ] }),
990
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx(
991
- MiniBoard,
992
- {
993
- graph,
994
- activeTool: tool,
995
- isDark,
996
- onBoardEvent
997
- }
998
- ) }),
999
- !isMobile && /* @__PURE__ */ jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
1000
- /* @__PURE__ */ jsx("span", { className: "text-xs text-slate-500", children: "Nh\u1EADp bi\u1EC3u th\u1EE9c trong b\u1EA3ng \u0111\u1EA1i s\u1ED1 b\xEAn tr\xE1i." }),
1001
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1002
- /* @__PURE__ */ jsx(
1003
- "button",
1004
- {
1005
- onClick: props.onClose,
1006
- className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
1007
- children: "Hu\u1EF7"
1008
- }
1009
- ),
1010
- /* @__PURE__ */ jsx(
1011
- "button",
1012
- {
1013
- onClick: handleInsert,
1014
- disabled: !hasContent,
1015
- "data-testid": "graph-insert-btn",
1016
- className: "rounded bg-orange-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-orange-700 disabled:opacity-50",
1017
- children: "Ch\xE8n"
1018
- }
1019
- )
1020
- ] })
1021
- ] })
1022
- ]
1023
- }
1024
- );
1025
- });
1026
- var INITIAL_GRAPH_STATE = {
1027
- tool: "move",
1028
- showAxis: true,
1029
- showGrid: true,
1030
- canUndo: false,
1031
- canRedo: false
1032
- };
1033
- var Graph2DStampHost = forwardRef(
1034
- function Graph2DStampHost2({ api, editingElement, onClose, isDark }, ref) {
1035
- const panelRef = useRef(null);
1036
- const [graphUIState, setGraphUIState] = useState(INITIAL_GRAPH_STATE);
1037
- const { isMobile } = useIsMobile();
1038
- const [drawerOpen, setDrawerOpen] = useState(false);
1039
- const initialState = useMemo(() => {
1040
- if (!editingElement) return null;
1041
- if (!isGraph2DCustomData(editingElement.customData)) return null;
1042
- return parseSerializedGraph(editingElement.customData.jsonState);
1043
- }, [editingElement]);
1044
- const [graphSnapshot, setGraphSnapshot] = useState(
1045
- initialState ?? EMPTY_GRAPH
1046
- );
1047
- const [errorsSnapshot, setErrorsSnapshot] = useState({});
1048
- const handleInsert = useCallback(
1049
- async (jsonState, svgString) => {
1050
- if (!api) return;
1051
- try {
1052
- await insertStampImage(api, {
1053
- svgString,
1054
- makeCustomData: (width, height) => ({
1055
- kind: "graph2d",
1056
- version: 1,
1057
- jsonState,
1058
- svgWidth: width,
1059
- svgHeight: height
1060
- }),
1061
- editingElementId: editingElement?.id ?? null
1062
- });
1063
- } catch (err) {
1064
- console.error("Graph2D insert failed:", err);
1065
- }
1066
- onClose();
1067
- },
1068
- [api, editingElement?.id, onClose]
1069
- );
1070
- useImperativeHandle(
1071
- ref,
1072
- () => ({
1073
- tryInsert: () => panelRef.current?.insert() ?? false,
1074
- hasContent: () => panelRef.current?.hasContent() ?? false
1075
- }),
1076
- []
1077
- );
1078
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1079
- /* @__PURE__ */ jsx(
1080
- GraphLeftPanel,
1081
- {
1082
- activeTool: graphUIState.tool,
1083
- onToolChange: (t) => panelRef.current?.setTool(t),
1084
- showAxis: graphUIState.showAxis,
1085
- showGrid: graphUIState.showGrid,
1086
- onShowAxisChange: (b) => panelRef.current?.setShowAxis(b),
1087
- onShowGridChange: (b) => panelRef.current?.setShowGrid(b),
1088
- onResetView: () => panelRef.current?.resetView(),
1089
- onUndo: () => panelRef.current?.undo(),
1090
- canUndo: graphUIState.canUndo,
1091
- onRedo: () => panelRef.current?.redo(),
1092
- canRedo: graphUIState.canRedo,
1093
- onClose,
1094
- isDark,
1095
- isMobile,
1096
- drawerOpen,
1097
- onDrawerClose: () => setDrawerOpen(false),
1098
- graph: graphSnapshot,
1099
- errors: errorsSnapshot,
1100
- onAddFunctionDraft: () => {
1101
- const result = panelRef.current?.addFunction("x");
1102
- if (result && !result.ok) console.warn("addFunction failed:", result.error);
1103
- },
1104
- onCommitFunctionExpr: (id, expr) => panelRef.current?.commitFunctionExpression(id, expr),
1105
- onToggleFunctionVisible: (id) => panelRef.current?.toggleFunctionVisible(id),
1106
- onRemoveFunction: (id) => panelRef.current?.removeFunction(id),
1107
- onParameterChange: (name, v) => panelRef.current?.setParameter(name, v),
1108
- onParameterRangeChange: (name, min, max, step) => panelRef.current?.setParameterRange(name, min, max, step),
1109
- onRemoveParameter: (name) => panelRef.current?.removeParameter(name)
1110
- }
1111
- ),
1112
- /* @__PURE__ */ jsx(
1113
- GraphEditorPanel,
1114
- {
1115
- ref: panelRef,
1116
- initialState,
1117
- onInsert: handleInsert,
1118
- onClose,
1119
- onStateChange: setGraphUIState,
1120
- onGraphChange: setGraphSnapshot,
1121
- onErrorsChange: setErrorsSnapshot,
1122
- withLeftPanel: !isMobile,
1123
- isDark,
1124
- isMobile,
1125
- onOpenDrawer: () => setDrawerOpen(true)
1126
- }
1127
- )
1128
- ] });
1129
- }
1130
- );
1131
-
1132
- export { Graph2DStampHost };
1133
- //# sourceMappingURL=host-NKGV6RF2.mjs.map
1134
- //# sourceMappingURL=host-NKGV6RF2.mjs.map