@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optilogic/core",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Core UI components for Optilogic - A professional React component library",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -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 - prefer explicit widths, fall back to estimated width from header
265
- const tableWidth = React.useMemo(() => {
266
- return visibleColumns.reduce((acc, col) => {
267
- const width = state.columnWidths[col.key] || col.width || estimateColumnWidth(col);
268
- return acc + width;
269
- }, 0);
270
- }, [visibleColumns, state.columnWidths]);
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={parentRef}
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 = state.columnWidths[column.key] || column.width || estimateColumnWidth(column);
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
- state.columnWidths[column.key] || column.width || estimateColumnWidth(column);
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
- "flex-shrink-0 px-3 py-2 text-sm overflow-hidden",
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 = state.columnWidths[column.key] || column.width || estimateColumnWidth(column);
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 = state.columnWidths[column.key] || column.width || estimateColumnWidth(column);
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
- "flex-shrink-0 px-3 py-2 text-sm overflow-hidden",
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 flex-shrink-0 border-r border-border last:border-r-0",
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;