Skip to content

Commit cba9891

Browse files
committed
feat: column drag-reorder and flex resize fixes
- Fix: manual column resize now sticks for flex columns - Feat: added drag-and-drop column reordering to Column Visibility Panel - Docs: merged all updates pending for v0.1.4 release
1 parent 7f2ab7e commit cba9891

9 files changed

Lines changed: 182 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55

66
---
77

8-
## [0.1.4] — March 10, 2026 🐛
8+
## [0.1.4] — March 10, 2026 ✨
9+
10+
### Added
11+
- **Column Visibility Reorder**: Added a drag handle to the `ColumnVisibilityPanel` letting users seamlessly reorder columns directly via the Visibility Panel dropdown checkbox list. Uses native HTML Drag and Drop API with no external dependencies.
12+
- Added `onColumnReorder` support to `ColumnVisibilityPanel` and `GridToolbar`.
913

1014
### Fixed
1115
- `import '@opencorestack/opengridx/styles'` now resolves correctly in TypeScript projects. The `./styles` subpath export in `package.json` now includes a `types` pointer to `dist/opengridx.css.d.ts`, eliminating the "Cannot find module" TS error.

demo/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ function App() {
117117
<img src={`${import.meta.env.BASE_URL}logo.png`} alt="OpenGridX Logo" className="app-logo" />
118118
<h2 className="app-title">
119119
OpenGridX
120-
<span className="app-version">v0.1.3</span>
120+
<span className="app-version">v0.1.4</span>
121121
</h2>
122122
</div>
123123

demo/Home.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export default function Home(_props: HomeProps) {
6868
<div className="home-logo-hero">
6969
<img src={`${import.meta.env.BASE_URL}banner.png`} alt="OpenGridX Logo" className="home-banner-image" />
7070
</div>
71-
<span className="home-badge">OpenGridX v0.1.3</span>
71+
<span className="home-badge">OpenGridX v0.1.4</span>
7272
<p className="home-subtitle">
7373
The elite, high-performance DataGrid for modern React.
7474
Built to handle massive data with a premium developer experience.

demo/public/llms.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# OpenGridX — AI Agent Context
22
# Package: @opencorestack/opengridx
3-
# Version: 0.1.3
3+
# Version: 0.1.4
44
# Docs: https://opencorestack.github.io/OpenGridX/
55
# This file is machine-readable. AI developers: start here.
66

lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,53 @@
5252
.ogx-column-visibility-panel__item {
5353
display: flex;
5454
align-items: center;
55-
gap: var(--ogx-spacing-md);
56-
padding: var(--ogx-spacing-xs) var(--ogx-spacing-md);
57-
cursor: pointer;
55+
gap: var(--ogx-spacing-sm);
56+
padding: var(--ogx-spacing-xs) var(--ogx-spacing-sm) var(--ogx-spacing-xs) var(--ogx-spacing-xs);
5857
transition: background var(--ogx-transition-duration-fast) var(--ogx-transition-easing);
58+
user-select: none;
5959
}
6060

6161
.ogx-column-visibility-panel__item:hover:not(.ogx-column-visibility-panel__item--disabled) {
6262
background: var(--ogx-color-gray-50);
6363
}
6464

65+
.ogx-column-visibility-panel__item--drag-over {
66+
background: var(--ogx-color-primary-light, rgba(59, 130, 246, 0.08));
67+
border-top: 2px solid var(--ogx-color-primary);
68+
}
69+
6570
.ogx-column-visibility-panel__item--disabled {
6671
opacity: 0.5;
67-
cursor: not-allowed;
72+
}
73+
74+
/* ── Drag handle ──────────────────────────────────────────────────────────── */
75+
.ogx-column-visibility-panel__drag-handle-wrapper {
76+
display: flex;
77+
align-items: center;
78+
justify-content: center;
79+
flex-shrink: 0;
80+
width: 20px;
81+
height: 100%;
82+
cursor: grab;
83+
color: var(--ogx-color-gray-400);
84+
border-radius: 3px;
85+
transition: color var(--ogx-transition-duration-fast) var(--ogx-transition-easing),
86+
background var(--ogx-transition-duration-fast) var(--ogx-transition-easing);
87+
}
88+
89+
.ogx-column-visibility-panel__drag-handle-wrapper:hover {
90+
color: var(--ogx-color-gray-600);
91+
background: var(--ogx-color-gray-100);
92+
}
93+
94+
.ogx-column-visibility-panel__drag-handle-wrapper:active {
95+
cursor: grabbing;
96+
color: var(--ogx-color-primary);
97+
}
98+
99+
.ogx-column-visibility-panel__drag-handle {
100+
display: block;
101+
pointer-events: none;
68102
}
69103

70104
.ogx-column-visibility-panel__label {

lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useMemo } from 'react';
1+
import { useState, useMemo, useRef } from 'react';
22
import { Checkbox } from '../ui/Checkbox';
33
import type { GridColDef, GridRowModel } from '../../types';
44

@@ -9,6 +9,7 @@ export interface ColumnVisibilityPanelProps<R extends GridRowModel = GridRowMode
99
onVisibilityChange: (field: string, isVisible: boolean) => void;
1010
onShowAll: () => void;
1111
onHideAll: () => void;
12+
onColumnReorder?: (fromField: string, toField: string) => void;
1213
}
1314

1415
function SearchIcon() {
@@ -20,11 +21,32 @@ function SearchIcon() {
2021
);
2122
}
2223

24+
function DragHandleIcon() {
25+
return (
26+
<svg
27+
className="ogx-column-visibility-panel__drag-handle"
28+
width="16"
29+
height="16"
30+
viewBox="0 0 24 24"
31+
fill="currentColor"
32+
>
33+
<circle cx="9" cy="5" r="1.5" />
34+
<circle cx="15" cy="5" r="1.5" />
35+
<circle cx="9" cy="12" r="1.5" />
36+
<circle cx="15" cy="12" r="1.5" />
37+
<circle cx="9" cy="19" r="1.5" />
38+
<circle cx="15" cy="19" r="1.5" />
39+
</svg>
40+
);
41+
}
42+
2343
export function ColumnVisibilityPanel<R extends GridRowModel = GridRowModel>(
2444
props: ColumnVisibilityPanelProps<R>
2545
) {
26-
const { columns, visibleColumns, onVisibilityChange, onShowAll, onHideAll } = props;
46+
const { columns, visibleColumns, onVisibilityChange, onShowAll, onHideAll, onColumnReorder } = props;
2747
const [searchQuery, setSearchQuery] = useState('');
48+
const [dragOverField, setDragOverField] = useState<string | null>(null);
49+
const dragFieldRef = useRef<string | null>(null);
2850

2951
const hideableColumns = useMemo(() => columns.filter(col => col.hideable !== false), [columns]);
3052
const allVisible = hideableColumns.every(col => visibleColumns.has(col.field));
@@ -45,6 +67,41 @@ export function ColumnVisibilityPanel<R extends GridRowModel = GridRowModel>(
4567
}
4668
};
4769

70+
// ── Drag handlers ──────────────────────────────────────────────────────────
71+
const handleDragStart = (field: string) => (e: React.DragEvent) => {
72+
dragFieldRef.current = field;
73+
e.dataTransfer.effectAllowed = 'move';
74+
// Use a ghost image offset so the handle looks natural
75+
e.dataTransfer.setDragImage(e.currentTarget.closest('.ogx-column-visibility-panel__item') as HTMLElement, 20, 20);
76+
};
77+
78+
const handleDragOver = (field: string) => (e: React.DragEvent) => {
79+
e.preventDefault();
80+
e.dataTransfer.dropEffect = 'move';
81+
if (field !== dragFieldRef.current) {
82+
setDragOverField(field);
83+
}
84+
};
85+
86+
const handleDragLeave = () => {
87+
setDragOverField(null);
88+
};
89+
90+
const handleDrop = (toField: string) => (e: React.DragEvent) => {
91+
e.preventDefault();
92+
const fromField = dragFieldRef.current;
93+
setDragOverField(null);
94+
dragFieldRef.current = null;
95+
if (fromField && fromField !== toField && onColumnReorder) {
96+
onColumnReorder(fromField, toField);
97+
}
98+
};
99+
100+
const handleDragEnd = () => {
101+
setDragOverField(null);
102+
dragFieldRef.current = null;
103+
};
104+
48105
return (
49106
<div className="ogx-column-visibility-panel">
50107
<div className="ogx-column-visibility-panel__search-section">
@@ -66,21 +123,47 @@ export function ColumnVisibilityPanel<R extends GridRowModel = GridRowModel>(
66123
{filteredColumns.map(col => {
67124
const isVisible = visibleColumns.has(col.field);
68125
const isHideable = col.hideable !== false;
126+
const isDragOver = dragOverField === col.field;
127+
const canReorder = !!onColumnReorder && !searchQuery;
69128

70129
return (
71-
<label
130+
<div
72131
key={col.field}
73-
className={`ogx-column-visibility-panel__item ${!isHideable ? 'ogx-column-visibility-panel__item--disabled' : ''}`}
132+
className={[
133+
'ogx-column-visibility-panel__item',
134+
!isHideable && 'ogx-column-visibility-panel__item--disabled',
135+
isDragOver && 'ogx-column-visibility-panel__item--drag-over',
136+
].filter(Boolean).join(' ')}
137+
draggable={canReorder}
138+
onDragOver={canReorder ? handleDragOver(col.field) : undefined}
139+
onDragLeave={canReorder ? handleDragLeave : undefined}
140+
onDrop={canReorder ? handleDrop(col.field) : undefined}
141+
onDragEnd={canReorder ? handleDragEnd : undefined}
74142
>
75-
<Checkbox
76-
checked={isVisible}
77-
onChange={(e) => onVisibilityChange(col.field, e.target.checked)}
78-
disabled={!isHideable}
79-
/>
80-
<span className="ogx-column-visibility-panel__label">
81-
{col.headerName || col.field}
82-
</span>
83-
</label>
143+
{canReorder && (
144+
<span
145+
className="ogx-column-visibility-panel__drag-handle-wrapper"
146+
draggable
147+
onDragStart={handleDragStart(col.field)}
148+
title="Drag to reorder"
149+
>
150+
<DragHandleIcon />
151+
</span>
152+
)}
153+
<label
154+
className="ogx-column-visibility-panel__item-label"
155+
style={{ display: 'flex', alignItems: 'center', gap: 'var(--ogx-spacing-md)', flex: 1, cursor: isHideable ? 'pointer' : 'not-allowed' }}
156+
>
157+
<Checkbox
158+
checked={isVisible}
159+
onChange={(e) => onVisibilityChange(col.field, e.target.checked)}
160+
disabled={!isHideable}
161+
/>
162+
<span className="ogx-column-visibility-panel__label">
163+
{col.headerName || col.field}
164+
</span>
165+
</label>
166+
</div>
84167
);
85168
})}
86169
</div>

lib/components/DataGrid/DataGrid.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
782782
}, 0);
783783

784784
const naturalFlexWidth = unpinnedCols.reduce((sum, col) => {
785-
if (col.flex && col.flex > 0) {
785+
const hasManualWidth = Object.prototype.hasOwnProperty.call(columnWidths, col.field);
786+
if (!hasManualWidth && col.flex && col.flex > 0) {
786787
const minWidth = col.minWidth ?? 150;
787788
const width = typeof col.width === 'number' ? col.width : 150;
788789
return sum + (minWidth || width);
@@ -804,9 +805,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
804805
let totalFlexUnits = 0;
805806

806807
unpinnedCols.forEach(col => {
808+
// If the user has manually resized this column, columnWidths has an explicit
809+
// entry for it. Respect that stored width as fixed-width so the resize sticks,
810+
// even if the column definition originally had flex.
811+
const hasManualWidth = Object.prototype.hasOwnProperty.call(columnWidths, col.field);
812+
807813
const parsed = parseWidth(getColWidth(col));
808814

809-
if (col.flex && col.flex > 0) {
815+
if (!hasManualWidth && col.flex && col.flex > 0) {
810816
flexCols.push({ col, flex: col.flex });
811817
totalFlexUnits += col.flex;
812818
return;
@@ -1665,6 +1671,18 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
16651671
columnVisibilityModel,
16661672
onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
16671673

1674+
onColumnReorder: (fromField: string, toField: string) => {
1675+
const currentOrder = [...effectiveColumnOrder];
1676+
const fromIdx = currentOrder.indexOf(fromField);
1677+
const toIdx = currentOrder.indexOf(toField);
1678+
if (fromIdx === -1 || toIdx === -1) return;
1679+
const newOrder = [...currentOrder];
1680+
newOrder.splice(fromIdx, 1);
1681+
newOrder.splice(toIdx, 0, fromField);
1682+
if (!columnOrder) setInternalColumnOrder(newOrder);
1683+
const col = effectiveColumns.find(c => c.field === fromField); if (col) onColumnOrderChange?.({ oldIndex: fromIdx, targetIndex: toIdx, column: col as any });
1684+
},
1685+
16681686
forceColumnsOpen: columnsPanelOpen,
16691687
onColumnsPanelClose: () => setColumnsPanelOpen(false),
16701688

@@ -1731,6 +1749,19 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
17311749
effectiveColumns.forEach(col => { if (col.hideable !== false) next[col.field] = false; });
17321750
handleColumnVisibilityModelChange(next);
17331751
}}
1752+
onColumnReorder={(fromField, toField) => {
1753+
const currentOrder = [...effectiveColumnOrder];
1754+
const fromIdx = currentOrder.indexOf(fromField);
1755+
const toIdx = currentOrder.indexOf(toField);
1756+
if (fromIdx === -1 || toIdx === -1) return;
1757+
const newOrder = [...currentOrder];
1758+
newOrder.splice(fromIdx, 1);
1759+
newOrder.splice(toIdx, 0, fromField);
1760+
if (!columnOrder) {
1761+
setInternalColumnOrder(newOrder);
1762+
}
1763+
onColumnOrderChange?.({ oldIndex: fromIdx, targetIndex: toIdx, column: effectiveColumns.find(c => c.field === fromField) as any });
1764+
}}
17341765
/>
17351766
</div>,
17361767
document.body

lib/components/Toolbar/GridToolbar.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface GridToolbarProps {
1717
onFilterModelChange?: (model: GridFilterModel) => void;
1818
columnVisibilityModel?: Record<string, boolean>;
1919
onColumnVisibilityModelChange?: (model: Record<string, boolean>) => void;
20+
onColumnReorder?: (fromField: string, toField: string) => void;
2021
forceColumnsOpen?: boolean;
2122
onColumnsPanelClose?: () => void;
2223
children?: React.ReactNode;
@@ -189,6 +190,7 @@ function ColumnsPanelWrapper({
189190
onVisibilityChange,
190191
onShowAll,
191192
onHideAll,
193+
onColumnReorder,
192194
onClose
193195
}: {
194196
anchorRef: React.RefObject<HTMLButtonElement | null>;
@@ -197,6 +199,7 @@ function ColumnsPanelWrapper({
197199
onVisibilityChange: (field: string, isVisible: boolean) => void;
198200
onShowAll: () => void;
199201
onHideAll: () => void;
202+
onColumnReorder?: (fromField: string, toField: string) => void;
200203
onClose: () => void;
201204
}) {
202205
const panelRef = useRef<HTMLDivElement>(null);
@@ -249,6 +252,7 @@ function ColumnsPanelWrapper({
249252
onVisibilityChange={onVisibilityChange}
250253
onShowAll={onShowAll}
251254
onHideAll={onHideAll}
255+
onColumnReorder={onColumnReorder}
252256
/>
253257
</div>
254258
);
@@ -372,6 +376,7 @@ export function GridToolbar({
372376
onFilterModelChange,
373377
columnVisibilityModel = {},
374378
onColumnVisibilityModelChange,
379+
onColumnReorder,
375380
forceColumnsOpen,
376381
onColumnsPanelClose,
377382
children,
@@ -521,6 +526,7 @@ export function GridToolbar({
521526
onVisibilityChange={handleVisibilityChange}
522527
onShowAll={handleShowAllColumns}
523528
onHideAll={handleHideAllColumns}
529+
onColumnReorder={onColumnReorder}
524530
onClose={() => { setColsOpen(false); onColumnsPanelClose?.(); }}
525531
/>
526532
)}

llms.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# OpenGridX — AI Agent Context
22
# Package: @opencorestack/opengridx
3-
# Version: 0.1.3
3+
# Version: 0.1.4
44
# Docs: https://opencorestack.github.io/OpenGridX/
55
# This file is machine-readable. AI developers: start here.
66

0 commit comments

Comments
 (0)