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