teachable-design-system 0.2.1 → 0.3.2

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 (50) hide show
  1. package/dist/index.cjs.js +875 -64
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.d.ts +72 -2
  4. package/dist/index.esm.js +877 -67
  5. package/dist/index.esm.js.map +1 -1
  6. package/dist/types/components/Dropdown/Dropdown.d.ts +1 -1
  7. package/dist/types/components/Dropdown/Dropdown.d.ts.map +1 -1
  8. package/dist/types/components/Dropdown/style.d.ts +1 -0
  9. package/dist/types/components/Dropdown/style.d.ts.map +1 -1
  10. package/dist/types/components/Table/Table.d.ts +3 -0
  11. package/dist/types/components/Table/Table.d.ts.map +1 -0
  12. package/dist/types/components/Table/index.d.ts +3 -0
  13. package/dist/types/components/Table/index.d.ts.map +1 -0
  14. package/dist/types/components/Table/style.d.ts +98 -0
  15. package/dist/types/components/Table/style.d.ts.map +1 -0
  16. package/dist/types/components/Table/table-body.d.ts +3 -0
  17. package/dist/types/components/Table/table-body.d.ts.map +1 -0
  18. package/dist/types/components/Table/table-cell.d.ts +3 -0
  19. package/dist/types/components/Table/table-cell.d.ts.map +1 -0
  20. package/dist/types/components/Table/table-header.d.ts +4 -0
  21. package/dist/types/components/Table/table-header.d.ts.map +1 -0
  22. package/dist/types/components/index.d.ts +1 -0
  23. package/dist/types/components/index.d.ts.map +1 -1
  24. package/dist/types/types/Dropdown.types.d.ts +1 -0
  25. package/dist/types/types/Dropdown.types.d.ts.map +1 -1
  26. package/dist/types/types/index.d.ts +6 -0
  27. package/dist/types/types/index.d.ts.map +1 -1
  28. package/dist/types/types/table.d.ts +141 -0
  29. package/dist/types/types/table.d.ts.map +1 -0
  30. package/package.json +1 -1
  31. package/src/components/Dropdown/Dropdown.stories.tsx +4 -0
  32. package/src/components/Dropdown/Dropdown.tsx +2 -1
  33. package/src/components/Dropdown/style.ts +2 -1
  34. package/src/components/Sidebar/style.ts +4 -4
  35. package/src/components/Table/Table.stories.tsx +179 -0
  36. package/src/components/Table/Table.tsx +510 -0
  37. package/src/components/Table/index.ts +2 -0
  38. package/src/components/Table/style.ts +345 -0
  39. package/src/components/Table/table-body.tsx +112 -0
  40. package/src/components/Table/table-cell.tsx +153 -0
  41. package/src/components/Table/table-header.tsx +52 -0
  42. package/src/components/index.ts +5 -4
  43. package/src/types/Dropdown.types.ts +1 -0
  44. package/src/types/index.ts +6 -0
  45. package/src/types/table.ts +150 -0
  46. package/dist/assets/icons/arrow-down.png +0 -0
  47. package/dist/assets/icons/checked.png +0 -0
  48. package/dist/assets/icons/icon_size.png +0 -0
  49. package/dist/assets/images/.gitkeep +0 -0
  50. package/dist/assets/index.ts +0 -1
