@xcelsior/ui-spreadsheets 1.0.1

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 (37) hide show
  1. package/.storybook/main.ts +27 -0
  2. package/.storybook/preview.tsx +28 -0
  3. package/.turbo/turbo-build.log +22 -0
  4. package/CHANGELOG.md +9 -0
  5. package/biome.json +3 -0
  6. package/dist/index.d.mts +687 -0
  7. package/dist/index.d.ts +687 -0
  8. package/dist/index.js +3459 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/index.mjs +3417 -0
  11. package/dist/index.mjs.map +1 -0
  12. package/package.json +51 -0
  13. package/postcss.config.js +5 -0
  14. package/src/components/ColorPickerPopover.tsx +73 -0
  15. package/src/components/ColumnHeaderActions.tsx +139 -0
  16. package/src/components/CommentModals.tsx +137 -0
  17. package/src/components/KeyboardShortcutsModal.tsx +119 -0
  18. package/src/components/RowIndexColumnHeader.tsx +70 -0
  19. package/src/components/Spreadsheet.stories.tsx +1146 -0
  20. package/src/components/Spreadsheet.tsx +1005 -0
  21. package/src/components/SpreadsheetCell.tsx +341 -0
  22. package/src/components/SpreadsheetFilterDropdown.tsx +341 -0
  23. package/src/components/SpreadsheetHeader.tsx +111 -0
  24. package/src/components/SpreadsheetSettingsModal.tsx +555 -0
  25. package/src/components/SpreadsheetToolbar.tsx +346 -0
  26. package/src/hooks/index.ts +40 -0
  27. package/src/hooks/useSpreadsheetComments.ts +132 -0
  28. package/src/hooks/useSpreadsheetFiltering.ts +379 -0
  29. package/src/hooks/useSpreadsheetHighlighting.ts +201 -0
  30. package/src/hooks/useSpreadsheetKeyboardShortcuts.ts +149 -0
  31. package/src/hooks/useSpreadsheetPinning.ts +203 -0
  32. package/src/hooks/useSpreadsheetUndoRedo.ts +167 -0
  33. package/src/index.ts +31 -0
  34. package/src/types.ts +612 -0
  35. package/src/utils.ts +16 -0
  36. package/tsconfig.json +30 -0
  37. package/tsup.config.ts +12 -0
