@xcelsior/ui-spreadsheets 1.1.15 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcelsior/ui-spreadsheets",
3
- "version": "1.1.15",
3
+ "version": "1.1.16",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -286,6 +286,29 @@ export function Spreadsheet<T extends Record<string, any>>({
286
286
  [controlledPageSize, controlledCurrentPage, onPageChange]
287
287
  );
288
288
 
289
+ // Reset pagination to page 1 when filters change
290
+ const resetPaginationToFirstPage = useCallback(() => {
291
+ if (controlledCurrentPage === undefined) {
292
+ setInternalCurrentPage(1);
293
+ }
294
+ onPageChange?.(1, pageSize);
295
+ }, [controlledCurrentPage, onPageChange, pageSize]);
296
+
297
+ // Wrapper for handleFilterChange that resets pagination
298
+ const handleFilterChangeWithReset = useCallback(
299
+ (columnId: string, filter: Parameters<typeof handleFilterChange>[1]) => {
300
+ handleFilterChange(columnId, filter);
301
+ resetPaginationToFirstPage();
302
+ },
303
+ [handleFilterChange, resetPaginationToFirstPage]
304
+ );
305
+
306
+ // Wrapper for clearAllFilters that resets pagination
307
+ const clearAllFiltersWithReset = useCallback(() => {
308
+ clearAllFilters();
309
+ resetPaginationToFirstPage();
310
+ }, [clearAllFilters, resetPaginationToFirstPage]);
311
+
289
312
  // Sync sortConfig to spreadsheetSettings when sorting changes
290
313
  useEffect(() => {
291
314
  setSpreadsheetSettings((prev) => ({
@@ -828,10 +851,10 @@ export function Spreadsheet<T extends Record<string, any>>({
828
851
  saveStatus={saveStatus}
829
852
  autoSave={spreadsheetSettings.autoSave}
830
853
  hasActiveFilters={hasActiveFilters}
831
- onClearFilters={clearAllFilters}
854
+ onClearFilters={clearAllFiltersWithReset}
832
855
  filters={filters}
833
856
  columns={columns}
834
- onClearFilter={(columnId) => handleFilterChange(columnId, undefined)}
857
+ onClearFilter={(columnId) => handleFilterChangeWithReset(columnId, undefined)}
835
858
  showFiltersPanel={showFiltersPanel}
836
859
  onToggleFiltersPanel={() => setShowFiltersPanel(!showFiltersPanel)}
837
860
  onZoomIn={() => setZoom((z) => Math.min(z + 10, 200))}
@@ -1013,7 +1036,7 @@ export function Spreadsheet<T extends Record<string, any>>({
1013
1036
  column={column}
1014
1037
  filter={filters[column.id]}
1015
1038
  onFilterChange={(filter) =>
1016
- handleFilterChange(column.id, filter)
1039
+ handleFilterChangeWithReset(column.id, filter)
1017
1040
  }
1018
1041
  onClose={() => setActiveFilterColumn(null)}
1019
1042
  />
@@ -154,235 +154,238 @@ export const SpreadsheetToolbar: React.FC<SpreadsheetToolbarProps> = ({
154
154
  className
155
155
  )}
156
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">
161
- <button
162
- type={'button'}
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)`}
172
- >
173
- <HiReply className="h-4 w-4" />
174
- </button>
175
- <button
176
- type={'button'}
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)' }}
187
- >
188
- <HiReply className="h-4 w-4" />
189
- </button>
190
- </div>
191
-
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>
218
- </div>
219
- </div>
220
-
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>
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">
229
161
  <button
230
162
  type={'button'}
231
- onClick={onClearSelection}
232
- className="p-0.5 hover:bg-blue-700 rounded"
233
- 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)`}
234
172
  >
235
- <HiX className="h-3 w-3" />
173
+ <HiReply className="h-4 w-4" />
174
+ </button>
175
+ <button
176
+ type={'button'}
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)' }}
187
+ >
188
+ <HiReply className="h-4 w-4" />
236
189
  </button>
237
190
  </div>
238
- )}
239
-
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' : ''} active
256
- </span>
257
- {showFiltersPanel ? (
258
- <HiChevronUp className="h-3 w-3" />
259
- ) : (
260
- <HiChevronDown className="h-3 w-3" />
261
- )}
262
- </button>
263
- )}
264
191
 
265
- {/* Summary badge */}
266
- {summary && (
267
- <div
268
- className={cn(
269
- 'flex items-center gap-2 px-2.5 py-1.5 rounded border text-xs',
270
- getSummaryVariantClasses(summary.variant)
271
- )}
272
- >
273
- <span className="font-semibold whitespace-nowrap">{summary.label}:</span>
274
- <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>
275
218
  </div>
276
- )}
277
- </div>
219
+ </div>
278
220
 
279
- {/* Right section: Action buttons */}
280
- <div className="flex items-center gap-2">
281
- {/* Save status */}
282
- {saveStatusDisplay && (
283
- <span
284
- className={cn(
285
- 'text-xs flex items-center gap-1',
286
- saveStatusDisplay.className
287
- )}
288
- >
289
- {saveStatusDisplay.text}
290
- </span>
291
- )}
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
+ )}
292
239
 
293
- {/* Manual save button (when auto-save is off) */}
294
- {!autoSave && onSave && (
295
- <button
296
- type={'button'}
297
- onClick={onSave}
298
- disabled={!hasUnsavedChanges}
299
- className={cn(
300
- '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',
301
- 'disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600'
302
- )}
303
- >
304
- <HiCheck className="h-3.5 w-3.5" />
305
- Save
306
- </button>
307
- )}
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
+ )}
308
265
 
