@xcelsior/ui-spreadsheets 1.0.3 → 1.0.5

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,12 +1,8 @@
1
1
  import type React from 'react';
2
2
  import { useState, useRef, useEffect } from 'react';
3
- import {
4
- HiOutlineClipboardCopy,
5
- HiOutlineClipboardCheck,
6
- HiOutlineAnnotation,
7
- HiOutlineChatAlt,
8
- HiOutlinePencil,
9
- } from 'react-icons/hi';
3
+ import { HiOutlineClipboardCopy, HiOutlineClipboardCheck } from 'react-icons/hi';
4
+ import { AiFillHighlight } from 'react-icons/ai';
5
+ import { FaComment, FaRegComment } from 'react-icons/fa';
10
6
  import { cn } from '../utils';
11
7
  import type { SpreadsheetCellProps } from '../types';
12
8
 
@@ -225,7 +221,7 @@ export const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
225
221
  {isEditing ? (
226
222
  renderEditInput()
227
223
  ) : (
228
- <div className="flex items-center gap-1">
224
+ <div className="flex items-center gap-1 relative">
229
225
  {/* Main content */}
230
226
  <div
231
227
  className={cn(
@@ -238,34 +234,9 @@ export const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
238
234
  {renderContent()}
239
235
  </div>
240
236
 
241
- {/* Comment indicator */}
242
- {hasComments && (
243
- <button
244
- type="button"
245
- onClick={(e) => {
246
- e.stopPropagation();
247
- onViewComments?.();
248
- }}
249
- className="p-0.5 hover:bg-gray-100 rounded relative shrink-0"
250
- title={`${unresolvedCommentCount} unresolved comment(s)`}
251
- >
252
- <HiOutlineChatAlt
253
- className={cn(
254
- 'h-3 w-3',
255
- unresolvedCommentCount > 0 ? 'text-amber-500' : 'text-gray-400'
256
- )}
257
- />
258
- {unresolvedCommentCount > 0 && (
259
- <span className="absolute -top-1 -right-1 bg-amber-500 text-white text-[8px] rounded-full w-3 h-3 flex items-center justify-center">
260
- {unresolvedCommentCount}
261
- </span>
262
- )}
263
- </button>
264
- )}
265
-
266
- {/* Action buttons - show on hover */}
267
- <div className="opacity-0 group-hover:opacity-100 flex items-center gap-0.5 transition-opacity shrink-0">
268
- {/* Copy down button */}
237
+ {/* Action buttons - show on hover, except comment indicator which is always visible */}
238
+ <div className="flex items-center gap-0.5 shrink-0">
239
+ {/* Copy down button - hover only */}
269
240
  {value !== null && value !== undefined && value !== '' && onCopyDown && (
270
241
  <button
271
242
  type="button"
@@ -273,14 +244,14 @@ export const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
273
244
  e.stopPropagation();
274
245
  onCopyDown();
275
246
  }}
276
- className="p-0.5 bg-gray-100 hover:bg-gray-200 rounded"
247
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-gray-100 hover:bg-gray-200 rounded"
277
248
  title="Copy value down to rows below"
278
249
  >
279
250
  <HiOutlineClipboardCopy className="h-2.5 w-2.5 text-gray-500" />
280
251
  </button>
281
252
  )}
282
253
 
283
- {/* Copy to selected button */}
254
+ {/* Copy to selected button - hover only */}
284
255
  {hasSelectedRows &&
285
256
  value !== null &&
286
257
  value !== undefined &&
@@ -292,14 +263,14 @@ export const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
292
263
  e.stopPropagation();
293
264
  onCopyToSelected();
294
265
  }}
295
- className="p-0.5 bg-green-100 hover:bg-green-200 rounded"
266
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-green-100 hover:bg-green-200 rounded"
296
267
  title="Copy to selected rows"
297
268
  >
298
269
  <HiOutlineClipboardCheck className="h-2.5 w-2.5 text-green-600" />
299
270
  </button>
300
271
  )}
