@alaarab/ogrid 1.2.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 (32) hide show
  1. package/README.md +56 -0
  2. package/dist/esm/ColumnChooser/ColumnChooser.js +22 -0
  3. package/dist/esm/ColumnChooser/ColumnChooser.module.css +139 -0
  4. package/dist/esm/ColumnHeaderFilter/ColumnHeaderFilter.js +50 -0
  5. package/dist/esm/ColumnHeaderFilter/ColumnHeaderFilter.module.css +332 -0
  6. package/dist/esm/ColumnHeaderFilter/MultiSelectFilterPopover.js +5 -0
  7. package/dist/esm/ColumnHeaderFilter/PeopleFilterPopover.js +19 -0
  8. package/dist/esm/ColumnHeaderFilter/TextFilterPopover.js +4 -0
  9. package/dist/esm/ColumnHeaderFilter/index.js +1 -0
  10. package/dist/esm/DataGridTable/DataGridTable.js +123 -0
  11. package/dist/esm/DataGridTable/DataGridTable.module.css +421 -0
  12. package/dist/esm/DataGridTable/GridContextMenu.js +26 -0
  13. package/dist/esm/DataGridTable/InlineCellEditor.js +22 -0
  14. package/dist/esm/DataGridTable/StatusBar.js +7 -0
  15. package/dist/esm/OGrid/OGrid.js +16 -0
  16. package/dist/esm/PaginationControls/PaginationControls.js +31 -0
  17. package/dist/esm/PaginationControls/PaginationControls.module.css +112 -0
  18. package/dist/esm/index.js +8 -0
  19. package/dist/types/ColumnChooser/ColumnChooser.d.ts +10 -0
  20. package/dist/types/ColumnHeaderFilter/ColumnHeaderFilter.d.ts +20 -0
  21. package/dist/types/ColumnHeaderFilter/MultiSelectFilterPopover.d.ts +14 -0
  22. package/dist/types/ColumnHeaderFilter/PeopleFilterPopover.d.ts +13 -0
  23. package/dist/types/ColumnHeaderFilter/TextFilterPopover.d.ts +8 -0
  24. package/dist/types/ColumnHeaderFilter/index.d.ts +1 -0
  25. package/dist/types/DataGridTable/DataGridTable.d.ts +7 -0
  26. package/dist/types/DataGridTable/GridContextMenu.d.ts +8 -0
  27. package/dist/types/DataGridTable/InlineCellEditor.d.ts +12 -0
  28. package/dist/types/DataGridTable/StatusBar.d.ts +8 -0
  29. package/dist/types/OGrid/OGrid.d.ts +5 -0
  30. package/dist/types/PaginationControls/PaginationControls.d.ts +11 -0
  31. package/dist/types/index.d.ts +6 -0
  32. package/package.json +56 -0
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # @alaarab/ogrid
2
+
3
+ [OGrid](https://github.com/alaarab/ogrid) data table with [Radix UI](https://www.radix-ui.com/) primitives and no Fluent/Material dependency. Sort, filter (text, multi-select, people), paginate, show/hide columns, spreadsheet-style selection (cell range, copy/paste, context menu), row selection, status bar, and CSV export. Use an in-memory array or plug in your own API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @alaarab/ogrid
9
+ ```
10
+
11
+ ### Peer Dependencies
12
+
13
+ ```
14
+ react ^17.0.0 || ^18.0.0 || ^19.0.0
15
+ react-dom ^17.0.0 || ^18.0.0 || ^19.0.0
16
+ ```
17
+
18
+ Radix UI primitives (`@radix-ui/react-checkbox`, `@radix-ui/react-popover`) are bundled as regular dependencies.
19
+
20
+ ## Quick Start
21
+
22
+ ```tsx
23
+ import { OGrid, type IColumnDef } from '@alaarab/ogrid';
24
+
25
+ const columns: IColumnDef<Product>[] = [
26
+ { columnId: 'name', name: 'Name', sortable: true, filterable: { type: 'text' }, renderCell: (item) => <span>{item.name}</span> },
27
+ { columnId: 'category', name: 'Category', sortable: true, filterable: { type: 'multiSelect', filterField: 'category' }, renderCell: (item) => <span>{item.category}</span> },
28
+ ];
29
+
30
+ <OGrid<Product>
31
+ data={products}
32
+ columns={columns}
33
+ getRowId={(r) => r.id}
34
+ entityLabelPlural="products"
35
+ />
36
+ ```
37
+
38
+ ## Components
39
+
40
+ - **`OGrid<T>`** -- Full table with column chooser, filters, and pagination (Radix/native implementation)
41
+ - **`DataGridTable<T>`** -- Lower-level grid for custom state management
42
+ - **`ColumnChooser`** -- Column visibility dropdown
43
+ - **`PaginationControls`** -- Pagination UI
44
+ - **`ColumnHeaderFilter`** -- Column header with sort/filter (used internally)
45
+
46
+ All core types, hooks, and utilities are re-exported from `@alaarab/ogrid-core`.
47
+
48
+ ## Storybook
49
+
50
+ ```bash
51
+ npm run storybook
52
+ ```
53
+
54
+ ## License
55
+
56
+ MIT
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as Popover from '@radix-ui/react-popover';
3
+ import * as Checkbox from '@radix-ui/react-checkbox';
4
+ import { useColumnChooserState } from '@alaarab/ogrid-core';
5
+ import styles from './ColumnChooser.module.css';
6
+ function TableSettingsIcon() {
7
+ return (_jsx("span", { className: styles.buttonIcon, "aria-hidden": true, children: "\u2699" }));
8
+ }
9
+ function ChevronDown() {
10
+ return _jsx("span", { className: styles.chevron, "aria-hidden": true, children: "\u25BC" });
11
+ }
12
+ function ChevronUp() {
13
+ return _jsx("span", { className: styles.chevron, "aria-hidden": true, children: "\u25B2" });
14
+ }
15
+ export const ColumnChooser = (props) => {
16
+ const { columns, visibleColumns, onVisibilityChange, className } = props;
17
+ const { open, setOpen, handleCheckboxChange: setColumnVisible, handleSelectAll, handleClearAll, visibleCount, totalCount, } = useColumnChooserState({ columns, visibleColumns, onVisibilityChange });
18
+ const handleCheckboxChange = (columnKey) => (checked) => {
19
+ setColumnVisible(columnKey)(checked === true);
20
+ };
21
+ return (_jsx("div", { className: `${styles.container} ${className || ''}`, children: _jsxs(Popover.Root, { open: open, onOpenChange: setOpen, children: [_jsx(Popover.Trigger, { asChild: true, children: _jsxs("button", { type: "button", className: styles.triggerButton, "aria-expanded": open, "aria-haspopup": "listbox", children: [_jsx(TableSettingsIcon, {}), _jsxs("span", { children: ["Column Visibility (", visibleCount, " of ", totalCount, ")"] }), open ? _jsx(ChevronUp, {}) : _jsx(ChevronDown, {})] }) }), _jsx(Popover.Portal, { children: _jsxs(Popover.Content, { className: styles.dropdown, sideOffset: 4, align: "end", onOpenAutoFocus: (e) => e.preventDefault(), children: [_jsxs("div", { className: styles.header, children: ["Select Columns (", visibleCount, " of ", totalCount, ")"] }), _jsx("div", { className: styles.optionsList, children: columns.map((column) => (_jsxs("div", { className: styles.optionItem, children: [_jsx(Checkbox.Root, { id: `col-${column.columnId}`, checked: visibleColumns.has(column.columnId), onCheckedChange: handleCheckboxChange(column.columnId), disabled: column.required === true, className: styles.checkbox, children: _jsx(Checkbox.Indicator, { className: styles.checkboxIndicator, children: "\u2713" }) }), _jsx("label", { htmlFor: `col-${column.columnId}`, style: { marginLeft: 8, cursor: 'pointer' }, children: column.name })] }, column.columnId))) }), _jsxs("div", { className: styles.actions, children: [_jsx("button", { type: "button", className: styles.clearButton, onClick: handleClearAll, children: "Clear All" }), _jsx("button", { type: "button", className: styles.selectAllButton, onClick: handleSelectAll, children: "Select All" })] })] }) })] }) }));
22
+ };
@@ -0,0 +1,139 @@
1
+ .container {
2
+ position: relative;
3
+ display: inline-flex;
4
+ }
5
+
6
+ .triggerButton {
7
+ display: inline-flex;
8
+ align-items: center;
9
+ gap: 6px;
10
+ padding: 6px 12px;
11
+ border: 1px solid var(--ogrid-border, #ccc);
12
+ border-radius: 6px;
13
+ background: var(--ogrid-bg, #fff);
14
+ cursor: pointer;
15
+ font-size: 13px;
16
+ font-weight: 600;
17
+ color: var(--ogrid-fg, #333);
18
+ transition: background 0.15s, border-color 0.15s;
19
+ }
20
+ .triggerButton:hover {
21
+ background: var(--ogrid-bg-hover, #f5f5f5);
22
+ border-color: var(--ogrid-border-hover, #999);
23
+ }
24
+ .triggerButton[data-state=open] {
25
+ border-color: var(--ogrid-primary, #0066cc);
26
+ }
27
+
28
+ .buttonIcon {
29
+ font-size: 16px;
30
+ line-height: 1;
31
+ }
32
+
33
+ .chevron {
34
+ font-size: 12px;
35
+ color: var(--ogrid-muted, #888);
36
+ }
37
+
38
+ .dropdown {
39
+ min-width: 220px;
40
+ background: var(--ogrid-bg, #fff);
41
+ border-radius: 6px;
42
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
43
+ border: 1px solid var(--ogrid-border, #e0e0e0);
44
+ display: flex;
45
+ flex-direction: column;
46
+ padding: 0;
47
+ }
48
+
49
+ .header {
50
+ padding: 8px 12px;
51
+ border-bottom: 1px solid var(--ogrid-border, #e5e5e5);
52
+ font-weight: 600;
53
+ font-size: 13px;
54
+ color: var(--ogrid-fg, #333);
55
+ background: var(--ogrid-bg-subtle, #f5f5f5);
56
+ }
57
+
58
+ .optionsList {
59
+ max-height: 320px;
60
+ overflow-y: auto;
61
+ padding: 0;
62
+ }
63
+
64
+ .optionItem {
65
+ padding: 4px 12px;
66
+ display: flex;
67
+ align-items: center;
68
+ min-height: 32px;
69
+ }
70
+ .optionItem:hover {
71
+ background: var(--ogrid-bg-hover, #f0f0f0);
72
+ }
73
+
74
+ .checkbox {
75
+ width: 16px;
76
+ height: 16px;
77
+ border: 1px solid var(--ogrid-border, #888);
78
+ border-radius: 3px;
79
+ background: var(--ogrid-bg, #fff);
80
+ cursor: pointer;
81
+ display: inline-flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ flex-shrink: 0;
85
+ }
86
+ .checkbox[data-state=checked] {
87
+ background: var(--ogrid-primary, #0066cc);
88
+ border-color: var(--ogrid-primary, #0066cc);
89
+ }
90
+ .checkbox[data-state=indeterminate] {
91
+ background: var(--ogrid-primary, #0066cc);
92
+ border-color: var(--ogrid-primary, #0066cc);
93
+ }
94
+
95
+ .checkboxIndicator {
96
+ color: var(--ogrid-primary-fg, #fff);
97
+ font-size: 12px;
98
+ line-height: 1;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ }
103
+
104
+ .actions {
105
+ display: flex;
106
+ justify-content: flex-end;
107
+ gap: 8px;
108
+ padding: 8px 12px;
109
+ border-top: 1px solid var(--ogrid-border, #e5e5e5);
110
+ background: var(--ogrid-bg-subtle, #f5f5f5);
111
+ }
112
+
113
+ .clearButton {
114
+ padding: 6px 12px;
115
+ border: 1px solid var(--ogrid-border, #ccc);
116
+ border-radius: 4px;
117
+ background: var(--ogrid-bg, #fff);
118
+ color: var(--ogrid-muted, #666);
119
+ font-size: 12px;
120
+ cursor: pointer;
121
+ }
122
+ .clearButton:hover {
123
+ background: var(--ogrid-bg-hover, #f5f5f5);
124
+ color: var(--ogrid-fg, #333);
125
+ }
126
+
127
+ .selectAllButton {
128
+ padding: 6px 16px;
129
+ border: none;
130
+ border-radius: 4px;
131
+ background: var(--ogrid-primary, #0066cc);
132
+ color: var(--ogrid-primary-fg, #fff);
133
+ font-size: 12px;
134
+ font-weight: 600;
135
+ cursor: pointer;
136
+ }
137
+ .selectAllButton:hover {
138
+ background: var(--ogrid-primary-hover, #0052a3);
139
+ }
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import * as Popover from '@radix-ui/react-popover';
4
+ import { useColumnHeaderFilterState } from '@alaarab/ogrid-core';
5
+ import { TextFilterPopover } from './TextFilterPopover';
6
+ import { MultiSelectFilterPopover } from './MultiSelectFilterPopover';
7
+ import { PeopleFilterPopover } from './PeopleFilterPopover';
8
+ import styles from './ColumnHeaderFilter.module.css';
9
+ function SortIcon({ isSorted, isDesc }) {
10
+ if (isSorted)
11
+ return _jsx("span", { "aria-hidden": true, children: isDesc ? '\u2193' : '\u2191' });
12
+ return _jsx("span", { "aria-hidden": true, children: '\u21C5' });
13
+ }
14
+ function FilterIcon() {
15
+ return _jsx("span", { "aria-hidden": true, children: '\u25BE' });
16
+ }
17
+ export const ColumnHeaderFilter = React.memo((props) => {
18
+ const { columnName, filterType, isSorted = false, isSortedDescending = false, onSort, selectedValues, onFilterChange, options = [], isLoadingOptions = false, textValue = '', onTextChange, selectedUser, onUserChange, peopleSearch, } = props;
19
+ const state = useColumnHeaderFilterState({
20
+ filterType,
21
+ isSorted,
22
+ isSortedDescending,
23
+ onSort,
24
+ selectedValues,
25
+ onFilterChange,
26
+ options,
27
+ isLoadingOptions,
28
+ textValue,
29
+ onTextChange,
30
+ selectedUser,
31
+ onUserChange,
32
+ peopleSearch,
33
+ });
34
+ const { headerRef, popoverRef, peopleInputRef, isFilterOpen, setFilterOpen, tempSelected, tempTextValue, setTempTextValue, searchText, setSearchText, filteredOptions, peopleSuggestions, isPeopleLoading, peopleSearchText, setPeopleSearchText, hasActiveFilter, handlers, } = state;
35
+ const safeOptions = options ?? [];
36
+ const renderPopoverContent = () => {
37
+ if (filterType === 'multiSelect') {
38
+ return (_jsx(MultiSelectFilterPopover, { searchText: searchText, onSearchChange: setSearchText, options: safeOptions, filteredOptions: filteredOptions, selected: tempSelected, onOptionToggle: handlers.handleCheckboxChange, onSelectAll: handlers.handleSelectAll, onClearSelection: handlers.handleClearSelection, onApply: handlers.handleApplyMultiSelect, isLoading: isLoadingOptions }));
39
+ }
40
+ if (filterType === 'text') {
41
+ return (_jsx(TextFilterPopover, { value: tempTextValue, onValueChange: setTempTextValue, onApply: handlers.handleTextApply, onClear: handlers.handleTextClear }));
42
+ }
43
+ if (filterType === 'people') {
44
+ return (_jsx(PeopleFilterPopover, { selectedUser: selectedUser, searchText: peopleSearchText, onSearchChange: setPeopleSearchText, suggestions: peopleSuggestions, isLoading: isPeopleLoading, onUserSelect: handlers.handleUserSelect, onClearUser: handlers.handleClearUser, inputRef: peopleInputRef }));
45
+ }
46
+ return null;
47
+ };
48
+ return (_jsxs("div", { className: styles.columnHeader, ref: headerRef, children: [_jsx("div", { className: styles.headerContent, children: _jsx("span", { className: styles.columnName, title: columnName, "data-header-label": true, children: columnName }) }), _jsxs("div", { className: styles.headerActions, children: [onSort && (_jsx("button", { type: "button", className: `${styles.sortIcon} ${isSorted ? styles.sortActive : ''}`, onClick: handlers.handleSortClick, "aria-label": `Sort by ${columnName}`, title: isSorted ? (isSortedDescending ? 'Sorted descending' : 'Sorted ascending') : 'Sort', children: _jsx(SortIcon, { isSorted: isSorted, isDesc: isSortedDescending }) })), filterType !== 'none' && (_jsxs(Popover.Root, { open: isFilterOpen, onOpenChange: setFilterOpen, children: [_jsx(Popover.Trigger, { asChild: true, children: _jsxs("button", { type: "button", className: `${styles.filterIcon} ${hasActiveFilter ? styles.filterActive : ''} ${isFilterOpen ? styles.filterOpen : ''}`, onClick: handlers.handleFilterIconClick, "aria-label": `Filter ${columnName}`, title: `Filter ${columnName}`, children: [_jsx(FilterIcon, {}), hasActiveFilter && _jsx("span", { className: styles.filterBadge })] }) }), _jsx(Popover.Portal, { children: _jsxs(Popover.Content, { ref: popoverRef, className: styles.popoverContent, sideOffset: 4, align: "start", onOpenAutoFocus: (e) => e.preventDefault(), children: [_jsxs("div", { className: styles.popoverHeader, children: ["Filter: ", columnName] }), renderPopoverContent()] }) })] }))] })] }));
49
+ });
50
+ ColumnHeaderFilter.displayName = 'ColumnHeaderFilter';
@@ -0,0 +1,332 @@
1
+ .columnHeader {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 4px;
5
+ width: 100%;
6
+ max-width: 100%;
7
+ min-width: 0;
8
+ position: relative;
9
+ box-sizing: border-box;
10
+ overflow: hidden;
11
+ }
12
+
13
+ .headerContent {
14
+ display: flex;
15
+ align-items: center;
16
+ flex: 1;
17
+ min-width: 0;
18
+ overflow: hidden;
19
+ }
20
+
21
+ .columnName {
22
+ display: block;
23
+ min-width: 0;
24
+ max-width: 100%;
25
+ overflow: hidden;
26
+ text-overflow: ellipsis;
27
+ white-space: nowrap;
28
+ font-weight: 600;
29
+ font-size: 14px;
30
+ color: var(--ogrid-fg, #242424);
31
+ }
32
+
33
+ .headerActions {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 2px;
37
+ margin-left: auto;
38
+ flex-shrink: 0;
39
+ }
40
+
41
+ .sortIcon {
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ width: 24px;
46
+ height: 24px;
47
+ padding: 4px;
48
+ border: none;
49
+ border-radius: 4px;
50
+ background: transparent;
51
+ color: var(--ogrid-muted, #616161);
52
+ cursor: pointer;
53
+ flex-shrink: 0;
54
+ font-size: 14px;
55
+ }
56
+ .sortIcon:hover {
57
+ background: var(--ogrid-bg-hover, #f5f5f5);
58
+ color: var(--ogrid-fg, #424242);
59
+ }
60
+ .sortIcon.sortActive {
61
+ background: var(--ogrid-bg-selected, #e0e0e0);
62
+ color: var(--ogrid-fg, #333);
63
+ }
64
+
65
+ .filterIcon {
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ width: 24px;
70
+ height: 24px;
71
+ padding: 4px;
72
+ border: none;
73
+ border-radius: 4px;
74
+ background: transparent;
75
+ color: var(--ogrid-muted, #616161);
76
+ cursor: pointer;
77
+ flex-shrink: 0;
78
+ position: relative;
79
+ font-size: 14px;
80
+ }
81
+ .filterIcon:hover {
82
+ background: var(--ogrid-bg-hover, #f5f5f5);
83
+ color: var(--ogrid-fg, #424242);
84
+ }
85
+ .filterIcon.filterActive {
86
+ background: var(--ogrid-bg-selected, #e0e0e0);
87
+ color: var(--ogrid-primary, #0066cc);
88
+ }
89
+ .filterIcon.filterOpen {
90
+ background: var(--ogrid-bg-selected, #e8e8e8);
91
+ }
92
+
93
+ .filterBadge {
94
+ position: absolute;
95
+ top: 2px;
96
+ right: 2px;
97
+ width: 6px;
98
+ height: 6px;
99
+ background: var(--ogrid-primary, #0066cc);
100
+ border-radius: 50%;
101
+ border: 1px solid var(--ogrid-bg, #fff);
102
+ }
103
+
104
+ .popoverContent {
105
+ z-index: 1000;
106
+ min-width: 280px;
107
+ max-width: 320px;
108
+ background: var(--ogrid-bg, #fff);
109
+ border: 1px solid var(--ogrid-border, #d1d1d1);
110
+ border-radius: 8px;
111
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.14);
112
+ overflow: hidden;
113
+ }
114
+
115
+ .popoverHeader {
116
+ padding: 10px 14px;
117
+ font-size: 12px;
118
+ font-weight: 600;
119
+ color: var(--ogrid-muted, #616161);
120
+ border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
121
+ background: var(--ogrid-bg-subtle, #fafafa);
122
+ }
123
+
124
+ .popoverSearch {
125
+ padding: 10px 12px;
126
+ border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
127
+ }
128
+
129
+ .searchInput {
130
+ width: 100%;
131
+ padding: 6px 10px;
132
+ border: 1px solid var(--ogrid-border, #d1d1d1);
133
+ border-radius: 4px;
134
+ font-size: 14px;
135
+ box-sizing: border-box;
136
+ }
137
+ .searchInput:focus {
138
+ outline: none;
139
+ border-color: var(--ogrid-primary, #0066cc);
140
+ }
141
+
142
+ .nativeInputWrapper {
143
+ display: flex;
144
+ align-items: center;
145
+ gap: 8px;
146
+ width: 100%;
147
+ border: 1px solid var(--ogrid-border, #d1d1d1);
148
+ border-radius: 4px;
149
+ background: var(--ogrid-bg, #fff);
150
+ padding: 6px 12px;
151
+ min-height: 36px;
152
+ box-sizing: border-box;
153
+ }
154
+ .nativeInputWrapper:focus-within {
155
+ border-color: var(--ogrid-primary, #0066cc);
156
+ outline: none;
157
+ }
158
+
159
+ .nativeInput {
160
+ flex: 1;
161
+ min-width: 0;
162
+ border: none;
163
+ outline: none;
164
+ padding: 0;
165
+ font-size: 14px;
166
+ font-family: inherit;
167
+ background: transparent;
168
+ color: var(--ogrid-fg, #242424);
169
+ }
170
+ .nativeInput::placeholder {
171
+ color: var(--ogrid-muted, #707070);
172
+ }
173
+
174
+ .resultCount {
175
+ margin-top: 6px;
176
+ font-size: 11px;
177
+ color: var(--ogrid-muted, #616161);
178
+ }
179
+
180
+ .selectAllRow {
181
+ display: flex;
182
+ gap: 8px;
183
+ padding: 6px 12px;
184
+ border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
185
+ background: var(--ogrid-bg-subtle, #fafafa);
186
+ }
187
+
188
+ .selectAllButton {
189
+ background: none;
190
+ border: none;
191
+ color: var(--ogrid-primary, #0066cc);
192
+ font-size: 12px;
193
+ font-weight: 500;
194
+ cursor: pointer;
195
+ padding: 4px 8px;
196
+ border-radius: 4px;
197
+ }
198
+ .selectAllButton:hover {
199
+ background: var(--ogrid-bg-hover, #e8f4fc);
200
+ }
201
+
202
+ .popoverOptions {
203
+ overflow-y: auto;
204
+ max-height: 250px;
205
+ padding: 6px 0;
206
+ }
207
+
208
+ .popoverOption {
209
+ padding: 4px 12px;
210
+ display: flex;
211
+ align-items: center;
212
+ }
213
+ .popoverOption:hover {
214
+ background: var(--ogrid-bg-hover, #f5f5f5);
215
+ }
216
+
217
+ .filterCheckbox {
218
+ width: 16px;
219
+ height: 16px;
220
+ border: 1px solid var(--ogrid-border, #888);
221
+ border-radius: 3px;
222
+ background: var(--ogrid-bg, #fff);
223
+ cursor: pointer;
224
+ display: inline-flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ flex-shrink: 0;
228
+ }
229
+ .filterCheckbox[data-state=checked] {
230
+ background: var(--ogrid-primary, #0066cc);
231
+ border-color: var(--ogrid-primary, #0066cc);
232
+ color: var(--ogrid-primary-fg, #fff);
233
+ }
234
+
235
+ .personOption {
236
+ padding: 8px 12px;
237
+ cursor: pointer;
238
+ }
239
+ .personOption:hover {
240
+ background: var(--ogrid-bg-hover, #f5f5f5);
241
+ }
242
+
243
+ .loadingContainer,
244
+ .noResults {
245
+ padding: 20px;
246
+ text-align: center;
247
+ font-size: 13px;
248
+ color: var(--ogrid-muted, #666);
249
+ }
250
+
251
+ .selectedUserSection {
252
+ padding: 8px 12px;
253
+ border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
254
+ }
255
+
256
+ .selectedUserLabel {
257
+ font-size: 11px;
258
+ color: var(--ogrid-muted, #666);
259
+ margin-bottom: 4px;
260
+ }
261
+
262
+ .selectedUser {
263
+ display: flex;
264
+ align-items: center;
265
+ justify-content: space-between;
266
+ gap: 8px;
267
+ }
268
+
269
+ .userInfo {
270
+ display: flex;
271
+ align-items: center;
272
+ gap: 8px;
273
+ min-width: 0;
274
+ }
275
+
276
+ .userText {
277
+ min-width: 0;
278
+ font-size: 13px;
279
+ }
280
+ .userText .userSecondary {
281
+ font-size: 12px;
282
+ color: var(--ogrid-muted, #666);
283
+ }
284
+
285
+ .removeUserButton {
286
+ padding: 4px;
287
+ border: none;
288
+ background: transparent;
289
+ cursor: pointer;
290
+ color: var(--ogrid-muted, #666);
291
+ flex-shrink: 0;
292
+ }
293
+ .removeUserButton:hover {
294
+ color: var(--ogrid-fg, #333);
295
+ }
296
+
297
+ .popoverActions {
298
+ display: flex;
299
+ justify-content: flex-end;
300
+ gap: 8px;
301
+ padding: 8px 12px;
302
+ border-top: 1px solid var(--ogrid-border, #e0e0e0);
303
+ background: var(--ogrid-bg-subtle, #f5f5f5);
304
+ }
305
+
306
+ .clearButton {
307
+ padding: 6px 12px;
308
+ border: 1px solid var(--ogrid-border, #ccc);
309
+ border-radius: 4px;
310
+ background: var(--ogrid-bg, #fff);
311
+ color: var(--ogrid-muted, #666);
312
+ font-size: 12px;
313
+ cursor: pointer;
314
+ }
315
+ .clearButton:hover {
316
+ background: var(--ogrid-bg-hover, #f5f5f5);
317
+ color: var(--ogrid-fg, #333);
318
+ }
319
+
320
+ .applyButton {
321
+ padding: 6px 16px;
322
+ border: none;
323
+ border-radius: 4px;
324
+ background: var(--ogrid-primary, #0066cc);
325
+ color: var(--ogrid-primary-fg, #fff);
326
+ font-size: 12px;
327
+ font-weight: 600;
328
+ cursor: pointer;
329
+ }
330
+ .applyButton:hover {
331
+ background: var(--ogrid-primary-hover, #0052a3);
332
+ }
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import * as Checkbox from '@radix-ui/react-checkbox';
3
+ import styles from './ColumnHeaderFilter.module.css';
4
+ export const MultiSelectFilterPopover = ({ searchText, onSearchChange, options, filteredOptions, selected, onOptionToggle, onSelectAll, onClearSelection, onApply, isLoading, }) => (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.popoverSearch, children: [_jsx("input", { type: "text", className: styles.searchInput, placeholder: "Search...", value: searchText, onChange: (e) => onSearchChange(e.target.value), autoComplete: "off" }), _jsxs("div", { className: styles.resultCount, children: [filteredOptions.length, " of ", options.length, " options"] })] }), _jsxs("div", { className: styles.selectAllRow, children: [_jsxs("button", { type: "button", className: styles.selectAllButton, onClick: onSelectAll, children: ["Select All (", filteredOptions.length, ")"] }), _jsx("button", { type: "button", className: styles.selectAllButton, onClick: onClearSelection, children: "Clear" })] }), _jsx("div", { className: styles.popoverOptions, children: isLoading ? (_jsx("div", { className: styles.loadingContainer, children: "Loading..." })) : filteredOptions.length === 0 ? (_jsx("div", { className: styles.noResults, children: "No options found" })) : (filteredOptions.map((option) => (_jsxs("div", { className: styles.popoverOption, children: [_jsx(Checkbox.Root, { checked: selected.has(option), onCheckedChange: (c) => onOptionToggle(option, c === true), className: styles.filterCheckbox, children: _jsx(Checkbox.Indicator, { children: "\u2713" }) }), _jsx("label", { style: { marginLeft: 8, cursor: 'pointer' }, children: option })] }, option)))) }), _jsxs("div", { className: styles.popoverActions, children: [_jsx("button", { type: "button", className: styles.clearButton, onClick: onClearSelection, children: "Clear" }), _jsx("button", { type: "button", className: styles.applyButton, onClick: onApply, children: "Apply" })] })] }));
5
+ MultiSelectFilterPopover.displayName = 'MultiSelectFilterPopover';
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import styles from './ColumnHeaderFilter.module.css';
3
+ function UserAvatar({ user, size = 32 }) {
4
+ if (user.photo) {
5
+ return _jsx("img", { src: user.photo, alt: "", width: size, height: size, style: { borderRadius: '50%' } });
6
+ }
7
+ return (_jsx("span", { style: {
8
+ width: size,
9
+ height: size,
10
+ borderRadius: '50%',
11
+ background: '#e0e0e0',
12
+ display: 'flex',
13
+ alignItems: 'center',
14
+ justifyContent: 'center',
15
+ fontSize: 12,
16
+ }, children: user.displayName?.charAt(0) ?? '?' }));
17
+ }
18
+ export const PeopleFilterPopover = ({ selectedUser, searchText, onSearchChange, suggestions, isLoading, onUserSelect, onClearUser, inputRef, }) => (_jsxs(_Fragment, { children: [selectedUser && (_jsxs("div", { className: styles.selectedUserSection, children: [_jsx("div", { className: styles.selectedUserLabel, children: "Currently filtered by:" }), _jsxs("div", { className: styles.selectedUser, children: [_jsxs("div", { className: styles.userInfo, children: [_jsx(UserAvatar, { user: selectedUser }), _jsxs("div", { className: styles.userText, children: [_jsx("div", { children: selectedUser.displayName }), _jsx("div", { className: styles.userSecondary, children: selectedUser.email })] })] }), _jsx("button", { type: "button", className: styles.removeUserButton, onClick: onClearUser, "aria-label": "Remove filter", children: "\u2715" })] })] })), _jsx("div", { className: styles.popoverSearch, children: _jsx("div", { className: styles.nativeInputWrapper, children: _jsx("input", { ref: inputRef, type: "text", className: styles.nativeInput, placeholder: "Search for a person...", value: searchText, onChange: (e) => onSearchChange(e.target.value), autoComplete: "off" }) }) }), _jsx("div", { className: styles.popoverOptions, children: isLoading && searchText.trim() ? (_jsx("div", { className: styles.loadingContainer, children: "Searching..." })) : suggestions.length === 0 && searchText.trim() ? (_jsx("div", { className: styles.noResults, children: "No results found" })) : searchText.trim() ? (suggestions.map((user) => (_jsx("div", { className: styles.personOption, onClick: () => onUserSelect(user), onKeyDown: (e) => e.key === 'Enter' && onUserSelect(user), role: "button", tabIndex: 0, children: _jsxs("div", { className: styles.userInfo, children: [_jsx(UserAvatar, { user: user }), _jsxs("div", { className: styles.userText, children: [_jsx("div", { children: user.displayName }), _jsx("div", { className: styles.userSecondary, children: user.email })] })] }) }, user.id ?? user.email ?? user.displayName ?? '')))) : (_jsx("div", { className: styles.noResults, children: "Type to search..." })) }), selectedUser && (_jsx("div", { className: styles.popoverActions, children: _jsx("button", { type: "button", className: styles.clearButton, onClick: onClearUser, children: "Clear Filter" }) }))] }));
19
+ PeopleFilterPopover.displayName = 'PeopleFilterPopover';
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import styles from './ColumnHeaderFilter.module.css';
3
+ export const TextFilterPopover = ({ value, onValueChange, onApply, onClear, }) => (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.popoverSearch, children: _jsx("input", { type: "text", className: styles.searchInput, placeholder: "Enter search term...", value: value, onChange: (e) => onValueChange(e.target.value), onKeyDown: (e) => e.key === 'Enter' && (e.preventDefault(), onApply()), autoComplete: "off" }) }), _jsxs("div", { className: styles.popoverActions, children: [_jsx("button", { type: "button", className: styles.clearButton, onClick: onClear, disabled: !value, children: "Clear" }), _jsx("button", { type: "button", className: styles.applyButton, onClick: onApply, children: "Apply" })] })] }));
4
+ TextFilterPopover.displayName = 'TextFilterPopover';
@@ -0,0 +1 @@
1
+ export { ColumnHeaderFilter } from './ColumnHeaderFilter';