@xom11/whiteboard 0.6.5 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +87 -1
  2. package/dist/chunk-74VEEZBV.mjs +619 -0
  3. package/dist/chunk-74VEEZBV.mjs.map +1 -0
  4. package/dist/chunk-7P7SQFOW.mjs +39 -0
  5. package/dist/chunk-7P7SQFOW.mjs.map +1 -0
  6. package/dist/chunk-C6SCVOMC.mjs +111 -0
  7. package/dist/chunk-C6SCVOMC.mjs.map +1 -0
  8. package/dist/chunk-DU2NFHRR.mjs +103 -0
  9. package/dist/chunk-DU2NFHRR.mjs.map +1 -0
  10. package/dist/chunk-DU3RHKT5.mjs +44 -0
  11. package/dist/chunk-DU3RHKT5.mjs.map +1 -0
  12. package/dist/chunk-HTBLO5JO.mjs +41 -0
  13. package/dist/chunk-HTBLO5JO.mjs.map +1 -0
  14. package/dist/chunk-IUVV52HO.mjs +144 -0
  15. package/dist/chunk-IUVV52HO.mjs.map +1 -0
  16. package/dist/chunk-KEYZ5EZT.mjs +154 -0
  17. package/dist/chunk-KEYZ5EZT.mjs.map +1 -0
  18. package/dist/chunk-P2AOIF7S.mjs +40 -0
  19. package/dist/chunk-P2AOIF7S.mjs.map +1 -0
  20. package/dist/chunk-SBDMF4NQ.mjs +212 -0
  21. package/dist/chunk-SBDMF4NQ.mjs.map +1 -0
  22. package/dist/chunk-X5R72SSJ.mjs +52 -0
  23. package/dist/chunk-X5R72SSJ.mjs.map +1 -0
  24. package/dist/chunk-ZVN356JZ.mjs +58 -0
  25. package/dist/chunk-ZVN356JZ.mjs.map +1 -0
  26. package/dist/geometry-2d.d.mts +16 -0
  27. package/dist/geometry-2d.d.ts +16 -0
  28. package/dist/geometry-2d.js +3581 -0
  29. package/dist/geometry-2d.js.map +1 -0
  30. package/dist/geometry-2d.mjs +7 -0
  31. package/dist/geometry-2d.mjs.map +1 -0
  32. package/dist/geometry-3d.d.mts +16 -0
  33. package/dist/geometry-3d.d.ts +16 -0
  34. package/dist/geometry-3d.js +4105 -0
  35. package/dist/geometry-3d.js.map +1 -0
  36. package/dist/geometry-3d.mjs +7 -0
  37. package/dist/geometry-3d.mjs.map +1 -0
  38. package/dist/graph-2d.d.mts +16 -0
  39. package/dist/graph-2d.d.ts +16 -0
  40. package/dist/graph-2d.js +2019 -0
  41. package/dist/graph-2d.js.map +1 -0
  42. package/dist/graph-2d.mjs +6 -0
  43. package/dist/graph-2d.mjs.map +1 -0
  44. package/dist/host-LZH2FZ2N.mjs +1066 -0
  45. package/dist/host-LZH2FZ2N.mjs.map +1 -0
  46. package/dist/host-PIIDSMVE.mjs +3187 -0
  47. package/dist/host-PIIDSMVE.mjs.map +1 -0
  48. package/dist/host-VDNAJMLC.mjs +2864 -0
  49. package/dist/host-VDNAJMLC.mjs.map +1 -0
  50. package/dist/host-Z3TEJKZA.mjs +466 -0
  51. package/dist/host-Z3TEJKZA.mjs.map +1 -0
  52. package/dist/index.d.mts +30 -148
  53. package/dist/index.d.ts +30 -148
  54. package/dist/index.js +8370 -5614
  55. package/dist/index.js.map +1 -1
  56. package/dist/index.mjs +395 -7294
  57. package/dist/index.mjs.map +1 -1
  58. package/dist/latex.d.mts +15 -0
  59. package/dist/latex.d.ts +15 -0
  60. package/dist/latex.js +750 -0
  61. package/dist/latex.js.map +1 -0
  62. package/dist/latex.mjs +6 -0
  63. package/dist/latex.mjs.map +1 -0
  64. package/dist/types-CinstD7T.d.mts +110 -0
  65. package/dist/types-CinstD7T.d.ts +110 -0
  66. package/package.json +26 -7
