@optilogic/core 1.0.0-beta.0

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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/dist/index.cjs +6003 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +2310 -0
  6. package/dist/index.d.ts +2310 -0
  7. package/dist/index.js +5828 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/styles.css +96 -0
  10. package/dist/tailwind-preset.cjs +106 -0
  11. package/dist/tailwind-preset.cjs.map +1 -0
  12. package/dist/tailwind-preset.d.cts +23 -0
  13. package/dist/tailwind-preset.d.ts +23 -0
  14. package/dist/tailwind-preset.js +101 -0
  15. package/dist/tailwind-preset.js.map +1 -0
  16. package/package.json +154 -0
  17. package/src/components/accordion.tsx +187 -0
  18. package/src/components/alert-dialog.tsx +143 -0
  19. package/src/components/autocomplete.tsx +271 -0
  20. package/src/components/badge.tsx +62 -0
  21. package/src/components/button.tsx +85 -0
  22. package/src/components/calendar.tsx +235 -0
  23. package/src/components/card.tsx +94 -0
  24. package/src/components/checkbox.tsx +77 -0
  25. package/src/components/chip.tsx +77 -0
  26. package/src/components/confirmation-modal.tsx +195 -0
  27. package/src/components/context-menu.tsx +406 -0
  28. package/src/components/copy-button.tsx +84 -0
  29. package/src/components/data-grid/DataGrid.tsx +1027 -0
  30. package/src/components/data-grid/components/CellEditor.tsx +346 -0
  31. package/src/components/data-grid/components/FilterPopover.tsx +459 -0
  32. package/src/components/data-grid/components/HeaderCell.tsx +207 -0
  33. package/src/components/data-grid/components/index.ts +14 -0
  34. package/src/components/data-grid/hooks/index.ts +28 -0
  35. package/src/components/data-grid/hooks/useColumnResize.ts +378 -0
  36. package/src/components/data-grid/hooks/useDataGridState.ts +346 -0
  37. package/src/components/data-grid/hooks/useKeyboardNavigation.ts +361 -0
  38. package/src/components/data-grid/index.ts +71 -0
  39. package/src/components/data-grid/types.ts +478 -0
  40. package/src/components/data-grid/utils/dataProcessing.ts +277 -0
  41. package/src/components/data-grid/utils/index.ts +12 -0
  42. package/src/components/date-picker.tsx +366 -0
  43. package/src/components/dropdown-menu.tsx +230 -0
  44. package/src/components/icon-button.tsx +157 -0
  45. package/src/components/input.tsx +40 -0
  46. package/src/components/label.tsx +37 -0
  47. package/src/components/loading-spinner.tsx +113 -0
  48. package/src/components/modal.tsx +207 -0
  49. package/src/components/popover.tsx +62 -0
  50. package/src/components/progress.tsx +41 -0
  51. package/src/components/resizable-panel.tsx +434 -0
  52. package/src/components/resize-handle.tsx +187 -0
  53. package/src/components/select.tsx +160 -0
  54. package/src/components/separator.tsx +50 -0
  55. package/src/components/skeleton.tsx +37 -0
  56. package/src/components/switch.tsx +59 -0
  57. package/src/components/table.tsx +136 -0
  58. package/src/components/tabs.tsx +102 -0
  59. package/src/components/textarea.tsx +36 -0
  60. package/src/components/theme-picker.tsx +245 -0
  61. package/src/components/toaster.tsx +84 -0
  62. package/src/components/tooltip.tsx +199 -0
  63. package/src/index.ts +318 -0
  64. package/src/styles.css +96 -0
  65. package/src/tailwind-preset.ts +129 -0
  66. package/src/theme/index.ts +41 -0
  67. package/src/theme/presets.ts +502 -0
  68. package/src/theme/types.ts +164 -0
  69. package/src/theme/utils.ts +309 -0
  70. package/src/utils/cn.ts +14 -0
