@xom11/whiteboard 0.11.0 → 0.24.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.
Files changed (112) hide show
  1. package/README.md +67 -0
  2. package/dist/{ExcalidrawWithMenus-EAVPOPJZ.mjs → ExcalidrawWithMenus-WENZRYYE.mjs} +2 -3
  3. package/dist/ExcalidrawWithMenus-WENZRYYE.mjs.map +1 -0
  4. package/dist/catalog.json +57 -0
  5. package/dist/chunk-4D5CSIJO.mjs +1167 -0
  6. package/dist/chunk-4D5CSIJO.mjs.map +1 -0
  7. package/dist/chunk-5UTGXHLJ.mjs +57 -0
  8. package/dist/chunk-5UTGXHLJ.mjs.map +1 -0
  9. package/dist/chunk-6V4SH4JJ.mjs +1801 -0
  10. package/dist/chunk-6V4SH4JJ.mjs.map +1 -0
  11. package/dist/chunk-AZIARTGX.mjs +23 -0
  12. package/dist/chunk-AZIARTGX.mjs.map +1 -0
  13. package/dist/chunk-BKSXPNPQ.mjs +348 -0
  14. package/dist/chunk-BKSXPNPQ.mjs.map +1 -0
  15. package/dist/{chunk-YVJP7NRG.mjs → chunk-CRAPWQKJ.mjs} +7 -9
  16. package/dist/chunk-CRAPWQKJ.mjs.map +1 -0
  17. package/dist/chunk-CSCF3YFZ.mjs +388 -0
  18. package/dist/chunk-CSCF3YFZ.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-IBTRMWD6.mjs +28 -0
  22. package/dist/chunk-IBTRMWD6.mjs.map +1 -0
  23. package/dist/chunk-ICR4CVOE.mjs +57 -0
  24. package/dist/chunk-ICR4CVOE.mjs.map +1 -0
  25. package/dist/chunk-LVNCYP4J.mjs +57 -0
  26. package/dist/chunk-LVNCYP4J.mjs.map +1 -0
  27. package/dist/chunk-MFOGFFIL.mjs +95 -0
  28. package/dist/chunk-MFOGFFIL.mjs.map +1 -0
  29. package/dist/chunk-NVJ7K3DK.mjs +29 -0
  30. package/dist/chunk-NVJ7K3DK.mjs.map +1 -0
  31. package/dist/chunk-O4WIZFRQ.mjs +11 -0
  32. package/dist/chunk-O4WIZFRQ.mjs.map +1 -0
  33. package/dist/{chunk-C6SCVOMC.mjs → chunk-QGNU34T7.mjs} +5 -41
  34. package/dist/chunk-QGNU34T7.mjs.map +1 -0
  35. package/dist/chunk-R5FL6S7L.mjs +22 -0
  36. package/dist/chunk-R5FL6S7L.mjs.map +1 -0
  37. package/dist/{chunk-7P7SQFOW.mjs → chunk-SGFJLHHG.mjs} +3 -3
  38. package/dist/chunk-SGFJLHHG.mjs.map +1 -0
  39. package/dist/{chunk-PWIMZIB6.mjs → chunk-WWMQ2VHZ.mjs} +7 -8
  40. package/dist/chunk-WWMQ2VHZ.mjs.map +1 -0
  41. package/dist/chunk-YIPI3WUL.mjs +61 -0
  42. package/dist/chunk-YIPI3WUL.mjs.map +1 -0
  43. package/dist/chunk-ZBJBQKJ2.mjs +330 -0
  44. package/dist/chunk-ZBJBQKJ2.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 +7007 -2633
  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 +5446 -2507
  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 +5300 -1677
  58. package/dist/graph-2d.js.map +1 -1
  59. package/dist/graph-2d.mjs +10 -3
  60. package/dist/host-DOAYVL35.mjs +3199 -0
  61. package/dist/host-DOAYVL35.mjs.map +1 -0
  62. package/dist/host-GKNQBBUE.mjs +1142 -0
  63. package/dist/host-GKNQBBUE.mjs.map +1 -0
  64. package/dist/{host-Z3TEJKZA.mjs → host-QS2EOTRJ.mjs} +4 -4
  65. package/dist/{host-Z3TEJKZA.mjs.map → host-QS2EOTRJ.mjs.map} +1 -1
  66. package/dist/host-TLIXN4CF.mjs +2374 -0
  67. package/dist/host-TLIXN4CF.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 +659 -19
  71. package/dist/index.d.ts +659 -19
  72. package/dist/index.js +13736 -9491
  73. package/dist/index.js.map +1 -1
  74. package/dist/index.mjs +1465 -342
  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-SA4JTOW3.mjs +8 -0
  82. package/dist/render-SA4JTOW3.mjs.map +1 -0
  83. package/dist/serialize-3NZS6A6Q.mjs +6 -0
  84. package/dist/serialize-3NZS6A6Q.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 +34 -6
  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