309
- {/* More menu dropdown */}
310
- <div className="relative" ref={menuRef}>
311
- <button
312
- type={'button'}
313
- onClick={() => setShowMoreMenu(!showMoreMenu)}
314
- 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"
315
- title="More actions"
316
- >
317
- <HiDotsVertical className="h-3.5 w-3.5" />
318
- <span className="hidden lg:inline">More</span>
319
- </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>
320
281
 
321
- {/* Dropdown Menu */}
322
- {showMoreMenu && (
323
- <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">
324
- {onSettings && (
325
- <button
326
- type={'button'}
327
- onClick={() => {
328
- onSettings();
329
- setShowMoreMenu(false);
330
- }}
331
- className="w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-xs transition-colors"
332
- >
333
- <HiCog className="h-3.5 w-3.5 text-gray-500" />
334
- <span className="text-gray-700">Settings</span>
335
- </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
336
290
  )}
291
+ >
292
+ {saveStatusDisplay.text}
293
+ </span>
294
+ )}
337
295
 
338
- {onShowShortcuts && (
339
- <button
340
- type={'button'}
341
- onClick={() => {
342
- onShowShortcuts();
343
- setShowMoreMenu(false);
344
- }}
345
- className="w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-xs transition-colors"
346
- >
347
- <HiOutlineQuestionMarkCircle className="h-3.5 w-3.5 text-gray-500" />
348
- <span className="text-gray-700">Keyboard Shortcuts</span>
349
- </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'
350
305
  )}
306
+ >
307
+ <HiCheck className="h-3.5 w-3.5" />
308
+ Save
309
+ </button>
310
+ )}
311
+
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>
351
323
 
352
- {/* Custom menu items */}
353
- {menuItems &&
354
- menuItems.length > 0 &&
355
- (onSettings || onShowShortcuts) && (
356
- <div className="border-t border-gray-100 my-1" />
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>
357
339
  )}
358
340
 
359
- {menuItems?.map((item) => (
360
- <button
361
- key={item.id}
362
- type={'button'}
363
- disabled={item.disabled}
364
- onClick={() => {
365
- item.onClick();
366
- setShowMoreMenu(false);
367
- }}
368
- className={cn(
369
- 'w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-xs transition-colors',
370
- item.disabled && 'opacity-50 cursor-not-allowed'
371
- )}
372
- >
373
- {item.icon && (
374
- <span className="h-3.5 w-3.5 text-gray-500 flex items-center justify-center">
375
- {item.icon}
376
- </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" />
377
360
  )}
378
- <span className="text-gray-700">{item.label}</span>
379
- </button>
380
- ))}
381
- </div>
382
- )}
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>
383
387
  </div>
384
388
  </div>
385
- </div>
386
389
 
387
390
  {/* Active filters panel */}
388
391
  {showFiltersPanel && filters && columns && onClearFilter && onClearFilters && (