@xcelsior/ui-spreadsheets 1.1.14 → 1.1.16

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.
@@ -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,234 +130,272 @@ export const SpreadsheetToolbar: React.FC<SpreadsheetToolbarProps> = ({
122
130
  }
123
131
  };
124
132
 
125
- return (
126
- <div
127
- className={cn(
128
- 'flex flex-wrap items-center justify-between gap-2 px-4 py-2 border-b border-gray-200 bg-white',
129
- className
130
- )}
131
- >
132
- {/* Left section: Primary actions */}
133
- <div className="flex items-center gap-2">
134
- {/* Undo/Redo buttons */}
135
- <div className="flex items-center gap-1">
136
- <button
137
- type={'button'}
138
- onClick={onUndo}
139
- disabled={!canUndo}
140
- className={cn(
141
- buttonBaseClasses,
142
- canUndo
143
- ? 'bg-gray-100 text-gray-700 hover:bg-gray-200'
144
- : 'bg-gray-50 text-gray-400'
145
- )}
146
- title={`Undo (${undoCount} changes)`}
147
- >
148
- <HiReply className="h-4 w-4" />
149
- </button>
150
- <button
151
- type={'button'}
152
- onClick={onRedo}
153
- disabled={!canRedo}
154
- className={cn(
155
- buttonBaseClasses,
156
- canRedo
157
- ? 'bg-gray-100 text-gray-700 hover:bg-gray-200'
158
- : 'bg-gray-50 text-gray-400'
159
- )}
160
- title={`Redo (${redoCount} changes)`}
161
- style={{ transform: 'scaleX(-1)' }}
162
- >
163
- <HiReply className="h-4 w-4" />
164
- </button>
165
- </div>
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;
166
148
 
167
- {/* Zoom controls */}
168
- <div className="flex items-center gap-1 px-1.5 py-1 bg-gray-100 rounded">
169
- <button
170
- type={'button'}
171
- onClick={onZoomOut}
172
- className="p-1 hover:bg-white rounded"
173
- title="Zoom out"
174
- >
175
- <HiZoomOut className="h-4 w-4 text-gray-600" />
176
- </button>
177
- <button
178
- type={'button'}
179
- onClick={onZoomReset}
180
- className="px-2 py-0.5 hover:bg-white rounded text-xs min-w-[45px] text-center text-gray-600"
181
- title="Reset zoom"
182
- >
183
- {zoom}%
184
- </button>
185
- <button
186
- type={'button'}
187
- onClick={onZoomIn}
188
- className="p-1 hover:bg-white rounded"
189
- title="Zoom in"
190
- >
191
- <HiZoomIn className="h-4 w-4 text-gray-600" />
192
- </button>
193
- </div>
194
- </div>
195
-
196
- {/* Center section: Status indicators */}
197
- <div className="flex items-center gap-2 flex-1 min-w-0">
198
- {/* Selected rows indicator */}
199
- {selectedRowCount > 0 && (
200
- <div className="flex items-center gap-2 px-2.5 py-1.5 bg-blue-600 text-white rounded">
201
- <span className="text-xs font-medium whitespace-nowrap">
202
- {selectedRowCount} row{selectedRowCount !== 1 ? 's' : ''} selected
203
- </span>
149
+ return (
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
+ >
157
+ {/* Left section: Primary actions */}
158
+ <div className="flex items-center gap-2">
159
+ {/* Undo/Redo buttons */}
160
+ <div className="flex items-center gap-1">
204
161
  <button
205
162
  type={'button'}
206
- onClick={onClearSelection}
207
- className="p-0.5 hover:bg-blue-700 rounded"
208
- title="Clear selection"
163
+ onClick={onUndo}
164
+ disabled={!canUndo}
165
+ className={cn(
166
+ buttonBaseClasses,
167
+ canUndo
168
+ ? 'bg-gray-100 text-gray-700 hover:bg-gray-200'
169
+ : 'bg-gray-50 text-gray-400'
170
+ )}
171
+ title={`Undo (${undoCount} changes)`}
209
172
  >
210
- <HiX className="h-3 w-3" />
173
+ <HiReply className="h-4 w-4" />
211
174
  </button>
212
- </div>
213
- )}
214
-
215
- {/* Clear filters button */}
216
- {hasActiveFilters && onClearFilters && (
217
- <div className="flex items-center gap-2 px-2.5 py-1.5 bg-amber-500 text-white rounded">
218
- <HiFilter className="h-3.5 w-3.5" />
219
- <span className="text-xs font-medium whitespace-nowrap">
220
- Filters active
221
- </span>
222
175
  <button
223
176
  type={'button'}
224
- onClick={onClearFilters}
225
- className="p-0.5 hover:bg-amber-600 rounded"
226
- title="Clear all filters"
177
+ onClick={onRedo}
178
+ disabled={!canRedo}
179
+ className={cn(
180
+ buttonBaseClasses,
181
+ canRedo
182
+ ? 'bg-gray-100 text-gray-700 hover:bg-gray-200'
183
+ : 'bg-gray-50 text-gray-400'
184
+ )}
185
+ title={`Redo (${redoCount} changes)`}
186
+ style={{ transform: 'scaleX(-1)' }}
227
187
  >
228
- <HiX className="h-3 w-3" />
188
+ <HiReply className="h-4 w-4" />
229
189
  </button>
230
190
  </div>
231
- )}
232
191
 
233
- {/* Summary badge */}
234
- {summary && (
235
- <div
236
- className={cn(
237
- 'flex items-center gap-2 px-2.5 py-1.5 rounded border text-xs',
238
- getSummaryVariantClasses(summary.variant)
239
- )}
240
- >
241
- <span className="font-semibold whitespace-nowrap">{summary.label}:</span>
242
- <span className="font-bold whitespace-nowrap">{summary.value}</span>
192
+ {/* Zoom controls */}
193
+ <div className="flex items-center gap-1 px-1.5 py-1 bg-gray-100 rounded">
194
+ <button
195
+ type={'button'}
196
+ onClick={onZoomOut}
197
+ className="p-1 hover:bg-white rounded"
198
+ title="Zoom out"
199
+ >
200
+ <HiZoomOut className="h-4 w-4 text-gray-600" />
201
+ </button>
202
+ <button
203
+ type={'button'}
204
+ onClick={onZoomReset}
205
+ className="px-2 py-0.5 hover:bg-white rounded text-xs min-w-[45px] text-center text-gray-600"
206
+ title="Reset zoom"
207
+ >
208
+ {zoom}%
209
+ </button>
210
+ <button
211
+ type={'button'}
212
+ onClick={onZoomIn}
213
+ className="p-1 hover:bg-white rounded"
214
+ title="Zoom in"
215
+ >
216
+ <HiZoomIn className="h-4 w-4 text-gray-600" />
217
+ </button>
243
218
  </div>
244
- )}
245
- </div>
219
+ </div>
246
220
 
247
- {/* Right section: Action buttons */}
248
- <div className="flex items-center gap-2">
249
- {/* Save status */}
250
- {saveStatusDisplay && (
251
- <span
252
- className={cn(
253
- 'text-xs flex items-center gap-1',
254
- saveStatusDisplay.className
255
- )}
256
- >
257
- {saveStatusDisplay.text}
258
- </span>
259
- )}
221
+ {/* Center section: Status indicators */}
222
+ <div className="flex items-center gap-2 flex-1 min-w-0">
223
+ {/* Selected rows indicator */}
224
+ {selectedRowCount > 0 && (
225
+ <div className="flex items-center gap-2 px-2.5 py-1.5 bg-blue-600 text-white rounded">
226
+ <span className="text-xs font-medium whitespace-nowrap">
227
+ {selectedRowCount} row{selectedRowCount !== 1 ? 's' : ''} selected
228
+ </span>
229
+ <button
230
+ type={'button'}
231
+ onClick={onClearSelection}
232
+ className="p-0.5 hover:bg-blue-700 rounded"
233
+ title="Clear selection"
234
+ >
235
+ <HiX className="h-3 w-3" />
236
+ </button>
237
+ </div>
238
+ )}
260
239
 
261
- {/* Manual save button (when auto-save is off) */}
262
- {!autoSave && onSave && (
263
- <button
264
- type={'button'}
265
- onClick={onSave}
266
- disabled={!hasUnsavedChanges}
267
- className={cn(
268
- 'px-3 py-1.5 text-xs bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors flex items-center gap-1.5',
269
- 'disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600'
270
- )}
271
- >
272
- <HiCheck className="h-3.5 w-3.5" />
273
- Save
274
- </button>
275
- )}
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
+ >
253
+ <HiFilter className="h-3.5 w-3.5" />
254
+ <span className="text-xs font-medium whitespace-nowrap">
255
+ {activeFilterCount} filter{activeFilterCount !== 1 ? 's' : ''}{' '}
256
+ active
257
+ </span>
258
+ {showFiltersPanel ? (
259
+ <HiChevronUp className="h-3 w-3" />
260
+ ) : (
261
+ <HiChevronDown className="h-3 w-3" />
262
+ )}
263
+ </button>
264
+ )}
276
265
 
