@optilogic/core 1.3.0 → 1.3.2
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.cjs +91 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -2
- package/dist/index.d.ts +15 -2
- package/dist/index.js +91 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/data-grid/DataGrid.tsx +81 -25
- package/src/components/data-grid/components/HeaderCell.tsx +5 -1
- package/src/components/data-grid/hooks/useColumnResize.ts +30 -1
- package/src/components/data-grid/types.ts +5 -0
package/package.json
CHANGED
|
@@ -89,6 +89,7 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
89
89
|
tooltipMinLength = 30,
|
|
90
90
|
enableKeyboardNavigation = true,
|
|
91
91
|
showColumnBorders = true,
|
|
92
|
+
fillWidth,
|
|
92
93
|
enableInternalSorting = true,
|
|
93
94
|
enableInternalFiltering = true,
|
|
94
95
|
|
|
@@ -163,6 +164,33 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
163
164
|
y: number;
|
|
164
165
|
} | null>(null);
|
|
165
166
|
|
|
167
|
+
// Measure container width for fillWidth mode
|
|
168
|
+
const [containerWidth, setContainerWidth] = React.useState(0);
|
|
169
|
+
const fillWidthObserverRef = React.useRef<ResizeObserver | null>(null);
|
|
170
|
+
|
|
171
|
+
const parentCallbackRef = React.useCallback(
|
|
172
|
+
(node: HTMLDivElement | null) => {
|
|
173
|
+
fillWidthObserverRef.current?.disconnect();
|
|
174
|
+
fillWidthObserverRef.current = null;
|
|
175
|
+
|
|
176
|
+
(parentRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
177
|
+
|
|
178
|
+
if (!fillWidth || !node) return;
|
|
179
|
+
|
|
180
|
+
const observer = new ResizeObserver((entries) => {
|
|
181
|
+
const w = entries[0]?.contentRect.width;
|
|
182
|
+
if (w && w > 0) setContainerWidth(w);
|
|
183
|
+
});
|
|
184
|
+
observer.observe(node);
|
|
185
|
+
fillWidthObserverRef.current = observer;
|
|
186
|
+
},
|
|
187
|
+
[fillWidth],
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
React.useEffect(() => {
|
|
191
|
+
return () => fillWidthObserverRef.current?.disconnect();
|
|
192
|
+
}, []);
|
|
193
|
+
|
|
166
194
|
// Filter visible columns
|
|
167
195
|
const visibleColumns = React.useMemo(
|
|
168
196
|
() => columns.filter((col) => !col.hidden),
|
|
@@ -232,16 +260,6 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
232
260
|
// Keep the state hook's ref in sync so editing resolves the correct row
|
|
233
261
|
processedDataRef.current = processedData;
|
|
234
262
|
|
|
235
|
-
// Use column resize manager
|
|
236
|
-
const { resizingColumn, getResizeProps } = useColumnResizeManager({
|
|
237
|
-
columns: visibleColumns,
|
|
238
|
-
columnWidths: state.columnWidths,
|
|
239
|
-
resizableColumns,
|
|
240
|
-
onColumnResize: actions.setColumnWidth,
|
|
241
|
-
onColumnResizeStart,
|
|
242
|
-
onColumnResizeEnd,
|
|
243
|
-
});
|
|
244
|
-
|
|
245
263
|
// Auto-enable virtualization for large datasets
|
|
246
264
|
const shouldVirtualize = virtualized ?? processedData.length > 100;
|
|
247
265
|
|
|
@@ -261,13 +279,47 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
261
279
|
enabled: shouldVirtualize,
|
|
262
280
|
});
|
|
263
281
|
|
|
264
|
-
// Calculate table width
|
|
265
|
-
const tableWidth = React.useMemo(() => {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
282
|
+
// Calculate table width and effective column widths (with fillWidth scaling)
|
|
283
|
+
const { tableWidth, effectiveColumnWidths } = React.useMemo(() => {
|
|
284
|
+
// 1. Collect raw widths
|
|
285
|
+
const rawWidths: Record<string, number> = {};
|
|
286
|
+
let rawTotal = 0;
|
|
287
|
+
for (const col of visibleColumns) {
|
|
288
|
+
const w = state.columnWidths[col.key] || col.width || estimateColumnWidth(col);
|
|
289
|
+
rawWidths[col.key] = w;
|
|
290
|
+
rawTotal += w;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 2. If fillWidth off or container not measured, use raw widths
|
|
294
|
+
if (!fillWidth || !containerWidth || rawTotal >= containerWidth) {
|
|
295
|
+
return { tableWidth: rawTotal, effectiveColumnWidths: rawWidths };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 3. Scale proportionally to fill container, respecting minWidth/maxWidth
|
|
299
|
+
const scale = containerWidth / rawTotal;
|
|
300
|
+
const scaled: Record<string, number> = {};
|
|
301
|
+
for (const col of visibleColumns) {
|
|
302
|
+
let w = Math.floor(rawWidths[col.key] * scale);
|
|
303
|
+
if (col.minWidth) w = Math.max(w, col.minWidth);
|
|
304
|
+
if (col.maxWidth) w = Math.min(w, col.maxWidth);
|
|
305
|
+
scaled[col.key] = w;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { tableWidth: containerWidth, effectiveColumnWidths: scaled };
|
|
309
|
+
}, [visibleColumns, state.columnWidths, fillWidth, containerWidth]);
|
|
310
|
+
|
|
311
|
+
// Use column resize manager
|
|
312
|
+
const { resizingColumn, getResizeProps } = useColumnResizeManager({
|
|
313
|
+
columns: visibleColumns,
|
|
314
|
+
columnWidths: state.columnWidths,
|
|
315
|
+
resizableColumns,
|
|
316
|
+
onColumnResize: actions.setColumnWidth,
|
|
317
|
+
onColumnResizeStart,
|
|
318
|
+
onColumnResizeEnd,
|
|
319
|
+
fillWidth,
|
|
320
|
+
containerWidth,
|
|
321
|
+
effectiveColumnWidths,
|
|
322
|
+
});
|
|
271
323
|
|
|
272
324
|
// Get visible row count for keyboard navigation
|
|
273
325
|
const visibleRowCount = React.useMemo(() => {
|
|
@@ -551,7 +603,7 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
551
603
|
)}
|
|
552
604
|
|
|
553
605
|
<div
|
|
554
|
-
ref={
|
|
606
|
+
ref={parentCallbackRef}
|
|
555
607
|
className="flex-1 overflow-auto bg-background relative w-full min-h-0 max-h-full"
|
|
556
608
|
style={{ contain: "strict" }}
|
|
557
609
|
>
|
|
@@ -577,7 +629,7 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
577
629
|
role="row"
|
|
578
630
|
>
|
|
579
631
|
{visibleColumns.map((column, colIndex) => {
|
|
580
|
-
const width =
|
|
632
|
+
const width = effectiveColumnWidths[column.key];
|
|
581
633
|
const resizeProps = getResizeProps(column.key);
|
|
582
634
|
|
|
583
635
|
return (
|
|
@@ -591,6 +643,7 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
591
643
|
isResizable={
|
|
592
644
|
resizableColumns && column.resizable !== false
|
|
593
645
|
}
|
|
646
|
+
fillWidth={fillWidth}
|
|
594
647
|
onSort={() => handleSort(column.key)}
|
|
595
648
|
onFilterChange={(filter) =>
|
|
596
649
|
handleFilterChange(column.key, filter)
|
|
@@ -646,7 +699,7 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
646
699
|
>
|
|
647
700
|
{visibleColumns.map((column, colIndex) => {
|
|
648
701
|
const width =
|
|
649
|
-
|
|
702
|
+
effectiveColumnWidths[column.key];
|
|
650
703
|
const isEditingThisCell =
|
|
651
704
|
state.editingCell?.rowIndex === virtualRow.index &&
|
|
652
705
|
state.editingCell?.columnKey === column.key;
|
|
@@ -671,7 +724,8 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
671
724
|
<div
|
|
672
725
|
key={column.key}
|
|
673
726
|
className={cn(
|
|
674
|
-
"
|
|
727
|
+
"px-3 py-2 text-sm overflow-hidden",
|
|
728
|
+
!fillWidth && "flex-shrink-0",
|
|
675
729
|
showColumnBorders && "border-r border-border last:border-r-0",
|
|
676
730
|
isFocused && !isEditingThisCell && "ring-2 ring-inset ring-primary",
|
|
677
731
|
isEditingThisCell && "ring-2 ring-inset ring-primary bg-background",
|
|
@@ -799,7 +853,7 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
799
853
|
</div>
|
|
800
854
|
)}
|
|
801
855
|
|
|
802
|
-
<div className="flex-1 overflow-auto min-h-0">
|
|
856
|
+
<div ref={parentCallbackRef} className="flex-1 overflow-auto min-h-0">
|
|
803
857
|
{/* Header row using HeaderCell for consistent features */}
|
|
804
858
|
<div
|
|
805
859
|
className={cn(
|
|
@@ -810,7 +864,7 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
810
864
|
role="row"
|
|
811
865
|
>
|
|
812
866
|
{visibleColumns.map((column, colIndex) => {
|
|
813
|
-
const width =
|
|
867
|
+
const width = effectiveColumnWidths[column.key];
|
|
814
868
|
const resizeProps = getResizeProps(column.key);
|
|
815
869
|
|
|
816
870
|
return (
|
|
@@ -822,6 +876,7 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
822
876
|
sorting={getColumnSort(column.key)}
|
|
823
877
|
filter={getColumnFilter(column.key)}
|
|
824
878
|
isResizable={resizableColumns && column.resizable !== false}
|
|
879
|
+
fillWidth={fillWidth}
|
|
825
880
|
onSort={() => handleSort(column.key)}
|
|
826
881
|
onFilterChange={(filter) => handleFilterChange(column.key, filter)}
|
|
827
882
|
onResizeMouseDown={resizeProps.handleMouseDown}
|
|
@@ -853,7 +908,7 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
853
908
|
role="row"
|
|
854
909
|
>
|
|
855
910
|
{visibleColumns.map((column) => {
|
|
856
|
-
const width =
|
|
911
|
+
const width = effectiveColumnWidths[column.key];
|
|
857
912
|
const isEditingThisCell =
|
|
858
913
|
state.editingCell?.rowIndex === rowIndex &&
|
|
859
914
|
state.editingCell?.columnKey === column.key;
|
|
@@ -865,7 +920,8 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
865
920
|
<div
|
|
866
921
|
key={column.key}
|
|
867
922
|
className={cn(
|
|
868
|
-
"
|
|
923
|
+
"px-3 py-2 text-sm overflow-hidden",
|
|
924
|
+
!fillWidth && "flex-shrink-0",
|
|
869
925
|
showColumnBorders && "border-r border-border last:border-r-0",
|
|
870
926
|
isFocused && !isEditingThisCell && "ring-2 ring-inset ring-primary",
|
|
871
927
|
isEditingThisCell && "ring-2 ring-inset ring-primary bg-background",
|
|
@@ -28,6 +28,8 @@ export interface HeaderCellProps<T = any> {
|
|
|
28
28
|
filter?: FilterConfig;
|
|
29
29
|
/** Whether this column is resizable */
|
|
30
30
|
isResizable: boolean;
|
|
31
|
+
/** Whether fillWidth mode is active */
|
|
32
|
+
fillWidth?: boolean;
|
|
31
33
|
/** Callback when sort is toggled */
|
|
32
34
|
onSort?: () => void;
|
|
33
35
|
/** Callback when filter changes */
|
|
@@ -50,6 +52,7 @@ export function HeaderCell<T = any>({
|
|
|
50
52
|
sorting,
|
|
51
53
|
filter,
|
|
52
54
|
isResizable,
|
|
55
|
+
fillWidth,
|
|
53
56
|
onSort,
|
|
54
57
|
onFilterChange,
|
|
55
58
|
onResizeMouseDown,
|
|
@@ -101,7 +104,8 @@ export function HeaderCell<T = any>({
|
|
|
101
104
|
return (
|
|
102
105
|
<div
|
|
103
106
|
className={cn(
|
|
104
|
-
"relative
|
|
107
|
+
"relative border-r border-border last:border-r-0",
|
|
108
|
+
!fillWidth && "flex-shrink-0",
|
|
105
109
|
"bg-muted select-none",
|
|
106
110
|
isResizing && "bg-accent/20"
|
|
107
111
|
)}
|
|
@@ -212,6 +212,12 @@ export interface UseColumnResizeManagerOptions<T = any> {
|
|
|
212
212
|
onColumnResizeStart?: (columnKey: string) => void;
|
|
213
213
|
/** Callback when resize ends */
|
|
214
214
|
onColumnResizeEnd?: (columnKey: string, width: number) => void;
|
|
215
|
+
/** Whether fillWidth mode is active */
|
|
216
|
+
fillWidth?: boolean;
|
|
217
|
+
/** Measured container width for fillWidth redistribution */
|
|
218
|
+
containerWidth?: number;
|
|
219
|
+
/** Effective column widths (after fillWidth scaling) */
|
|
220
|
+
effectiveColumnWidths?: Record<string, number>;
|
|
215
221
|
}
|
|
216
222
|
|
|
217
223
|
export interface UseColumnResizeManagerReturn {
|
|
@@ -238,11 +244,15 @@ export function useColumnResizeManager<T = any>(
|
|
|
238
244
|
onColumnResize,
|
|
239
245
|
onColumnResizeStart,
|
|
240
246
|
onColumnResizeEnd,
|
|
247
|
+
fillWidth,
|
|
248
|
+
containerWidth,
|
|
249
|
+
effectiveColumnWidths,
|
|
241
250
|
} = options;
|
|
242
251
|
|
|
243
252
|
const [resizingColumn, setResizingColumn] = useState<string | null>(null);
|
|
244
253
|
const startXRef = useRef(0);
|
|
245
254
|
const startWidthRef = useRef(0);
|
|
255
|
+
const startEffectiveWidthsRef = useRef<Record<string, number>>({});
|
|
246
256
|
|
|
247
257
|
/**
|
|
248
258
|
* Get column by key
|
|
@@ -280,9 +290,22 @@ export function useColumnResizeManager<T = any>(
|
|
|
280
290
|
startWidthRef.current + deltaX
|
|
281
291
|
);
|
|
282
292
|
|
|
293
|
+
if (fillWidth && containerWidth) {
|
|
294
|
+
const otherCols = columns.filter(c => c.key !== resizingColumn);
|
|
295
|
+
const startEffective = startEffectiveWidthsRef.current;
|
|
296
|
+
const otherTotal = otherCols.reduce((s, c) => s + (startEffective[c.key] || 0), 0);
|
|
297
|
+
const remaining = containerWidth - newWidth;
|
|
298
|
+
|
|
299
|
+
for (const col of otherCols) {
|
|
300
|
+
const proportion = (startEffective[col.key] || 0) / otherTotal;
|
|
301
|
+
const adjusted = clampWidth(col.key, Math.floor(remaining * proportion));
|
|
302
|
+
onColumnResize(col.key, adjusted);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
283
306
|
onColumnResize(resizingColumn, newWidth);
|
|
284
307
|
},
|
|
285
|
-
[resizingColumn, clampWidth, onColumnResize]
|
|
308
|
+
[resizingColumn, clampWidth, onColumnResize, fillWidth, containerWidth, columns]
|
|
286
309
|
);
|
|
287
310
|
|
|
288
311
|
/**
|
|
@@ -336,6 +359,10 @@ export function useColumnResizeManager<T = any>(
|
|
|
336
359
|
document.body.style.cursor = "col-resize";
|
|
337
360
|
|
|
338
361
|
onColumnResizeStart?.(columnKey);
|
|
362
|
+
|
|
363
|
+
if (fillWidth && effectiveColumnWidths) {
|
|
364
|
+
startEffectiveWidthsRef.current = { ...effectiveColumnWidths };
|
|
365
|
+
}
|
|
339
366
|
};
|
|
340
367
|
|
|
341
368
|
const handleDoubleClick = (event: React.MouseEvent) => {
|
|
@@ -368,6 +395,8 @@ export function useColumnResizeManager<T = any>(
|
|
|
368
395
|
onColumnResize,
|
|
369
396
|
onColumnResizeStart,
|
|
370
397
|
onColumnResizeEnd,
|
|
398
|
+
fillWidth,
|
|
399
|
+
effectiveColumnWidths,
|
|
371
400
|
]
|
|
372
401
|
);
|
|
373
402
|
|
|
@@ -282,6 +282,10 @@ export interface DataGridProps<T = Record<string, CellValue>> {
|
|
|
282
282
|
tooltipMinLength?: number;
|
|
283
283
|
/** Show column dividing borders between cells (default: true) */
|
|
284
284
|
showColumnBorders?: boolean;
|
|
285
|
+
/** When true, columns scale proportionally to fill the container width.
|
|
286
|
+
* Columns shrink/grow to keep total width = container width.
|
|
287
|
+
* Horizontal scroll only kicks in if columns hit their minWidth constraints. */
|
|
288
|
+
fillWidth?: boolean;
|
|
285
289
|
/** Enable internal sorting when in uncontrolled mode (default: true) */
|
|
286
290
|
enableInternalSorting?: boolean;
|
|
287
291
|
/** Enable internal filtering when in uncontrolled mode (default: true) */
|
|
@@ -436,6 +440,7 @@ export interface HeaderCellProps<T = Record<string, CellValue>> {
|
|
|
436
440
|
sorting?: SortConfig;
|
|
437
441
|
filter?: FilterConfig;
|
|
438
442
|
isResizable: boolean;
|
|
443
|
+
fillWidth?: boolean;
|
|
439
444
|
onSort?: () => void;
|
|
440
445
|
onFilterChange?: (filter: FilterConfig | null) => void;
|
|
441
446
|
onResize?: (width: number) => void;
|