301
272
 
302
- {/* Highlight button */}
273
+ {/* Highlight button - hover only */}
303
274
  {onHighlight && (
304
275
  <button
305
276
  type="button"
@@ -307,32 +278,51 @@ export const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
307
278
  e.stopPropagation();
308
279
  onHighlight();
309
280
  }}
310
- className="p-0.5 hover:bg-gray-100 rounded"
281
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-gray-100 hover:bg-gray-200 rounded"
311
282
  title="Highlight cell"
312
283
  >
313
- <HiOutlinePencil
284
+ <AiFillHighlight
314
285
  className={cn(
315
286
  'h-2.5 w-2.5',
316
- highlightColor ? 'text-amber-500' : 'text-gray-400'
287
+ highlightColor ? 'text-amber-500' : 'text-gray-500'
317
288
  )}
318
289
  />
319
290
  </button>
320
291
  )}
321
292
 
322
- {/* Add comment button */}
323
- {onAddComment && (
293
+ {/* Comment button - always visible when has comments, hover only when adding */}
294
+ {hasComments && onViewComments ? (
295
+ <button
296
+ type="button"
297
+ onClick={(e) => {
298
+ e.stopPropagation();
299
+ onViewComments();
300
+ }}
301
+ className="p-0.5 bg-amber-100 hover:bg-amber-200 rounded transition-colors flex items-center gap-0.5"
302
+ title={`${unresolvedCommentCount} comment(s) - click to view`}
303
+ >
304
+ <FaComment className="h-2.5 w-2.5 text-amber-500" />
305
+ {unresolvedCommentCount > 0 && (
306
+ <span className="text-[9px] font-medium text-amber-600">
307
+ {unresolvedCommentCount > 99
308
+ ? '99+'
309
+ : unresolvedCommentCount}
310
+ </span>
311
+ )}
312
+ </button>
313
+ ) : onAddComment ? (
324
314
  <button
325
315
  type="button"
326
316
  onClick={(e) => {
327
317
  e.stopPropagation();
328
318
  onAddComment();
329
319
  }}
330
- className="p-0.5 hover:bg-gray-100 rounded"
320
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-gray-100 hover:bg-gray-200 rounded"
331
321
  title="Add comment"
332
322
  >
333
- <HiOutlineAnnotation className="h-2.5 w-2.5 text-gray-400" />
323
+ <FaRegComment className="h-2.5 w-2.5 text-gray-500" />
334
324
  </button>
335
- )}
325
+ ) : null}
336
326
  </div>
337
327
  </div>
338
328
  )}
@@ -1,92 +1,94 @@
1
1
  import { useCallback, useState } from 'react';
2
- import type { CellComment } from '../types';
2
+ import type { CellComment, CellPosition } from '../types';
3
3
 
4
4
  export interface UseSpreadsheetCommentsOptions {
5
- /** External row comments (controlled mode) */
6
- externalRowComments?: CellComment[];
7
- /** Callback when a row comment is added (controlled mode) */
8
- onAddRowComment?: (rowId: string | number, comment: string) => void;
5
+ /** External cell comments (controlled mode) */
6
+ externalCellComments?: CellComment[];
7
+ /** Callback when a cell comment is added (controlled mode) */
8
+ onAddCellComment?: (rowId: string | number, columnId: string, comment: string) => void;
9
9
  }
10
10
 
