gp-grid-react 0.1.1 → 0.1.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/README.md ADDED
@@ -0,0 +1,520 @@
1
+ # gp-grid-react
2
+
3
+ A high-performance React data grid component built on [gp-grid-core](../core/README.md), featuring virtual scrolling, cell selection, sorting, filtering, editing, and Excel-like fill handle.
4
+
5
+ ## Features
6
+
7
+ - **Virtual Scrolling**: Efficiently handles 150,000+ rows through slot-based recycling
8
+ - **Cell Selection**: Single cell, range selection, Shift+click extend, Ctrl+click toggle
9
+ - **Multi-Column Sorting**: Click to sort, Shift+click for multi-column sort
10
+ - **Column Filtering**: Built-in filter row with debounced input
11
+ - **Cell Editing**: Double-click or press Enter to edit, with custom editor support
12
+ - **Fill Handle**: Excel-like drag-to-fill for editable cells
13
+ - **Keyboard Navigation**: Arrow keys, Tab, Enter, Escape, Ctrl+A, Ctrl+C
14
+ - **Custom Renderers**: Registry-based cell, edit, and header renderers
15
+ - **Dark Mode**: Built-in dark theme support
16
+ - **TypeScript**: Full type safety with exported types
17
+
18
+ ## Installation
19
+
20
+ Use `npm`, `yarn` or `pnpm`
21
+ ```bash
22
+ pnpm add gp-grid-react
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```tsx
28
+ import { Grid, type ColumnDefinition } from "gp-grid-react";
29
+
30
+ interface Person {
31
+ id: number;
32
+ name: string;
33
+ age: number;
34
+ email: string;
35
+ }
36
+
37
+ const columns: ColumnDefinition[] = [
38
+ { field: "id", cellDataType: "number", width: 80, headerName: "ID" },
39
+ { field: "name", cellDataType: "text", width: 150, headerName: "Name" },
40
+ { field: "age", cellDataType: "number", width: 80, headerName: "Age" },
41
+ { field: "email", cellDataType: "text", width: 250, headerName: "Email" },
42
+ ];
43
+
44
+ const data: Person[] = [
45
+ { id: 1, name: "Alice", age: 30, email: "alice@example.com" },
46
+ { id: 2, name: "Bob", age: 25, email: "bob@example.com" },
47
+ { id: 3, name: "Charlie", age: 35, email: "charlie@example.com" },
48
+ ];
49
+
50
+ function App() {
51
+ return (
52
+ <div style={{ width: "800px", height: "400px" }}>
53
+ <Grid
54
+ columns={columns}
55
+ rowData={data}
56
+ rowHeight={36}
57
+ />
58
+ </div>
59
+ );
60
+ }
61
+ ```
62
+
63
+ ## Examples
64
+
65
+ ### Client-Side Data Source with Sorting and Filtering
66
+
67
+ For larger datasets with client-side sort/filter operations:
68
+
69
+ ```tsx
70
+ import { useMemo } from "react";
71
+ import { Grid, createClientDataSource, type ColumnDefinition } from "gp-grid-react";
72
+
73
+ interface Product {
74
+ id: number;
75
+ name: string;
76
+ price: number;
77
+ category: string;
78
+ }
79
+
80
+ const columns: ColumnDefinition[] = [
81
+ { field: "id", cellDataType: "number", width: 80, headerName: "ID" },
82
+ { field: "name", cellDataType: "text", width: 200, headerName: "Product" },
83
+ { field: "price", cellDataType: "number", width: 100, headerName: "Price" },
84
+ { field: "category", cellDataType: "text", width: 150, headerName: "Category" },
85
+ ];
86
+
87
+ function ProductGrid() {
88
+ const products: Product[] = useMemo(() =>
89
+ Array.from({ length: 10000 }, (_, i) => ({
90
+ id: i + 1,
91
+ name: `Product ${i + 1}`,
92
+ price: Math.round(Math.random() * 1000) / 10,
93
+ category: ["Electronics", "Clothing", "Food", "Books"][i % 4],
94
+ })),
95
+ []);
96
+
97
+ const dataSource = useMemo(
98
+ () => createClientDataSource(products),
99
+ [products]
100
+ );
101
+
102
+ return (
103
+ <div style={{ width: "100%", height: "500px" }}>
104
+ <Grid
105
+ columns={columns}
106
+ dataSource={dataSource}
107
+ rowHeight={36}
108
+ headerHeight={40}
109
+ showFilters={true}
110
+ filterDebounce={300}
111
+ />
112
+ </div>
113
+ );
114
+ }
115
+ ```
116
+
117
+ ### Server-Side Data Source
118
+
119
+ For datasets too large to load entirely in memory, use a server-side data source:
120
+
121
+ ```tsx
122
+ import { useMemo } from "react";
123
+ import {
124
+ Grid,
125
+ createServerDataSource,
126
+ type ColumnDefinition,
127
+ type DataSourceRequest,
128
+ type DataSourceResponse,
129
+ } from "gp-grid-react";
130
+
131
+ interface User {
132
+ id: number;
133
+ name: string;
134
+ email: string;
135
+ role: string;
136
+ createdAt: string;
137
+ }
138
+
139
+ const columns: ColumnDefinition[] = [
140
+ { field: "id", cellDataType: "number", width: 80, headerName: "ID" },
141
+ { field: "name", cellDataType: "text", width: 150, headerName: "Name" },
142
+ { field: "email", cellDataType: "text", width: 250, headerName: "Email" },
143
+ { field: "role", cellDataType: "text", width: 120, headerName: "Role" },
144
+ { field: "createdAt", cellDataType: "dateString", width: 150, headerName: "Created" },
145
+ ];
146
+
147
+ // API fetch function that handles pagination, sorting, and filtering
148
+ async function fetchUsers(request: DataSourceRequest): Promise<DataSourceResponse<User>> {
149
+ const { pagination, sort, filter } = request;
150
+
151
+ // Build query parameters
152
+ const params = new URLSearchParams({
153
+ page: String(pagination.pageIndex),
154
+ limit: String(pagination.pageSize),
155
+ });
156
+
157
+ // Add sorting parameters
158
+ if (sort && sort.length > 0) {
159
+ // Format: sortBy=name:asc,email:desc
160
+ const sortString = sort
161
+ .map((s) => `${s.colId}:${s.direction}`)
162
+ .join(",");
163
+ params.set("sortBy", sortString);
164
+ }
165
+
166
+ // Add filter parameters
167
+ if (filter) {
168
+ Object.entries(filter).forEach(([field, value]) => {
169
+ if (value) {
170
+ params.set(`filter[${field}]`, value);
171
+ }
172
+ });
173
+ }
174
+
175
+ // Make API request
176
+ const response = await fetch(`https://api.example.com/users?${params}`);
177
+
178
+ if (!response.ok) {
179
+ throw new Error(`API error: ${response.status}`);
180
+ }
181
+
182
+ const data = await response.json();
183
+
184
+ // Return in DataSourceResponse format
185
+ return {
186
+ rows: data.users, // Array of User objects
187
+ totalRows: data.total, // Total count for virtual scrolling
188
+ };
189
+ }
190
+
191
+ function UserGrid() {
192
+ // Create server data source - memoize to prevent recreation
193
+ const dataSource = useMemo(
194
+ () => createServerDataSource<User>(fetchUsers),
195
+ []
196
+ );
197
+
198
+ return (
199
+ <div style={{ width: "100%", height: "600px" }}>
200
+ <Grid
201
+ columns={columns}
202
+ dataSource={dataSource}
203
+ rowHeight={36}
204
+ headerHeight={40}
205
+ showFilters={true}
206
+ filterDebounce={500} // Debounce filter requests
207
+ darkMode={true}
208
+ />
209
+ </div>
210
+ );
211
+ }
212
+ ```
213
+
214
+ ### Custom Cell Renderers
215
+
216
+ Use the registry pattern to define reusable renderers:
217
+
218
+ ```tsx
219
+ import { Grid, type ColumnDefinition, type CellRendererParams } from "gp-grid-react";
220
+
221
+ interface Order {
222
+ id: number;
223
+ customer: string;
224
+ total: number;
225
+ status: "pending" | "shipped" | "delivered" | "cancelled";
226
+ }
227
+
228
+ // Define reusable renderers
229
+ const cellRenderers = {
230
+ // Currency formatter
231
+ currency: (params: CellRendererParams) => {
232
+ const value = params.value as number;
233
+ return (
234
+ <span style={{ color: "#047857", fontWeight: 600 }}>
235
+ ${value.toLocaleString("en-US", { minimumFractionDigits: 2 })}
236
+ </span>
237
+ );
238
+ },
239
+
240
+ // Status badge
241
+ statusBadge: (params: CellRendererParams) => {
242
+ const status = params.value as Order["status"];
243
+ const colors: Record<string, { bg: string; text: string }> = {
244
+ pending: { bg: "#fef3c7", text: "#92400e" },
245
+ shipped: { bg: "#dbeafe", text: "#1e40af" },
246
+ delivered: { bg: "#dcfce7", text: "#166534" },
247
+ cancelled: { bg: "#fee2e2", text: "#991b1b" },
248
+ };
249
+ const color = colors[status] ?? { bg: "#f3f4f6", text: "#374151" };
250
+
251
+ return (
252
+ <span
253
+ style={{
254
+ backgroundColor: color.bg,
255
+ color: color.text,
256
+ padding: "2px 8px",
257
+ borderRadius: "12px",
258
+ fontSize: "12px",
259
+ fontWeight: 600,
260
+ }}
261
+ >
262
+ {status.toUpperCase()}
263
+ </span>
264
+ );
265
+ },
266
+
267
+ // Bold text
268
+ bold: (params: CellRendererParams) => (
269
+ <strong>{String(params.value ?? "")}</strong>
270
+ ),
271
+ };
272
+
273
+ const columns: ColumnDefinition[] = [
274
+ { field: "id", cellDataType: "number", width: 80, headerName: "ID", cellRenderer: "bold" },
275
+ { field: "customer", cellDataType: "text", width: 200, headerName: "Customer" },
276
+ { field: "total", cellDataType: "number", width: 120, headerName: "Total", cellRenderer: "currency" },
277
+ { field: "status", cellDataType: "text", width: 120, headerName: "Status", cellRenderer: "statusBadge" },
278
+ ];
279
+
280
+ function OrderGrid({ orders }: { orders: Order[] }) {
281
+ return (
282
+ <div style={{ width: "100%", height: "400px" }}>
283
+ <Grid
284
+ columns={columns}
285
+ rowData={orders}
286
+ rowHeight={40}
287
+ cellRenderers={cellRenderers}
288
+ />
289
+ </div>
290
+ );
291
+ }
292
+ ```
293
+
294
+ ### Editable Cells with Custom Editors
295
+
296
+ ```tsx
297
+ import { useState } from "react";
298
+ import {
299
+ Grid,
300
+ createClientDataSource,
301
+ type ColumnDefinition,
302
+ type EditRendererParams
303
+ } from "gp-grid-react";
304
+
305
+ interface Task {
306
+ id: number;
307
+ title: string;
308
+ priority: "low" | "medium" | "high";
309
+ completed: boolean;
310
+ }
311
+
312
+ // Custom select editor for priority field
313
+ const editRenderers = {
314
+ prioritySelect: (params: EditRendererParams) => {
315
+ const [value, setValue] = useState(params.initialValue as string);
316
+
317
+ return (
318
+ <select
319
+ autoFocus
320
+ value={value}
321
+ onChange={(e) => {
322
+ setValue(e.target.value);
323
+ params.onValueChange(e.target.value);
324
+ }}
325
+ onBlur={() => params.onCommit()}
326
+ onKeyDown={(e) => {
327
+ if (e.key === "Enter") params.onCommit();
328
+ if (e.key === "Escape") params.onCancel();
329
+ }}
330
+ style={{
331
+ width: "100%",
332
+ height: "100%",
333
+ border: "none",
334
+ outline: "none",
335
+ padding: "0 8px",
336
+ }}
337
+ >
338
+ <option value="low">Low</option>
339
+ <option value="medium">Medium</option>
340
+ <option value="high">High</option>
341
+ </select>
342
+ );
343
+ },
344
+
345
+ checkbox: (params: EditRendererParams) => (
346
+ <input
347
+ type="checkbox"
348
+ autoFocus
349
+ defaultChecked={params.initialValue as boolean}
350
+ onChange={(e) => {
351
+ params.onValueChange(e.target.checked);
352
+ params.onCommit();
353
+ }}
354
+ style={{ width: 20, height: 20 }}
355
+ />
356
+ ),
357
+ };
358
+
359
+ const columns: ColumnDefinition[] = [
360
+ { field: "id", cellDataType: "number", width: 60, headerName: "ID" },
361
+ {
362
+ field: "title",
363
+ cellDataType: "text",
364
+ width: 300,
365
+ headerName: "Title",
366
+ editable: true, // Uses default text input
367
+ },
368
+ {
369
+ field: "priority",
370
+ cellDataType: "text",
371
+ width: 120,
372
+ headerName: "Priority",
373
+ editable: true,
374
+ editRenderer: "prioritySelect", // Custom editor
375
+ },
376
+ {
377
+ field: "completed",
378
+ cellDataType: "boolean",
379
+ width: 100,
380
+ headerName: "Done",
381
+ editable: true,
382
+ editRenderer: "checkbox", // Custom editor
383
+ },
384
+ ];
385
+
386
+ function TaskGrid() {
387
+ const tasks: Task[] = [
388
+ { id: 1, title: "Write documentation", priority: "high", completed: false },
389
+ { id: 2, title: "Fix bugs", priority: "medium", completed: true },
390
+ { id: 3, title: "Add tests", priority: "low", completed: false },
391
+ ];
392
+
393
+ const dataSource = createClientDataSource(tasks);
394
+
395
+ return (
396
+ <div style={{ width: "600px", height: "300px" }}>
397
+ <Grid
398
+ columns={columns}
399
+ dataSource={dataSource}
400
+ rowHeight={40}
401
+ editRenderers={editRenderers}
402
+ />
403
+ </div>
404
+ );
405
+ }
406
+ ```
407
+
408
+ ### Dark Mode
409
+
410
+ ```tsx
411
+ <Grid
412
+ columns={columns}
413
+ rowData={data}
414
+ rowHeight={36}
415
+ darkMode={true}
416
+ />
417
+ ```
418
+
419
+ ## API Reference
420
+
421
+ ### GridProps
422
+
423
+ | Prop | Type | Default | Description |
424
+ |------|------|---------|-------------|
425
+ | `columns` | `ColumnDefinition[]` | required | Column definitions |
426
+ | `dataSource` | `DataSource<TData>` | - | Data source for fetching data |
427
+ | `rowData` | `TData[]` | - | Alternative: raw data array (wrapped in client data source) |
428
+ | `rowHeight` | `number` | required | Height of each row in pixels |
429
+ | `headerHeight` | `number` | `rowHeight` | Height of header row |
430
+ | `overscan` | `number` | `3` | Number of rows to render outside viewport |
431
+ | `showFilters` | `boolean` | `false` | Show filter row below headers |
432
+ | `filterDebounce` | `number` | `300` | Debounce time for filter input (ms) |
433
+ | `darkMode` | `boolean` | `false` | Enable dark theme |
434
+ | `cellRenderers` | `Record<string, ReactCellRenderer>` | `{}` | Cell renderer registry |
435
+ | `editRenderers` | `Record<string, ReactEditRenderer>` | `{}` | Edit renderer registry |
436
+ | `headerRenderers` | `Record<string, ReactHeaderRenderer>` | `{}` | Header renderer registry |
437
+ | `cellRenderer` | `ReactCellRenderer` | - | Global fallback cell renderer |
438
+ | `editRenderer` | `ReactEditRenderer` | - | Global fallback edit renderer |
439
+ | `headerRenderer` | `ReactHeaderRenderer` | - | Global fallback header renderer |
440
+
441
+ ### ColumnDefinition
442
+
443
+ | Property | Type | Description |
444
+ |----------|------|-------------|
445
+ | `field` | `string` | Property path in row data (supports dot notation: `"address.city"`) |
446
+ | `colId` | `string` | Unique column ID (defaults to `field`) |
447
+ | `cellDataType` | `CellDataType` | `"text"` \| `"number"` \| `"boolean"` \| `"date"` \| `"object"` |
448
+ | `width` | `number` | Column width in pixels |
449
+ | `headerName` | `string` | Display name in header (defaults to `field`) |
450
+ | `editable` | `boolean` | Enable cell editing |
451
+ | `cellRenderer` | `string` | Key in `cellRenderers` registry |
452
+ | `editRenderer` | `string` | Key in `editRenderers` registry |
453
+ | `headerRenderer` | `string` | Key in `headerRenderers` registry |
454
+
455
+ ### Renderer Types
456
+
457
+ ```typescript
458
+ // Cell renderer receives these params
459
+ interface CellRendererParams {
460
+ value: CellValue; // Current cell value
461
+ rowData: Row; // Full row data
462
+ column: ColumnDefinition; // Column definition
463
+ rowIndex: number; // Row index
464
+ colIndex: number; // Column index
465
+ isActive: boolean; // Is this the active cell?
466
+ isSelected: boolean; // Is this cell in selection?
467
+ isEditing: boolean; // Is this cell being edited?
468
+ }
469
+
470
+ // Edit renderer receives additional callbacks
471
+ interface EditRendererParams extends CellRendererParams {
472
+ initialValue: CellValue;
473
+ onValueChange: (newValue: CellValue) => void;
474
+ onCommit: () => void;
475
+ onCancel: () => void;
476
+ }
477
+
478
+ // Header renderer params
479
+ interface HeaderRendererParams {
480
+ column: ColumnDefinition;
481
+ colIndex: number;
482
+ sortDirection?: "asc" | "desc";
483
+ sortIndex?: number; // For multi-column sort
484
+ onSort: (direction: "asc" | "desc" | null, addToExisting: boolean) => void;
485
+ }
486
+ ```
487
+
488
+ ## Keyboard Shortcuts
489
+
490
+ | Key | Action |
491
+ |-----|--------|
492
+ | Arrow keys | Navigate between cells |
493
+ | Shift + Arrow | Extend selection |
494
+ | Enter | Start editing / Commit edit |
495
+ | Escape | Cancel edit / Clear selection |
496
+ | Tab | Commit and move right |
497
+ | Shift + Tab | Commit and move left |
498
+ | F2 | Start editing |
499
+ | Delete / Backspace | Start editing with empty value |
500
+ | Ctrl + A | Select all |
501
+ | Ctrl + C | Copy selection to clipboard |
502
+ | Any character | Start editing with that character |
503
+
504
+ ## Styling
505
+
506
+ The grid injects its own styles automatically. The main container uses these CSS classes:
507
+
508
+ - `.gp-grid-container` - Main container
509
+ - `.gp-grid-container--dark` - Dark mode modifier
510
+ - `.gp-grid-header` - Header row container
511
+ - `.gp-grid-header-cell` - Individual header cell
512
+ - `.gp-grid-row` - Row container
513
+ - `.gp-grid-cell` - Cell container
514
+ - `.gp-grid-cell--active` - Active cell
515
+ - `.gp-grid-cell--selected` - Selected cell
516
+ - `.gp-grid-cell--editing` - Cell in edit mode
517
+ - `.gp-grid-filter-row` - Filter row container
518
+ - `.gp-grid-filter-input` - Filter input field
519
+ - `.gp-grid-fill-handle` - Fill handle element
520
+
package/dist/index.js CHANGED
@@ -101,6 +101,8 @@ const gridStyles = `
101
101
  background-color: var(--gp-grid-bg);
