@xcelsior/ui-spreadsheets 1.3.5 → 1.4.0

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,6 @@
1
- import { useState, useEffect } from 'react';
2
- import { HiX, HiCog, HiViewBoards, HiSortAscending, HiEye } from 'react-icons/hi';
3
- import type { SpreadsheetColumn, SpreadsheetSortConfig } from '../types';
1
+ import { useState, useEffect, useMemo } from 'react';
2
+ import { HiX, HiCog, HiViewBoards, HiSortAscending, HiEye, HiEyeOff } from 'react-icons/hi';
3
+ import type { SpreadsheetColumn, SpreadsheetColumnGroup, SpreadsheetSortConfig } from '../types';
4
4
 
5
5
  /**
6
6
  * Settings configuration for the Spreadsheet
@@ -22,6 +22,8 @@ export interface SpreadsheetSettings {
22
22
  columnWidths?: Record<string, number>;
23
23
  /** Column IDs with duplicate checking enabled */
24
24
  duplicateCheckColumns?: string[];
25
+ /** Column IDs that are hidden from view */
26
+ hiddenColumns?: string[];
25
27
  }
26
28
 
27
29
  export interface SpreadsheetSettingsModalProps {
@@ -35,6 +37,8 @@ export interface SpreadsheetSettingsModalProps {
35
37
  onSave: (settings: SpreadsheetSettings) => void;
36
38
  /** Available columns for pinning/sorting */
37
39
  columns: SpreadsheetColumn[];
40
+ /** Column groups for organized visibility toggles */
41
+ columnGroups?: SpreadsheetColumnGroup[];
38
42
  /** Title for the modal */
39
43
  title?: string;
40
44
  /** Available page size options */
@@ -71,10 +75,11 @@ export const SpreadsheetSettingsModal: React.FC<SpreadsheetSettingsModalProps> =
71
75
  settings,
72
76
  onSave,
73
77
  columns,
78
+ columnGroups,
74
79
  title = 'Spreadsheet Settings',
75
80
  pageSizeOptions = [25, 50, 100, 200],
76
81
  }) => {
77
- const [activeTab, setActiveTab] = useState<'columns' | 'sorting' | 'display'>('columns');
82
+ const [activeTab, setActiveTab] = useState<'visibility' | 'columns' | 'sorting' | 'display'>('visibility');
78
83
  const [localSettings, setLocalSettings] = useState<SpreadsheetSettings>(settings);
79
84
 
80
85
  // Update local settings when parent settings change
@@ -82,6 +87,18 @@ export const SpreadsheetSettingsModal: React.FC<SpreadsheetSettingsModalProps> =
82
87
  setLocalSettings(settings);
83
88
  }, [settings]);
84
89
 
90
+ const hiddenColumnsSet = useMemo(
91
+ () => new Set(localSettings.hiddenColumns ?? []),
92
+ [localSettings.hiddenColumns]
93
+ );
94
+
95
+ /** Columns not assigned to any group */
96
+ const ungroupedColumns = useMemo(() => {
97
+ if (!columnGroups?.length) return columns;
98
+ const groupedIds = new Set(columnGroups.flatMap((g) => g.columns));
99
+ return columns.filter((c) => !groupedIds.has(c.id));
100
+ }, [columns, columnGroups]);
101
+
85
102
  if (!isOpen) return null;
86
103
 
87
104
  const handleSave = () => {
@@ -137,7 +154,28 @@ export const SpreadsheetSettingsModal: React.FC<SpreadsheetSettingsModalProps> =
137
154
  }
138
155
  };
139
156
 