11
11
  export interface UseSpreadsheetCommentsReturn {
12
12
  // Comments data
13
- rowComments: CellComment[];
14
- getRowComments: (rowId: string | number) => CellComment[];
15
- getUnresolvedCommentCount: (rowId: string | number) => number;
13
+ cellComments: CellComment[];
14
+ getCellComments: (rowId: string | number, columnId: string) => CellComment[];
15
+ getCellUnresolvedCommentCount: (rowId: string | number, columnId: string) => number;
16
16
 
17
17
  // Add comment modal state
18
- commentModalRow: string | number | null;
19
- setCommentModalRow: (rowId: string | number | null) => void;
18
+ commentModalCell: CellPosition | null;
19
+ setCommentModalCell: (cell: CellPosition | null) => void;
20
20
  commentText: string;
21
21
  setCommentText: (text: string) => void;
22
22
 
23
23
  // View comments modal state
24
- viewCommentsRow: string | number | null;
25
- setViewCommentsRow: (rowId: string | number | null) => void;
24
+ viewCommentsCell: CellPosition | null;
25
+ setViewCommentsCell: (cell: CellPosition | null) => void;
26
26
 
27
27
  // Actions
28
- handleAddRowComment: (rowId: string | number) => void;
28
+ handleAddCellComment: (rowId: string | number, columnId: string) => void;
29
29
  handleToggleCommentResolved: (commentId: string) => void;
30
30
 
31
31
  // Utility
32
- hasComments: (rowId: string | number) => boolean;
32
+ cellHasComments: (rowId: string | number, columnId: string) => boolean;
33
33
  }
34
34
 