102
102
  border: 1px solid var(--gp-grid-border);
103
103
  border-radius: 6px;
104
+ user-select: none;
105
+ -webkit-user-select: none;
104
106
  }
105
107
 
106
108
  .gp-grid-container:focus {
@@ -238,6 +240,8 @@ const gridStyles = `
238
240
  overflow: hidden;
239
241
  text-overflow: ellipsis;
240
242
  white-space: nowrap;
243
+ user-select: none;
244
+ -webkit-user-select: none;
241
245
  }
242
246
 
243
247
  /* Alternating row colors */
@@ -548,6 +552,7 @@ function Grid(props) {
548
552
  const [fillTarget, setFillTarget] = useState(null);
549
553
  const [fillSourceRange, setFillSourceRange] = useState(null);
550
554
  const autoScrollIntervalRef = useRef(null);
555
+ const [isDraggingSelection, setIsDraggingSelection] = useState(false);
551
556
  const filterRowHeight = showFilters ? 40 : 0;
552
557
  const totalHeaderHeight = headerHeight + filterRowHeight;
553
558
  const dataSource = useMemo(() => {
@@ -711,9 +716,10 @@ function Grid(props) {
711
716
  columnPositions,
712
717
  columns
713
718
  ]);
714
- const handleCellClick = useCallback((rowIndex, colIndex, e) => {
719
+ const handleCellMouseDown = useCallback((rowIndex, colIndex, e) => {
715
720
  const core = coreRef.current;
716
721
  if (!core || core.getEditState() !== null) return;
722
+ if (e.button !== 0) return;
717
723
  containerRef.current?.focus();
718
724
  core.selection.startSelection({
719
725
  row: rowIndex,
@@ -722,6 +728,7 @@ function Grid(props) {
722
728
  shift: e.shiftKey,
723
729
  ctrl: e.ctrlKey || e.metaKey
724
730
  });
731
+ if (!e.shiftKey) setIsDraggingSelection(true);
725
732
  }, []);
726
733
  const handleCellDoubleClick = useCallback((rowIndex, colIndex) => {
727
734
  const core = coreRef.current;
@@ -838,6 +845,76 @@ function Grid(props) {
838
845
  rowHeight,
839
846
  columnPositions
840
847
  ]);
848
+ useEffect(() => {
849
+ if (!isDraggingSelection) return;
850
+ const SCROLL_THRESHOLD = 40;
851
+ const SCROLL_SPEED = 10;
852
+ const handleMouseMove = (e) => {
853
+ const core = coreRef.current;
854
+ const container = containerRef.current;
855
+ if (!core || !container) return;
856
+ const rect = container.getBoundingClientRect();
857
+ const scrollLeft = container.scrollLeft;
858
+ const scrollTop = container.scrollTop;
859
+ const mouseX = e.clientX - rect.left + scrollLeft;
860
+ const mouseY = e.clientY - rect.top + scrollTop - totalHeaderHeight;
861
+ const targetRow = Math.max(0, Math.min(Math.floor(mouseY / rowHeight), core.getRowCount() - 1));
862
+ let targetCol = 0;
863
+ for (let i = 0; i < columnPositions.length - 1; i++) {
864
+ if (mouseX >= columnPositions[i] && mouseX < columnPositions[i + 1]) {
865
+ targetCol = i;
866
+ break;
867
+ }
868
+ if (mouseX >= columnPositions[columnPositions.length - 1]) targetCol = columnPositions.length - 2;
869
+ }
870
+ targetCol = Math.max(0, Math.min(targetCol, columns.length - 1));
871
+ core.selection.startSelection({
872
+ row: targetRow,
873
+ col: targetCol
874
+ }, { shift: true });
875
+ const mouseYInContainer = e.clientY - rect.top;
876
+ const mouseXInContainer = e.clientX - rect.left;
877
+ if (autoScrollIntervalRef.current) {
878
+ clearInterval(autoScrollIntervalRef.current);
879
+ autoScrollIntervalRef.current = null;
880
+ }
881
+ let scrollDeltaX = 0;
882
+ let scrollDeltaY = 0;
883
+ if (mouseYInContainer < SCROLL_THRESHOLD + totalHeaderHeight) scrollDeltaY = -SCROLL_SPEED;
884
+ else if (mouseYInContainer > rect.height - SCROLL_THRESHOLD) scrollDeltaY = SCROLL_SPEED;
885
+ if (mouseXInContainer < SCROLL_THRESHOLD) scrollDeltaX = -SCROLL_SPEED;
886
+ else if (mouseXInContainer > rect.width - SCROLL_THRESHOLD) scrollDeltaX = SCROLL_SPEED;
887
+ if (scrollDeltaX !== 0 || scrollDeltaY !== 0) autoScrollIntervalRef.current = setInterval(() => {
888
+ if (containerRef.current) {
889
+ containerRef.current.scrollTop += scrollDeltaY;
890
+ containerRef.current.scrollLeft += scrollDeltaX;
891
+ }
892
+ }, 16);
893
+ };
894
+ const handleMouseUp = () => {
895
+ if (autoScrollIntervalRef.current) {
896
+ clearInterval(autoScrollIntervalRef.current);
897
+ autoScrollIntervalRef.current = null;
898
+ }
899
+ setIsDraggingSelection(false);
900
+ };
901
+ document.addEventListener("mousemove", handleMouseMove);
902
+ document.addEventListener("mouseup", handleMouseUp);
903
+ return () => {
904
+ if (autoScrollIntervalRef.current) {
905
+ clearInterval(autoScrollIntervalRef.current);
906
+ autoScrollIntervalRef.current = null;
907
+ }
908
+ document.removeEventListener("mousemove", handleMouseMove);
909
+ document.removeEventListener("mouseup", handleMouseUp);
910
+ };
911
+ }, [
912
+ isDraggingSelection,
913
+ totalHeaderHeight,
914
+ rowHeight,
915
+ columnPositions,
916
+ columns.length
917
+ ]);
841
918
  const isSelected = useCallback((row, col) => {
842
919
  const { selectionRange } = state;
843
920
  if (!selectionRange) return false;
@@ -1130,7 +1207,7 @@ function Grid(props) {
1130
1207
  width: `${column.width}px`,
1131
1208
  height: `${rowHeight}px`
1132
1209
  },
1133
- onClick: (e) => handleCellClick(slot.rowIndex, colIndex, e),
1210
+ onMouseDown: (e) => handleCellMouseDown(slot.rowIndex, colIndex, e),
1134
1211
  onDoubleClick: () => handleCellDoubleClick(slot.rowIndex, colIndex),
1135
1212
  children: isEditing && state.editingCell ? renderEditCell(column, slot.rowData, slot.rowIndex, colIndex, state.editingCell.initialValue) : renderCell(column, slot.rowData, slot.rowIndex, colIndex)
1136
1213
  }, `${slot.slotId}-${colIndex}`);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["stateChanges: Partial<GridState>","createDataSourceFromArray","createClientDataSource","newDirection: SortDirection | null","value: unknown","rowData","params: CellRendererParams","params: EditRendererParams","params: HeaderRendererParams","row: number","col: number","minCol: number","maxCol: number"],"sources":["../src/styles.ts","../src/Grid.tsx"],"sourcesContent":["// packages/react/src/styles.ts\r\n// Dynamic CSS injection for gp-grid-react\r\n\r\nconst STYLE_ID = \"gp-grid-styles\";\r\n\r\nexport const gridStyles = `\r\n/* =============================================================================\r\n GP Grid - CSS Variables for Theming\r\n ============================================================================= */\r\n\r\n.gp-grid-container {\r\n /* Colors - Light Mode (default) */\r\n --gp-grid-bg: #ffffff;\r\n --gp-grid-bg-alt: #f8f9fa;\r\n --gp-grid-text: #212529;\r\n --gp-grid-text-secondary: #6c757d;\r\n --gp-grid-text-muted: #adb5bd;\r\n --gp-grid-border: #dee2e6;\r\n --gp-grid-border-light: #e9ecef;\r\n \r\n /* Header */\r\n --gp-grid-header-bg: #f1f3f5;\r\n --gp-grid-header-text: #212529;\r\n \r\n /* Selection */\r\n --gp-grid-primary: #228be6;\r\n --gp-grid-primary-light: #e7f5ff;\r\n --gp-grid-primary-border: #74c0fc;\r\n --gp-grid-hover: #f1f3f5;\r\n \r\n /* Filter */\r\n --gp-grid-filter-bg: #f8f9fa;\r\n --gp-grid-input-bg: #ffffff;\r\n --gp-grid-input-border: #ced4da;\r\n \r\n /* Error */\r\n --gp-grid-error-bg: #fff5f5;\r\n --gp-grid-error-text: #c92a2a;\r\n \r\n /* Loading */\r\n --gp-grid-loading-bg: rgba(255, 255, 255, 0.95);\r\n --gp-grid-loading-text: #495057;\r\n \r\n /* Scrollbar */\r\n --gp-grid-scrollbar-track: #f1f3f5;\r\n --gp-grid-scrollbar-thumb: #ced4da;\r\n --gp-grid-scrollbar-thumb-hover: #adb5bd;\r\n}\r\n\r\n/* Dark Mode */\r\n.gp-grid-container--dark {\r\n --gp-grid-bg: #1a1b1e;\r\n --gp-grid-bg-alt: #25262b;\r\n --gp-grid-text: #c1c2c5;\r\n --gp-grid-text-secondary: #909296;\r\n --gp-grid-text-muted: #5c5f66;\r\n --gp-grid-border: #373a40;\r\n --gp-grid-border-light: #2c2e33;\r\n \r\n /* Header */\r\n --gp-grid-header-bg: #25262b;\r\n --gp-grid-header-text: #c1c2c5;\r\n \r\n /* Selection */\r\n --gp-grid-primary: #339af0;\r\n --gp-grid-primary-light: #1c3d5a;\r\n --gp-grid-primary-border: #1c7ed6;\r\n --gp-grid-hover: #2c2e33;\r\n \r\n /* Filter */\r\n --gp-grid-filter-bg: #25262b;\r\n --gp-grid-input-bg: #1a1b1e;\r\n --gp-grid-input-border: #373a40;\r\n \r\n /* Error */\r\n --gp-grid-error-bg: #2c1a1a;\r\n --gp-grid-error-text: #ff6b6b;\r\n \r\n /* Loading */\r\n --gp-grid-loading-bg: rgba(26, 27, 30, 0.95);\r\n --gp-grid-loading-text: #c1c2c5;\r\n \r\n /* Scrollbar */\r\n --gp-grid-scrollbar-track: #25262b;\r\n --gp-grid-scrollbar-thumb: #373a40;\r\n --gp-grid-scrollbar-thumb-hover: #4a4d52;\r\n}\r\n\r\n/* =============================================================================\r\n GP Grid - Clean Flat Design\r\n ============================================================================= */\r\n\r\n/* Grid Container */\r\n.gp-grid-container {\r\n outline: none;\r\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\r\n font-size: 13px;\r\n line-height: 1.5;\r\n color: var(--gp-grid-text);\r\n background-color: var(--gp-grid-bg);\r\n border: 1px solid var(--gp-grid-border);\r\n border-radius: 6px;\r\n}\r\n\r\n.gp-grid-container:focus {\r\n outline: none;\r\n border-color: var(--gp-grid-primary);\r\n}\r\n\r\n/* =============================================================================\r\n Header\r\n ============================================================================= */\r\n\r\n.gp-grid-header {\r\n position: sticky;\r\n top: 0;\r\n left: 0;\r\n z-index: 100;\r\n background-color: var(--gp-grid-header-bg);\r\n border-bottom: 1px solid var(--gp-grid-border);\r\n}\r\n\r\n.gp-grid-container .gp-grid-header-cell {\r\n position: absolute;\r\n box-sizing: border-box;\r\n border-right: 1px solid var(--gp-grid-border);\r\n font-weight: 600;\r\n font-size: 12px;\r\n text-transform: uppercase;\r\n letter-spacing: 0.5px;\r\n color: var(--gp-grid-header-text);\r\n cursor: pointer;\r\n user-select: none;\r\n display: flex;\r\n align-items: center;\r\n padding: 0 12px;\r\n background-color: transparent;\r\n transition: background-color 0.1s ease;\r\n}\r\n\r\n.gp-grid-container .gp-grid-header-cell:hover {\r\n background-color: var(--gp-grid-hover);\r\n}\r\n\r\n.gp-grid-container .gp-grid-header-cell:active {\r\n background-color: var(--gp-grid-border-light);\r\n}\r\n\r\n.gp-grid-container .gp-grid-header-text {\r\n flex: 1;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n color: var(--gp-grid-header-text);\r\n}\r\n\r\n.gp-grid-sort-indicator {\r\n margin-left: 6px;\r\n font-size: 10px;\r\n color: var(--gp-grid-primary);\r\n display: flex;\r\n align-items: center;\r\n}\r\n\r\n.gp-grid-sort-index {\r\n font-size: 9px;\r\n margin-left: 2px;\r\n color: var(--gp-grid-text-secondary);\r\n}\r\n\r\n/* =============================================================================\r\n Filter Row\r\n ============================================================================= */\r\n\r\n.gp-grid-filter-row {\r\n position: sticky;\r\n left: 0;\r\n z-index: 99;\r\n background-color: var(--gp-grid-filter-bg);\r\n border-bottom: 1px solid var(--gp-grid-border);\r\n}\r\n\r\n.gp-grid-filter-cell {\r\n position: absolute;\r\n box-sizing: border-box;\r\n border-right: 1px solid var(--gp-grid-border);\r\n padding: 6px 8px;\r\n display: flex;\r\n align-items: center;\r\n background-color: var(--gp-grid-filter-bg);\r\n}\r\n\r\n.gp-grid-filter-input {\r\n width: 100%;\r\n height: 28px;\r\n padding: 0 10px;\r\n font-family: inherit;\r\n font-size: 12px;\r\n border: 1px solid var(--gp-grid-input-border);\r\n border-radius: 4px;\r\n background-color: var(--gp-grid-input-bg);\r\n color: var(--gp-grid-text);\r\n transition: border-color 0.15s ease;\r\n}\r\n\r\n.gp-grid-filter-input:focus {\r\n outline: none;\r\n border-color: var(--gp-grid-primary);\r\n}\r\n\r\n.gp-grid-filter-input::placeholder {\r\n color: var(--gp-grid-text-muted);\r\n}\r\n\r\n/* =============================================================================\r\n Data Cells\r\n ============================================================================= */\r\n\r\n.gp-grid-row {\r\n position: absolute;\r\n top: 0;\r\n left: 0;\r\n}\r\n\r\n.gp-grid-cell {\r\n position: absolute;\r\n top: 0;\r\n box-sizing: border-box;\r\n padding: 0 12px;\r\n display: flex;\r\n align-items: center;\r\n cursor: cell;\r\n color: var(--gp-grid-text);\r\n border-right: 1px solid var(--gp-grid-border-light);\r\n border-bottom: 1px solid var(--gp-grid-border-light);\r\n background-color: var(--gp-grid-bg);\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n\r\n/* Alternating row colors */\r\n.gp-grid-row--even .gp-grid-cell {\r\n background-color: var(--gp-grid-bg-alt);\r\n}\r\n\r\n.gp-grid-cell:hover {\r\n background-color: var(--gp-grid-hover) !important;\r\n}\r\n\r\n/* Active cell (focused) */\r\n.gp-grid-cell--active {\r\n background-color: var(--gp-grid-primary-light) !important;\r\n border: 2px solid var(--gp-grid-primary) !important;\r\n outline: none;\r\n z-index: 5;\r\n padding: 0 11px;\r\n}\r\n\r\n/* Selected cells (range selection) */\r\n.gp-grid-cell--selected {\r\n background-color: var(--gp-grid-primary-light) !important;\r\n}\r\n\r\n/* Editing cell */\r\n.gp-grid-cell--editing {\r\n background-color: var(--gp-grid-bg) !important;\r\n border: 2px solid var(--gp-grid-primary) !important;\r\n padding: 0 !important;\r\n z-index: 10;\r\n}\r\n\r\n/* =============================================================================\r\n Fill Handle (drag to fill)\r\n ============================================================================= */\r\n\r\n.gp-grid-fill-handle {\r\n position: absolute;\r\n width: 8px;\r\n height: 8px;\r\n background-color: var(--gp-grid-primary);\r\n border: 2px solid var(--gp-grid-bg);\r\n cursor: crosshair;\r\n z-index: 100;\r\n pointer-events: auto;\r\n box-sizing: border-box;\r\n border-radius: 1px;\r\n}\r\n\r\n.gp-grid-fill-handle:hover {\r\n transform: scale(1.2);\r\n}\r\n\r\n/* Fill preview (cells being filled) */\r\n.gp-grid-cell--fill-preview {\r\n background-color: var(--gp-grid-primary-light) !important;\r\n border: 1px dashed var(--gp-grid-primary) !important;\r\n}\r\n\r\n/* =============================================================================\r\n Edit Input\r\n ============================================================================= */\r\n\r\n.gp-grid-edit-input {\r\n width: 100%;\r\n height: 100%;\r\n padding: 0 11px;\r\n font-family: inherit;\r\n font-size: inherit;\r\n color: var(--gp-grid-text);\r\n border: none;\r\n background-color: transparent;\r\n}\r\n\r\n.gp-grid-edit-input:focus {\r\n outline: none;\r\n}\r\n\r\n/* =============================================================================\r\n Loading & Error States\r\n ============================================================================= */\r\n\r\n.gp-grid-loading {\r\n position: absolute;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%);\r\n padding: 12px 20px;\r\n background-color: var(--gp-grid-loading-bg);\r\n color: var(--gp-grid-loading-text);\r\n border-radius: 6px;\r\n border: 1px solid var(--gp-grid-border);\r\n font-weight: 500;\r\n font-size: 13px;\r\n z-index: 1000;\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n}\r\n\r\n.gp-grid-loading-spinner {\r\n width: 16px;\r\n height: 16px;\r\n border: 2px solid var(--gp-grid-border);\r\n border-top-color: var(--gp-grid-primary);\r\n border-radius: 50%;\r\n animation: gp-grid-spin 0.7s linear infinite;\r\n}\r\n\r\n@keyframes gp-grid-spin {\r\n to {\r\n transform: rotate(360deg);\r\n }\r\n}\r\n\r\n.gp-grid-error {\r\n position: absolute;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%);\r\n padding: 12px 20px;\r\n background-color: var(--gp-grid-error-bg);\r\n color: var(--gp-grid-error-text);\r\n border-radius: 6px;\r\n border: 1px solid var(--gp-grid-error-text);\r\n font-weight: 500;\r\n font-size: 13px;\r\n z-index: 1000;\r\n max-width: 80%;\r\n text-align: center;\r\n}\r\n\r\n/* =============================================================================\r\n Empty State\r\n ============================================================================= */\r\n\r\n.gp-grid-empty {\r\n position: absolute;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%);\r\n color: var(--gp-grid-text-muted);\r\n font-size: 14px;\r\n text-align: center;\r\n}\r\n\r\n/* =============================================================================\r\n Scrollbar Styling\r\n ============================================================================= */\r\n\r\n.gp-grid-container::-webkit-scrollbar {\r\n width: 8px;\r\n height: 8px;\r\n}\r\n\r\n.gp-grid-container::-webkit-scrollbar-track {\r\n background-color: var(--gp-grid-scrollbar-track);\r\n}\r\n\r\n.gp-grid-container::-webkit-scrollbar-thumb {\r\n background-color: var(--gp-grid-scrollbar-thumb);\r\n border-radius: 4px;\r\n}\r\n\r\n.gp-grid-container::-webkit-scrollbar-thumb:hover {\r\n background-color: var(--gp-grid-scrollbar-thumb-hover);\r\n}\r\n\r\n.gp-grid-container::-webkit-scrollbar-corner {\r\n background-color: var(--gp-grid-scrollbar-track);\r\n}\r\n`;\r\n\r\nlet stylesInjected = false;\r\n\r\n/**\r\n * Inject grid styles into the document head.\r\n * This is called automatically when the Grid component mounts.\r\n * Styles are only injected once, even if multiple Grid instances exist.\r\n */\r\nexport function injectStyles(): void {\r\n if (stylesInjected) return;\r\n if (typeof document === \"undefined\") return; // SSR safety\r\n\r\n // Check if styles already exist (e.g., from a previous mount)\r\n if (document.getElementById(STYLE_ID)) {\r\n stylesInjected = true;\r\n return;\r\n }\r\n\r\n const styleElement = document.createElement(\"style\");\r\n styleElement.id = STYLE_ID;\r\n styleElement.textContent = gridStyles;\r\n document.head.appendChild(styleElement);\r\n stylesInjected = true;\r\n}\r\n","// packages/react/src/Grid.tsx\r\n\r\nimport React, {\r\n useEffect,\r\n useRef,\r\n useReducer,\r\n useCallback,\r\n useMemo,\r\n useState,\r\n} from \"react\";\r\nimport {\r\n GridCore,\r\n createClientDataSource,\r\n createDataSourceFromArray,\r\n} from \"gp-grid-core\";\r\nimport type {\r\n GridInstruction,\r\n ColumnDefinition,\r\n DataSource,\r\n Row,\r\n CellRendererParams,\r\n EditRendererParams,\r\n HeaderRendererParams,\r\n CellPosition,\r\n CellRange,\r\n CellValue,\r\n SortDirection,\r\n} from \"gp-grid-core\";\r\nimport { injectStyles } from \"./styles\";\r\n\r\n// =============================================================================\r\n// Types\r\n// =============================================================================\r\n\r\nexport type ReactCellRenderer = (params: CellRendererParams) => React.ReactNode;\r\nexport type ReactEditRenderer = (params: EditRendererParams) => React.ReactNode;\r\nexport type ReactHeaderRenderer = (params: HeaderRendererParams) => React.ReactNode;\r\n\r\nexport interface GridProps<TData extends Row = Row> {\r\n columns: ColumnDefinition[];\r\n /** Data source for the grid */\r\n dataSource?: DataSource<TData>;\r\n /** Legacy: Raw row data (will be wrapped in a client data source) */\r\n rowData?: TData[];\r\n rowHeight: number;\r\n headerHeight?: number;\r\n overscan?: number;\r\n /** Show filter row below headers */\r\n showFilters?: boolean;\r\n /** Debounce time for filter input (ms) */\r\n filterDebounce?: number;\r\n /** Enable dark mode styling */\r\n darkMode?: boolean;\r\n\r\n // Renderer registries\r\n cellRenderers?: Record<string, ReactCellRenderer>;\r\n editRenderers?: Record<string, ReactEditRenderer>;\r\n headerRenderers?: Record<string, ReactHeaderRenderer>;\r\n\r\n // Global fallback renderers\r\n cellRenderer?: ReactCellRenderer;\r\n editRenderer?: ReactEditRenderer;\r\n headerRenderer?: ReactHeaderRenderer;\r\n}\r\n\r\n// =============================================================================\r\n// State Types\r\n// =============================================================================\r\n\r\ninterface SlotData {\r\n slotId: string;\r\n rowIndex: number;\r\n rowData: Row;\r\n translateY: number;\r\n}\r\n\r\ninterface GridState {\r\n slots: Map<string, SlotData>;\r\n activeCell: CellPosition | null;\r\n selectionRange: CellRange | null;\r\n editingCell: { row: number; col: number; initialValue: CellValue } | null;\r\n contentWidth: number;\r\n contentHeight: number;\r\n headers: Map<number, { column: ColumnDefinition; sortDirection?: SortDirection; sortIndex?: number }>;\r\n isLoading: boolean;\r\n error: string | null;\r\n totalRows: number;\r\n}\r\n\r\ntype GridAction =\r\n | { type: \"BATCH_INSTRUCTIONS\"; instructions: GridInstruction[] }\r\n | { type: \"RESET\" };\r\n\r\n// =============================================================================\r\n// Reducer\r\n// =============================================================================\r\n\r\n/**\r\n * Apply a single instruction to mutable slot maps and return other state changes.\r\n * This allows batching multiple slot operations efficiently.\r\n */\r\nfunction applyInstruction(\r\n instruction: GridInstruction,\r\n slots: Map<string, SlotData>,\r\n headers: Map<number, { column: ColumnDefinition; sortDirection?: SortDirection; sortIndex?: number }>\r\n): Partial<GridState> | null {\r\n switch (instruction.type) {\r\n case \"CREATE_SLOT\":\r\n slots.set(instruction.slotId, {\r\n slotId: instruction.slotId,\r\n rowIndex: -1,\r\n rowData: {},\r\n translateY: 0,\r\n });\r\n return null; // Slots map is mutated\r\n\r\n case \"DESTROY_SLOT\":\r\n slots.delete(instruction.slotId);\r\n return null;\r\n\r\n case \"ASSIGN_SLOT\": {\r\n const existing = slots.get(instruction.slotId);\r\n if (existing) {\r\n slots.set(instruction.slotId, {\r\n ...existing,\r\n rowIndex: instruction.rowIndex,\r\n rowData: instruction.rowData,\r\n });\r\n }\r\n return null;\r\n }\r\n\r\n case \"MOVE_SLOT\": {\r\n const existing = slots.get(instruction.slotId);\r\n if (existing) {\r\n slots.set(instruction.slotId, {\r\n ...existing,\r\n translateY: instruction.translateY,\r\n });\r\n }\r\n return null;\r\n }\r\n\r\n case \"SET_ACTIVE_CELL\":\r\n return { activeCell: instruction.position };\r\n\r\n case \"SET_SELECTION_RANGE\":\r\n return { selectionRange: instruction.range };\r\n\r\n case \"START_EDIT\":\r\n return {\r\n editingCell: {\r\n row: instruction.row,\r\n col: instruction.col,\r\n initialValue: instruction.initialValue,\r\n },\r\n };\r\n\r\n case \"STOP_EDIT\":\r\n return { editingCell: null };\r\n\r\n case \"SET_CONTENT_SIZE\":\r\n return {\r\n contentWidth: instruction.width,\r\n contentHeight: instruction.height,\r\n };\r\n\r\n case \"UPDATE_HEADER\":\r\n headers.set(instruction.colIndex, {\r\n column: instruction.column,\r\n sortDirection: instruction.sortDirection,\r\n sortIndex: instruction.sortIndex,\r\n });\r\n return null;\r\n\r\n case \"DATA_LOADING\":\r\n return { isLoading: true, error: null };\r\n\r\n case \"DATA_LOADED\":\r\n return { isLoading: false, totalRows: instruction.totalRows };\r\n\r\n case \"DATA_ERROR\":\r\n return { isLoading: false, error: instruction.error };\r\n\r\n default:\r\n return null;\r\n }\r\n}\r\n\r\nfunction gridReducer(state: GridState, action: GridAction): GridState {\r\n if (action.type === \"RESET\") {\r\n return createInitialState();\r\n }\r\n\r\n // Process batch of instructions in one state update\r\n const { instructions } = action;\r\n // console.log(\"[GP-Grid Reducer] Processing batch:\", instructions.map(i => i.type));\r\n if (instructions.length === 0) {\r\n return state;\r\n }\r\n\r\n // Create mutable copies of Maps to batch updates\r\n const newSlots = new Map(state.slots);\r\n const newHeaders = new Map(state.headers);\r\n let stateChanges: Partial<GridState> = {};\r\n\r\n // Apply all instructions\r\n for (const instruction of instructions) {\r\n const changes = applyInstruction(instruction, newSlots, newHeaders);\r\n if (changes) {\r\n stateChanges = { ...stateChanges, ...changes };\r\n }\r\n }\r\n\r\n // Return new state with all changes applied\r\n return {\r\n ...state,\r\n ...stateChanges,\r\n slots: newSlots,\r\n headers: newHeaders,\r\n };\r\n}\r\n\r\nfunction createInitialState(): GridState {\r\n return {\r\n slots: new Map(),\r\n activeCell: null,\r\n selectionRange: null,\r\n editingCell: null,\r\n contentWidth: 0,\r\n contentHeight: 0,\r\n headers: new Map(),\r\n isLoading: false,\r\n error: null,\r\n totalRows: 0,\r\n };\r\n}\r\n\r\n// =============================================================================\r\n// Grid Component\r\n// =============================================================================\r\n\r\nexport function Grid<TData extends Row = Row>(props: GridProps<TData>) {\r\n // Inject styles on first render (safe to call multiple times)\r\n injectStyles();\r\n\r\n const {\r\n columns,\r\n dataSource: providedDataSource,\r\n rowData,\r\n rowHeight,\r\n headerHeight = rowHeight,\r\n overscan = 3,\r\n showFilters = false,\r\n filterDebounce = 300,\r\n darkMode = false,\r\n cellRenderers = {},\r\n editRenderers = {},\r\n headerRenderers = {},\r\n cellRenderer,\r\n editRenderer,\r\n headerRenderer,\r\n } = props;\r\n\r\n const containerRef = useRef<HTMLDivElement>(null);\r\n const coreRef = useRef<GridCore<TData> | null>(null);\r\n const [state, dispatch] = useReducer(gridReducer, null, createInitialState);\r\n const [filterValues, setFilterValues] = useState<Record<string, string>>({});\r\n const filterTimeoutRef = useRef<Record<string, ReturnType<typeof setTimeout>>>({});\r\n const [isDraggingFill, setIsDraggingFill] = useState(false);\r\n const [fillTarget, setFillTarget] = useState<{ row: number; col: number } | null>(null);\r\n const [fillSourceRange, setFillSourceRange] = useState<{ startRow: number; startCol: number; endRow: number; endCol: number } | null>(null);\r\n const autoScrollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\r\n\r\n // Computed heights\r\n const filterRowHeight = showFilters ? 40 : 0;\r\n const totalHeaderHeight = headerHeight + filterRowHeight;\r\n\r\n // Create data source from rowData if not provided\r\n const dataSource = useMemo(() => {\r\n if (providedDataSource) {\r\n return providedDataSource;\r\n }\r\n if (rowData) {\r\n return createDataSourceFromArray(rowData);\r\n }\r\n // Empty data source\r\n return createClientDataSource<TData>([]);\r\n }, [providedDataSource, rowData]);\r\n\r\n // Compute column positions\r\n const columnPositions = useMemo(() => {\r\n const positions = [0];\r\n let pos = 0;\r\n for (const col of columns) {\r\n pos += col.width;\r\n positions.push(pos);\r\n }\r\n return positions;\r\n }, [columns]);\r\n\r\n const totalWidth = columnPositions[columnPositions.length - 1] ?? 0;\r\n\r\n // Initialize GridCore\r\n useEffect(() => {\r\n const core = new GridCore<TData>({\r\n columns,\r\n dataSource,\r\n rowHeight,\r\n headerHeight: totalHeaderHeight,\r\n overscan,\r\n });\r\n\r\n coreRef.current = core;\r\n\r\n // Subscribe to batched instructions for efficient state updates\r\n const unsubscribe = core.onBatchInstruction((instructions) => {\r\n dispatch({ type: \"BATCH_INSTRUCTIONS\", instructions });\r\n });\r\n\r\n // Initialize\r\n core.initialize();\r\n\r\n return () => {\r\n unsubscribe();\r\n coreRef.current = null;\r\n };\r\n }, [columns, dataSource, rowHeight, totalHeaderHeight, overscan]);\r\n\r\n // Handle scroll\r\n const handleScroll = useCallback(() => {\r\n const container = containerRef.current;\r\n const core = coreRef.current;\r\n if (!container || !core) return;\r\n\r\n core.setViewport(\r\n container.scrollTop,\r\n container.scrollLeft,\r\n container.clientWidth,\r\n container.clientHeight\r\n );\r\n }, []);\r\n\r\n // Initial measurement\r\n useEffect(() => {\r\n const container = containerRef.current;\r\n const core = coreRef.current;\r\n if (!container || !core) return;\r\n\r\n const resizeObserver = new ResizeObserver(() => {\r\n core.setViewport(\r\n container.scrollTop,\r\n container.scrollLeft,\r\n container.clientWidth,\r\n container.clientHeight\r\n );\r\n });\r\n\r\n resizeObserver.observe(container);\r\n handleScroll();\r\n\r\n return () => resizeObserver.disconnect();\r\n }, [handleScroll]);\r\n\r\n // Handle filter change with debounce\r\n const handleFilterChange = useCallback(\r\n (colId: string, value: string) => {\r\n setFilterValues((prev) => ({ ...prev, [colId]: value }));\r\n\r\n // Clear existing timeout\r\n if (filterTimeoutRef.current[colId]) {\r\n clearTimeout(filterTimeoutRef.current[colId]);\r\n }\r\n\r\n // Debounce the actual filter application\r\n filterTimeoutRef.current[colId] = setTimeout(() => {\r\n const core = coreRef.current;\r\n if (core) {\r\n core.setFilter(colId, value);\r\n }\r\n }, filterDebounce);\r\n },\r\n [filterDebounce]\r\n );\r\n\r\n // Keyboard navigation\r\n const handleKeyDown = useCallback(\r\n (e: React.KeyboardEvent) => {\r\n const core = coreRef.current;\r\n if (!core) return;\r\n\r\n // Don't handle keyboard events when editing\r\n if (state.editingCell && e.key !== \"Enter\" && e.key !== \"Escape\" && e.key !== \"Tab\") {\r\n return;\r\n }\r\n\r\n const { selection } = core;\r\n const isShift = e.shiftKey;\r\n const isCtrl = e.ctrlKey || e.metaKey;\r\n\r\n switch (e.key) {\r\n case \"ArrowUp\":\r\n e.preventDefault();\r\n selection.moveFocus(\"up\", isShift);\r\n break;\r\n case \"ArrowDown\":\r\n e.preventDefault();\r\n selection.moveFocus(\"down\", isShift);\r\n break;\r\n case \"ArrowLeft\":\r\n e.preventDefault();\r\n selection.moveFocus(\"left\", isShift);\r\n break;\r\n case \"ArrowRight\":\r\n e.preventDefault();\r\n selection.moveFocus(\"right\", isShift);\r\n break;\r\n case \"Enter\":\r\n e.preventDefault();\r\n if (state.editingCell) {\r\n core.commitEdit();\r\n } else if (state.activeCell) {\r\n core.startEdit(state.activeCell.row, state.activeCell.col);\r\n }\r\n break;\r\n case \"Escape\":\r\n e.preventDefault();\r\n if (state.editingCell) {\r\n core.cancelEdit();\r\n } else {\r\n selection.clearSelection();\r\n }\r\n break;\r\n case \"Tab\":\r\n e.preventDefault();\r\n if (state.editingCell) {\r\n core.commitEdit();\r\n }\r\n selection.moveFocus(isShift ? \"left\" : \"right\", false);\r\n break;\r\n case \"a\":\r\n if (isCtrl) {\r\n e.preventDefault();\r\n selection.selectAll();\r\n }\r\n break;\r\n case \"c\":\r\n if (isCtrl) {\r\n e.preventDefault();\r\n selection.copySelectionToClipboard();\r\n }\r\n break;\r\n case \"F2\":\r\n e.preventDefault();\r\n if (state.activeCell && !state.editingCell) {\r\n core.startEdit(state.activeCell.row, state.activeCell.col);\r\n }\r\n break;\r\n case \"Delete\":\r\n case \"Backspace\":\r\n // Start editing with empty value on delete/backspace\r\n if (state.activeCell && !state.editingCell) {\r\n e.preventDefault();\r\n core.startEdit(state.activeCell.row, state.activeCell.col);\r\n }\r\n break;\r\n default:\r\n // Start editing on any printable character\r\n if (\r\n state.activeCell &&\r\n !state.editingCell &&\r\n !isCtrl &&\r\n e.key.length === 1\r\n ) {\r\n core.startEdit(state.activeCell.row, state.activeCell.col);\r\n }\r\n break;\r\n }\r\n },\r\n [state.activeCell, state.editingCell]\r\n );\r\n\r\n // Scroll active cell into view when navigating with keyboard\r\n useEffect(() => {\r\n // Skip scrolling when editing - the user just clicked on the cell so it's already visible\r\n if (!state.activeCell || !containerRef.current || state.editingCell) return;\r\n\r\n const { row, col } = state.activeCell;\r\n const container = containerRef.current;\r\n\r\n // Calculate cell position\r\n const cellTop = row * rowHeight + totalHeaderHeight;\r\n const cellBottom = cellTop + rowHeight;\r\n const cellLeft = columnPositions[col] ?? 0;\r\n const cellRight = cellLeft + (columns[col]?.width ?? 0);\r\n\r\n // Get visible area\r\n const visibleTop = container.scrollTop + totalHeaderHeight;\r\n const visibleBottom = container.scrollTop + container.clientHeight;\r\n const visibleLeft = container.scrollLeft;\r\n const visibleRight = container.scrollLeft + container.clientWidth;\r\n\r\n // Scroll vertically if needed\r\n if (cellTop < visibleTop) {\r\n container.scrollTop = cellTop - totalHeaderHeight;\r\n } else if (cellBottom > visibleBottom) {\r\n container.scrollTop = cellBottom - container.clientHeight;\r\n }\r\n\r\n // Scroll horizontally if needed\r\n if (cellLeft < visibleLeft) {\r\n container.scrollLeft = cellLeft;\r\n } else if (cellRight > visibleRight) {\r\n container.scrollLeft = cellRight - container.clientWidth;\r\n }\r\n }, [state.activeCell, state.editingCell, rowHeight, totalHeaderHeight, columnPositions, columns]);\r\n\r\n // Cell click handler\r\n const handleCellClick = useCallback(\r\n (rowIndex: number, colIndex: number, e: React.MouseEvent) => {\r\n // console.log(\"[GP-Grid] Cell click:\", { rowIndex, colIndex, coreExists: !!coreRef.current });\r\n const core = coreRef.current;\r\n if (!core || core.getEditState() !== null) {\r\n // console.warn(\"[GP-Grid] Core not initialized on cell click\");\r\n return;\r\n }\r\n\r\n // Focus the container to enable keyboard navigation\r\n containerRef.current?.focus();\r\n\r\n core.selection.startSelection(\r\n { row: rowIndex, col: colIndex },\r\n { shift: e.shiftKey, ctrl: e.ctrlKey || e.metaKey }\r\n );\r\n },\r\n []\r\n );\r\n\r\n // Cell double-click handler\r\n const handleCellDoubleClick = useCallback(\r\n (rowIndex: number, colIndex: number) => {\r\n const core = coreRef.current;\r\n if (!core) return;\r\n\r\n core.startEdit(rowIndex, colIndex);\r\n },\r\n []\r\n );\r\n\r\n // Header click handler (sort)\r\n const handleHeaderClick = useCallback(\r\n (colIndex: number, e: React.MouseEvent) => {\r\n // console.log(\"[GP-Grid] Header click:\", { colIndex, coreExists: !!coreRef.current });\r\n const core = coreRef.current;\r\n if (!core) {\r\n // console.warn(\"[GP-Grid] Core not initialized on header click\");\r\n return;\r\n }\r\n\r\n const column = columns[colIndex];\r\n if (!column) {\r\n // console.warn(\"[GP-Grid] Column not found for index:\", colIndex);\r\n return;\r\n }\r\n\r\n const colId = column.colId ?? column.field;\r\n const headerInfo = state.headers.get(colIndex);\r\n const currentDirection = headerInfo?.sortDirection;\r\n\r\n // Cycle: none -> asc -> desc -> none\r\n let newDirection: SortDirection | null;\r\n if (!currentDirection) {\r\n newDirection = \"asc\";\r\n } else if (currentDirection === \"asc\") {\r\n newDirection = \"desc\";\r\n } else {\r\n newDirection = null;\r\n }\r\n\r\n // console.log(\"[GP-Grid] Setting sort:\", { colId, newDirection });\r\n core.setSort(colId, newDirection, e.shiftKey);\r\n },\r\n [columns, state.headers]\r\n );\r\n\r\n // Fill handle drag handlers\r\n const handleFillHandleMouseDown = useCallback(\r\n (e: React.MouseEvent) => {\r\n // console.log(\"[GP-Grid] Fill handle mousedown triggered\");\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n const core = coreRef.current;\r\n if (!core) return;\r\n\r\n const { activeCell, selectionRange } = state;\r\n if (!activeCell && !selectionRange) return;\r\n\r\n // Create source range from selection or active cell\r\n const sourceRange = selectionRange ?? {\r\n startRow: activeCell!.row,\r\n startCol: activeCell!.col,\r\n endRow: activeCell!.row,\r\n endCol: activeCell!.col,\r\n };\r\n\r\n // console.log(\"[GP-Grid] Starting fill drag with source range:\", sourceRange);\r\n core.fill.startFillDrag(sourceRange);\r\n setFillSourceRange(sourceRange);\r\n setFillTarget({ \r\n row: Math.max(sourceRange.startRow, sourceRange.endRow),\r\n col: Math.max(sourceRange.startCol, sourceRange.endCol)\r\n });\r\n setIsDraggingFill(true);\r\n },\r\n [state.activeCell, state.selectionRange]\r\n );\r\n\r\n // Handle mouse move during fill drag\r\n useEffect(() => {\r\n if (!isDraggingFill) return;\r\n\r\n // Auto-scroll configuration\r\n const SCROLL_THRESHOLD = 40; // pixels from edge to trigger scroll\r\n const SCROLL_SPEED = 10; // pixels per frame\r\n\r\n const handleMouseMove = (e: MouseEvent) => {\r\n const core = coreRef.current;\r\n const container = containerRef.current;\r\n if (!core || !container) return;\r\n\r\n // Get container bounds\r\n const rect = container.getBoundingClientRect();\r\n const scrollLeft = container.scrollLeft;\r\n const scrollTop = container.scrollTop;\r\n\r\n // Calculate mouse position relative to grid content\r\n const mouseX = e.clientX - rect.left + scrollLeft;\r\n const mouseY = e.clientY - rect.top + scrollTop - totalHeaderHeight;\r\n\r\n // Find the row and column under the mouse\r\n const targetRow = Math.max(0, Math.floor(mouseY / rowHeight));\r\n \r\n // Find column by checking column positions\r\n let targetCol = 0;\r\n for (let i = 0; i < columnPositions.length - 1; i++) {\r\n if (mouseX >= columnPositions[i]! && mouseX < columnPositions[i + 1]!) {\r\n targetCol = i;\r\n break;\r\n }\r\n if (mouseX >= columnPositions[columnPositions.length - 1]!) {\r\n targetCol = columnPositions.length - 2;\r\n }\r\n }\r\n\r\n core.fill.updateFillDrag(targetRow, targetCol);\r\n setFillTarget({ row: targetRow, col: targetCol });\r\n\r\n // Auto-scroll logic\r\n const mouseYInContainer = e.clientY - rect.top;\r\n const mouseXInContainer = e.clientX - rect.left;\r\n\r\n // Clear any existing auto-scroll\r\n if (autoScrollIntervalRef.current) {\r\n clearInterval(autoScrollIntervalRef.current);\r\n autoScrollIntervalRef.current = null;\r\n }\r\n\r\n // Check if we need to auto-scroll\r\n let scrollDeltaX = 0;\r\n let scrollDeltaY = 0;\r\n\r\n // Vertical scrolling\r\n if (mouseYInContainer < SCROLL_THRESHOLD + totalHeaderHeight) {\r\n scrollDeltaY = -SCROLL_SPEED;\r\n } else if (mouseYInContainer > rect.height - SCROLL_THRESHOLD) {\r\n scrollDeltaY = SCROLL_SPEED;\r\n }\r\n\r\n // Horizontal scrolling\r\n if (mouseXInContainer < SCROLL_THRESHOLD) {\r\n scrollDeltaX = -SCROLL_SPEED;\r\n } else if (mouseXInContainer > rect.width - SCROLL_THRESHOLD) {\r\n scrollDeltaX = SCROLL_SPEED;\r\n }\r\n\r\n // Start auto-scroll if needed\r\n if (scrollDeltaX !== 0 || scrollDeltaY !== 0) {\r\n autoScrollIntervalRef.current = setInterval(() => {\r\n if (containerRef.current) {\r\n containerRef.current.scrollTop += scrollDeltaY;\r\n containerRef.current.scrollLeft += scrollDeltaX;\r\n }\r\n }, 16); // ~60fps\r\n }\r\n };\r\n\r\n const handleMouseUp = () => {\r\n // Clear auto-scroll\r\n if (autoScrollIntervalRef.current) {\r\n clearInterval(autoScrollIntervalRef.current);\r\n autoScrollIntervalRef.current = null;\r\n }\r\n\r\n const core = coreRef.current;\r\n if (core) {\r\n core.fill.commitFillDrag();\r\n // Refresh slots to show updated values\r\n core.refreshSlotData();\r\n }\r\n setIsDraggingFill(false);\r\n setFillTarget(null);\r\n setFillSourceRange(null);\r\n };\r\n\r\n document.addEventListener(\"mousemove\", handleMouseMove);\r\n document.addEventListener(\"mouseup\", handleMouseUp);\r\n\r\n return () => {\r\n // Clear auto-scroll on cleanup\r\n if (autoScrollIntervalRef.current) {\r\n clearInterval(autoScrollIntervalRef.current);\r\n autoScrollIntervalRef.current = null;\r\n }\r\n document.removeEventListener(\"mousemove\", handleMouseMove);\r\n document.removeEventListener(\"mouseup\", handleMouseUp);\r\n };\r\n }, [isDraggingFill, totalHeaderHeight, rowHeight, columnPositions]);\r\n\r\n // Render helpers\r\n const isSelected = useCallback(\r\n (row: number, col: number): boolean => {\r\n const { selectionRange } = state;\r\n if (!selectionRange) return false;\r\n\r\n const minRow = Math.min(selectionRange.startRow, selectionRange.endRow);\r\n const maxRow = Math.max(selectionRange.startRow, selectionRange.endRow);\r\n const minCol = Math.min(selectionRange.startCol, selectionRange.endCol);\r\n const maxCol = Math.max(selectionRange.startCol, selectionRange.endCol);\r\n\r\n return row >= minRow && row <= maxRow && col >= minCol && col <= maxCol;\r\n },\r\n [state.selectionRange]\r\n );\r\n\r\n const isActiveCell = useCallback(\r\n (row: number, col: number): boolean => {\r\n return state.activeCell?.row === row && state.activeCell?.col === col;\r\n },\r\n [state.activeCell]\r\n );\r\n\r\n const isEditingCell = useCallback(\r\n (row: number, col: number): boolean => {\r\n return state.editingCell?.row === row && state.editingCell?.col === col;\r\n },\r\n [state.editingCell]\r\n );\r\n\r\n // Check if cell is in fill preview range\r\n const isInFillPreview = useCallback(\r\n (row: number, col: number): boolean => {\r\n if (!isDraggingFill || !fillSourceRange || !fillTarget) return false;\r\n\r\n const srcMinRow = Math.min(fillSourceRange.startRow, fillSourceRange.endRow);\r\n const srcMaxRow = Math.max(fillSourceRange.startRow, fillSourceRange.endRow);\r\n const srcMinCol = Math.min(fillSourceRange.startCol, fillSourceRange.endCol);\r\n const srcMaxCol = Math.max(fillSourceRange.startCol, fillSourceRange.endCol);\r\n\r\n // Determine fill direction and range\r\n const fillDown = fillTarget.row > srcMaxRow;\r\n const fillUp = fillTarget.row < srcMinRow;\r\n const fillRight = fillTarget.col > srcMaxCol;\r\n const fillLeft = fillTarget.col < srcMinCol;\r\n\r\n // Check if cell is in the fill preview area (not the source area)\r\n if (fillDown) {\r\n return row > srcMaxRow && row <= fillTarget.row && col >= srcMinCol && col <= srcMaxCol;\r\n }\r\n if (fillUp) {\r\n return row < srcMinRow && row >= fillTarget.row && col >= srcMinCol && col <= srcMaxCol;\r\n }\r\n if (fillRight) {\r\n return col > srcMaxCol && col <= fillTarget.col && row >= srcMinRow && row <= srcMaxRow;\r\n }\r\n if (fillLeft) {\r\n return col < srcMinCol && col >= fillTarget.col && row >= srcMinRow && row <= srcMaxRow;\r\n }\r\n\r\n return false;\r\n },\r\n [isDraggingFill, fillSourceRange, fillTarget]\r\n );\r\n\r\n // Get cell value from row data\r\n const getCellValue = useCallback((rowData: Row, field: string): CellValue => {\r\n const parts = field.split(\".\");\r\n let value: unknown = rowData;\r\n\r\n for (const part of parts) {\r\n if (value == null || typeof value !== \"object\") {\r\n return null;\r\n }\r\n value = (value as Record<string, unknown>)[part];\r\n }\r\n\r\n return (value ?? null) as CellValue;\r\n }, []);\r\n\r\n // Render cell content\r\n const renderCell = useCallback(\r\n (\r\n column: ColumnDefinition,\r\n rowData: Row,\r\n rowIndex: number,\r\n colIndex: number\r\n ): React.ReactNode => {\r\n const value = getCellValue(rowData, column.field);\r\n const params: CellRendererParams = {\r\n value,\r\n rowData,\r\n column,\r\n rowIndex,\r\n colIndex,\r\n isActive: isActiveCell(rowIndex, colIndex),\r\n isSelected: isSelected(rowIndex, colIndex),\r\n isEditing: isEditingCell(rowIndex, colIndex),\r\n };\r\n\r\n // Check for column-specific renderer\r\n if (column.cellRenderer && typeof column.cellRenderer === \"string\") {\r\n const renderer = cellRenderers[column.cellRenderer];\r\n if (renderer) {\r\n return renderer(params);\r\n }\r\n }\r\n\r\n // Fall back to global renderer\r\n if (cellRenderer) {\r\n return cellRenderer(params);\r\n }\r\n\r\n // Default text rendering\r\n return value == null ? \"\" : String(value);\r\n },\r\n [getCellValue, isActiveCell, isSelected, isEditingCell, cellRenderers, cellRenderer]\r\n );\r\n\r\n // Render edit cell\r\n const renderEditCell = useCallback(\r\n (\r\n column: ColumnDefinition,\r\n rowData: Row,\r\n rowIndex: number,\r\n colIndex: number,\r\n initialValue: CellValue\r\n ): React.ReactNode => {\r\n const core = coreRef.current;\r\n if (!core) return null;\r\n\r\n const value = getCellValue(rowData, column.field);\r\n const params: EditRendererParams = {\r\n value,\r\n rowData,\r\n column,\r\n rowIndex,\r\n colIndex,\r\n isActive: true,\r\n isSelected: true,\r\n isEditing: true,\r\n initialValue,\r\n onValueChange: (newValue) => core.updateEditValue(newValue),\r\n onCommit: () => core.commitEdit(),\r\n onCancel: () => core.cancelEdit(),\r\n };\r\n\r\n // Check for column-specific renderer\r\n if (column.editRenderer && typeof column.editRenderer === \"string\") {\r\n const renderer = editRenderers[column.editRenderer];\r\n if (renderer) {\r\n return renderer(params);\r\n }\r\n }\r\n\r\n // Fall back to global renderer\r\n if (editRenderer) {\r\n return editRenderer(params);\r\n }\r\n\r\n // Default input\r\n return (\r\n <input\r\n className=\"gp-grid-edit-input\"\r\n type=\"text\"\r\n defaultValue={initialValue == null ? \"\" : String(initialValue)}\r\n autoFocus\r\n onFocus={(e) => e.target.select()}\r\n onChange={(e) => core.updateEditValue(e.target.value)}\r\n onKeyDown={(e) => {\r\n e.stopPropagation();\r\n if (e.key === \"Enter\") {\r\n core.commitEdit();\r\n } else if (e.key === \"Escape\") {\r\n core.cancelEdit();\r\n } else if (e.key === \"Tab\") {\r\n e.preventDefault();\r\n core.commitEdit();\r\n core.selection.moveFocus(e.shiftKey ? \"left\" : \"right\", false);\r\n }\r\n }}\r\n onBlur={() => core.commitEdit()}\r\n />\r\n );\r\n },\r\n [getCellValue, editRenderers, editRenderer]\r\n );\r\n\r\n // Render header\r\n const renderHeader = useCallback(\r\n (\r\n column: ColumnDefinition,\r\n colIndex: number,\r\n sortDirection?: SortDirection,\r\n sortIndex?: number\r\n ): React.ReactNode => {\r\n const core = coreRef.current;\r\n const params: HeaderRendererParams = {\r\n column,\r\n colIndex,\r\n sortDirection,\r\n sortIndex,\r\n onSort: (direction, addToExisting) => {\r\n if (core) {\r\n core.setSort(column.colId ?? column.field, direction, addToExisting);\r\n }\r\n },\r\n };\r\n\r\n // Check for column-specific renderer\r\n if (column.headerRenderer && typeof column.headerRenderer === \"string\") {\r\n const renderer = headerRenderers[column.headerRenderer];\r\n if (renderer) {\r\n return renderer(params);\r\n }\r\n }\r\n\r\n // Fall back to global renderer\r\n if (headerRenderer) {\r\n return headerRenderer(params);\r\n }\r\n\r\n // Default header\r\n return (\r\n <>\r\n <span className=\"gp-grid-header-text\">\r\n {column.headerName ?? column.field}\r\n </span>\r\n {sortDirection && (\r\n <span className=\"gp-grid-sort-indicator\">\r\n {sortDirection === \"asc\" ? \"▲\" : \"▼\"}\r\n {sortIndex !== undefined && sortIndex > 0 && (\r\n <span className=\"gp-grid-sort-index\">{sortIndex}</span>\r\n )}\r\n </span>\r\n )}\r\n </>\r\n );\r\n },\r\n [headerRenderers, headerRenderer]\r\n );\r\n\r\n // Convert slots map to array for rendering\r\n const slotsArray = useMemo(() => Array.from(state.slots.values()), [state.slots]);\r\n\r\n // Calculate fill handle position (only show for editable columns)\r\n const fillHandlePosition = useMemo(() => {\r\n const { activeCell, selectionRange } = state;\r\n if (!activeCell && !selectionRange) return null;\r\n\r\n // Get the bottom-right corner and column range of selection or active cell\r\n let row: number, col: number;\r\n let minCol: number, maxCol: number;\r\n \r\n if (selectionRange) {\r\n row = Math.max(selectionRange.startRow, selectionRange.endRow);\r\n col = Math.max(selectionRange.startCol, selectionRange.endCol);\r\n minCol = Math.min(selectionRange.startCol, selectionRange.endCol);\r\n maxCol = Math.max(selectionRange.startCol, selectionRange.endCol);\r\n } else if (activeCell) {\r\n row = activeCell.row;\r\n col = activeCell.col;\r\n minCol = col;\r\n maxCol = col;\r\n } else {\r\n return null;\r\n }\r\n\r\n // Check if ALL columns in the selection are editable\r\n for (let c = minCol; c <= maxCol; c++) {\r\n const column = columns[c];\r\n if (!column || column.editable !== true) {\r\n return null; // Don't show fill handle if any column is not editable\r\n }\r\n }\r\n\r\n const cellTop = row * rowHeight + totalHeaderHeight;\r\n const cellLeft = columnPositions[col] ?? 0;\r\n const cellWidth = columns[col]?.width ?? 0;\r\n\r\n return {\r\n top: cellTop + rowHeight - 5,\r\n left: cellLeft + cellWidth - 20, // Move significantly left to avoid scrollbar overlap\r\n };\r\n }, [state.activeCell, state.selectionRange, rowHeight, totalHeaderHeight, columnPositions, columns]);\r\n\r\n return (\r\n <div\r\n ref={containerRef}\r\n className={`gp-grid-container${darkMode ? \" gp-grid-container--dark\" : \"\"}`}\r\n style={{\r\n width: \"100%\",\r\n height: \"100%\",\r\n overflow: \"auto\",\r\n position: \"relative\",\r\n }}\r\n onScroll={handleScroll}\r\n onKeyDown={handleKeyDown}\r\n tabIndex={0}\r\n >\r\n {/* Content sizer */}\r\n <div\r\n style={{\r\n width: Math.max(state.contentWidth, totalWidth),\r\n height: Math.max(state.contentHeight, totalHeaderHeight),\r\n position: \"relative\",\r\n minWidth: \"100%\",\r\n }}\r\n >\r\n {/* Headers */}\r\n <div\r\n className=\"gp-grid-header\"\r\n style={{\r\n position: \"sticky\",\r\n top: 0,\r\n left: 0,\r\n height: headerHeight,\r\n width: Math.max(state.contentWidth, totalWidth),\r\n minWidth: \"100%\",\r\n zIndex: 100,\r\n }}\r\n >\r\n {columns.map((column, colIndex) => {\r\n const headerInfo = state.headers.get(colIndex);\r\n return (\r\n <div\r\n key={column.colId ?? column.field}\r\n className=\"gp-grid-header-cell\"\r\n style={{\r\n position: \"absolute\",\r\n left: `${columnPositions[colIndex]}px`,\r\n top: 0,\r\n width: `${column.width}px`,\r\n height: `${headerHeight}px`,\r\n background: \"transparent\",\r\n }}\r\n onClick={(e) => handleHeaderClick(colIndex, e)}\r\n >\r\n {renderHeader(\r\n column,\r\n colIndex,\r\n headerInfo?.sortDirection,\r\n headerInfo?.sortIndex\r\n )}\r\n </div>\r\n );\r\n })}\r\n </div>\r\n\r\n {/* Filter Row */}\r\n {showFilters && (\r\n <div\r\n className=\"gp-grid-filter-row\"\r\n style={{\r\n position: \"sticky\",\r\n top: headerHeight,\r\n left: 0,\r\n height: filterRowHeight,\r\n width: Math.max(state.contentWidth, totalWidth),\r\n minWidth: \"100%\",\r\n zIndex: 99,\r\n }}\r\n >\r\n {columns.map((column, colIndex) => {\r\n const colId = column.colId ?? column.field;\r\n return (\r\n <div\r\n key={`filter-${colId}`}\r\n className=\"gp-grid-filter-cell\"\r\n style={{\r\n position: \"absolute\",\r\n left: `${columnPositions[colIndex]}px`,\r\n top: 0,\r\n width: `${column.width}px`,\r\n height: `${filterRowHeight}px`,\r\n }}\r\n >\r\n <input\r\n className=\"gp-grid-filter-input\"\r\n type=\"text\"\r\n placeholder={`Filter ${column.headerName ?? column.field}...`}\r\n value={filterValues[colId] ?? \"\"}\r\n onChange={(e) => handleFilterChange(colId, e.target.value)}\r\n onKeyDown={(e) => e.stopPropagation()}\r\n />\r\n </div>\r\n );\r\n })}\r\n </div>\r\n )}\r\n\r\n {/* Row slots */}\r\n {slotsArray.map((slot) => {\r\n if (slot.rowIndex < 0) return null;\r\n\r\n const isEvenRow = slot.rowIndex % 2 === 0;\r\n\r\n return (\r\n <div\r\n key={slot.slotId}\r\n className={`gp-grid-row ${isEvenRow ? \"gp-grid-row--even\" : \"\"}`}\r\n style={{\r\n position: \"absolute\",\r\n top: 0,\r\n left: 0,\r\n transform: `translateY(${slot.translateY}px)`,\r\n width: `${Math.max(state.contentWidth, totalWidth)}px`,\r\n height: `${rowHeight}px`,\r\n }}\r\n >\r\n {columns.map((column, colIndex) => {\r\n const isEditing = isEditingCell(slot.rowIndex, colIndex);\r\n const active = isActiveCell(slot.rowIndex, colIndex);\r\n const selected = isSelected(slot.rowIndex, colIndex);\r\n const inFillPreview = isInFillPreview(slot.rowIndex, colIndex);\r\n\r\n const cellClasses = [\r\n \"gp-grid-cell\",\r\n active && \"gp-grid-cell--active\",\r\n selected && !active && \"gp-grid-cell--selected\",\r\n isEditing && \"gp-grid-cell--editing\",\r\n inFillPreview && \"gp-grid-cell--fill-preview\",\r\n ]\r\n .filter(Boolean)\r\n .join(\" \");\r\n\r\n return (\r\n <div\r\n key={`${slot.slotId}-${colIndex}`}\r\n className={cellClasses}\r\n style={{\r\n position: \"absolute\",\r\n left: `${columnPositions[colIndex]}px`,\r\n top: 0,\r\n width: `${column.width}px`,\r\n height: `${rowHeight}px`,\r\n }}\r\n onClick={(e) => handleCellClick(slot.rowIndex, colIndex, e)}\r\n onDoubleClick={() => handleCellDoubleClick(slot.rowIndex, colIndex)}\r\n >\r\n {isEditing && state.editingCell\r\n ? renderEditCell(\r\n column,\r\n slot.rowData,\r\n slot.rowIndex,\r\n colIndex,\r\n state.editingCell.initialValue\r\n )\r\n : renderCell(column, slot.rowData, slot.rowIndex, colIndex)}\r\n </div>\r\n );\r\n })}\r\n </div>\r\n );\r\n })}\r\n\r\n {/* Fill handle (drag to fill) */}\r\n {fillHandlePosition && !state.editingCell && (\r\n <div\r\n className=\"gp-grid-fill-handle\"\r\n style={{\r\n position: \"absolute\",\r\n top: fillHandlePosition.top,\r\n left: fillHandlePosition.left,\r\n zIndex: 200,\r\n }}\r\n onMouseDown={handleFillHandleMouseDown}\r\n />\r\n )}\r\n\r\n {/* Loading indicator */}\r\n {state.isLoading && (\r\n <div className=\"gp-grid-loading\">\r\n <div className=\"gp-grid-loading-spinner\" />\r\n Loading...\r\n </div>\r\n )}\r\n\r\n {/* Error message */}\r\n {state.error && (\r\n <div className=\"gp-grid-error\">Error: {state.error}</div>\r\n )}\r\n\r\n {/* Empty state */}\r\n {!state.isLoading && !state.error && state.totalRows === 0 && (\r\n <div className=\"gp-grid-empty\">No data to display</div>\r\n )}\r\n </div>\r\n </div>\r\n );\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["stateChanges: Partial<GridState>","createDataSourceFromArray","createClientDataSource","newDirection: SortDirection | null","value: unknown","rowData","params: CellRendererParams","params: EditRendererParams","params: HeaderRendererParams","row: number","col: number","minCol: number","maxCol: number"],"sources":["../src/styles.ts","../src/Grid.tsx"],"sourcesContent":["// packages/react/src/styles.ts\r\n// Dynamic CSS injection for gp-grid-react\r\n\r\nconst STYLE_ID = \"gp-grid-styles\";\r\n\r\nexport const gridStyles = `\r\n/* =============================================================================\r\n GP Grid - CSS Variables for Theming\r\n ============================================================================= */\r\n\r\n.gp-grid-container {\r\n /* Colors - Light Mode (default) */\r\n --gp-grid-bg: #ffffff;\r\n --gp-grid-bg-alt: #f8f9fa;\r\n --gp-grid-text: #212529;\r\n --gp-grid-text-secondary: #6c757d;\r\n --gp-grid-text-muted: #adb5bd;\r\n --gp-grid-border: #dee2e6;\r\n --gp-grid-border-light: #e9ecef;\r\n \r\n /* Header */\r\n --gp-grid-header-bg: #f1f3f5;\r\n --gp-grid-header-text: #212529;\r\n \r\n /* Selection */\r\n --gp-grid-primary: #228be6;\r\n --gp-grid-primary-light: #e7f5ff;\r\n --gp-grid-primary-border: #74c0fc;\r\n --gp-grid-hover: #f1f3f5;\r\n \r\n /* Filter */\r\n --gp-grid-filter-bg: #f8f9fa;\r\n --gp-grid-input-bg: #ffffff;\r\n --gp-grid-input-border: #ced4da;\r\n \r\n /* Error */\r\n --gp-grid-error-bg: #fff5f5;\r\n --gp-grid-error-text: #c92a2a;\r\n \r\n /* Loading */\r\n --gp-grid-loading-bg: rgba(255, 255, 255, 0.95);\r\n --gp-grid-loading-text: #495057;\r\n \r\n /* Scrollbar */\r\n --gp-grid-scrollbar-track: #f1f3f5;\r\n --gp-grid-scrollbar-thumb: #ced4da;\r\n --gp-grid-scrollbar-thumb-hover: #adb5bd;\r\n}\r\n\r\n/* Dark Mode */\r\n.gp-grid-container--dark {\r\n --gp-grid-bg: #1a1b1e;\r\n --gp-grid-bg-alt: #25262b;\r\n --gp-grid-text: #c1c2c5;\r\n --gp-grid-text-secondary: #909296;\r\n --gp-grid-text-muted: #5c5f66;\r\n --gp-grid-border: #373a40;\r\n --gp-grid-border-light: #2c2e33;\r\n \r\n /* Header */\r\n --gp-grid-header-bg: #25262b;\r\n --gp-grid-header-text: #c1c2c5;\r\n \r\n /* Selection */\r\n --gp-grid-primary: #339af0;\r\n --gp-grid-primary-light: #1c3d5a;\r\n --gp-grid-primary-border: #1c7ed6;\r\n --gp-grid-hover: #2c2e33;\r\n \r\n /* Filter */\r\n --gp-grid-filter-bg: #25262b;\r\n --gp-grid-input-bg: #1a1b1e;\r\n --gp-grid-input-border: #373a40;\r\n \r\n /* Error */\r\n --gp-grid-error-bg: #2c1a1a;\r\n --gp-grid-error-text: #ff6b6b;\r\n \r\n /* Loading */\r\n --gp-grid-loading-bg: rgba(26, 27, 30, 0.95);\r\n --gp-grid-loading-text: #c1c2c5;\r\n \r\n /* Scrollbar */\r\n --gp-grid-scrollbar-track: #25262b;\r\n --gp-grid-scrollbar-thumb: #373a40;\r\n --gp-grid-scrollbar-thumb-hover: #4a4d52;\r\n}\r\n\r\n/* =============================================================================\r\n GP Grid - Clean Flat Design\r\n ============================================================================= */\r\n\r\n/* Grid Container */\r\n.gp-grid-container {\r\n outline: none;\r\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\r\n font-size: 13px;\r\n line-height: 1.5;\r\n color: var(--gp-grid-text);\r\n background-color: var(--gp-grid-bg);\r\n border: 1px solid var(--gp-grid-border);\r\n border-radius: 6px;\r\n user-select: none;\r\n -webkit-user-select: none;\r\n}\r\n\r\n.gp-grid-container:focus {\r\n outline: none;\r\n border-color: var(--gp-grid-primary);\r\n}\r\n\r\n/* =============================================================================\r\n Header\r\n ============================================================================= */\r\n\r\n.gp-grid-header {\r\n position: sticky;\r\n top: 0;\r\n left: 0;\r\n z-index: 100;\r\n background-color: var(--gp-grid-header-bg);\r\n border-bottom: 1px solid var(--gp-grid-border);\r\n}\r\n\r\n.gp-grid-container .gp-grid-header-cell {\r\n position: absolute;\r\n box-sizing: border-box;\r\n border-right: 1px solid var(--gp-grid-border);\r\n font-weight: 600;\r\n font-size: 12px;\r\n text-transform: uppercase;\r\n letter-spacing: 0.5px;\r\n color: var(--gp-grid-header-text);\r\n cursor: pointer;\r\n user-select: none;\r\n display: flex;\r\n align-items: center;\r\n padding: 0 12px;\r\n background-color: transparent;\r\n transition: background-color 0.1s ease;\r\n}\r\n\r\n.gp-grid-container .gp-grid-header-cell:hover {\r\n background-color: var(--gp-grid-hover);\r\n}\r\n\r\n.gp-grid-container .gp-grid-header-cell:active {\r\n background-color: var(--gp-grid-border-light);\r\n}\r\n\r\n.gp-grid-container .gp-grid-header-text {\r\n flex: 1;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n color: var(--gp-grid-header-text);\r\n}\r\n\r\n.gp-grid-sort-indicator {\r\n margin-left: 6px;\r\n font-size: 10px;\r\n color: var(--gp-grid-primary);\r\n display: flex;\r\n align-items: center;\r\n}\r\n\r\n.gp-grid-sort-index {\r\n font-size: 9px;\r\n margin-left: 2px;\r\n color: var(--gp-grid-text-secondary);\r\n}\r\n\r\n/* =============================================================================\r\n Filter Row\r\n ============================================================================= */\r\n\r\n.gp-grid-filter-row {\r\n position: sticky;\r\n left: 0;\r\n z-index: 99;\r\n background-color: var(--gp-grid-filter-bg);\r\n border-bottom: 1px solid var(--gp-grid-border);\r\n}\r\n\r\n.gp-grid-filter-cell {\r\n position: absolute;\r\n box-sizing: border-box;\r\n border-right: 1px solid var(--gp-grid-border);\r\n padding: 6px 8px;\r\n display: flex;\r\n align-items: center;\r\n background-color: var(--gp-grid-filter-bg);\r\n}\r\n\r\n.gp-grid-filter-input {\r\n width: 100%;\r\n height: 28px;\r\n padding: 0 10px;\r\n font-family: inherit;\r\n font-size: 12px;\r\n border: 1px solid var(--gp-grid-input-border);\r\n border-radius: 4px;\r\n background-color: var(--gp-grid-input-bg);\r\n color: var(--gp-grid-text);\r\n transition: border-color 0.15s ease;\r\n}\r\n\r\n.gp-grid-filter-input:focus {\r\n outline: none;\r\n border-color: var(--gp-grid-primary);\r\n}\r\n\r\n.gp-grid-filter-input::placeholder {\r\n color: var(--gp-grid-text-muted);\r\n}\r\n\r\n/* =============================================================================\r\n Data Cells\r\n ============================================================================= */\r\n\r\n.gp-grid-row {\r\n position: absolute;\r\n top: 0;\r\n left: 0;\r\n}\r\n\r\n.gp-grid-cell {\r\n position: absolute;\r\n top: 0;\r\n box-sizing: border-box;\r\n padding: 0 12px;\r\n display: flex;\r\n align-items: center;\r\n cursor: cell;\r\n color: var(--gp-grid-text);\r\n border-right: 1px solid var(--gp-grid-border-light);\r\n border-bottom: 1px solid var(--gp-grid-border-light);\r\n background-color: var(--gp-grid-bg);\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n user-select: none;\r\n -webkit-user-select: none;\r\n}\r\n\r\n/* Alternating row colors */\r\n.gp-grid-row--even .gp-grid-cell {\r\n background-color: var(--gp-grid-bg-alt);\r\n}\r\n\r\n.gp-grid-cell:hover {\r\n background-color: var(--gp-grid-hover) !important;\r\n}\r\n\r\n/* Active cell (focused) */\r\n.gp-grid-cell--active {\r\n background-color: var(--gp-grid-primary-light) !important;\r\n border: 2px solid var(--gp-grid-primary) !important;\r\n outline: none;\r\n z-index: 5;\r\n padding: 0 11px;\r\n}\r\n\r\n/* Selected cells (range selection) */\r\n.gp-grid-cell--selected {\r\n background-color: var(--gp-grid-primary-light) !important;\r\n}\r\n\r\n/* Editing cell */\r\n.gp-grid-cell--editing {\r\n background-color: var(--gp-grid-bg) !important;\r\n border: 2px solid var(--gp-grid-primary) !important;\r\n padding: 0 !important;\r\n z-index: 10;\r\n}\r\n\r\n/* =============================================================================\r\n Fill Handle (drag to fill)\r\n ============================================================================= */\r\n\r\n.gp-grid-fill-handle {\r\n position: absolute;\r\n width: 8px;\r\n height: 8px;\r\n background-color: var(--gp-grid-primary);\r\n border: 2px solid var(--gp-grid-bg);\r\n cursor: crosshair;\r\n z-index: 100;\r\n pointer-events: auto;\r\n box-sizing: border-box;\r\n border-radius: 1px;\r\n}\r\n\r\n.gp-grid-fill-handle:hover {\r\n transform: scale(1.2);\r\n}\r\n\r\n/* Fill preview (cells being filled) */\r\n.gp-grid-cell--fill-preview {\r\n background-color: var(--gp-grid-primary-light) !important;\r\n border: 1px dashed var(--gp-grid-primary) !important;\r\n}\r\n\r\n/* =============================================================================\r\n Edit Input\r\n ============================================================================= */\r\n\r\n.gp-grid-edit-input {\r\n width: 100%;\r\n height: 100%;\r\n padding: 0 11px;\r\n font-family: inherit;\r\n font-size: inherit;\r\n color: var(--gp-grid-text);\r\n border: none;\r\n background-color: transparent;\r\n}\r\n\r\n.gp-grid-edit-input:focus {\r\n outline: none;\r\n}\r\n\r\n/* =============================================================================\r\n Loading & Error States\r\n ============================================================================= */\r\n\r\n.gp-grid-loading {\r\n position: absolute;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%);\r\n padding: 12px 20px;\r\n background-color: var(--gp-grid-loading-bg);\r\n color: var(--gp-grid-loading-text);\r\n border-radius: 6px;\r\n border: 1px solid var(--gp-grid-border);\r\n font-weight: 500;\r\n font-size: 13px;\r\n z-index: 1000;\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n}\r\n\r\n.gp-grid-loading-spinner {\r\n width: 16px;\r\n height: 16px;\r\n border: 2px solid var(--gp-grid-border);\r\n border-top-color: var(--gp-grid-primary);\r\n border-radius: 50%;\r\n animation: gp-grid-spin 0.7s linear infinite;\r\n}\r\n\r\n@keyframes gp-grid-spin {\r\n to {\r\n transform: rotate(360deg);\r\n }\r\n}\r\n\r\n.gp-grid-error {\r\n position: absolute;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%);\r\n padding: 12px 20px;\r\n background-color: var(--gp-grid-error-bg);\r\n color: var(--gp-grid-error-text);\r\n border-radius: 6px;\r\n border: 1px solid var(--gp-grid-error-text);\r\n font-weight: 500;\r\n font-size: 13px;\r\n z-index: 1000;\r\n max-width: 80%;\r\n text-align: center;\r\n}\r\n\r\n/* =============================================================================\r\n Empty State\r\n ============================================================================= */\r\n\r\n.gp-grid-empty {\r\n position: absolute;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%);\r\n color: var(--gp-grid-text-muted);\r\n font-size: 14px;\r\n text-align: center;\r\n}\r\n\r\n/* =============================================================================\r\n Scrollbar Styling\r\n ============================================================================= */\r\n\r\n.gp-grid-container::-webkit-scrollbar {\r\n width: 8px;\r\n height: 8px;\r\n}\r\n\r\n.gp-grid-container::-webkit-scrollbar-track {\r\n background-color: var(--gp-grid-scrollbar-track);\r\n}\r\n\r\n.gp-grid-container::-webkit-scrollbar-thumb {\r\n background-color: var(--gp-grid-scrollbar-thumb);\r\n border-radius: 4px;\r\n}\r\n\r\n.gp-grid-container::-webkit-scrollbar-thumb:hover {\r\n background-color: var(--gp-grid-scrollbar-thumb-hover);\r\n}\r\n\r\n.gp-grid-container::-webkit-scrollbar-corner {\r\n background-color: var(--gp-grid-scrollbar-track);\r\n}\r\n`;\r\n\r\nlet stylesInjected = false;\r\n\r\n/**\r\n * Inject grid styles into the document head.\r\n * This is called automatically when the Grid component mounts.\r\n * Styles are only injected once, even if multiple Grid instances exist.\r\n */\r\nexport function injectStyles(): void {\r\n if (stylesInjected) return;\r\n if (typeof document === \"undefined\") return; // SSR safety\r\n\r\n // Check if styles already exist (e.g., from a previous mount)\r\n if (document.getElementById(STYLE_ID)) {\r\n stylesInjected = true;\r\n return;\r\n }\r\n\r\n const styleElement = document.createElement(\"style\");\r\n styleElement.id = STYLE_ID;\r\n styleElement.textContent = gridStyles;\r\n document.head.appendChild(styleElement);\r\n stylesInjected = true;\r\n}\r\n","// packages/react/src/Grid.tsx\r\n\r\nimport React, {\r\n useEffect,\r\n useRef,\r\n useReducer,\r\n useCallback,\r\n useMemo,\r\n useState,\r\n} from \"react\";\r\nimport {\r\n GridCore,\r\n createClientDataSource,\r\n createDataSourceFromArray,\r\n} from \"gp-grid-core\";\r\nimport type {\r\n GridInstruction,\r\n ColumnDefinition,\r\n DataSource,\r\n Row,\r\n CellRendererParams,\r\n EditRendererParams,\r\n HeaderRendererParams,\r\n CellPosition,\r\n CellRange,\r\n CellValue,\r\n SortDirection,\r\n} from \"gp-grid-core\";\r\nimport { injectStyles } from \"./styles\";\r\n\r\n// =============================================================================\r\n// Types\r\n// =============================================================================\r\n\r\nexport type ReactCellRenderer = (params: CellRendererParams) => React.ReactNode;\r\nexport type ReactEditRenderer = (params: EditRendererParams) => React.ReactNode;\r\nexport type ReactHeaderRenderer = (params: HeaderRendererParams) => React.ReactNode;\r\n\r\nexport interface GridProps<TData extends Row = Row> {\r\n columns: ColumnDefinition[];\r\n /** Data source for the grid */\r\n dataSource?: DataSource<TData>;\r\n /** Legacy: Raw row data (will be wrapped in a client data source) */\r\n rowData?: TData[];\r\n rowHeight: number;\r\n headerHeight?: number;\r\n overscan?: number;\r\n /** Show filter row below headers */\r\n showFilters?: boolean;\r\n /** Debounce time for filter input (ms) */\r\n filterDebounce?: number;\r\n /** Enable dark mode styling */\r\n darkMode?: boolean;\r\n\r\n // Renderer registries\r\n cellRenderers?: Record<string, ReactCellRenderer>;\r\n editRenderers?: Record<string, ReactEditRenderer>;\r\n headerRenderers?: Record<string, ReactHeaderRenderer>;\r\n\r\n // Global fallback renderers\r\n cellRenderer?: ReactCellRenderer;\r\n editRenderer?: ReactEditRenderer;\r\n headerRenderer?: ReactHeaderRenderer;\r\n}\r\n\r\n// =============================================================================\r\n// State Types\r\n// =============================================================================\r\n\r\ninterface SlotData {\r\n slotId: string;\r\n rowIndex: number;\r\n rowData: Row;\r\n translateY: number;\r\n}\r\n\r\ninterface GridState {\r\n slots: Map<string, SlotData>;\r\n activeCell: CellPosition | null;\r\n selectionRange: CellRange | null;\r\n editingCell: { row: number; col: number; initialValue: CellValue } | null;\r\n contentWidth: number;\r\n contentHeight: number;\r\n headers: Map<number, { column: ColumnDefinition; sortDirection?: SortDirection; sortIndex?: number }>;\r\n isLoading: boolean;\r\n error: string | null;\r\n totalRows: number;\r\n}\r\n\r\ntype GridAction =\r\n | { type: \"BATCH_INSTRUCTIONS\"; instructions: GridInstruction[] }\r\n | { type: \"RESET\" };\r\n\r\n// =============================================================================\r\n// Reducer\r\n// =============================================================================\r\n\r\n/**\r\n * Apply a single instruction to mutable slot maps and return other state changes.\r\n * This allows batching multiple slot operations efficiently.\r\n */\r\nfunction applyInstruction(\r\n instruction: GridInstruction,\r\n slots: Map<string, SlotData>,\r\n headers: Map<number, { column: ColumnDefinition; sortDirection?: SortDirection; sortIndex?: number }>\r\n): Partial<GridState> | null {\r\n switch (instruction.type) {\r\n case \"CREATE_SLOT\":\r\n slots.set(instruction.slotId, {\r\n slotId: instruction.slotId,\r\n rowIndex: -1,\r\n rowData: {},\r\n translateY: 0,\r\n });\r\n return null; // Slots map is mutated\r\n\r\n case \"DESTROY_SLOT\":\r\n slots.delete(instruction.slotId);\r\n return null;\r\n\r\n case \"ASSIGN_SLOT\": {\r\n const existing = slots.get(instruction.slotId);\r\n if (existing) {\r\n slots.set(instruction.slotId, {\r\n ...existing,\r\n rowIndex: instruction.rowIndex,\r\n rowData: instruction.rowData,\r\n });\r\n }\r\n return null;\r\n }\r\n\r\n case \"MOVE_SLOT\": {\r\n const existing = slots.get(instruction.slotId);\r\n if (existing) {\r\n slots.set(instruction.slotId, {\r\n ...existing,\r\n translateY: instruction.translateY,\r\n });\r\n }\r\n return null;\r\n }\r\n\r\n case \"SET_ACTIVE_CELL\":\r\n return { activeCell: instruction.position };\r\n\r\n case \"SET_SELECTION_RANGE\":\r\n return { selectionRange: instruction.range };\r\n\r\n case \"START_EDIT\":\r\n return {\r\n editingCell: {\r\n row: instruction.row,\r\n col: instruction.col,\r\n initialValue: instruction.initialValue,\r\n },\r\n };\r\n\r\n case \"STOP_EDIT\":\r\n return { editingCell: null };\r\n\r\n case \"SET_CONTENT_SIZE\":\r\n return {\r\n contentWidth: instruction.width,\r\n contentHeight: instruction.height,\r\n };\r\n\r\n case \"UPDATE_HEADER\":\r\n headers.set(instruction.colIndex, {\r\n column: instruction.column,\r\n sortDirection: instruction.sortDirection,\r\n sortIndex: instruction.sortIndex,\r\n });\r\n return null;\r\n\r\n case \"DATA_LOADING\":\r\n return { isLoading: true, error: null };\r\n\r\n case \"DATA_LOADED\":\r\n return { isLoading: false, totalRows: instruction.totalRows };\r\n\r\n case \"DATA_ERROR\":\r\n return { isLoading: false, error: instruction.error };\r\n\r\n default:\r\n return null;\r\n }\r\n}\r\n\r\nfunction gridReducer(state: GridState, action: GridAction): GridState {\r\n if (action.type === \"RESET\") {\r\n return createInitialState();\r\n }\r\n\r\n // Process batch of instructions in one state update\r\n const { instructions } = action;\r\n // console.log(\"[GP-Grid Reducer] Processing batch:\", instructions.map(i => i.type));\r\n if (instructions.length === 0) {\r\n return state;\r\n }\r\n\r\n // Create mutable copies of Maps to batch updates\r\n const newSlots = new Map(state.slots);\r\n const newHeaders = new Map(state.headers);\r\n let stateChanges: Partial<GridState> = {};\r\n\r\n // Apply all instructions\r\n for (const instruction of instructions) {\r\n const changes = applyInstruction(instruction, newSlots, newHeaders);\r\n if (changes) {\r\n stateChanges = { ...stateChanges, ...changes };\r\n }\r\n }\r\n\r\n // Return new state with all changes applied\r\n return {\r\n ...state,\r\n ...stateChanges,\r\n slots: newSlots,\r\n headers: newHeaders,\r\n };\r\n}\r\n\r\nfunction createInitialState(): GridState {\r\n return {\r\n slots: new Map(),\r\n activeCell: null,\r\n selectionRange: null,\r\n editingCell: null,\r\n contentWidth: 0,\r\n contentHeight: 0,\r\n headers: new Map(),\r\n isLoading: false,\r\n error: null,\r\n totalRows: 0,\r\n };\r\n}\r\n\r\n// =============================================================================\r\n// Grid Component\r\n// =============================================================================\r\n\r\nexport function Grid<TData extends Row = Row>(props: GridProps<TData>) {\r\n // Inject styles on first render (safe to call multiple times)\r\n injectStyles();\r\n\r\n const {\r\n columns,\r\n dataSource: providedDataSource,\r\n rowData,\r\n rowHeight,\r\n headerHeight = rowHeight,\r\n overscan = 3,\r\n showFilters = false,\r\n filterDebounce = 300,\r\n darkMode = false,\r\n cellRenderers = {},\r\n editRenderers = {},\r\n headerRenderers = {},\r\n cellRenderer,\r\n editRenderer,\r\n headerRenderer,\r\n } = props;\r\n\r\n const containerRef = useRef<HTMLDivElement>(null);\r\n const coreRef = useRef<GridCore<TData> | null>(null);\r\n const [state, dispatch] = useReducer(gridReducer, null, createInitialState);\r\n const [filterValues, setFilterValues] = useState<Record<string, string>>({});\r\n const filterTimeoutRef = useRef<Record<string, ReturnType<typeof setTimeout>>>({});\r\n const [isDraggingFill, setIsDraggingFill] = useState(false);\r\n const [fillTarget, setFillTarget] = useState<{ row: number; col: number } | null>(null);\r\n const [fillSourceRange, setFillSourceRange] = useState<{ startRow: number; startCol: number; endRow: number; endCol: number } | null>(null);\r\n const autoScrollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\r\n const [isDraggingSelection, setIsDraggingSelection] = useState(false);\r\n\r\n // Computed heights\r\n const filterRowHeight = showFilters ? 40 : 0;\r\n const totalHeaderHeight = headerHeight + filterRowHeight;\r\n\r\n // Create data source from rowData if not provided\r\n const dataSource = useMemo(() => {\r\n if (providedDataSource) {\r\n return providedDataSource;\r\n }\r\n if (rowData) {\r\n return createDataSourceFromArray(rowData);\r\n }\r\n // Empty data source\r\n return createClientDataSource<TData>([]);\r\n }, [providedDataSource, rowData]);\r\n\r\n // Compute column positions\r\n const columnPositions = useMemo(() => {\r\n const positions = [0];\r\n let pos = 0;\r\n for (const col of columns) {\r\n pos += col.width;\r\n positions.push(pos);\r\n }\r\n return positions;\r\n }, [columns]);\r\n\r\n const totalWidth = columnPositions[columnPositions.length - 1] ?? 0;\r\n\r\n // Initialize GridCore\r\n useEffect(() => {\r\n const core = new GridCore<TData>({\r\n columns,\r\n dataSource,\r\n rowHeight,\r\n headerHeight: totalHeaderHeight,\r\n overscan,\r\n });\r\n\r\n coreRef.current = core;\r\n\r\n // Subscribe to batched instructions for efficient state updates\r\n const unsubscribe = core.onBatchInstruction((instructions) => {\r\n dispatch({ type: \"BATCH_INSTRUCTIONS\", instructions });\r\n });\r\n\r\n // Initialize\r\n core.initialize();\r\n\r\n return () => {\r\n unsubscribe();\r\n coreRef.current = null;\r\n };\r\n }, [columns, dataSource, rowHeight, totalHeaderHeight, overscan]);\r\n\r\n // Handle scroll\r\n const handleScroll = useCallback(() => {\r\n const container = containerRef.current;\r\n const core = coreRef.current;\r\n if (!container || !core) return;\r\n\r\n core.setViewport(\r\n container.scrollTop,\r\n container.scrollLeft,\r\n container.clientWidth,\r\n container.clientHeight\r\n );\r\n }, []);\r\n\r\n // Initial measurement\r\n useEffect(() => {\r\n const container = containerRef.current;\r\n const core = coreRef.current;\r\n if (!container || !core) return;\r\n\r\n const resizeObserver = new ResizeObserver(() => {\r\n core.setViewport(\r\n container.scrollTop,\r\n container.scrollLeft,\r\n container.clientWidth,\r\n container.clientHeight\r\n );\r\n });\r\n\r\n resizeObserver.observe(container);\r\n handleScroll();\r\n\r\n return () => resizeObserver.disconnect();\r\n }, [handleScroll]);\r\n\r\n // Handle filter change with debounce\r\n const handleFilterChange = useCallback(\r\n (colId: string, value: string) => {\r\n setFilterValues((prev) => ({ ...prev, [colId]: value }));\r\n\r\n // Clear existing timeout\r\n if (filterTimeoutRef.current[colId]) {\r\n clearTimeout(filterTimeoutRef.current[colId]);\r\n }\r\n\r\n // Debounce the actual filter application\r\n filterTimeoutRef.current[colId] = setTimeout(() => {\r\n const core = coreRef.current;\r\n if (core) {\r\n core.setFilter(colId, value);\r\n }\r\n }, filterDebounce);\r\n },\r\n [filterDebounce]\r\n );\r\n\r\n // Keyboard navigation\r\n const handleKeyDown = useCallback(\r\n (e: React.KeyboardEvent) => {\r\n const core = coreRef.current;\r\n if (!core) return;\r\n\r\n // Don't handle keyboard events when editing\r\n if (state.editingCell && e.key !== \"Enter\" && e.key !== \"Escape\" && e.key !== \"Tab\") {\r\n return;\r\n }\r\n\r\n const { selection } = core;\r\n const isShift = e.shiftKey;\r\n const isCtrl = e.ctrlKey || e.metaKey;\r\n\r\n switch (e.key) {\r\n case \"ArrowUp\":\r\n e.preventDefault();\r\n selection.moveFocus(\"up\", isShift);\r\n break;\r\n case \"ArrowDown\":\r\n e.preventDefault();\r\n selection.moveFocus(\"down\", isShift);\r\n break;\r\n case \"ArrowLeft\":\r\n e.preventDefault();\r\n selection.moveFocus(\"left\", isShift);\r\n break;\r\n case \"ArrowRight\":\r\n e.preventDefault();\r\n selection.moveFocus(\"right\", isShift);\r\n break;\r\n case \"Enter\":\r\n e.preventDefault();\r\n if (state.editingCell) {\r\n core.commitEdit();\r\n } else if (state.activeCell) {\r\n core.startEdit(state.activeCell.row, state.activeCell.col);\r\n }\r\n break;\r\n case \"Escape\":\r\n e.preventDefault();\r\n if (state.editingCell) {\r\n core.cancelEdit();\r\n } else {\r\n selection.clearSelection();\r\n }\r\n break;\r\n case \"Tab\":\r\n e.preventDefault();\r\n if (state.editingCell) {\r\n core.commitEdit();\r\n }\r\n selection.moveFocus(isShift ? \"left\" : \"right\", false);\r\n break;\r\n case \"a\":\r\n if (isCtrl) {\r\n e.preventDefault();\r\n selection.selectAll();\r\n }\r\n break;\r\n case \"c\":\r\n if (isCtrl) {\r\n e.preventDefault();\r\n selection.copySelectionToClipboard();\r\n }\r\n break;\r\n case \"F2\":\r\n e.preventDefault();\r\n if (state.activeCell && !state.editingCell) {\r\n core.startEdit(state.activeCell.row, state.activeCell.col);\r\n }\r\n break;\r\n case \"Delete\":\r\n case \"Backspace\":\r\n // Start editing with empty value on delete/backspace\r\n if (state.activeCell && !state.editingCell) {\r\n e.preventDefault();\r\n core.startEdit(state.activeCell.row, state.activeCell.col);\r\n }\r\n break;\r\n default:\r\n // Start editing on any printable character\r\n if (\r\n state.activeCell &&\r\n !state.editingCell &&\r\n !isCtrl &&\r\n e.key.length === 1\r\n ) {\r\n core.startEdit(state.activeCell.row, state.activeCell.col);\r\n }\r\n break;\r\n }\r\n },\r\n [state.activeCell, state.editingCell]\r\n );\r\n\r\n // Scroll active cell into view when navigating with keyboard\r\n useEffect(() => {\r\n // Skip scrolling when editing - the user just clicked on the cell so it's already visible\r\n if (!state.activeCell || !containerRef.current || state.editingCell) return;\r\n\r\n const { row, col } = state.activeCell;\r\n const container = containerRef.current;\r\n\r\n // Calculate cell position\r\n const cellTop = row * rowHeight + totalHeaderHeight;\r\n const cellBottom = cellTop + rowHeight;\r\n const cellLeft = columnPositions[col] ?? 0;\r\n const cellRight = cellLeft + (columns[col]?.width ?? 0);\r\n\r\n // Get visible area\r\n const visibleTop = container.scrollTop + totalHeaderHeight;\r\n const visibleBottom = container.scrollTop + container.clientHeight;\r\n const visibleLeft = container.scrollLeft;\r\n const visibleRight = container.scrollLeft + container.clientWidth;\r\n\r\n // Scroll vertically if needed\r\n if (cellTop < visibleTop) {\r\n container.scrollTop = cellTop - totalHeaderHeight;\r\n } else if (cellBottom > visibleBottom) {\r\n container.scrollTop = cellBottom - container.clientHeight;\r\n }\r\n\r\n // Scroll horizontally if needed\r\n if (cellLeft < visibleLeft) {\r\n container.scrollLeft = cellLeft;\r\n } else if (cellRight > visibleRight) {\r\n container.scrollLeft = cellRight - container.clientWidth;\r\n }\r\n }, [state.activeCell, state.editingCell, rowHeight, totalHeaderHeight, columnPositions, columns]);\r\n\r\n // Cell mouse down handler (starts selection and drag)\r\n const handleCellMouseDown = useCallback(\r\n (rowIndex: number, colIndex: number, e: React.MouseEvent) => {\r\n // console.log(\"[GP-Grid] Cell mousedown:\", { rowIndex, colIndex, coreExists: !!coreRef.current });\r\n const core = coreRef.current;\r\n if (!core || core.getEditState() !== null) {\r\n // console.warn(\"[GP-Grid] Core not initialized on cell mousedown\");\r\n return;\r\n }\r\n\r\n // Only handle left mouse button\r\n if (e.button !== 0) return;\r\n\r\n // Focus the container to enable keyboard navigation\r\n containerRef.current?.focus();\r\n\r\n core.selection.startSelection(\r\n { row: rowIndex, col: colIndex },\r\n { shift: e.shiftKey, ctrl: e.ctrlKey || e.metaKey }\r\n );\r\n\r\n // Start drag selection (unless shift is held - that's a one-time extend)\r\n if (!e.shiftKey) {\r\n setIsDraggingSelection(true);\r\n }\r\n },\r\n []\r\n );\r\n\r\n // Cell double-click handler\r\n const handleCellDoubleClick = useCallback(\r\n (rowIndex: number, colIndex: number) => {\r\n const core = coreRef.current;\r\n if (!core) return;\r\n\r\n core.startEdit(rowIndex, colIndex);\r\n },\r\n []\r\n );\r\n\r\n // Header click handler (sort)\r\n const handleHeaderClick = useCallback(\r\n (colIndex: number, e: React.MouseEvent) => {\r\n // console.log(\"[GP-Grid] Header click:\", { colIndex, coreExists: !!coreRef.current });\r\n const core = coreRef.current;\r\n if (!core) {\r\n // console.warn(\"[GP-Grid] Core not initialized on header click\");\r\n return;\r\n }\r\n\r\n const column = columns[colIndex];\r\n if (!column) {\r\n // console.warn(\"[GP-Grid] Column not found for index:\", colIndex);\r\n return;\r\n }\r\n\r\n const colId = column.colId ?? column.field;\r\n const headerInfo = state.headers.get(colIndex);\r\n const currentDirection = headerInfo?.sortDirection;\r\n\r\n // Cycle: none -> asc -> desc -> none\r\n let newDirection: SortDirection | null;\r\n if (!currentDirection) {\r\n newDirection = \"asc\";\r\n } else if (currentDirection === \"asc\") {\r\n newDirection = \"desc\";\r\n } else {\r\n newDirection = null;\r\n }\r\n\r\n // console.log(\"[GP-Grid] Setting sort:\", { colId, newDirection });\r\n core.setSort(colId, newDirection, e.shiftKey);\r\n },\r\n [columns, state.headers]\r\n );\r\n\r\n // Fill handle drag handlers\r\n const handleFillHandleMouseDown = useCallback(\r\n (e: React.MouseEvent) => {\r\n // console.log(\"[GP-Grid] Fill handle mousedown triggered\");\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n const core = coreRef.current;\r\n if (!core) return;\r\n\r\n const { activeCell, selectionRange } = state;\r\n if (!activeCell && !selectionRange) return;\r\n\r\n // Create source range from selection or active cell\r\n const sourceRange = selectionRange ?? {\r\n startRow: activeCell!.row,\r\n startCol: activeCell!.col,\r\n endRow: activeCell!.row,\r\n endCol: activeCell!.col,\r\n };\r\n\r\n // console.log(\"[GP-Grid] Starting fill drag with source range:\", sourceRange);\r\n core.fill.startFillDrag(sourceRange);\r\n setFillSourceRange(sourceRange);\r\n setFillTarget({ \r\n row: Math.max(sourceRange.startRow, sourceRange.endRow),\r\n col: Math.max(sourceRange.startCol, sourceRange.endCol)\r\n });\r\n setIsDraggingFill(true);\r\n },\r\n [state.activeCell, state.selectionRange]\r\n );\r\n\r\n // Handle mouse move during fill drag\r\n useEffect(() => {\r\n if (!isDraggingFill) return;\r\n\r\n // Auto-scroll configuration\r\n const SCROLL_THRESHOLD = 40; // pixels from edge to trigger scroll\r\n const SCROLL_SPEED = 10; // pixels per frame\r\n\r\n const handleMouseMove = (e: MouseEvent) => {\r\n const core = coreRef.current;\r\n const container = containerRef.current;\r\n if (!core || !container) return;\r\n\r\n // Get container bounds\r\n const rect = container.getBoundingClientRect();\r\n const scrollLeft = container.scrollLeft;\r\n const scrollTop = container.scrollTop;\r\n\r\n // Calculate mouse position relative to grid content\r\n const mouseX = e.clientX - rect.left + scrollLeft;\r\n const mouseY = e.clientY - rect.top + scrollTop - totalHeaderHeight;\r\n\r\n // Find the row and column under the mouse\r\n const targetRow = Math.max(0, Math.floor(mouseY / rowHeight));\r\n \r\n // Find column by checking column positions\r\n let targetCol = 0;\r\n for (let i = 0; i < columnPositions.length - 1; i++) {\r\n if (mouseX >= columnPositions[i]! && mouseX < columnPositions[i + 1]!) {\r\n targetCol = i;\r\n break;\r\n }\r\n if (mouseX >= columnPositions[columnPositions.length - 1]!) {\r\n targetCol = columnPositions.length - 2;\r\n }\r\n }\r\n\r\n core.fill.updateFillDrag(targetRow, targetCol);\r\n setFillTarget({ row: targetRow, col: targetCol });\r\n\r\n // Auto-scroll logic\r\n const mouseYInContainer = e.clientY - rect.top;\r\n const mouseXInContainer = e.clientX - rect.left;\r\n\r\n // Clear any existing auto-scroll\r\n if (autoScrollIntervalRef.current) {\r\n clearInterval(autoScrollIntervalRef.current);\r\n autoScrollIntervalRef.current = null;\r\n }\r\n\r\n // Check if we need to auto-scroll\r\n let scrollDeltaX = 0;\r\n let scrollDeltaY = 0;\r\n\r\n // Vertical scrolling\r\n if (mouseYInContainer < SCROLL_THRESHOLD + totalHeaderHeight) {\r\n scrollDeltaY = -SCROLL_SPEED;\r\n } else if (mouseYInContainer > rect.height - SCROLL_THRESHOLD) {\r\n scrollDeltaY = SCROLL_SPEED;\r\n }\r\n\r\n // Horizontal scrolling\r\n if (mouseXInContainer < SCROLL_THRESHOLD) {\r\n scrollDeltaX = -SCROLL_SPEED;\r\n } else if (mouseXInContainer > rect.width - SCROLL_THRESHOLD) {\r\n scrollDeltaX = SCROLL_SPEED;\r\n }\r\n\r\n // Start auto-scroll if needed\r\n if (scrollDeltaX !== 0 || scrollDeltaY !== 0) {\r\n autoScrollIntervalRef.current = setInterval(() => {\r\n if (containerRef.current) {\r\n containerRef.current.scrollTop += scrollDeltaY;\r\n containerRef.current.scrollLeft += scrollDeltaX;\r\n }\r\n }, 16); // ~60fps\r\n }\r\n };\r\n\r\n const handleMouseUp = () => {\r\n // Clear auto-scroll\r\n if (autoScrollIntervalRef.current) {\r\n clearInterval(autoScrollIntervalRef.current);\r\n autoScrollIntervalRef.current = null;\r\n }\r\n\r\n const core = coreRef.current;\r\n if (core) {\r\n core.fill.commitFillDrag();\r\n // Refresh slots to show updated values\r\n core.refreshSlotData();\r\n }\r\n setIsDraggingFill(false);\r\n setFillTarget(null);\r\n setFillSourceRange(null);\r\n };\r\n\r\n document.addEventListener(\"mousemove\", handleMouseMove);\r\n document.addEventListener(\"mouseup\", handleMouseUp);\r\n\r\n return () => {\r\n // Clear auto-scroll on cleanup\r\n if (autoScrollIntervalRef.current) {\r\n clearInterval(autoScrollIntervalRef.current);\r\n autoScrollIntervalRef.current = null;\r\n }\r\n document.removeEventListener(\"mousemove\", handleMouseMove);\r\n document.removeEventListener(\"mouseup\", handleMouseUp);\r\n };\r\n }, [isDraggingFill, totalHeaderHeight, rowHeight, columnPositions]);\r\n\r\n // Handle mouse move/up during selection drag\r\n useEffect(() => {\r\n if (!isDraggingSelection) return;\r\n\r\n // Auto-scroll configuration\r\n const SCROLL_THRESHOLD = 40;\r\n const SCROLL_SPEED = 10;\r\n\r\n const handleMouseMove = (e: MouseEvent) => {\r\n const core = coreRef.current;\r\n const container = containerRef.current;\r\n if (!core || !container) return;\r\n\r\n // Get container bounds\r\n const rect = container.getBoundingClientRect();\r\n const scrollLeft = container.scrollLeft;\r\n const scrollTop = container.scrollTop;\r\n\r\n // Calculate mouse position relative to grid content\r\n const mouseX = e.clientX - rect.left + scrollLeft;\r\n const mouseY = e.clientY - rect.top + scrollTop - totalHeaderHeight;\r\n\r\n // Find the row and column under the mouse\r\n const targetRow = Math.max(0, Math.min(Math.floor(mouseY / rowHeight), core.getRowCount() - 1));\r\n \r\n // Find column by checking column positions\r\n let targetCol = 0;\r\n for (let i = 0; i < columnPositions.length - 1; i++) {\r\n if (mouseX >= columnPositions[i]! && mouseX < columnPositions[i + 1]!) {\r\n targetCol = i;\r\n break;\r\n }\r\n if (mouseX >= columnPositions[columnPositions.length - 1]!) {\r\n targetCol = columnPositions.length - 2;\r\n }\r\n }\r\n targetCol = Math.max(0, Math.min(targetCol, columns.length - 1));\r\n\r\n // Extend selection to target cell (like shift+click)\r\n core.selection.startSelection(\r\n { row: targetRow, col: targetCol },\r\n { shift: true }\r\n );\r\n\r\n // Auto-scroll logic\r\n const mouseYInContainer = e.clientY - rect.top;\r\n const mouseXInContainer = e.clientX - rect.left;\r\n\r\n // Clear any existing auto-scroll\r\n if (autoScrollIntervalRef.current) {\r\n clearInterval(autoScrollIntervalRef.current);\r\n autoScrollIntervalRef.current = null;\r\n }\r\n\r\n // Check if we need to auto-scroll\r\n let scrollDeltaX = 0;\r\n let scrollDeltaY = 0;\r\n\r\n // Vertical scrolling\r\n if (mouseYInContainer < SCROLL_THRESHOLD + totalHeaderHeight) {\r\n scrollDeltaY = -SCROLL_SPEED;\r\n } else if (mouseYInContainer > rect.height - SCROLL_THRESHOLD) {\r\n scrollDeltaY = SCROLL_SPEED;\r\n }\r\n\r\n // Horizontal scrolling\r\n if (mouseXInContainer < SCROLL_THRESHOLD) {\r\n scrollDeltaX = -SCROLL_SPEED;\r\n } else if (mouseXInContainer > rect.width - SCROLL_THRESHOLD) {\r\n scrollDeltaX = SCROLL_SPEED;\r\n }\r\n\r\n // Start auto-scroll if needed\r\n if (scrollDeltaX !== 0 || scrollDeltaY !== 0) {\r\n autoScrollIntervalRef.current = setInterval(() => {\r\n if (containerRef.current) {\r\n containerRef.current.scrollTop += scrollDeltaY;\r\n containerRef.current.scrollLeft += scrollDeltaX;\r\n }\r\n }, 16); // ~60fps\r\n }\r\n };\r\n\r\n const handleMouseUp = () => {\r\n // Clear auto-scroll\r\n if (autoScrollIntervalRef.current) {\r\n clearInterval(autoScrollIntervalRef.current);\r\n autoScrollIntervalRef.current = null;\r\n }\r\n setIsDraggingSelection(false);\r\n };\r\n\r\n document.addEventListener(\"mousemove\", handleMouseMove);\r\n document.addEventListener(\"mouseup\", handleMouseUp);\r\n\r\n return () => {\r\n if (autoScrollIntervalRef.current) {\r\n clearInterval(autoScrollIntervalRef.current);\r\n autoScrollIntervalRef.current = null;\r\n }\r\n document.removeEventListener(\"mousemove\", handleMouseMove);\r\n document.removeEventListener(\"mouseup\", handleMouseUp);\r\n };\r\n }, [isDraggingSelection, totalHeaderHeight, rowHeight, columnPositions, columns.length]);\r\n\r\n // Render helpers\r\n const isSelected = useCallback(\r\n (row: number, col: number): boolean => {\r\n const { selectionRange } = state;\r\n if (!selectionRange) return false;\r\n\r\n const minRow = Math.min(selectionRange.startRow, selectionRange.endRow);\r\n const maxRow = Math.max(selectionRange.startRow, selectionRange.endRow);\r\n const minCol = Math.min(selectionRange.startCol, selectionRange.endCol);\r\n const maxCol = Math.max(selectionRange.startCol, selectionRange.endCol);\r\n\r\n return row >= minRow && row <= maxRow && col >= minCol && col <= maxCol;\r\n },\r\n [state.selectionRange]\r\n );\r\n\r\n const isActiveCell = useCallback(\r\n (row: number, col: number): boolean => {\r\n return state.activeCell?.row === row && state.activeCell?.col === col;\r\n },\r\n [state.activeCell]\r\n );\r\n\r\n const isEditingCell = useCallback(\r\n (row: number, col: number): boolean => {\r\n return state.editingCell?.row === row && state.editingCell?.col === col;\r\n },\r\n [state.editingCell]\r\n );\r\n\r\n // Check if cell is in fill preview range\r\n const isInFillPreview = useCallback(\r\n (row: number, col: number): boolean => {\r\n if (!isDraggingFill || !fillSourceRange || !fillTarget) return false;\r\n\r\n const srcMinRow = Math.min(fillSourceRange.startRow, fillSourceRange.endRow);\r\n const srcMaxRow = Math.max(fillSourceRange.startRow, fillSourceRange.endRow);\r\n const srcMinCol = Math.min(fillSourceRange.startCol, fillSourceRange.endCol);\r\n const srcMaxCol = Math.max(fillSourceRange.startCol, fillSourceRange.endCol);\r\n\r\n // Determine fill direction and range\r\n const fillDown = fillTarget.row > srcMaxRow;\r\n const fillUp = fillTarget.row < srcMinRow;\r\n const fillRight = fillTarget.col > srcMaxCol;\r\n const fillLeft = fillTarget.col < srcMinCol;\r\n\r\n // Check if cell is in the fill preview area (not the source area)\r\n if (fillDown) {\r\n return row > srcMaxRow && row <= fillTarget.row && col >= srcMinCol && col <= srcMaxCol;\r\n }\r\n if (fillUp) {\r\n return row < srcMinRow && row >= fillTarget.row && col >= srcMinCol && col <= srcMaxCol;\r\n }\r\n if (fillRight) {\r\n return col > srcMaxCol && col <= fillTarget.col && row >= srcMinRow && row <= srcMaxRow;\r\n }\r\n if (fillLeft) {\r\n return col < srcMinCol && col >= fillTarget.col && row >= srcMinRow && row <= srcMaxRow;\r\n }\r\n\r\n return false;\r\n },\r\n [isDraggingFill, fillSourceRange, fillTarget]\r\n );\r\n\r\n // Get cell value from row data\r\n const getCellValue = useCallback((rowData: Row, field: string): CellValue => {\r\n const parts = field.split(\".\");\r\n let value: unknown = rowData;\r\n\r\n for (const part of parts) {\r\n if (value == null || typeof value !== \"object\") {\r\n return null;\r\n }\r\n value = (value as Record<string, unknown>)[part];\r\n }\r\n\r\n return (value ?? null) as CellValue;\r\n }, []);\r\n\r\n // Render cell content\r\n const renderCell = useCallback(\r\n (\r\n column: ColumnDefinition,\r\n rowData: Row,\r\n rowIndex: number,\r\n colIndex: number\r\n ): React.ReactNode => {\r\n const value = getCellValue(rowData, column.field);\r\n const params: CellRendererParams = {\r\n value,\r\n rowData,\r\n column,\r\n rowIndex,\r\n colIndex,\r\n isActive: isActiveCell(rowIndex, colIndex),\r\n isSelected: isSelected(rowIndex, colIndex),\r\n isEditing: isEditingCell(rowIndex, colIndex),\r\n };\r\n\r\n // Check for column-specific renderer\r\n if (column.cellRenderer && typeof column.cellRenderer === \"string\") {\r\n const renderer = cellRenderers[column.cellRenderer];\r\n if (renderer) {\r\n return renderer(params);\r\n }\r\n }\r\n\r\n // Fall back to global renderer\r\n if (cellRenderer) {\r\n return cellRenderer(params);\r\n }\r\n\r\n // Default text rendering\r\n return value == null ? \"\" : String(value);\r\n },\r\n [getCellValue, isActiveCell, isSelected, isEditingCell, cellRenderers, cellRenderer]\r\n );\r\n\r\n // Render edit cell\r\n const renderEditCell = useCallback(\r\n (\r\n column: ColumnDefinition,\r\n rowData: Row,\r\n rowIndex: number,\r\n colIndex: number,\r\n initialValue: CellValue\r\n ): React.ReactNode => {\r\n const core = coreRef.current;\r\n if (!core) return null;\r\n\r\n const value = getCellValue(rowData, column.field);\r\n const params: EditRendererParams = {\r\n value,\r\n rowData,\r\n column,\r\n rowIndex,\r\n colIndex,\r\n isActive: true,\r\n isSelected: true,\r\n isEditing: true,\r\n initialValue,\r\n onValueChange: (newValue) => core.updateEditValue(newValue),\r\n onCommit: () => core.commitEdit(),\r\n onCancel: () => core.cancelEdit(),\r\n };\r\n\r\n // Check for column-specific renderer\r\n if (column.editRenderer && typeof column.editRenderer === \"string\") {\r\n const renderer = editRenderers[column.editRenderer];\r\n if (renderer) {\r\n return renderer(params);\r\n }\r\n }\r\n\r\n // Fall back to global renderer\r\n if (editRenderer) {\r\n return editRenderer(params);\r\n }\r\n\r\n // Default input\r\n return (\r\n <input\r\n className=\"gp-grid-edit-input\"\r\n type=\"text\"\r\n defaultValue={initialValue == null ? \"\" : String(initialValue)}\r\n autoFocus\r\n onFocus={(e) => e.target.select()}\r\n onChange={(e) => core.updateEditValue(e.target.value)}\r\n onKeyDown={(e) => {\r\n e.stopPropagation();\r\n if (e.key === \"Enter\") {\r\n core.commitEdit();\r\n } else if (e.key === \"Escape\") {\r\n core.cancelEdit();\r\n } else if (e.key === \"Tab\") {\r\n e.preventDefault();\r\n core.commitEdit();\r\n core.selection.moveFocus(e.shiftKey ? \"left\" : \"right\", false);\r\n }\r\n }}\r\n onBlur={() => core.commitEdit()}\r\n />\r\n );\r\n },\r\n [getCellValue, editRenderers, editRenderer]\r\n );\r\n\r\n // Render header\r\n const renderHeader = useCallback(\r\n (\r\n column: ColumnDefinition,\r\n colIndex: number,\r\n sortDirection?: SortDirection,\r\n sortIndex?: number\r\n ): React.ReactNode => {\r\n const core = coreRef.current;\r\n const params: HeaderRendererParams = {\r\n column,\r\n colIndex,\r\n sortDirection,\r\n sortIndex,\r\n onSort: (direction, addToExisting) => {\r\n if (core) {\r\n core.setSort(column.colId ?? column.field, direction, addToExisting);\r\n }\r\n },\r\n };\r\n\r\n // Check for column-specific renderer\r\n if (column.headerRenderer && typeof column.headerRenderer === \"string\") {\r\n const renderer = headerRenderers[column.headerRenderer];\r\n if (renderer) {\r\n return renderer(params);\r\n }\r\n }\r\n\r\n // Fall back to global renderer\r\n if (headerRenderer) {\r\n return headerRenderer(params);\r\n }\r\n\r\n // Default header\r\n return (\r\n <>\r\n <span className=\"gp-grid-header-text\">\r\n {column.headerName ?? column.field}\r\n </span>\r\n {sortDirection && (\r\n <span className=\"gp-grid-sort-indicator\">\r\n {sortDirection === \"asc\" ? \"▲\" : \"▼\"}\r\n {sortIndex !== undefined && sortIndex > 0 && (\r\n <span className=\"gp-grid-sort-index\">{sortIndex}</span>\r\n )}\r\n </span>\r\n )}\r\n </>\r\n );\r\n },\r\n [headerRenderers, headerRenderer]\r\n );\r\n\r\n // Convert slots map to array for rendering\r\n const slotsArray = useMemo(() => Array.from(state.slots.values()), [state.slots]);\r\n\r\n // Calculate fill handle position (only show for editable columns)\r\n const fillHandlePosition = useMemo(() => {\r\n const { activeCell, selectionRange } = state;\r\n if (!activeCell && !selectionRange) return null;\r\n\r\n // Get the bottom-right corner and column range of selection or active cell\r\n let row: number, col: number;\r\n let minCol: number, maxCol: number;\r\n \r\n if (selectionRange) {\r\n row = Math.max(selectionRange.startRow, selectionRange.endRow);\r\n col = Math.max(selectionRange.startCol, selectionRange.endCol);\r\n minCol = Math.min(selectionRange.startCol, selectionRange.endCol);\r\n maxCol = Math.max(selectionRange.startCol, selectionRange.endCol);\r\n } else if (activeCell) {\r\n row = activeCell.row;\r\n col = activeCell.col;\r\n minCol = col;\r\n maxCol = col;\r\n } else {\r\n return null;\r\n }\r\n\r\n // Check if ALL columns in the selection are editable\r\n for (let c = minCol; c <= maxCol; c++) {\r\n const column = columns[c];\r\n if (!column || column.editable !== true) {\r\n return null; // Don't show fill handle if any column is not editable\r\n }\r\n }\r\n\r\n const cellTop = row * rowHeight + totalHeaderHeight;\r\n const cellLeft = columnPositions[col] ?? 0;\r\n const cellWidth = columns[col]?.width ?? 0;\r\n\r\n return {\r\n top: cellTop + rowHeight - 5,\r\n left: cellLeft + cellWidth - 20, // Move significantly left to avoid scrollbar overlap\r\n };\r\n }, [state.activeCell, state.selectionRange, rowHeight, totalHeaderHeight, columnPositions, columns]);\r\n\r\n return (\r\n <div\r\n ref={containerRef}\r\n className={`gp-grid-container${darkMode ? \" gp-grid-container--dark\" : \"\"}`}\r\n style={{\r\n width: \"100%\",\r\n height: \"100%\",\r\n overflow: \"auto\",\r\n position: \"relative\",\r\n }}\r\n onScroll={handleScroll}\r\n onKeyDown={handleKeyDown}\r\n tabIndex={0}\r\n >\r\n {/* Content sizer */}\r\n <div\r\n style={{\r\n width: Math.max(state.contentWidth, totalWidth),\r\n height: Math.max(state.contentHeight, totalHeaderHeight),\r\n position: \"relative\",\r\n minWidth: \"100%\",\r\n }}\r\n >\r\n {/* Headers */}\r\n <div\r\n className=\"gp-grid-header\"\r\n style={{\r\n position: \"sticky\",\r\n top: 0,\r\n left: 0,\r\n height: headerHeight,\r\n width: Math.max(state.contentWidth, totalWidth),\r\n minWidth: \"100%\",\r\n zIndex: 100,\r\n }}\r\n >\r\n {columns.map((column, colIndex) => {\r\n const headerInfo = state.headers.get(colIndex);\r\n return (\r\n <div\r\n key={column.colId ?? column.field}\r\n className=\"gp-grid-header-cell\"\r\n style={{\r\n position: \"absolute\",\r\n left: `${columnPositions[colIndex]}px`,\r\n top: 0,\r\n width: `${column.width}px`,\r\n height: `${headerHeight}px`,\r\n background: \"transparent\",\r\n }}\r\n onClick={(e) => handleHeaderClick(colIndex, e)}\r\n >\r\n {renderHeader(\r\n column,\r\n colIndex,\r\n headerInfo?.sortDirection,\r\n headerInfo?.sortIndex\r\n )}\r\n </div>\r\n );\r\n })}\r\n </div>\r\n\r\n {/* Filter Row */}\r\n {showFilters && (\r\n <div\r\n className=\"gp-grid-filter-row\"\r\n style={{\r\n position: \"sticky\",\r\n top: headerHeight,\r\n left: 0,\r\n height: filterRowHeight,\r\n width: Math.max(state.contentWidth, totalWidth),\r\n minWidth: \"100%\",\r\n zIndex: 99,\r\n }}\r\n >\r\n {columns.map((column, colIndex) => {\r\n const colId = column.colId ?? column.field;\r\n return (\r\n <div\r\n key={`filter-${colId}`}\r\n className=\"gp-grid-filter-cell\"\r\n style={{\r\n position: \"absolute\",\r\n left: `${columnPositions[colIndex]}px`,\r\n top: 0,\r\n width: `${column.width}px`,\r\n height: `${filterRowHeight}px`,\r\n }}\r\n >\r\n <input\r\n className=\"gp-grid-filter-input\"\r\n type=\"text\"\r\n placeholder={`Filter ${column.headerName ?? column.field}...`}\r\n value={filterValues[colId] ?? \"\"}\r\n onChange={(e) => handleFilterChange(colId, e.target.value)}\r\n onKeyDown={(e) => e.stopPropagation()}\r\n />\r\n </div>\r\n );\r\n })}\r\n </div>\r\n )}\r\n\r\n {/* Row slots */}\r\n {slotsArray.map((slot) => {\r\n if (slot.rowIndex < 0) return null;\r\n\r\n const isEvenRow = slot.rowIndex % 2 === 0;\r\n\r\n return (\r\n <div\r\n key={slot.slotId}\r\n className={`gp-grid-row ${isEvenRow ? \"gp-grid-row--even\" : \"\"}`}\r\n style={{\r\n position: \"absolute\",\r\n top: 0,\r\n left: 0,\r\n transform: `translateY(${slot.translateY}px)`,\r\n width: `${Math.max(state.contentWidth, totalWidth)}px`,\r\n height: `${rowHeight}px`,\r\n }}\r\n >\r\n {columns.map((column, colIndex) => {\r\n const isEditing = isEditingCell(slot.rowIndex, colIndex);\r\n const active = isActiveCell(slot.rowIndex, colIndex);\r\n const selected = isSelected(slot.rowIndex, colIndex);\r\n const inFillPreview = isInFillPreview(slot.rowIndex, colIndex);\r\n\r\n const cellClasses = [\r\n \"gp-grid-cell\",\r\n active && \"gp-grid-cell--active\",\r\n selected && !active && \"gp-grid-cell--selected\",\r\n isEditing && \"gp-grid-cell--editing\",\r\n inFillPreview && \"gp-grid-cell--fill-preview\",\r\n ]\r\n .filter(Boolean)\r\n .join(\" \");\r\n\r\n return (\r\n <div\r\n key={`${slot.slotId}-${colIndex}`}\r\n className={cellClasses}\r\n style={{\r\n position: \"absolute\",\r\n left: `${columnPositions[colIndex]}px`,\r\n top: 0,\r\n width: `${column.width}px`,\r\n height: `${rowHeight}px`,\r\n }}\r\n onMouseDown={(e) => handleCellMouseDown(slot.rowIndex, colIndex, e)}\r\n onDoubleClick={() => handleCellDoubleClick(slot.rowIndex, colIndex)}\r\n >\r\n {isEditing && state.editingCell\r\n ? renderEditCell(\r\n column,\r\n slot.rowData,\r\n slot.rowIndex,\r\n colIndex,\r\n state.editingCell.initialValue\r\n )\r\n : renderCell(column, slot.rowData, slot.rowIndex, colIndex)}\r\n </div>\r\n );\r\n })}\r\n </div>\r\n );\r\n })}\r\n\r\n {/* Fill handle (drag to fill) */}\r\n {fillHandlePosition && !state.editingCell && (\r\n <div\r\n className=\"gp-grid-fill-handle\"\r\n style={{\r\n position: \"absolute\",\r\n top: fillHandlePosition.top,\r\n left: fillHandlePosition.left,\r\n zIndex: 200,\r\n }}\r\n onMouseDown={handleFillHandleMouseDown}\r\n />\r\n )}\r\n\r\n {/* Loading indicator */}\r\n {state.isLoading && (\r\n <div className=\"gp-grid-loading\">\r\n <div className=\"gp-grid-loading-spinner\" />\r\n Loading...\r\n </div>\r\n )}\r\n\r\n {/* Error message */}\r\n {state.error && (\r\n <div className=\"gp-grid-error\">Error: {state.error}</div>\r\n )}\r\n\r\n {/* Empty state */}\r\n {!state.isLoading && !state.error && state.totalRows === 0 && (\r\n <div className=\"gp-grid-empty\">No data to display</div>\r\n )}\r\n </div>\r\n </div>\r\n );\r\n}\r\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gp-grid-react",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,7 +8,7 @@
8
8
  "link-workspace-packages": false
9
9
  },
10
10
  "dependencies": {
11
- "gp-grid-core": "0.1.0"
11
+ "gp-grid-core": "0.1.1"
12
12
  },
13
13
  "peerDependencies": {
14
14
  "react": "^19.0.0",
package/src/Grid.tsx CHANGED
@@ -271,6 +271,7 @@ export function Grid<TData extends Row = Row>(props: GridProps<TData>) {
271
271
  const [fillTarget, setFillTarget] = useState<{ row: number; col: number } | null>(null);
272
272
  const [fillSourceRange, setFillSourceRange] = useState<{ startRow: number; startCol: number; endRow: number; endCol: number } | null>(null);
273
273
  const autoScrollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
274
+ const [isDraggingSelection, setIsDraggingSelection] = useState(false);
274
275
 
275
276
  // Computed heights
276
277
  const filterRowHeight = showFilters ? 40 : 0;
@@ -515,16 +516,19 @@ export function Grid<TData extends Row = Row>(props: GridProps<TData>) {
515
516
  }
516
517
  }, [state.activeCell, state.editingCell, rowHeight, totalHeaderHeight, columnPositions, columns]);
517
518
 
518
- // Cell click handler
519
- const handleCellClick = useCallback(
519
+ // Cell mouse down handler (starts selection and drag)
520
+ const handleCellMouseDown = useCallback(
520
521
  (rowIndex: number, colIndex: number, e: React.MouseEvent) => {
521
- // console.log("[GP-Grid] Cell click:", { rowIndex, colIndex, coreExists: !!coreRef.current });
522
+ // console.log("[GP-Grid] Cell mousedown:", { rowIndex, colIndex, coreExists: !!coreRef.current });
522
523
  const core = coreRef.current;
523
524
  if (!core || core.getEditState() !== null) {
524
- // console.warn("[GP-Grid] Core not initialized on cell click");
525
+ // console.warn("[GP-Grid] Core not initialized on cell mousedown");
525
526
  return;
526
527
  }
527
528
 
529
+ // Only handle left mouse button
530
+ if (e.button !== 0) return;
531
+
528
532
  // Focus the container to enable keyboard navigation
529
533
  containerRef.current?.focus();
530
534
 
@@ -532,6 +536,11 @@ export function Grid<TData extends Row = Row>(props: GridProps<TData>) {
532
536
  { row: rowIndex, col: colIndex },
533
537
  { shift: e.shiftKey, ctrl: e.ctrlKey || e.metaKey }
534
538
  );
539
+
540
+ // Start drag selection (unless shift is held - that's a one-time extend)
541
+ if (!e.shiftKey) {
542
+ setIsDraggingSelection(true);
543
+ }
535
544
  },
536
545
  []
537
546
  );
@@ -727,6 +736,111 @@ export function Grid<TData extends Row = Row>(props: GridProps<TData>) {
727
736
  };
728
737
  }, [isDraggingFill, totalHeaderHeight, rowHeight, columnPositions]);