@@ -0,0 +1,111 @@
1
+ import type React from 'react';
2
+ import { HiChevronDown, HiChevronUp } from 'react-icons/hi';
3
+ import { cn } from '../utils';
4
+ import type { SpreadsheetHeaderProps } from '../types';
5
+ import { ColumnHeaderActions } from './ColumnHeaderActions';
6
+
7
+ const cellPaddingCompact = 'px-1.5 py-1';
8
+ const cellPaddingNormal = 'px-2 py-1.5';
9
+
10
+ /**
11
+ * SpreadsheetHeader component - A column header cell with sorting, filtering, and pinning capabilities.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <SpreadsheetHeader
16
+ * column={{ id: 'name', label: 'Name', sortable: true }}
17
+ * sortConfig={{ columnId: 'name', direction: 'asc' }}
18
+ * onClick={() => handleSort('name')}
19
+ * onFilterClick={() => setActiveFilter('name')}
20
+ * />
21
+ * ```
22
+ */
23
+ export const SpreadsheetHeader: React.FC<
24
+ SpreadsheetHeaderProps & { children?: React.ReactNode }
25
+ > = ({
26
+ column,
27
+ sortConfig,
28
+ hasActiveFilter = false,
29
+ isPinned = false,
30
+ pinSide,
31
+ leftOffset = 0,
32
+ rightOffset = 0,
33
+ highlightColor,
34
+ compactMode = false,
35
+ onClick,
36
+ onFilterClick,
37
+ onPinClick,
38
+ onHighlightClick,
39
+ className,
40
+ children,
41
+ }) => {
42
+ const isSorted = sortConfig?.columnId === column.id;
43
+ const sortDirection = isSorted ? sortConfig.direction : null;
44
+
45
+ const cellPadding = compactMode ? cellPaddingCompact : cellPaddingNormal;
46
+
47
+ // Build sticky positioning styles for pinned columns
48
+ const positionStyles: React.CSSProperties = {};
49
+ if (isPinned) {
50
+ positionStyles.position = 'sticky';
51
+ if (pinSide === 'left') {
52
+ positionStyles.left = `${leftOffset}px`;
53
+ } else if (pinSide === 'right') {
54
+ positionStyles.right = `${rightOffset}px`;
55
+ }
56
+ }
57
+
58
+ return (
59
+ <th
60
+ onClick={column.sortable ? onClick : undefined}
61
+ className={cn(
62
+ 'border border-gray-200 text-xs font-semibold text-gray-700 sticky group',
63
+ cellPadding,
64
+ column.align === 'right' && 'text-right',
65
+ column.align === 'center' && 'text-center',
66
+ column.sortable && 'cursor-pointer hover:bg-gray-100',
67
+ isPinned ? 'z-30' : 'z-20',
68
+ className
69
+ )}
70
+ style={{
71
+ backgroundColor: highlightColor || 'rgb(243 244 246)', // gray-100
72
+ minWidth: column.minWidth || column.width,
73
+ top: 0, // For sticky header
74
+ ...positionStyles,
75
+ }}
76
+ >
77
+ <div className="flex items-center justify-between gap-1">
78
+ {/* Label and sort indicator */}
79
+ <span className="flex-1 flex items-center gap-1">
80
+ {column.label}
81
+ {isSorted && (
82
+ <span className="text-blue-600">
83
+ {sortDirection === 'asc' ? (
84
+ <HiChevronUp className="h-3 w-3" />
85
+ ) : (
86
+ <HiChevronDown className="h-3 w-3" />
87
+ )}
88
+ </span>
89
+ )}
90
+ </span>
91
+
92
+ {/* Action buttons using unified ColumnHeaderActions */}
93
+ <ColumnHeaderActions
94
+ enableFiltering={column.filterable}
95
+ enableHighlighting={!!onHighlightClick}
96
+ enablePinning={column.pinnable !== false}
97
+ hasActiveFilter={hasActiveFilter}
98
+ hasActiveHighlight={!!highlightColor}
99
+ isPinned={isPinned}
100
+ onFilterClick={onFilterClick}
101
+ onHighlightClick={onHighlightClick}
102
+ onPinClick={onPinClick}
103
+ />
104
+ </div>
105
+ {/* Filter dropdown rendered inside th for proper positioning */}
106
+ {children}
107
+ </th>
108
+ );
109
+ };
110
+
111
+ SpreadsheetHeader.displayName = 'SpreadsheetHeader';
@@ -0,0 +1,555 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { HiX, HiCog, HiViewBoards, HiSortAscending, HiEye } from 'react-icons/hi';
3
+ import type { SpreadsheetColumn, SpreadsheetSortConfig } from '../types';
4
+
5
+ /**
6
+ * Settings configuration for the Spreadsheet
7
+ */
8
+ export interface SpreadsheetSettings {
9
+ /** Default pinned column IDs */
10
+ defaultPinnedColumns: string[];
11
+ /** Default sort configuration */
12
+ defaultSort: SpreadsheetSortConfig | null;
13
+ /** Default page size */
14
+ defaultPageSize: number;
15
+ /** Default zoom level */
16
+ defaultZoom: number;
17
+ /** Whether auto-save is enabled */
18
+ autoSave: boolean;
19
+ /** Whether compact view is enabled */
20
+ compactView: boolean;
21
+ /** Whether to show row index column */
22
+ showRowIndex?: boolean;
23
+ /** Whether row index column is pinned */
24
+ pinRowIndex?: boolean;
25
+ /** Row index column highlight color */
26
+ rowIndexHighlightColor?: string;
27
+ }
28
+
29
+ export interface SpreadsheetSettingsModalProps {
30
+ /** Whether the modal is open */
31
+ isOpen: boolean;
32
+ /** Callback to close the modal */
33
+ onClose: () => void;
34
+ /** Current settings */
35
+ settings: SpreadsheetSettings;
36
+ /** Callback to save settings */
37
+ onSave: (settings: SpreadsheetSettings) => void;
38
+ /** Available columns for pinning/sorting */
39
+ columns: SpreadsheetColumn[];
40
+ /** Title for the modal */
41
+ title?: string;
42
+ /** Available page size options */
43
+ pageSizeOptions?: number[];
44
+ }
45
+
46
+ const DEFAULT_SETTINGS: SpreadsheetSettings = {
47
+ defaultPinnedColumns: [],
48
+ defaultSort: null,
49
+ defaultPageSize: 25,
50
+ defaultZoom: 100,
51
+ autoSave: true,
52
+ compactView: false,
53
+ showRowIndex: true,
54
+ pinRowIndex: false,
55
+ rowIndexHighlightColor: undefined,
56
+ };
57
+
58
+ /**
59
+ * SpreadsheetSettingsModal - A generic settings modal for configuring spreadsheet options.
60
+ *
61
+ * @example
62
+ * ```tsx
63
+ * <SpreadsheetSettingsModal
64
+ * isOpen={showSettings}
65
+ * onClose={() => setShowSettings(false)}
66
+ * settings={currentSettings}
67
+ * onSave={(newSettings) => setSettings(newSettings)}
68
+ * columns={columns}
69
+ * title="Spreadsheet Settings"
70
+ * />
71
+ * ```
72
+ */
73
+ export const SpreadsheetSettingsModal: React.FC<SpreadsheetSettingsModalProps> = ({
74
+ isOpen,
75
+ onClose,
76
+ settings,
77
+ onSave,
78
+ columns,
79
+ title = 'Spreadsheet Settings',
80
+ pageSizeOptions = [25, 50, 100, 200],
81
+ }) => {
82
+ const [activeTab, setActiveTab] = useState<'columns' | 'sorting' | 'display'>('columns');
83
+ const [localSettings, setLocalSettings] = useState<SpreadsheetSettings>(settings);
84
+
85
+ // Update local settings when parent settings change
86
+ useEffect(() => {
87
+ setLocalSettings(settings);
88
+ }, [settings]);
89
+
90
+ if (!isOpen) return null;
91
+
92
+ const handleSave = () => {
93
+ onSave(localSettings);
94
+ onClose();
95
+ };
96
+
97
+ const handleReset = () => {
98
+ setLocalSettings(DEFAULT_SETTINGS);
99
+ };
100
+
101
+ const togglePinnedColumn = (columnId: string) => {
102
+ const isPinned = localSettings.defaultPinnedColumns.includes(columnId);
103
+ if (isPinned) {
104
+ setLocalSettings({
105
+ ...localSettings,
106
+ defaultPinnedColumns: localSettings.defaultPinnedColumns.filter(
107
+ (col) => col !== columnId
108
+ ),
109
+ });
110
+ } else {
111
+ setLocalSettings({
112
+ ...localSettings,
113
+ defaultPinnedColumns: [...localSettings.defaultPinnedColumns, columnId],
114
+ });
115
+ }
116
+ };
117
+
118
+ const handleSortChange = (columnId: string, direction: 'asc' | 'desc') => {
119
+ if (columnId === '') {
120
+ setLocalSettings({
121
+ ...localSettings,
122
+ defaultSort: null,
123
+ });
124
+ } else {
125
+ setLocalSettings({
126
+ ...localSettings,
127
+ defaultSort: { columnId, direction },
128
+ });
129
+ }
130
+ };
131
+
132
+ const handleDisplaySettingChange = (key: keyof SpreadsheetSettings, value: any) => {
133
+ const newSettings = {
134
+ ...localSettings,
135
+ [key]: value,
136
+ };
137
+ setLocalSettings(newSettings);
138
+
139
+ // Apply changes immediately for visual settings
140
+ if (key === 'compactView' || key === 'autoSave') {
141
+ onSave(newSettings);
142
+ }
143
+ };
144
+
145
+ const tabs = [
146
+ { id: 'columns' as const, label: 'Pinned Columns', Icon: HiViewBoards },
147
+ { id: 'sorting' as const, label: 'Default Sorting', Icon: HiSortAscending },
148
+ { id: 'display' as const, label: 'Display Options', Icon: HiEye },
149
+ ];
150
+
151
+ return (
152
+ <div
153
+ className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
154
+ role="dialog"
155
+ aria-modal="true"
156
+ aria-labelledby="settings-modal-title"
157
+ >
158
+ {/* Backdrop */}
159
+ <button
160
+ type="button"
161
+ className="absolute inset-0 cursor-default"
162
+ onClick={onClose}
163
+ onKeyDown={(e) => e.key === 'Escape' && onClose()}
164
+ aria-label="Close settings"
165
+ />
166
+ <div className="bg-white rounded-lg w-[90%] max-w-[700px] max-h-[90vh] flex flex-col shadow-xl relative z-10">
167
+ {/* Header */}
168
+ <div className="px-6 py-5 border-b border-gray-200 flex items-center justify-between">
169
+ <div className="flex items-center gap-3">
170
+ <HiCog className="h-6 w-6 text-blue-600" />
171
+ <h2 id="settings-modal-title" className="text-xl font-bold text-gray-900">
172
+ {title}
173
+ </h2>
174
+ </div>
175
+ <button
176
+ type="button"
177
+ onClick={onClose}
178
+ className="p-2 hover:bg-gray-100 rounded-lg transition-colors text-gray-500 hover:text-gray-700"
179
+ >
180
+ <HiX className="h-5 w-5" />
181
+ </button>
182
+ </div>
183
+
184
+ {/* Tabs */}
185
+ <div className="flex border-b border-gray-200 px-6">
186
+ {tabs.map((tab) => (
187
+ <button
188
+ key={tab.id}
189
+ type="button"
190
+ onClick={() => setActiveTab(tab.id)}
191
+ className={`px-4 py-3 flex items-center gap-2 text-sm font-medium transition-colors border-b-2 ${
192
+ activeTab === tab.id
193
+ ? 'text-blue-600 border-blue-600'
194
+ : 'text-gray-500 border-transparent hover:text-gray-700'
195
+ }`}
196
+ >
197
+ <tab.Icon className="h-4 w-4" />
198
+ {tab.label}
199
+ </button>
200
+ ))}
201
+ </div>
202
+
203
+ {/* Content */}
204
+ <div className="flex-1 overflow-auto p-6">
205
+ {/* Pinned Columns Tab */}
206
+ {activeTab === 'columns' && (
207
+ <div>
208
+ <div className="p-4 bg-blue-50 border border-blue-200 rounded-lg mb-4 flex gap-3">
209
+ <HiViewBoards className="h-4 w-4 text-blue-600 shrink-0 mt-0.5" />
210
+ <div>
211
+ <p className="text-sm font-semibold text-gray-900 mb-1">
212
+ About Pinned Columns
213
+ </p>
214
+ <p className="text-sm text-gray-600">
215
+ Pinned columns stay visible while you scroll horizontally
216
+ through the table.
217
+ </p>
218
+ </div>
219
+ </div>
220
+ <p className="text-sm text-gray-600 mb-4">
221
+ Select which columns should be pinned to the left by default.
222
+ </p>
223
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
224
+ {/* Row Index Column - Special handling */}
225
+ <button
226
+ type="button"
227
+ onClick={() =>
228
+ setLocalSettings({
229
+ ...localSettings,
230
+ pinRowIndex: !localSettings.pinRowIndex,
231
+ })
232
+ }
233
+ className={`flex items-center gap-2 p-3 rounded-lg border transition-colors text-left ${
234
+ localSettings.pinRowIndex !== false
235
+ ? 'bg-blue-50 border-blue-300 text-blue-700'
236
+ : 'bg-gray-50 border-gray-200 text-gray-700 hover:border-blue-300'
237
+ }`}
238
+ >
239
+ <HiViewBoards className="h-4 w-4 shrink-0" />
240
+ <span className="text-sm flex-1 truncate"># (Row Index)</span>
241
+ </button>
242
+ {columns.map((column) => {
243
+ const isPinned = localSettings.defaultPinnedColumns.includes(
244
+ column.id
245
+ );
246
+
247
+ return (
248
+ <button
249
+ key={column.id}
250
+ type="button"
251
+ onClick={() => togglePinnedColumn(column.id)}
252
+ className={`flex items-center gap-2 p-3 rounded-lg border transition-colors text-left ${
253
+ isPinned
254
+ ? 'bg-blue-50 border-blue-300 text-blue-700'
255
+ : 'bg-gray-50 border-gray-200 text-gray-700 hover:border-blue-300'
256
+ }`}
257
+ >
258
+ <HiViewBoards className="h-4 w-4 shrink-0" />
259
+ <span className="text-sm flex-1 truncate">
260
+ {column.label}
261
+ </span>
262
+ </button>
263
+ );
264
+ })}
265
+ </div>
266
+
267
+ {/* Row Index Highlight */}
268
+ <div className="mt-6 p-4 bg-amber-50 border border-amber-200 rounded-lg">
269
+ <p className="text-sm font-semibold text-gray-900 mb-3">
270
+ Row Index Column Highlight
271
+ </p>
272
+ <p className="text-sm text-gray-600 mb-3">
273
+ Apply a highlight color to the # (row index) column to make it
274
+ stand out.
275
+ </p>
276
+ <div className="flex items-center gap-2 flex-wrap">
277
+ {[
278
+ '#fef3c7',
279
+ '#dbeafe',
280
+ '#dcfce7',
281
+ '#fce7f3',
282
+ '#f3e8ff',
283
+ '#e0e7ff',
284
+ '#fed7d7',
285
+ '#c6f6d5',
286
+ ].map((color) => (
287
+ <button
288
+ key={color}
289
+ type="button"
290
+ onClick={() =>
291
+ setLocalSettings({
292
+ ...localSettings,
293
+ rowIndexHighlightColor:
294
+ localSettings.rowIndexHighlightColor ===
295
+ color
296
+ ? undefined
297
+ : color,
298
+ })
299
+ }
300
+ className={`w-8 h-8 rounded-lg border-2 transition-all ${
301
+ localSettings.rowIndexHighlightColor === color
302
+ ? 'border-blue-600 scale-110 ring-2 ring-blue-300'
303
+ : 'border-gray-300 hover:border-gray-400'
304
+ }`}
305
+ style={{ backgroundColor: color }}
306
+ title={
307
+ localSettings.rowIndexHighlightColor === color
308
+ ? 'Remove highlight'
309
+ : 'Apply highlight'
310
+ }
311
+ />
312
+ ))}
313
+ {localSettings.rowIndexHighlightColor && (
314
+ <button
315
+ type="button"
316
+ onClick={() =>
317
+ setLocalSettings({
318
+ ...localSettings,
319
+ rowIndexHighlightColor: undefined,
320
+ })
321
+ }
322
+ className="text-sm text-red-600 hover:text-red-700 ml-2 px-2 py-1 rounded hover:bg-red-50"
323
+ >
324
+ Clear
325
+ </button>
326
+ )}
327
+ </div>
328
+ </div>
329
+ </div>
330
+ )}
331
+
332
+ {/* Default Sorting Tab */}
333
+ {activeTab === 'sorting' && (
334
+ <div>
335
+ <p className="text-sm text-gray-600 mb-4">
336
+ Set the default column sorting when opening the spreadsheet.
337
+ </p>
338
+
339
+ <div className="space-y-4">
340
+ <div>
341
+ <label className="block text-sm font-medium text-gray-900 mb-2">
342
+ Sort Column
343
+ </label>
344
+ <select
345
+ value={localSettings.defaultSort?.columnId || ''}
346
+ onChange={(e) =>
347
+ handleSortChange(
348
+ e.target.value,
349
+ localSettings.defaultSort?.direction || 'asc'
350
+ )
351
+ }
352
+ className="w-full p-3 text-sm bg-white border border-gray-300 rounded-lg text-gray-900 cursor-pointer focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
353
+ >
354
+ <option value="">No default sorting</option>
355
+ {columns
356
+ .filter((col) => col.sortable !== false)
357
+ .map((column) => (
358
+ <option key={column.id} value={column.id}>
359
+ {column.label}
360
+ </option>
361
+ ))}
362
+ </select>
363
+ </div>
364
+
365
+ {localSettings.defaultSort && (
366
+ <div>
367
+ <label className="block text-sm font-medium text-gray-900 mb-2">
368
+ Sort Direction
369
+ </label>
370
+ <select
371
+ value={localSettings.defaultSort.direction}
372
+ onChange={(e) =>
373
+ handleSortChange(
374
+ localSettings.defaultSort!.columnId,
375
+ e.target.value as 'asc' | 'desc'
376
+ )
377
+ }
378
+ className="w-full p-3 text-sm bg-white border border-gray-300 rounded-lg text-gray-900 cursor-pointer focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
379
+ >
380
+ <option value="asc">Ascending (A → Z, 0 → 9)</option>
381
+ <option value="desc">Descending (Z → A, 9 → 0)</option>
382
+ </select>
383
+ </div>
384
+ )}
385
+ </div>
386
+ </div>
387
+ )}
388
+
389
+ {/* Display Options Tab */}
390
+ {activeTab === 'display' && (
391
+ <div className="space-y-5">
392
+ <p className="text-sm text-gray-600">
393
+ Customize the display and behavior of the spreadsheet.
394
+ </p>
395
+
396
+ {/* Row Index Options */}
397
+ <div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
398
+ <p className="text-sm font-semibold text-gray-900 mb-3">
399
+ Row Index Column
400
+ </p>
401
+ <div className="space-y-3">
402
+ {/* Show Row Index */}
403
+ <label className="flex items-center gap-3 cursor-pointer">
404
+ <input
405
+ type="checkbox"
406
+ checked={localSettings.showRowIndex !== false}
407
+ onChange={(e) =>
408
+ setLocalSettings({
409
+ ...localSettings,
410
+ showRowIndex: e.target.checked,
411
+ })
412
+ }
413
+ className="w-4 h-4 cursor-pointer rounded border-gray-300 text-blue-600 focus:ring-blue-500"
414
+ />
415
+ <span className="text-sm text-gray-700">
416
+ Show row index (#) column
417
+ </span>
418
+ </label>
419
+ <p className="text-xs text-gray-500 ml-7">
420
+ Tip: Use the "Pinned Columns" tab to pin and highlight the
421
+ row index column.
422
+ </p>
423
+ </div>
424
+ </div>
425
+
426
+ {/* Page Size */}
427
+ <div>
428
+ <label className="block text-sm font-medium text-gray-900 mb-2">
429
+ Default Page Size
430
+ </label>
431
+ <select
432
+ value={localSettings.defaultPageSize}
433
+ onChange={(e) =>
434
+ setLocalSettings({
435
+ ...localSettings,
436
+ defaultPageSize: parseInt(e.target.value, 10),
437
+ })
438
+ }
439
+ className="w-full p-3 text-sm bg-white border border-gray-300 rounded-lg text-gray-900 cursor-pointer focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
440
+ >
441
+ {pageSizeOptions.map((size) => (
442
+ <option key={size} value={size}>
443
+ {size} rows
444
+ </option>
445
+ ))}
446
+ </select>
447
+ </div>
448
+
449
+ {/* Default Zoom */}
450
+ <div>
451
+ <label className="block text-sm font-medium text-gray-900 mb-2">
452
+ Default Zoom Level: {localSettings.defaultZoom}%
453
+ </label>
454
+ <input
455
+ type="range"
456
+ min="50"
457
+ max="150"
458
+ step="10"
459
+ value={localSettings.defaultZoom}
460
+ onChange={(e) =>
461
+ setLocalSettings({
462
+ ...localSettings,
463
+ defaultZoom: parseInt(e.target.value, 10),
464
+ })
465
+ }
466
+ className="w-full cursor-pointer"
467
+ />
468
+ <div className="flex justify-between mt-1 text-xs text-gray-500">
469
+ <span>50%</span>
470
+ <span>150%</span>
471
+ </div>
472
+ </div>
473
+
474
+ {/* Toggle Options */}
475
+ <div className="space-y-3">
476
+ {/* Auto Save */}
477
+ <label className="flex items-center gap-3 p-4 bg-gray-50 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-100 transition-colors">
478
+ <input
479
+ type="checkbox"
480
+ checked={localSettings.autoSave}
481
+ onChange={(e) =>
482
+ handleDisplaySettingChange('autoSave', e.target.checked)
483
+ }
484
+ className="w-5 h-5 cursor-pointer rounded border-gray-300 text-blue-600 focus:ring-blue-500"
485
+ />
486
+ <div className="flex-1">
487
+ <div className="text-sm font-medium text-gray-900">
488
+ Auto-save changes
489
+ </div>
490
+ <div className="text-sm text-gray-500 mt-0.5">
491
+ Automatically save changes without confirmation
492
+ </div>
493
+ </div>
494
+ </label>
495
+
496
+ {/* Compact View */}
497
+ <label className="flex items-center gap-3 p-4 bg-gray-50 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-100 transition-colors">
498
+ <input
499
+ type="checkbox"
500
+ checked={localSettings.compactView}
501
+ onChange={(e) =>
502
+ handleDisplaySettingChange(
503
+ 'compactView',
504
+ e.target.checked
505
+ )
506
+ }
507
+ className="w-5 h-5 cursor-pointer rounded border-gray-300 text-blue-600 focus:ring-blue-500"
508
+ />
509
+ <div className="flex-1">
510
+ <div className="text-sm font-medium text-gray-900">
511
+ Compact view
512
+ </div>
513
+ <div className="text-sm text-gray-500 mt-0.5">
514
+ Reduce padding and spacing to show more rows on screen
515
+ </div>
516
+ </div>
517
+ </label>
518
+ </div>
519
+ </div>
520
+ )}
521
+ </div>
522
+
523
+ {/* Footer */}
524
+ <div className="px-6 py-4 border-t border-gray-200 flex justify-between items-center gap-3">
525
+ <button
526
+ type="button"
527
+ onClick={handleReset}
528
+ className="px-4 py-2.5 text-sm font-medium text-red-600 bg-red-50 border border-red-200 rounded-lg hover:bg-red-100 transition-colors"
529
+ >
530
+ Reset to Defaults
531
+ </button>
532
+
533
+ <div className="flex gap-2">
534
+ <button
535
+ type="button"
536
+ onClick={onClose}
537
+ className="px-4 py-2.5 text-sm font-medium text-gray-700 bg-gray-100 border border-gray-200 rounded-lg hover:bg-gray-200 transition-colors"
538
+ >
539
+ Cancel
540
+ </button>
541
+ <button
542
+ type="button"
543
+ onClick={handleSave}
544
+ className="px-4 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors"
545
+ >
546
+ Save Settings
547
+ </button>
548
+ </div>
549
+ </div>
550
+ </div>
551
+ </div>
552
+ );
553
+ };
554
+
555
+ SpreadsheetSettingsModal.displayName = 'SpreadsheetSettingsModal';