teachable-design-system 0.2.0 → 0.3.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.
@@ -0,0 +1,345 @@
1
+ import styled from '@emotion/styled';
2
+ import { colors as themeColors, typography } from '../../style/theme';
3
+ import type { SortDirection } from '../../types/table';
4
+
5
+ // ============================================
6
+ // 디자인 토큰
7
+ // ============================================
8
+ const colors = {
9
+ header: themeColors.surface['secondary-subtler'],
10
+ headerHover: themeColors.action['secondary-pressed'],
11
+ body: themeColors.surface.white,
12
+ bodyHover: themeColors.surface['gray-subtler'],
13
+ border: themeColors.border['gray-light'],
14
+ borderLight: themeColors.border['secondary-light'],
15
+ text: themeColors.text.bolder,
16
+ textSecondary: themeColors.text.subtle,
17
+ scrollThumb: themeColors.surface['gray-subtle'],
18
+ scrollThumbBorder: themeColors.border.gray,
19
+ selected: themeColors.surface['information-subtler'],
20
+ selectedBorder: themeColors.border.primary,
21
+ disabledText: themeColors.text.disabled,
22
+ } as const;
23
+
24
+ const spacing = {
25
+ cellPadding: '4px 16px',
26
+ headerPadding: '4px 16px',
27
+ } as const;
28
+
29
+ // ============================================
30
+ // 레이아웃 컴포넌트
31
+ // ============================================
32
+ export const TableOuterWrapper = styled.div`
33
+ position: relative;
34
+ display: inline-block;
35
+ outline: none;
36
+
37
+ &:focus {
38
+ outline: none;
39
+ }
40
+ `;
41
+
42
+ export const TableWrapper = styled.div`
43
+ display: flex;
44
+ flex-direction: column;
45
+ width: 100%;
46
+ overflow: hidden;
47
+ `;
48
+
49
+ export const TableContainer = styled.div<{ maxHeight?: string }>`
50
+ width: 100%;
51
+ overflow: auto;
52
+ position: relative;
53
+ ${({ maxHeight }) => maxHeight && `max-height: ${maxHeight};`}
54
+
55
+ /* 스크롤바 스타일 */
56
+ &::-webkit-scrollbar {
57
+ width: 20px;
58
+ height: 20px;
59
+ }
60
+
61
+ &::-webkit-scrollbar-track {
62
+ background: ${colors.body};
63
+ border: 1px solid ${colors.border};
64
+ margin-top: 20px; /* 상단 화살표 버튼 높이만큼 여백 */
65
+ }
66
+
67
+ &::-webkit-scrollbar-thumb {
68
+ background: ${colors.scrollThumb};
69
+ border: 1px solid ${colors.scrollThumbBorder};
70
+
71
+ &:hover {
72
+ background: ${colors.headerHover};
73
+ }
74
+ }
75
+
76
+ &::-webkit-scrollbar-button:vertical:start:decrement,
77
+ &::-webkit-scrollbar-button:vertical:end:increment {
78
+ display: block;
79
+ height: 20px;
80
+ background: ${colors.header};
81
+ border: 1px solid ${colors.borderLight};
82
+ }
83
+
84
+ &::-webkit-scrollbar-button:vertical:start:decrement:hover,
85
+ &::-webkit-scrollbar-button:vertical:end:increment:hover {
86
+ background: ${colors.headerHover};
87
+ }
88
+ `;
89
+
90
+ // ============================================
91
+ // 테이블 기본 컴포넌트
92
+ // ============================================
93
+ export const StyledTable = styled.table`
94
+ width: 100%;
95
+ border-collapse: separate;
96
+ border-spacing: 0;
97
+ table-layout: auto;
98
+ font-family: ${typography.fontFamily.primary};
99
+ `;
100
+
101
+ export const TableHead = styled.thead`
102
+ position: sticky;
103
+ top: 0;
104
+ z-index: 10;
105
+ background: ${colors.header};
106
+ `;
107
+
108
+ export const TableBody = styled.tbody``;
109
+
110
+ export const TableRow = styled.tr<{ striped?: boolean }>`
111
+ &:nth-of-type(even) {
112
+ ${({ striped }) => striped && `background-color: ${colors.bodyHover};`}
113
+ }
114
+
115
+ &:hover {
116
+ background-color: ${colors.bodyHover};
117
+ }
118
+ `;
119
+
120
+ // ============================================
121
+ // 셀 컴포넌트
122
+ // ============================================
123
+ const baseCellStyle = `
124
+ box-sizing: border-box;
125
+ vertical-align: middle;
126
+ line-height: 1.5;
127
+ `;
128
+
129
+ export const TableHeaderCell = styled.th<{ width?: string; sortable?: boolean }>`
130
+ ${baseCellStyle}
131
+ min-width: ${({ width }) => (width ? '0' : '80px')};
132
+ background: ${colors.header};
133
+ border: 1px solid ${colors.borderLight};
134
+ border-left: none;
135
+ padding: ${spacing.headerPadding};
136
+ text-align: left;
137
+ font-weight: 700;
138
+ font-size: 15px;
139
+ color: ${colors.text};
140
+ height: 30px;
141
+ white-space: nowrap;
142
+ position: relative;
143
+ ${({ width }) => width && `width: ${width};`}
144
+
145
+ &:first-of-type {
146
+ border-left: 1px solid ${colors.borderLight};
147
+ }
148
+
149
+ ${({ sortable }) =>
150
+ sortable &&
151
+ `
152
+ cursor: pointer;
153
+ user-select: none;
154
+
155
+ &:hover {
156
+ background: ${colors.headerHover};
157
+ }
158
+ `}
159
+ `;
160
+
161
+ export const TableDataCell = styled.td<{
162
+ editable?: boolean;
163
+ width?: string;
164
+ height?: string;
165
+ isHeaderColumn?: boolean;
166
+ isSelected?: boolean;
167
+ $edgeTop?: boolean;
168
+ $edgeBottom?: boolean;
169
+ $edgeLeft?: boolean;
170
+ $edgeRight?: boolean;
171
+ $rowSelected?: boolean;
172
+ }>`
173
+ ${baseCellStyle}
174
+ min-width: ${({ width }) => (width ? '0' : '80px')};
175
+ background: ${({ isHeaderColumn, isSelected, $rowSelected }) =>
176
+ isSelected ? colors.selected : (isHeaderColumn ? colors.header : ($rowSelected ? 'inherit' : colors.body))};
177
+ border-right: 1px solid ${({ isHeaderColumn }) =>
178
+ isHeaderColumn ? colors.borderLight : colors.border};
179
+ border-bottom: 1px solid ${({ isHeaderColumn }) =>
180
+ isHeaderColumn ? colors.borderLight : colors.border};
181
+ border-left: none;
182
+ border-top: none;
183
+ padding: ${({ isHeaderColumn }) => (isHeaderColumn ? spacing.headerPadding : spacing.cellPadding)};
184
+ font-weight: ${({ isHeaderColumn }) => (isHeaderColumn ? 700 : 400)};
185
+ font-size: ${({ isHeaderColumn }) => (isHeaderColumn ? '15px' : '13px')};
186
+ color: ${({ isHeaderColumn }) => (isHeaderColumn ? colors.text : colors.textSecondary)};
187
+ height: ${({ height }) => height ?? '30px'};
188
+ position: relative;
189
+ user-select: none;
190
+
191
+ &:first-of-type {
192
+ border-left: 1px solid ${({ isHeaderColumn }) =>
193
+ isHeaderColumn ? colors.borderLight : colors.border};
194
+ }
195
+
196
+ ${({ isSelected, $edgeTop, $edgeBottom, $edgeLeft, $edgeRight }) =>
197
+ isSelected &&
198
+ `
199
+ z-index: 1;
200
+ box-shadow:
201
+ ${$edgeTop ? `inset 0 2px 0 0 ${colors.selectedBorder}` : `inset 0 0.5px 0 0 ${colors.selectedBorder}50`}${$edgeBottom ? `, inset 0 -2px 0 0 ${colors.selectedBorder}` : `, inset 0 -0.5px 0 0 ${colors.selectedBorder}50`}${$edgeLeft ? `, inset 2px 0 0 0 ${colors.selectedBorder}` : `, inset 0.5px 0 0 0 ${colors.selectedBorder}50`}${$edgeRight ? `, inset -2px 0 0 0 ${colors.selectedBorder}` : `, inset -0.5px 0 0 0 ${colors.selectedBorder}50`};
202
+ `}
203
+
204
+ ${({ editable, isSelected, $rowSelected }) =>
205
+ editable && !isSelected && !$rowSelected &&
206
+ `
207
+ cursor: cell;
208
+ &:hover {
209
+ background-color: ${colors.bodyHover};
210
+ }
211
+ `}
212
+
213
+ ${({ isSelected }) =>
214
+ isSelected &&
215
+ `
216
+ &:hover {
217
+ background-color: ${colors.selected};
218
+ }
219
+ `}
220
+ `;
221
+
222
+ // ============================================
223
+ // 정렬 및 입력 컴포넌트
224
+ // ============================================
225
+ export const SortIcon = styled.span<{ active?: boolean; direction?: SortDirection }>`
226
+ display: inline-flex;
227
+ flex-direction: column;
228
+ margin-left: 4px;
229
+ opacity: ${({ active }) => (active ? 1 : 0.3)};
230
+
231
+ svg {
232
+ width: 12px;
233
+ height: 12px;
234
+ }
235
+ `;
236
+
237
+ export const EditableInput = styled.input`
238
+ width: 100%;
239
+ border: none;
240
+ outline: none;
241
+ background: transparent;
242
+ font: inherit;
243
+ color: inherit;
244
+ margin: -12px -16px;
245
+ padding: ${spacing.cellPadding};
246
+
247
+ &:focus {
248
+ outline: none;
249
+ }
250
+ `;
251
+
252
+ // ============================================
253
+ // 스크롤 컨트롤
254
+ // ============================================
255
+ export const ScrollContainer = styled.div`
256
+ display: flex;
257
+ flex-direction: column;
258
+ position: absolute;
259
+ right: 0;
260
+ top: 0;
261
+ `;
262
+
263
+ export const ScrollButton = styled.button<{ position: 'top' | 'bottom' }>`
264
+ width: 20px;
265
+ height: 20px;
266
+ padding: 0;
267
+ background: ${colors.header};
268
+ border: 1px solid ${colors.borderLight};
269
+ cursor: pointer;
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: center;
273
+ transition: background-color 0.2s;
274
+
275
+ &:hover {
276
+ background: ${colors.headerHover};
277
+ }
278
+
279
+ &:disabled {
280
+ opacity: 0.5;
281
+ cursor: not-allowed;
282
+ }
283
+
284
+ svg {
285
+ width: 14px;
286
+ height: 14px;
287
+ color: ${colors.text};
288
+ }
289
+ `;
290
+
291
+ // ============================================
292
+ // 컨텍스트 메뉴
293
+ // ============================================
294
+ export const ContextMenuOverlay = styled.div`
295
+ position: fixed;
296
+ top: 0;
297
+ left: 0;
298
+ right: 0;
299
+ bottom: 0;
300
+ z-index: 999;
301
+ `;
302
+
303
+ export const ContextMenu = styled.div<{ x: number; y: number }>`
304
+ position: fixed;
305
+ top: ${({ y }) => y}px;
306
+ left: ${({ x }) => x}px;
307
+ z-index: 1000;
308
+ min-width: 160px;
309
+ background: ${colors.body};
310
+ border: 1px solid ${colors.border};
311
+ border-radius: 6px;
312
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
313
+ padding: 4px 0;
314
+ font-family: ${typography.fontFamily.primary};
315
+ `;
316
+
317
+ export const ContextMenuItem = styled.button<{ disabled?: boolean }>`
318
+ width: 100%;
319
+ padding: 8px 12px;
320
+ border: none;
321
+ background: transparent;
322
+ text-align: left;
323
+ font-size: 13px;
324
+ color: ${({ disabled }) => (disabled ? colors.disabledText : colors.text)};
325
+ cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
326
+ display: flex;
327
+ align-items: center;
328
+ gap: 8px;
329
+
330
+ &:hover {
331
+ background: ${({ disabled }) => (disabled ? 'transparent' : colors.bodyHover)};
332
+ }
333
+
334
+ span.shortcut {
335
+ margin-left: auto;
336
+ font-size: 11px;
337
+ color: ${colors.disabledText};
338
+ }
339
+ `;
340
+
341
+ export const ContextMenuDivider = styled.div`
342
+ height: 1px;
343
+ background: ${colors.borderLight};
344
+ margin: 4px 0;
345
+ `;
@@ -0,0 +1,112 @@
1
+ import React from 'react';
2
+ import type { TableBodyProps, CellPosition } from '../../types/table';
3
+ import { TableBody as StyledTableBody, TableRow } from './style';
4
+ import TableCell from './table-cell';
5
+
6
+ const getSelectionInfo = (
7
+ rowIndex: number,
8
+ colIndex: number,
9
+ start: CellPosition | null | undefined,
10
+ end: CellPosition | null | undefined
11
+ ): { isSelected: boolean; edge: { top: boolean; bottom: boolean; left: boolean; right: boolean } } => {
12
+ if (!start || !end) {
13
+ return { isSelected: false, edge: { top: false, bottom: false, left: false, right: false } };
14
+ }
15
+
16
+ const minRow = Math.min(start.row, end.row);
17
+ const maxRow = Math.max(start.row, end.row);
18
+ const minCol = Math.min(start.col, end.col);
19
+ const maxCol = Math.max(start.col, end.col);
20
+
21
+ const isSelected = rowIndex >= minRow && rowIndex <= maxRow && colIndex >= minCol && colIndex <= maxCol;
22
+
23
+ if (!isSelected) {
24
+ return { isSelected: false, edge: { top: false, bottom: false, left: false, right: false } };
25
+ }
26
+
27
+ return {
28
+ isSelected: true,
29
+ edge: {
30
+ top: rowIndex === minRow,
31
+ bottom: rowIndex === maxRow,
32
+ left: colIndex === minCol,
33
+ right: colIndex === maxCol,
34
+ }
35
+ };
36
+ };
37
+
38
+ export default function TableBody<T extends Record<string, unknown> = Record<string, unknown>>({
39
+ columns,
40
+ data,
41
+ rowHeight,
42
+ onCellEdit,
43
+ selectionStart,
44
+ selectionEnd,
45
+ editingCell,
46
+ editStartValue,
47
+ editToken,
48
+ onCellMouseDown,
49
+ onCellMouseEnter,
50
+ onCellMouseUp,
51
+ enableRowSelection,
52
+ selectedRowIndex,
53
+ hoveredRowIndex,
54
+ onRowClick,
55
+ onRowHover,
56
+ }: TableBodyProps<T>) {
57
+ const getRowBackground = (rowIndex: number): string | undefined => {
58
+ if (!enableRowSelection) return undefined;
59
+ if (selectedRowIndex === rowIndex) return '#e7f4fe';
60
+ if (hoveredRowIndex === rowIndex) return '#f4f5f6';
61
+ return undefined;
62
+ };
63
+
64
+ return (
65
+ <StyledTableBody>
66
+ {data.map((row, rowIndex) => (
67
+ <TableRow
68
+ key={rowIndex}
69
+ onClick={enableRowSelection ? () => onRowClick?.(rowIndex) : undefined}
70
+ onMouseEnter={enableRowSelection ? () => onRowHover?.(rowIndex) : undefined}
71
+ onMouseLeave={enableRowSelection ? () => onRowHover?.(null) : undefined}
72
+ style={{
73
+ cursor: enableRowSelection ? 'pointer' : undefined,
74
+ backgroundColor: getRowBackground(rowIndex),
75
+ transition: enableRowSelection ? 'background-color 0.15s ease' : undefined,
76
+ }}
77
+ >
78
+ {columns.map((col, colIndex) => {
79
+ const { isSelected, edge } = getSelectionInfo(rowIndex, colIndex, selectionStart, selectionEnd);
80
+ const isEditingRequested =
81
+ !!editingCell && editingCell.row === rowIndex && editingCell.col === colIndex;
82
+ return (
83
+ <TableCell
84
+ key={`${rowIndex}-${col.key}`}
85
+ value={row[col.key]}
86
+ editable={col.editable !== false}
87
+ width={col.width}
88
+ height={col.height}
89
+ rowHeight={rowHeight}
90
+ dataType={col.dataType}
91
+ isHeaderColumn={col.isHeaderColumn}
92
+ isSelected={isSelected}
93
+ rowSelected={enableRowSelection && (selectedRowIndex === rowIndex || hoveredRowIndex === rowIndex)}
94
+ isEditingRequested={isEditingRequested}
95
+ startEditingToken={editToken}
96
+ startEditingValue={isEditingRequested ? editStartValue : null}
97
+ selectionEdge={isSelected ? edge : undefined}
98
+ rowSpan={col.rowSpan}
99
+ colSpan={col.colSpan}
100
+ onEdit={(value) => onCellEdit?.(rowIndex, col.key, value)}
101
+ render={col.render ? (value) => col.render!(value, row, rowIndex) : undefined}
102
+ onMouseDown={enableRowSelection ? undefined : () => onCellMouseDown?.(rowIndex, colIndex)}
103
+ onMouseEnter={enableRowSelection ? undefined : () => onCellMouseEnter?.(rowIndex, colIndex)}
104
+ onMouseUp={enableRowSelection ? undefined : onCellMouseUp}
105
+ />
106
+ );
107
+ })}
108
+ </TableRow>
109
+ ))}
110
+ </StyledTableBody>
111
+ );
112
+ }
@@ -0,0 +1,153 @@
1
+ import React, { useEffect, useRef, useState, useCallback, type KeyboardEvent, type MouseEvent } from 'react';
2
+ import type { TableCellProps, DataType } from '../../types/table';
3
+ import { TableDataCell, EditableInput } from './style';
4
+
5
+ const formatValue = (val: unknown, dataType: DataType): string => {
6
+ if (val == null) return '';
7
+
8
+ switch (dataType) {
9
+ case 'number':
10
+ return typeof val === 'number' ? val.toLocaleString() : String(val);
11
+ case 'date':
12
+ return val instanceof Date ? val.toLocaleDateString('ko-KR') : String(val);
13
+ case 'boolean':
14
+ return val ? '예' : '아니오';
15
+ default:
16
+ return String(val);
17
+ }
18
+ };
19
+
20
+ const parseValue = (val: string, dataType: DataType): unknown => {
21
+ switch (dataType) {
22
+ case 'number': {
23
+ const num = parseFloat(val.replace(/,/g, ''));
24
+ return isNaN(num) ? 0 : num;
25
+ }
26
+ case 'date':
27
+ return new Date(val);
28
+ case 'boolean':
29
+ return val === '예' || val === 'true' || val === '1';
30
+ default:
31
+ return val;
32
+ }
33
+ };
34
+
35
+ const getInputType = (dataType: DataType): string => {
36
+ if (dataType === 'number') return 'number';
37
+ if (dataType === 'date') return 'date';
38
+ return 'text';
39
+ };
40
+
41
+ export default function TableCell({
42
+ value,
43
+ editable = false,
44
+ width,
45
+ height,
46
+ rowHeight,
47
+ dataType = 'text',
48
+ isHeaderColumn = false,
49
+ isSelected = false,
50
+ rowSelected = false,
51
+ isEditingRequested,
52
+ startEditingToken,
53
+ startEditingValue,
54
+ selectionEdge,
55
+ rowSpan,
56
+ colSpan,
57
+ onEdit,
58
+ render,
59
+ onMouseDown,
60
+ onMouseEnter,
61
+ onMouseUp,
62
+ }: TableCellProps) {
63
+ const [isEditing, setIsEditing] = useState(false);
64
+ const [editValue, setEditValue] = useState('');
65
+ const lastStartTokenRef = useRef<number | undefined>(undefined);
66
+
67
+ const startEditing = useCallback(() => {
68
+ if (!editable) return;
69
+ setEditValue(formatValue(value, dataType));
70
+ setIsEditing(true);
71
+ }, [editable, value, dataType]);
72
+
73
+ // 부모에서 "편집 시작" 요청이 오면 (예: 선택 셀에서 타이핑) 편집 모드로 진입
74
+ useEffect(() => {
75
+ if (!isEditingRequested) return;
76
+ if (startEditingToken == null) return;
77
+ if (lastStartTokenRef.current === startEditingToken) return;
78
+
79
+ lastStartTokenRef.current = startEditingToken;
80
+ if (!editable) return;
81
+
82
+ const nextValue = startEditingValue ?? formatValue(value, dataType);
83
+ setEditValue(nextValue);
84
+ setIsEditing(true);
85
+ }, [isEditingRequested, startEditingToken, startEditingValue, editable, value, dataType]);
86
+
87
+ const save = useCallback(() => {
88
+ onEdit?.(parseValue(editValue, dataType));
89
+ setIsEditing(false);
90
+ }, [editValue, dataType, onEdit]);
91
+
92
+ const cancel = useCallback(() => {
93
+ setEditValue('');
94
+ setIsEditing(false);
95
+ }, []);
96
+
97
+ const handleKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
98
+ if (e.key === 'Enter') {
99
+ e.preventDefault();
100
+ save();
101
+ } else if (e.key === 'Escape') {
102
+ e.preventDefault();
103
+ cancel();
104
+ }
105
+ }, [save, cancel]);
106
+
107
+ const handleMouseDown = useCallback((e: MouseEvent) => {
108
+ if (isEditing) return;
109
+ e.preventDefault();
110
+ onMouseDown?.();
111
+ }, [isEditing, onMouseDown]);
112
+
113
+ const handleMouseEnter = useCallback(() => {
114
+ if (isEditing) return;
115
+ onMouseEnter?.();
116
+ }, [isEditing, onMouseEnter]);
117
+
118
+ const displayValue = render ? render(value) : formatValue(value, dataType);
119
+
120
+ return (
121
+ <TableDataCell
122
+ editable={editable}
123
+ width={width}
124
+ height={height || rowHeight}
125
+ isHeaderColumn={isHeaderColumn}
126
+ isSelected={isSelected}
127
+ $rowSelected={rowSelected}
128
+ $edgeTop={selectionEdge?.top}
129
+ $edgeBottom={selectionEdge?.bottom}
130
+ $edgeLeft={selectionEdge?.left}
131
+ $edgeRight={selectionEdge?.right}
132
+ rowSpan={rowSpan}
133
+ colSpan={colSpan}
134
+ onDoubleClick={startEditing}
135
+ onMouseDown={handleMouseDown}
136
+ onMouseEnter={handleMouseEnter}
137
+ onMouseUp={onMouseUp}
138
+ >
139
+ {isEditing ? (
140
+ <EditableInput
141
+ type={getInputType(dataType)}
142
+ value={editValue}
143
+ onChange={(e) => setEditValue(e.target.value)}
144
+ onBlur={save}
145
+ onKeyDown={handleKeyDown}
146
+ autoFocus
147
+ />
148
+ ) : (
149
+ displayValue
150
+ )}
151
+ </TableDataCell>
152
+ );
153
+ }
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import { ChevronUp, ChevronDown } from 'lucide-react';
3
+ import type { TableHeaderProps, SortDirection } from '../../types/table';
4
+ import { TableHead, TableRow, TableHeaderCell, SortIcon } from './style';
5
+
6
+ /** 정렬 아이콘 컴포넌트 */
7
+ function SortIndicator({ isActive, direction }: { isActive: boolean; direction?: SortDirection }) {
8
+ if (!isActive || !direction) {
9
+ return <ChevronUp style={{ opacity: 0.3 }} />;
10
+ }
11
+ return direction === 'asc' ? <ChevronUp /> : <ChevronDown />;
12
+ }
13
+
14
+ /** 테이블 헤더 */
15
+ export default function TableHeader<T = Record<string, unknown>>({
16
+ columns,
17
+ sortColumn,
18
+ sortDirection,
19
+ onSort,
20
+ }: TableHeaderProps<T>) {
21
+ const handleClick = (key: string, sortable?: boolean) => {
22
+ if (sortable) onSort?.(key);
23
+ };
24
+
25
+ return (
26
+ <TableHead>
27
+ <TableRow>
28
+ {columns.map(({ key, header, width, sortable, rowSpan, colSpan }) => {
29
+ const isActive = sortColumn === key;
30
+
31
+ return (
32
+ <TableHeaderCell
33
+ key={key}
34
+ width={width}
35
+ sortable={sortable}
36
+ rowSpan={rowSpan}
37
+ colSpan={colSpan}
38
+ onClick={() => handleClick(key, sortable)}
39
+ >
40
+ {header}
41
+ {sortable && (
42
+ <SortIcon active={isActive} direction={isActive ? sortDirection : null}>
43
+ <SortIndicator isActive={isActive} direction={sortDirection} />
44
+ </SortIcon>
45
+ )}
46
+ </TableHeaderCell>
47
+ );
48
+ })}
49
+ </TableRow>
50
+ </TableHead>
51
+ );
52
+ }
@@ -1,6 +1,7 @@
1
- export * from './Button'
2
- export * from './CheckBox'
1
+ export * from './Button';
2
+ export * from './CheckBox';
3
3
  export * from './Dropdown'
4
- export * from './Input'
4
+ export * from './Input';
5
5
  export * from './Sidebar'
6
- export * from './TabBar'
6
+ export * from './TabBar';
7
+ export * from './Table';