@xom11/whiteboard 0.24.2 → 0.27.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 (103) hide show
  1. package/README.md +84 -11
  2. package/dist/{ExcalidrawWithMenus-WENZRYYE.mjs → ExcalidrawWithMenus-2QPPTXJM.mjs} +3 -2
  3. package/dist/ExcalidrawWithMenus-2QPPTXJM.mjs.map +1 -0
  4. package/dist/ai.d.mts +3217 -434
  5. package/dist/ai.d.ts +3217 -434
  6. package/dist/ai.js +7679 -598
  7. package/dist/ai.js.map +1 -1
  8. package/dist/ai.mjs +5707 -679
  9. package/dist/ai.mjs.map +1 -1
  10. package/dist/catalog.json +5 -5
  11. package/dist/{chunk-7WQXXEVR.mjs → chunk-4ETJ4CDY.mjs} +5 -5
  12. package/dist/{chunk-7WQXXEVR.mjs.map → chunk-4ETJ4CDY.mjs.map} +1 -1
  13. package/dist/chunk-AJAHD35N.mjs +1708 -0
  14. package/dist/chunk-AJAHD35N.mjs.map +1 -0
  15. package/dist/chunk-AYJPOHCI.mjs +265 -0
  16. package/dist/chunk-AYJPOHCI.mjs.map +1 -0
  17. package/dist/chunk-B4NJJZFR.mjs +18 -0
  18. package/dist/chunk-B4NJJZFR.mjs.map +1 -0
  19. package/dist/{chunk-AZIARTGX.mjs → chunk-BNBOIDO5.mjs} +3 -3
  20. package/dist/{chunk-AZIARTGX.mjs.map → chunk-BNBOIDO5.mjs.map} +1 -1
  21. package/dist/{chunk-LVNCYP4J.mjs → chunk-CXHNVYMD.mjs} +5 -5
  22. package/dist/{chunk-LVNCYP4J.mjs.map → chunk-CXHNVYMD.mjs.map} +1 -1
  23. package/dist/{chunk-45CGKJ7S.mjs → chunk-D5JLJ3PT.mjs} +4 -4
  24. package/dist/{chunk-45CGKJ7S.mjs.map → chunk-D5JLJ3PT.mjs.map} +1 -1
  25. package/dist/{chunk-WM2VDYQA.mjs → chunk-D5LWSN2Y.mjs} +944 -196
  26. package/dist/chunk-D5LWSN2Y.mjs.map +1 -0
  27. package/dist/{chunk-KRC2XOIG.mjs → chunk-HLAOGXEK.mjs} +3 -3
  28. package/dist/{chunk-KRC2XOIG.mjs.map → chunk-HLAOGXEK.mjs.map} +1 -1
  29. package/dist/{chunk-2WF6KIGF.mjs → chunk-I3L56GVH.mjs} +212 -71
  30. package/dist/chunk-I3L56GVH.mjs.map +1 -0
  31. package/dist/{chunk-ZBJBQKJ2.mjs → chunk-IHUFOV7L.mjs} +4 -19
  32. package/dist/chunk-IHUFOV7L.mjs.map +1 -0
  33. package/dist/chunk-J5LGTIGS.mjs +10 -0
  34. package/dist/chunk-J5LGTIGS.mjs.map +1 -0
  35. package/dist/{chunk-BEZSQKPY.mjs → chunk-KYMBUTPO.mjs} +5 -4
  36. package/dist/chunk-KYMBUTPO.mjs.map +1 -0
  37. package/dist/{chunk-4DS3MKID.mjs → chunk-KZGPSTZI.mjs} +4 -4
  38. package/dist/{chunk-4DS3MKID.mjs.map → chunk-KZGPSTZI.mjs.map} +1 -1
  39. package/dist/{chunk-SGFJLHHG.mjs → chunk-PPKHCRRE.mjs} +3 -3
  40. package/dist/{chunk-SGFJLHHG.mjs.map → chunk-PPKHCRRE.mjs.map} +1 -1
  41. package/dist/{chunk-BKSXPNPQ.mjs → chunk-SZDAS7LK.mjs} +81 -3
  42. package/dist/chunk-SZDAS7LK.mjs.map +1 -0
  43. package/dist/chunk-T3SOHYB2.mjs +851 -0
  44. package/dist/chunk-T3SOHYB2.mjs.map +1 -0
  45. package/dist/geometry-2d.d.mts +2 -2
  46. package/dist/geometry-2d.d.ts +2 -2
  47. package/dist/geometry-2d.js +6288 -901
  48. package/dist/geometry-2d.js.map +1 -1
  49. package/dist/geometry-2d.mjs +7 -5
  50. package/dist/geometry-3d.d.mts +2 -2
  51. package/dist/geometry-3d.d.ts +2 -2
  52. package/dist/geometry-3d.js +1335 -253
  53. package/dist/geometry-3d.js.map +1 -1
  54. package/dist/geometry-3d.mjs +6 -4
  55. package/dist/graph-2d.d.mts +2 -2
  56. package/dist/graph-2d.d.ts +2 -2
  57. package/dist/graph-2d.js +1501 -342
  58. package/dist/graph-2d.js.map +1 -1
  59. package/dist/graph-2d.mjs +9 -7
  60. package/dist/handleExtractProblem-C-U5KluK.d.mts +158 -0
  61. package/dist/handleExtractProblem-C-U5KluK.d.ts +158 -0
  62. package/dist/{host-EPZCNFLH.mjs → host-HAYCJJ2T.mjs} +1390 -376
  63. package/dist/host-HAYCJJ2T.mjs.map +1 -0
  64. package/dist/{host-LKCMYEAV.mjs → host-LTJHAY5A.mjs} +12 -10
  65. package/dist/host-LTJHAY5A.mjs.map +1 -0
  66. package/dist/{host-ZIQ77W33.mjs → host-M26FS244.mjs} +8 -6
  67. package/dist/host-M26FS244.mjs.map +1 -0
  68. package/dist/{host-QS2EOTRJ.mjs → host-ZQCDAT6O.mjs} +3 -2
  69. package/dist/host-ZQCDAT6O.mjs.map +1 -0
  70. package/dist/index.d.mts +4 -3
  71. package/dist/index.d.ts +4 -3
  72. package/dist/index.js +6493 -1102
  73. package/dist/index.js.map +1 -1
  74. package/dist/index.mjs +24 -21
  75. package/dist/index.mjs.map +1 -1
  76. package/dist/latex.d.mts +2 -2
  77. package/dist/latex.d.ts +2 -2
  78. package/dist/latex.mjs +2 -1
  79. package/dist/render-ZX2O2IK7.mjs +10 -0
  80. package/dist/{render-SA4JTOW3.mjs.map → render-ZX2O2IK7.mjs.map} +1 -1
  81. package/dist/serialize-C3LSUMSA.mjs +9 -0
  82. package/dist/{serialize-JAVOU22E.mjs.map → serialize-C3LSUMSA.mjs.map} +1 -1
  83. package/dist/types-zc_Pa0mp.d.mts +418 -0
  84. package/dist/types-zc_Pa0mp.d.ts +418 -0
  85. package/package.json +10 -1
  86. package/dist/ExcalidrawWithMenus-WENZRYYE.mjs.map +0 -1
  87. package/dist/chunk-2WF6KIGF.mjs.map +0 -1
  88. package/dist/chunk-BEZSQKPY.mjs.map +0 -1
  89. package/dist/chunk-BKSXPNPQ.mjs.map +0 -1
  90. package/dist/chunk-CGZZO4BX.mjs +0 -96
  91. package/dist/chunk-CGZZO4BX.mjs.map +0 -1
  92. package/dist/chunk-WM2VDYQA.mjs.map +0 -1
  93. package/dist/chunk-ZBJBQKJ2.mjs.map +0 -1
  94. package/dist/host-EPZCNFLH.mjs.map +0 -1
  95. package/dist/host-LKCMYEAV.mjs.map +0 -1
  96. package/dist/host-QS2EOTRJ.mjs.map +0 -1
  97. package/dist/host-ZIQ77W33.mjs.map +0 -1
  98. package/dist/render-SA4JTOW3.mjs +0 -8
  99. package/dist/serialize-JAVOU22E.mjs +0 -7
  100. package/dist/types-Crbefnfe.d.ts +0 -128
  101. package/dist/types-DxlMPh-6.d.mts +0 -49
  102. package/dist/types-DxlMPh-6.d.ts +0 -49
  103. package/dist/types-vtvyKGAA.d.mts +0 -128
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { serializeScene } from './chunk-WM2VDYQA.mjs';
2
+ import { serializeScene } from './chunk-D5LWSN2Y.mjs';
3
3
 