157
+ const hiddenCount = hiddenColumnsSet.size;
158
+
159
+ const toggleColumnVisibility = (columnId: string) => {
160
+ const isHidden = hiddenColumnsSet.has(columnId);
161
+ const newHidden = isHidden
162
+ ? (localSettings.hiddenColumns ?? []).filter((id) => id !== columnId)
163
+ : [...(localSettings.hiddenColumns ?? []), columnId];
164
+ setLocalSettings({ ...localSettings, hiddenColumns: newHidden });
165
+ };
166
+
167
+ const setGroupVisibility = (groupColumnIds: string[], visible: boolean) => {
168
+ const currentHidden = new Set(localSettings.hiddenColumns ?? []);
169
+ if (visible) {
170
+ groupColumnIds.forEach((id) => currentHidden.delete(id));
171
+ } else {
172
+ groupColumnIds.forEach((id) => currentHidden.add(id));
173
+ }
174
+ setLocalSettings({ ...localSettings, hiddenColumns: Array.from(currentHidden) });
175
+ };
176
+
140
177
  const tabs = [
178
+ { id: 'visibility' as const, label: `Column Visibility${hiddenCount > 0 ? ` (${hiddenCount})` : ''}`, Icon: HiEyeOff },
141
179
  { id: 'columns' as const, label: 'Pinned Columns', Icon: HiViewBoards },
142
180
  { id: 'sorting' as const, label: 'Default Sorting', Icon: HiSortAscending },
143
181
  { id: 'display' as const, label: 'Display Options', Icon: HiEye },
@@ -158,7 +196,7 @@ export const SpreadsheetSettingsModal: React.FC<SpreadsheetSettingsModalProps> =
158
196
  onKeyDown={(e) => e.key === 'Escape' && onClose()}
159
197
  aria-label="Close settings"
160
198
  />
161
- <div className="bg-white rounded-lg w-[90%] max-w-[700px] max-h-[90vh] flex flex-col shadow-xl relative z-10">
199
+ <div className="bg-white rounded-lg w-[70%] max-w-1/2 max-h-[70vh] flex flex-col shadow-xl relative z-10">
162
200
  {/* Header */}
163
201
  <div className="px-6 py-5 border-b border-gray-200 flex items-center justify-between">
164
202
  <div className="flex items-center gap-3">
@@ -197,6 +235,160 @@ export const SpreadsheetSettingsModal: React.FC<SpreadsheetSettingsModalProps> =
197
235
 
198
236
  {/* Content */}
199
237
  <div className="flex-1 overflow-auto p-6">
238
+ {/* Column Visibility Tab */}
239
+ {activeTab === 'visibility' && (
240
+ <div>
241
+ <div className="p-4 bg-amber-50 border border-amber-200 rounded-lg mb-4 flex gap-3">
242
+ <HiEyeOff className="h-4 w-4 text-amber-600 shrink-0 mt-0.5" />
243
+ <div>
244
+ <p className="text-sm font-semibold text-gray-900 mb-1">
245
+ Column Visibility
246
+ </p>
247
+ <p className="text-sm text-gray-600">
248
+ Toggle columns on or off. Hidden columns are removed from the table but their data is preserved.
249
+ {hiddenCount > 0 && (
250
+ <span className="ml-1 font-medium text-amber-700">
251
+ {hiddenCount} column{hiddenCount !== 1 ? 's' : ''} hidden.
252
+ </span>
253
+ )}
254
+ </p>
255
+ </div>
256
+ </div>
257
+
258
+ {/* Show All / Hide All quick actions */}
259
+ <div className="flex gap-2 mb-4">
260
+ <button
261
+ type="button"
262
+ onClick={() => setLocalSettings({ ...localSettings, hiddenColumns: [] })}
263
+ className="px-3 py-1.5 text-xs font-medium text-green-700 bg-green-50 border border-green-200 rounded-lg hover:bg-green-100 transition-colors"
264
+ >
265
+ Show All
266
+ </button>
267
+ <button
268
+ type="button"
269
+ onClick={() =>
270
+ setLocalSettings({
271
+ ...localSettings,
272
+ hiddenColumns: columns.map((c) => c.id),
273
+ })
274
+ }
275
+ className="px-3 py-1.5 text-xs font-medium text-red-700 bg-red-50 border border-red-200 rounded-lg hover:bg-red-100 transition-colors"
276
+ >
277
+ Hide All
278
+ </button>
279
+ </div>
280
+
281
+ {/* Grouped columns */}
282
+ {columnGroups?.map((group) => {
283
+ const groupCols = columns.filter((c) => group.columns.includes(c.id));
284
+ if (groupCols.length === 0) return null;
285
+ const hiddenInGroup = groupCols.filter((c) => hiddenColumnsSet.has(c.id)).length;
286
+ const allHidden = hiddenInGroup === groupCols.length;
287
+ const allVisible = hiddenInGroup === 0;
288
+
289
+ return (
290
+ <div key={group.id} className="mb-4">
291
+ <div className="flex items-center justify-between mb-2">
292
+ <div className="flex items-center gap-2">
293
+ <div
294
+ className="w-3 h-3 rounded-sm"
295
+ style={{ backgroundColor: group.headerColor }}
296
+ />
297
+ <span className="text-sm font-medium text-gray-900">
298
+ {group.label}
299
+ </span>
300
+ <span className="text-xs text-gray-500">
301
+ ({groupCols.length - hiddenInGroup}/{groupCols.length})
302
+ </span>
303
+ </div>
304
+ <div className="flex gap-1">
305
+ <button
306
+ type="button"
307
+ onClick={() => setGroupVisibility(group.columns, true)}
308
+ disabled={allVisible}
309
+ className="px-2 py-0.5 text-xs text-green-700 hover:bg-green-50 rounded transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
310
+ >
311
+ Show all
312
+ </button>
313
+ <button
314
+ type="button"
315
+ onClick={() => setGroupVisibility(group.columns, false)}
316
+ disabled={allHidden}
317
+ className="px-2 py-0.5 text-xs text-red-700 hover:bg-red-50 rounded transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
318
+ >
319
+ Hide all
320
+ </button>
321
+ </div>
322
+ </div>
323
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
324
+ {groupCols.map((column) => {
325
+ const isHidden = hiddenColumnsSet.has(column.id);
326
+ return (
327
+ <button
328
+ key={column.id}
329
+ type="button"
330
+ onClick={() => toggleColumnVisibility(column.id)}
331
+ className={`flex items-center gap-2 p-3 rounded-lg border transition-colors text-left ${
332
+ isHidden
333
+ ? 'bg-gray-100 border-gray-200 text-gray-400'
334
+ : 'bg-green-50 border-green-300 text-green-800'
335
+ }`}
336
+ >
337
+ {isHidden ? (
338
+ <HiEyeOff className="h-4 w-4 shrink-0" />
339
+ ) : (
340
+ <HiEye className="h-4 w-4 shrink-0" />
341
+ )}
342
+ <span className="text-sm flex-1 truncate">
343
+ {column.label}
344
+ </span>
345
+ </button>
346
+ );
347
+ })}
348
+ </div>
349
+ </div>
350
+ );
351
+ })}
352
+
353
+ {/* Ungrouped columns */}
354
+ {ungroupedColumns.length > 0 && (
355
+ <div className="mb-4">
356
+ {columnGroups?.length ? (
357
+ <div className="flex items-center gap-2 mb-2">
358
+ <span className="text-sm font-medium text-gray-900">Other</span>
359
+ </div>
360
+ ) : null}
361
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
362
+ {ungroupedColumns.map((column) => {
363
+ const isHidden = hiddenColumnsSet.has(column.id);
364
+ return (
365
+ <button
366
+ key={column.id}
367
+ type="button"
368
+ onClick={() => toggleColumnVisibility(column.id)}
369
+ className={`flex items-center gap-2 p-3 rounded-lg border transition-colors text-left ${
370
+ isHidden
371
+ ? 'bg-gray-100 border-gray-200 text-gray-400'
372
+ : 'bg-green-50 border-green-300 text-green-800'
373
+ }`}
374
+ >
375
+ {isHidden ? (
376
+ <HiEyeOff className="h-4 w-4 shrink-0" />
377
+ ) : (
378
+ <HiEye className="h-4 w-4 shrink-0" />
379
+ )}
380
+ <span className="text-sm flex-1 truncate">
381
+ {column.label}
382
+ </span>
383
+ </button>
384
+ );
385
+ })}
386
+ </div>
387
+ </div>
388
+ )}
389
+ </div>
390
+ )}
391
+
200
392
  {/* Pinned Columns Tab */}
201
393
  {activeTab === 'columns' && (
202
394
  <div>
@@ -68,6 +68,9 @@ export const SpreadsheetToolbar: React.FC<SpreadsheetToolbarProps> = ({
68
68
  onClearFilter,
69
69
  showFiltersPanel,
70
70
  onToggleFiltersPanel,
71
+ hiddenColumnCount = 0,
72
+ onShowAllColumns,
73
+ onManageColumns,
71
74
  className,
72
75
  }) => {
73
76
  const [showMoreMenu, setShowMoreMenu] = React.useState(false);
@@ -309,6 +312,28 @@ export const SpreadsheetToolbar: React.FC<SpreadsheetToolbarProps> = ({
309
312
  </button>
310
313
  )}
311
314
 
315
+ {/* Hidden columns indicator */}
316
+ {hiddenColumnCount > 0 && (
317
+ <div className="flex items-center gap-1.5 px-2.5 py-1.5 bg-amber-50 border border-amber-200 rounded text-xs text-amber-700">
318
+ <span className="font-medium">{hiddenColumnCount} hidden</span>
319
+ <button
320
+ type="button"
321
+ onClick={onShowAllColumns}
322
+ className="text-amber-800 underline hover:text-amber-900 transition-colors"
323
+ >
324
+ Show
325
+ </button>
326
+ <span className="text-amber-300">|</span>
327
+ <button
328
+ type="button"
329
+ onClick={onManageColumns}
330
+ className="text-amber-800 underline hover:text-amber-900 transition-colors"
331
+ >
332
+ Manage
333
+ </button>
334
+ </div>
335
+ )}
336
+
312
337
  {/* More menu dropdown */}
313
338
  <div className="relative" ref={menuRef}>
314
339
  <button
@@ -15,6 +15,8 @@ export interface UseSpreadsheetPinningOptions<T> {
15
15
  defaultPinnedRightColumns?: string[];
16
16
  /** Function to get the current (possibly resized) width for a column */
17
17
  getColumnWidth?: (columnId: string, defaultWidth?: number) => number | undefined;
18
+ /** Column IDs that are hidden from view */
19
+ hiddenColumns?: string[];
18
20
  }
19
21
 
20
22
  export interface UseSpreadsheetPinningReturn<T> {
@@ -40,6 +42,7 @@ export function useSpreadsheetPinning<T>({
40
42
  defaultPinnedColumns = [],
41
43
  defaultPinnedRightColumns = [],
42
44
  getColumnWidth,
45
+ hiddenColumns = [],
43
46
  }: UseSpreadsheetPinningOptions<T>): UseSpreadsheetPinningReturn<T> {
44
47
  const [pinnedColumns, setPinnedColumns] = useState<Map<string, 'left' | 'right'>>(() => {
45
48
  const map = new Map<string, 'left' | 'right'>();
@@ -139,12 +142,20 @@ export function useSpreadsheetPinning<T>({
139
142
  });
140
143
  }, []);
141
144
 
142
- // Visible columns filtered by collapse, natural order preserved
145
+ // Stable reference for hidden columns set
146
+ const hiddenColumnsSet = useMemo(() => new Set(hiddenColumns), [hiddenColumns]);
147
+
148
+ // Visible columns — filtered by hidden columns and collapse, natural order preserved
143
149
  const visibleColumns = useMemo(() => {
144
150
  if (!columns || !Array.isArray(columns) || columns.length === 0) return [];
145
151
 
146
152
  let result: SpreadsheetColumn<T>[] = [...columns];
147
153
 
154
+ // Filter out hidden columns
155
+ if (hiddenColumnsSet.size > 0) {
156
+ result = result.filter((column) => !hiddenColumnsSet.has(column.id));
157
+ }
158
+
148
159
  if (columnGroups && Array.isArray(columnGroups)) {
149
160
  result = result.filter((column) => {
150
161
  const group = columnGroups.find((g) => g.columns.includes(column.id));
@@ -155,7 +166,7 @@ export function useSpreadsheetPinning<T>({
155
166
  }
156
167
 
157
168
  return result;
158
- }, [columns, columnGroups, collapsedGroups, pinnedColumns]);
169
+ }, [columns, columnGroups, collapsedGroups, pinnedColumns, hiddenColumnsSet]);
159
170
 
160
171
  // Measure after render when layout changes
161
172
  useEffect(() => {
package/src/types.ts CHANGED
@@ -415,6 +415,8 @@ export interface SpreadsheetProps<T = any> {
415
415
  compactView?: boolean;
416
416
  /** Persisted column widths (columnId → width in px) */
417
417
  columnWidths?: Record<string, number>;
418
+ /** Column IDs that are hidden from view */
419
+ hiddenColumns?: string[];
418
420
  };
419
421
  /** Callback when spreadsheet settings are changed by the user */
420
422
  onSettingsChange?: (settings: {
@@ -425,6 +427,7 @@ export interface SpreadsheetProps<T = any> {
425
427
  defaultPinnedColumns?: string[];
426
428
  defaultPinnedRightColumns?: string[];
427
429
  defaultSort?: SpreadsheetSortConfig | null;
430
+ hiddenColumns?: string[];
428
431
  }) => void;
429
432
  /** Loading state */
430
433
  isLoading?: boolean;
@@ -623,6 +626,8 @@ export interface SpreadsheetHeaderProps {
623
626
  onDuplicateCheckClick?: () => void;
624
627
  /** Number of rows with duplicate values (shown as badge when > 0) */
625
628
  duplicateCount?: number;
629
+ /** Callback when hide column is clicked */
630
+ onHideClick?: () => void;
626
631
  /** Resize handle props from useSpreadsheetColumnResize */
627
632
  resizeHandleProps?: {
628
633
  onMouseDown: (e: React.MouseEvent) => void;
@@ -763,6 +768,12 @@ export interface SpreadsheetToolbarProps {
763
768
  showFiltersPanel?: boolean;
764
769
  /** Callback to toggle the active filters panel */
765
770
  onToggleFiltersPanel?: () => void;
771
+ /** Number of hidden columns (shows indicator when > 0) */
772
+ hiddenColumnCount?: number;
773
+ /** Callback to show all hidden columns */
774
+ onShowAllColumns?: () => void;
775
+ /** Callback to open column visibility settings */
776
+ onManageColumns?: () => void;
766
777
  /** Custom className */
767
778
  className?: string;
768
779
  }