729
738
 
739
+ // Handle mouse move/up during selection drag
740
+ useEffect(() => {
741
+ if (!isDraggingSelection) return;
742
+
743
+ // Auto-scroll configuration
744
+ const SCROLL_THRESHOLD = 40;
745
+ const SCROLL_SPEED = 10;
746
+
747
+ const handleMouseMove = (e: MouseEvent) => {
748
+ const core = coreRef.current;
749
+ const container = containerRef.current;
750
+ if (!core || !container) return;
751
+
752
+ // Get container bounds
753
+ const rect = container.getBoundingClientRect();
754
+ const scrollLeft = container.scrollLeft;
755
+ const scrollTop = container.scrollTop;
756
+
757
+ // Calculate mouse position relative to grid content
758
+ const mouseX = e.clientX - rect.left + scrollLeft;
759
+ const mouseY = e.clientY - rect.top + scrollTop - totalHeaderHeight;
760
+
761
+ // Find the row and column under the mouse
762
+ const targetRow = Math.max(0, Math.min(Math.floor(mouseY / rowHeight), core.getRowCount() - 1));
763
+
764
+ // Find column by checking column positions
765
+ let targetCol = 0;
766
+ for (let i = 0; i < columnPositions.length - 1; i++) {
767
+ if (mouseX >= columnPositions[i]! && mouseX < columnPositions[i + 1]!) {
768
+ targetCol = i;
769
+ break;
770
+ }
771
+ if (mouseX >= columnPositions[columnPositions.length - 1]!) {
772
+ targetCol = columnPositions.length - 2;
773
+ }
774
+ }
775
+ targetCol = Math.max(0, Math.min(targetCol, columns.length - 1));
776
+
777
+ // Extend selection to target cell (like shift+click)
778
+ core.selection.startSelection(
779
+ { row: targetRow, col: targetCol },
780
+ { shift: true }
781
+ );
782
+
783
+ // Auto-scroll logic
784
+ const mouseYInContainer = e.clientY - rect.top;
785
+ const mouseXInContainer = e.clientX - rect.left;
786
+
787
+ // Clear any existing auto-scroll
788
+ if (autoScrollIntervalRef.current) {
789
+ clearInterval(autoScrollIntervalRef.current);
790
+ autoScrollIntervalRef.current = null;
791
+ }
792
+
793
+ // Check if we need to auto-scroll
794
+ let scrollDeltaX = 0;
795
+ let scrollDeltaY = 0;
796
+
797
+ // Vertical scrolling
798
+ if (mouseYInContainer < SCROLL_THRESHOLD + totalHeaderHeight) {
799
+ scrollDeltaY = -SCROLL_SPEED;
800
+ } else if (mouseYInContainer > rect.height - SCROLL_THRESHOLD) {
801
+ scrollDeltaY = SCROLL_SPEED;
802
+ }
803
+
804
+ // Horizontal scrolling
805
+ if (mouseXInContainer < SCROLL_THRESHOLD) {
806
+ scrollDeltaX = -SCROLL_SPEED;
807
+ } else if (mouseXInContainer > rect.width - SCROLL_THRESHOLD) {
808
+ scrollDeltaX = SCROLL_SPEED;
809
+ }
810
+
811
+ // Start auto-scroll if needed
812
+ if (scrollDeltaX !== 0 || scrollDeltaY !== 0) {
813
+ autoScrollIntervalRef.current = setInterval(() => {
814
+ if (containerRef.current) {
815
+ containerRef.current.scrollTop += scrollDeltaY;
816
+ containerRef.current.scrollLeft += scrollDeltaX;
817
+ }
818
+ }, 16); // ~60fps
819
+ }
820
+ };
821
+
822
+ const handleMouseUp = () => {
823
+ // Clear auto-scroll
824
+ if (autoScrollIntervalRef.current) {
825
+ clearInterval(autoScrollIntervalRef.current);
826
+ autoScrollIntervalRef.current = null;
827
+ }
828
+ setIsDraggingSelection(false);
829
+ };
830
+
831
+ document.addEventListener("mousemove", handleMouseMove);
832
+ document.addEventListener("mouseup", handleMouseUp);
833
+
834
+ return () => {
835
+ if (autoScrollIntervalRef.current) {
836
+ clearInterval(autoScrollIntervalRef.current);
837
+ autoScrollIntervalRef.current = null;
838
+ }
839
+ document.removeEventListener("mousemove", handleMouseMove);
840
+ document.removeEventListener("mouseup", handleMouseUp);
841
+ };
842
+ }, [isDraggingSelection, totalHeaderHeight, rowHeight, columnPositions, columns.length]);
843
+
730
844
  // Render helpers