4
4
  // src/stamps/graph-2d/serialize.ts
5
5
  function stringifySceneState(state) {
@@ -24,5 +24,5 @@ function parseSceneState(json) {
24
24
  }
25
25
 
26
26
  export { parseSceneState, stringifySceneState };
27
- //# sourceMappingURL=chunk-KRC2XOIG.mjs.map
28
- //# sourceMappingURL=chunk-KRC2XOIG.mjs.map
27
+ //# sourceMappingURL=chunk-HLAOGXEK.mjs.map
28
+ //# sourceMappingURL=chunk-HLAOGXEK.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/stamps/graph-2d/serialize.ts"],"names":[],"mappings":";;;AASO,SAAS,oBAAoB,KAAA,EAAsB;AACxD,EAAA,OAAO,eAAe,KAAK,CAAA;AAC7B;AAEO,SAAS,gBAAgB,IAAA,EAA4B;AAC1D,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI;AACF,IAAA,GAAA,GAAM,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,EACvB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI,CAAC,GAAA,IAAO,OAAO,GAAA,KAAQ,UAAU,OAAO,IAAA;AAC5C,EAAA,MAAM,CAAA,GAAI,GAAA;AACV,EAAA,IAAI,CAAA,CAAE,IAAA,EAAM,MAAA,KAAW,SAAA,EAAW,OAAO,IAAA;AACzC,EAAA,IAAI,CAAC,EAAE,IAAA,EAAM,IAAA,IAAQ,OAAO,CAAA,CAAE,IAAA,CAAK,IAAA,KAAS,QAAA,EAAU,OAAO,IAAA;AAC7D,EAAA,IAAI,OAAO,CAAA,CAAE,OAAA,KAAY,QAAA,EAAU,OAAO,IAAA;AAC1C,EAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,CAAA,CAAE,KAAK,GAAG,OAAO,IAAA;AACpC,EAAA,IAAI,CAAC,CAAA,CAAE,OAAA,IAAW,OAAO,CAAA,CAAE,OAAA,KAAY,UAAU,OAAO,IAAA;AACxD,EAAA,OAAO,GAAA;AACT","file":"chunk-KRC2XOIG.mjs","sourcesContent":["// src/stamps/graph-2d/serialize.ts\n//\n// graph-2d đã dùng plain State (không envelope) ngay từ đầu. Sau Tier D PR 3,\n// thin wrapper qua shared helper cho serialize. parseSceneState giữ behavior\n// null-on-invalid để host/index.tsx có thể discriminate \"customData hỏng\".\n\nimport { serializeScene } from '../shared/serializeScene';\nimport type { State } from '../../core/scene/types';\n\nexport function stringifySceneState(state: State): string {\n return serializeScene(state);\n}\n\nexport function parseSceneState(json: string): State | null {\n if (!json) return null;\n let raw: unknown;\n try {\n raw = JSON.parse(json);\n } catch {\n return null;\n }\n if (!raw || typeof raw !== 'object') return null;\n const v = raw as Partial<State>;\n if (v.meta?.domain !== 'graph2d') return null;\n if (!v.meta?.view || typeof v.meta.view !== 'object') return null;\n if (typeof v.counter !== 'number') return null;\n if (!Array.isArray(v.order)) return null;\n if (!v.objects || typeof v.objects !== 'object') return null;\n return raw as State;\n}\n"]}
1
+ {"version":3,"sources":["../src/stamps/graph-2d/serialize.ts"],"names":[],"mappings":";;;AASO,SAAS,oBAAoB,KAAA,EAAsB;AACxD,EAAA,OAAO,eAAe,KAAK,CAAA;AAC7B;AAEO,SAAS,gBAAgB,IAAA,EAA4B;AAC1D,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI;AACF,IAAA,GAAA,GAAM,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,EACvB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI,CAAC,GAAA,IAAO,OAAO,GAAA,KAAQ,UAAU,OAAO,IAAA;AAC5C,EAAA,MAAM,CAAA,GAAI,GAAA;AACV,EAAA,IAAI,CAAA,CAAE,IAAA,EAAM,MAAA,KAAW,SAAA,EAAW,OAAO,IAAA;AACzC,EAAA,IAAI,CAAC,EAAE,IAAA,EAAM,IAAA,IAAQ,OAAO,CAAA,CAAE,IAAA,CAAK,IAAA,KAAS,QAAA,EAAU,OAAO,IAAA;AAC7D,EAAA,IAAI,OAAO,CAAA,CAAE,OAAA,KAAY,QAAA,EAAU,OAAO,IAAA;AAC1C,EAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,CAAA,CAAE,KAAK,GAAG,OAAO,IAAA;AACpC,EAAA,IAAI,CAAC,CAAA,CAAE,OAAA,IAAW,OAAO,CAAA,CAAE,OAAA,KAAY,UAAU,OAAO,IAAA;AACxD,EAAA,OAAO,GAAA;AACT","file":"chunk-HLAOGXEK.mjs","sourcesContent":["// src/stamps/graph-2d/serialize.ts\n//\n// graph-2d đã dùng plain State (không envelope) ngay từ đầu. Sau Tier D PR 3,\n// thin wrapper qua shared helper cho serialize. parseSceneState giữ behavior\n// null-on-invalid để host/index.tsx có thể discriminate \"customData hỏng\".\n\nimport { serializeScene } from '../shared/serializeScene';\nimport type { State } from '../../core/scene/types';\n\nexport function stringifySceneState(state: State): string {\n return serializeScene(state);\n}\n\nexport function parseSceneState(json: string): State | null {\n if (!json) return null;\n let raw: unknown;\n try {\n raw = JSON.parse(json);\n } catch {\n return null;\n }\n if (!raw || typeof raw !== 'object') return null;\n const v = raw as Partial<State>;\n if (v.meta?.domain !== 'graph2d') return null;\n if (!v.meta?.view || typeof v.meta.view !== 'object') return null;\n if (typeof v.counter !== 'number') return null;\n if (!Array.isArray(v.order)) return null;\n if (!v.objects || typeof v.objects !== 'object') return null;\n return raw as State;\n}\n"]}
@@ -1,12 +1,30 @@
1
1
  "use client";
2
- import { listObjects } from './chunk-WM2VDYQA.mjs';
3
- import { createStore, getKind } from './chunk-ZBJBQKJ2.mjs';
2
+ import { listObjects } from './chunk-D5LWSN2Y.mjs';
3
+ import { createStore } from './chunk-IHUFOV7L.mjs';
4
4
  import { createEmptyState } from './chunk-73Q7ADVL.mjs';
5
- import * as React2 from 'react';
6
- import React2__default, { createContext, useRef, useReducer, useCallback, useEffect, useMemo, useContext, useState } from 'react';
7
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
+ import { getKind } from './chunk-B4NJJZFR.mjs';
6
+ import * as React from 'react';
7
+ import React__default, { createContext, useRef, useReducer, useCallback, useEffect, useMemo, useContext, useState } from 'react';
8
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
8
9
  import { createPortal } from 'react-dom';
9
10
 
11
+ var FALLBACK_DEFAULT_WIDTH = 240;
12
+ var FALLBACK_MIN_WIDTH = 220;
13
+ var FALLBACK_MAX_WIDTH = 480;
14
+ function clamp(n, min, max) {
15
+ return Math.max(min, Math.min(max, n));
16
+ }
17
+ function readStoredWidth(key, fallback, min, max) {
18
+ if (!key || typeof window === "undefined") return fallback;
19
+ try {
20
+ const raw = window.localStorage.getItem(key);
21
+ if (!raw) return fallback;
22
+ const n = parseInt(raw, 10);
23
+ if (Number.isFinite(n)) return clamp(n, min, max);
24
+ } catch {
25
+ }
26
+ return fallback;
27
+ }
10
28
  function CloseIcon() {
11
29
  return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
12
30
  /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
@@ -14,8 +32,64 @@ function CloseIcon() {
14
32
  ] });
