@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.
- package/.storybook/main.ts +27 -0
- package/.storybook/preview.tsx +28 -0
- package/.turbo/turbo-build.log +22 -0
- package/CHANGELOG.md +9 -0
- package/biome.json +3 -0
- package/dist/index.d.mts +687 -0
- package/dist/index.d.ts +687 -0
- package/dist/index.js +3459 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3417 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +51 -0
- package/postcss.config.js +5 -0
- package/src/components/ColorPickerPopover.tsx +73 -0
- package/src/components/ColumnHeaderActions.tsx +139 -0
- package/src/components/CommentModals.tsx +137 -0
- package/src/components/KeyboardShortcutsModal.tsx +119 -0
- package/src/components/RowIndexColumnHeader.tsx +70 -0
- package/src/components/Spreadsheet.stories.tsx +1146 -0
- package/src/components/Spreadsheet.tsx +1005 -0
- package/src/components/SpreadsheetCell.tsx +341 -0
- package/src/components/SpreadsheetFilterDropdown.tsx +341 -0
- package/src/components/SpreadsheetHeader.tsx +111 -0
- package/src/components/SpreadsheetSettingsModal.tsx +555 -0
- package/src/components/SpreadsheetToolbar.tsx +346 -0
- package/src/hooks/index.ts +40 -0
- package/src/hooks/useSpreadsheetComments.ts +132 -0
- package/src/hooks/useSpreadsheetFiltering.ts +379 -0
- package/src/hooks/useSpreadsheetHighlighting.ts +201 -0
- package/src/hooks/useSpreadsheetKeyboardShortcuts.ts +149 -0
- package/src/hooks/useSpreadsheetPinning.ts +203 -0
- package/src/hooks/useSpreadsheetUndoRedo.ts +167 -0
- package/src/index.ts +31 -0
- package/src/types.ts +612 -0
- package/src/utils.ts +16 -0
- package/tsconfig.json +30 -0
- 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';
|