@xcelsior/ui-spreadsheets 1.1.14 → 1.1.15
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/dist/index.d.mts +28 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +639 -399
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +632 -393
- package/dist/index.mjs.map +1 -1
- package/dist/styles/globals.css +58 -0
- package/dist/styles/globals.css.map +1 -1
- package/package.json +1 -1
- package/src/components/ActiveFiltersDisplay.tsx +257 -0
- package/src/components/Spreadsheet.tsx +14 -3
- package/src/components/SpreadsheetCell.tsx +8 -2
- package/src/components/SpreadsheetHeader.tsx +8 -2
- package/src/components/SpreadsheetToolbar.tsx +62 -19
- package/src/hooks/useSpreadsheetPinning.ts +1 -1
- package/src/index.ts +2 -0
- package/src/types.ts +10 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { HiX } from 'react-icons/hi';
|
|
3
|
+
import { cn } from '../utils';
|
|
4
|
+
import type {
|
|
5
|
+
SpreadsheetColumn,
|
|
6
|
+
SpreadsheetColumnFilter,
|
|
7
|
+
TextFilterOperator,
|
|
8
|
+
NumberFilterOperator,
|
|
9
|
+
DateFilterOperator,
|
|
10
|
+
} from '../types';
|
|
11
|
+
|
|
12
|
+
/** Text filter operator labels */
|
|
13
|
+
const TEXT_OPERATOR_LABELS: Record<TextFilterOperator, string> = {
|
|
14
|
+
contains: 'contains',
|
|
15
|
+
notContains: 'does not contain',
|
|
16
|
+
equals: 'equals',
|
|
17
|
+
notEquals: 'does not equal',
|
|
18
|
+
startsWith: 'starts with',
|
|
19
|
+
endsWith: 'ends with',
|
|
20
|
+
isEmpty: 'is empty',
|
|
21
|
+
isNotEmpty: 'is not empty',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Number filter operator labels */
|
|
25
|
+
const NUMBER_OPERATOR_LABELS: Record<NumberFilterOperator, string> = {
|
|
26
|
+
equals: '=',
|
|
27
|
+
notEquals: '≠',
|
|
28
|
+
greaterThan: '>',
|
|
29
|
+
greaterThanOrEqual: '≥',
|
|
30
|
+
lessThan: '<',
|
|
31
|
+
lessThanOrEqual: '≤',
|
|
32
|
+
between: 'between',
|
|
33
|
+
isEmpty: 'is empty',
|
|
34
|
+
isNotEmpty: 'is not empty',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Date filter operator labels */
|
|
38
|
+
const DATE_OPERATOR_LABELS: Record<DateFilterOperator, string> = {
|
|
39
|
+
equals: 'is',
|
|
40
|
+
notEquals: 'is not',
|
|
41
|
+
before: 'before',
|
|
42
|
+
after: 'after',
|
|
43
|
+
between: 'between',
|
|
44
|
+
today: 'is today',
|
|
45
|
+
yesterday: 'is yesterday',
|
|
46
|
+
thisWeek: 'is this week',
|
|
47
|
+
lastWeek: 'is last week',
|
|
48
|
+
thisMonth: 'is this month',
|
|
49
|
+
lastMonth: 'is last month',
|
|
50
|
+
thisYear: 'is this year',
|
|
51
|
+
isEmpty: 'is empty',
|
|
52
|
+
isNotEmpty: 'is not empty',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export interface ActiveFiltersDisplayProps {
|
|
56
|
+
/** Current filters */
|
|
57
|
+
filters: Record<string, SpreadsheetColumnFilter>;
|
|
58
|
+
/** Column definitions */
|
|
59
|
+
columns: SpreadsheetColumn[];
|
|
60
|
+
/** Callback to clear individual filter */
|
|
61
|
+
onClearFilter: (columnId: string) => void;
|
|
62
|
+
/** Callback to clear all filters */
|
|
63
|
+
onClearAllFilters: () => void;
|
|
64
|
+
/** Custom className */
|
|
65
|
+
className?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Format a filter into a human-readable string
|
|
70
|
+
*/
|
|
71
|
+
function formatFilter(filter: SpreadsheetColumnFilter, _column: SpreadsheetColumn): string {
|
|
72
|
+
const parts: string[] = [];
|
|
73
|
+
|
|
74
|
+
// Text condition
|
|
75
|
+
if (filter.textCondition) {
|
|
76
|
+
const { operator, value } = filter.textCondition;
|
|
77
|
+
const label = TEXT_OPERATOR_LABELS[operator];
|
|
78
|
+
if (['isEmpty', 'isNotEmpty'].includes(operator)) {
|
|
79
|
+
parts.push(label);
|
|
80
|
+
} else if (value) {
|
|
81
|
+
parts.push(`${label} "${value}"`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Number condition
|
|
86
|
+
if (filter.numberCondition) {
|
|
87
|
+
const { operator, value, valueTo } = filter.numberCondition;
|
|
88
|
+
const label = NUMBER_OPERATOR_LABELS[operator];
|
|
89
|
+
if (['isEmpty', 'isNotEmpty'].includes(operator)) {
|
|
90
|
+
parts.push(label);
|
|
91
|
+
} else if (operator === 'between' && value !== undefined && valueTo !== undefined) {
|
|
92
|
+
parts.push(`${label} ${value} and ${valueTo}`);
|
|
93
|
+
} else if (value !== undefined) {
|
|
94
|
+
parts.push(`${label} ${value}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Date condition
|
|
99
|
+
if (filter.dateCondition) {
|
|
100
|
+
const { operator, value, valueTo } = filter.dateCondition;
|
|
101
|
+
const label = DATE_OPERATOR_LABELS[operator];
|
|
102
|
+
const noValueOperators = [
|
|
103
|
+
'isEmpty',
|
|
104
|
+
'isNotEmpty',
|
|
105
|
+
'today',
|
|
106
|
+
'yesterday',
|
|
107
|
+
'thisWeek',
|
|
108
|
+
'lastWeek',
|
|
109
|
+
'thisMonth',
|
|
110
|
+
'lastMonth',
|
|
111
|
+
'thisYear',
|
|
112
|
+
];
|
|
113
|
+
if (noValueOperators.includes(operator)) {
|
|
114
|
+
parts.push(label);
|
|
115
|
+
} else if (operator === 'between' && value && valueTo) {
|
|
116
|
+
parts.push(`${label} ${formatDate(value)} and ${formatDate(valueTo)}`);
|
|
117
|
+
} else if (value) {
|
|
118
|
+
parts.push(`${label} ${formatDate(value)}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Legacy filters
|
|
123
|
+
if (filter.text) {
|
|
124
|
+
parts.push(`contains "${filter.text}"`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (filter.selectedValues && filter.selectedValues.length > 0) {
|
|
128
|
+
if (filter.selectedValues.length === 1) {
|
|
129
|
+
parts.push(`is "${filter.selectedValues[0]}"`);
|
|
130
|
+
} else {
|
|
131
|
+
parts.push(`is one of ${filter.selectedValues.length} values`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (filter.min !== undefined && filter.max !== undefined) {
|
|
136
|
+
parts.push(`between ${filter.min} and ${filter.max}`);
|
|
137
|
+
} else if (filter.min !== undefined) {
|
|
138
|
+
parts.push(`≥ ${filter.min}`);
|
|
139
|
+
} else if (filter.max !== undefined) {
|
|
140
|
+
parts.push(`≤ ${filter.max}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (filter.includeBlanks) {
|
|
144
|
+
parts.push('includes blanks');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (filter.excludeBlanks) {
|
|
148
|
+
parts.push('excludes blanks');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return parts.join(', ') || 'filtered';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Format a date string for display
|
|
156
|
+
*/
|
|
157
|
+
function formatDate(dateStr: string): string {
|
|
158
|
+
try {
|
|
159
|
+
const date = new Date(dateStr);
|
|
160
|
+
return date.toLocaleDateString('en-US', {
|
|
161
|
+
month: 'short',
|
|
162
|
+
day: 'numeric',
|
|
163
|
+
year: 'numeric',
|
|
164
|
+
});
|
|
165
|
+
} catch {
|
|
166
|
+
return dateStr;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* ActiveFiltersDisplay component - Shows active filters as chips with clear buttons
|
|
172
|
+
*/
|
|
173
|
+
export const ActiveFiltersDisplay: React.FC<ActiveFiltersDisplayProps> = ({
|
|
174
|
+
filters,
|
|
175
|
+
columns,
|
|
176
|
+
onClearFilter,
|
|
177
|
+
onClearAllFilters,
|
|
178
|
+
className,
|
|
179
|
+
}) => {
|
|
180
|
+
const activeFilters = Object.entries(filters).filter(([_, filter]) => {
|
|
181
|
+
// Check if filter has any active conditions
|
|
182
|
+
return (
|
|
183
|
+
filter.textCondition ||
|
|
184
|
+
filter.numberCondition ||
|
|
185
|
+
filter.dateCondition ||
|
|
186
|
+
filter.text ||
|
|
187
|
+
(filter.selectedValues && filter.selectedValues.length > 0) ||
|
|
188
|
+
filter.min !== undefined ||
|
|
189
|
+
filter.max !== undefined ||
|
|
190
|
+
filter.includeBlanks ||
|
|
191
|
+
filter.excludeBlanks
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (activeFilters.length === 0) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const getColumnLabel = (columnId: string): string => {
|
|
200
|
+
const column = columns.find((c) => c.id === columnId);
|
|
201
|
+
return column?.label || columnId;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const getColumn = (columnId: string): SpreadsheetColumn | undefined => {
|
|
205
|
+
return columns.find((c) => c.id === columnId);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div
|
|
210
|
+
className={cn(
|
|
211
|
+
'flex flex-wrap items-center gap-2 px-4 py-2 bg-amber-50 border-b border-amber-200',
|
|
212
|
+
className
|
|
213
|
+
)}
|
|
214
|
+
>
|
|
215
|
+
<span className="text-xs font-medium text-amber-700 mr-1">Active filters:</span>
|
|
216
|
+
|
|
217
|
+
{activeFilters.map(([columnId, filter]) => {
|
|
218
|
+
const column = getColumn(columnId);
|
|
219
|
+
const filterDescription = column
|
|
220
|
+
? formatFilter(filter, column)
|
|
221
|
+
: formatFilter(filter, { id: columnId, label: columnId });
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div
|
|
225
|
+
key={columnId}
|
|
226
|
+
className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-white border border-amber-300 rounded-full shadow-sm"
|
|
227
|
+
>
|
|
228
|
+
<span className="text-xs font-medium text-gray-700">
|
|
229
|
+
{getColumnLabel(columnId)}
|
|
230
|
+
</span>
|
|
231
|
+
<span className="text-xs text-gray-500">{filterDescription}</span>
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
onClick={() => onClearFilter(columnId)}
|
|
235
|
+
className="p-0.5 hover:bg-amber-100 rounded-full transition-colors"
|
|
236
|
+
title={`Clear filter for ${getColumnLabel(columnId)}`}
|
|
237
|
+
>
|
|
238
|
+
<HiX className="h-3 w-3 text-amber-600 hover:text-amber-800" />
|
|
239
|
+
</button>
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
})}
|
|
243
|
+
|
|
244
|
+
{activeFilters.length > 1 && (
|
|
245
|
+
<button
|
|
246
|
+
type="button"
|
|
247
|
+
onClick={onClearAllFilters}
|
|
248
|
+
className="text-xs text-amber-700 hover:text-amber-900 underline ml-2 transition-colors"
|
|
249
|
+
>
|
|
250
|
+
Clear all
|
|
251
|
+
</button>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
ActiveFiltersDisplay.displayName = 'ActiveFiltersDisplay';
|
|
@@ -226,6 +226,9 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
226
226
|
// Modal state
|
|
227
227
|
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
|
228
228
|
|
|
229
|
+
// Filters panel state
|
|
230
|
+
const [showFiltersPanel, setShowFiltersPanel] = useState(false);
|
|
231
|
+
|
|
229
232
|
// Undo/Redo hook
|
|
230
233
|
const {
|
|
231
234
|
canUndo,
|
|
@@ -826,6 +829,11 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
826
829
|
autoSave={spreadsheetSettings.autoSave}
|
|
827
830
|
hasActiveFilters={hasActiveFilters}
|
|
828
831
|
onClearFilters={clearAllFilters}
|
|
832
|
+
filters={filters}
|
|
833
|
+
columns={columns}
|
|
834
|
+
onClearFilter={(columnId) => handleFilterChange(columnId, undefined)}
|
|
835
|
+
showFiltersPanel={showFiltersPanel}
|
|
836
|
+
onToggleFiltersPanel={() => setShowFiltersPanel(!showFiltersPanel)}
|
|
829
837
|
onZoomIn={() => setZoom((z) => Math.min(z + 10, 200))}
|
|
830
838
|
onZoomOut={() => setZoom((z) => Math.max(z - 10, 50))}
|
|
831
839
|
onZoomReset={() => setZoom(100)}
|
|
@@ -869,7 +877,12 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
869
877
|
if (item.type === 'pinned-column') {
|
|
870
878
|
const col = columns.find((c) => c.id === item.columnId);
|
|
871
879
|
const isPinnedLeft = item.pinSide === 'left';
|
|
872
|
-
const pinnedWidth = Math.max(
|
|
880
|
+
const pinnedWidth = Math.max(
|
|
881
|
+
col?.minWidth ||
|
|
882
|
+
col?.width ||
|
|
883
|
+
MIN_PINNED_COLUMN_WIDTH,
|
|
884
|
+
MIN_PINNED_COLUMN_WIDTH
|
|
885
|
+
);
|
|
873
886
|
return (
|
|
874
887
|
<th
|
|
875
888
|
key={`pinned-group-${item.columnId}`}
|
|
@@ -888,8 +901,6 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
888
901
|
? `${getColumnRightOffset(item.columnId)}px`
|
|
889
902
|
: undefined,
|
|
890
903
|
minWidth: pinnedWidth,
|
|
891
|
-
width: pinnedWidth,
|
|
892
|
-
maxWidth: pinnedWidth,
|
|
893
904
|
}}
|
|
894
905
|
/>
|
|
895
906
|
);
|
|
@@ -277,8 +277,14 @@ const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
|
|
|
277
277
|
// Pinned columns must have a fixed width so sticky offsets stay correct.
|
|
278
278
|
// Enforce MIN_PINNED_COLUMN_WIDTH so header actions always fit.
|
|
279
279
|
...(isPinned && {
|
|
280
|
-
width: Math.max(
|
|
281
|
-
|
|
280
|
+
width: Math.max(
|
|
281
|
+
column.minWidth || column.width || MIN_PINNED_COLUMN_WIDTH,
|
|
282
|
+
MIN_PINNED_COLUMN_WIDTH
|
|
283
|
+
),
|
|
284
|
+
maxWidth: Math.max(
|
|
285
|
+
column.minWidth || column.width || MIN_PINNED_COLUMN_WIDTH,
|
|
286
|
+
MIN_PINNED_COLUMN_WIDTH
|
|
287
|
+
),
|
|
282
288
|
}),
|
|
283
289
|
...positionStyles,
|
|
284
290
|
...selectionBorderStyles,
|
|
@@ -75,8 +75,14 @@ export const SpreadsheetHeader: React.FC<
|
|
|
75
75
|
// Pinned columns must have a fixed width so sticky offsets stay correct.
|
|
76
76
|
// Enforce MIN_PINNED_COLUMN_WIDTH so header actions (pin/filter/highlight) always fit.
|
|
77
77
|
...(isPinned && {
|
|
78
|
-
width: Math.max(
|
|
79
|
-
|
|
78
|
+
width: Math.max(
|
|
79
|
+
column.minWidth || column.width || MIN_PINNED_COLUMN_WIDTH,
|
|
80
|
+
MIN_PINNED_COLUMN_WIDTH
|
|
81
|
+
),
|
|
82
|
+
maxWidth: Math.max(
|
|
83
|
+
column.minWidth || column.width || MIN_PINNED_COLUMN_WIDTH,
|
|
84
|
+
MIN_PINNED_COLUMN_WIDTH
|
|
85
|
+
),
|
|
80
86
|
}),
|
|
81
87
|
top: 0, // For sticky header
|
|
82
88
|
...positionStyles,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import {
|
|
3
3
|
HiCheck,
|
|
4
|
+
HiChevronDown,
|
|
5
|
+
HiChevronUp,
|
|
4
6
|
HiCog,
|
|
5
7
|
HiDotsVertical,
|
|
6
8
|
HiFilter,
|
|
@@ -12,6 +14,7 @@ import {
|
|
|
12
14
|
} from 'react-icons/hi';
|
|
13
15
|
import { cn } from '../utils';
|
|
14
16
|
import type { SpreadsheetToolbarProps } from '../types';
|
|
17
|
+
import { ActiveFiltersDisplay } from './ActiveFiltersDisplay';
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* SpreadsheetToolbar component - Top toolbar with zoom controls, undo/redo, filters, and actions.
|
|
@@ -60,6 +63,11 @@ export const SpreadsheetToolbar: React.FC<SpreadsheetToolbarProps> = ({
|
|
|
60
63
|
onShowShortcuts,
|
|
61
64
|
hasActiveFilters,
|
|
62
65
|
onClearFilters,
|
|
66
|
+
filters,
|
|
67
|
+
columns,
|
|
68
|
+
onClearFilter,
|
|
69
|
+
showFiltersPanel,
|
|
70
|
+
onToggleFiltersPanel,
|
|
63
71
|
className,
|
|
64
72
|
}) => {
|
|
65
73
|
const [showMoreMenu, setShowMoreMenu] = React.useState(false);
|
|
@@ -122,13 +130,30 @@ export const SpreadsheetToolbar: React.FC<SpreadsheetToolbarProps> = ({
|
|
|
122
130
|
}
|
|
123
131
|
};
|
|
124
132
|
|
|
133
|
+
// Count active filters
|
|
134
|
+
const activeFilterCount = filters
|
|
135
|
+
? Object.values(filters).filter(
|
|
136
|
+
(f) =>
|
|
137
|
+
f.textCondition ||
|
|
138
|
+
f.numberCondition ||
|
|
139
|
+
f.dateCondition ||
|
|
140
|
+
f.text ||
|
|
141
|
+
(f.selectedValues && f.selectedValues.length > 0) ||
|
|
142
|
+
f.min !== undefined ||
|
|
143
|
+
f.max !== undefined ||
|
|
144
|
+
f.includeBlanks ||
|
|
145
|
+
f.excludeBlanks
|
|
146
|
+
).length
|
|
147
|
+
: 0;
|
|
148
|
+
|
|
125
149
|
return (
|
|
126
|
-
<div
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
150
|
+
<div className="flex flex-col">
|
|
151
|
+
<div
|
|
152
|
+
className={cn(
|
|
153
|
+
'flex flex-wrap items-center justify-between gap-2 px-4 py-2 border-b border-gray-200 bg-white',
|
|
154
|
+
className
|
|
155
|
+
)}
|
|
156
|
+
>
|
|
132
157
|
{/* Left section: Primary actions */}
|
|
133
158
|
<div className="flex items-center gap-2">
|
|
134
159
|
{/* Undo/Redo buttons */}
|
|
@@ -212,22 +237,29 @@ export const SpreadsheetToolbar: React.FC<SpreadsheetToolbarProps> = ({
|
|
|
212
237
|
</div>
|
|
213
238
|
)}
|
|
214
239
|
|
|
215
|
-
{/*
|
|
216
|
-
{hasActiveFilters &&
|
|
217
|
-
<
|
|
240
|
+
{/* Show filters button */}
|
|
241
|
+
{hasActiveFilters && onToggleFiltersPanel && (
|
|
242
|
+
<button
|
|
243
|
+
type={'button'}
|
|
244
|
+
onClick={onToggleFiltersPanel}
|
|
245
|
+
className={cn(
|
|
246
|
+
'flex items-center gap-2 px-2.5 py-1.5 rounded transition-colors',
|
|
247
|
+
showFiltersPanel
|
|
248
|
+
? 'bg-amber-600 text-white hover:bg-amber-700'
|
|
249
|
+
: 'bg-amber-500 text-white hover:bg-amber-600'
|
|
250
|
+
)}
|
|
251
|
+
title={showFiltersPanel ? 'Hide active filters' : 'Show active filters'}
|
|
252
|
+
>
|
|
218
253
|
<HiFilter className="h-3.5 w-3.5" />
|
|
219
254
|
<span className="text-xs font-medium whitespace-nowrap">
|
|
220
|
-
|
|
255
|
+
{activeFilterCount} filter{activeFilterCount !== 1 ? 's' : ''} active
|
|
221
256
|
</span>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
className="
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
<HiX className="h-3 w-3" />
|
|
229
|
-
</button>
|
|
230
|
-
</div>
|
|
257
|
+
{showFiltersPanel ? (
|
|
258
|
+
<HiChevronUp className="h-3 w-3" />
|
|
259
|
+
) : (
|
|
260
|
+
<HiChevronDown className="h-3 w-3" />
|
|
261
|
+
)}
|
|
262
|
+
</button>
|
|
231
263
|
)}
|
|
232
264
|
|
|
233
265
|
{/* Summary badge */}
|
|
@@ -350,6 +382,17 @@ export const SpreadsheetToolbar: React.FC<SpreadsheetToolbarProps> = ({
|
|
|
350
382
|
)}
|
|
351
383
|
</div>
|
|
352
384
|
</div>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
{/* Active filters panel */}
|
|
388
|
+
{showFiltersPanel && filters && columns && onClearFilter && onClearFilters && (
|
|
389
|
+
<ActiveFiltersDisplay
|
|
390
|
+
filters={filters}
|
|
391
|
+
columns={columns}
|
|
392
|
+
onClearFilter={onClearFilter}
|
|
393
|
+
onClearAllFilters={onClearFilters}
|
|
394
|
+
/>
|
|
395
|
+
)}
|
|
353
396
|
</div>
|
|
354
397
|
);
|
|
355
398
|
};
|
|
@@ -5,7 +5,7 @@ import type { SpreadsheetColumn, SpreadsheetColumnGroup } from '../types';
|
|
|
5
5
|
export const ROW_INDEX_COLUMN_ID = '__row_index__';
|
|
6
6
|
export const ROW_INDEX_COLUMN_WIDTH = 80;
|
|
7
7
|
// Minimum width for any pinned column to ensure header actions (pin, filter, highlight icons) fit
|
|
8
|
-
export const MIN_PINNED_COLUMN_WIDTH =
|
|
8
|
+
export const MIN_PINNED_COLUMN_WIDTH = 150;
|
|
9
9
|
|
|
10
10
|
export interface UseSpreadsheetPinningOptions<T> {
|
|
11
11
|
columns: SpreadsheetColumn<T>[];
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,8 @@ export { SpreadsheetFilterDropdown } from './components/SpreadsheetFilterDropdow
|
|
|
8
8
|
export { SpreadsheetToolbar } from './components/SpreadsheetToolbar';
|
|
9
9
|
export { SpreadsheetSettingsModal } from './components/SpreadsheetSettingsModal';
|
|
10
10
|
export { RowContextMenu } from './components/RowContextMenu';
|
|
11
|
+
export { ActiveFiltersDisplay } from './components/ActiveFiltersDisplay';
|
|
12
|
+
export type { ActiveFiltersDisplayProps } from './components/ActiveFiltersDisplay';
|
|
11
13
|
export type { SpreadsheetSettings } from './components/SpreadsheetSettingsModal';
|
|
12
14
|
|
|
13
15
|
// Types
|
package/src/types.ts
CHANGED
|
@@ -703,6 +703,16 @@ export interface SpreadsheetToolbarProps {
|
|
|
703
703
|
hasActiveFilters?: boolean;
|
|
704
704
|
/** Callback to clear all filters */
|
|
705
705
|
onClearFilters?: () => void;
|
|
706
|
+
/** Current filters (for displaying active filters) */
|
|
707
|
+
filters?: Record<string, SpreadsheetColumnFilter>;
|
|
708
|
+
/** Column definitions (for displaying filter column names) */
|
|
709
|
+
columns?: SpreadsheetColumn[];
|
|
710
|
+
/** Callback to clear individual filter */
|
|
711
|
+
onClearFilter?: (columnId: string) => void;
|
|
712
|
+
/** Whether to show the active filters panel */
|
|
713
|
+
showFiltersPanel?: boolean;
|
|
714
|
+
/** Callback to toggle the active filters panel */
|
|
715
|
+
onToggleFiltersPanel?: () => void;
|
|
706
716
|
/** Custom className */
|
|
707
717
|
className?: string;
|
|
708
718
|
}
|