15
33
  }
16
34
  function LeftPanelShell(props) {
17
- const { title, icon, onClose, isDark, tabs, activeTab, onTabChange, testId, children } = props;
35
+ const {
36
+ title,
37
+ icon,
38
+ onClose,
39
+ isDark,
40
+ tabs,
41
+ activeTab,
42
+ onTabChange,
43
+ testId,
44
+ resizable,
45
+ widthStorageKey,
46
+ defaultWidth,
47
+ minWidth,
48
+ maxWidth,
49
+ children
50
+ } = props;
18
51
  const showTabs = !!tabs && tabs.length >= 2;
52
+ const min = minWidth ?? FALLBACK_MIN_WIDTH;
53
+ const max = maxWidth ?? FALLBACK_MAX_WIDTH;
54
+ const initial = clamp(defaultWidth ?? FALLBACK_DEFAULT_WIDTH, min, max);
55
+ const [width, setWidth] = React.useState(
56
+ () => resizable ? readStoredWidth(widthStorageKey, initial, min, max) : initial
57
+ );
58
+ const widthRef = React.useRef(width);
59
+ widthRef.current = width;
60
+ React.useEffect(() => {
61
+ if (!resizable || !widthStorageKey || typeof window === "undefined") return;
62
+ try {
63
+ window.localStorage.setItem(widthStorageKey, String(width));
64
+ } catch {
65
+ }
66
+ }, [resizable, widthStorageKey, width]);
67
+ const onResizeStart = React.useCallback(
68
+ (e) => {
69
+ if (!resizable) return;
70
+ e.preventDefault();
71
+ const startX = e.clientX;
72
+ const startW = widthRef.current;
73
+ const onMove = (ev) => {
74
+ setWidth(clamp(startW + (ev.clientX - startX), min, max));
75
+ };
76
+ const onUp = () => {
77
+ window.removeEventListener("mousemove", onMove);
78
+ window.removeEventListener("mouseup", onUp);
79
+ document.body.style.cursor = "";
80
+ document.body.style.userSelect = "";
81
+ };
82
+ window.addEventListener("mousemove", onMove);
83
+ window.addEventListener("mouseup", onUp);
84
+ document.body.style.cursor = "ew-resize";
85
+ document.body.style.userSelect = "none";
86
+ },
87
+ [resizable, min, max]
88
+ );
89
+ const onResizeDoubleClick = React.useCallback(() => {
90
+ if (!resizable) return;
91
+ setWidth(initial);
92
+ }, [resizable, initial]);
19
93
  return /* @__PURE__ */ jsxs(
20
94
  "aside",
21
95
  {
@@ -23,10 +97,12 @@ function LeftPanelShell(props) {
23
97
  "aria-label": title,
24
98
  "data-testid": testId ?? "left-panel",
25
99
  "data-stamp-area": "true",
100
+ style: resizable ? { width: `${width}px` } : void 0,
26
101
  className: [
27
102
  isDark ? "theme--dark " : "",
28
- "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"
29
- ].join(""),
103
+ "absolute left-0 top-0 z-30 flex h-full flex-col border-r border-slate-200 bg-white shadow-md animate-in slide-in-from-left duration-200",
104
+ resizable ? "" : "w-60"
105
+ ].join(" "),
30
106
  children: [
31
107
  /* @__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: [
32
108
  /* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
@@ -61,6 +137,20 @@ function LeftPanelShell(props) {
61
137
  className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-3",
62
138
  children
63
139
  }
140
+ ),
141
+ resizable && /* @__PURE__ */ jsx(
142
+ "div",
143
+ {
144
+ role: "separator",
145
+ "aria-orientation": "vertical",
146
+ "aria-label": "K\xE9o \u0111\u1EC3 \u0111\u1ED5i r\u1ED9ng panel",
147
+ "data-testid": "left-panel-resizer",
148
+ onMouseDown: onResizeStart,
149
+ onDoubleClick: onResizeDoubleClick,
150
+ className: "group absolute right-0 top-0 z-40 h-full w-1.5 -mr-0.5 cursor-ew-resize select-none",
151
+ title: "K\xE9o \u0111\u1EC3 \u0111\u1ED5i r\u1ED9ng (double-click \u0111\u1EC3 reset)",
152
+ children: /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-y-0 right-0 w-px bg-slate-200 transition group-hover:bg-emerald-400 group-hover:w-0.5 group-active:bg-emerald-500 group-active:w-0.5" })
153
+ }
64
154
  )
65
155
  ]
66
156
  }
@@ -133,7 +223,7 @@ function getKindUiMeta(kind) {
133
223
  }
134
224
  function ObjectRowMenu(props) {
135
225
  const { locked, onToggleLocked, onRename, onChangeColor, onDelete } = props;
136
- const [open, setOpen] = React2.useState(false);
226
+ const [open, setOpen] = React.useState(false);
137
227
  return /* @__PURE__ */ jsxs("div", { className: "relative inline-block", children: [
138
228
  /* @__PURE__ */ jsx(
139
229
  "button",
@@ -203,11 +293,11 @@ function formatMeasure(items) {
203
293
  return items.map((it) => `${it.label} = ${it.value.toFixed(2)}`).join(", ");
204
294
  }
205
295
  function ObjectRow(props) {
206
- const { obj, state, selected, onSelect, onToggleVisible, onToggleLocked, onRename, onChangeColor, onDelete } = props;
296
+ const { obj, state, selected, onSelect, onToggleVisible, onToggleLocked, onRename, onChangeColor, onDelete, describe } = props;
207
297
  const meta = getKindUiMeta(obj.kind);
208
298
  let title = "";
209
299
  try {
210
- title = getKind(obj.kind).describe(obj, state);
300
+ title = describe ? describe(obj, state) : getKind(obj.kind).describe(obj, state);
211
301
  } catch {
212
302
  title = `${meta.displayName} ${obj.label}`;
213
303
  }
@@ -273,11 +363,11 @@ function ObjectRow(props) {
273
363
  }
274
364
  function ObjectListPanel(props) {
275
365
  const { store, selectedId, onSelect, renderRow } = props;
276
- const subscribe = React2.useCallback(
366
+ const subscribe = React.useCallback(
277
367
  (cb) => store.subscribe(() => cb()),
278
368
  [store]
279
369
  );
280
- const state = React2.useSyncExternalStore(subscribe, store.getState, store.getState);
370
+ const state = React.useSyncExternalStore(subscribe, store.getState, store.getState);
281
371
  const objects = listObjects(state);
282
372
  function handleSelect(id) {
283
373
  onSelect?.(id === selectedId ? null : id);
@@ -308,7 +398,7 @@ function ObjectListPanel(props) {
308
398
  if (renderRow) {
309
399
  const custom = renderRow(obj, { selected, onClick });
310
400
  if (custom != null) {
311
- return /* @__PURE__ */ jsx(React2.Fragment, { children: custom }, obj.id);
401
+ return /* @__PURE__ */ jsx(React.Fragment, { children: custom }, obj.id);
312
402
  }
313
403
  }
314
404
  return /* @__PURE__ */ jsx(
@@ -436,24 +526,115 @@ function useToolHoverTooltip() {
436
526
  }, []);
437
527
  return { hover, portalReady, showHover, hideHover };
438
528
  }
529
+ function normalize(s) {
530
+ return s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/đ/g, "d").replace(/Đ/g, "d");
531
+ }
532
+ function SearchIcon() {
533
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
534
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "7" }),
535
+ /* @__PURE__ */ jsx("line", { x1: "20", y1: "20", x2: "16.5", y2: "16.5" })
536
+ ] });
537
+ }
538
+ function ClearIcon() {
539
+ return /* @__PURE__ */ jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
540
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
541
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
542
+ ] });
543
+ }
544
+ function ToolResultList(props) {
545
+ const { tools, activeTool, onToolChange } = props;
546
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-0.5", "data-testid": "tool-result-list", children: tools.map((t) => {
547
+ const active = activeTool === t.key;
548
+ return /* @__PURE__ */ jsxs(
549
+ "button",
550
+ {
551
+ type: "button",
552
+ "data-tool": t.key,
553
+ "aria-label": t.label,
554
+ "aria-pressed": active,
555
+ onClick: () => onToolChange(t.key),
556
+ className: [
557
+ "flex items-center gap-2 rounded-md px-2 py-1.5 text-left transition",
558
+ active ? "bg-emerald-600 text-white" : "text-slate-700 hover:bg-slate-100"
559
+ ].join(" "),
560
+ children: [
561
+ /* @__PURE__ */ jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center", children: t.icon }),
562
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
563
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-[12px] font-medium leading-tight", children: t.label }),
564
+ t.hint && /* @__PURE__ */ jsx("span", { className: ["block truncate text-[10px] leading-tight", active ? "text-emerald-50" : "text-slate-400"].join(" "), children: t.hint })
565
+ ] })
566
+ ]
567
+ },
568
+ t.key
569
+ );
570
+ }) });
571
+ }
439
572
  function ToolGrid(props) {
440
573
  const { tools, groupOrder, groupLabels, activeTool, onToolChange, chord } = props;
441
574
  const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip();
575
+ const [query, setQuery] = useState("");
576
+ const normalizedQuery = useMemo(() => normalize(query.trim()), [query]);
577
+ const filteredTools = useMemo(() => {
578
+ if (!normalizedQuery) return tools;
579
+ return tools.filter((t) => {
580
+ if (normalize(t.label).includes(normalizedQuery)) return true;
581
+ if (t.hint && normalize(t.hint).includes(normalizedQuery)) return true;
582
+ return false;
583
+ });
584
+ }, [tools, normalizedQuery]);
442
585
  const grouped = useMemo(() => {
443
586
  var _a;
444
587
  const acc = {};
445
- for (const t of tools) {
588
+ for (const t of filteredTools) {
446
589
  (acc[_a = t.group] ?? (acc[_a] = [])).push(t);
447
590
  }
448
591
  return acc;
449
- }, [tools]);
592
+ }, [filteredTools]);
450
593
  const groupKeys = useMemo(
451
- () => groupOrder.filter((g) => grouped[g]),
594
+ () => groupOrder.filter((g) => grouped[g] && grouped[g].length > 0),
452
595
  [grouped, groupOrder]
453
596
  );