@@ -0,0 +1,459 @@
1
+ /**
2
+ * FilterPopover Component
3
+ *
4
+ * Renders filter UI based on column filter type:
5
+ * - Text: Input with operator selection
6
+ * - Number: Input with comparison operators
7
+ * - Date: DatePicker with calendar and range support
8
+ * - Select: Multi-select from predefined options
9
+ * - Boolean: Checkbox toggle
10
+ */
11
+
12
+ import * as React from "react";
13
+ import { format, parseISO, isValid } from "date-fns";
14
+ import { cn } from "../../../utils/cn";
15
+ import { Button } from "../../button";
16
+ import { Input } from "../../input";
17
+ import { Label } from "../../label";
18
+ import { Checkbox } from "../../checkbox";
19
+ import { Switch } from "../../switch";
20
+ import { DatePicker } from "../../date-picker";
21
+ import {
22
+ Select,
23
+ SelectContent,
24
+ SelectItem,
25
+ SelectTrigger,
26
+ SelectValue,
27
+ } from "../../select";
28
+ import type {
29
+ ColumnDef,
30
+ FilterConfig,
31
+ FilterType,
32
+ TextFilterOperator,
33
+ NumberFilterOperator,
34
+ FilterOperator,
35
+ CellValue,
36
+ FilterValue,
37
+ } from "../types";
38
+
39
+ export interface FilterPopoverProps<T = Record<string, CellValue>> {
40
+ /** Column definition */
41
+ column: ColumnDef<T>;
42
+ /** Current filter value */
43
+ filter?: FilterConfig;
44
+ /** Callback when filter changes */
45
+ onFilterChange: (filter: FilterConfig | null) => void;
46
+ }
47
+
48
+ /** Text filter operators */
49
+ const TEXT_OPERATORS: { value: TextFilterOperator; label: string }[] = [
50
+ { value: "contains", label: "Contains" },
51
+ { value: "notContains", label: "Does not contain" },
52
+ { value: "equals", label: "Equals" },
53
+ { value: "notEquals", label: "Does not equal" },
54
+ { value: "startsWith", label: "Starts with" },
55
+ { value: "endsWith", label: "Ends with" },
56
+ { value: "isEmpty", label: "Is empty" },
57
+ { value: "isNotEmpty", label: "Is not empty" },
58
+ ];
59
+
60
+ /** Number filter operators */
61
+ const NUMBER_OPERATORS: { value: NumberFilterOperator; label: string }[] = [
62
+ { value: "equals", label: "Equals" },
63
+ { value: "notEquals", label: "Does not equal" },
64
+ { value: "gt", label: "Greater than" },
65
+ { value: "gte", label: "Greater than or equal" },
66
+ { value: "lt", label: "Less than" },
67
+ { value: "lte", label: "Less than or equal" },
68
+ { value: "between", label: "Between" },
69
+ { value: "isEmpty", label: "Is empty" },
70
+ { value: "isNotEmpty", label: "Is not empty" },
71
+ ];
72
+
73
+ /** Date filter operators */
74
+ const DATE_OPERATORS: { value: string; label: string }[] = [
75
+ { value: "equals", label: "Equals" },
76
+ { value: "notEquals", label: "Does not equal" },
77
+ { value: "before", label: "Before" },
78
+ { value: "after", label: "After" },
79
+ { value: "between", label: "Between" },
80
+ { value: "isEmpty", label: "Is empty" },
81
+ { value: "isNotEmpty", label: "Is not empty" },
82
+ ];
83
+
84
+ /**
85
+ * Check if operator requires no value input
86
+ */
87
+ function isNoValueOperator(operator: string): boolean {
88
+ return operator === "isEmpty" || operator === "isNotEmpty";
89
+ }
90
+
91
+ /**
92
+ * Check if operator requires two values (between)
93
+ */
94
+ function isBetweenOperator(operator: string): boolean {
95
+ return operator === "between";
96
+ }
97
+
98
+ /**
99
+ * FilterPopover Component
100
+ */
101
+ export function FilterPopover<T = Record<string, CellValue>>({
102
+ column,
103
+ filter,
104
+ onFilterChange,
105
+ }: FilterPopoverProps<T>) {
106
+ const filterType = column.filterType || "text";
107
+
108
+ const [operator, setOperator] = React.useState<FilterOperator>(
109
+ filter?.operator || getDefaultOperator(filterType)
110
+ );
111
+ const [value, setValue] = React.useState<FilterValue>(filter?.value ?? "");
112
+ const [valueTo, setValueTo] = React.useState<FilterValue>(filter?.valueTo ?? "");
113
+
114
+ function getDefaultOperator(type: FilterType): FilterOperator {
115
+ switch (type) {
116
+ case "text":
117
+ return "contains";
118
+ case "number":
119
+ return "equals";
120
+ case "date":
121
+ return "equals";
122
+ case "select":
123
+ return "equals";
124
+ case "boolean":
125
+ return "equals";
126
+ default:
127
+ return "contains";
128
+ }
129
+ }
130
+
131
+ const handleApply = () => {
132
+ if (isNoValueOperator(operator)) {
133
+ onFilterChange({
134
+ columnKey: column.key,
135
+ operator,
136
+ value: null,
137
+ });
138
+ return;
139
+ }
140
+
141
+ if (isBetweenOperator(operator)) {
142
+ if (value !== "" && valueTo !== "") {
143
+ onFilterChange({
144
+ columnKey: column.key,
145
+ operator,
146
+ value,
147
+ valueTo,
148
+ });
149
+ }
150
+ return;
151
+ }
152
+
153
+ if (value !== "" && value !== null && value !== undefined) {
154
+ onFilterChange({
155
+ columnKey: column.key,
156
+ operator,
157
+ value,
158
+ });
159
+ }
160
+ };
161
+
162
+ const handleClear = () => {
163
+ setOperator(getDefaultOperator(filterType));
164
+ setValue("");
165
+ setValueTo("");
166
+ onFilterChange(null);
167
+ };
168
+
169
+ /**
170
+ * Get operators based on filter type
171
+ */
172
+ const getOperators = () => {
173
+ switch (filterType) {
174
+ case "text":
175
+ return TEXT_OPERATORS;
176
+ case "number":
177
+ return NUMBER_OPERATORS;
178
+ case "date":
179
+ return DATE_OPERATORS;
180
+ default:
181
+ return TEXT_OPERATORS;
182
+ }
183
+ };
184
+
185
+ /**
186
+ * Render text filter
187
+ */
188
+ const renderTextFilter = () => (
189
+ <div className="space-y-3">
190
+ <div className="space-y-1.5">
191
+ <Label className="text-xs">Condition</Label>
192
+ <Select
193
+ value={operator}
194
+ onValueChange={(val) => setOperator(val as TextFilterOperator)}
195
+ >
196
+ <SelectTrigger className="h-8 text-sm">
197
+ <SelectValue />
198
+ </SelectTrigger>
199
+ <SelectContent>
200
+ {TEXT_OPERATORS.map((op) => (
201
+ <SelectItem key={op.value} value={op.value}>
202
+ {op.label}
203
+ </SelectItem>
204
+ ))}
205
+ </SelectContent>
206
+ </Select>
207
+ </div>
208
+
209
+ {!isNoValueOperator(operator) && (
210
+ <div className="space-y-1.5">
211
+ <Label className="text-xs">Value</Label>
212
+ <Input
213
+ type="text"
214
+ value={value != null && !Array.isArray(value) ? String(value) : ""}
215
+ onChange={(e) => setValue(e.target.value)}
216
+ placeholder="Enter value..."
217
+ className="h-8 text-sm"
218
+ onKeyDown={(e) => {
219
+ if (e.key === "Enter") {
220
+ e.preventDefault();
221
+ handleApply();
222
+ }
223
+ }}
224
+ />
225
+ </div>
226
+ )}
227
+ </div>
228
+ );
229
+
230
+ /**
231
+ * Render number filter
232
+ */
233
+ const renderNumberFilter = () => (
234
+ <div className="space-y-3">
235
+ <div className="space-y-1.5">
236
+ <Label className="text-xs">Condition</Label>
237
+ <Select
238
+ value={operator}
239
+ onValueChange={(val) => setOperator(val as NumberFilterOperator)}
240
+ >
241
+ <SelectTrigger className="h-8 text-sm">
242
+ <SelectValue />
243
+ </SelectTrigger>
244
+ <SelectContent>
245
+ {NUMBER_OPERATORS.map((op) => (
246
+ <SelectItem key={op.value} value={op.value}>
247
+ {op.label}
248
+ </SelectItem>
249
+ ))}
250
+ </SelectContent>
251
+ </Select>
252
+ </div>
253
+
254
+ {!isNoValueOperator(operator) && (
255
+ <>
256
+ <div className="space-y-1.5">
257
+ <Label className="text-xs">
258
+ {isBetweenOperator(operator) ? "From" : "Value"}
259
+ </Label>
260
+ <Input
261
+ type="number"
262
+ value={value != null && !Array.isArray(value) ? String(value) : ""}
263
+ onChange={(e) => setValue(e.target.value)}
264
+ placeholder="Enter number..."
265
+ className="h-8 text-sm"
266
+ onKeyDown={(e) => {
267
+ if (e.key === "Enter" && !isBetweenOperator(operator)) {
268
+ e.preventDefault();
269
+ handleApply();
270
+ }
271
+ }}
272
+ />
273
+ </div>
274
+
275
+ {isBetweenOperator(operator) && (
276
+ <div className="space-y-1.5">
277
+ <Label className="text-xs">To</Label>
278
+ <Input
279
+ type="number"
280
+ value={valueTo != null && !Array.isArray(valueTo) ? String(valueTo) : ""}
281
+ onChange={(e) => setValueTo(e.target.value)}
282
+ placeholder="Enter number..."
283
+ className="h-8 text-sm"
284
+ onKeyDown={(e) => {
285
+ if (e.key === "Enter") {
286
+ e.preventDefault();
287
+ handleApply();
288
+ }
289
+ }}
290
+ />
291
+ </div>
292
+ )}
293
+ </>
294
+ )}
295
+ </div>
296
+ );
297
+
298
+ const renderDateFilter = () => {
299
+ const dateValue = React.useMemo(() => {
300
+ if (!value) return undefined;
301
+ if (value instanceof Date) return value;
302
+ if (typeof value === "string") {
303
+ const parsed = parseISO(value);
304
+ return isValid(parsed) ? parsed : undefined;
305
+ }
306
+ return undefined;
307
+ }, [value]);
308
+
309
+ const dateToValue = React.useMemo(() => {
310
+ if (!valueTo) return undefined;
311
+ if (valueTo instanceof Date) return valueTo;
312
+ if (typeof valueTo === "string") {
313
+ const parsed = parseISO(valueTo);
314
+ return isValid(parsed) ? parsed : undefined;
315
+ }
316
+ return undefined;
317
+ }, [valueTo]);
318
+
319
+ const handleDateChange = (date: Date | undefined) => {
320
+ setValue(date ? format(date, "yyyy-MM-dd") : "");
321
+ };
322
+
323
+ const handleDateToChange = (date: Date | undefined) => {
324
+ setValueTo(date ? format(date, "yyyy-MM-dd") : "");
325
+ };
326
+
327
+ return (
328
+ <div className="space-y-3">
329
+ <div className="space-y-1.5">
330
+ <Label className="text-xs">Condition</Label>
331
+ <Select
332
+ value={operator}
333
+ onValueChange={(val) => setOperator(val as FilterOperator)}
334
+ >
335
+ <SelectTrigger className="h-8 text-sm">
336
+ <SelectValue />
337
+ </SelectTrigger>
338
+ <SelectContent>
339
+ {DATE_OPERATORS.map((op) => (
340
+ <SelectItem key={op.value} value={op.value}>
341
+ {op.label}
342
+ </SelectItem>
343
+ ))}
344
+ </SelectContent>
345
+ </Select>
346
+ </div>
347
+
348
+ {!isNoValueOperator(operator) && (
349
+ <>
350
+ <div className="space-y-1.5">
351
+ <Label className="text-xs">
352
+ {isBetweenOperator(operator) ? "From" : "Date"}
353
+ </Label>
354
+ <DatePicker
355
+ value={dateValue}
356
+ onChange={handleDateChange}
357
+ placeholder="Select date..."
358
+ className="h-8 text-sm"
359
+ />
360
+ </div>
361
+
362
+ {isBetweenOperator(operator) && (
363
+ <div className="space-y-1.5">
364
+ <Label className="text-xs">To</Label>
365
+ <DatePicker
366
+ value={dateToValue}
367
+ onChange={handleDateToChange}
368
+ placeholder="Select date..."
369
+ className="h-8 text-sm"
370
+ minDate={dateValue}
371
+ />
372
+ </div>
373
+ )}
374
+ </>
375
+ )}
376
+ </div>
377
+ );
378
+ };
379
+
380
+ const renderSelectFilter = () => {
381
+ const options = column.filterOptions || [];
382
+
383
+ return (
384
+ <div className="space-y-3">
385
+ <div className="space-y-1.5">
386
+ <Label className="text-xs">Select value</Label>
387
+ <Select value={value != null && !Array.isArray(value) ? String(value) : undefined} onValueChange={setValue}>
388
+ <SelectTrigger className="h-8 text-sm">
389
+ <SelectValue placeholder="Select..." />
390
+ </SelectTrigger>
391
+ <SelectContent>
392
+ {options.map((option) => (
393
+ <SelectItem key={option.value} value={option.value}>
394
+ {option.label}
395
+ </SelectItem>
396
+ ))}
397
+ </SelectContent>
398
+ </Select>
399
+ </div>
400
+ </div>
401
+ );
402
+ };
403
+
404
+ const renderBooleanFilter = () => (
405
+ <div className="space-y-3">
406
+ <div className="flex items-center justify-between">
407
+ <Label className="text-sm">{column.header}</Label>
408
+ <Switch
409
+ checked={value === true}
410
+ onCheckedChange={(checked) => setValue(checked)}
411
+ />
412
+ </div>
413
+ <p className="text-xs text-muted-foreground">
414
+ Filter rows where this value is {value ? "true" : "false"}
415
+ </p>
416
+ </div>
417
+ );
418
+
419
+ const renderFilter = () => {
420
+ switch (filterType) {
421
+ case "text":
422
+ return renderTextFilter();
423
+ case "number":
424
+ return renderNumberFilter();
425
+ case "date":
426
+ return renderDateFilter();
427
+ case "select":
428
+ return renderSelectFilter();
429
+ case "boolean":
430
+ return renderBooleanFilter();
431
+ default:
432
+ return renderTextFilter();
433
+ }
434
+ };
435
+
436
+ return (
437
+ <div className="p-3 space-y-4">
438
+ <div className="font-medium text-sm">
439
+ Filter: {typeof column.header === "string" ? column.header : "Column"}
440
+ </div>
441
+
442
+ {renderFilter()}
443
+
444
+ <div className="flex gap-2 pt-2 border-t border-border">
445
+ <Button
446
+ variant="outline"
447
+ size="sm"
448
+ onClick={handleClear}
449
+ className="flex-1"
450
+ >
451
+ Clear
452
+ </Button>
453
+ <Button size="sm" onClick={handleApply} className="flex-1">
454
+ Apply
455
+ </Button>
456
+ </div>
457
+ </div>
458
+ );
459
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * HeaderCell Component
3
+ *
4
+ * Renders a single header cell with:
5
+ * - Sort indicator and click-to-sort
6
+ * - Filter trigger (popover)
7
+ * - Resize handle (drag to resize)
8
+ */
9
+
10
+ import * as React from "react";
11
+ import { ChevronUp, ChevronDown, Filter, X, GripVertical } from "lucide-react";
12
+ import { cn } from "../../../utils/cn";
13
+ import { Button } from "../../button";
14
+ import { Popover, PopoverTrigger, PopoverContent } from "../../popover";
15
+ import type { ColumnDef, SortConfig, FilterConfig } from "../types";
16
+ import { FilterPopover } from "./FilterPopover";
17
+
18
+ export interface HeaderCellProps<T = any> {
19
+ /** Column definition */
20
+ column: ColumnDef<T>;
21
+ /** Column index */
22
+ columnIndex: number;
23
+ /** Current width of the column */
24
+ width: number;
25
+ /** Current sort config for this column (if sorted) */
26
+ sorting?: SortConfig;
27
+ /** Current filter config for this column (if filtered) */
28
+ filter?: FilterConfig;
29
+ /** Whether this column is resizable */
30
+ isResizable: boolean;
31
+ /** Callback when sort is toggled */
32
+ onSort?: () => void;
33
+ /** Callback when filter changes */
34
+ onFilterChange?: (filter: FilterConfig | null) => void;
35
+ /** Resize handle mouse down handler */
36
+ onResizeMouseDown?: (event: React.MouseEvent) => void;
37
+ /** Resize handle double click handler */
38
+ onResizeDoubleClick?: (event: React.MouseEvent) => void;
39
+ /** Whether currently resizing this column */
40
+ isResizing?: boolean;
41
+ }
42
+
43
+ /**
44
+ * HeaderCell Component
45
+ */
46
+ export function HeaderCell<T = any>({
47
+ column,
48
+ columnIndex,
49
+ width,
50
+ sorting,
51
+ filter,
52
+ isResizable,
53
+ onSort,
54
+ onFilterChange,
55
+ onResizeMouseDown,
56
+ onResizeDoubleClick,
57
+ isResizing,
58
+ }: HeaderCellProps<T>) {
59
+ const [filterOpen, setFilterOpen] = React.useState(false);
60
+
61
+ const isSorted = sorting?.field === column.key;
62
+ const isFiltered = !!filter;
63
+ const isSortable = column.sortable && onSort;
64
+ const isFilterable = column.filterable && onFilterChange;
65
+
66
+ const handleHeaderClick = () => {
67
+ if (isSortable) {
68
+ onSort();
69
+ }
70
+ };
71
+
72
+ const handleFilterChange = (newFilter: FilterConfig | null) => {
73
+ onFilterChange?.(newFilter);
74
+ if (!newFilter) {
75
+ setFilterOpen(false);
76
+ }
77
+ };
78
+
79
+ const handleClearFilter = (e: React.MouseEvent) => {
80
+ e.stopPropagation();
81
+ onFilterChange?.(null);
82
+ };
83
+
84
+ const renderSortIndicator = () => {
85
+ if (!column.sortable) return null;
86
+
87
+ const Icon = isSorted && sorting?.direction === "desc" ? ChevronDown : ChevronUp;
88
+
89
+ return (
90
+ <Icon
91
+ className={cn(
92
+ "w-4 h-4 flex-shrink-0 transition-opacity",
93
+ isSorted
94
+ ? "opacity-100 text-foreground"
95
+ : "opacity-30 hover:opacity-60 text-muted-foreground"
96
+ )}
97
+ />
98
+ );
99
+ };
100
+
101
+ return (
102
+ <div
103
+ className={cn(
104
+ "relative flex-shrink-0 border-r border-border last:border-r-0",
105
+ "bg-muted select-none",
106
+ isResizing && "bg-accent/20"
107
+ )}
108
+ style={{ width }}
109
+ >
110
+ <div
111
+ className={cn(
112
+ "flex items-center gap-1 px-3 py-2 h-full",
113
+ isResizable && "pr-6", // Extra padding to create space for resize handle
114
+ isSortable && "cursor-pointer hover:bg-accent/10",
115
+ column.align === "center" && "justify-center",
116
+ column.align === "right" && "justify-end"
117
+ )}
118
+ onClick={handleHeaderClick}
119
+ role={isSortable ? "button" : undefined}
120
+ tabIndex={isSortable ? 0 : undefined}
121
+ onKeyDown={(e) => {
122
+ if (isSortable && (e.key === "Enter" || e.key === " ")) {
123
+ e.preventDefault();
124
+ handleHeaderClick();
125
+ }
126
+ }}
127
+ >
128
+ <span className="text-sm font-medium truncate flex-1">
129
+ {column.header}
130
+ </span>
131
+
132
+ {renderSortIndicator()}
133
+
134
+ {isFilterable && (
135
+ <Popover open={filterOpen} onOpenChange={setFilterOpen}>
136
+ <PopoverTrigger asChild>
137
+ <Button
138
+ variant="ghost"
139
+ size="icon"
140
+ className={cn(
141
+ "h-6 w-6 p-0",
142
+ isFiltered && "text-primary"
143
+ )}
144
+ onClick={(e) => {
145
+ e.stopPropagation();
146
+ setFilterOpen(!filterOpen);
147
+ }}
148
+ aria-label={`Filter ${column.header}`}
149
+ >
150
+ <Filter className="w-3.5 h-3.5" />
151
+ </Button>
152
+ </PopoverTrigger>
153
+ <PopoverContent
154
+ align="start"
155
+ className="w-72 p-0"
156
+ onClick={(e) => e.stopPropagation()}
157
+ >
158
+ <FilterPopover
159
+ column={column}
160
+ filter={filter}
161
+ onFilterChange={handleFilterChange}
162
+ />
163
+ </PopoverContent>
164
+ </Popover>
165
+ )}
166
+
167
+ {isFiltered && !filterOpen && (
168
+ <Button
169
+ variant="ghost"
170
+ size="icon"
171
+ className="h-5 w-5 p-0 text-primary hover:text-destructive"
172
+ onClick={handleClearFilter}
173
+ aria-label="Clear filter"
174
+ >
175
+ <X className="w-3 h-3" />
176
+ </Button>
177
+ )}
178
+ </div>
179
+
180
+ {isResizable && (
181
+ <div
182
+ className={cn(
183
+ "absolute top-0 right-0 w-4 h-full cursor-col-resize z-10",
184
+ "flex items-center justify-center",
185
+ "hover:bg-accent/50 transition-colors",
186
+ "group",
187
+ isResizing && "bg-accent"
188
+ )}
189
+ onMouseDown={onResizeMouseDown}
190
+ onDoubleClick={onResizeDoubleClick}
191
+ role="separator"
192
+ aria-orientation="vertical"
193
+ aria-label={`Resize column ${column.header}`}
194
+ >
195
+ <GripVertical
196
+ className={cn(
197
+ "w-3 h-4 transition-colors",
198
+ "text-muted-foreground/40",
199
+ "group-hover:text-primary",
200
+ isResizing && "text-primary"
201
+ )}
202
+ />
203
+ </div>
204
+ )}
205
+ </div>
206
+ );
207
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * DataGrid Components
3
+ *
4
+ * Export all sub-components used by the DataGrid
5
+ */
6
+
7
+ export { HeaderCell } from "./HeaderCell";
8
+ export type { HeaderCellProps } from "./HeaderCell";
9
+
10
+ export { FilterPopover } from "./FilterPopover";
11
+ export type { FilterPopoverProps } from "./FilterPopover";
12
+
13
+ export { CellEditor } from "./CellEditor";
14
+ export type { CellEditorProps } from "./CellEditor";
@@ -0,0 +1,28 @@
1
+ /**
2
+ * DataGrid Hooks
3
+ *
4
+ * Export all hooks used by the DataGrid component
5
+ */
6
+
7
+ export { useDataGridState } from "./useDataGridState";
8
+ export type {
9
+ UseDataGridStateOptions,
10
+ UseDataGridStateReturn,
11
+ } from "./useDataGridState";
12
+
13
+ export { useKeyboardNavigation } from "./useKeyboardNavigation";
14
+ export type {
15
+ UseKeyboardNavigationOptions,
16
+ UseKeyboardNavigationReturn,
17
+ } from "./useKeyboardNavigation";
18
+
19
+ export {
20
+ useColumnResize,
21
+ useColumnResizeManager,
22
+ } from "./useColumnResize";
23
+ export type {
24
+ UseColumnResizeOptions,
25
+ UseColumnResizeReturn,
26
+ UseColumnResizeManagerOptions,
27
+ UseColumnResizeManagerReturn,
28
+ } from "./useColumnResize";