@topgrid/grid-pro-master 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # @topgrid/grid-pro-master
2
+
3
+ Pro: Master-Detail, TreeGrid, Context Menu
4
+
5
+ Provides master-detail row expansion, tree grid, and right-click context menu for TOMIS Grid — expand rows to reveal nested detail content, display hierarchical tree data, and add context actions to rows.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pnpm add @topgrid/grid-pro-master
11
+ # or
12
+ npm install @topgrid/grid-pro-master
13
+ ```
14
+
15
+ ## License Activation
16
+
17
+ > **This is a Pro package requiring a valid license key.**
18
+
19
+ ```tsx
20
+ import { setLicenseKey } from '@topgrid/grid-license';
21
+
22
+ // Call once at your app entry point (e.g., main.tsx)
23
+ setLicenseKey('YOUR-LICENSE-KEY');
24
+ ```
25
+
26
+ Without a valid license, the component will render a watermark.
27
+ Contact [sales@topvel.com](mailto:sales@topvel.com) to obtain a license key.
28
+
29
+ ## Peer Dependencies
30
+
31
+ | Package | Version |
32
+ |---------|---------|
33
+ | `@tanstack/react-table` | `^8.0.0` |
34
+ | `@topgrid/grid-core` | `workspace:*` |
35
+ | `react` | `^18.0.0 \|\| ^19.0.0` |
36
+ | `react-dom` | `^18.0.0 \|\| ^19.0.0` |
37
+
38
+ ## Usage
39
+
40
+ ### Master-Detail
41
+
42
+ ```tsx
43
+ import { setLicenseKey } from '@topgrid/grid-license';
44
+ import { MasterDetailGrid } from '@topgrid/grid-pro-master';
45
+
46
+ setLicenseKey('YOUR-LICENSE-KEY');
47
+
48
+ export function OrdersGrid({ orders }) {
49
+ return (
50
+ <MasterDetailGrid
51
+ columns={columns}
52
+ data={orders}
53
+ renderDetailRow={({ row }) => (
54
+ <div>
55
+ <h4>Order Items</h4>
56
+ <ul>
57
+ {row.original.items.map((item) => (
58
+ <li key={item.id}>{item.name} × {item.qty}</li>
59
+ ))}
60
+ </ul>
61
+ </div>
62
+ )}
63
+ />
64
+ );
65
+ }
66
+ ```
67
+
68
+ ### Context Menu
69
+
70
+ ```tsx
71
+ import { ContextMenuGrid } from '@topgrid/grid-pro-master';
72
+
73
+ export function GridWithMenu({ data }) {
74
+ return (
75
+ <ContextMenuGrid
76
+ columns={columns}
77
+ data={data}
78
+ contextMenuItems={[
79
+ { label: 'Edit', onClick: (row) => openEdit(row) },
80
+ { label: 'Delete', onClick: (row) => deleteRow(row) },
81
+ ]}
82
+ />
83
+ );
84
+ }
85
+ ```
86
+
87
+ ## Main API
88
+
89
+ | Export | Description |
90
+ |--------|-------------|
91
+ | `MasterDetailGrid` | Grid with expandable detail rows |
92
+ | `ContextMenuGrid` | Grid with right-click context menu |
93
+ | `useExpandedPersistence` | Hook to persist row expansion state |
94
+ | `TreeGrid` | Re-export of tree grid from `@topgrid/grid-core` |
95
+ | `ColumnPinGrid` | Re-export of column pin grid from `@topgrid/grid-core` |
96
+ | `MasterDetailGridProps` | Props type |
97
+ | `ContextMenuGridProps` | Props type |
98
+ | `ContextMenuItem` | Context menu item type |
99
+
100
+ ## License
101
+
102
+ SEE LICENSE IN [EULA.md](./EULA.md)
103
+
104
+ License terms subject to change. Contact [sales@topvel.com](mailto:sales@topvel.com) for current EULA.
105
+
106
+ ---
107
+
108
+ [Documentation](https://grid.tomis.dev) | [Pricing](https://topvel.com/grid/pricing)
package/dist/index.cjs ADDED
@@ -0,0 +1,571 @@
1
+ 'use strict';
2
+
3
+ var gridLicense = require('@topgrid/grid-license');
4
+ var react = require('react');
5
+ var reactTable = require('@tanstack/react-table');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+ var reactDom = require('react-dom');
8
+ var storage = require('@topgrid/grid-core/internal/storage');
9
+ var gridCore = require('@topgrid/grid-core');
10
+
11
+ // src/index.ts
12
+ var INDENT_PX = 16;
13
+ function ExpandToggleCell({ isExpanded, depth, onToggle }) {
14
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center", style: { paddingLeft: `${depth * INDENT_PX}px` }, children: /* @__PURE__ */ jsxRuntime.jsx(
15
+ "button",
16
+ {
17
+ onClick: (e) => {
18
+ e.stopPropagation();
19
+ onToggle(e);
20
+ },
21
+ className: "text-gray-500 hover:text-gray-700 focus:outline-none",
22
+ "aria-label": isExpanded ? "\uC811\uAE30" : "\uD3BC\uCE58\uAE30",
23
+ "aria-expanded": isExpanded,
24
+ children: isExpanded ? "\u25BC" : "\u25B6"
25
+ }
26
+ ) });
27
+ }
28
+ function DetailRow({ row, colSpan, renderDetailRow }) {
29
+ return /* @__PURE__ */ jsxRuntime.jsx("tr", { "data-detail-row": true, className: "bg-gray-50", children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan, className: "px-4 py-2", children: renderDetailRow(row) }) });
30
+ }
31
+ function shouldToggleExpand(key) {
32
+ return key === "Enter" || key === " ";
33
+ }
34
+ function useRowKeyboardNav(row, enabled) {
35
+ const canExpand = row.getCanExpand();
36
+ const onKeyDown = react.useCallback(
37
+ (event) => {
38
+ if (shouldToggleExpand(event.key)) {
39
+ event.preventDefault();
40
+ row.toggleExpanded();
41
+ }
42
+ },
43
+ // Re-create only when row reference changes (row.toggleExpanded is bound to the row instance).
44
+ [row]
45
+ );
46
+ if (enabled === false || !canExpand) {
47
+ return {};
48
+ }
49
+ return { tabIndex: 0, onKeyDown };
50
+ }
51
+ var EXPAND_COLUMN_ID = "__expand__";
52
+ function buildExpandColumn(renderDetailRowProvided) {
53
+ if (!renderDetailRowProvided) return null;
54
+ return {
55
+ id: EXPAND_COLUMN_ID,
56
+ header: () => null,
57
+ cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx(
58
+ ExpandToggleCell,
59
+ {
60
+ isExpanded: row.getIsExpanded(),
61
+ depth: row.depth,
62
+ onToggle: () => row.toggleExpanded()
63
+ }
64
+ ),
65
+ size: 40,
66
+ enableSorting: false,
67
+ enableColumnFilter: false
68
+ };
69
+ }
70
+ function keysToExpandedState(keys) {
71
+ return keys.reduce((acc, key) => {
72
+ acc[key] = true;
73
+ return acc;
74
+ }, {});
75
+ }
76
+ function expandedStateToKeys(state) {
77
+ if (state === true) return [];
78
+ return Object.keys(state).filter((k) => state[k] === true);
79
+ }
80
+ function MasterRow({
81
+ row,
82
+ renderDetailRow,
83
+ colCount,
84
+ onRowClick,
85
+ onRowDoubleClick,
86
+ onCellClick
87
+ }) {
88
+ const keyboardProps = useRowKeyboardNav(row, renderDetailRow !== void 0);
89
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
90
+ /* @__PURE__ */ jsxRuntime.jsx(
91
+ "tr",
92
+ {
93
+ ...keyboardProps,
94
+ className: "border-b border-gray-100 hover:bg-gray-50 focus:outline-none focus-visible:outline-2 focus-visible:outline-blue-500",
95
+ onClick: onRowClick !== void 0 ? (e) => onRowClick(row.original, e) : void 0,
96
+ onDoubleClick: onRowDoubleClick !== void 0 ? (e) => onRowDoubleClick(row.original, e) : void 0,
97
+ children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsxRuntime.jsx(
98
+ "td",
99
+ {
100
+ className: "px-4 py-2",
101
+ onClick: onCellClick !== void 0 ? (e) => onCellClick(cell, row.original, e) : void 0,
102
+ children: reactTable.flexRender(cell.column.columnDef.cell, cell.getContext())
103
+ },
104
+ cell.id
105
+ ))
106
+ }
107
+ ),
108
+ row.getIsExpanded() && renderDetailRow !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
109
+ DetailRow,
110
+ {
111
+ row,
112
+ colSpan: colCount,
113
+ renderDetailRow
114
+ }
115
+ )
116
+ ] });
117
+ }
118
+ function MasterDetailGridInner(props, ref) {
119
+ const _lic = gridLicense.useLicenseStatus();
120
+ const { renderDetailRow, masterDetail } = props;
121
+ const isControlled = masterDetail?.expandedRowKeys !== void 0;
122
+ const [internalExpanded, setInternalExpanded] = react.useState(
123
+ () => isControlled ? keysToExpandedState(masterDetail.expandedRowKeys) : {}
124
+ );
125
+ react.useEffect(() => {
126
+ if (isControlled && masterDetail?.expandedRowKeys !== void 0) {
127
+ setInternalExpanded(keysToExpandedState(masterDetail.expandedRowKeys));
128
+ }
129
+ }, [masterDetail?.expandedRowKeys]);
130
+ const expanded = internalExpanded;
131
+ function handleExpandedChange(updaterOrValue) {
132
+ setInternalExpanded((prev) => {
133
+ const next = typeof updaterOrValue === "function" ? updaterOrValue(prev) : updaterOrValue;
134
+ if (masterDetail?.onExpandChange !== void 0) {
135
+ masterDetail.onExpandChange(expandedStateToKeys(next));
136
+ }
137
+ return next;
138
+ });
139
+ }
140
+ const expandCol = buildExpandColumn(renderDetailRow !== void 0);
141
+ const effectiveColumns = expandCol ? [expandCol, ...props.columns] : [...props.columns];
142
+ const table = reactTable.useReactTable({
143
+ data: props.data,
144
+ columns: effectiveColumns,
145
+ state: {
146
+ expanded
147
+ },
148
+ onExpandedChange: handleExpandedChange,
149
+ getCoreRowModel: reactTable.getCoreRowModel(),
150
+ getExpandedRowModel: reactTable.getExpandedRowModel(),
151
+ // C-29 spread-skip for exactOptionalPropertyTypes: true
152
+ ...props.getSubRows !== void 0 ? { getSubRows: props.getSubRows } : {},
153
+ ...props.debug !== void 0 ? { debugTable: props.debug } : {}
154
+ });
155
+ react.useImperativeHandle(
156
+ ref,
157
+ () => ({
158
+ addRow: (seed) => {
159
+ if (typeof process !== "undefined" && process?.env?.NODE_ENV !== "production") {
160
+ if (props.onAddRow === void 0) {
161
+ console.warn("[tomis/grid-pro-master] addRow called but onAddRow prop is not provided.");
162
+ }
163
+ }
164
+ props.onAddRow?.(seed);
165
+ },
166
+ deleteRow: (rowId) => {
167
+ if (typeof process !== "undefined" && process?.env?.NODE_ENV !== "production") {
168
+ if (props.onDeleteRow === void 0) {
169
+ console.warn("[tomis/grid-pro-master] deleteRow called but onDeleteRow prop is not provided.");
170
+ }
171
+ }
172
+ props.onDeleteRow?.(rowId);
173
+ },
174
+ updateRow: (rowId, patch) => {
175
+ if (typeof process !== "undefined" && process?.env?.NODE_ENV !== "production") {
176
+ if (props.onUpdateRow === void 0) {
177
+ console.warn("[tomis/grid-pro-master] updateRow called but onUpdateRow prop is not provided.");
178
+ }
179
+ }
180
+ props.onUpdateRow?.(rowId, patch);
181
+ },
182
+ scrollTo: (_index) => {
183
+ },
184
+ getSelection: () => {
185
+ return table.getSelectedRowModel().rows.map((r) => r.original);
186
+ },
187
+ clearSelection: () => {
188
+ table.setRowSelection({});
189
+ },
190
+ refresh: () => {
191
+ table.resetRowSelection();
192
+ },
193
+ expandAll: () => {
194
+ table.toggleAllRowsExpanded(true);
195
+ },
196
+ collapseAll: () => {
197
+ table.toggleAllRowsExpanded(false);
198
+ }
199
+ }),
200
+ // eslint-disable-next-line react-hooks/exhaustive-deps
201
+ [table]
202
+ );
203
+ const headerGroups = table.getHeaderGroups();
204
+ const rows = table.getRowModel().rows;
205
+ const colCount = effectiveColumns.length;
206
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${props.className ?? ""} relative`.trim(), children: [
207
+ /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full border-collapse text-sm", children: [
208
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: headerGroups.map((headerGroup) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "border-b border-gray-200 bg-gray-100", children: headerGroup.headers.map((header) => /* @__PURE__ */ jsxRuntime.jsx(
209
+ "th",
210
+ {
211
+ className: "px-4 py-2 text-left font-semibold text-gray-700",
212
+ style: header.column.id === EXPAND_COLUMN_ID ? { width: 40 } : void 0,
213
+ children: header.isPlaceholder ? null : reactTable.flexRender(header.column.columnDef.header, header.getContext())
214
+ },
215
+ header.id
216
+ )) }, headerGroup.id)) }),
217
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: rows.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: colCount, className: "px-4 py-8 text-center text-gray-400", children: props.emptyText ?? "\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }) }) : rows.map((row) => /* @__PURE__ */ jsxRuntime.jsx(
218
+ MasterRow,
219
+ {
220
+ row,
221
+ renderDetailRow,
222
+ colCount,
223
+ onRowClick: props.onRowClick,
224
+ onRowDoubleClick: props.onRowDoubleClick,
225
+ onCellClick: props.onCellClick
226
+ },
227
+ row.id
228
+ )) })
229
+ ] }),
230
+ _lic.watermarkRequired && /* @__PURE__ */ jsxRuntime.jsx(gridLicense.Watermark, { required: true })
231
+ ] });
232
+ }
233
+ var MasterDetailGrid = react.forwardRef(MasterDetailGridInner);
234
+ function useContextMenu() {
235
+ const [isOpen, setIsOpen] = react.useState(false);
236
+ const [position, setPosition] = react.useState({ x: 0, y: 0 });
237
+ const [targetRow, setTargetRow] = react.useState(null);
238
+ const [targetCell, setTargetCell] = react.useState(null);
239
+ const [focusedIndex, setFocusedIndex] = react.useState(-1);
240
+ const openAt = react.useCallback(
241
+ (x, y, row, cell) => {
242
+ setPosition({ x, y });
243
+ setTargetRow(row);
244
+ setTargetCell(cell);
245
+ setFocusedIndex(-1);
246
+ setIsOpen(true);
247
+ },
248
+ []
249
+ );
250
+ const close = react.useCallback(() => {
251
+ setIsOpen(false);
252
+ setTargetRow(null);
253
+ setTargetCell(null);
254
+ setFocusedIndex(-1);
255
+ }, []);
256
+ return {
257
+ isOpen,
258
+ position,
259
+ targetRow,
260
+ targetCell,
261
+ openAt,
262
+ close,
263
+ focusedIndex,
264
+ setFocusedIndex
265
+ };
266
+ }
267
+ function clampPosition(x, y, menuWidth, menuHeight) {
268
+ const vw = window.innerWidth;
269
+ const vh = window.innerHeight;
270
+ return {
271
+ x: x + menuWidth > vw ? Math.max(0, vw - menuWidth) : x,
272
+ y: y + menuHeight > vh ? Math.max(0, vh - menuHeight) : y
273
+ };
274
+ }
275
+ function ContextMenuPortalInner(props) {
276
+ const { isOpen, position, items, targetRow, targetCell, onClose } = props;
277
+ const menuRef = react.useRef(null);
278
+ react.useEffect(() => {
279
+ if (!isOpen) return;
280
+ function handleMouseDown(e) {
281
+ if (menuRef.current !== null && !menuRef.current.contains(e.target)) {
282
+ onClose();
283
+ }
284
+ }
285
+ document.addEventListener("mousedown", handleMouseDown);
286
+ return () => {
287
+ document.removeEventListener("mousedown", handleMouseDown);
288
+ };
289
+ }, [isOpen, onClose]);
290
+ if (!isOpen) return null;
291
+ const estimatedHeight = items.length * 32 + 8;
292
+ const estimatedWidth = 200;
293
+ const { x, y } = clampPosition(position.x, position.y, estimatedWidth, estimatedHeight);
294
+ const menu = /* @__PURE__ */ jsxRuntime.jsx(
295
+ "ul",
296
+ {
297
+ ref: menuRef,
298
+ role: "menu",
299
+ className: "fixed z-50 bg-white border border-gray-200 rounded shadow-lg py-1 text-sm min-w-[160px]",
300
+ style: { left: x, top: y },
301
+ children: items.map((item, index) => {
302
+ if (item.separator === true) {
303
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { role: "separator", children: /* @__PURE__ */ jsxRuntime.jsx("hr", { className: "my-1 border-gray-200" }) }, index);
304
+ }
305
+ const isDisabled = typeof item.disabled === "function" ? item.disabled(targetRow) : item.disabled ?? false;
306
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { role: "menuitem", "aria-disabled": isDisabled, children: /* @__PURE__ */ jsxRuntime.jsxs(
307
+ "button",
308
+ {
309
+ type: "button",
310
+ className: [
311
+ "w-full flex items-center gap-2 px-3 py-1.5 text-left text-gray-700",
312
+ "hover:bg-gray-100 focus:bg-gray-100 focus:outline-none",
313
+ isDisabled ? "opacity-50 cursor-not-allowed pointer-events-none" : "cursor-pointer"
314
+ ].join(" "),
315
+ disabled: isDisabled,
316
+ onClick: isDisabled ? void 0 : (e) => {
317
+ item.onClick(targetRow, targetCell, e.nativeEvent);
318
+ onClose();
319
+ },
320
+ children: [
321
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1", children: item.label }),
322
+ item.shortcut !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-auto text-xs text-gray-400 shrink-0", children: item.shortcut })
323
+ ]
324
+ }
325
+ ) }, index);
326
+ })
327
+ }
328
+ );
329
+ return reactDom.createPortal(menu, document.body);
330
+ }
331
+ function ContextMenuPortal(props) {
332
+ return ContextMenuPortalInner(props);
333
+ }
334
+ function parseShortcut(shortcut) {
335
+ if (shortcut.trim() === "") return null;
336
+ const parts = shortcut.split("+");
337
+ const keyPart = parts[parts.length - 1];
338
+ if (keyPart === void 0 || keyPart.trim() === "") {
339
+ if (typeof process !== "undefined" && process?.env?.NODE_ENV !== "production") {
340
+ console.warn(`[tomis/grid-pro-master] Invalid shortcut grammar: "${shortcut}". Key part is empty.`);
341
+ }
342
+ return null;
343
+ }
344
+ const modifiers = parts.slice(0, -1).map((m) => m.toLowerCase());
345
+ const validModifiers = /* @__PURE__ */ new Set(["ctrl", "alt", "shift"]);
346
+ for (const mod of modifiers) {
347
+ if (!validModifiers.has(mod)) {
348
+ if (typeof process !== "undefined" && process?.env?.NODE_ENV !== "production") {
349
+ console.warn(`[tomis/grid-pro-master] Invalid shortcut modifier: "${mod}" in "${shortcut}". Valid modifiers: Ctrl, Alt, Shift.`);
350
+ }
351
+ return null;
352
+ }
353
+ }
354
+ return {
355
+ ctrl: modifiers.includes("ctrl"),
356
+ alt: modifiers.includes("alt"),
357
+ shift: modifiers.includes("shift"),
358
+ key: keyPart.toLowerCase()
359
+ };
360
+ }
361
+ function matchesShortcut(e, parsed) {
362
+ return e.ctrlKey === parsed.ctrl && e.altKey === parsed.alt && e.shiftKey === parsed.shift && e.key.toLowerCase() === parsed.key;
363
+ }
364
+ function ContextMenuGridInner(props, ref) {
365
+ const { contextMenuItems, ...rest } = props;
366
+ const { isOpen, position, targetRow, targetCell, openAt, close } = useContextMenu();
367
+ const table = reactTable.useReactTable({
368
+ data: rest.data,
369
+ columns: rest.columns,
370
+ getCoreRowModel: reactTable.getCoreRowModel(),
371
+ // C-29 spread-skip for exactOptionalPropertyTypes: true
372
+ ...rest.getSubRows !== void 0 ? { getSubRows: rest.getSubRows } : {},
373
+ ...rest.debug !== void 0 ? { debugTable: rest.debug } : {}
374
+ });
375
+ react.useImperativeHandle(
376
+ ref,
377
+ () => ({
378
+ addRow: (seed) => {
379
+ if (typeof process !== "undefined" && process?.env?.NODE_ENV !== "production") {
380
+ if (props.onAddRow === void 0) {
381
+ console.warn("[tomis/grid-pro-master] addRow called but onAddRow prop is not provided.");
382
+ }
383
+ }
384
+ props.onAddRow?.(seed);
385
+ },
386
+ deleteRow: (rowId) => {
387
+ if (typeof process !== "undefined" && process?.env?.NODE_ENV !== "production") {
388
+ if (props.onDeleteRow === void 0) {
389
+ console.warn("[tomis/grid-pro-master] deleteRow called but onDeleteRow prop is not provided.");
390
+ }
391
+ }
392
+ props.onDeleteRow?.(rowId);
393
+ },
394
+ updateRow: (rowId, patch) => {
395
+ if (typeof process !== "undefined" && process?.env?.NODE_ENV !== "production") {
396
+ if (props.onUpdateRow === void 0) {
397
+ console.warn("[tomis/grid-pro-master] updateRow called but onUpdateRow prop is not provided.");
398
+ }
399
+ }
400
+ props.onUpdateRow?.(rowId, patch);
401
+ },
402
+ scrollTo: (_index) => {
403
+ },
404
+ getSelection: () => {
405
+ return table.getSelectedRowModel().rows.map((r) => r.original);
406
+ },
407
+ clearSelection: () => {
408
+ table.setRowSelection({});
409
+ },
410
+ refresh: () => {
411
+ table.resetRowSelection();
412
+ }
413
+ }),
414
+ // eslint-disable-next-line react-hooks/exhaustive-deps
415
+ [table]
416
+ );
417
+ const headerGroups = table.getHeaderGroups();
418
+ const rows = table.getRowModel().rows;
419
+ const colCount = rest.columns.length;
420
+ function handleKeyDown(e) {
421
+ if (e.key === "Escape") {
422
+ close();
423
+ return;
424
+ }
425
+ if (contextMenuItems === void 0 || contextMenuItems.length === 0) return;
426
+ if (targetRow === null || targetCell === null) return;
427
+ for (const item of contextMenuItems) {
428
+ if (item.separator === true || item.shortcut === void 0) continue;
429
+ const parsed = parseShortcut(item.shortcut);
430
+ if (parsed === null) continue;
431
+ if (matchesShortcut(e, parsed)) {
432
+ const isDisabled = typeof item.disabled === "function" ? item.disabled(targetRow) : item.disabled ?? false;
433
+ if (!isDisabled) {
434
+ item.onClick(targetRow, targetCell, new MouseEvent("click"));
435
+ close();
436
+ }
437
+ e.preventDefault();
438
+ return;
439
+ }
440
+ }
441
+ }
442
+ function handleCellContextMenu(e, row, cell) {
443
+ if (contextMenuItems === void 0 || contextMenuItems.length === 0) return;
444
+ e.preventDefault();
445
+ openAt(e.clientX, e.clientY, row, cell);
446
+ }
447
+ return (
448
+ // tabIndex={0} makes the wrapper focusable for onKeyDown (D5).
449
+ // focus:outline-none suppresses default browser focus ring on the container.
450
+ /* @__PURE__ */ jsxRuntime.jsxs(
451
+ "div",
452
+ {
453
+ className: ["focus:outline-none", rest.className].filter(Boolean).join(" "),
454
+ tabIndex: contextMenuItems !== void 0 ? 0 : void 0,
455
+ onKeyDown: contextMenuItems !== void 0 ? handleKeyDown : void 0,
456
+ children: [
457
+ /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full border-collapse text-sm", children: [
458
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: headerGroups.map((headerGroup) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "border-b border-gray-200 bg-gray-100", children: headerGroup.headers.map((header) => /* @__PURE__ */ jsxRuntime.jsx(
459
+ "th",
460
+ {
461
+ className: "px-4 py-2 text-left font-semibold text-gray-700",
462
+ children: header.isPlaceholder ? null : reactTable.flexRender(header.column.columnDef.header, header.getContext())
463
+ },
464
+ header.id
465
+ )) }, headerGroup.id)) }),
466
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: rows.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: colCount, className: "px-4 py-8 text-center text-gray-400", children: rest.emptyText ?? "\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }) }) : rows.map((row) => /* @__PURE__ */ jsxRuntime.jsx(
467
+ "tr",
468
+ {
469
+ className: "border-b border-gray-100 hover:bg-gray-50",
470
+ onClick: rest.onRowClick !== void 0 ? (e) => rest.onRowClick(row.original, e) : void 0,
471
+ onDoubleClick: rest.onRowDoubleClick !== void 0 ? (e) => rest.onRowDoubleClick(row.original, e) : void 0,
472
+ children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsxRuntime.jsx(
473
+ "td",
474
+ {
475
+ className: "px-4 py-2",
476
+ onClick: rest.onCellClick !== void 0 ? (e) => rest.onCellClick(cell, row.original, e) : void 0,
477
+ onContextMenu: contextMenuItems !== void 0 ? (e) => handleCellContextMenu(e, row.original, cell) : void 0,
478
+ children: reactTable.flexRender(cell.column.columnDef.cell, cell.getContext())
479
+ },
480
+ cell.id
481
+ ))
482
+ },
483
+ row.id
484
+ )) })
485
+ ] }),
486
+ isOpen && targetRow !== null && targetCell !== null && contextMenuItems !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
487
+ ContextMenuPortal,
488
+ {
489
+ isOpen,
490
+ position,
491
+ items: contextMenuItems,
492
+ targetRow,
493
+ targetCell,
494
+ onClose: close
495
+ }
496
+ )
497
+ ]
498
+ }
499
+ )
500
+ );
501
+ }
502
+ var ContextMenuGrid = react.forwardRef(ContextMenuGridInner);
503
+ function isDev() {
504
+ return typeof process !== "undefined" && process?.env?.NODE_ENV !== "production";
505
+ }
506
+ function useExpandedPersistence(options) {
507
+ const { storageKey, storageType = "localStorage", initialExpanded = {} } = options;
508
+ const storageRef = react.useRef(null);
509
+ const warnedUnavailable = react.useRef(false);
510
+ if (storageRef.current === null) {
511
+ storageRef.current = storage.getStorage(storageType);
512
+ if (storageRef.current === null && !warnedUnavailable.current) {
513
+ warnedUnavailable.current = true;
514
+ if (isDev()) {
515
+ console.warn(
516
+ "[tomis/grid-pro-master] useExpandedPersistence: storage unavailable, falling back to in-memory."
517
+ );
518
+ }
519
+ }
520
+ }
521
+ const [expanded, setExpandedInternal] = react.useState(() => {
522
+ const stored = storage.readJson(storageRef.current, storageKey);
523
+ return stored !== null ? stored : initialExpanded;
524
+ });
525
+ react.useEffect(() => {
526
+ const nextStorage = storage.getStorage(storageType);
527
+ if (nextStorage === null && !warnedUnavailable.current) {
528
+ warnedUnavailable.current = true;
529
+ if (isDev()) {
530
+ console.warn(
531
+ "[tomis/grid-pro-master] useExpandedPersistence: storage unavailable, falling back to in-memory."
532
+ );
533
+ }
534
+ }
535
+ storageRef.current = nextStorage;
536
+ }, [storageType]);
537
+ const setExpanded = react.useCallback(
538
+ (updated) => {
539
+ setExpandedInternal((prev) => {
540
+ const next = typeof updated === "function" ? updated(prev) : updated;
541
+ storage.writeJson(
542
+ storageRef.current,
543
+ storageKey,
544
+ next,
545
+ isDev() ? "tomis/grid-pro-master useExpandedPersistence" : void 0
546
+ );
547
+ return next;
548
+ });
549
+ },
550
+ // storageKey changes are intentionally NOT in deps — changing the key is an unusual
551
+ // operation; consumers should remount to switch keys. This avoids stale-closure risk.
552
+ [storageKey]
553
+ // eslint-disable-line react-hooks/exhaustive-deps -- storageRef intentionally omitted (stable ref)
554
+ );
555
+ return [expanded, setExpanded];
556
+ }
557
+ gridLicense.checkLicense();
558
+
559
+ Object.defineProperty(exports, "ColumnPinGrid", {
560
+ enumerable: true,
561
+ get: function () { return gridCore.ColumnPinGrid; }
562
+ });
563
+ Object.defineProperty(exports, "TreeGrid", {
564
+ enumerable: true,
565
+ get: function () { return gridCore.TreeGrid; }
566
+ });
567
+ exports.ContextMenuGrid = ContextMenuGrid;
568
+ exports.MasterDetailGrid = MasterDetailGrid;
569
+ exports.useExpandedPersistence = useExpandedPersistence;
570
+ //# sourceMappingURL=index.cjs.map
571
+ //# sourceMappingURL=index.cjs.map