@alaarab/ogrid-fluent 1.0.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.
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # @alaarab/ogrid-fluent
2
+
3
+ [OGrid](https://github.com/alaarab/ogrid) data table for [Fluent UI](https://react.fluentui.dev/). Sort, filter (text, multi-select, people), paginate, show/hide columns, and export to CSV. Use an in-memory array or plug in your own API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @alaarab/ogrid-fluent
9
+ ```
10
+
11
+ ### Peer Dependencies
12
+
13
+ ```
14
+ @fluentui/react-components ^9.0.0
15
+ @fluentui/react-icons ^2.0.0
16
+ react ^17.0.0 || ^18.0.0
17
+ react-dom ^17.0.0 || ^18.0.0
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```tsx
23
+ import { OGrid, type IColumnDef } from '@alaarab/ogrid-fluent';
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 (Fluent UI 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` for convenience.
47
+
48
+ ## Storybook
49
+
50
+ ```bash
51
+ npm run storybook # port 6006
52
+ ```
53
+
54
+ ## License
55
+
56
+ MIT
@@ -0,0 +1,182 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useCallback, useRef, useEffect } from 'react';
3
+ import { Checkbox, makeStyles, tokens, mergeClasses, } from '@fluentui/react-components';
4
+ import { TableSettingsRegular, ChevronDownRegular, ChevronUpRegular } from '@fluentui/react-icons';
5
+ const useStyles = makeStyles({
6
+ container: {
7
+ position: 'relative',
8
+ display: 'inline-flex',
9
+ },
10
+ triggerButton: {
11
+ display: 'inline-flex',
12
+ alignItems: 'center',
13
+ gap: '6px',
14
+ padding: '6px 12px',
15
+ border: `1px solid ${tokens.colorNeutralStroke1}`,
16
+ borderRadius: tokens.borderRadiusMedium,
17
+ backgroundColor: tokens.colorNeutralBackground1,
18
+ cursor: 'pointer',
19
+ fontSize: tokens.fontSizeBase300,
20
+ fontWeight: tokens.fontWeightSemibold,
21
+ color: tokens.colorNeutralForeground1,
22
+ transitionDuration: '0.15s',
23
+ transitionProperty: 'all',
24
+ transitionTimingFunction: 'ease',
25
+ ':hover': {
26
+ backgroundColor: tokens.colorNeutralBackground1Hover,
27
+ border: `1px solid ${tokens.colorNeutralStroke1Hover}`,
28
+ },
29
+ },
30
+ triggerButtonOpen: {
31
+ border: `1px solid ${tokens.colorBrandStroke1}`,
32
+ },
33
+ buttonIcon: {
34
+ fontSize: '16px',
35
+ },
36
+ chevron: {
37
+ fontSize: '12px',
38
+ color: tokens.colorNeutralForeground3,
39
+ },
40
+ dropdown: {
41
+ position: 'absolute',
42
+ top: 'calc(100% + 4px)',
43
+ right: '0',
44
+ zIndex: 10000,
45
+ minWidth: '220px',
46
+ backgroundColor: tokens.colorNeutralBackground1,
47
+ borderRadius: tokens.borderRadiusMedium,
48
+ boxShadow: tokens.shadow16,
49
+ border: `1px solid ${tokens.colorNeutralStroke1}`,
50
+ display: 'flex',
51
+ flexDirection: 'column',
52
+ },
53
+ header: {
54
+ padding: '8px 12px',
55
+ borderBottom: `1px solid ${tokens.colorNeutralStroke2}`,
56
+ fontWeight: tokens.fontWeightSemibold,
57
+ fontSize: tokens.fontSizeBase300,
58
+ color: tokens.colorNeutralForeground1,
59
+ backgroundColor: tokens.colorNeutralBackground2,
60
+ },
61
+ optionsList: {
62
+ maxHeight: '320px',
63
+ overflowY: 'auto',
64
+ padding: 0,
65
+ },
66
+ optionItem: {
67
+ padding: '4px 12px',
68
+ display: 'flex',
69
+ alignItems: 'center',
70
+ minHeight: '32px',
71
+ ':hover': {
72
+ backgroundColor: tokens.colorNeutralBackground1Hover,
73
+ },
74
+ },
75
+ actions: {
76
+ display: 'flex',
77
+ justifyContent: 'flex-end',
78
+ gap: '8px',
79
+ padding: '8px 12px',
80
+ borderTop: `1px solid ${tokens.colorNeutralStroke2}`,
81
+ backgroundColor: tokens.colorNeutralBackground2,
82
+ },
83
+ clearButton: {
84
+ padding: '6px 12px',
85
+ border: `1px solid ${tokens.colorNeutralStroke1}`,
86
+ borderRadius: tokens.borderRadiusSmall,
87
+ backgroundColor: tokens.colorNeutralBackground1,
88
+ color: tokens.colorNeutralForeground2,
89
+ fontSize: tokens.fontSizeBase200,
90
+ fontWeight: tokens.fontWeightRegular,
91
+ cursor: 'pointer',
92
+ transitionDuration: '0.15s',
93
+ transitionProperty: 'all',
94
+ ':hover': {
95
+ backgroundColor: tokens.colorNeutralBackground1Hover,
96
+ color: tokens.colorNeutralForeground1,
97
+ border: `1px solid ${tokens.colorNeutralStroke1Hover}`,
98
+ },
99
+ },
100
+ selectAllButton: {
101
+ padding: '6px 16px',
102
+ border: 'none',
103
+ borderRadius: tokens.borderRadiusSmall,
104
+ backgroundColor: tokens.colorBrandBackground,
105
+ color: tokens.colorNeutralForegroundOnBrand,
106
+ fontSize: tokens.fontSizeBase200,
107
+ fontWeight: tokens.fontWeightSemibold,
108
+ cursor: 'pointer',
109
+ transitionDuration: '0.15s',
110
+ transitionProperty: 'all',
111
+ ':hover': {
112
+ backgroundColor: tokens.colorBrandBackgroundHover,
113
+ },
114
+ },
115
+ });
116
+ export const ColumnChooser = (props) => {
117
+ const { columns, visibleColumns, onVisibilityChange, className } = props;
118
+ const classes = useStyles();
119
+ const [isOpen, setIsOpen] = useState(false);
120
+ const buttonRef = useRef(null);
121
+ const dropdownRef = useRef(null);
122
+ useEffect(() => {
123
+ if (!isOpen)
124
+ return;
125
+ const handleClickOutside = (event) => {
126
+ const target = event.target;
127
+ const isOutsideDropdown = dropdownRef.current && !dropdownRef.current.contains(target);
128
+ const isOutsideButton = buttonRef.current && !buttonRef.current.contains(target);
129
+ if (isOutsideDropdown && isOutsideButton) {
130
+ setIsOpen(false);
131
+ }
132
+ };
133
+ const timeoutId = setTimeout(() => {
134
+ document.addEventListener('mousedown', handleClickOutside);
135
+ }, 0);
136
+ return () => {
137
+ clearTimeout(timeoutId);
138
+ document.removeEventListener('mousedown', handleClickOutside);
139
+ };
140
+ }, [isOpen]);
141
+ useEffect(() => {
142
+ if (!isOpen)
143
+ return;
144
+ const handleKeyDown = (event) => {
145
+ if (event.key === 'Escape') {
146
+ event.preventDefault();
147
+ setIsOpen(false);
148
+ }
149
+ };
150
+ document.addEventListener('keydown', handleKeyDown, true);
151
+ return () => document.removeEventListener('keydown', handleKeyDown, true);
152
+ }, [isOpen]);
153
+ const handleToggle = useCallback(() => {
154
+ setIsOpen(prev => !prev);
155
+ }, []);
156
+ const handleCheckboxChange = useCallback((columnKey) => {
157
+ return (ev, data) => {
158
+ ev.stopPropagation();
159
+ onVisibilityChange(columnKey, data.checked === true);
160
+ };
161
+ }, [onVisibilityChange]);
162
+ const handleDropdownClick = useCallback((e) => {
163
+ e.stopPropagation();
164
+ }, []);
165
+ const handleSelectAll = useCallback(() => {
166
+ columns.forEach(col => {
167
+ if (!visibleColumns.has(col.columnId)) {
168
+ onVisibilityChange(col.columnId, true);
169
+ }
170
+ });
171
+ }, [columns, visibleColumns, onVisibilityChange]);
172
+ const handleClearAll = useCallback(() => {
173
+ columns.forEach(col => {
174
+ if (!col.required && visibleColumns.has(col.columnId)) {
175
+ onVisibilityChange(col.columnId, false);
176
+ }
177
+ });
178
+ }, [columns, visibleColumns, onVisibilityChange]);
179
+ const visibleCount = visibleColumns.size;
180
+ const totalCount = columns.length;
181
+ return (_jsxs("div", { className: `${classes.container} ${className || ''}`, children: [_jsxs("button", { type: "button", ref: buttonRef, className: mergeClasses(classes.triggerButton, isOpen && classes.triggerButtonOpen), onClick: handleToggle, "aria-expanded": isOpen, "aria-haspopup": "listbox", children: [_jsx(TableSettingsRegular, { className: classes.buttonIcon }), _jsxs("span", { children: ["Column Visibility (", visibleCount, " of ", totalCount, ")"] }), isOpen ? _jsx(ChevronUpRegular, { className: classes.chevron }) : _jsx(ChevronDownRegular, { className: classes.chevron })] }), isOpen && (_jsxs("div", { className: classes.dropdown, ref: dropdownRef, onClick: handleDropdownClick, children: [_jsxs("div", { className: classes.header, children: ["Select Columns (", visibleCount, " of ", totalCount, ")"] }), _jsx("div", { className: classes.optionsList, children: columns.map(column => (_jsx("div", { className: classes.optionItem, children: _jsx(Checkbox, { label: column.name, checked: visibleColumns.has(column.columnId), onChange: handleCheckboxChange(column.columnId) }) }, column.columnId))) }), _jsxs("div", { className: classes.actions, children: [_jsx("button", { type: "button", className: classes.clearButton, onClick: handleClearAll, children: "Clear All" }), _jsx("button", { type: "button", className: classes.selectAllButton, onClick: handleSelectAll, children: "Select All" })] })] }))] }));
182
+ };
@@ -0,0 +1,15 @@
1
+ .columnChooser {
2
+ display: inline-block;
3
+ }
4
+
5
+ .columnChooserHeader {
6
+ padding: 10px 15px;
7
+ border-bottom: 1px solid var(--colorNeutralStroke2, #edebe9);
8
+ margin-bottom: 5px;
9
+ }
10
+ .columnChooserHeader h3 {
11
+ margin: 0;
12
+ font-size: 14px;
13
+ font-weight: 600;
14
+ color: var(--colorNeutralForeground1, #323130);
15
+ }
@@ -0,0 +1,252 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
4
+ import { Input, Checkbox, Spinner, Avatar, Tooltip } from '@fluentui/react-components';
5
+ import { SearchRegular, ArrowUpRegular, ArrowDownRegular, ArrowSortRegular, FilterRegular } from '@fluentui/react-icons';
6
+ import styles from './ColumnHeaderFilter.module.css';
7
+ const SEARCH_DEBOUNCE_MS = 150;
8
+ const EMPTY_ARRAY = [];
9
+ export const ColumnHeaderFilter = React.memo((props) => {
10
+ const { columnName, filterType, isSorted = false, isSortedDescending = false, onSort, selectedValues, onFilterChange, options, isLoadingOptions = false, textValue = '', onTextChange, selectedUser, onUserChange, peopleSearch } = props;
11
+ const safeSelectedValues = selectedValues ?? EMPTY_ARRAY;
12
+ const safeOptions = options ?? EMPTY_ARRAY;
13
+ const [isFilterOpen, setIsFilterOpen] = useState(false);
14
+ const [tempSelected, setTempSelected] = useState(new Set(safeSelectedValues));
15
+ const [tempTextValue, setTempTextValue] = useState(textValue);
16
+ const [searchText, setSearchText] = useState('');
17
+ const [debouncedSearchText, setDebouncedSearchText] = useState('');
18
+ const [peopleSuggestions, setPeopleSuggestions] = useState([]);
19
+ const [isPeopleLoading, setIsPeopleLoading] = useState(false);
20
+ const [peopleSearchText, setPeopleSearchText] = useState('');
21
+ const [popoverPosition, setPopoverPosition] = useState(null);
22
+ const headerRef = useRef(null);
23
+ const popoverRef = useRef(null);
24
+ const searchDebounceRef = useRef();
25
+ const peopleSearchTimeoutRef = useRef(undefined);
26
+ const peopleInputRef = useRef(null);
27
+ useEffect(() => {
28
+ if (isFilterOpen) {
29
+ setTempSelected(new Set(safeSelectedValues));
30
+ setTempTextValue(textValue);
31
+ setSearchText('');
32
+ setDebouncedSearchText('');
33
+ setPeopleSearchText('');
34
+ setPeopleSuggestions([]);
35
+ if (headerRef.current) {
36
+ const rect = headerRef.current.getBoundingClientRect();
37
+ setPopoverPosition({
38
+ top: rect.bottom + 4,
39
+ left: rect.left
40
+ });
41
+ }
42
+ if (filterType === 'people') {
43
+ setTimeout(() => {
44
+ peopleInputRef.current?.focus();
45
+ }, 50);
46
+ }
47
+ }
48
+ else {
49
+ setPopoverPosition(null);
50
+ }
51
+ // eslint-disable-next-line react-hooks/exhaustive-deps
52
+ }, [isFilterOpen]);
53
+ useEffect(() => {
54
+ if (searchDebounceRef.current) {
55
+ clearTimeout(searchDebounceRef.current);
56
+ }
57
+ searchDebounceRef.current = setTimeout(() => {
58
+ setDebouncedSearchText(searchText);
59
+ }, SEARCH_DEBOUNCE_MS);
60
+ return () => {
61
+ if (searchDebounceRef.current) {
62
+ clearTimeout(searchDebounceRef.current);
63
+ }
64
+ };
65
+ }, [searchText]);
66
+ useEffect(() => {
67
+ if (!isFilterOpen)
68
+ return;
69
+ const handleClickOutside = (event) => {
70
+ const target = event.target;
71
+ const isOutsidePopover = popoverRef.current && !popoverRef.current.contains(target);
72
+ const isOutsideHeader = headerRef.current && !headerRef.current.contains(target);
73
+ if (isOutsidePopover && isOutsideHeader) {
74
+ setIsFilterOpen(false);
75
+ }
76
+ };
77
+ const timeoutId = setTimeout(() => {
78
+ document.addEventListener('mousedown', handleClickOutside);
79
+ }, 0);
80
+ return () => {
81
+ clearTimeout(timeoutId);
82
+ document.removeEventListener('mousedown', handleClickOutside);
83
+ };
84
+ }, [isFilterOpen]);
85
+ useEffect(() => {
86
+ if (!isFilterOpen)
87
+ return;
88
+ const handleKeyDown = (event) => {
89
+ if (event.key === 'Escape' || event.key === 'Esc') {
90
+ event.preventDefault();
91
+ event.stopPropagation();
92
+ setIsFilterOpen(false);
93
+ }
94
+ };
95
+ document.addEventListener('keydown', handleKeyDown, true);
96
+ return () => document.removeEventListener('keydown', handleKeyDown, true);
97
+ }, [isFilterOpen]);
98
+ const filteredOptions = useMemo(() => {
99
+ if (!debouncedSearchText.trim()) {
100
+ return safeOptions;
101
+ }
102
+ const searchLower = debouncedSearchText.toLowerCase().trim();
103
+ return safeOptions.filter(opt => opt.toLowerCase().includes(searchLower));
104
+ }, [safeOptions, debouncedSearchText]);
105
+ useEffect(() => {
106
+ if (!peopleSearch || !isFilterOpen || filterType !== 'people')
107
+ return;
108
+ if (peopleSearchTimeoutRef.current) {
109
+ window.clearTimeout(peopleSearchTimeoutRef.current);
110
+ }
111
+ if (!peopleSearchText.trim()) {
112
+ setPeopleSuggestions([]);
113
+ return;
114
+ }
115
+ setIsPeopleLoading(true);
116
+ peopleSearchTimeoutRef.current = window.setTimeout(async () => {
117
+ try {
118
+ const results = await peopleSearch(peopleSearchText);
119
+ setPeopleSuggestions(results.slice(0, 10));
120
+ }
121
+ catch (error) {
122
+ console.error('Error searching people:', error);
123
+ setPeopleSuggestions([]);
124
+ }
125
+ finally {
126
+ setIsPeopleLoading(false);
127
+ }
128
+ }, 300);
129
+ return () => {
130
+ if (peopleSearchTimeoutRef.current) {
131
+ window.clearTimeout(peopleSearchTimeoutRef.current);
132
+ }
133
+ };
134
+ }, [peopleSearchText, peopleSearch, isFilterOpen, filterType]);
135
+ const handleFilterIconClick = useCallback((e) => {
136
+ e.stopPropagation();
137
+ e.preventDefault();
138
+ setIsFilterOpen(prev => !prev);
139
+ }, []);
140
+ const handlePopoverClick = useCallback((e) => {
141
+ e.stopPropagation();
142
+ }, []);
143
+ const handleInputFocus = useCallback((e) => {
144
+ e.stopPropagation();
145
+ }, []);
146
+ const handleInputMouseDown = useCallback((e) => {
147
+ e.stopPropagation();
148
+ }, []);
149
+ const handleInputClick = useCallback((e) => {
150
+ e.stopPropagation();
151
+ }, []);
152
+ const handleInputKeyDown = useCallback((e) => {
153
+ if (e.key !== 'Escape' && e.key !== 'Esc') {
154
+ e.stopPropagation();
155
+ }
156
+ }, []);
157
+ const handleSortClick = useCallback((e) => {
158
+ e.stopPropagation();
159
+ if (onSort) {
160
+ onSort();
161
+ }
162
+ }, [onSort]);
163
+ const handleCheckboxChange = useCallback((option, checked) => {
164
+ setTempSelected(prev => {
165
+ const newSet = new Set(prev);
166
+ if (checked) {
167
+ newSet.add(option);
168
+ }
169
+ else {
170
+ newSet.delete(option);
171
+ }
172
+ return newSet;
173
+ });
174
+ }, []);
175
+ const handleSelectAll = useCallback(() => {
176
+ setTempSelected(new Set(filteredOptions));
177
+ }, [filteredOptions]);
178
+ const handleClearSelection = useCallback(() => {
179
+ setTempSelected(new Set());
180
+ }, []);
181
+ const handleApplyMultiSelect = useCallback(() => {
182
+ if (onFilterChange) {
183
+ onFilterChange(Array.from(tempSelected));
184
+ }
185
+ setIsFilterOpen(false);
186
+ }, [onFilterChange, tempSelected]);
187
+ const handleTextApply = useCallback(() => {
188
+ if (onTextChange) {
189
+ onTextChange(tempTextValue.trim());
190
+ }
191
+ setIsFilterOpen(false);
192
+ }, [onTextChange, tempTextValue]);
193
+ const handleTextClear = useCallback(() => {
194
+ setTempTextValue('');
195
+ }, []);
196
+ const handleTextKeyDown = useCallback((event) => {
197
+ if (event.key === 'Enter') {
198
+ event.preventDefault();
199
+ handleTextApply();
200
+ }
201
+ }, [handleTextApply]);
202
+ const handleUserSelect = useCallback((user) => {
203
+ if (onUserChange) {
204
+ onUserChange(user);
205
+ }
206
+ setIsFilterOpen(false);
207
+ }, [onUserChange]);
208
+ const handleClearUser = useCallback(() => {
209
+ if (onUserChange) {
210
+ onUserChange(undefined);
211
+ }
212
+ setIsFilterOpen(false);
213
+ }, [onUserChange]);
214
+ const hasActiveFilter = useMemo(() => {
215
+ if (filterType === 'multiSelect') {
216
+ return safeSelectedValues.length > 0;
217
+ }
218
+ if (filterType === 'text') {
219
+ return !!textValue.trim();
220
+ }
221
+ if (filterType === 'people') {
222
+ return !!selectedUser;
223
+ }
224
+ return false;
225
+ }, [filterType, safeSelectedValues, textValue, selectedUser]);
226
+ const renderPopoverContent = () => {
227
+ if (filterType === 'multiSelect') {
228
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.popoverSearch, onClick: handlePopoverClick, children: [_jsx(Input, { placeholder: "Search...", value: searchText, onChange: (e, data) => setSearchText(data.value || ''), onFocus: handleInputFocus, onMouseDown: handleInputMouseDown, onClick: handleInputClick, onKeyDown: handleInputKeyDown, autoComplete: "off", className: styles.searchInput, contentBefore: _jsx(SearchRegular, {}) }), _jsxs("div", { className: styles.resultCount, children: [filteredOptions.length, " of ", safeOptions.length, " options"] })] }), _jsxs("div", { className: styles.selectAllRow, onClick: handlePopoverClick, children: [_jsxs("button", { type: "button", className: styles.selectAllButton, onClick: handleSelectAll, children: ["Select All (", filteredOptions.length, ")"] }), _jsx("button", { type: "button", className: styles.selectAllButton, onClick: handleClearSelection, children: "Clear" })] }), _jsx("div", { className: styles.popoverOptions, onClick: handlePopoverClick, children: isLoadingOptions ? (_jsx("div", { className: styles.loadingContainer, children: _jsx(Spinner, { size: "small", label: "Loading..." }) })) : filteredOptions.length === 0 ? (_jsx("div", { className: styles.noResults, children: "No options found" })) : (filteredOptions.map(option => (_jsx("div", { className: styles.popoverOption, children: _jsx(Checkbox, { label: option, checked: tempSelected.has(option), onChange: (ev, data) => {
229
+ ev.stopPropagation();
230
+ handleCheckboxChange(option, data.checked === true);
231
+ } }) }, option)))) }), _jsxs("div", { className: styles.popoverActions, onClick: handlePopoverClick, children: [_jsx("button", { type: "button", className: styles.clearButton, onClick: handleClearSelection, children: "Clear" }), _jsx("button", { type: "button", className: styles.applyButton, onClick: handleApplyMultiSelect, children: "Apply" })] })] }));
232
+ }
233
+ if (filterType === 'text') {
234
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.popoverSearch, onClick: handlePopoverClick, children: _jsx(Input, { placeholder: "Enter search term...", value: tempTextValue, onChange: (e, data) => setTempTextValue(data.value || ''), onKeyDown: (e) => {
235
+ handleInputKeyDown(e);
236
+ handleTextKeyDown(e);
237
+ }, onFocus: handleInputFocus, onMouseDown: handleInputMouseDown, onClick: handleInputClick, autoComplete: "off", className: styles.searchInput, contentBefore: _jsx(SearchRegular, {}) }) }), _jsxs("div", { className: styles.popoverActions, onClick: handlePopoverClick, children: [_jsx("button", { type: "button", className: styles.clearButton, onClick: handleTextClear, disabled: !tempTextValue, children: "Clear" }), _jsx("button", { type: "button", className: styles.applyButton, onClick: handleTextApply, children: "Apply" })] })] }));
238
+ }
239
+ if (filterType === 'people') {
240
+ return (_jsxs(_Fragment, { children: [selectedUser && (_jsxs("div", { className: styles.selectedUserSection, onClick: handlePopoverClick, children: [_jsx("div", { className: styles.selectedUserLabel, children: "Currently filtered by:" }), _jsxs("div", { className: styles.selectedUser, children: [_jsxs("div", { className: styles.userInfo, children: [_jsx(Avatar, { name: selectedUser.displayName, image: { src: selectedUser.photo }, size: 32 }), _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: handleClearUser, "aria-label": "Remove filter", children: _jsx(FilterRegular, {}) })] })] })), _jsx("div", { className: styles.popoverSearch, onClick: handlePopoverClick, children: _jsxs("div", { className: styles.nativeInputWrapper, children: [_jsx(SearchRegular, { className: styles.nativeInputIcon }), _jsx("input", { ref: peopleInputRef, type: "text", placeholder: "Search for a person...", value: peopleSearchText, onChange: (e) => setPeopleSearchText(e.target.value), onFocus: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), autoComplete: "off", className: styles.nativeInput })] }) }), _jsx("div", { className: styles.popoverOptions, onClick: handlePopoverClick, children: isPeopleLoading && peopleSearchText.trim() ? (_jsx("div", { className: styles.loadingContainer, children: _jsx(Spinner, { size: "small", label: "Searching..." }) })) : peopleSuggestions.length === 0 && peopleSearchText.trim() ? (_jsx("div", { className: styles.noResults, children: "No results found" })) : peopleSearchText.trim() ? (peopleSuggestions.map(user => (_jsx("div", { className: styles.personOption, onClick: (e) => {
241
+ e.stopPropagation();
242
+ handleUserSelect(user);
243
+ }, children: _jsxs("div", { className: styles.userInfo, children: [_jsx(Avatar, { name: user.displayName, image: { src: user.photo }, size: 32 }), _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, onClick: handlePopoverClick, children: _jsx("button", { type: "button", className: styles.clearButton, onClick: handleClearUser, children: "Clear Filter" }) }))] }));
244
+ }
245
+ return null;
246
+ };
247
+ return (_jsxs("div", { className: styles.columnHeader, ref: headerRef, children: [_jsx("div", { className: styles.headerContent, children: _jsx(Tooltip, { content: columnName, relationship: "label", withArrow: true, children: _jsx("span", { className: styles.columnNameTooltipTrigger, children: _jsx("span", { className: styles.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: handleSortClick, "aria-label": `Sort by ${columnName}`, title: isSorted ? (isSortedDescending ? 'Sorted descending' : 'Sorted ascending') : 'Sort', children: isSorted ? (isSortedDescending ? _jsx(ArrowDownRegular, {}) : _jsx(ArrowUpRegular, {})) : (_jsx(ArrowSortRegular, {})) })), filterType !== 'none' && (_jsxs("button", { type: "button", className: `${styles.filterIcon} ${hasActiveFilter ? styles.filterActive : ''} ${isFilterOpen ? styles.filterOpen : ''}`, onClick: handleFilterIconClick, "aria-label": `Filter ${columnName}`, title: `Filter ${columnName}`, children: [_jsx(FilterRegular, {}), hasActiveFilter && _jsx("span", { className: styles.filterBadge })] }))] }), isFilterOpen && filterType !== 'none' && popoverPosition && (_jsxs("div", { className: styles.filterPopover, ref: popoverRef, onClick: handlePopoverClick, style: {
248
+ top: `${popoverPosition.top}px`,
249
+ left: `${popoverPosition.left}px`
250
+ }, children: [_jsxs("div", { className: styles.popoverHeader, children: ["Filter: ", columnName] }), renderPopoverContent()] }))] }));
251
+ });
252
+ ColumnHeaderFilter.displayName = 'ColumnHeaderFilter';