@@ -0,0 +1,1142 @@
1
+ "use client";
2
+ import { isGraph2DCustomData } from './chunk-O4WIZFRQ.mjs';
3
+ import { paletteFor } from './chunk-AZIARTGX.mjs';
4
+ import { parseSceneState } from './chunk-IBTRMWD6.mjs';
5
+ import { useToolStateMachine } from './chunk-NVJ7K3DK.mjs';
6
+ import { JxgRenderer } from './chunk-BKSXPNPQ.mjs';
7
+ import { initJxgBoard, attachJxgWheelZoom, ToastHost, STAMP_PANEL_DESKTOP, ToastProvider, useStampStore, StampLeftPanel, safeJsx } from './chunk-4D5CSIJO.mjs';
8
+ import { useEditorState, nextLabel } from './chunk-6V4SH4JJ.mjs';
9
+ import { compile } from './chunk-ZBJBQKJ2.mjs';
10
+ import { useIsMobile } from './chunk-P2AOIF7S.mjs';
11
+ import { insertStampImage } from './chunk-QGNU34T7.mjs';
12
+ import './chunk-5UTGXHLJ.mjs';
13
+ import React, { useRef, useId, useEffect, useImperativeHandle, forwardRef, useState, useCallback, useMemo } from 'react';
14
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
15
+
16
+ // src/stamps/graph-2d/editor/handlers.ts
17
+ function handleDown(ctx, coord) {
18
+ const tool = ctx.toolRef.current;
19
+ switch (tool) {
20
+ case "move":
21
+ return;
22
+ case "point":
23
+ addFreePoint(ctx, coord);
24
+ return;
25
+ case "slider":
26
+ ctx.setTool("move");
27
+ return;
28
+ case "pointOnCurve":
29
+ addPointOnCurve(ctx, coord);
30
+ return;
31
+ case "intersect":
32
+ handleIntersect(ctx, coord);
33
+ return;
34
+ case "tangent":
35
+ handleTangent(ctx, coord);
36
+ return;
37
+ case "slope":
38
+ handleSlope(ctx, coord);
39
+ return;
40
+ case "extremum":
41
+ case "root":
42
+ handleAnalysisTool(ctx, coord, tool);
43
+ return;
44
+ case "segment":
45
+ case "line":
46
+ handleTwoPointTool(ctx, coord, tool);
47
+ return;
48
+ case "polygon":
49
+ handlePolygonTool(ctx, coord);
50
+ return;
51
+ }
52
+ }
53
+ function addFreePoint(ctx, coord) {
54
+ const id = ctx.nextLabel("point");
55
+ ctx.store.dispatch({
56
+ type: "ADD",
57
+ payload: {
58
+ obj: {
59
+ id,
60
+ kind: "point",
61
+ label: id,
62
+ visible: true,
63
+ locked: false,
64
+ layer: "default",
65
+ schemaVersion: 1,
66
+ attrs: { constraint: { kind: "free", x: coord.x, y: coord.y } }
67
+ }
68
+ }
69
+ });
70
+ ctx.setTool("move");
71
+ }
72
+ function addPointOnCurve(ctx, coord) {
73
+ const fid = ctx.getNearestFunctionId(coord);
74
+ if (!fid) return;
75
+ const id = ctx.nextLabel("pointOnCurve");
76
+ ctx.store.dispatch({
77
+ type: "ADD",
78
+ payload: {
79
+ obj: {
80
+ id,
81
+ kind: "pointOnCurve",
82
+ label: id,
83
+ visible: true,
84
+ locked: false,
85
+ layer: "default",
86
+ schemaVersion: 1,
87
+ attrs: { functionId: fid, x: coord.x }
88
+ }
89
+ }
90
+ });
91
+ ctx.setTool("move");
92
+ }
93
+ function handleIntersect(ctx, coord) {
94
+ const fid = ctx.getNearestFunctionId(coord);
95
+ if (!fid) return;
96
+ if (ctx.pendingIdsRef.current.length === 0) {
97
+ ctx.pushPending(fid);
98
+ return;
99
+ }
100
+ const fa = ctx.pendingIdsRef.current[0];
101
+ if (fa === fid) return;
102
+ const id = ctx.nextLabel("intersection");
103
+ ctx.store.dispatch({
104
+ type: "ADD",
105
+ payload: {
106
+ obj: {
107
+ id,
108
+ kind: "intersection",
109
+ label: id,
110
+ visible: true,
111
+ locked: false,
112
+ layer: "default",
113
+ schemaVersion: 1,
114
+ // NOTE: 'lineLine' is semantically imprecise for function2d curves but
115
+ // works as a discriminant tag — TODO refactor to 'curveCurve' kind in
116
+ // a follow-up when intersection kind is extended.
117
+ attrs: { kind: "lineLine", ref1: fa, ref2: fid }
118
+ }
119
+ }
120
+ });
121
+ ctx.clearPending();
122
+ ctx.setTool("move");
123
+ }
124
+ function handleTangent(ctx, coord) {
125
+ const hitId = ctx.getHitObjectId(coord);
126
+ if (!hitId) return;
127
+ const obj = ctx.store.getState().objects[hitId];
128
+ if (!obj || obj.kind !== "pointOnCurve") return;
129
+ const id = ctx.nextLabel("tangent2d");
130
+ ctx.store.dispatch({
131
+ type: "ADD",
132
+ payload: {
133
+ obj: {
134
+ id,
135
+ kind: "tangent2d",
136
+ label: id,
137
+ visible: true,
138
+ locked: false,
139
+ layer: "default",
140
+ schemaVersion: 1,
141
+ attrs: { pointId: hitId }
142
+ }
143
+ }
144
+ });
145
+ ctx.setTool("move");
146
+ }
147
+ function handleSlope(ctx, coord) {
148
+ const hitId = ctx.getHitObjectId(coord);
149
+ if (!hitId) return;
150
+ const obj = ctx.store.getState().objects[hitId];
151
+ if (!obj || obj.kind !== "pointOnCurve") return;
152
+ const id = ctx.nextLabel("slope2d");
153
+ ctx.store.dispatch({
154
+ type: "ADD",
155
+ payload: {
156
+ obj: {
157
+ id,
158
+ kind: "slope2d",
159
+ label: id,
160
+ visible: true,
161
+ locked: false,
162
+ layer: "default",
163
+ schemaVersion: 1,
164
+ attrs: { pointId: hitId }
165
+ }
166
+ }
167
+ });
168
+ ctx.setTool("move");
169
+ }
170
+ function handleAnalysisTool(ctx, coord, tool) {
171
+ const fid = ctx.getNearestFunctionId(coord);
172
+ if (!fid) return;
173
+ if (tool === "extremum") {
174
+ const id = ctx.nextLabel("extremum2d");
175
+ ctx.store.dispatch({
176
+ type: "ADD",
177
+ payload: {
178
+ obj: {
179
+ id,
180
+ kind: "extremum2d",
181
+ label: id,
182
+ visible: true,
183
+ locked: false,
184
+ layer: "default",
185
+ schemaVersion: 1,
186
+ attrs: { functionId: fid, interval: { min: -10, max: 10 }, mode: "min" }
187
+ }
188
+ }
189
+ });
190
+ } else {
191
+ const id = ctx.nextLabel("root2d");
192
+ ctx.store.dispatch({
193
+ type: "ADD",
194
+ payload: {
195
+ obj: {
196
+ id,
197
+ kind: "root2d",
198
+ label: id,
199
+ visible: true,
200
+ locked: false,
201
+ layer: "default",
202
+ schemaVersion: 1,
203
+ attrs: { functionId: fid, interval: { min: -10, max: 10 } }
204
+ }
205
+ }
206
+ });
207
+ }
208
+ ctx.setTool("move");
209
+ }
210
+ function handleTwoPointTool(ctx, coord, tool) {
211
+ const hitId = ctx.getHitObjectId(coord);
212
+ const pid = hitId ?? (() => {
213
+ const id2 = ctx.nextLabel("point");
214
+ ctx.store.dispatch({
215
+ type: "ADD",
216
+ payload: {
217
+ obj: {
218
+ id: id2,
219
+ kind: "point",
220
+ label: id2,
221
+ visible: true,
222
+ locked: false,
223
+ layer: "default",
224
+ schemaVersion: 1,
225
+ attrs: { constraint: { kind: "free", x: coord.x, y: coord.y } }
226
+ }
227
+ }
228
+ });
229
+ return id2;
230
+ })();
231
+ if (ctx.pendingIdsRef.current.length === 0) {
232
+ ctx.pushPending(pid);
233
+ return;
234
+ }
235
+ const p1 = ctx.pendingIdsRef.current[0];
236
+ if (p1 === pid) return;
237
+ const id = ctx.nextLabel(tool);
238
+ ctx.store.dispatch({
239
+ type: "ADD",
240
+ payload: {
241
+ obj: {
242
+ id,
243
+ kind: tool,
244
+ label: id,
245
+ visible: true,
246
+ locked: false,
247
+ layer: "default",
248
+ schemaVersion: 1,
249
+ attrs: { p1, p2: pid }
250
+ }
251
+ }
252
+ });
253
+ ctx.clearPending();
254
+ ctx.setTool("move");
255
+ }
256
+ function handlePolygonTool(ctx, coord) {
257
+ const hitId = ctx.getHitObjectId(coord);
258
+ if (hitId && ctx.pendingIdsRef.current[0] === hitId && ctx.pendingIdsRef.current.length >= 3) {
259
+ const id = ctx.nextLabel("polygon");
260
+ ctx.store.dispatch({
261
+ type: "ADD",
262
+ payload: {
263
+ obj: {
264
+ id,
265
+ kind: "polygon",
266
+ label: id,
267
+ visible: true,
268
+ locked: false,
269
+ layer: "default",
270
+ schemaVersion: 1,
271
+ attrs: { points: [...ctx.pendingIdsRef.current] }
272
+ }
273
+ }
274
+ });
275
+ ctx.clearPending();
276
+ ctx.setTool("move");
277
+ return;
278
+ }
279
+ const pid = hitId ?? (() => {
280
+ const id = ctx.nextLabel("point");
281
+ ctx.store.dispatch({
282
+ type: "ADD",
283
+ payload: {
284
+ obj: {
285
+ id,
286
+ kind: "point",
287
+ label: id,
288
+ visible: true,
289
+ locked: false,
290
+ layer: "default",
291
+ schemaVersion: 1,
292
+ attrs: { constraint: { kind: "free", x: coord.x, y: coord.y } }
293
+ }
294
+ }
295
+ });
296
+ return id;
297
+ })();
298
+ ctx.pushPending(pid);
299
+ }
300
+ var MiniBoard = React.forwardRef(
301
+ function MiniBoard2({ store, selectedTool, showAxis, showGrid, isDark, onReady, onSelectionChange: _onSelectionChange }, ref) {
302
+ const isDarkRef = useRef(!!isDark);
303
+ isDarkRef.current = !!isDark;
304
+ const containerId = useId().replace(/:/g, "_") + "_graph_jxg";
305
+ const containerRef = useRef(null);
306
+ const boardRef = useRef(null);
307
+ const jxgRef = useRef(null);
308
+ const rendererRef = useRef(null);
309
+ const toolSM = useToolStateMachine(selectedTool);
310
+ const showAxisRef = useRef(showAxis);
311
+ showAxisRef.current = showAxis;
312
+ const showGridRef = useRef(showGrid);
313
+ showGridRef.current = showGrid;
314
+ useEffect(() => {
315
+ if (toolSM.toolRef.current !== selectedTool) toolSM.setTool(selectedTool);
316
+ }, [selectedTool]);
317
+ useEffect(() => {
318
+ if (typeof window === "undefined" || !containerRef.current) return;
319
+ let cancelled = false;
320
+ let wheelCleanup = null;
321
+ let freeBoard = null;
322
+ void (async () => {
323
+ const { JXG, board, cleanup } = await initJxgBoard(containerId, {
324
+ label: "MiniBoard.graph",
325
+ boardOptions: {
326
+ boundingbox: [-10, 10, 10, -10],
327
+ axis: showAxisRef.current,
328
+ grid: showGridRef.current,
329
+ showCopyright: false,
330
+ showNavigation: true,
331
+ keepAspectRatio: false,
332
+ pan: { enabled: true, needShift: false },
333
+ zoom: { wheel: false }
334
+ }
335
+ });
336
+ if (cancelled || !containerRef.current) {
337
+ cleanup();
338
+ return;
339
+ }
340
+ jxgRef.current = JXG;
341
+ boardRef.current = board;
342
+ freeBoard = cleanup;
343
+ const theme = paletteFor(isDarkRef.current);
344
+ rendererRef.current = new JxgRenderer(store, board, { theme });
345
+ if (containerRef.current) {
346
+ wheelCleanup = attachJxgWheelZoom(containerRef.current, board, "MiniBoard.graph");
347
+ }
348
+ const onDown = (evt) => {
349
+ const b = boardRef.current;
350
+ if (!b || toolSM.toolRef.current === "move") return;
351
+ let ux = 0, uy = 0;
352
+ safeJsx("MiniBoard.graph.pointerCoords", () => {
353
+ const usr = b.getUsrCoordsOfMouse?.(evt);
354
+ if (Array.isArray(usr) && usr.length >= 2 && Number.isFinite(usr[0]) && Number.isFinite(usr[1])) {
355
+ ux = usr[0];
356
+ uy = usr[1];
357
+ } else if (b.origin?.scrCoords && containerRef.current) {
358
+ const rect = containerRef.current.getBoundingClientRect();
359
+ const px = (evt.clientX ?? 0) - rect.left;
360
+ const py = (evt.clientY ?? 0) - rect.top;
361
+ const ox = b.origin.scrCoords[1];
362
+ const oy = b.origin.scrCoords[2];
363
+ const bUnitX = b.unitX || 1;
364
+ const bUnitY = b.unitY || 1;
365
+ ux = (px - ox) / bUnitX;
366
+ uy = (oy - py) / bUnitY;
367
+ }
368
+ });
369
+ const ctx = {
370
+ store,
371
+ toolRef: toolSM.toolRef,
372
+ pendingIdsRef: toolSM.pendingIdsRef,
373
+ pushPending: toolSM.pushPending,
374
+ clearPending: toolSM.clearPending,
375
+ setTool: toolSM.setTool,
376
+ nextLabel: (kind) => nextLabel(store.getState(), kind),
377
+ getNearestFunctionId: ({ x, y }) => findNearestFunction(b, store, rendererRef.current, x, y),
378
+ getHitObjectId: ({ x, y }) => findHitObject(b, rendererRef.current, x, y)
379
+ };
380
+ safeJsx(
381
+ "MiniBoard.graph.handleDown",
382
+ () => handleDown(ctx, { x: ux, y: uy })
383
+ );
384
+ };
385
+ board.on("down", onDown);
386
+ onReady?.();
387
+ })();
388
+ return () => {
389
+ cancelled = true;
390
+ if (wheelCleanup) {
391
+ wheelCleanup();
392
+ wheelCleanup = null;
393
+ }
394
+ rendererRef.current?.dispose();
395
+ rendererRef.current = null;
396
+ if (freeBoard) {
397
+ freeBoard();
398
+ freeBoard = null;
399
+ }
400
+ boardRef.current = null;
401
+ };
402
+ }, [containerId]);
403
+ useImperativeHandle(
404
+ ref,
405
+ () => ({
406
+ getState: () => store.getState(),
407
+ getStore: () => store,
408
+ highlight: (id) => rendererRef.current?.highlight(id),
409
+ getContainer: () => containerRef.current,
410
+ getBbox: () => boardRef.current?.getBoundingBox() ?? [-10, 10, 10, -10]
411
+ }),
412
+ [store]
413
+ );
414
+ return /* @__PURE__ */ jsx(
415
+ "div",
416
+ {
417
+ ref: containerRef,
418
+ id: containerId,
419
+ "data-testid": "graph-miniboard",
420
+ className: "h-full w-full",
421
+ style: { touchAction: "none" }
422
+ }
423
+ );
424
+ }
425
+ );
426
+ function findNearestFunction(_board, store, renderer, x, y, tolY = 0.5) {
427
+ if (!renderer) return null;
428
+ const state = store.getState();
429
+ let bestId = null;
430
+ let bestDist = Infinity;
431
+ for (const id of state.order) {
432
+ const obj = state.objects[id];
433
+ if (obj.kind !== "function2d") continue;
434
+ const el = renderer.getElement(id);
435
+ if (!el || typeof el.Y !== "function") continue;
436
+ let fy;
437
+ try {
438
+ fy = el.Y(x);
439
+ } catch {
440
+ continue;
441
+ }
442
+ if (!Number.isFinite(fy)) continue;
443
+ const d = Math.abs(y - fy);
444
+ if (d < tolY && d < bestDist) {
445
+ bestDist = d;
446
+ bestId = id;
447
+ }
448
+ }
449
+ return bestId;
450
+ }
451
+ function findHitObject(board, renderer, x, y) {
452
+ if (!renderer || !board) return null;
453
+ let screen = null;
454
+ try {
455
+ screen = board.create("point", [x, y], { visible: false, withLabel: false, name: "" });
456
+ } catch {
457
+ return null;
458
+ }
459
+ let result = null;
460
+ try {
461
+ for (const [id, el] of renderer.listElements().entries()) {
462
+ const e = el;
463
+ if (e?.hasPoint?.(screen.X(), screen.Y())) {
464
+ result = id;
465
+ break;
466
+ }
467
+ }
468
+ } finally {
469
+ try {
470
+ board.removeObject(screen);
471
+ } catch {
472
+ }
473
+ }
474
+ return result;
475
+ }
476
+ var GraphEditorPanelInner = forwardRef(
477
+ function GraphEditorPanel({
478
+ store,
479
+ onInsert,
480
+ onClose,
481
+ onSelectionChange,
482
+ selectedTool,
483
+ showAxis,
484
+ showGrid,
485
+ onHistoryChange,
486
+ isDark,
487
+ withLeftPanel = false,
488
+ isMobile = false,
489
+ onOpenDrawer,
490
+ onUndo,
491
+ onRedo,
492
+ canUndo,
493
+ canRedo
494
+ }, ref) {
495
+ const miniRef = useRef(null);
496
+ const [ready, setReady] = useState(false);
497
+ const [hasContent, setHasContent] = useState(false);
498
+ const onSelectionChangeRef = useRef(onSelectionChange);
499
+ useEffect(() => {
500
+ onSelectionChangeRef.current = onSelectionChange;
501
+ }, [onSelectionChange]);
502
+ useEditorState({ store, onHistoryChange });
503
+ useEffect(() => {
504
+ const sync = () => setHasContent(Object.keys(store.getState().objects).length > 0);
505
+ sync();
506
+ return store.subscribe(sync);
507
+ }, [store]);
508
+ const handleReady = useCallback(() => {
509
+ setReady(true);
510
+ }, []);
511
+ const performInsert = useCallback(() => {
512
+ const h = miniRef.current;
513
+ if (!h) return false;
514
+ const state = h.getState();
515
+ if (Object.keys(state.objects).length === 0) return false;
516
+ void (async () => {
517
+ try {
518
+ const { renderGraphSvgFromState } = await import('./render-SA4JTOW3.mjs');
519
+ const { stringifySceneState } = await import('./serialize-3NZS6A6Q.mjs');
520
+ const jsonState = stringifySceneState(state);
521
+ const svgString = await renderGraphSvgFromState(state, !!isDark);
522
+ onInsert(jsonState, svgString);
523
+ } catch (err) {
524
+ console.error("[GraphEditorPanel] insert failed:", err);
525
+ }
526
+ })();
527
+ return true;
528
+ }, [isDark, onInsert]);
529
+ useImperativeHandle(ref, () => ({
530
+ insert: performInsert,
531
+ hasContent: () => Object.keys(miniRef.current?.getState().objects ?? {}).length > 0,
532
+ getStore: () => miniRef.current?.getStore() ?? null,
533
+ highlight: (id) => miniRef.current?.highlight(id)
534
+ }), [performInsert]);
535
+ const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
536
+ position: "absolute",
537
+ top: "50%",
538
+ left: withLeftPanel ? "calc(50% + 120px)" : "50%",
539
+ transform: "translate(-50%, -50%)",
540
+ zIndex: 40
541
+ };
542
+ return /* @__PURE__ */ jsxs(
543
+ "div",
544
+ {
545
+ role: "dialog",
546
+ "aria-label": "\u0110\u1ED3 th\u1ECB h\xE0m s\u1ED1",
547
+ "data-testid": "graph-editor-panel",
548
+ "data-stamp-area": "true",
549
+ "data-mobile-editor": isMobile ? "true" : void 0,
550
+ style: wrapperStyle,
551
+ className: [
552
+ isDark ? "theme--dark " : "",
553
+ "relative flex flex-col overflow-hidden bg-white",
554
+ isMobile ? "h-full w-full" : `${STAMP_PANEL_DESKTOP} rounded-lg border border-slate-300 shadow-2xl ring-1 ring-black/5`
555
+ ].join(" "),
556
+ children: [
557
+ /* @__PURE__ */ jsxs("header", { className: "flex items-center gap-2 border-b border-slate-200 bg-gradient-to-r from-emerald-600 to-teal-600 px-3 py-2 text-white", children: [
558
+ isMobile && /* @__PURE__ */ jsx(
559
+ "button",
560
+ {
561
+ type: "button",
562
+ onClick: onOpenDrawer,
563
+ "aria-label": "M\u1EDF ng\u0103n c\xF4ng c\u1EE5",
564
+ className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
565
+ children: /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
566
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
567
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
568
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
569
+ ] })
570
+ }
571
+ ),
572
+ /* @__PURE__ */ jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
573
+ /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
574
+ /* @__PURE__ */ jsx("path", { d: "M3 3 L3 21 L21 21" }),
575
+ /* @__PURE__ */ jsx("path", { d: "M6 14 Q9 8 12 10 Q15 12 18 6" })
576
+ ] }),
577
+ "\u0110\u1ED3 th\u1ECB h\xE0m s\u1ED1"
578
+ ] }),
579
+ isMobile && /* @__PURE__ */ jsxs(Fragment, { children: [
580
+ /* @__PURE__ */ jsx(
581
+ "button",
582
+ {
583
+ type: "button",
584
+ onClick: onUndo,
585
+ disabled: !canUndo,
586
+ "aria-label": "Ho\xE0n t\xE1c",
587
+ title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
588
+ "data-testid": "undo-btn-mobile",
589
+ className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15 disabled:opacity-40",
590
+ children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
591
+ /* @__PURE__ */ jsx("path", { d: "M3 10 L8 5 L8 8 L15 8 A5 5 0 0 1 20 13 L20 16" }),
592
+ /* @__PURE__ */ jsx("path", { d: "M3 10 L8 15 L8 12" })
593
+ ] })
594
+ }
595
+ ),
596
+ /* @__PURE__ */ jsx(
597
+ "button",
598
+ {
599
+ type: "button",
600
+ onClick: onRedo,
601
+ disabled: !canRedo,
602
+ "aria-label": "L\xE0m l\u1EA1i",
603
+ title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
604
+ "data-testid": "redo-btn-mobile",
605
+ className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15 disabled:opacity-40",
606
+ children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
607
+ /* @__PURE__ */ jsx("path", { d: "M21 10 L16 5 L16 8 L9 8 A5 5 0 0 0 4 13 L4 16" }),
608
+ /* @__PURE__ */ jsx("path", { d: "M21 10 L16 15 L16 12" })
609
+ ] })
610
+ }
611
+ ),
612
+ /* @__PURE__ */ jsx(
613
+ "button",
614
+ {
615
+ type: "button",
616
+ onClick: performInsert,
617
+ disabled: !ready || !hasContent,
618
+ title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
619
+ "data-testid": "graph-insert-btn-mobile",
620
+ className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
621
+ children: "Ch\xE8n"
622
+ }
623
+ )
624
+ ] }),
625
+ /* @__PURE__ */ jsx(
626
+ "button",
627
+ {
628
+ type: "button",
629
+ "data-testid": "graph-editor-close-btn",
630
+ onClick: onClose,
631
+ "aria-label": "\u0110\xF3ng",
632
+ className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15",
633
+ children: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
634
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
635
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
636
+ ] })
637
+ }
638
+ )
639
+ ] }),
640
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsx(
641
+ MiniBoard,
642
+ {
643
+ ref: miniRef,
644
+ store,
645
+ selectedTool,
646
+ showAxis,
647
+ showGrid,
648
+ isDark,
649
+ onReady: handleReady,
650
+ onSelectionChange: (id) => {
651
+ onSelectionChangeRef.current?.(id);
652
+ }
653
+ }
654
+ ) }),
655
+ !isMobile && /* @__PURE__ */ jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
656
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-slate-500", children: "Ch\u1ECDn c\xF4ng c\u1EE5 b\xEAn tr\xE1i, nh\u1EA5p tr\xEAn b\u1EA3ng \u0111\u1EC3 t\u01B0\u01A1ng t\xE1c." }),
657
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
658
+ /* @__PURE__ */ jsx(
659
+ "button",
660
+ {
661
+ type: "button",
662
+ onClick: onClose,
663
+ className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
664
+ children: "Hu\u1EF7"
665
+ }
666
+ ),
667
+ /* @__PURE__ */ jsx(
668
+ "button",
669
+ {
670
+ type: "button",
671
+ onClick: performInsert,
672
+ disabled: !ready || !hasContent,
673
+ title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
674
+ "data-testid": "graph-insert-btn",
675
+ className: "rounded bg-emerald-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-emerald-700 disabled:opacity-50",
676
+ children: "Ch\xE8n"
677
+ }
678
+ )
679
+ ] })
680
+ ] }),
681
+ /* @__PURE__ */ jsx(ToastHost, {})
682
+ ]
683
+ }
684
+ );
685
+ }
686
+ );
687
+ var GraphEditorPanel2 = forwardRef(
688
+ function GraphEditorPanel3(props, ref) {
689
+ return /* @__PURE__ */ jsx(ToastProvider, { children: /* @__PURE__ */ jsx(GraphEditorPanelInner, { ...props, ref }) });
690
+ }
691
+ );
692
+ var GROUPS = ["basic", "function", "analysis", "draw"];
693
+ var GROUP_LABELS = {
694
+ basic: "C\u01A1 b\u1EA3n",
695
+ function: "H\xE0m",
696
+ analysis: "Ph\xE2n t\xEDch",
697
+ draw: "V\u1EBD"
698
+ };
699
+ var C_POINT = "#2563eb";
700
+ var C_FUNC = "#059669";
701
+ var C_HELP = "#dc2626";
702
+ var wrap = (children) => /* @__PURE__ */ jsx("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children });
703
+ var Icon = {
704
+ move: wrap(/* @__PURE__ */ jsx("path", { d: "M5 3 L5 18 L9.5 14 L12 20 L14 19.2 L11.5 13.5 L17.5 13.5 Z", fill: "currentColor", fillOpacity: "0.12" })),
705
+ point: wrap(/* @__PURE__ */ jsxs(Fragment, { children: [
706
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2.4", fill: C_POINT, stroke: "none" }),
707
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "4", fill: "none" })
708
+ ] })),
709
+ slider: wrap(/* @__PURE__ */ jsxs(Fragment, { children: [
710
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12" }),
711
+ /* @__PURE__ */ jsx("circle", { cx: "15", cy: "12", r: "2.4", fill: "currentColor", stroke: "none" })
712
+ ] })),
713
+ pointOnCurve: wrap(/* @__PURE__ */ jsxs(Fragment, { children: [
714
+ /* @__PURE__ */ jsx("path", { d: "M3 18 Q8 4 14 14 T21 6" }),
715
+ /* @__PURE__ */ jsx("circle", { cx: "14", cy: "14", r: "2", fill: C_POINT, stroke: "none" })
716
+ ] })),
717
+ intersect: wrap(/* @__PURE__ */ jsxs(Fragment, { children: [
718
+ /* @__PURE__ */ jsx("path", { d: "M3 6 Q12 22 21 6" }),
719
+ /* @__PURE__ */ jsx("path", { d: "M3 18 Q12 2 21 18" }),
720
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1.6", fill: C_POINT, stroke: "none" })
721
+ ] })),
722
+ tangent: wrap(/* @__PURE__ */ jsxs(Fragment, { children: [
723
+ /* @__PURE__ */ jsx("path", { d: "M3 16 Q12 4 21 16", stroke: C_FUNC }),
724
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "10", x2: "20", y2: "10", stroke: C_HELP, strokeDasharray: "3 2" }),
725
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "10", r: "1.8", fill: C_POINT, stroke: "none" })
726
+ ] })),
727
+ slope: wrap(/* @__PURE__ */ jsxs(Fragment, { children: [
728
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "20", y2: "6" }),
729
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "14", y2: "20", stroke: C_HELP, strokeDasharray: "2 2" }),
730
+ /* @__PURE__ */ jsx("line", { x1: "14", y1: "20", x2: "14", y2: "11", stroke: C_HELP, strokeDasharray: "2 2" })
731
+ ] })),
732
+ extremum: wrap(/* @__PURE__ */ jsxs(Fragment, { children: [
733
+ /* @__PURE__ */ jsx("path", { d: "M3 20 Q9 4 15 16 T21 8" }),
734
+ /* @__PURE__ */ jsx("circle", { cx: "9", cy: "8.5", r: "1.8", fill: C_HELP, stroke: "none" }),
735
+ /* @__PURE__ */ jsx("circle", { cx: "15", cy: "16", r: "1.8", fill: C_HELP, stroke: "none" })
736
+ ] })),
737
+ root: wrap(/* @__PURE__ */ jsxs(Fragment, { children: [
738
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12" }),
739
+ /* @__PURE__ */ jsx("path", { d: "M5 6 Q9 18 12 12 Q15 6 19 18" }),
740
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1.6", fill: C_HELP, stroke: "none" })
741
+ ] })),
742
+ segment: wrap(/* @__PURE__ */ jsxs(Fragment, { children: [
743
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "18", x2: "19", y2: "6" }),
744
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "18", r: "1.4", fill: "currentColor", stroke: "none" }),
745
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "6", r: "1.4", fill: "currentColor", stroke: "none" })
746
+ ] })),
747
+ line: wrap(/* @__PURE__ */ jsxs(Fragment, { children: [
748
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "20", x2: "21", y2: "4" }),
749
+ /* @__PURE__ */ jsx("circle", { cx: "9", cy: "14.7", r: "1.4", fill: "currentColor", stroke: "none" }),
750
+ /* @__PURE__ */ jsx("circle", { cx: "15", cy: "9.3", r: "1.4", fill: "currentColor", stroke: "none" })
751
+ ] })),
752
+ polygon: wrap(/* @__PURE__ */ jsxs(Fragment, { children: [
753
+ /* @__PURE__ */ jsx("polygon", { points: "5,18 12,4 19,12 16,20 8,20", fill: "currentColor", fillOpacity: "0.12" }),
754
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "18", r: "1.2", fill: "currentColor", stroke: "none" }),
755
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "4", r: "1.2", fill: "currentColor", stroke: "none" }),
756
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "12", r: "1.2", fill: "currentColor", stroke: "none" })
757
+ ] }))
758
+ };
759
+ var TOOLS = [
760
+ { key: "move", label: "Di chuy\u1EC3n", hint: "Di chuy\u1EC3n / ch\u1ECDn", icon: Icon.move, group: "basic", shortcut: "S" },
761
+ { key: "point", label: "\u0110i\u1EC3m", hint: "T\u1EA1o \u0111i\u1EC3m t\u1EF1 do", icon: Icon.point, group: "basic", shortcut: "P" },
762
+ { key: "slider", label: "Slider", hint: "T\u1EA1o tham s\u1ED1", icon: Icon.slider, group: "basic", shortcut: "B" },
763
+ { key: "pointOnCurve", label: "\u0110i\u1EC3m tr\xEAn \u0111\u1ED3 th\u1ECB", hint: "T\u1EA1o \u0111i\u1EC3m tr\xEAn h\xE0m s\u1ED1", icon: Icon.pointOnCurve, group: "function", shortcut: "O" },
764
+ { key: "intersect", label: "Giao \u0111i\u1EC3m", hint: "Giao 2 \u0111\u1ED3 th\u1ECB", icon: Icon.intersect, group: "function", shortcut: "I" },
765
+ { key: "tangent", label: "Ti\u1EBFp tuy\u1EBFn", hint: "Ti\u1EBFp tuy\u1EBFn t\u1EA1i \u0111i\u1EC3m", icon: Icon.tangent, group: "function", shortcut: "T" },
766
+ { key: "slope", label: "H\u1EC7 s\u1ED1 g\xF3c", hint: "Slope triangle", icon: Icon.slope, group: "function", shortcut: "K" },
767
+ { key: "extremum", label: "C\u1EF1c tr\u1ECB", hint: "T\xECm c\u1EF1c tr\u1ECB trong kho\u1EA3ng", icon: Icon.extremum, group: "analysis", shortcut: "E" },
768
+ { key: "root", label: "Nghi\u1EC7m", hint: "T\xECm nghi\u1EC7m trong kho\u1EA3ng", icon: Icon.root, group: "analysis", shortcut: "R" },
769
+ { key: "segment", label: "\u0110o\u1EA1n th\u1EB3ng", hint: "V\u1EBD \u0111o\u1EA1n th\u1EB3ng", icon: Icon.segment, group: "draw", shortcut: "M" },
770
+ { key: "line", label: "\u0110\u01B0\u1EDDng th\u1EB3ng", hint: "V\u1EBD \u0111\u01B0\u1EDDng th\u1EB3ng", icon: Icon.line, group: "draw", shortcut: "L" },
771
+ { key: "polygon", label: "\u0110a gi\xE1c", hint: "V\u1EBD \u0111a gi\xE1c", icon: Icon.polygon, group: "draw", shortcut: "Y" }
772
+ ];
773
+ function FunctionRow({ obj, store, selected, onClick }) {
774
+ const [local, setLocal] = useState(obj.attrs.expression);
775
+ const [error, setError] = useState(null);
776
+ useEffect(() => {
777
+ setLocal(obj.attrs.expression);
778
+ setError(null);
779
+ }, [obj.attrs.expression]);
780
+ function commit(value) {
781
+ if (value === obj.attrs.expression) {
782
+ setError(null);
783
+ return;
784
+ }
785
+ const result = compile(value, {});
786
+ if (typeof result === "string") {
787
+ setError(result);
788
+ return;
789
+ }
790
+ setError(null);
791
+ store.dispatch({
792
+ type: "UPDATE_ATTRS",
793
+ payload: { id: obj.id, patch: { expression: value } }
794
+ });
795
+ }
796
+ function handleKeyDown(e) {
797
+ if (e.key === "Enter") {
798
+ e.preventDefault();
799
+ commit(local);
800
+ e.target.blur();
801
+ } else if (e.key === "Escape") {
802
+ setLocal(obj.attrs.expression);
803
+ setError(null);
804
+ e.target.blur();
805
+ }
806
+ }
807
+ function handleToggleVisible() {
808
+ store.dispatch({
809
+ type: "UPDATE_ATTRS",
810
+ payload: { id: obj.id, patch: { visible: !obj.attrs.visible } }
811
+ });
812
+ }
813
+ return /* @__PURE__ */ jsxs(
814
+ "li",
815
+ {
816
+ "data-testid": `function-row-${obj.id}`,
817
+ "aria-selected": selected,
818
+ onClick,
819
+ className: "flex items-center gap-1.5 border-b border-zinc-100 px-2 py-1 text-xs cursor-pointer dark:border-zinc-800 " + (selected ? "bg-slate-200" : "hover:bg-zinc-50 dark:hover:bg-zinc-900"),
820
+ children: [
821
+ /* @__PURE__ */ jsx(
822
+ "span",
823
+ {
824
+ className: "inline-block h-3 w-3 shrink-0 rounded-full",
825
+ style: { backgroundColor: obj.attrs.color },
826
+ "aria-hidden": "true"
827
+ }
828
+ ),
829
+ /* @__PURE__ */ jsxs("span", { className: "shrink-0 font-mono text-[11px] text-slate-700", children: [
830
+ obj.label,
831
+ "(x)\xA0="
832
+ ] }),
833
+ /* @__PURE__ */ jsx(
834
+ "input",
835
+ {
836
+ type: "text",
837
+ value: local,
838
+ onChange: (e) => {
839
+ setLocal(e.target.value);
840
+ setError(null);
841
+ },
842
+ onKeyDown: handleKeyDown,
843
+ onBlur: () => commit(local),
844
+ onClick: (e) => e.stopPropagation(),
845
+ className: [
846
+ "min-w-0 flex-1 rounded border px-1.5 py-0.5 font-mono text-xs outline-none focus:ring-1",
847
+ error ? "border-red-400 focus:ring-red-300" : "border-slate-300 focus:ring-blue-300"
848
+ ].join(" "),
849
+ "data-testid": `function-row-input-${obj.id}`,
850
+ "aria-label": "Bi\u1EC3u th\u1EE9c"
851
+ }
852
+ ),
853
+ error && /* @__PURE__ */ jsx(
854
+ "span",
855
+ {
856
+ "data-testid": `function-row-error-${obj.id}`,
857
+ className: "shrink-0 text-[10px] text-red-600",
858
+ title: error,
859
+ children: "\u26A0"
860
+ }
861
+ ),
862
+ /* @__PURE__ */ jsx(
863
+ "button",
864
+ {
865
+ type: "button",
866
+ "aria-label": "\u1EA8n/hi\u1EC7n h\xE0m",
867
+ "aria-pressed": !obj.attrs.visible,
868
+ onClick: (e) => {
869
+ e.stopPropagation();
870
+ handleToggleVisible();
871
+ },
872
+ className: "shrink-0 rounded px-1 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800",
873
+ children: obj.attrs.visible ? "\u{1F441}" : "\u{1F6AB}"
874
+ }
875
+ )
876
+ ]
877
+ }
878
+ );
879
+ }
880
+ function ParameterRow({ obj, store, selected, onClick }) {
881
+ const { value, min, max, step } = obj.attrs;
882
+ function handleSliderChange(e) {
883
+ const newVal = parseFloat(e.target.value);
884
+ if (!Number.isFinite(newVal)) return;
885
+ store.dispatch({
886
+ type: "UPDATE_ATTRS",
887
+ payload: { id: obj.id, patch: { value: newVal } }
888
+ });
889
+ }
890
+ return /* @__PURE__ */ jsxs(
891
+ "li",
892
+ {
893
+ "data-testid": `parameter-row-${obj.id}`,
894
+ "aria-selected": selected,
895
+ onClick,
896
+ className: "flex items-center gap-1.5 border-b border-zinc-100 px-2 py-1 text-xs cursor-pointer dark:border-zinc-800 " + (selected ? "bg-slate-200" : "hover:bg-zinc-50 dark:hover:bg-zinc-900"),
897
+ children: [
898
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 w-4 font-mono text-[11px] font-semibold text-slate-700", children: obj.label }),
899
+ /* @__PURE__ */ jsx(
900
+ "input",
901
+ {
902
+ type: "range",
903
+ min,
904
+ max,
905
+ step,
906
+ value,
907
+ onChange: handleSliderChange,
908
+ onClick: (e) => e.stopPropagation(),
909
+ className: "min-w-0 flex-1 accent-blue-600",
910
+ "data-testid": `parameter-row-slider-${obj.id}`,
911
+ "aria-label": `Tham s\u1ED1 ${obj.label}`
912
+ }
913
+ ),
914
+ /* @__PURE__ */ jsx(
915
+ "span",
916
+ {
917
+ "data-testid": `parameter-row-value-${obj.id}`,
918
+ className: "shrink-0 w-8 text-right font-mono text-[11px] text-slate-600",
919
+ children: Number.isInteger(value) ? value : parseFloat(value.toFixed(3))
920
+ }
921
+ )
922
+ ]
923
+ }
924
+ );
925
+ }
926
+ 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: [
927
+ /* @__PURE__ */ jsx("path", { d: "M3 3 L3 21 L21 21" }),
928
+ /* @__PURE__ */ jsx("path", { d: "M6 14 Q9 8 12 10 Q15 12 18 6" })
929
+ ] });
930
+ function makeRenderRow(store) {
931
+ return function renderRow(obj, defaults) {
932
+ if (obj.kind === "function2d") {
933
+ return /* @__PURE__ */ jsx(
934
+ FunctionRow,
935
+ {
936
+ obj,
937
+ store,
938
+ selected: defaults.selected,
939
+ onClick: defaults.onClick
940
+ }
941
+ );
942
+ }
943
+ if (obj.kind === "parameter") {
944
+ return /* @__PURE__ */ jsx(
945
+ ParameterRow,
946
+ {
947
+ obj,
948
+ store,
949
+ selected: defaults.selected,
950
+ onClick: defaults.onClick
951
+ }
952
+ );
953
+ }
954
+ return null;
955
+ };
956
+ }
957
+ function parseInitialState(data) {
958
+ if (!isGraph2DCustomData(data)) return null;
959
+ const state = parseSceneState(data.jsonState);
960
+ if (!state) {
961
+ console.warn("Graph2DStampHost: jsonState corrupted ho\u1EB7c kh\xF4ng h\u1EE3p l\u1EC7");
962
+ return null;
963
+ }
964
+ return state;
965
+ }
966
+ var Graph2DStampHost = forwardRef(
967
+ function Graph2DStampHost2({ api, editingElement, onClose, isDark }, ref) {
968
+ const panelRef = useRef(null);
969
+ const { isMobile } = useIsMobile();
970
+ const [drawerOpen, setDrawerOpen] = useState(false);
971
+ const sceneStore = useStampStore("graph2d", editingElement, parseInitialState);
972
+ const [selectedObjectId, setSelectedObjectId] = useState(void 0);
973
+ const initialMeta = sceneStore.getState().meta;
974
+ const initialView = initialMeta.domain === "graph2d" ? initialMeta.view : null;
975
+ const [selectedTool, setSelectedTool] = useState("move");
976
+ const [showAxis, setShowAxisState] = useState(initialView?.showAxis ?? true);
977
+ const [showGrid, setShowGridState] = useState(initialView?.showGrid ?? true);
978
+ const [canUndo, setCanUndo] = useState(false);
979
+ const [canRedo, setCanRedo] = useState(false);
980
+ const handleHistoryChange = useCallback((u, r) => {
981
+ setCanUndo(u);
982
+ setCanRedo(r);
983
+ }, []);
984
+ const handleUndo = useCallback(() => sceneStore.undo(), [sceneStore]);
985
+ const handleRedo = useCallback(() => sceneStore.redo(), [sceneStore]);
986
+ const handleShowAxisChange = useCallback((b) => {
987
+ setShowAxisState(b);
988
+ sceneStore.dispatch({ type: "UPDATE_VIEW", payload: { patch: { showAxis: b } } });
989
+ }, [sceneStore]);
990
+ const handleShowGridChange = useCallback((b) => {
991
+ setShowGridState(b);
992
+ sceneStore.dispatch({ type: "UPDATE_VIEW", payload: { patch: { showGrid: b } } });
993
+ }, [sceneStore]);
994
+ const handleAddFunction = useCallback(() => {
995
+ const existing = Object.values(sceneStore.getState().objects).filter((o) => o.kind === "function2d");
996
+ const id = `f${existing.length + 1}`;
997
+ sceneStore.dispatch({
998
+ type: "ADD",
999
+ payload: {
1000
+ obj: {
1001
+ id,
1002
+ kind: "function2d",
1003
+ label: id,
1004
+ visible: true,
1005
+ locked: false,
1006
+ layer: "default",
1007
+ schemaVersion: 1,
1008
+ attrs: { expression: "x", color: "#2563eb", visible: true }
1009
+ }
1010
+ }
1011
+ });
1012
+ }, [sceneStore]);
1013
+ const handleAddParameter = useCallback(() => {
1014
+ const existing = Object.values(sceneStore.getState().objects).filter((o) => o.kind === "parameter");
1015
+ const labels = "abcdefghijklmnopqrstuvwxyz";
1016
+ const usedLabels = new Set(existing.map((o) => o.label));
1017
+ let label = "a";
1018
+ for (const c of labels) {
1019
+ if (!usedLabels.has(c)) {
1020
+ label = c;
1021
+ break;
1022
+ }
1023
+ }
1024
+ const id = label;
1025
+ sceneStore.dispatch({
1026
+ type: "ADD",
1027
+ payload: {
1028
+ obj: {
1029
+ id,
1030
+ kind: "parameter",
1031
+ label,
1032
+ visible: true,
1033
+ locked: false,
1034
+ layer: "default",
1035
+ schemaVersion: 1,
1036
+ attrs: { value: 1, min: -5, max: 5, step: 0.1 }
1037
+ }
1038
+ }
1039
+ });
1040
+ }, [sceneStore]);
1041
+ const handleInsert = useCallback(
1042
+ async (jsonState, svgString) => {
1043
+ if (!api) return;
1044
+ try {
1045
+ await insertStampImage(api, {
1046
+ svgString,
1047
+ makeCustomData: () => ({
1048
+ kind: "graph2d",
1049
+ version: 2,
1050
+ jsonState
1051
+ }),
1052
+ editingElementId: editingElement?.id ?? null
1053
+ });
1054
+ } catch (err) {
1055
+ console.error("Graph2D insert failed:", err);
1056
+ }
1057
+ onClose();
1058
+ },
1059
+ [api, editingElement?.id, onClose]
1060
+ );
1061
+ useImperativeHandle(
1062
+ ref,
1063
+ () => ({
1064
+ tryInsert: () => panelRef.current?.insert() ?? false,
1065
+ hasContent: () => panelRef.current?.hasContent() ?? false
1066
+ }),
1067
+ []
1068
+ );
1069
+ const renderRow = useMemo(() => makeRenderRow(sceneStore), [sceneStore]);
1070
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1071
+ /* @__PURE__ */ jsx(
1072
+ StampLeftPanel,
1073
+ {
1074
+ title: "\u0110\u1ED3 th\u1ECB",
1075
+ icon: GraphIconHeader,
1076
+ onClose,
1077
+ isDark,
1078
+ testId: "stamp-left-panel",
1079
+ tools: TOOLS,
1080
+ groupOrder: GROUPS,
1081
+ groupLabels: GROUP_LABELS,
1082
+ activeTool: selectedTool,
1083
+ onToolChange: setSelectedTool,
1084
+ view: {
1085
+ showAxis,
1086
+ showGrid,
1087
+ onShowAxisChange: handleShowAxisChange,
1088
+ onShowGridChange: handleShowGridChange
1089
+ },
1090
+ history: {
1091
+ onUndo: handleUndo,
1092
+ canUndo,
1093
+ onRedo: handleRedo,
1094
+ canRedo
1095
+ },
1096
+ objects: {
1097
+ store: sceneStore,
1098
+ selectedObjectId,
1099
+ onObjectSelect: (id) => {
1100
+ setSelectedObjectId(id ?? void 0);
1101
+ panelRef.current?.highlight(id);
1102
+ },
1103
+ renderRow,
1104
+ addButtons: [
1105
+ { label: "+ H\xE0m f(x)", testId: "add-function-btn", onClick: handleAddFunction },
1106
+ { label: "+ Tham s\u1ED1", testId: "add-parameter-btn", onClick: handleAddParameter }
1107
+ ]
1108
+ },
1109
+ isMobile,
1110
+ drawerOpen,
1111
+ onDrawerClose: () => setDrawerOpen(false)
1112
+ }
1113
+ ),
1114
+ /* @__PURE__ */ jsx(
1115
+ GraphEditorPanel2,
1116
+ {
1117
+ ref: panelRef,
1118
+ store: sceneStore,
1119
+ onInsert: handleInsert,
1120
+ onClose,
1121
+ selectedTool,
1122
+ showAxis,
1123
+ showGrid,
1124
+ onHistoryChange: handleHistoryChange,
1125
+ isDark,
1126
+ withLeftPanel: !isMobile,
1127
+ isMobile,
1128
+ onOpenDrawer: () => setDrawerOpen(true),
1129
+ onUndo: handleUndo,
1130
+ onRedo: handleRedo,
1131
+ canUndo,
1132
+ canRedo,
1133
+ onSelectionChange: setSelectedObjectId
1134
+ }
1135
+ )
1136
+ ] });
1137
+ }
1138
+ );
1139
+
1140
+ export { Graph2DStampHost };
1141
+ //# sourceMappingURL=host-GKNQBBUE.mjs.map
1142
+ //# sourceMappingURL=host-GKNQBBUE.mjs.map