@conscience-technology/ctds 0.0.0-stage → 0.1.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.
- package/README.md +128 -2
- package/assets/components/AppFrame/AppFrame.module.css +177 -0
- package/assets/components/AppFrame/AppFrame.tsx +93 -0
- package/assets/components/AppFrame/index.ts +6 -0
- package/assets/components/AttachmentChip/AttachmentChip.module.css +71 -0
- package/assets/components/AttachmentChip/AttachmentChip.tsx +74 -0
- package/assets/components/AttachmentChip/index.ts +2 -0
- package/assets/components/AttachmentThumb/AttachmentThumb.module.css +107 -0
- package/assets/components/AttachmentThumb/AttachmentThumb.tsx +91 -0
- package/assets/components/AttachmentThumb/index.ts +2 -0
- package/assets/components/Avatar/Avatar.module.css +167 -0
- package/assets/components/Avatar/Avatar.tsx +228 -0
- package/assets/components/Avatar/index.ts +2 -0
- package/assets/components/AvatarUpload/AvatarUpload.module.css +130 -0
- package/assets/components/AvatarUpload/AvatarUpload.tsx +139 -0
- package/assets/components/AvatarUpload/index.ts +2 -0
- package/assets/components/Badge/Badge.module.css +76 -0
- package/assets/components/Badge/Badge.tsx +68 -0
- package/assets/components/Badge/index.ts +2 -0
- package/assets/components/Breadcrumb/Breadcrumb.module.css +213 -0
- package/assets/components/Breadcrumb/Breadcrumb.tsx +279 -0
- package/assets/components/Breadcrumb/index.ts +8 -0
- package/assets/components/Button/Button.module.css +216 -0
- package/assets/components/Button/Button.tsx +149 -0
- package/assets/components/Button/index.ts +2 -0
- package/assets/components/Callout/Callout.module.css +146 -0
- package/assets/components/Callout/Callout.tsx +138 -0
- package/assets/components/Callout/index.ts +2 -0
- package/assets/components/ChatInput/ChatInput.module.css +241 -0
- package/assets/components/ChatInput/ChatInput.tsx +322 -0
- package/assets/components/ChatInput/ModelSelector.tsx +40 -0
- package/assets/components/ChatInput/SendButton.tsx +41 -0
- package/assets/components/ChatInput/index.ts +10 -0
- package/assets/components/Checkbox/Checkbox.module.css +66 -0
- package/assets/components/Checkbox/Checkbox.tsx +36 -0
- package/assets/components/Checkbox/index.ts +2 -0
- package/assets/components/Chip/Chip.module.css +128 -0
- package/assets/components/Chip/Chip.tsx +103 -0
- package/assets/components/Chip/index.ts +2 -0
- package/assets/components/ChoiceCard/ChoiceCard.module.css +142 -0
- package/assets/components/ChoiceCard/ChoiceCard.tsx +132 -0
- package/assets/components/ChoiceCard/index.ts +2 -0
- package/assets/components/ContextMenu/ContextMenu.module.css +102 -0
- package/assets/components/ContextMenu/ContextMenu.tsx +392 -0
- package/assets/components/ContextMenu/index.ts +9 -0
- package/assets/components/DatePicker/Calendar.tsx +234 -0
- package/assets/components/DatePicker/DESIGN_NOTES.md +106 -0
- package/assets/components/DatePicker/DESIGN_NOTES_TIME.md +125 -0
- package/assets/components/DatePicker/DatePicker.module.css +295 -0
- package/assets/components/DatePicker/DatePicker.tsx +387 -0
- package/assets/components/DatePicker/TimeSelector.tsx +69 -0
- package/assets/components/DatePicker/index.ts +8 -0
- package/assets/components/DatePicker/presets.ts +80 -0
- package/assets/components/DatePicker/utils.ts +148 -0
- package/assets/components/Disclosure/Disclosure.module.css +73 -0
- package/assets/components/Disclosure/Disclosure.tsx +129 -0
- package/assets/components/Disclosure/index.ts +6 -0
- package/assets/components/Drawer/Drawer.module.css +257 -0
- package/assets/components/Drawer/Drawer.tsx +126 -0
- package/assets/components/Drawer/index.ts +7 -0
- package/assets/components/EmptyState/EmptyState.module.css +102 -0
- package/assets/components/EmptyState/EmptyState.tsx +80 -0
- package/assets/components/EmptyState/index.ts +2 -0
- package/assets/components/FileIcon/FileIcon.module.css +66 -0
- package/assets/components/FileIcon/FileIcon.tsx +84 -0
- package/assets/components/FileIcon/fileExtMap.ts +112 -0
- package/assets/components/FileIcon/index.ts +9 -0
- package/assets/components/FilterButton/FilterButton.module.css +130 -0
- package/assets/components/FilterButton/FilterButton.tsx +94 -0
- package/assets/components/FilterButton/index.ts +2 -0
- package/assets/components/GalleryItem/GalleryItem.module.css +220 -0
- package/assets/components/GalleryItem/GalleryItem.tsx +163 -0
- package/assets/components/GalleryItem/index.ts +7 -0
- package/assets/components/Icon/Icon.tsx +616 -0
- package/assets/components/Icon/context.tsx +39 -0
- package/assets/components/Icon/index.ts +4 -0
- package/assets/components/IconButton/IconButton.module.css +109 -0
- package/assets/components/IconButton/IconButton.tsx +110 -0
- package/assets/components/IconButton/index.ts +2 -0
- package/assets/components/Input/Input.module.css +338 -0
- package/assets/components/Input/Input.tsx +371 -0
- package/assets/components/Input/index.ts +2 -0
- package/assets/components/ListItem/ListItem.module.css +318 -0
- package/assets/components/ListItem/ListItem.tsx +262 -0
- package/assets/components/ListItem/index.ts +9 -0
- package/assets/components/MenuItem/MenuItem.module.css +437 -0
- package/assets/components/MenuItem/MenuItem.tsx +529 -0
- package/assets/components/MenuItem/index.ts +8 -0
- package/assets/components/MetricCard/MetricCard.module.css +153 -0
- package/assets/components/MetricCard/MetricCard.tsx +140 -0
- package/assets/components/MetricCard/index.ts +2 -0
- package/assets/components/Modal/Modal.module.css +175 -0
- package/assets/components/Modal/Modal.tsx +98 -0
- package/assets/components/Modal/index.ts +2 -0
- package/assets/components/Onboarding/DocsIndex.tsx +82 -0
- package/assets/components/Onboarding/LayoutBuilder.tsx +160 -0
- package/assets/components/Onboarding/Onboarding.module.css +1394 -0
- package/assets/components/Onboarding/OnboardingAxisPanel.tsx +515 -0
- package/assets/components/Onboarding/OnboardingPage.tsx +1193 -0
- package/assets/components/Onboarding/PresetsGallery.tsx +65 -0
- package/assets/components/Onboarding/actions.ts +250 -0
- package/assets/components/Onboarding/index.ts +8 -0
- package/assets/components/Onboarding/palette.css +40 -0
- package/assets/components/Onboarding/wizardData.ts +265 -0
- package/assets/components/Pagination/Pagination.module.css +82 -0
- package/assets/components/Pagination/Pagination.tsx +248 -0
- package/assets/components/Pagination/index.ts +6 -0
- package/assets/components/ProgressBar/ProgressBar.module.css +64 -0
- package/assets/components/ProgressBar/ProgressBar.tsx +54 -0
- package/assets/components/ProgressBar/index.ts +2 -0
- package/assets/components/QuotationBar/QuotationBar.module.css +61 -0
- package/assets/components/QuotationBar/QuotationBar.tsx +48 -0
- package/assets/components/QuotationBar/index.ts +2 -0
- package/assets/components/Radio/Radio.module.css +63 -0
- package/assets/components/Radio/Radio.tsx +33 -0
- package/assets/components/Radio/index.ts +2 -0
- package/assets/components/Select/Select.module.css +151 -0
- package/assets/components/Select/Select.tsx +425 -0
- package/assets/components/Select/index.ts +10 -0
- package/assets/components/SelectableCard/SelectableCard.module.css +177 -0
- package/assets/components/SelectableCard/SelectableCard.tsx +83 -0
- package/assets/components/SelectableCard/index.ts +2 -0
- package/assets/components/Shortcut/Shortcut.module.css +56 -0
- package/assets/components/Shortcut/Shortcut.tsx +156 -0
- package/assets/components/Shortcut/index.ts +2 -0
- package/assets/components/Sidebar/Sidebar.module.css +242 -0
- package/assets/components/Sidebar/Sidebar.tsx +264 -0
- package/assets/components/Sidebar/index.ts +7 -0
- package/assets/components/SidebarSplit/SidebarSplit.module.css +126 -0
- package/assets/components/SidebarSplit/SidebarSplit.tsx +222 -0
- package/assets/components/SidebarSplit/index.ts +6 -0
- package/assets/components/Slider/Slider.module.css +120 -0
- package/assets/components/Slider/Slider.tsx +88 -0
- package/assets/components/Slider/index.ts +2 -0
- package/assets/components/SplitButton/SplitButton.module.css +190 -0
- package/assets/components/SplitButton/SplitButton.tsx +145 -0
- package/assets/components/SplitButton/index.ts +6 -0
- package/assets/components/Stepper/Stepper.module.css +182 -0
- package/assets/components/Stepper/Stepper.tsx +185 -0
- package/assets/components/Stepper/index.ts +2 -0
- package/assets/components/Switch/Switch.module.css +116 -0
- package/assets/components/Switch/Switch.tsx +58 -0
- package/assets/components/Switch/index.ts +2 -0
- package/assets/components/TableCell/TableCell.module.css +137 -0
- package/assets/components/TableCell/TableCellAvatar.tsx +42 -0
- package/assets/components/TableCell/TableCellBadge.tsx +23 -0
- package/assets/components/TableCell/TableCellIcon.tsx +39 -0
- package/assets/components/TableCell/TableCellImage.tsx +49 -0
- package/assets/components/TableCell/TableCellLink.tsx +33 -0
- package/assets/components/TableCell/TableCellMeter.tsx +55 -0
- package/assets/components/TableCell/TableCellNumber.tsx +43 -0
- package/assets/components/TableCell/TableCellText.tsx +30 -0
- package/assets/components/TableCell/index.ts +23 -0
- package/assets/components/TableHeader/TableHeader.module.css +287 -0
- package/assets/components/TableHeader/TableHeader.tsx +590 -0
- package/assets/components/TableHeader/index.ts +9 -0
- package/assets/components/TableRow/TableRow.module.css +290 -0
- package/assets/components/TableRow/TableRow.tsx +253 -0
- package/assets/components/TableRow/index.ts +8 -0
- package/assets/components/TableRowEdit/TableRowEdit.module.css +126 -0
- package/assets/components/TableRowEdit/TableRowEdit.tsx +247 -0
- package/assets/components/TableRowEdit/index.ts +2 -0
- package/assets/components/TableRowSelect/TableRowSelect.module.css +23 -0
- package/assets/components/TableRowSelect/TableRowSelect.tsx +175 -0
- package/assets/components/TableRowSelect/index.ts +2 -0
- package/assets/components/Tabs/Tabs.module.css +234 -0
- package/assets/components/Tabs/Tabs.tsx +426 -0
- package/assets/components/Tabs/index.ts +9 -0
- package/assets/components/Textarea/Textarea.module.css +318 -0
- package/assets/components/Textarea/Textarea.tsx +426 -0
- package/assets/components/Textarea/index.ts +7 -0
- package/assets/components/ThemeController.tsx +271 -0
- package/assets/components/TimePicker/TimePicker.module.css +317 -0
- package/assets/components/TimePicker/TimePicker.tsx +821 -0
- package/assets/components/TimePicker/index.ts +8 -0
- package/assets/components/TimePicker/utils.ts +115 -0
- package/assets/components/Toast/Toast.module.css +264 -0
- package/assets/components/Toast/Toast.tsx +243 -0
- package/assets/components/Toast/index.ts +15 -0
- package/assets/components/Toolbar/Toolbar.module.css +72 -0
- package/assets/components/Toolbar/Toolbar.tsx +75 -0
- package/assets/components/Toolbar/index.ts +2 -0
- package/assets/components/Tooltip/Tooltip.module.css +42 -0
- package/assets/components/Tooltip/Tooltip.tsx +332 -0
- package/assets/components/Tooltip/index.ts +2 -0
- package/assets/components/Topbar/Topbar.module.css +26 -0
- package/assets/components/Topbar/Topbar.tsx +47 -0
- package/assets/components/Topbar/index.ts +2 -0
- package/assets/components/_cn.ts +3 -0
- package/assets/components/_color.ts +48 -0
- package/assets/components/_data.ts +659 -0
- package/assets/components/_utils.css +19 -0
- package/assets/components/axisOptions.ts +24 -0
- package/assets/components/tokens.css +742 -0
- package/assets/prompts/GUIDE.md +251 -0
- package/assets/prompts/OVERRIDE.md +170 -0
- package/assets/prompts/PRINCIPLES.md +130 -0
- package/assets/prompts/axes/density.md +38 -0
- package/assets/prompts/axes/font.md +36 -0
- package/assets/prompts/axes/hue.md +36 -0
- package/assets/prompts/axes/icon.md +48 -0
- package/assets/prompts/axes/mode.md +50 -0
- package/assets/prompts/axes/motion.md +77 -0
- package/assets/prompts/axes/palette.md +35 -0
- package/assets/prompts/axes/shape.md +33 -0
- package/assets/prompts/axes/voice.md +54 -0
- package/assets/prompts/components/README.md +84 -0
- package/assets/prompts/components/app-frame.md +94 -0
- package/assets/prompts/components/attachment-chip.md +46 -0
- package/assets/prompts/components/attachment-thumb.md +49 -0
- package/assets/prompts/components/avatar.md +86 -0
- package/assets/prompts/components/badge.md +104 -0
- package/assets/prompts/components/breadcrumb.md +77 -0
- package/assets/prompts/components/button.md +99 -0
- package/assets/prompts/components/callout.md +81 -0
- package/assets/prompts/components/chat-input.md +84 -0
- package/assets/prompts/components/checkbox.md +46 -0
- package/assets/prompts/components/chip.md +84 -0
- package/assets/prompts/components/choice-card.md +127 -0
- package/assets/prompts/components/context-menu.md +145 -0
- package/assets/prompts/components/date-picker.md +104 -0
- package/assets/prompts/components/drawer.md +152 -0
- package/assets/prompts/components/empty-state.md +69 -0
- package/assets/prompts/components/file-icon.md +73 -0
- package/assets/prompts/components/filter-button.md +42 -0
- package/assets/prompts/components/form-field.md +74 -0
- package/assets/prompts/components/gallery-item.md +158 -0
- package/assets/prompts/components/icon-button.md +82 -0
- package/assets/prompts/components/icon.md +68 -0
- package/assets/prompts/components/input.md +95 -0
- package/assets/prompts/components/list-item.md +235 -0
- package/assets/prompts/components/menu-item.md +234 -0
- package/assets/prompts/components/metric-card.md +132 -0
- package/assets/prompts/components/modal.md +131 -0
- package/assets/prompts/components/pagination.md +92 -0
- package/assets/prompts/components/progress-bar.md +61 -0
- package/assets/prompts/components/quotation-bar.md +39 -0
- package/assets/prompts/components/radio.md +41 -0
- package/assets/prompts/components/select.md +104 -0
- package/assets/prompts/components/selectable-card.md +67 -0
- package/assets/prompts/components/shortcut.md +76 -0
- package/assets/prompts/components/sidebar.md +107 -0
- package/assets/prompts/components/slider.md +89 -0
- package/assets/prompts/components/split-button.md +71 -0
- package/assets/prompts/components/stepper.md +93 -0
- package/assets/prompts/components/switch.md +86 -0
- package/assets/prompts/components/table-cell.md +87 -0
- package/assets/prompts/components/table-header.md +157 -0
- package/assets/prompts/components/table-row-edit.md +129 -0
- package/assets/prompts/components/table-row-select.md +82 -0
- package/assets/prompts/components/table-row.md +156 -0
- package/assets/prompts/components/tabs.md +122 -0
- package/assets/prompts/components/textarea.md +105 -0
- package/assets/prompts/components/toast.md +149 -0
- package/assets/prompts/components/tooltip.md +71 -0
- package/assets/prompts/components/topbar.md +148 -0
- package/assets/prompts/primitives/emerge.md +43 -0
- package/assets/prompts/primitives/fade.md +31 -0
- package/assets/prompts/primitives/scale.md +32 -0
- package/assets/prompts/primitives/scroll.md +75 -0
- package/assets/prompts/primitives/slide.md +31 -0
- package/assets/prompts/themes/_SCHEMA.md +135 -0
- package/assets/prompts/themes/_TEMPLATE.md +73 -0
- package/assets/prompts/themes/expressive.md +118 -0
- package/assets/prompts/themes/pure.md +87 -0
- package/assets/prompts/themes/spare.md +73 -0
- package/assets/prompts/tokens/breakpoint.md +29 -0
- package/assets/prompts/tokens/color.md +78 -0
- package/assets/prompts/tokens/radius.md +33 -0
- package/assets/prompts/tokens/shadow.md +35 -0
- package/assets/prompts/tokens/spacing.md +86 -0
- package/assets/prompts/tokens/typography.md +76 -0
- package/assets/prompts/tokens/z-index.md +35 -0
- package/assets/public/expressive.png +0 -0
- package/assets/public/pure.png +0 -0
- package/assets/public/spare.png +0 -0
- package/assets/skill/SKILL.md +36 -0
- package/bin/ctds.js +76 -0
- package/package.json +35 -4
- package/src/commands/init.js +215 -0
- package/src/commands/update.js +275 -0
- package/src/utils/assets.js +78 -0
- package/src/utils/deps.js +51 -0
- package/src/utils/detect.js +47 -0
- package/src/utils/diff.js +52 -0
- package/src/utils/hash.js +16 -0
- package/src/utils/log.js +41 -0
- package/src/utils/meta.js +73 -0
- package/src/utils/prompt.js +28 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/* TableRow
|
|
2
|
+
───────────────────────────────────────────────────────────────
|
|
3
|
+
navigate 모드. 행 전체가 클릭 요소 (anchor 또는 role=button).
|
|
4
|
+
TableHeader와 짝 — 같은 columns/order/widths 스펙을 공유.
|
|
5
|
+
|
|
6
|
+
시각 원칙
|
|
7
|
+
- line : 행 하단 hairline. hover 시 전체 행 배경(hover).
|
|
8
|
+
- plate : 라운드 라인 안쪽 셀. hover 시 라운드 안쪽만 bg 변화.
|
|
9
|
+
- sm/md/lg : 세로 폭 + 셀 폰트 크기만 차이. padding은 공유.
|
|
10
|
+
- active : 현재 보고 있는 상세의 선택 상태. hover보다 한 톤 아래.
|
|
11
|
+
- disabled : 톤 다운 + pointer-events none.
|
|
12
|
+
|
|
13
|
+
토큰만 사용 — 색/시간/폰트 리터럴 금지.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
.root {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: stretch;
|
|
19
|
+
/* Header와 완전히 동일한 width/min-width 공식.
|
|
20
|
+
flex 컬럼 셀에 inline max-width:0 + width:0 을 걸어 콘텐츠가 max-content 계산에
|
|
21
|
+
참여하지 못하게 끊어낸다. 그래야 Row의 .root max-content 합이 Header와 일치. */
|
|
22
|
+
width: max-content;
|
|
23
|
+
min-width: 100%;
|
|
24
|
+
color: var(--fg);
|
|
25
|
+
text-decoration: none;
|
|
26
|
+
background: transparent;
|
|
27
|
+
transition: var(--transition-colors);
|
|
28
|
+
/* 폰트는 세 사이즈 공통 — 폰트가 바뀌면 테이블 리듬이 흔들린다. */
|
|
29
|
+
font-size: var(--text-sm);
|
|
30
|
+
line-height: var(--lh-sm);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* ── Size ───────────────────────────────────────────────────
|
|
34
|
+
세로 폭(min-height)만 사이즈 따라 스왑. 테이블 행의 "밀도"가 사이즈의 본질.
|
|
35
|
+
sm/md는 아주 가까운 두 밀도 — 데이터량이 많을 때 sm, 기본은 md.
|
|
36
|
+
lg는 이미지(아바타/썸네일)가 셀에 들어가는 경우 전용.
|
|
37
|
+
Density 축은 space-* 가 자동 반영. */
|
|
38
|
+
.sizeSm { min-height: var(--space-10); } /* 40 */
|
|
39
|
+
.sizeMd { min-height: calc(var(--space-10) + var(--space-2)); } /* 48 */
|
|
40
|
+
.sizeLg { min-height: var(--space-16); } /* 64 */
|
|
41
|
+
|
|
42
|
+
/* ── Variant: line ─────────────────────────────────────────
|
|
43
|
+
행 하단 hairline — 테이블의 default 흐름. header line variant와 짝. */
|
|
44
|
+
.line {
|
|
45
|
+
border-bottom: var(--hairline) solid var(--border);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* 마지막 행은 하단 hairline 없이 — 상위 컨테이너에서 :last-child 적용. */
|
|
49
|
+
.line:last-child {
|
|
50
|
+
border-bottom: none;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* line variant는 셀 자체 좌우 패딩을 먹지 않는다 — Header line과 짝.
|
|
54
|
+
콘텐츠가 테이블 좌/우 경계에 바짝 붙어 "선 사이의 데이터" 톤 유지.
|
|
55
|
+
plate에서는 .cell 기본 padding-inline을 유지해 셀 간 간격·hover 영역 확보. */
|
|
56
|
+
.line .cell {
|
|
57
|
+
padding-inline: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* line variant는 셀 사이에 최소한의 숨 쉴 공간(gap)을 둔다 — 셀 패딩이 0이라
|
|
61
|
+
콘텐츠가 서로 바짝 붙는 걸 방지. plate는 셀 자체 패딩으로 간격이 생겨 불필요. */
|
|
62
|
+
.line {
|
|
63
|
+
column-gap: var(--space-2);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* ── Variant: plate ────────────────────────────────────────
|
|
67
|
+
라운드 라인. 셀 간격감이 느껴지되 행 자체는 pill처럼 보임.
|
|
68
|
+
hover/active 배경은 라운드 안쪽에서만 발생.
|
|
69
|
+
padding은 TableHeader.plate와 동일(space-1 사면 균일)로 — 안쪽 셀들의 가로
|
|
70
|
+
origin을 헤더와 정확히 맞춰 열이 어긋나지 않도록 보장. */
|
|
71
|
+
.plate {
|
|
72
|
+
border-radius: var(--radius-lg);
|
|
73
|
+
padding: var(--space-1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ── Interactive states ────────────────────────────────────
|
|
77
|
+
anchor/button 역할일 때만 hover/active cursor 반응.
|
|
78
|
+
active 상태는 hover보다 약한 톤 — hover가 active를 덮어쓰지 않도록
|
|
79
|
+
active에선 bg를 먼저 깔고 hover 시엔 shadow만 추가. */
|
|
80
|
+
.root[role="row"][aria-selected]:not(.disabled),
|
|
81
|
+
a.root:not(.disabled),
|
|
82
|
+
.root[tabindex]:not(.disabled) {
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* hover: 전체 행 bg. active 상태 위에서 hover 시엔 bg를 덮지 않고
|
|
87
|
+
미세한 ring/강조만 — "hover가 active를 덮어쓰지 않는다" 원칙. */
|
|
88
|
+
a.root:not(.disabled):hover,
|
|
89
|
+
.root[tabindex]:not(.disabled):hover {
|
|
90
|
+
background: var(--hover);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.active {
|
|
94
|
+
background: var(--hover);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.active:hover {
|
|
98
|
+
background: var(--hover) !important; /* hover가 active를 덮지 않도록 유지. */
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* focus-visible — 행 외곽 ring. 키보드 탐색 가시화. */
|
|
102
|
+
a.root:not(.disabled):focus-visible,
|
|
103
|
+
.root[tabindex]:not(.disabled):focus-visible {
|
|
104
|
+
outline: none;
|
|
105
|
+
box-shadow: var(--ring-shadow);
|
|
106
|
+
border-radius: var(--radius-sm);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.plate:focus-visible {
|
|
110
|
+
border-radius: var(--radius-lg);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ── Disabled ──────────────────────────────────────────── */
|
|
114
|
+
.disabled {
|
|
115
|
+
color: var(--fg-disabled);
|
|
116
|
+
pointer-events: none;
|
|
117
|
+
cursor: not-allowed;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ── CellSlot ──────────────────────────────────────────
|
|
121
|
+
Header의 .cellSlot과 동일한 flex item 역할 + 추가로 contain: inline-size.
|
|
122
|
+
자식(.cell → .cellInner → 긴 nowrap 텍스트)의 가로 intrinsic size가
|
|
123
|
+
부모 .root의 width:max-content 계산에 전파되지 않도록 가로축만 격리한다.
|
|
124
|
+
이게 없으면 Row마다 콘텐츠 길이가 달라서 root 폭이 달라지고 셀 폭이 어긋남.
|
|
125
|
+
세로축은 격리 안 함 — min-height/stretch 정상 작동.
|
|
126
|
+
Header는 라벨이 짧아 전파돼도 min-width 하한에서 끊겨 눈에 안 띔. Row는 필수. */
|
|
127
|
+
.cellSlot {
|
|
128
|
+
display: flex;
|
|
129
|
+
flex-shrink: 0;
|
|
130
|
+
min-width: 0;
|
|
131
|
+
contain: inline-size;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* ── Pinned cellSlot ───────────────────────────────────
|
|
135
|
+
position: sticky는 inline style로 걸림. 배경만 깔아 스크롤되는 셀이 비치지
|
|
136
|
+
않게 함.
|
|
137
|
+
contain: inline-size 해제 — 가로 격리가 sticky anchor 계산을 깨 pinned
|
|
138
|
+
셀 경계가 실제 스크롤 위치와 안 맞는 원인. 세로 늘어남 격리는 유지 안 해도 OK. */
|
|
139
|
+
.cellSlot[data-pinned] {
|
|
140
|
+
background: var(--bg);
|
|
141
|
+
contain: none;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ── Cell ───────────────────────────────────────────────
|
|
145
|
+
Header의 .cell과 1:1 동일. slot 안쪽 width:100%로 가득 채우는 콘텐츠 그릇.
|
|
146
|
+
display:inline-flex 는 Header 그대로 따라감 — flex vs inline-flex 차이가
|
|
147
|
+
intrinsic 폭 계산에 미묘하게 영향할 수 있어 완전 동일하게 유지.
|
|
148
|
+
|
|
149
|
+
mask-image를 cell에 걸어 콘텐츠 overflow 끝단이 자연스럽게 fade.
|
|
150
|
+
cellInner가 아닌 cell에 걸어 ::selection 하이라이트는 자식(cellInner) 레벨에서
|
|
151
|
+
정상 렌더되도록. 색 하드코딩 없이 어떤 행 배경(hover/active/selected) 위에서도
|
|
152
|
+
띠 없이 작동.
|
|
153
|
+
fade 영역은 2단: 바깥 --space-2(=8px)는 완전 투명, 그 안쪽 --space-3(=12px)은
|
|
154
|
+
점진적 그라데이션. 합쳐서 20px 폭의 자연스러운 소실 — 좁은 fade가 "뚝 끊긴다"는
|
|
155
|
+
인상을 주는 걸 방지. */
|
|
156
|
+
.cell {
|
|
157
|
+
position: relative;
|
|
158
|
+
display: inline-flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
width: 100%;
|
|
161
|
+
min-width: 0;
|
|
162
|
+
padding-inline: var(--space-2);
|
|
163
|
+
height: 100%;
|
|
164
|
+
color: inherit;
|
|
165
|
+
overflow: hidden;
|
|
166
|
+
mask-image: linear-gradient(
|
|
167
|
+
to right,
|
|
168
|
+
#000 0,
|
|
169
|
+
#000 calc(100% - var(--space-2) - var(--space-3)),
|
|
170
|
+
transparent calc(100% - var(--space-2)),
|
|
171
|
+
transparent 100%
|
|
172
|
+
);
|
|
173
|
+
-webkit-mask-image: linear-gradient(
|
|
174
|
+
to right,
|
|
175
|
+
#000 0,
|
|
176
|
+
#000 calc(100% - var(--space-2) - var(--space-3)),
|
|
177
|
+
transparent calc(100% - var(--space-2)),
|
|
178
|
+
transparent 100%
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* cellInner — 실제 콘텐츠 래퍼. 한 줄 nowrap. mask는 부모 .cell에 걸려 있어
|
|
183
|
+
콘텐츠 끝단이 자연스럽게 fade. cellInner 자체엔 mask/overflow 조작 없음 —
|
|
184
|
+
::selection 하이라이트가 정상 동작하도록 유지. */
|
|
185
|
+
.cellInner {
|
|
186
|
+
display: flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
gap: var(--space-1);
|
|
189
|
+
width: 100%;
|
|
190
|
+
min-width: 0;
|
|
191
|
+
white-space: nowrap;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* 우측 정렬 셀 — 숫자가 왼쪽부터 사라지도록 cell mask 방향도 좌측으로 뒤집음. */
|
|
195
|
+
.cell[data-align="end"] .cellInner {
|
|
196
|
+
font-variant-numeric: tabular-nums;
|
|
197
|
+
justify-content: flex-end;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.cell[data-align="end"] {
|
|
201
|
+
mask-image: linear-gradient(
|
|
202
|
+
to left,
|
|
203
|
+
#000 0,
|
|
204
|
+
#000 calc(100% - var(--space-2) - var(--space-3)),
|
|
205
|
+
transparent calc(100% - var(--space-2)),
|
|
206
|
+
transparent 100%
|
|
207
|
+
);
|
|
208
|
+
-webkit-mask-image: linear-gradient(
|
|
209
|
+
to left,
|
|
210
|
+
#000 0,
|
|
211
|
+
#000 calc(100% - var(--space-2) - var(--space-3)),
|
|
212
|
+
transparent calc(100% - var(--space-2)),
|
|
213
|
+
transparent 100%
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* 가운데 정렬 — 양쪽 fade. */
|
|
218
|
+
.cell[data-align="center"] .cellInner {
|
|
219
|
+
justify-content: center;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.cell[data-align="center"] {
|
|
223
|
+
mask-image: linear-gradient(
|
|
224
|
+
to right,
|
|
225
|
+
transparent 0,
|
|
226
|
+
transparent var(--space-2),
|
|
227
|
+
#000 calc(var(--space-2) + var(--space-3)),
|
|
228
|
+
#000 calc(100% - var(--space-2) - var(--space-3)),
|
|
229
|
+
transparent calc(100% - var(--space-2)),
|
|
230
|
+
transparent 100%
|
|
231
|
+
);
|
|
232
|
+
-webkit-mask-image: linear-gradient(
|
|
233
|
+
to right,
|
|
234
|
+
transparent 0,
|
|
235
|
+
transparent var(--space-2),
|
|
236
|
+
#000 calc(var(--space-2) + var(--space-3)),
|
|
237
|
+
#000 calc(100% - var(--space-2) - var(--space-3)),
|
|
238
|
+
transparent calc(100% - var(--space-2)),
|
|
239
|
+
transparent 100%
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* ── Wrap mode ─────────────────────────────────────────
|
|
244
|
+
긴 셀 콘텐츠를 줄바꿈으로 전부 보여주는 모드.
|
|
245
|
+
- 행 min-height 해제: 콘텐츠 높이대로 자람.
|
|
246
|
+
- cell 상단 정렬 + 세로 padding + height:auto: 여러 줄 사이 리듬 유지.
|
|
247
|
+
- fade mask 제거: 전체 가시화가 목적.
|
|
248
|
+
- cellInner + 하위 요소의 white-space/min-width 전부 해제: TableCellText/Number 등
|
|
249
|
+
포맷터가 자체 nowrap을 들고 있어 universal selector로 뚫어야 함. descendant
|
|
250
|
+
selector는 .cellInner 하위에만 적용돼 selectCell(Checkbox) 영향 없음. */
|
|
251
|
+
.wrap {
|
|
252
|
+
min-height: 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.wrap + .wrap {
|
|
256
|
+
margin-top: 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.wrap .cell {
|
|
260
|
+
overflow: visible;
|
|
261
|
+
align-items: flex-start;
|
|
262
|
+
height: auto;
|
|
263
|
+
padding-block: var(--space-2);
|
|
264
|
+
mask-image: none;
|
|
265
|
+
-webkit-mask-image: none;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.wrap .cellInner {
|
|
269
|
+
white-space: normal;
|
|
270
|
+
overflow-wrap: anywhere;
|
|
271
|
+
align-items: flex-start;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.wrap .cellInner * {
|
|
275
|
+
white-space: normal;
|
|
276
|
+
overflow-wrap: anywhere;
|
|
277
|
+
min-width: 0;
|
|
278
|
+
max-width: 100%;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* ── Select cell (leading checkbox) ────────────────────
|
|
282
|
+
체크박스가 자체 크기를 가져 중앙 정렬만 하면 됨 — 좌우 패딩 불필요. */
|
|
283
|
+
.selectCell {
|
|
284
|
+
position: relative;
|
|
285
|
+
display: inline-flex;
|
|
286
|
+
align-items: center;
|
|
287
|
+
justify-content: flex-start;
|
|
288
|
+
flex-shrink: 0;
|
|
289
|
+
width: var(--control-h-sm);
|
|
290
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
forwardRef,
|
|
5
|
+
type AnchorHTMLAttributes,
|
|
6
|
+
type CSSProperties,
|
|
7
|
+
type HTMLAttributes,
|
|
8
|
+
type KeyboardEvent,
|
|
9
|
+
type MouseEvent,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react";
|
|
12
|
+
|
|
13
|
+
import { Checkbox } from "../Checkbox";
|
|
14
|
+
import type {
|
|
15
|
+
TableHeaderAlign,
|
|
16
|
+
TableHeaderColumn,
|
|
17
|
+
TableHeaderVariant,
|
|
18
|
+
} from "../TableHeader";
|
|
19
|
+
import styles from "./TableRow.module.css";
|
|
20
|
+
|
|
21
|
+
/* ────────────────────────────────────────────────────────────
|
|
22
|
+
Types
|
|
23
|
+
──────────────────────────────────────────────────────────── */
|
|
24
|
+
|
|
25
|
+
export type TableRowSize = "sm" | "md" | "lg";
|
|
26
|
+
|
|
27
|
+
/** Row가 columns 스펙 안에 선택적으로 심을 수 있는 "셀 렌더" 콜백.
|
|
28
|
+
* TableHeaderColumn은 label·width만 정의 — 데이터 렌더링 책임은 Row 쪽에서
|
|
29
|
+
* columns[i].render(data)로 확장. columns 참조를 공유하면 header와 row가 자동으로
|
|
30
|
+
* 정렬·폭이 맞는다. */
|
|
31
|
+
export type TableRowColumn<T = unknown> = TableHeaderColumn & {
|
|
32
|
+
/** 셀 렌더러. 미지정이면 data[key]를 그대로 노출. */
|
|
33
|
+
render?: (data: T) => ReactNode;
|
|
34
|
+
/** edit 모드 전용. false면 해당 셀은 활성화·세로선 대상에서 빠지고 read-only로 보임.
|
|
35
|
+
* 기본은 true — 생략하면 편집 가능 셀로 간주. navigate/select 모드에선 무시된다. */
|
|
36
|
+
editable?: boolean;
|
|
37
|
+
/** 사용자가 column 삭제 가능. TableHeader가 context menu / × 액션을 노출.
|
|
38
|
+
* 기본 false — 삭제는 호출 측 콜백으로 columns 배열 변경. */
|
|
39
|
+
removable?: boolean;
|
|
40
|
+
/* resizable / movable / pinned는 TableHeaderColumn에 정의되어 자동 상속. */
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type BaseProps<T> = {
|
|
44
|
+
variant?: TableHeaderVariant;
|
|
45
|
+
size?: TableRowSize;
|
|
46
|
+
columns: TableRowColumn<T>[];
|
|
47
|
+
|
|
48
|
+
/** 헤더와 공유하는 컬럼 순서. uncontrolled면 columns 원본 순서. */
|
|
49
|
+
order?: string[];
|
|
50
|
+
/** 헤더가 resize로 적용한 컬럼별 폭. */
|
|
51
|
+
widths?: Record<string, number>;
|
|
52
|
+
|
|
53
|
+
/** 이 행의 데이터. columns[].render(data) 또는 data[key]로 사용. */
|
|
54
|
+
data: T;
|
|
55
|
+
|
|
56
|
+
/** 맨 앞 체크박스 셀 — 헤더의 selectable과 짝. */
|
|
57
|
+
selectable?: boolean;
|
|
58
|
+
selected?: boolean;
|
|
59
|
+
onSelectedChange?: (next: boolean) => void;
|
|
60
|
+
|
|
61
|
+
/** navigate 모드에서 '선택된 행'처럼 보이게 하는 활성 상태 (현재 보고 있는 상세 등). */
|
|
62
|
+
active?: boolean;
|
|
63
|
+
|
|
64
|
+
/** 비활성 — 톤 다운 + 상호작용 차단. */
|
|
65
|
+
disabled?: boolean;
|
|
66
|
+
|
|
67
|
+
/** 셀 콘텐츠 줄바꿈 허용. 기본 false — 긴 텍스트는 한 줄 + 끝단 fade로 잘라냄.
|
|
68
|
+
* true면 white-space:normal 로 줄바꿈, 끝단 mask 제거, 행 높이가 콘텐츠 따라 자람.
|
|
69
|
+
* 보통 테이블 전체 Row에 동일 값을 넘김 — 열별 분기는 지원하지 않음(테이블 리듬 유지). */
|
|
70
|
+
wrap?: boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** 두 가지 "render as" 모드:
|
|
74
|
+
* - href 있으면 <a>로 렌더 (navigate 라우팅)
|
|
75
|
+
* - 아니면 <div role="button">로 렌더 (onClick 핸들러)
|
|
76
|
+
* 둘 다 안 주면 <div role="row"> (단순 디스플레이). */
|
|
77
|
+
export type TableRowAnchorProps<T> = BaseProps<T> &
|
|
78
|
+
Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof BaseProps<T> | "role"> & {
|
|
79
|
+
href: string;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type TableRowButtonProps<T> = BaseProps<T> &
|
|
83
|
+
Omit<HTMLAttributes<HTMLDivElement>, keyof BaseProps<T> | "role"> & {
|
|
84
|
+
href?: undefined;
|
|
85
|
+
onClick?: (e: MouseEvent<HTMLDivElement>) => void;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type TableRowProps<T = unknown> =
|
|
89
|
+
| TableRowAnchorProps<T>
|
|
90
|
+
| TableRowButtonProps<T>;
|
|
91
|
+
|
|
92
|
+
/* ────────────────────────────────────────────────────────────
|
|
93
|
+
Utilities
|
|
94
|
+
──────────────────────────────────────────────────────────── */
|
|
95
|
+
|
|
96
|
+
function toCssSize(v: number | string | undefined): string | undefined {
|
|
97
|
+
if (v === undefined) return undefined;
|
|
98
|
+
return typeof v === "number" ? `${v}px` : v;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function alignToJustify(align: TableHeaderAlign | undefined): CSSProperties["justifyContent"] {
|
|
102
|
+
return align === "end" ? "flex-end" : align === "center" ? "center" : "flex-start";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveOrder<T>(columns: TableRowColumn<T>[], order?: string[]): string[] {
|
|
106
|
+
if (!order) return columns.map((c) => c.key);
|
|
107
|
+
const known = new Set(columns.map((c) => c.key));
|
|
108
|
+
const filtered = order.filter((k) => known.has(k));
|
|
109
|
+
const missing = columns.map((c) => c.key).filter((k) => !filtered.includes(k));
|
|
110
|
+
return [...filtered, ...missing];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ────────────────────────────────────────────────────────────
|
|
114
|
+
TableRow
|
|
115
|
+
──────────────────────────────────────────────────────────── */
|
|
116
|
+
|
|
117
|
+
function TableRowInner<T>(
|
|
118
|
+
props: TableRowProps<T>,
|
|
119
|
+
ref: React.ForwardedRef<HTMLDivElement | HTMLAnchorElement>,
|
|
120
|
+
) {
|
|
121
|
+
const {
|
|
122
|
+
className,
|
|
123
|
+
variant = "line",
|
|
124
|
+
size = "sm",
|
|
125
|
+
columns,
|
|
126
|
+
order,
|
|
127
|
+
widths,
|
|
128
|
+
data,
|
|
129
|
+
selectable = false,
|
|
130
|
+
selected = false,
|
|
131
|
+
onSelectedChange,
|
|
132
|
+
active = false,
|
|
133
|
+
disabled = false,
|
|
134
|
+
wrap = false,
|
|
135
|
+
...rest
|
|
136
|
+
} = props as BaseProps<T> & {
|
|
137
|
+
className?: string;
|
|
138
|
+
href?: string;
|
|
139
|
+
onClick?: (e: MouseEvent<HTMLDivElement>) => void;
|
|
140
|
+
} & Record<string, unknown>;
|
|
141
|
+
|
|
142
|
+
const orderedKeys = resolveOrder(columns, order);
|
|
143
|
+
const columnMap = new Map(columns.map((c) => [c.key, c]));
|
|
144
|
+
|
|
145
|
+
const rootClass = [
|
|
146
|
+
styles.root,
|
|
147
|
+
variant === "plate" ? styles.plate : styles.line,
|
|
148
|
+
size === "sm" ? styles.sizeSm : size === "lg" ? styles.sizeLg : styles.sizeMd,
|
|
149
|
+
active ? styles.active : "",
|
|
150
|
+
disabled ? styles.disabled : "",
|
|
151
|
+
wrap ? styles.wrap : "",
|
|
152
|
+
className ?? "",
|
|
153
|
+
]
|
|
154
|
+
.filter(Boolean)
|
|
155
|
+
.join(" ");
|
|
156
|
+
|
|
157
|
+
const isInteractive = !disabled && (!!(props as TableRowAnchorProps<T>).href || !!(props as TableRowButtonProps<T>).onClick);
|
|
158
|
+
|
|
159
|
+
/* 체크박스 셀 자체 클릭이 행 클릭으로 전파되지 않도록 stop. */
|
|
160
|
+
const stop = (e: MouseEvent) => e.stopPropagation();
|
|
161
|
+
|
|
162
|
+
const cells = (
|
|
163
|
+
<>
|
|
164
|
+
{selectable && (
|
|
165
|
+
<div className={styles.selectCell} onClick={stop}>
|
|
166
|
+
<Checkbox
|
|
167
|
+
checked={selected}
|
|
168
|
+
disabled={disabled}
|
|
169
|
+
onChange={(e) => onSelectedChange?.(e.currentTarget.checked)}
|
|
170
|
+
aria-label="행 선택"
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{orderedKeys.map((key) => {
|
|
176
|
+
const col = columnMap.get(key);
|
|
177
|
+
if (!col) return null;
|
|
178
|
+
const cssWidth = widths?.[col.key] !== undefined ? `${widths[col.key]}px` : toCssSize(col.width);
|
|
179
|
+
/* Header와 완전히 동일한 2단계 구조 + 동일한 inline 공식.
|
|
180
|
+
cellSlot: flex item, 폭 결정. cell: slot 안쪽 width:100%로 콘텐츠 그릇. */
|
|
181
|
+
const slotStyle: CSSProperties = {
|
|
182
|
+
width: cssWidth,
|
|
183
|
+
minWidth: toCssSize(col.minWidth) ?? cssWidth,
|
|
184
|
+
flex: col.flex ? "1 1 0" : undefined,
|
|
185
|
+
justifyContent: alignToJustify(col.align),
|
|
186
|
+
};
|
|
187
|
+
const content = col.render ? col.render(data) : (data as Record<string, ReactNode>)?.[col.key];
|
|
188
|
+
return (
|
|
189
|
+
<div key={col.key} className={styles.cellSlot} style={slotStyle}>
|
|
190
|
+
<div role="cell" className={styles.cell} data-align={col.align ?? "start"}>
|
|
191
|
+
<span className={styles.cellInner}>{content}</span>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
})}
|
|
196
|
+
</>
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
/* ── 라우팅: href 있으면 anchor, onClick 있으면 div[role=button], 둘 다 없으면 div[role=row] ── */
|
|
200
|
+
if (!disabled && "href" in props && props.href) {
|
|
201
|
+
const { href, onClick, ...anchorRest } = rest as AnchorHTMLAttributes<HTMLAnchorElement> & { href: string };
|
|
202
|
+
return (
|
|
203
|
+
<a
|
|
204
|
+
ref={ref as React.ForwardedRef<HTMLAnchorElement>}
|
|
205
|
+
className={rootClass}
|
|
206
|
+
href={href}
|
|
207
|
+
role="row"
|
|
208
|
+
aria-selected={active || undefined}
|
|
209
|
+
aria-disabled={disabled || undefined}
|
|
210
|
+
onClick={onClick}
|
|
211
|
+
{...anchorRest}
|
|
212
|
+
>
|
|
213
|
+
{cells}
|
|
214
|
+
</a>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const divRest = rest as HTMLAttributes<HTMLDivElement>;
|
|
219
|
+
const { onClick, onKeyDown, tabIndex, ...divRestClean } = divRest;
|
|
220
|
+
|
|
221
|
+
/* div[role=button] 분기에서 키보드 활성화 — Enter/Space로 클릭 트리거. */
|
|
222
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
223
|
+
onKeyDown?.(e);
|
|
224
|
+
if (!isInteractive || !onClick) return;
|
|
225
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
226
|
+
e.preventDefault();
|
|
227
|
+
onClick(e as unknown as MouseEvent<HTMLDivElement>);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<div
|
|
233
|
+
ref={ref as React.ForwardedRef<HTMLDivElement>}
|
|
234
|
+
className={rootClass}
|
|
235
|
+
role="row"
|
|
236
|
+
aria-selected={active || undefined}
|
|
237
|
+
aria-disabled={disabled || undefined}
|
|
238
|
+
tabIndex={isInteractive ? (tabIndex ?? 0) : tabIndex}
|
|
239
|
+
onClick={isInteractive ? onClick : undefined}
|
|
240
|
+
onKeyDown={isInteractive ? handleKeyDown : onKeyDown}
|
|
241
|
+
{...divRestClean}
|
|
242
|
+
>
|
|
243
|
+
{cells}
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* generic + forwardRef 조합 — 캐스팅으로 generic 보존. */
|
|
249
|
+
const TableRow = forwardRef(TableRowInner) as <T>(
|
|
250
|
+
props: TableRowProps<T> & { ref?: React.ForwardedRef<HTMLDivElement | HTMLAnchorElement> },
|
|
251
|
+
) => ReturnType<typeof TableRowInner>;
|
|
252
|
+
|
|
253
|
+
export { TableRow };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/* TableRowEdit
|
|
2
|
+
───────────────────────────────────────────────────────────────
|
|
3
|
+
edit 모드 전용 CSS. 레이아웃은 TableRow.module.css를 부분 상속하되
|
|
4
|
+
.line 클래스는 쓰지 않고 edit 전용 외곽을 여기서 그린다.
|
|
5
|
+
|
|
6
|
+
원칙
|
|
7
|
+
- 세로 구분선 강제: 엑셀/Notion 톤의 "셀 그리드" 신호.
|
|
8
|
+
- 셀 자체가 입력 영역: 활성 셀 안에 editor가 바로 들어감.
|
|
9
|
+
- selected 행은 primary-subtle.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/* ── Row 외곽 ─────────────────────────────────────────────
|
|
13
|
+
TableRow .line 클래스를 쓰지 않는 이유: .line .cell { padding-inline: 0 }
|
|
14
|
+
cascade가 edit 모드 셀 좌우 패딩을 지워버림. 하단 hairline과 column-gap은
|
|
15
|
+
여기서 직접. */
|
|
16
|
+
.edit {
|
|
17
|
+
cursor: default;
|
|
18
|
+
border-bottom: var(--hairline) solid var(--border);
|
|
19
|
+
/* TableHeader(.line)와 동일한 column-gap. gap이 다르면 각 셀 폭이 오른쪽으로
|
|
20
|
+
갈수록 어긋나 헤더 resize handle 위치와 데이터 Row 세로선이 안 맞음. */
|
|
21
|
+
column-gap: var(--space-2);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* ── Size ─────────────────────────────────────────────────
|
|
25
|
+
edit 전용 2단계. 스프레드시트 톤이라 조밀한 2단만.
|
|
26
|
+
sm=36(control-h-md), md=40(control-h-lg). TableRow sizeMd(48)보다 작다. */
|
|
27
|
+
.sizeSm { min-height: var(--control-h-md); }
|
|
28
|
+
.sizeMd { min-height: var(--control-h-lg); }
|
|
29
|
+
|
|
30
|
+
.edit:last-child {
|
|
31
|
+
border-bottom: none;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ── 공통 셀 (edit/readonly) ──────────────────────────────
|
|
35
|
+
TableRow .cell의 overflow: hidden / mask-image / padding-inline:0 를 전부
|
|
36
|
+
edit 모드용으로 재설정. mask는 흰 그라데이션 노이즈, padding 0은 글자가
|
|
37
|
+
세로선에 붙어 보이는 원인. */
|
|
38
|
+
.editCell,
|
|
39
|
+
.readonlyCell {
|
|
40
|
+
padding-inline: var(--space-2);
|
|
41
|
+
mask-image: none;
|
|
42
|
+
-webkit-mask-image: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ── 세로 구분선 ────────────────────────────────────────── */
|
|
46
|
+
.editCell[data-divider]::before,
|
|
47
|
+
.readonlyCell[data-divider]::before {
|
|
48
|
+
content: "";
|
|
49
|
+
position: absolute;
|
|
50
|
+
inset-block: 0;
|
|
51
|
+
inset-inline-start: 0;
|
|
52
|
+
width: var(--hairline);
|
|
53
|
+
background: var(--border);
|
|
54
|
+
pointer-events: none;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ── Edit cell ────────────────────────────────────────────
|
|
58
|
+
클릭/포커스 타겟. 활성 상태엔 input/select가 셀을 꽉 채움. */
|
|
59
|
+
.editCell {
|
|
60
|
+
position: relative;
|
|
61
|
+
cursor: text;
|
|
62
|
+
transition: var(--transition-colors);
|
|
63
|
+
outline: none;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
.edit .editCell:hover {
|
|
68
|
+
background: var(--hover);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* ── Read-only cell ────────────────────────────────────── */
|
|
72
|
+
.readonlyCell {
|
|
73
|
+
cursor: default;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ── Active cell ─────────────────────────────────────────
|
|
77
|
+
active 상태 셀 자체는 톤 변화 없이 그대로. overflow visible로 editor의
|
|
78
|
+
shadow·multiline 확장이 셀 밖으로 번지도록. z-raised로 세로선/다른 행
|
|
79
|
+
위에 올라오게 — 헤더(z-sticky)보다는 아래라 아래 방향 확장만 안전. */
|
|
80
|
+
.cellActive {
|
|
81
|
+
overflow: visible;
|
|
82
|
+
z-index: var(--z-raised);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ── Floating editor host ────────────────────────────────
|
|
86
|
+
active 셀 위에 떠있는 editor 컨테이너. 톤 변화는 border/shadow만 —
|
|
87
|
+
포인트 컬러 없이 중성 hairline으로. 셀 bounding box에 정확히 얹혀
|
|
88
|
+
폭·좌우 origin이 셀과 완전 일치해야 헤더나 옆 셀 아래로 안 들어감.
|
|
89
|
+
top만 아래로 확장되도록 inset-block-end: auto. */
|
|
90
|
+
.editorHost {
|
|
91
|
+
position: absolute;
|
|
92
|
+
top: 0;
|
|
93
|
+
left: 0;
|
|
94
|
+
right: 0;
|
|
95
|
+
/* 1줄 상태엔 셀 높이에 딱 맞도록 min-height: 100%. textarea autosize가
|
|
96
|
+
여러 줄로 늘어나면 host는 content 높이를 따라감(아래로 확장). */
|
|
97
|
+
min-height: 100%;
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
padding-inline: var(--space-2);
|
|
101
|
+
background: var(--bg);
|
|
102
|
+
border: var(--hairline) solid var(--border-strong);
|
|
103
|
+
border-radius: var(--radius-sm);
|
|
104
|
+
box-shadow: var(--shadow-md);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* editor(input/select)는 popover 폭을 꽉 채움. */
|
|
108
|
+
.editorHost > * {
|
|
109
|
+
width: 100%;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* ── 키보드 포커스 (활성 전) ───────────────────────────── */
|
|
113
|
+
.editCell:focus-visible:not([data-active]) {
|
|
114
|
+
box-shadow: var(--ring-shadow);
|
|
115
|
+
border-radius: var(--radius-sm);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* ── Selected 행 ───────────────────────────────────────── */
|
|
119
|
+
.selected {
|
|
120
|
+
background: var(--primary-subtle);
|
|
121
|
+
color: var(--selected-fg);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.selected:hover {
|
|
125
|
+
background: var(--primary-subtle) !important;
|
|
126
|
+
}
|