277
- {/* More menu dropdown */}
278
- <div className="relative" ref={menuRef}>
279
- <button
280
- type={'button'}
281
- onClick={() => setShowMoreMenu(!showMoreMenu)}
282
- className="px-2.5 py-1.5 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors flex items-center gap-1.5 text-xs"
283
- title="More actions"
284
- >
285
- <HiDotsVertical className="h-3.5 w-3.5" />
286
- <span className="hidden lg:inline">More</span>
287
- </button>
266
+ {/* Summary badge */}
267
+ {summary && (
268
+ <div
269
+ className={cn(
270
+ 'flex items-center gap-2 px-2.5 py-1.5 rounded border text-xs',
271
+ getSummaryVariantClasses(summary.variant)
272
+ )}
273
+ >
274
+ <span className="font-semibold whitespace-nowrap">
275
+ {summary.label}:
276
+ </span>
277
+ <span className="font-bold whitespace-nowrap">{summary.value}</span>
278
+ </div>
279
+ )}
280
+ </div>
288
281
 
289
- {/* Dropdown Menu */}
290
- {showMoreMenu && (
291
- <div className="absolute right-0 top-full mt-1 bg-white border border-gray-200 shadow-lg rounded py-1 min-w-[180px] z-20">
292
- {onSettings && (
293
- <button
294
- type={'button'}
295
- onClick={() => {
296
- onSettings();
297
- setShowMoreMenu(false);
298
- }}
299
- className="w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-xs transition-colors"
300
- >
301
- <HiCog className="h-3.5 w-3.5 text-gray-500" />
302
- <span className="text-gray-700">Settings</span>
303
- </button>
282
+ {/* Right section: Action buttons */}
283
+ <div className="flex items-center gap-2">
284
+ {/* Save status */}
285
+ {saveStatusDisplay && (
286
+ <span
287
+ className={cn(
288
+ 'text-xs flex items-center gap-1',
289
+ saveStatusDisplay.className
304
290
  )}
291
+ >
292
+ {saveStatusDisplay.text}
293
+ </span>
294
+ )}
305
295
 
306
- {onShowShortcuts && (
307
- <button
308
- type={'button'}
309
- onClick={() => {
310
- onShowShortcuts();
311
- setShowMoreMenu(false);
312
- }}
313
- className="w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-xs transition-colors"
314
- >
315
- <HiOutlineQuestionMarkCircle className="h-3.5 w-3.5 text-gray-500" />
316
- <span className="text-gray-700">Keyboard Shortcuts</span>
317
- </button>
296
+ {/* Manual save button (when auto-save is off) */}
297
+ {!autoSave && onSave && (
298
+ <button
299
+ type={'button'}
300
+ onClick={onSave}
301
+ disabled={!hasUnsavedChanges}
302
+ className={cn(
303
+ 'px-3 py-1.5 text-xs bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors flex items-center gap-1.5',
304
+ 'disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600'
318
305
  )}
306
+ >
307
+ <HiCheck className="h-3.5 w-3.5" />
308
+ Save
309
+ </button>
310
+ )}
319
311
 
320
- {/* Custom menu items */}
321
- {menuItems &&
322
- menuItems.length > 0 &&
323
- (onSettings || onShowShortcuts) && (
324
- <div className="border-t border-gray-100 my-1" />
312
+ {/* More menu dropdown */}
313
+ <div className="relative" ref={menuRef}>
314
+ <button
315
+ type={'button'}
316
+ onClick={() => setShowMoreMenu(!showMoreMenu)}
317
+ className="px-2.5 py-1.5 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors flex items-center gap-1.5 text-xs"
318
+ title="More actions"
319
+ >
320
+ <HiDotsVertical className="h-3.5 w-3.5" />
321
+ <span className="hidden lg:inline">More</span>
322
+ </button>
323
+
324
+ {/* Dropdown Menu */}
325
+ {showMoreMenu && (
326
+ <div className="absolute right-0 top-full mt-1 bg-white border border-gray-200 shadow-lg rounded py-1 min-w-[180px] z-50">
327
+ {onSettings && (
328
+ <button
329
+ type={'button'}
330
+ onClick={() => {
331
+ onSettings();
332
+ setShowMoreMenu(false);
333
+ }}
334
+ className="w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-xs transition-colors"
335
+ >
336
+ <HiCog className="h-3.5 w-3.5 text-gray-500" />
337
+ <span className="text-gray-700">Settings</span>
338
+ </button>
325
339
  )}
326
340
 
327
- {menuItems?.map((item) => (
328
- <button
329
- key={item.id}
330
- type={'button'}
331
- disabled={item.disabled}
332
- onClick={() => {
333
- item.onClick();
334
- setShowMoreMenu(false);
335
- }}
336
- className={cn(
337
- 'w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-xs transition-colors',
338
- item.disabled && 'opacity-50 cursor-not-allowed'
339
- )}
340
- >
341
- {item.icon && (
342
- <span className="h-3.5 w-3.5 text-gray-500 flex items-center justify-center">
343
- {item.icon}
344
- </span>
341
+ {onShowShortcuts && (
342
+ <button
343
+ type={'button'}
344
+ onClick={() => {
345
+ onShowShortcuts();
346
+ setShowMoreMenu(false);
347
+ }}
348
+ className="w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-xs transition-colors"
349
+ >
350
+ <HiOutlineQuestionMarkCircle className="h-3.5 w-3.5 text-gray-500" />
351
+ <span className="text-gray-700">Keyboard Shortcuts</span>
352
+ </button>
353
+ )}
354
+
355
+ {/* Custom menu items */}
356
+ {menuItems &&
357
+ menuItems.length > 0 &&
358
+ (onSettings || onShowShortcuts) && (
359
+ <div className="border-t border-gray-100 my-1" />
345
360
  )}
346
- <span className="text-gray-700">{item.label}</span>
347
- </button>
348
- ))}
349
- </div>
350
- )}
361
+
362
+ {menuItems?.map((item) => (
363
+ <button
364
+ key={item.id}
365
+ type={'button'}
366
+ disabled={item.disabled}
367
+ onClick={() => {
368
+ item.onClick();
369
+ setShowMoreMenu(false);
370
+ }}
371
+ className={cn(
372
+ 'w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-xs transition-colors',
373
+ item.disabled && 'opacity-50 cursor-not-allowed'
374
+ )}
375
+ >
376
+ {item.icon && (
377
+ <span className="h-3.5 w-3.5 text-gray-500 flex items-center justify-center">
378
+ {item.icon}
379
+ </span>
380
+ )}
381
+ <span className="text-gray-700">{item.label}</span>
382
+ </button>
383
+ ))}
384
+ </div>
385
+ )}
386
+ </div>
351
387
  </div>
352
388
  </div>
389
+
390
+ {/* Active filters panel */}
391
+ {showFiltersPanel && filters && columns && onClearFilter && onClearFilters && (
392
+ <ActiveFiltersDisplay
393
+ filters={filters}
394
+ columns={columns}
395
+ onClearFilter={onClearFilter}
396
+ onClearAllFilters={onClearFilters}
397
+ />
398
+ )}
353
399
  </div>
354
400
  );
355
401
  };
@@ -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 = 100;
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
  }