@optilogic/core 1.3.0 → 1.3.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.
- package/dist/index.cjs +80 -25
- 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 +80 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/data-grid/DataGrid.tsx +66 -24
- 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,19 @@ 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
|
+
|
|
170
|
+
React.useLayoutEffect(() => {
|
|
171
|
+
if (!fillWidth || !parentRef.current) return;
|
|
172
|
+
const observer = new ResizeObserver((entries) => {
|
|
173
|
+
const w = entries[0]?.contentRect.width;
|
|
174
|
+
if (w && w > 0) setContainerWidth(w);
|
|
175
|
+
});
|
|
176
|
+
observer.observe(parentRef.current);
|
|
177
|
+
return () => observer.disconnect();
|
|
178
|
+
}, [fillWidth]);
|
|
179
|
+
|
|
166
180
|
// Filter visible columns
|
|
167
181
|
const visibleColumns = React.useMemo(
|
|
168
182
|
() => columns.filter((col) => !col.hidden),
|
|
@@ -232,16 +246,6 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
232
246
|
// Keep the state hook's ref in sync so editing resolves the correct row
|
|
233
247
|
processedDataRef.current = processedData;
|
|
234
248
|
|
|
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
249
|
// Auto-enable virtualization for large datasets
|
|
246
250
|
const shouldVirtualize = virtualized ?? processedData.length > 100;
|
|
247
251
|
|
|
@@ -261,13 +265,47 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
261
265
|
enabled: shouldVirtualize,
|
|
262
266
|
});
|
|
263
267
|
|
|
264
|
-
// Calculate table width
|
|
265
|
-
const tableWidth = React.useMemo(() => {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
268
|
+
// Calculate table width and effective column widths (with fillWidth scaling)
|
|
269
|
+
const { tableWidth, effectiveColumnWidths } = React.useMemo(() => {
|
|
270
|
+
// 1. Collect raw widths
|
|
271
|
+
const rawWidths: Record<string, number> = {};
|
|
272
|
+
let rawTotal = 0;
|
|
273
|
+
for (const col of visibleColumns) {
|
|
274
|
+
const w = state.columnWidths[col.key] || col.width || estimateColumnWidth(col);
|
|
275
|
+
rawWidths[col.key] = w;
|
|
276
|
+
rawTotal += w;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 2. If fillWidth off or container not measured, use raw widths
|
|
280
|
+
if (!fillWidth || !containerWidth || rawTotal >= containerWidth) {
|
|
281
|
+
return { tableWidth: rawTotal, effectiveColumnWidths: rawWidths };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 3. Scale proportionally to fill container, respecting minWidth/maxWidth
|
|
285
|
+
const scale = containerWidth / rawTotal;
|
|
286
|
+
const scaled: Record<string, number> = {};
|
|
287
|
+
for (const col of visibleColumns) {
|
|
288
|
+
let w = Math.floor(rawWidths[col.key] * scale);
|
|
289
|
+
if (col.minWidth) w = Math.max(w, col.minWidth);
|
|
290
|
+
if (col.maxWidth) w = Math.min(w, col.maxWidth);
|
|
291
|
+
scaled[col.key] = w;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return { tableWidth: containerWidth, effectiveColumnWidths: scaled };
|
|
295
|
+
}, [visibleColumns, state.columnWidths, fillWidth, containerWidth]);
|
|
296
|
+
|
|
297
|
+
// Use column resize manager
|
|
298
|
+
const { resizingColumn, getResizeProps } = useColumnResizeManager({
|
|
299
|
+
columns: visibleColumns,
|
|
300
|
+
columnWidths: state.columnWidths,
|
|
301
|
+
resizableColumns,
|
|
302
|
+
onColumnResize: actions.setColumnWidth,
|
|
303
|
+
onColumnResizeStart,
|
|
304
|
+
onColumnResizeEnd,
|
|
305
|
+
fillWidth,
|
|
306
|
+
containerWidth,
|
|
307
|
+
effectiveColumnWidths,
|
|
308
|
+
});
|
|
271
309
|
|
|
272
310
|
// Get visible row count for keyboard navigation
|
|
273
311
|
const visibleRowCount = React.useMemo(() => {
|
|
@@ -577,7 +615,7 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
577
615
|
role="row"
|
|
578
616
|
>
|
|
579
617
|
{visibleColumns.map((column, colIndex) => {
|
|
580
|
-
const width =
|
|
618
|
+
const width = effectiveColumnWidths[column.key];
|
|
581
619
|
const resizeProps = getResizeProps(column.key);
|
|
582
620
|
|
|
583
621
|
return (
|
|
@@ -591,6 +629,7 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
591
629
|
isResizable={
|
|
592
630
|
resizableColumns && column.resizable !== false
|
|
593
631
|
}
|
|
632
|
+
fillWidth={fillWidth}
|
|
594
633
|
onSort={() => handleSort(column.key)}
|
|
595
634
|
onFilterChange={(filter) =>
|
|
596
635
|
handleFilterChange(column.key, filter)
|
|
@@ -646,7 +685,7 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
646
685
|
>
|
|
647
686
|
{visibleColumns.map((column, colIndex) => {
|
|
648
687
|
const width =
|
|
649
|
-
|
|
688
|
+
effectiveColumnWidths[column.key];
|
|
650
689
|
const isEditingThisCell =
|
|
651
690
|
state.editingCell?.rowIndex === virtualRow.index &&
|
|
652
691
|
state.editingCell?.columnKey === column.key;
|
|
@@ -671,7 +710,8 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
671
710
|
<div
|
|
672
711
|
key={column.key}
|
|
673
712
|
className={cn(
|
|
674
|
-
"
|
|
713
|
+
"px-3 py-2 text-sm overflow-hidden",
|
|
714
|
+
!fillWidth && "flex-shrink-0",
|
|
675
715
|
showColumnBorders && "border-r border-border last:border-r-0",
|
|
676
716
|
isFocused && !isEditingThisCell && "ring-2 ring-inset ring-primary",
|
|
677
717
|
isEditingThisCell && "ring-2 ring-inset ring-primary bg-background",
|
|
@@ -799,7 +839,7 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
799
839
|
</div>
|
|
800
840
|
)}
|
|
801
841
|
|
|
802
|
-
<div className="flex-1 overflow-auto min-h-0">
|
|
842
|
+
<div ref={parentRef} className="flex-1 overflow-auto min-h-0">
|
|
803
843
|
{/* Header row using HeaderCell for consistent features */}
|
|
804
844
|
<div
|
|
805
845
|
className={cn(
|
|
@@ -810,7 +850,7 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
810
850
|
role="row"
|
|
811
851
|
>
|
|
812
852
|
{visibleColumns.map((column, colIndex) => {
|
|
813
|
-
const width =
|
|
853
|
+
const width = effectiveColumnWidths[column.key];
|
|
814
854
|
const resizeProps = getResizeProps(column.key);
|
|
815
855
|
|
|
816
856
|
return (
|
|
@@ -822,6 +862,7 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
822
862
|
sorting={getColumnSort(column.key)}
|
|
823
863
|
filter={getColumnFilter(column.key)}
|
|
824
864
|
isResizable={resizableColumns && column.resizable !== false}
|
|
865
|
+
fillWidth={fillWidth}
|
|
825
866
|
onSort={() => handleSort(column.key)}
|
|
826
867
|
onFilterChange={(filter) => handleFilterChange(column.key, filter)}
|
|
827
868
|
onResizeMouseDown={resizeProps.handleMouseDown}
|
|
@@ -853,7 +894,7 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
853
894
|
role="row"
|
|
854
895
|
>
|
|
855
896
|
{visibleColumns.map((column) => {
|
|
856
|
-
const width =
|
|
897
|
+
const width = effectiveColumnWidths[column.key];
|
|
857
898
|
const isEditingThisCell =
|
|
858
899
|
state.editingCell?.rowIndex === rowIndex &&
|
|
859
900
|
state.editingCell?.columnKey === column.key;
|
|
@@ -865,7 +906,8 @@ export function DataGrid<T = Record<string, CellValue>>({
|
|
|
865
906
|
<div
|
|
866
907
|
key={column.key}
|
|
867
908
|
className={cn(
|
|
868
|
-
"
|
|
909
|
+
"px-3 py-2 text-sm overflow-hidden",
|
|
910
|
+
!fillWidth && "flex-shrink-0",
|
|
869
911
|
showColumnBorders && "border-r border-border last:border-r-0",
|
|
870
912
|
isFocused && !isEditingThisCell && "ring-2 ring-inset ring-primary",
|
|
871
913
|
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;
|