731
845
  const isSelected = useCallback(
732
846
  (row: number, col: number): boolean => {
@@ -1164,7 +1278,7 @@ export function Grid<TData extends Row = Row>(props: GridProps<TData>) {
1164
1278
  width: `${column.width}px`,
1165
1279
  height: `${rowHeight}px`,
1166
1280
  }}
1167
- onClick={(e) => handleCellClick(slot.rowIndex, colIndex, e)}
1281
+ onMouseDown={(e) => handleCellMouseDown(slot.rowIndex, colIndex, e)}
1168
1282
  onDoubleClick={() => handleCellDoubleClick(slot.rowIndex, colIndex)}
1169
1283
  >
1170
1284
  {isEditing && state.editingCell
package/src/styles.ts CHANGED
@@ -100,6 +100,8 @@ export const gridStyles = `
100
100
  background-color: var(--gp-grid-bg);
101
101
  border: 1px solid var(--gp-grid-border);
102
102
  border-radius: 6px;
103
+ user-select: none;
104
+ -webkit-user-select: none;
103
105
  }
104
106
 
105
107
  .gp-grid-container:focus {
@@ -237,6 +239,8 @@ export const gridStyles = `
237
239
  overflow: hidden;
238
240
  text-overflow: ellipsis;
239
241
  white-space: nowrap;
242
+ user-select: none;
243
+ -webkit-user-select: none;
240
244
  }
241
245
 
242
246
  /* Alternating row colors */