@@ -0,0 +1,144 @@
1
+ "use client";
2
+ import { isGeometry3DCustomData, parseSerializedBoard3D, VIEW3D_ATTRS, GROUND_PLANE_RANGE, GROUND_PLANE_ATTRS } from './chunk-DU2NFHRR.mjs';
3
+ import { lazy } from 'react';
4
+ import { jsxs, jsx } from 'react/jsx-runtime';
5
+
6
+ // src/stamps/geometry-3d/render.ts
7
+ var OUTPUT_WIDTH = 1024;
8
+ var OUTPUT_HEIGHT = 768;
9
+ async function renderGeometry3DSvgFromState(jsonState) {
10
+ const state = parseSerializedBoard3D(jsonState);
11
+ const JXG = (await import('jsxgraph')).default;
12
+ const div = document.createElement("div");
13
+ div.style.cssText = `position:absolute;left:-9999px;top:-9999px;width:${OUTPUT_WIDTH}px;height:${OUTPUT_HEIGHT}px;`;
14
+ document.body.appendChild(div);
15
+ try {
16
+ JXG.Options.text.display = "internal";
17
+ const board = JXG.JSXGraph.initBoard(div, {
18
+ boundingbox: state.bbox,
19
+ axis: false,
20
+ showCopyright: false,
21
+ showNavigation: false,
22
+ renderer: "svg"
23
+ });
24
+ const baseAttrs = VIEW3D_ATTRS(false);
25
+ const view = board.create(
26
+ "view3d",
27
+ [
28
+ [-5, -5],
29
+ [10, 10],
30
+ [
31
+ [state.view.bbox3D[0], state.view.bbox3D[3]],
32
+ [state.view.bbox3D[1], state.view.bbox3D[4]],
33
+ [state.view.bbox3D[2], state.view.bbox3D[5]]
34
+ ]
35
+ ],
36
+ {
37
+ ...baseAttrs,
38
+ az: { ...baseAttrs.az, value: state.view.azimuth },
39
+ el: { ...baseAttrs.el, value: state.view.elevation }
40
+ }
41
+ );
42
+ if (!state.showAxes) {
43
+ view.defaultAxes = [];
44
+ }
45
+ try {
46
+ view.create(
47
+ "plane3d",
48
+ [
49
+ [0, 0, 0],
50
+ [1, 0, 0],
51
+ [0, 1, 0],
52
+ GROUND_PLANE_RANGE,
53
+ GROUND_PLANE_RANGE
54
+ ],
55
+ GROUND_PLANE_ATTRS(false)
56
+ );
57
+ } catch {
58
+ }
59
+ const idMap = /* @__PURE__ */ new Map();
60
+ for (const el of state.elements) {
61
+ const parents = el.parents.map(
62
+ (p) => typeof p === "string" && p.startsWith("@id:") ? idMap.get(p.slice(4)) : p
63
+ );
64
+ const obj = view.create(el.type, parents, {
65
+ ...el.attributes,
66
+ id: el.id,
67
+ name: el.label
68
+ });
69
+ idMap.set(el.id, obj);
70
+ }
71
+ const svg = div.querySelector("svg");
72
+ if (!svg) {
73
+ throw new Error("renderGeometry3DSvgFromState: SVG not produced");
74
+ }
75
+ const clone = svg.cloneNode(true);
76
+ clone.setAttribute("width", String(OUTPUT_WIDTH));
77
+ clone.setAttribute("height", String(OUTPUT_HEIGHT));
78
+ const svgString = new XMLSerializer().serializeToString(clone);
79
+ try {
80
+ JXG.JSXGraph.freeBoard(board);
81
+ } catch {
82
+ }
83
+ return { svgString, width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT };
84
+ } finally {
85
+ document.body.removeChild(div);
86
+ }
87
+ }
88
+ var Geometry3DStampHost = lazy(
89
+ () => import('./host-PIIDSMVE.mjs').then((m) => ({ default: m.Geometry3DStampHost }))
90
+ );
91
+ var Geometry3DIcon = /* @__PURE__ */ jsxs(
92
+ "svg",
93
+ {
94
+ width: "20",
95
+ height: "20",
96
+ viewBox: "0 0 24 24",
97
+ fill: "none",
98
+ stroke: "currentColor",
99
+ strokeWidth: "1.6",
100
+ strokeLinecap: "round",
101
+ strokeLinejoin: "round",
102
+ "aria-hidden": "true",
103
+ children: [
104
+ /* @__PURE__ */ jsx("path", { d: "M4 9 L4 20 L14 20 L14 9 Z" }),
105
+ /* @__PURE__ */ jsx("path", { d: "M4 9 L10 4 L20 4 L14 9 Z" }),
106
+ /* @__PURE__ */ jsx("path", { d: "M14 9 L20 4 L20 15 L14 20 Z" })
107
+ ]
108
+ }
109
+ );
110
+ var geometry3dStamp = {
111
+ kind: "geometry3d",
112
+ experimental: true,
113
+ shortcutKey: "d",
114
+ toolbarLabel: "D",
115
+ toolbarTitle: "H\xECnh 3D (D)",
116
+ toolbarIcon: Geometry3DIcon,
117
+ toolbarTestId: "stamp-toolbar-geometry3d",
118
+ matchesCustomData: isGeometry3DCustomData,
119
+ async renderSvgFromCustomData(data) {
120
+ if (!isGeometry3DCustomData(data)) {
121
+ throw new Error("geometry3dStamp.renderSvgFromCustomData: customData kh\xF4ng ph\u1EA3i geometry3d");
122
+ }
123
+ const { svgString } = await renderGeometry3DSvgFromState(data.jsonState);
124
+ return svgString;
125
+ },
126
+ restoreFileFromCustomData: async (element) => {
127
+ const data = element.customData;
128
+ const fileId = element.fileId;
129
+ if (!data || !fileId) return null;
130
+ if (!isGeometry3DCustomData(data)) return null;
131
+ try {
132
+ const { svgString } = await renderGeometry3DSvgFromState(data.jsonState);
133
+ const dataURL = `data:image/svg+xml;base64,${typeof btoa !== "undefined" ? btoa(unescape(encodeURIComponent(svgString))) : Buffer.from(svgString).toString("base64")}`;
134
+ return { fileId, dataURL, mimeType: "image/svg+xml" };
135
+ } catch {
136
+ return null;
137
+ }
138
+ },
139
+ Host: Geometry3DStampHost
140
+ };
141
+
142
+ export { geometry3dStamp };
143
+ //# sourceMappingURL=chunk-IUVV52HO.mjs.map
144
+ //# sourceMappingURL=chunk-IUVV52HO.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stamps/geometry-3d/render.ts","../src/stamps/geometry-3d/index.tsx"],"names":[],"mappings":"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","file":"chunk-IUVV52HO.mjs","sourcesContent":["\"use client\";\n\nimport { parseSerializedBoard3D } from './serialize';\nimport { GROUND_PLANE_ATTRS, GROUND_PLANE_RANGE, VIEW3D_ATTRS } from './editor/theme';\n\nexport interface RenderResult {\n svgString: string;\n width: number;\n height: number;\n}\n\nconst OUTPUT_WIDTH = 1024;\nconst OUTPUT_HEIGHT = 768;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype JxgObj = any;\n\nexport async function renderGeometry3DSvgFromState(\n jsonState: string,\n): Promise<RenderResult> {\n const state = parseSerializedBoard3D(jsonState);\n const JXG = (await import('jsxgraph')).default;\n\n const div = document.createElement('div');\n div.style.cssText = `position:absolute;left:-9999px;top:-9999px;width:${OUTPUT_WIDTH}px;height:${OUTPUT_HEIGHT}px;`;\n document.body.appendChild(div);\n\n try {\n JXG.Options.text.display = 'internal';\n\n const board = JXG.JSXGraph.initBoard(div, {\n boundingbox: state.bbox,\n axis: false,\n showCopyright: false,\n showNavigation: false,\n renderer: 'svg',\n }) as { create: (k: string, p: unknown[], a: unknown) => JxgObj };\n\n const baseAttrs = VIEW3D_ATTRS(false);\n const view: JxgObj = board.create(\n 'view3d',\n [\n [-5, -5],\n [10, 10],\n [\n [state.view.bbox3D[0], state.view.bbox3D[3]],\n [state.view.bbox3D[1], state.view.bbox3D[4]],\n [state.view.bbox3D[2], state.view.bbox3D[5]],\n ],\n ],\n {\n ...baseAttrs,\n az: { ...baseAttrs.az, value: state.view.azimuth },\n el: { ...baseAttrs.el, value: state.view.elevation },\n },\n );\n\n if (!state.showAxes) {\n (view as { defaultAxes?: unknown[] }).defaultAxes = [];\n }\n\n // XY ground plane through origin (matches editor's MiniBoard3D).\n try {\n (view as { create: (k: string, p: unknown[], a: unknown) => JxgObj }).create(\n 'plane3d',\n [\n [0, 0, 0],\n [1, 0, 0],\n [0, 1, 0],\n GROUND_PLANE_RANGE,\n GROUND_PLANE_RANGE,\n ],\n GROUND_PLANE_ATTRS(false),\n );\n } catch {\n /* swallow */\n }\n\n const idMap = new Map<string, JxgObj>();\n for (const el of state.elements) {\n const parents = el.parents.map((p) =>\n typeof p === 'string' && p.startsWith('@id:')\n ? idMap.get(p.slice(4))\n : p,\n );\n const obj = (\n view as { create: (k: string, p: unknown[], a: unknown) => JxgObj }\n ).create(el.type, parents, {\n ...el.attributes,\n id: el.id,\n name: el.label,\n });\n idMap.set(el.id, obj);\n }\n\n const svg = div.querySelector('svg');\n if (!svg) {\n throw new Error('renderGeometry3DSvgFromState: SVG not produced');\n }\n const clone = svg.cloneNode(true) as SVGElement;\n clone.setAttribute('width', String(OUTPUT_WIDTH));\n clone.setAttribute('height', String(OUTPUT_HEIGHT));\n const svgString = new XMLSerializer().serializeToString(clone);\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n JXG.JSXGraph.freeBoard(board as any);\n } catch {\n /* ignore teardown */\n }\n\n return { svgString, width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT };\n } finally {\n document.body.removeChild(div);\n }\n}\n","'use client';\n\nimport { lazy, type ReactNode } from 'react';\nimport {\n isGeometry3DCustomData,\n type Geometry3DCustomData,\n} from './serialize';\nimport { renderGeometry3DSvgFromState } from './render';\nimport type {\n RestoredStampFile,\n StampType,\n} from '../shared/types';\n\nexport { isGeometry3DCustomData };\nexport type { Geometry3DCustomData };\n\nconst Geometry3DStampHost = lazy(() =>\n import('./host').then((m) => ({ default: m.Geometry3DStampHost })),\n);\n\nconst Geometry3DIcon: ReactNode = (\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.6\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n {/* Mặt trước */}\n <path d=\"M4 9 L4 20 L14 20 L14 9 Z\" />\n {/* Mặt trên */}\n <path d=\"M4 9 L10 4 L20 4 L14 9 Z\" />\n {/* Mặt phải */}\n <path d=\"M14 9 L20 4 L20 15 L14 20 Z\" />\n </svg>\n);\n\nexport const geometry3dStamp: StampType = {\n kind: 'geometry3d',\n experimental: true,\n shortcutKey: 'd',\n toolbarLabel: 'D',\n toolbarTitle: 'Hình 3D (D)',\n toolbarIcon: Geometry3DIcon,\n toolbarTestId: 'stamp-toolbar-geometry3d',\n matchesCustomData: isGeometry3DCustomData,\n async renderSvgFromCustomData(data: unknown): Promise<string> {\n if (!isGeometry3DCustomData(data)) {\n throw new Error('geometry3dStamp.renderSvgFromCustomData: customData không phải geometry3d');\n }\n const { svgString } = await renderGeometry3DSvgFromState(data.jsonState);\n return svgString;\n },\n restoreFileFromCustomData: async (element): Promise<RestoredStampFile | null> => {\n const data = element.customData as Geometry3DCustomData | undefined;\n const fileId = (element as { fileId?: string | null }).fileId;\n if (!data || !fileId) return null;\n if (!isGeometry3DCustomData(data)) return null;\n try {\n const { svgString } = await renderGeometry3DSvgFromState(data.jsonState);\n const dataURL = `data:image/svg+xml;base64,${\n typeof btoa !== 'undefined'\n ? btoa(unescape(encodeURIComponent(svgString)))\n : Buffer.from(svgString).toString('base64')\n }`;\n return { fileId, dataURL, mimeType: 'image/svg+xml' };\n } catch {\n return null;\n }\n },\n Host: Geometry3DStampHost,\n};\n"]}
@@ -0,0 +1,154 @@
1
+ "use client";
2
+ import { paletteFor, resolveAttrColors } from './chunk-HTBLO5JO.mjs';
3
+
4
+ // src/stamps/geometry-2d/renderInline.ts
5
+ function renderGeometryToSvg(boardContainer) {
6
+ const svgEl = boardContainer.querySelector("svg");
7
+ if (!svgEl) throw new Error("renderGeometryToSvg: no SVG found in board container");
8
+ const clone = svgEl.cloneNode(true);
9
+ if (!clone.getAttribute("xmlns")) {
10
+ clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
11
+ }
12
+ return new XMLSerializer().serializeToString(clone);
13
+ }
14
+
15
+ // src/stamps/geometry-2d/serialize.ts
16
+ function serializeBoard(board, log, options = {}) {
17
+ return {
18
+ bbox: board.getBoundingBox(),
19
+ elements: log.map((e) => ({ type: e.type, args: e.args, attrs: e.attrs, id: e.id })),
20
+ showAxis: !!options.showAxis,
21
+ showGrid: !!options.showGrid
22
+ };
23
+ }
24
+ function createValueLabel(board, target) {
25
+ if (!board || !target) return null;
26
+ const e = (target.elType ?? target.type ?? "").toString().toLowerCase();
27
+ if (e === "segment" || e === "line" || e === "arrow") {
28
+ const p1 = target.point1, p2 = target.point2;
29
+ if (!p1 || !p2) return null;
30
+ return board.create("text", [
31
+ () => (p1.X() + p2.X()) / 2 + 0.15,
32
+ () => (p1.Y() + p2.Y()) / 2 + 0.25,
33
+ () => {
34
+ const len = Math.hypot(p2.X() - p1.X(), p2.Y() - p1.Y());
35
+ const name = typeof target.name === "string" && target.name ? target.name : "d";
36
+ return `${name} = ${len.toFixed(2)}`;
37
+ }
38
+ ], { fontSize: 12, color: "#dc2626", fixed: true, highlight: false });
39
+ }
40
+ if (e === "circle" || e === "circumcircle") {
41
+ const center = target.center ?? target.midpoint ?? target.point1;
42
+ if (!center) return null;
43
+ return board.create("text", [
44
+ () => center.X() + 0.3,
45
+ () => center.Y() + 0.3,
46
+ () => {
47
+ const r = typeof target.Radius === "function" ? target.Radius() : 0;
48
+ const name = typeof target.name === "string" && target.name ? target.name : "r";
49
+ return `${name} = ${r.toFixed(2)}`;
50
+ }
51
+ ], { fontSize: 12, color: "#dc2626", fixed: true, highlight: false });
52
+ }
53
+ return null;
54
+ }
55
+ function deserializeIntoBoard(board, serialized, options = {}) {
56
+ const palette = options.palette ?? paletteFor(false);
57
+ const idMap = /* @__PURE__ */ new Map();
58
+ const resolve = (a) => {
59
+ if (typeof a === "string" && idMap.has(a)) return idMap.get(a);
60
+ if (Array.isArray(a)) return a.map(resolve);
61
+ return a;
62
+ };
63
+ for (const el of serialized.elements) {
64
+ const resolvedArgs = el.args.map(resolve);
65
+ if (el.type === "valueLabel") {
66
+ const target = resolvedArgs[0];
67
+ const txt = createValueLabel(board, target);
68
+ if (txt) idMap.set(el.id, txt);
69
+ continue;
70
+ }
71
+ const themedAttrs = resolveAttrColors({ ...el.attrs }, palette);
72
+ const created = board.create(el.type, resolvedArgs, themedAttrs);
73
+ idMap.set(el.id, created);
74
+ }
75
+ }
76
+
77
+ // src/stamps/shared/safeJsx.ts
78
+ var isDev = (() => {
79
+ try {
80
+ return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
81
+ } catch {
82
+ return false;
83
+ }
84
+ })();
85
+ function safeJsx(label, fn, fallback) {
86
+ try {
87
+ return fn();
88
+ } catch (err) {
89
+ if (isDev) {
90
+ console.warn("[whiteboard:jsxgraph]", label, err);
91
+ }
92
+ return fallback;
93
+ }
94
+ }
95
+
96
+ // src/stamps/geometry-2d/render.ts
97
+ async function renderGeometrySvgFromState(jsonState) {
98
+ const parsed = JSON.parse(jsonState);
99
+ const palette = paletteFor(false);
100
+ const JXG = (await import('jsxgraph')).default;
101
+ safeJsx("render.applyOptions", () => {
102
+ const opts = JXG.Options;
103
+ if (opts) {
104
+ opts.text = opts.text || {};
105
+ opts.text.display = "internal";
106
+ opts.text.useASCIIMathML = false;
107
+ opts.text.useMathJax = false;
108
+ opts.text.useKatex = false;
109
+ opts.text.strokeColor = palette.label;
110
+ opts.label = opts.label || {};
111
+ opts.label.display = "internal";
112
+ opts.label.strokeColor = palette.label;
113
+ opts.axis = opts.axis || {};
114
+ opts.axis.strokeColor = palette.axis;
115
+ opts.grid = opts.grid || {};
116
+ opts.grid.strokeColor = palette.grid;
117
+ }
118
+ });
119
+ const container = document.createElement("div");
120
+ const containerId = "jxg_offscreen_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
121
+ container.id = containerId;
122
+ container.style.cssText = "position:absolute;top:-99999px;left:-99999px;width:400px;height:300px;visibility:hidden;pointer-events:none;";
123
+ document.body.appendChild(container);
124
+ let board = null;
125
+ try {
126
+ board = JXG.JSXGraph.initBoard(containerId, {
127
+ boundingbox: parsed.bbox,
128
+ axis: !!parsed.showAxis,
129
+ grid: !!parsed.showGrid,
130
+ showCopyright: false,
131
+ showNavigation: false,
132
+ keepAspectRatio: false
133
+ });
134
+ deserializeIntoBoard(board, parsed, { palette });
135
+ board.update();
136
+ return renderGeometryToSvg(container);
137
+ } finally {
138
+ safeJsx("render.freeBoard", () => {
139
+ if (board) JXG.JSXGraph.freeBoard(board);
140
+ });
141
+ if (container.parentNode) container.parentNode.removeChild(container);
142
+ }
143
+ }
144
+
145
+ // src/stamps/geometry-2d/types.ts
146
+ function isGeometryCustomData(data) {
147
+ if (!data || typeof data !== "object") return false;
148
+ const d = data;
149
+ return d.kind === "geometry" && d.version === 1 && typeof d.jsonState === "string";
150
+ }
151
+
152
+ export { isGeometryCustomData, renderGeometrySvgFromState, safeJsx, serializeBoard };
153
+ //# sourceMappingURL=chunk-KEYZ5EZT.mjs.map
154
+ //# sourceMappingURL=chunk-KEYZ5EZT.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stamps/geometry-2d/renderInline.ts","../src/stamps/geometry-2d/serialize.ts","../src/stamps/shared/safeJsx.ts","../src/stamps/geometry-2d/render.ts","../src/stamps/geometry-2d/types.ts"],"names":[],"mappings":"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","file":"chunk-KEYZ5EZT.mjs","sourcesContent":["export function renderGeometryToSvg(boardContainer: HTMLElement): string {\n const svgEl = boardContainer.querySelector('svg');\n if (!svgEl) throw new Error('renderGeometryToSvg: no SVG found in board container');\n const clone = svgEl.cloneNode(true) as SVGElement;\n if (!clone.getAttribute('xmlns')) {\n clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');\n }\n return new XMLSerializer().serializeToString(clone);\n}\n","// JSXGraph không có built-in getJSON. Component giữ MỘT LOG riêng của các create() call\n// do user trigger, pass log đó vào serializeBoard. Replay = gọi board.create() theo thứ tự log.\n//\n// type === 'transform': args là [refs đến điểm/đường/scalar], attrs là { type: 'translate'|'rotate'|'reflect'|'scale', ... }.\n// Object trả về (kết quả board.create('transform', ...)) được đăng ký vào idMap như mọi element khác\n// để point/line phụ thuộc reference được bằng id ('j5' → JSXGraph transform object).\n//\n// Log lưu màu dưới dạng sentinel ('@stroke', '@axis', '@grid', '@label') để\n// theme-neutral. Khi replay, palette resolve thành màu thực theo `isDark` hiện\n// tại (truyền qua options.palette).\n\nimport { paletteFor, resolveAttrColors, type GeomPalette } from './editor/theme';\n\nexport interface SerializedElement {\n type: string;\n args: unknown[];\n attrs: Record<string, unknown>;\n id: string;\n}\n\nexport interface SerializedBoard {\n bbox: [number, number, number, number];\n elements: SerializedElement[];\n showAxis?: boolean;\n showGrid?: boolean;\n}\n\ninterface BoardLike {\n getBoundingBox(): [number, number, number, number];\n create(type: string, args: unknown[], attrs: Record<string, unknown>): unknown;\n}\n\nexport function serializeBoard(\n board: BoardLike,\n log: SerializedElement[],\n options: { showAxis?: boolean; showGrid?: boolean } = {},\n): SerializedBoard {\n return {\n bbox: board.getBoundingBox(),\n elements: log.map(e => ({ type: e.type, args: e.args, attrs: e.attrs, id: e.id })),\n showAxis: !!options.showAxis,\n showGrid: !!options.showGrid,\n };\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction createValueLabel(board: any, target: any): unknown {\n if (!board || !target) return null;\n const e = (target.elType ?? target.type ?? '').toString().toLowerCase();\n if (e === 'segment' || e === 'line' || e === 'arrow') {\n const p1 = target.point1, p2 = target.point2;\n if (!p1 || !p2) return null;\n return board.create('text', [\n () => (p1.X() + p2.X()) / 2 + 0.15,\n () => (p1.Y() + p2.Y()) / 2 + 0.25,\n () => {\n const len = Math.hypot(p2.X() - p1.X(), p2.Y() - p1.Y());\n const name = typeof target.name === 'string' && target.name ? target.name : 'd';\n return `${name} = ${len.toFixed(2)}`;\n },\n ], { fontSize: 12, color: '#dc2626', fixed: true, highlight: false });\n }\n if (e === 'circle' || e === 'circumcircle') {\n const center = target.center ?? target.midpoint ?? target.point1;\n if (!center) return null;\n return board.create('text', [\n () => center.X() + 0.3,\n () => center.Y() + 0.3,\n () => {\n const r = typeof target.Radius === 'function' ? target.Radius() : 0;\n const name = typeof target.name === 'string' && target.name ? target.name : 'r';\n return `${name} = ${r.toFixed(2)}`;\n },\n ], { fontSize: 12, color: '#dc2626', fixed: true, highlight: false });\n }\n return null;\n}\n\nexport interface DeserializeOptions {\n /** Theme-aware palette để resolve sentinel attrs. Mặc định = light. */\n palette?: GeomPalette;\n}\n\nexport function deserializeIntoBoard(\n board: BoardLike,\n serialized: SerializedBoard,\n options: DeserializeOptions = {},\n): void {\n // Replay: args may contain references to earlier elements by our serialized id (\"j0\", \"j1\"…).\n // We resolve those to actual JSXGraph objects via a local id→object map. Nested\n // arrays are also resolved recursively — needed for dilate, which logs the\n // transform parent of a transformed point as [\"j2\",\"j3\",\"j4\"] (a chain of 3\n // transforms passed to `board.create('point', [src, [t1,t2,t3]])`).\n const palette = options.palette ?? paletteFor(false);\n const idMap = new Map<string, unknown>();\n const resolve = (a: unknown): unknown => {\n if (typeof a === 'string' && idMap.has(a)) return idMap.get(a);\n if (Array.isArray(a)) return a.map(resolve);\n return a;\n };\n for (const el of serialized.elements) {\n const resolvedArgs = el.args.map(resolve);\n if (el.type === 'valueLabel') {\n const target = resolvedArgs[0];\n const txt = createValueLabel(board, target);\n if (txt) idMap.set(el.id, txt);\n continue;\n }\n const themedAttrs = resolveAttrColors({ ...el.attrs }, palette);\n const created = board.create(el.type, resolvedArgs, themedAttrs);\n idMap.set(el.id, created);\n }\n}\n","const isDev = (() => {\n try {\n return typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';\n } catch {\n return false;\n }\n})();\n\n/**\n * Wrap JSXGraph operations that can throw on stale/missing state.\n * In dev mode: log to console. In prod: silent swallow + return fallback.\n *\n * @param label Short tag for grep-ability (vd \"removeObject\", \"board.update\").\n * @param fn Operation to execute.\n * @param fallback Value to return if fn throws (default: undefined).\n */\nexport function safeJsx<T>(label: string, fn: () => T): T | undefined;\nexport function safeJsx<T>(label: string, fn: () => T, fallback: T): T;\nexport function safeJsx<T>(label: string, fn: () => T, fallback?: T): T | undefined {\n try {\n return fn();\n } catch (err) {\n if (isDev) {\n // eslint-disable-next-line no-console\n console.warn('[whiteboard:jsxgraph]', label, err);\n }\n return fallback;\n }\n}\n","import { renderGeometryToSvg } from './renderInline';\nimport { deserializeIntoBoard, type SerializedBoard } from './serialize';\nimport { paletteFor } from './editor/theme';\nimport { safeJsx } from '../shared/safeJsx';\n\n/**\n * Re-render geometry SVG từ jsonState đã serialize. Dùng cho:\n * 1. Restore math-stamp file sau khi reload page (Excalidraw mất binary files).\n * 2. Generate SVG lúc INSERT (thay vì clone DOM với màu theo theme editor).\n *\n * LƯU Ý quan trọng — luôn dùng LIGHT palette (nét đậm). Excalidraw apply CSS\n * `filter: invert(93%) hue-rotate(180deg)` lên canvas trong dark mode → nét\n * đậm tự đảo thành sáng. Nếu ta bake nét sáng vào SVG cho dark mode, filter\n * sẽ đảo thành đậm → chìm vào nền tối. Giải pháp: luôn dùng nét đậm + để\n * Excalidraw tự lo invert.\n *\n * Implementation: tạo 1 div ẩn (off-screen, real dimensions để JSXGraph render\n * chuẩn), initBoard, replay creation log từ jsonState, dump SVG, dọn dẹp.\n *\n * Lý do JXG.Options.text.display = 'internal': JSXGraph mặc định render\n * label bằng HTML <div> overlay → clone SVG export sẽ thiếu label.\n */\nexport async function renderGeometrySvgFromState(jsonState: string): Promise<string> {\n const parsed = JSON.parse(jsonState) as SerializedBoard;\n // Stamps inserted vào Excalidraw canvas → luôn dùng light palette.\n // Excalidraw's THEME_FILTER tự đảo nét trong dark mode.\n const palette = paletteFor(false);\n const JXG = (await import('jsxgraph')).default;\n safeJsx('render.applyOptions', () => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const opts = (JXG as any).Options;\n if (opts) {\n opts.text = opts.text || {};\n opts.text.display = 'internal';\n opts.text.useASCIIMathML = false;\n opts.text.useMathJax = false;\n opts.text.useKatex = false;\n opts.text.strokeColor = palette.label;\n opts.label = opts.label || {};\n opts.label.display = 'internal';\n opts.label.strokeColor = palette.label;\n opts.axis = opts.axis || {};\n opts.axis.strokeColor = palette.axis;\n opts.grid = opts.grid || {};\n opts.grid.strokeColor = palette.grid;\n }\n });\n const container = document.createElement('div');\n const containerId = 'jxg_offscreen_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);\n container.id = containerId;\n container.style.cssText = 'position:absolute;top:-99999px;left:-99999px;width:400px;height:300px;visibility:hidden;pointer-events:none;';\n document.body.appendChild(container);\n let board: unknown = null;\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n board = (JXG as any).JSXGraph.initBoard(containerId, {\n boundingbox: parsed.bbox,\n axis: !!parsed.showAxis,\n grid: !!parsed.showGrid,\n showCopyright: false,\n showNavigation: false,\n keepAspectRatio: false,\n });\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n deserializeIntoBoard(board as any, parsed, { palette });\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (board as any).update();\n return renderGeometryToSvg(container);\n } finally {\n safeJsx('render.freeBoard', () => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if (board) (JXG as any).JSXGraph.freeBoard(board);\n });\n if (container.parentNode) container.parentNode.removeChild(container);\n }\n}\n","import type { BaseStampCustomData } from '../shared/types';\n\nexport interface GeometryCustomData extends BaseStampCustomData {\n kind: 'geometry';\n version: 1;\n jsonState: string;\n svgWidth: number;\n svgHeight: number;\n}\n\nexport function isGeometryCustomData(data: unknown): data is GeometryCustomData {\n if (!data || typeof data !== 'object') return false;\n const d = data as Partial<GeometryCustomData>;\n return d.kind === 'geometry' && d.version === 1 && typeof d.jsonState === 'string';\n}\n"]}
@@ -0,0 +1,40 @@
1
+ "use client";
2
+ import { useState, useEffect } from 'react';
3
+
4
+ // src/stamps/shared/useIsMobile.ts
5
+ var MOBILE_QUERY = "(max-width: 768px)";
6
+ var NO_HOVER_QUERY = "(hover: none)";
7
+ function readMatch(query) {
8
+ if (typeof window === "undefined" || !window.matchMedia) return false;
9
+ try {
10
+ return window.matchMedia(query).matches;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+ function useIsMobile() {
16
+ const [state, setState] = useState(() => ({
17
+ isMobile: readMatch(MOBILE_QUERY),
18
+ isTouchOnly: readMatch(NO_HOVER_QUERY)
19
+ }));
20
+ useEffect(() => {
21
+ if (typeof window === "undefined" || !window.matchMedia) return;
22
+ const mql = window.matchMedia(MOBILE_QUERY);
23
+ const tql = window.matchMedia(NO_HOVER_QUERY);
24
+ const update = () => {
25
+ setState({ isMobile: mql.matches, isTouchOnly: tql.matches });
26
+ };
27
+ update();
28
+ mql.addEventListener("change", update);
29
+ tql.addEventListener("change", update);
30
+ return () => {
31
+ mql.removeEventListener("change", update);
32
+ tql.removeEventListener("change", update);
33
+ };
34
+ }, []);
35
+ return state;
36
+ }
37
+
38
+ export { useIsMobile };
39
+ //# sourceMappingURL=chunk-P2AOIF7S.mjs.map
40
+ //# sourceMappingURL=chunk-P2AOIF7S.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stamps/shared/useIsMobile.ts"],"names":[],"mappings":";;;AAIA,IAAM,YAAA,GAAe,oBAAA;AACrB,IAAM,cAAA,GAAiB,eAAA;AASvB,SAAS,UAAU,KAAA,EAAwB;AACzC,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,CAAC,MAAA,CAAO,YAAY,OAAO,KAAA;AAChE,EAAA,IAAI;AACF,IAAA,OAAO,MAAA,CAAO,UAAA,CAAW,KAAK,CAAA,CAAE,OAAA;AAAA,EAClC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAMO,SAAS,WAAA,GAA2B;AACzC,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAAsB,OAAO;AAAA,IACrD,QAAA,EAAU,UAAU,YAAY,CAAA;AAAA,IAChC,WAAA,EAAa,UAAU,cAAc;AAAA,GACvC,CAAE,CAAA;AAEF,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,CAAC,OAAO,UAAA,EAAY;AACzD,IAAA,MAAM,GAAA,GAAM,MAAA,CAAO,UAAA,CAAW,YAAY,CAAA;AAC1C,IAAA,MAAM,GAAA,GAAM,MAAA,CAAO,UAAA,CAAW,cAAc,CAAA;AAC5C,IAAA,MAAM,SAAS,MAAM;AACnB,MAAA,QAAA,CAAS,EAAE,QAAA,EAAU,GAAA,CAAI,SAAS,WAAA,EAAa,GAAA,CAAI,SAAS,CAAA;AAAA,IAC9D,CAAA;AACA,IAAA,MAAA,EAAO;AACP,IAAA,GAAA,CAAI,gBAAA,CAAiB,UAAU,MAAM,CAAA;AACrC,IAAA,GAAA,CAAI,gBAAA,CAAiB,UAAU,MAAM,CAAA;AACrC,IAAA,OAAO,MAAM;AACX,MAAA,GAAA,CAAI,mBAAA,CAAoB,UAAU,MAAM,CAAA;AACxC,MAAA,GAAA,CAAI,mBAAA,CAAoB,UAAU,MAAM,CAAA;AAAA,IAC1C,CAAA;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,OAAO,KAAA;AACT","file":"chunk-P2AOIF7S.mjs","sourcesContent":["'use client';\n\nimport { useEffect, useState } from 'react';\n\nconst MOBILE_QUERY = '(max-width: 768px)';\nconst NO_HOVER_QUERY = '(hover: none)';\n\nexport interface MobileState {\n /** Viewport ≤ 768px — dùng để chuyển full-screen modal + drawer. */\n isMobile: boolean;\n /** Device không có hover (touch-only) — dùng để ẩn hover tooltips. */\n isTouchOnly: boolean;\n}\n\nfunction readMatch(query: string): boolean {\n if (typeof window === 'undefined' || !window.matchMedia) return false;\n try {\n return window.matchMedia(query).matches;\n } catch {\n return false;\n }\n}\n\n/**\n * SSR-safe mobile detection qua matchMedia. Trả về `{ isMobile, isTouchOnly }`.\n * Re-render khi viewport resize hoặc input modality đổi (e.g., gắn Bluetooth mouse).\n */\nexport function useIsMobile(): MobileState {\n const [state, setState] = useState<MobileState>(() => ({\n isMobile: readMatch(MOBILE_QUERY),\n isTouchOnly: readMatch(NO_HOVER_QUERY),\n }));\n\n useEffect(() => {\n if (typeof window === 'undefined' || !window.matchMedia) return;\n const mql = window.matchMedia(MOBILE_QUERY);\n const tql = window.matchMedia(NO_HOVER_QUERY);\n const update = () => {\n setState({ isMobile: mql.matches, isTouchOnly: tql.matches });\n };\n update();\n mql.addEventListener('change', update);\n tql.addEventListener('change', update);\n return () => {\n mql.removeEventListener('change', update);\n tql.removeEventListener('change', update);\n };\n }, []);\n\n return state;\n}\n"]}
@@ -0,0 +1,212 @@
1
+ "use client";
2
+ import { useState, useRef, useCallback, useEffect } from 'react';
3
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
4
+
5
+ // src/stamps/shared/useChordShortcut.ts
6
+ var A_CODE = "a".charCodeAt(0);
7
+ function isFieldFocused() {
8
+ const ae = typeof document !== "undefined" ? document.activeElement : null;
9
+ return !!(ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable));
10
+ }
11
+ function useChordShortcut(args) {
12
+ const { groupOrder, tools, onSelect, enabled } = args;
13
+ const [chordGroup, setChordGroup] = useState(null);
14
+ const groupOrderRef = useRef(groupOrder);
15
+ const toolsRef = useRef(tools);
16
+ const onSelectRef = useRef(onSelect);
17
+ const chordGroupRef = useRef(null);
18
+ groupOrderRef.current = groupOrder;
19
+ toolsRef.current = tools;
20
+ onSelectRef.current = onSelect;
21
+ const cancel = useCallback(() => {
22
+ chordGroupRef.current = null;
23
+ setChordGroup(null);
24
+ }, []);
25
+ useEffect(() => {
26
+ if (!enabled) return;
27
+ const setChord = (next) => {
28
+ chordGroupRef.current = next;
29
+ setChordGroup(next);
30
+ };
31
+ const onKey = (e) => {
32
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
33
+ if (isFieldFocused()) return;
34
+ const key = e.key;
35
+ const lower = key.length === 1 ? key.toLowerCase() : key;
36
+ if (key === "Escape") {
37
+ if (chordGroupRef.current !== null) {
38
+ e.preventDefault();
39
+ e.stopPropagation();
40
+ setChord(null);
41
+ }
42
+ return;
43
+ }
44
+ if (lower.length === 1 && lower >= "a" && lower <= "z") {
45
+ const idx = lower.charCodeAt(0) - A_CODE;
46
+ if (idx < groupOrderRef.current.length) {
47
+ e.preventDefault();
48
+ e.stopPropagation();
49
+ setChord(groupOrderRef.current[idx]);
50
+ }
51
+ return;
52
+ }
53
+ if (key >= "1" && key <= "9") {
54
+ const active = chordGroupRef.current;
55
+ if (active === null) return;
56
+ const n = key.charCodeAt(0) - "1".charCodeAt(0);
57
+ const toolsInGroup = toolsRef.current.filter(
58
+ (t) => t.group === active
59
+ );
60
+ e.preventDefault();
61
+ e.stopPropagation();
62
+ if (n < toolsInGroup.length) {
63
+ onSelectRef.current(toolsInGroup[n].key);
64
+ }
65
+ setChord(null);
66
+ return;
67
+ }
68
+ };
69
+ window.addEventListener("keydown", onKey, { capture: true });
70
+ return () => {
71
+ window.removeEventListener("keydown", onKey, { capture: true });
72
+ };
73
+ }, [enabled]);
74
+ return { chordGroup, cancel };
75
+ }
76
+ function MobileToolDrawer({
77
+ title,
78
+ headerIcon,
79
+ chips,
80
+ actions,
81
+ groups,
82
+ activeTool,
83
+ onToolSelect,
84
+ drawerOpen,
85
+ onDrawerClose,
86
+ isDark,
87
+ testId
88
+ }) {
89
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
90
+ drawerOpen && /* @__PURE__ */ jsx(
91
+ "div",
92
+ {
93
+ className: "stamp-drawer-backdrop",
94
+ onPointerDown: onDrawerClose,
95
+ "aria-hidden": "true"
96
+ }
97
+ ),
98
+ /* @__PURE__ */ jsxs(
99
+ "aside",
100
+ {
101
+ role: "complementary",
102
+ "aria-label": title,
103
+ "aria-hidden": !drawerOpen ? "true" : void 0,
104
+ "data-testid": testId,
105
+ "data-stamp-area": "true",
106
+ "data-mobile-drawer": "true",
107
+ "data-geo-mobile": "true",
108
+ "data-drawer-state": drawerOpen ? "open" : "closed",
109
+ className: [
110
+ isDark ? "theme--dark " : "",
111
+ "stamp-drawer-mobile flex flex-col border-r border-slate-200 bg-white shadow-md"
112
+ ].join(""),
113
+ children: [
114
+ /* @__PURE__ */ jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-4 py-3", children: [
115
+ /* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 text-base font-semibold text-slate-800", children: [
116
+ /* @__PURE__ */ jsx("span", { className: "inline-flex h-7 w-7 items-center justify-center rounded-lg bg-emerald-50 text-emerald-700", children: headerIcon }),
117
+ title
118
+ ] }),
119
+ /* @__PURE__ */ jsx(
120
+ "button",
121
+ {
122
+ type: "button",
123
+ onClick: onDrawerClose,
124
+ "aria-label": "\u0110\xF3ng ng\u0103n c\xF4ng c\u1EE5",
125
+ className: "inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
126
+ children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
127
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
128
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
129
+ ] })
130
+ }
131
+ )
132
+ ] }),
133
+ /* @__PURE__ */ jsxs("div", { className: "sticky top-0 z-10 flex items-center gap-2 border-b border-slate-200 bg-white/95 px-3 py-2 backdrop-blur", children: [
134
+ chips.map((c) => /* @__PURE__ */ jsxs(
135
+ "button",
136
+ {
137
+ type: "button",
138
+ role: "switch",
139
+ "aria-pressed": c.pressed,
140
+ "aria-label": c.label,
141
+ "data-testid": c.testId,
142
+ onClick: () => c.onToggle(!c.pressed),
143
+ className: "geo-mobile-chip",
144
+ children: [
145
+ c.icon,
146
+ c.label
147
+ ]
148
+ },
149
+ c.label
150
+ )),
151
+ actions.length > 0 && /* @__PURE__ */ jsx("div", { className: "ml-auto flex items-center gap-1", children: actions.map((a) => /* @__PURE__ */ jsx(
152
+ "button",
153
+ {
154
+ type: "button",
155
+ onClick: a.onClick,
156
+ disabled: a.disabled,
157
+ "aria-label": a.label,
158
+ title: a.title ?? a.label,
159
+ "data-testid": a.testId,
160
+ className: "inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
161
+ children: a.icon
162
+ },
163
+ a.label
164
+ )) })
165
+ ] }),
166
+ /* @__PURE__ */ jsx(
167
+ "div",
168
+ {
169
+ className: "min-h-0 flex-1 overflow-y-auto",
170
+ style: { paddingBottom: "calc(0.75rem + env(safe-area-inset-bottom))" },
171
+ children: groups.map((g) => /* @__PURE__ */ jsxs("section", { className: "px-3 pt-3 pb-1", children: [
172
+ /* @__PURE__ */ jsxs("h4", { className: "mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: [
173
+ /* @__PURE__ */ jsx("span", { className: "h-1 w-1 rounded-full bg-emerald-500" }),
174
+ g.groupLabel
175
+ ] }),
176
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-2", children: g.tools.map((t) => {
177
+ const active = activeTool === t.key;
178
+ return /* @__PURE__ */ jsxs(
179
+ "button",
180
+ {
181
+ type: "button",
182
+ "aria-label": t.label,
183
+ "aria-pressed": active,
184
+ "data-tool": t.key,
185
+ onClick: () => {
186
+ onToolSelect(t.key);
187
+ onDrawerClose();
188
+ },
189
+ className: [
190
+ "flex flex-col items-center justify-center gap-1.5 rounded-2xl px-2 py-3 transition active:scale-95",
191
+ active ? "geo-mobile-tool-active" : "bg-slate-50 text-slate-700 hover:bg-slate-100"
192
+ ].join(" "),
193
+ children: [
194
+ /* @__PURE__ */ jsx("span", { className: "flex h-6 w-6 items-center justify-center", children: t.icon }),
195
+ /* @__PURE__ */ jsx("span", { className: "text-center text-[11px] font-medium leading-tight line-clamp-2", children: t.label })
196
+ ]
197
+ },
198
+ t.key
199
+ );
200
+ }) })
201
+ ] }, g.group))
202
+ }
203
+ )
204
+ ]
205
+ }
206
+ )
207
+ ] });
208
+ }
209
+
210
+ export { MobileToolDrawer, useChordShortcut };
211
+ //# sourceMappingURL=chunk-SBDMF4NQ.mjs.map
212
+ //# sourceMappingURL=chunk-SBDMF4NQ.mjs.map