@@ -0,0 +1,510 @@
1
+ import React, { useRef, useState, useMemo, useCallback, useEffect } from 'react';
2
+ import { ChevronUp, ChevronDown, Copy, ClipboardPaste, Trash2, XCircle } from 'lucide-react';
3
+ import type { TableProps, SortDirection, DataType, CellPosition } from '../../types/table';
4
+ import {
5
+ TableWrapper,
6
+ TableContainer,
7
+ StyledTable,
8
+ TableOuterWrapper,
9
+ ScrollContainer,
10
+ ScrollButton,
11
+ ContextMenu,
12
+ ContextMenuItem,
13
+ ContextMenuDivider,
14
+ ContextMenuOverlay,
15
+ } from './style';
16
+ import TableHeader from './table-header';
17
+ import TableBody from './table-body';
18
+
19
+ interface ContextMenuState {
20
+ visible: boolean;
21
+ x: number;
22
+ y: number;
23
+ }
24
+
25
+ const compareValues = (a: unknown, b: unknown, dataType?: DataType): number => {
26
+ if (a == null) return 1;
27
+ if (b == null) return -1;
28
+
29
+ switch (dataType) {
30
+ case 'number':
31
+ return Number(a) - Number(b);
32
+ case 'date':
33
+ return new Date(a as string).getTime() - new Date(b as string).getTime();
34
+ case 'boolean':
35
+ return (a ? 1 : 0) - (b ? 1 : 0);
36
+ default:
37
+ return String(a).localeCompare(String(b), 'ko-KR');
38
+ }
39
+ };
40
+
41
+ const getNextSortDirection = (current: SortDirection): SortDirection => {
42
+ if (current === 'asc') return 'desc';
43
+ if (current === 'desc') return null;
44
+ return 'asc';
45
+ };
46
+
47
+ const formatCellValue = (value: unknown): string => {
48
+ if (value == null) return '';
49
+ if (value instanceof Date) return value.toLocaleDateString('ko-KR');
50
+ if (typeof value === 'boolean') return value ? '예' : '아니오';
51
+ if (typeof value === 'number') return value.toString();
52
+ return String(value);
53
+ };
54
+
55
+ export default function Table<T extends Record<string, unknown> = Record<string, unknown>>({
56
+ columns,
57
+ data,
58
+ onCellEdit,
59
+ onSort,
60
+ onSelectionChange,
61
+ onPaste,
62
+ maxHeight,
63
+ rowHeight,
64
+ className,
65
+ enableRowSelection,
66
+ selectedRowIndex,
67
+ onRowClick,
68
+ enableKeyboardNavigation,
69
+ }: TableProps<T>) {
70
+ const outerRef = useRef<HTMLDivElement>(null);
71
+ const containerRef = useRef<HTMLDivElement>(null);
72
+ const [sortColumn, setSortColumn] = useState<string | null>(null);
73
+ const [sortDirection, setSortDirection] = useState<SortDirection>(null);
74
+ const [isSelecting, setIsSelecting] = useState(false);
75
+ const [selectionStart, setSelectionStart] = useState<CellPosition | null>(null);
76
+ const [selectionEnd, setSelectionEnd] = useState<CellPosition | null>(null);
77
+ const [contextMenu, setContextMenu] = useState<ContextMenuState>({ visible: false, x: 0, y: 0 });
78
+ const [hoveredRowIndex, setHoveredRowIndex] = useState<number | null>(null);
79
+ const [editingCell, setEditingCell] = useState<CellPosition | null>(null);
80
+ const [editStartValue, setEditStartValue] = useState<string | null>(null);
81
+ const [editToken, setEditToken] = useState(0);
82
+
83
+ const sortedData = useMemo(() => {
84
+ if (!sortColumn || !sortDirection) return data;
85
+
86
+ const column = columns.find((col) => col.key === sortColumn);
87
+ if (!column) return data;
88
+
89
+ return [...data].sort((a, b) => {
90
+ const aVal = a[sortColumn as keyof T];
91
+ const bVal = b[sortColumn as keyof T];
92
+
93
+ const result = column.sortFn
94
+ ? column.sortFn(aVal, bVal)
95
+ : compareValues(aVal, bVal, column.dataType);
96
+
97
+ return sortDirection === 'asc' ? result : -result;
98
+ });
99
+ }, [data, sortColumn, sortDirection, columns]);
100
+
101
+ const handleSort = useCallback((columnKey: string) => {
102
+ const isSameColumn = sortColumn === columnKey;
103
+ const nextDirection = isSameColumn
104
+ ? getNextSortDirection(sortDirection)
105
+ : 'asc';
106
+
107
+ setSortColumn(nextDirection ? columnKey : null);
108
+ setSortDirection(nextDirection);
109
+ onSort?.(columnKey, nextDirection);
110
+ }, [sortColumn, sortDirection, onSort]);
111
+
112
+ const scroll = useCallback((delta: number) => {
113
+ containerRef.current?.scrollBy({ top: delta, behavior: 'smooth' });
114
+ }, []);
115
+
116
+ const handleCellMouseDown = useCallback((rowIndex: number, colIndex: number) => {
117
+ setIsSelecting(true);
118
+ setSelectionStart({ row: rowIndex, col: colIndex });
119
+ setSelectionEnd({ row: rowIndex, col: colIndex });
120
+ }, []);
121
+
122
+ const handleCellMouseEnter = useCallback((rowIndex: number, colIndex: number) => {
123
+ if (isSelecting) {
124
+ setSelectionEnd({ row: rowIndex, col: colIndex });
125
+ }
126
+ }, [isSelecting]);
127
+
128
+ const handleCellMouseUp = useCallback(() => {
129
+ if (isSelecting && selectionStart && selectionEnd) {
130
+ const cells: CellPosition[] = [];
131
+ const minRow = Math.min(selectionStart.row, selectionEnd.row);
132
+ const maxRow = Math.max(selectionStart.row, selectionEnd.row);
133
+ const minCol = Math.min(selectionStart.col, selectionEnd.col);
134
+ const maxCol = Math.max(selectionStart.col, selectionEnd.col);
135
+
136
+ for (let r = minRow; r <= maxRow; r++) {
137
+ for (let c = minCol; c <= maxCol; c++) {
138
+ cells.push({ row: r, col: c });
139
+ }
140
+ }
141
+ onSelectionChange?.(cells);
142
+ }
143
+ setIsSelecting(false);
144
+ }, [isSelecting, selectionStart, selectionEnd, onSelectionChange]);
145
+
146
+ const getSelectedData = useCallback((): string => {
147
+ if (!selectionStart || !selectionEnd) return '';
148
+
149
+ const minRow = Math.min(selectionStart.row, selectionEnd.row);
150
+ const maxRow = Math.max(selectionStart.row, selectionEnd.row);
151
+ const minCol = Math.min(selectionStart.col, selectionEnd.col);
152
+ const maxCol = Math.max(selectionStart.col, selectionEnd.col);
153
+
154
+ const rows: string[] = [];
155
+ for (let r = minRow; r <= maxRow; r++) {
156
+ const cols: string[] = [];
157
+ for (let c = minCol; c <= maxCol; c++) {
158
+ const colKey = columns[c]?.key;
159
+ const value = sortedData[r]?.[colKey as keyof T];
160
+ cols.push(formatCellValue(value));
161
+ }
162
+ rows.push(cols.join('\t'));
163
+ }
164
+ return rows.join('\n');
165
+ }, [selectionStart, selectionEnd, columns, sortedData]);
166
+
167
+ const handleKeyDown = useCallback((e: KeyboardEvent) => {
168
+ // 셀 편집(input/textarea) 중에는 전역 키 핸들러가 Backspace/Delete 등을 가로채지 않도록 무시
169
+ const target = e.target as HTMLElement | null;
170
+ if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) {
171
+ return;
172
+ }
173
+
174
+ // 행 선택 모드에서 키보드 네비게이션
175
+ if (enableRowSelection && enableKeyboardNavigation && onRowClick) {
176
+ if (e.key === 'ArrowUp') {
177
+ e.preventDefault();
178
+ const currentIndex = selectedRowIndex ?? 0;
179
+ const newIndex = Math.max(0, currentIndex - 1);
180
+ onRowClick(newIndex, sortedData[newIndex]);
181
+ return;
182
+ }
183
+ if (e.key === 'ArrowDown') {
184
+ e.preventDefault();
185
+ const currentIndex = selectedRowIndex ?? -1;
186
+ const newIndex = Math.min(sortedData.length - 1, currentIndex + 1);
187
+ onRowClick(newIndex, sortedData[newIndex]);
188
+ return;
189
+ }
190
+ }
191
+
192
+ if (!selectionStart || !selectionEnd) return;
193
+
194
+ // 셀 선택 상태에서 문자 입력 시 즉시 편집 모드로 전환 (스프레드시트 UX)
195
+ // - 단일 셀 선택일 때만 동작
196
+ // - onCellEdit가 있어야 의미 있는 편집이 가능
197
+ if (
198
+ onCellEdit &&
199
+ !e.ctrlKey &&
200
+ !e.metaKey &&
201
+ !e.altKey &&
202
+ e.key.length === 1
203
+ ) {
204
+ const isSingleCell = selectionStart.row === selectionEnd.row && selectionStart.col === selectionEnd.col;
205
+ if (!isSingleCell) {
206
+ setSelectionEnd(selectionStart);
207
+ }
208
+ e.preventDefault();
209
+ setEditingCell({ row: selectionStart.row, col: selectionStart.col });
210
+ setEditStartValue(e.key);
211
+ setEditToken((t) => t + 1);
212
+ setContextMenu({ visible: false, x: 0, y: 0 });
213
+ return;
214
+ }
215
+
216
+ if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
217
+ e.preventDefault();
218
+ const text = getSelectedData();
219
+ navigator.clipboard.writeText(text).catch(console.error);
220
+ }
221
+
222
+ if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
223
+ e.preventDefault();
224
+ navigator.clipboard.readText().then((text) => {
225
+ if (!text || !selectionStart) return;
226
+
227
+ const rows = text
228
+ .replace(/\r\n/g, '\n')
229
+ .replace(/\r/g, '\n')
230
+ .split('\n')
231
+ .filter((row, idx, arr) => !(idx === arr.length - 1 && row === ''))
232
+ .map(row => row.split('\t'));
233
+
234
+ const startRow = Math.min(selectionStart.row, selectionEnd?.row ?? selectionStart.row);
235
+ const startCol = Math.min(selectionStart.col, selectionEnd?.col ?? selectionStart.col);
236
+
237
+ if (onPaste) {
238
+ onPaste(startRow, startCol, rows);
239
+ } else if (onCellEdit) {
240
+ rows.forEach((row, rIdx) => {
241
+ row.forEach((cellValue, cIdx) => {
242
+ const targetRow = startRow + rIdx;
243
+ const targetCol = startCol + cIdx;
244
+ if (targetRow < data.length && targetCol < columns.length) {
245
+ const col = columns[targetCol];
246
+ if (col.editable !== false) {
247
+ let parsedValue: unknown = cellValue;
248
+ if (col.dataType === 'number') {
249
+ const num = parseFloat(cellValue.replace(/,/g, ''));
250
+ parsedValue = isNaN(num) ? 0 : num;
251
+ }
252
+ onCellEdit(targetRow, col.key, parsedValue);
253
+ }
254
+ }
255
+ });
256
+ });
257
+ }
258
+ }).catch(console.error);
259
+ }
260
+
261
+ if (e.key === 'Delete' || e.key === 'Backspace') {
262
+ if (!onCellEdit) return;
263
+ e.preventDefault();
264
+ const minRow = Math.min(selectionStart.row, selectionEnd.row);
265
+ const maxRow = Math.max(selectionStart.row, selectionEnd.row);
266
+ const minCol = Math.min(selectionStart.col, selectionEnd.col);
267
+ const maxCol = Math.max(selectionStart.col, selectionEnd.col);
268
+
269
+ for (let r = minRow; r <= maxRow; r++) {
270
+ for (let c = minCol; c <= maxCol; c++) {
271
+ const col = columns[c];
272
+ if (col.editable !== false) {
273
+ onCellEdit(r, col.key, '');
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ if (e.key === 'Escape') {
280
+ setSelectionStart(null);
281
+ setSelectionEnd(null);
282
+ setContextMenu({ visible: false, x: 0, y: 0 });
283
+ }
284
+ }, [enableRowSelection, enableKeyboardNavigation, selectedRowIndex, sortedData, onRowClick, selectionStart, selectionEnd, getSelectedData, onPaste, onCellEdit, data, columns]);
285
+
286
+ // 표 밖을 클릭하면 셀 선택(타겟) 해제
287
+ useEffect(() => {
288
+ const handleMouseDownOutside = (event: MouseEvent) => {
289
+ const root = outerRef.current;
290
+ const target = event.target as Node | null;
291
+ if (!root || !target) return;
292
+ if (root.contains(target)) return;
293
+
294
+ setIsSelecting(false);
295
+ setSelectionStart(null);
296
+ setSelectionEnd(null);
297
+ setEditingCell(null);
298
+ setEditStartValue(null);
299
+ setContextMenu({ visible: false, x: 0, y: 0 });
300
+ };
301
+
302
+ document.addEventListener('mousedown', handleMouseDownOutside, true);
303
+ return () => {
304
+ document.removeEventListener('mousedown', handleMouseDownOutside, true);
305
+ };
306
+ }, []);
307
+
308
+ const handleContextMenu = useCallback((e: React.MouseEvent) => {
309
+ e.preventDefault();
310
+ if (selectionStart && selectionEnd) {
311
+ setContextMenu({ visible: true, x: e.clientX, y: e.clientY });
312
+ }
313
+ }, [selectionStart, selectionEnd]);
314
+
315
+ const closeContextMenu = useCallback(() => {
316
+ setContextMenu({ visible: false, x: 0, y: 0 });
317
+ }, []);
318
+
319
+ const handleCopy = useCallback(() => {
320
+ const text = getSelectedData();
321
+ navigator.clipboard.writeText(text).catch(console.error);
322
+ closeContextMenu();
323
+ }, [getSelectedData, closeContextMenu]);
324
+
325
+ const handlePaste = useCallback(() => {
326
+ if (!selectionStart || !selectionEnd) return;
327
+
328
+ navigator.clipboard.readText().then((text) => {
329
+ if (!text) return;
330
+
331
+ const rows = text
332
+ .replace(/\r\n/g, '\n')
333
+ .replace(/\r/g, '\n')
334
+ .split('\n')
335
+ .filter((row, idx, arr) => !(idx === arr.length - 1 && row === ''))
336
+ .map(row => row.split('\t'));
337
+
338
+ const startRow = Math.min(selectionStart.row, selectionEnd.row);
339
+ const startCol = Math.min(selectionStart.col, selectionEnd.col);
340
+
341
+ if (onPaste) {
342
+ onPaste(startRow, startCol, rows);
343
+ } else if (onCellEdit) {
344
+ rows.forEach((row, rIdx) => {
345
+ row.forEach((cellValue, cIdx) => {
346
+ const targetRow = startRow + rIdx;
347
+ const targetCol = startCol + cIdx;
348
+ if (targetRow < data.length && targetCol < columns.length) {
349
+ const col = columns[targetCol];
350
+ if (col.editable !== false) {
351
+ let parsedValue: unknown = cellValue;
352
+ if (col.dataType === 'number') {
353
+ const num = parseFloat(cellValue.replace(/,/g, ''));
354
+ parsedValue = isNaN(num) ? 0 : num;
355
+ }
356
+ onCellEdit(targetRow, col.key, parsedValue);
357
+ }
358
+ }
359
+ });
360
+ });
361
+ }
362
+ }).catch(console.error);
363
+ closeContextMenu();
364
+ }, [selectionStart, selectionEnd, onPaste, onCellEdit, data, columns, closeContextMenu]);
365
+
366
+ const handleDelete = useCallback(() => {
367
+ if (!selectionStart || !selectionEnd || !onCellEdit) return;
368
+
369
+ const minRow = Math.min(selectionStart.row, selectionEnd.row);
370
+ const maxRow = Math.max(selectionStart.row, selectionEnd.row);
371
+ const minCol = Math.min(selectionStart.col, selectionEnd.col);
372
+ const maxCol = Math.max(selectionStart.col, selectionEnd.col);
373
+
374
+ for (let r = minRow; r <= maxRow; r++) {
375
+ for (let c = minCol; c <= maxCol; c++) {
376
+ const col = columns[c];
377
+ if (col.editable !== false) {
378
+ onCellEdit(r, col.key, '');
379
+ }
380
+ }
381
+ }
382
+ closeContextMenu();
383
+ }, [selectionStart, selectionEnd, onCellEdit, columns, closeContextMenu]);
384
+
385
+ const handleClearSelection = useCallback(() => {
386
+ setSelectionStart(null);
387
+ setSelectionEnd(null);
388
+ closeContextMenu();
389
+ }, [closeContextMenu]);
390
+
391
+ const hasSelection = selectionStart !== null && selectionEnd !== null;
392
+ const selectionCellCount = useMemo(() => {
393
+ if (!selectionStart || !selectionEnd) return 0;
394
+ const rows = Math.abs(selectionEnd.row - selectionStart.row) + 1;
395
+ const cols = Math.abs(selectionEnd.col - selectionStart.col) + 1;
396
+ return rows * cols;
397
+ }, [selectionStart, selectionEnd]);
398
+
399
+ useEffect(() => {
400
+ const container = containerRef.current;
401
+ if (!container) return;
402
+
403
+ const handleMouseUp = () => {
404
+ if (isSelecting) {
405
+ handleCellMouseUp();
406
+ }
407
+ };
408
+
409
+ document.addEventListener('mouseup', handleMouseUp);
410
+ document.addEventListener('keydown', handleKeyDown as unknown as EventListener);
411
+
412
+ return () => {
413
+ document.removeEventListener('mouseup', handleMouseUp);
414
+ document.removeEventListener('keydown', handleKeyDown as unknown as EventListener);
415
+ };
416
+ }, [isSelecting, handleCellMouseUp, handleKeyDown]);
417
+
418
+ return (
419
+ <TableOuterWrapper ref={outerRef} className={className} tabIndex={0} onContextMenu={handleContextMenu}>
420
+ <TableWrapper>
421
+ <TableContainer ref={containerRef} maxHeight={maxHeight}>
422
+ <StyledTable>
423
+ <colgroup>
424
+ {columns.map((col) => (
425
+ <col
426
+ key={String(col.key)}
427
+ style={col.width ? { width: col.width } : undefined}
428
+ />
429
+ ))}
430
+ </colgroup>
431
+ <TableHeader<T>
432
+ columns={columns}
433
+ sortColumn={sortColumn ?? undefined}
434
+ sortDirection={sortDirection}
435
+ onSort={handleSort}
436
+ />
437
+ <TableBody<T>
438
+ columns={columns}
439
+ data={sortedData}
440
+ rowHeight={rowHeight}
441
+ onCellEdit={onCellEdit}
442
+ selectionStart={enableRowSelection ? null : selectionStart}
443
+ selectionEnd={enableRowSelection ? null : selectionEnd}
444
+ editingCell={enableRowSelection ? null : editingCell}
445
+ editStartValue={editStartValue}
446
+ editToken={editToken}
447
+ onCellMouseDown={enableRowSelection ? undefined : handleCellMouseDown}
448
+ onCellMouseEnter={enableRowSelection ? undefined : handleCellMouseEnter}
449
+ onCellMouseUp={enableRowSelection ? undefined : handleCellMouseUp}
450
+ enableRowSelection={enableRowSelection}
451
+ selectedRowIndex={selectedRowIndex}
452
+ hoveredRowIndex={hoveredRowIndex ?? undefined}
453
+ onRowClick={(rowIndex) => onRowClick?.(rowIndex, sortedData[rowIndex])}
454
+ onRowHover={setHoveredRowIndex}
455
+ />
456
+ </StyledTable>
457
+ </TableContainer>
458
+ </TableWrapper>
459
+
460
+ {maxHeight && (
461
+ <ScrollContainer>
462
+ <ScrollButton position="top" onClick={() => scroll(-100)}>
463
+ <ChevronUp />
464
+ </ScrollButton>
465
+ <ScrollButton position="bottom" onClick={() => scroll(100)}>
466
+ <ChevronDown />
467
+ </ScrollButton>
468
+ </ScrollContainer>
469
+ )}
470
+
471
+ {contextMenu.visible && (
472
+ <>
473
+ <ContextMenuOverlay onClick={closeContextMenu} />
474
+ <ContextMenu x={contextMenu.x} y={contextMenu.y}>
475
+ <ContextMenuItem onClick={handleCopy} disabled={!hasSelection}>
476
+ <Copy size={14} />
477
+ 복사
478
+ <span className="shortcut">⌘C</span>
479
+ </ContextMenuItem>
480
+ <ContextMenuItem onClick={handlePaste} disabled={!hasSelection}>
481
+ <ClipboardPaste size={14} />
482
+ 붙여넣기
483
+ <span className="shortcut">⌘V</span>
484
+ </ContextMenuItem>
485
+ <ContextMenuDivider />
486
+ <ContextMenuItem onClick={handleDelete} disabled={!hasSelection || !onCellEdit}>
487
+ <Trash2 size={14} />
488
+ 삭제
489
+ <span className="shortcut">Del</span>
490
+ </ContextMenuItem>
491
+ <ContextMenuDivider />
492
+ <ContextMenuItem onClick={handleClearSelection} disabled={!hasSelection}>
493
+ <XCircle size={14} />
494
+ 선택 해제
495
+ <span className="shortcut">Esc</span>
496
+ </ContextMenuItem>
497
+ {hasSelection && (
498
+ <>
499
+ <ContextMenuDivider />
500
+ <ContextMenuItem disabled style={{ fontSize: '11px', color: '#9ca3af' }}>
501
+ {selectionCellCount}개 셀 선택됨
502
+ </ContextMenuItem>
503
+ </>
504
+ )}
505
+ </ContextMenu>
506
+ </>
507
+ )}
508
+ </TableOuterWrapper>
509
+ );
510
+ }
@@ -0,0 +1,2 @@
1
+ export { default } from './Table';
2
+ export { default as Table } from './Table';