35
35
  export function useSpreadsheetComments({
36
- externalRowComments,
37
- onAddRowComment,
36
+ externalCellComments,
37
+ onAddCellComment,
38
38
  }: UseSpreadsheetCommentsOptions = {}): UseSpreadsheetCommentsReturn {
39
39
  // Internal comments state
40
- const [rowCommentsInternal, setRowCommentsInternal] = useState<CellComment[]>([]);
40
+ const [cellCommentsInternal, setCellCommentsInternal] = useState<CellComment[]>([]);
41
41
 
42
42
  // Modal states
43
- const [commentModalRow, setCommentModalRow] = useState<string | number | null>(null);
43
+ const [commentModalCell, setCommentModalCell] = useState<CellPosition | null>(null);
44
44
  const [commentText, setCommentText] = useState('');
45
- const [viewCommentsRow, setViewCommentsRow] = useState<string | number | null>(null);
45
+ const [viewCommentsCell, setViewCommentsCell] = useState<CellPosition | null>(null);
46
46
 
47
47
  // Use external comments if provided, otherwise use internal
48
- const rowComments = externalRowComments || rowCommentsInternal;
48
+ const cellComments = externalCellComments || cellCommentsInternal;
49
49
 
50
- // Get comments for a specific row
51
- const getRowComments = useCallback(
52
- (rowId: string | number): CellComment[] => {
53
- return rowComments.filter((c) => c.rowId === rowId && !c.columnId);
50
+ // Get comments for a specific cell
51
+ const getCellComments = useCallback(
52
+ (rowId: string | number, columnId: string): CellComment[] => {
53
+ return cellComments.filter((c) => c.rowId === rowId && c.columnId === columnId);
54
54
  },
55
- [rowComments]
55
+ [cellComments]
56
56
  );
57
57
 
58
- // Get unresolved comment count for a row
59
- const getUnresolvedCommentCount = useCallback(
60
- (rowId: string | number): number => {
61
- return rowComments.filter((c) => c.rowId === rowId && !c.columnId && !c.resolved)
62
- .length;
58
+ // Get unresolved comment count for a cell
59
+ const getCellUnresolvedCommentCount = useCallback(
60
+ (rowId: string | number, columnId: string): number => {
61
+ return cellComments.filter(
62
+ (c) => c.rowId === rowId && c.columnId === columnId && !c.resolved
63
+ ).length;
63
64
  },
64
- [rowComments]
65
+ [cellComments]
65
66
  );
66
67
 
67
- // Check if row has comments
68
- const hasComments = useCallback(
69
- (rowId: string | number): boolean => {
70
- return rowComments.some((c) => c.rowId === rowId && !c.columnId);
68
+ // Check if cell has comments
69
+ const cellHasComments = useCallback(
70
+ (rowId: string | number, columnId: string): boolean => {
71
+ return cellComments.some((c) => c.rowId === rowId && c.columnId === columnId);
71
72
  },
72
- [rowComments]
73
+ [cellComments]
73
74
  );
74
75
 
75
- // Add a row comment
76
- const handleAddRowComment = useCallback(
77
- (rowId: string | number) => {
76
+ // Add a cell comment
77
+ const handleAddCellComment = useCallback(
78
+ (rowId: string | number, columnId: string) => {
78
79
  if (!commentText.trim()) return;
79
80
 
80
- if (onAddRowComment) {
81
+ if (onAddCellComment) {
81
82
  // Controlled mode
82
- onAddRowComment(rowId, commentText);
83
+ onAddCellComment(rowId, columnId, commentText);
83
84
  } else {
84
85
  // Uncontrolled mode
85
- setRowCommentsInternal((prev) => [
86
+ setCellCommentsInternal((prev) => [
86
87
  ...prev,
87
88
  {
88
89
  id: `comment-${Date.now()}`,
89
90
  rowId,
91
+ columnId,
90
92
  text: commentText,
91
93
  timestamp: new Date(),
92
94
  resolved: false,
@@ -94,39 +96,39 @@ export function useSpreadsheetComments({
94
96
  ]);
95
97
  }
96
98
  setCommentText('');
97
- setCommentModalRow(null);
99
+ setCommentModalCell(null);
98
100
  },
99
- [commentText, onAddRowComment]
101
+ [commentText, onAddCellComment]
100
102
  );
101
103
 
102
104
  // Toggle comment resolved status
103
105
  const handleToggleCommentResolved = useCallback((commentId: string) => {
104
- setRowCommentsInternal((prev) =>
106
+ setCellCommentsInternal((prev) =>
105
107
  prev.map((c) => (c.id === commentId ? { ...c, resolved: !c.resolved } : c))
106
108
  );
107
109
  }, []);
108
110
 
109
111
  return {
110
112
  // Comments data
111
- rowComments,
112
- getRowComments,
113
- getUnresolvedCommentCount,
113
+ cellComments,
114
+ getCellComments,
115
+ getCellUnresolvedCommentCount,
114
116
 
115
117
  // Add comment modal state
116
- commentModalRow,
117
- setCommentModalRow,
118
+ commentModalCell,
119
+ setCommentModalCell,
118
120
  commentText,
119
121
  setCommentText,
120
122
 
121
123
  // View comments modal state
122
- viewCommentsRow,
123
- setViewCommentsRow,
124
+ viewCommentsCell,
125
+ setViewCommentsCell,
124
126
 
125
127
  // Actions
126
- handleAddRowComment,
128
+ handleAddCellComment,
127
129
  handleToggleCommentResolved,
128
130
 
129
131
  // Utility
130
- hasComments,
132
+ cellHasComments,
131
133
  };
132
134
  }
@@ -38,6 +38,8 @@ export interface UseSpreadsheetFilteringReturn<T> {
38
38
  setActiveFilterColumn: (columnId: string | null) => void;
39
39
  handleFilterChange: (columnId: string, filter: SpreadsheetColumnFilter | undefined) => void;
40
40
  handleSort: (columnId: string) => void;
41
+ setSortConfig: (config: SpreadsheetSortConfig | null) => void;
42
+ clearSort: () => void;
41
43
  clearAllFilters: () => void;
42
44
  hasActiveFilters: boolean;
43
45
  }
@@ -368,10 +370,21 @@ export function useSpreadsheetFiltering<T extends Record<string, any>>({
368
370
 
369
371
  const handleSort = useCallback(
370
372
  (columnId: string) => {
371
- const newSortConfig: SpreadsheetSortConfig =
372
- sortConfig?.columnId === columnId
373
- ? { columnId, direction: sortConfig.direction === 'asc' ? 'desc' : 'asc' }
374
- : { columnId, direction: 'asc' };
373
+ let newSortConfig: SpreadsheetSortConfig | null;
374
+
375
+ if (sortConfig?.columnId === columnId) {
376
+ // Cycle through: asc → desc → null (clear)
377
+ if (sortConfig.direction === 'asc') {
378
+ newSortConfig = { columnId, direction: 'desc' };
379
+ } else {
380
+ // Currently desc, clear the sort
381
+ newSortConfig = null;
382
+ }
383
+ } else {
384
+ // Different column, start with asc
385
+ newSortConfig = { columnId, direction: 'asc' };
386
+ }
387
+
375
388
  // Only update internal state if not controlled
376
389
  if (controlledSortConfig === undefined) {
377
390
  setInternalSortConfig(newSortConfig);
@@ -381,6 +394,25 @@ export function useSpreadsheetFiltering<T extends Record<string, any>>({
381
394
  [sortConfig, onSortChange, controlledSortConfig]
382
395
  );
383
396
 
397
+ const clearSort = useCallback(() => {
398
+ // Only update internal state if not controlled
399
+ if (controlledSortConfig === undefined) {
400
+ setInternalSortConfig(null);
401
+ }
402
+ onSortChange?.(null);
403
+ }, [onSortChange, controlledSortConfig]);
404
+
405
+ const setSortConfig = useCallback(
406
+ (config: SpreadsheetSortConfig | null) => {
407
+ // Only update internal state if not controlled
408
+ if (controlledSortConfig === undefined) {
409
+ setInternalSortConfig(config);
410
+ }
411
+ onSortChange?.(config);
412
+ },
413
+ [onSortChange, controlledSortConfig]
414
+ );
415
+
384
416
  const clearAllFilters = useCallback(() => {
385
417
  // Only update internal state if not controlled
386
418
  if (controlledFilters === undefined) {
@@ -399,6 +431,8 @@ export function useSpreadsheetFiltering<T extends Record<string, any>>({
399
431
  setActiveFilterColumn,
400
432
  handleFilterChange,
401
433
  handleSort,
434
+ setSortConfig,
435
+ clearSort,
402
436
  clearAllFilters,
403
437
  hasActiveFilters,
404
438
  };
@@ -40,7 +40,11 @@ export interface UseSpreadsheetHighlightingReturn {
40
40
  // Cell highlights
41
41
  cellHighlights: CellHighlight[];
42
42
  getCellHighlight: (rowId: string | number, columnId: string) => string | undefined;
43
- handleCellHighlightToggle: (rowId: string | number, columnId: string, color?: string) => void;
43
+ handleCellHighlightToggle: (
44
+ rowId: string | number,
45
+ columnId: string,
46
+ color?: string | null
47
+ ) => void;
44
48
 
45
49
  // Row highlights
46
50
  rowHighlights: CellHighlight[];
@@ -57,6 +61,8 @@ export interface UseSpreadsheetHighlightingReturn {
57
61
  setHighlightPickerRow: (rowId: string | number | null) => void;
58
62
  highlightPickerColumn: string | null;
59
63
  setHighlightPickerColumn: (columnId: string | null) => void;
64
+ highlightPickerCell: { rowId: string | number; columnId: string } | null;
65
+ setHighlightPickerCell: (cell: { rowId: string | number; columnId: string } | null) => void;
60
66
 
61
67
  // Utility
62
68
  clearAllHighlights: () => void;
@@ -81,6 +87,10 @@ export function useSpreadsheetHighlighting({
81
87
  // Picker states
82
88
  const [highlightPickerRow, setHighlightPickerRow] = useState<string | number | null>(null);
83
89
  const [highlightPickerColumn, setHighlightPickerColumn] = useState<string | null>(null);
90
+ const [highlightPickerCell, setHighlightPickerCell] = useState<{
91
+ rowId: string | number;
92
+ columnId: string;
93
+ } | null>(null);
84
94
 
85
95
  // Use external row highlights if provided, otherwise use internal
86
96
  const rowHighlights = externalRowHighlights || rowHighlightsInternal;
@@ -95,14 +105,25 @@ export function useSpreadsheetHighlighting({
95
105
 
96
106
  // Toggle cell highlight
97
107
  const handleCellHighlightToggle = useCallback(
98
- (rowId: string | number, columnId: string, color: string = '#fef08a') => {
108
+ (rowId: string | number, columnId: string, color: string | null = '#fef08a') => {
99
109
  setCellHighlights((prev) => {
100
110
  const existing = prev.find((h) => h.rowId === rowId && h.columnId === columnId);
101
111
  if (existing) {
102
- return prev.filter((h) => !(h.rowId === rowId && h.columnId === columnId));
112
+ if (color === null) {
113
+ // Remove highlight
114
+ return prev.filter((h) => !(h.rowId === rowId && h.columnId === columnId));
115
+ }
116
+ // Update color
117
+ return prev.map((h) =>
118
+ h.rowId === rowId && h.columnId === columnId ? { ...h, color } : h
119
+ );
120
+ }
121
+ if (color) {
122
+ return [...prev, { rowId, columnId, color }];
103
123
  }
104
- return [...prev, { rowId, columnId, color }];
124
+ return prev;
105
125
  });
126
+ setHighlightPickerCell(null);
106
127
  },
107
128
  []
108
129
  );
@@ -194,6 +215,8 @@ export function useSpreadsheetHighlighting({
194
215
  setHighlightPickerRow,
195
216
  highlightPickerColumn,
196
217
  setHighlightPickerColumn,
218
+ highlightPickerCell,
219
+ setHighlightPickerCell,
197
220
 
198
221
  // Utility
199
222
  clearAllHighlights,
package/src/types.ts CHANGED
@@ -312,8 +312,8 @@ export interface SpreadsheetProps<T = any> {
312
312
  onRowDoubleClick?: (row: T, rowIndex: number) => void;
313
313
  /** Callback when a row is cloned/duplicated */
314
314
  onRowClone?: (row: T, rowId: string | number) => void;
315
- /** Callback when a row comment is added */
316
- onAddRowComment?: (rowId: string | number, comment: string) => void;
315
+ /** Callback when a cell comment is added */
316
+ onAddCellComment?: (rowId: string | number, columnId: string, comment: string) => void;
317
317
  /** Callback when row highlight is toggled */
318
318
  onRowHighlight?: (rowId: string | number, color: string | null) => void;
319
319
  /** Whether to show the toolbar */
@@ -350,8 +350,8 @@ export interface SpreadsheetProps<T = any> {
350
350
  emptyMessage?: string;
351
351
  /** Row highlights (externally controlled) */
352
352
  rowHighlights?: CellHighlight[];
353
- /** Row comments (externally controlled) */
354
- rowComments?: CellComment[];
353
+ /** Cell comments (externally controlled) */
354
+ cellComments?: CellComment[];
355
355
  /** Custom row actions to display in the index column */
356
356
  rowActions?: RowAction<T>[];
357
357
 
@@ -1,22 +0,0 @@
1
-
2
- > @xcelsior/ui-spreadsheets@1.0.3 build /home/circleci/repo/packages/ui/ui-spreadsheets
3
- > tsup && tsc --noEmit
4
-
5
- CLI Building entry: src/index.ts
6
- CLI Using tsconfig: tsconfig.json
7
- CLI tsup v8.5.1
8
- CLI Using tsup config: /home/circleci/repo/packages/ui/ui-spreadsheets/tsup.config.ts
9
- CLI Target: es2020
10
- CLI Cleaning output folder
11
- CJS Build start
12
- ESM Build start
13
- CJS dist/index.js 151.12 KB
14
- CJS dist/index.js.map 2.63 MB
15
- CJS ⚡️ Build success in 576ms
16
- ESM dist/index.mjs 140.30 KB
17
- ESM dist/index.mjs.map 2.63 MB
18
- ESM ⚡️ Build success in 577ms
19
- DTS Build start
20
- DTS ⚡️ Build success in 8502ms
21
- DTS dist/index.d.ts 22.18 KB
22
- DTS dist/index.d.mts 22.18 KB