454
- const activeGroupTools = chord?.activeGroup ? grouped[chord.activeGroup] ?? null : null;
597
+ const noMatch = normalizedQuery !== "" && groupKeys.length === 0;
455
598
  return /* @__PURE__ */ jsxs(Fragment, { children: [
456
- groupKeys.map((group) => {
599
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
600
+ /* @__PURE__ */ jsx("span", { className: "pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-slate-400", children: /* @__PURE__ */ jsx(SearchIcon, {}) }),
601
+ /* @__PURE__ */ jsx(
602
+ "input",
603
+ {
604
+ type: "search",
605
+ value: query,
606
+ onChange: (e) => setQuery(e.target.value),
607
+ placeholder: "T\xECm c\xF4ng c\u1EE5\u2026",
608
+ "aria-label": "T\xECm c\xF4ng c\u1EE5",
609
+ "data-testid": "tool-search-input",
610
+ className: "w-full rounded-md border border-slate-200 bg-slate-50 py-1.5 pl-7 pr-7 text-[12px] text-slate-800 placeholder:text-slate-400 focus:border-emerald-400 focus:bg-white focus:outline-none focus:ring-1 focus:ring-emerald-300"
611
+ }
612
+ ),
613
+ query && /* @__PURE__ */ jsx(
614
+ "button",
615
+ {
616
+ type: "button",
617
+ onClick: () => setQuery(""),
618
+ "aria-label": "Xo\xE1 t\xECm ki\u1EBFm",
619
+ "data-testid": "tool-search-clear",
620
+ className: "absolute right-1.5 top-1/2 -translate-y-1/2 rounded p-0.5 text-slate-400 transition hover:bg-slate-200 hover:text-slate-700",
621
+ children: /* @__PURE__ */ jsx(ClearIcon, {})
622
+ }
623
+ )
624
+ ] }),
625
+ noMatch && /* @__PURE__ */ jsxs(
626
+ "div",
627
+ {
628
+ "data-testid": "tool-search-empty",
629
+ className: "rounded-md border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-center text-[11px] text-slate-500",
630
+ children: [
631
+ "Kh\xF4ng c\xF3 c\xF4ng c\u1EE5 n\xE0o kh\u1EDBp \u201C",
632
+ query.trim(),
633
+ "\u201D."
634
+ ]
635
+ }
636
+ ),
637
+ normalizedQuery !== "" && !noMatch ? /* @__PURE__ */ jsx(ToolResultList, { tools: filteredTools, activeTool, onToolChange }) : groupKeys.map((group) => {
457
638
  const isChordActive = chord?.activeGroup === group;
458
639
  const dimmed = chord?.activeGroup != null && !isChordActive;
459
640
  return /* @__PURE__ */ jsxs(
@@ -467,23 +648,10 @@ function ToolGrid(props) {
467
648
  dimmed ? "opacity-55" : "opacity-100"
468
649
  ].join(" "),
469
650
  children: [
470
- /* @__PURE__ */ jsxs("h4", { className: "mb-1.5 flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: [
471
- /* @__PURE__ */ jsx("span", { children: groupLabels[group] }),
472
- chord && /* @__PURE__ */ jsx(
473
- "span",
474
- {
475
- "data-testid": `chord-letter-${group}`,
476
- className: [
477
- "font-mono text-[10px] leading-none transition",
478
- isChordActive ? "text-emerald-700 font-bold" : "text-slate-400"
479
- ].join(" "),
480
- children: chord.letterForGroup(group)
481
- }
482
- )
483
- ] }),
484
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 gap-1", children: grouped[group].map((t, i) => {
651
+ /* @__PURE__ */ jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: groupLabels[group] }),
652
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 gap-1", children: grouped[group].map((t) => {
485
653
  const active = activeTool === t.key;
486
- return /* @__PURE__ */ jsxs(
654
+ return /* @__PURE__ */ jsx(
487
655
  "button",
488
656
  {
489
657
  type: "button",
@@ -500,20 +668,7 @@ function ToolGrid(props) {
500
668
  "relative flex h-10 items-center justify-center rounded-md transition",
501
669
  active ? "bg-emerald-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
502
670
  ].join(" "),
503
- children: [
504
- t.icon,
505
- chord && /* @__PURE__ */ jsx(
506
- "span",
507
- {
508
- "data-testid": `chord-num-${t.key}`,
509
- className: [
510
- "pointer-events-none absolute bottom-0 right-0.5 font-mono text-[9px] leading-none transition",
511
- active ? "text-white/70" : isChordActive ? "text-emerald-700 font-bold" : "text-slate-400"
512
- ].join(" "),
513
- children: i + 1
514
- }
515
- )
516
- ]
671
+ children: t.icon
517
672
  },
518
673
  t.key
519
674
  );
@@ -523,22 +678,6 @@ function ToolGrid(props) {
523
678
  group
524
679
  );
525
680
  }),
526
- chord?.activeGroup && activeGroupTools && /* @__PURE__ */ jsxs(
527
- "div",
528
- {
529
- "data-testid": "chord-hint",
530
- className: "mt-1 rounded border border-emerald-200 bg-emerald-50/60 px-2 py-1 text-[11px] leading-snug text-slate-600",
531
- children: [
532
- /* @__PURE__ */ jsx("span", { className: "font-mono font-semibold text-emerald-700", children: chord.letterForGroup(chord.activeGroup) }),
533
- /* @__PURE__ */ jsx("span", { className: "mx-1 text-slate-400", children: "\u2192" }),
534
- activeGroupTools.map((t, i) => /* @__PURE__ */ jsxs("span", { className: "mr-2 inline-block", children: [
535
- /* @__PURE__ */ jsx("span", { className: "font-mono font-semibold text-emerald-700", children: i + 1 }),
536
- /* @__PURE__ */ jsx("span", { className: "ml-1", children: t.label })
537
- ] }, t.key)),
538
- /* @__PURE__ */ jsx("span", { className: "text-slate-400", children: "Esc hu\u1EF7" })
539
- ]
540
- }
541
- ),
542
681
  portalReady && hover && typeof document !== "undefined" ? createPortal(
543
682
  /* @__PURE__ */ jsxs(
544
683
  "div",
@@ -599,6 +738,8 @@ function StampLeftPanelDesktop(props) {
599
738
  tabs: tabSpecs,
600
739
  activeTab: hasObjects ? tab : void 0,
601
740
  onTabChange: hasObjects ? setTab : void 0,
741
+ resizable: true,
742
+ widthStorageKey: "xom11.stamp-left-panel.width",
602
743
  children: !hasObjects || tab === "tools" ? /* @__PURE__ */ jsxs(Fragment, { children: [
603
744
  /* @__PURE__ */ jsx(AxisGridSection, { view, history }),
604
745
  /* @__PURE__ */ jsx(
@@ -651,9 +792,9 @@ function MobileToolDrawer({
651
792
  testId,
652
793
  objectsTab
653
794
  }) {
654
- const [mobileTab, setMobileTab] = React2__default.useState("tools");
655
- const prevOpenRef = React2__default.useRef(drawerOpen);
656
- React2__default.useEffect(() => {
795
+ const [mobileTab, setMobileTab] = React__default.useState("tools");
796
+ const prevOpenRef = React__default.useRef(drawerOpen);
797
+ React__default.useEffect(() => {
657
798
  if (!prevOpenRef.current && drawerOpen) setMobileTab("tools");
658
799
  prevOpenRef.current = drawerOpen;
659
800
  }, [drawerOpen]);
@@ -1163,6 +1304,6 @@ async function initJxgBoard(target, config) {
1163
1304
  return { JXG, board, cleanup };
1164
1305
  }
1165
1306
 
1166
- export { STAMP_PANEL_DESKTOP, StampLeftPanel, ToastHost, ToastProvider, attachJxgWheelZoom, initJxgBoard, safeJsx, useStampStore, useToast };
1167
- //# sourceMappingURL=chunk-2WF6KIGF.mjs.map
1168
- //# sourceMappingURL=chunk-2WF6KIGF.mjs.map
1307
+ export { ObjectRow, STAMP_PANEL_DESKTOP, StampLeftPanel, ToastHost, ToastProvider, attachJxgWheelZoom, initJxgBoard, safeJsx, useStampStore, useToast };
1308
+ //# sourceMappingURL=chunk-I3L56GVH.mjs.map
1309
+ //# sourceMappingURL=chunk-I3L56GVH.mjs.map