@conscience-technology/ctds 0.0.0-stage → 0.1.1

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 (289) hide show
  1. package/README.md +128 -2
  2. package/assets/components/AppFrame/AppFrame.module.css +177 -0
  3. package/assets/components/AppFrame/AppFrame.tsx +93 -0
  4. package/assets/components/AppFrame/index.ts +6 -0
  5. package/assets/components/AttachmentChip/AttachmentChip.module.css +71 -0
  6. package/assets/components/AttachmentChip/AttachmentChip.tsx +74 -0
  7. package/assets/components/AttachmentChip/index.ts +2 -0
  8. package/assets/components/AttachmentThumb/AttachmentThumb.module.css +107 -0
  9. package/assets/components/AttachmentThumb/AttachmentThumb.tsx +91 -0
  10. package/assets/components/AttachmentThumb/index.ts +2 -0
  11. package/assets/components/Avatar/Avatar.module.css +167 -0
  12. package/assets/components/Avatar/Avatar.tsx +228 -0
  13. package/assets/components/Avatar/index.ts +2 -0
  14. package/assets/components/AvatarUpload/AvatarUpload.module.css +130 -0
  15. package/assets/components/AvatarUpload/AvatarUpload.tsx +139 -0
  16. package/assets/components/AvatarUpload/index.ts +2 -0
  17. package/assets/components/Badge/Badge.module.css +76 -0
  18. package/assets/components/Badge/Badge.tsx +68 -0
  19. package/assets/components/Badge/index.ts +2 -0
  20. package/assets/components/Breadcrumb/Breadcrumb.module.css +213 -0
  21. package/assets/components/Breadcrumb/Breadcrumb.tsx +279 -0
  22. package/assets/components/Breadcrumb/index.ts +8 -0
  23. package/assets/components/Button/Button.module.css +216 -0
  24. package/assets/components/Button/Button.tsx +149 -0
  25. package/assets/components/Button/index.ts +2 -0
  26. package/assets/components/Callout/Callout.module.css +146 -0
  27. package/assets/components/Callout/Callout.tsx +138 -0
  28. package/assets/components/Callout/index.ts +2 -0
  29. package/assets/components/ChatInput/ChatInput.module.css +254 -0
  30. package/assets/components/ChatInput/ChatInput.tsx +322 -0
  31. package/assets/components/ChatInput/ModelSelector.tsx +40 -0
  32. package/assets/components/ChatInput/SendButton.tsx +41 -0
  33. package/assets/components/ChatInput/index.ts +10 -0
  34. package/assets/components/Checkbox/Checkbox.module.css +66 -0
  35. package/assets/components/Checkbox/Checkbox.tsx +36 -0
  36. package/assets/components/Checkbox/index.ts +2 -0
  37. package/assets/components/Chip/Chip.module.css +128 -0
  38. package/assets/components/Chip/Chip.tsx +103 -0
  39. package/assets/components/Chip/index.ts +2 -0
  40. package/assets/components/ChoiceCard/ChoiceCard.module.css +142 -0
  41. package/assets/components/ChoiceCard/ChoiceCard.tsx +132 -0
  42. package/assets/components/ChoiceCard/index.ts +2 -0
  43. package/assets/components/ContextMenu/ContextMenu.module.css +102 -0
  44. package/assets/components/ContextMenu/ContextMenu.tsx +392 -0
  45. package/assets/components/ContextMenu/index.ts +9 -0
  46. package/assets/components/DatePicker/Calendar.tsx +234 -0
  47. package/assets/components/DatePicker/DESIGN_NOTES.md +106 -0
  48. package/assets/components/DatePicker/DESIGN_NOTES_TIME.md +125 -0
  49. package/assets/components/DatePicker/DatePicker.module.css +295 -0
  50. package/assets/components/DatePicker/DatePicker.tsx +387 -0
  51. package/assets/components/DatePicker/TimeSelector.tsx +69 -0
  52. package/assets/components/DatePicker/index.ts +8 -0
  53. package/assets/components/DatePicker/presets.ts +80 -0
  54. package/assets/components/DatePicker/utils.ts +148 -0
  55. package/assets/components/Disclosure/Disclosure.module.css +73 -0
  56. package/assets/components/Disclosure/Disclosure.tsx +129 -0
  57. package/assets/components/Disclosure/index.ts +6 -0
  58. package/assets/components/Drawer/Drawer.module.css +257 -0
  59. package/assets/components/Drawer/Drawer.tsx +126 -0
  60. package/assets/components/Drawer/index.ts +7 -0
  61. package/assets/components/EmptyState/EmptyState.module.css +102 -0
  62. package/assets/components/EmptyState/EmptyState.tsx +80 -0
  63. package/assets/components/EmptyState/index.ts +2 -0
  64. package/assets/components/FileIcon/FileIcon.module.css +66 -0
  65. package/assets/components/FileIcon/FileIcon.tsx +84 -0
  66. package/assets/components/FileIcon/fileExtMap.ts +112 -0
  67. package/assets/components/FileIcon/index.ts +9 -0
  68. package/assets/components/FilterButton/FilterButton.module.css +130 -0
  69. package/assets/components/FilterButton/FilterButton.tsx +94 -0
  70. package/assets/components/FilterButton/index.ts +2 -0
  71. package/assets/components/GalleryItem/GalleryItem.module.css +220 -0
  72. package/assets/components/GalleryItem/GalleryItem.tsx +163 -0
  73. package/assets/components/GalleryItem/index.ts +7 -0
  74. package/assets/components/Icon/Icon.tsx +616 -0
  75. package/assets/components/Icon/context.tsx +39 -0
  76. package/assets/components/Icon/index.ts +4 -0
  77. package/assets/components/IconButton/IconButton.module.css +109 -0
  78. package/assets/components/IconButton/IconButton.tsx +110 -0
  79. package/assets/components/IconButton/index.ts +2 -0
  80. package/assets/components/Input/Input.module.css +338 -0
  81. package/assets/components/Input/Input.tsx +371 -0
  82. package/assets/components/Input/index.ts +2 -0
  83. package/assets/components/ListItem/ListItem.module.css +318 -0
  84. package/assets/components/ListItem/ListItem.tsx +262 -0
  85. package/assets/components/ListItem/index.ts +9 -0
  86. package/assets/components/MenuItem/MenuItem.module.css +437 -0
  87. package/assets/components/MenuItem/MenuItem.tsx +529 -0
  88. package/assets/components/MenuItem/index.ts +8 -0
  89. package/assets/components/MetricCard/MetricCard.module.css +153 -0
  90. package/assets/components/MetricCard/MetricCard.tsx +140 -0
  91. package/assets/components/MetricCard/index.ts +2 -0
  92. package/assets/components/Modal/Modal.module.css +175 -0
  93. package/assets/components/Modal/Modal.tsx +98 -0
  94. package/assets/components/Modal/index.ts +2 -0
  95. package/assets/components/Onboarding/DocsIndex.tsx +82 -0
  96. package/assets/components/Onboarding/LayoutBuilder.tsx +160 -0
  97. package/assets/components/Onboarding/Onboarding.module.css +1394 -0
  98. package/assets/components/Onboarding/OnboardingAxisPanel.tsx +515 -0
  99. package/assets/components/Onboarding/OnboardingPage.tsx +1193 -0
  100. package/assets/components/Onboarding/PresetsGallery.tsx +65 -0
  101. package/assets/components/Onboarding/actions.ts +250 -0
  102. package/assets/components/Onboarding/index.ts +8 -0
  103. package/assets/components/Onboarding/palette.css +40 -0
  104. package/assets/components/Onboarding/wizardData.ts +265 -0
  105. package/assets/components/Pagination/Pagination.module.css +82 -0
  106. package/assets/components/Pagination/Pagination.tsx +248 -0
  107. package/assets/components/Pagination/index.ts +6 -0
  108. package/assets/components/ProgressBar/ProgressBar.module.css +64 -0
  109. package/assets/components/ProgressBar/ProgressBar.tsx +54 -0
  110. package/assets/components/ProgressBar/index.ts +2 -0
  111. package/assets/components/QuotationBar/QuotationBar.module.css +61 -0
  112. package/assets/components/QuotationBar/QuotationBar.tsx +48 -0
  113. package/assets/components/QuotationBar/index.ts +2 -0
  114. package/assets/components/Radio/Radio.module.css +63 -0
  115. package/assets/components/Radio/Radio.tsx +33 -0
  116. package/assets/components/Radio/index.ts +2 -0
  117. package/assets/components/Select/Select.module.css +151 -0
  118. package/assets/components/Select/Select.tsx +425 -0
  119. package/assets/components/Select/index.ts +10 -0
  120. package/assets/components/SelectableCard/SelectableCard.module.css +177 -0
  121. package/assets/components/SelectableCard/SelectableCard.tsx +83 -0
  122. package/assets/components/SelectableCard/index.ts +2 -0
  123. package/assets/components/Shortcut/Shortcut.module.css +56 -0
  124. package/assets/components/Shortcut/Shortcut.tsx +156 -0
  125. package/assets/components/Shortcut/index.ts +2 -0
  126. package/assets/components/Sidebar/Sidebar.module.css +242 -0
  127. package/assets/components/Sidebar/Sidebar.tsx +264 -0
  128. package/assets/components/Sidebar/index.ts +7 -0
  129. package/assets/components/SidebarSplit/SidebarSplit.module.css +126 -0
  130. package/assets/components/SidebarSplit/SidebarSplit.tsx +222 -0
  131. package/assets/components/SidebarSplit/index.ts +6 -0
  132. package/assets/components/Slider/Slider.module.css +120 -0
  133. package/assets/components/Slider/Slider.tsx +88 -0
  134. package/assets/components/Slider/index.ts +2 -0
  135. package/assets/components/SplitButton/SplitButton.module.css +190 -0
  136. package/assets/components/SplitButton/SplitButton.tsx +145 -0
  137. package/assets/components/SplitButton/index.ts +6 -0
  138. package/assets/components/Stepper/Stepper.module.css +182 -0
  139. package/assets/components/Stepper/Stepper.tsx +185 -0
  140. package/assets/components/Stepper/index.ts +2 -0
  141. package/assets/components/Switch/Switch.module.css +116 -0
  142. package/assets/components/Switch/Switch.tsx +58 -0
  143. package/assets/components/Switch/index.ts +2 -0
  144. package/assets/components/TableCell/TableCell.module.css +137 -0
  145. package/assets/components/TableCell/TableCellAvatar.tsx +42 -0
  146. package/assets/components/TableCell/TableCellBadge.tsx +23 -0
  147. package/assets/components/TableCell/TableCellIcon.tsx +39 -0
  148. package/assets/components/TableCell/TableCellImage.tsx +49 -0
  149. package/assets/components/TableCell/TableCellLink.tsx +33 -0
  150. package/assets/components/TableCell/TableCellMeter.tsx +55 -0
  151. package/assets/components/TableCell/TableCellNumber.tsx +43 -0
  152. package/assets/components/TableCell/TableCellText.tsx +30 -0
  153. package/assets/components/TableCell/index.ts +23 -0
  154. package/assets/components/TableHeader/TableHeader.module.css +287 -0
  155. package/assets/components/TableHeader/TableHeader.tsx +590 -0
  156. package/assets/components/TableHeader/index.ts +9 -0
  157. package/assets/components/TableRow/TableRow.module.css +290 -0
  158. package/assets/components/TableRow/TableRow.tsx +253 -0
  159. package/assets/components/TableRow/index.ts +8 -0
  160. package/assets/components/TableRowEdit/TableRowEdit.module.css +126 -0
  161. package/assets/components/TableRowEdit/TableRowEdit.tsx +247 -0
  162. package/assets/components/TableRowEdit/index.ts +2 -0
  163. package/assets/components/TableRowSelect/TableRowSelect.module.css +23 -0
  164. package/assets/components/TableRowSelect/TableRowSelect.tsx +175 -0
  165. package/assets/components/TableRowSelect/index.ts +2 -0
  166. package/assets/components/Tabs/Tabs.module.css +234 -0
  167. package/assets/components/Tabs/Tabs.tsx +426 -0
  168. package/assets/components/Tabs/index.ts +9 -0
  169. package/assets/components/Textarea/Textarea.module.css +318 -0
  170. package/assets/components/Textarea/Textarea.tsx +426 -0
  171. package/assets/components/Textarea/index.ts +7 -0
  172. package/assets/components/ThemeController.tsx +271 -0
  173. package/assets/components/TimePicker/TimePicker.module.css +317 -0
  174. package/assets/components/TimePicker/TimePicker.tsx +821 -0
  175. package/assets/components/TimePicker/index.ts +8 -0
  176. package/assets/components/TimePicker/utils.ts +115 -0
  177. package/assets/components/Toast/Toast.module.css +264 -0
  178. package/assets/components/Toast/Toast.tsx +243 -0
  179. package/assets/components/Toast/index.ts +15 -0
  180. package/assets/components/Toolbar/Toolbar.module.css +72 -0
  181. package/assets/components/Toolbar/Toolbar.tsx +75 -0
  182. package/assets/components/Toolbar/index.ts +2 -0
  183. package/assets/components/Tooltip/Tooltip.module.css +42 -0
  184. package/assets/components/Tooltip/Tooltip.tsx +332 -0
  185. package/assets/components/Tooltip/index.ts +2 -0
  186. package/assets/components/Topbar/Topbar.module.css +26 -0
  187. package/assets/components/Topbar/Topbar.tsx +47 -0
  188. package/assets/components/Topbar/index.ts +2 -0
  189. package/assets/components/_cn.ts +3 -0
  190. package/assets/components/_color.ts +48 -0
  191. package/assets/components/_data.ts +659 -0
  192. package/assets/components/_utils.css +19 -0
  193. package/assets/components/axisOptions.ts +24 -0
  194. package/assets/components/tokens.css +742 -0
  195. package/assets/prompts/GUIDE.md +251 -0
  196. package/assets/prompts/OVERRIDE.md +170 -0
  197. package/assets/prompts/PRINCIPLES.md +130 -0
  198. package/assets/prompts/axes/density.md +38 -0
  199. package/assets/prompts/axes/font.md +36 -0
  200. package/assets/prompts/axes/hue.md +36 -0
  201. package/assets/prompts/axes/icon.md +48 -0
  202. package/assets/prompts/axes/mode.md +50 -0
  203. package/assets/prompts/axes/motion.md +77 -0
  204. package/assets/prompts/axes/palette.md +35 -0
  205. package/assets/prompts/axes/shape.md +33 -0
  206. package/assets/prompts/axes/voice.md +54 -0
  207. package/assets/prompts/components/README.md +84 -0
  208. package/assets/prompts/components/app-frame.md +94 -0
  209. package/assets/prompts/components/attachment-chip.md +46 -0
  210. package/assets/prompts/components/attachment-thumb.md +49 -0
  211. package/assets/prompts/components/avatar.md +86 -0
  212. package/assets/prompts/components/badge.md +104 -0
  213. package/assets/prompts/components/breadcrumb.md +77 -0
  214. package/assets/prompts/components/button.md +99 -0
  215. package/assets/prompts/components/callout.md +81 -0
  216. package/assets/prompts/components/chat-input.md +84 -0
  217. package/assets/prompts/components/checkbox.md +46 -0
  218. package/assets/prompts/components/chip.md +84 -0
  219. package/assets/prompts/components/choice-card.md +127 -0
  220. package/assets/prompts/components/context-menu.md +145 -0
  221. package/assets/prompts/components/date-picker.md +104 -0
  222. package/assets/prompts/components/drawer.md +152 -0
  223. package/assets/prompts/components/empty-state.md +69 -0
  224. package/assets/prompts/components/file-icon.md +73 -0
  225. package/assets/prompts/components/filter-button.md +42 -0
  226. package/assets/prompts/components/form-field.md +74 -0
  227. package/assets/prompts/components/gallery-item.md +158 -0
  228. package/assets/prompts/components/icon-button.md +82 -0
  229. package/assets/prompts/components/icon.md +68 -0
  230. package/assets/prompts/components/input.md +95 -0
  231. package/assets/prompts/components/list-item.md +235 -0
  232. package/assets/prompts/components/menu-item.md +234 -0
  233. package/assets/prompts/components/metric-card.md +132 -0
  234. package/assets/prompts/components/modal.md +131 -0
  235. package/assets/prompts/components/pagination.md +92 -0
  236. package/assets/prompts/components/progress-bar.md +61 -0
  237. package/assets/prompts/components/quotation-bar.md +39 -0
  238. package/assets/prompts/components/radio.md +41 -0
  239. package/assets/prompts/components/select.md +104 -0
  240. package/assets/prompts/components/selectable-card.md +67 -0
  241. package/assets/prompts/components/shortcut.md +76 -0
  242. package/assets/prompts/components/sidebar.md +107 -0
  243. package/assets/prompts/components/slider.md +89 -0
  244. package/assets/prompts/components/split-button.md +71 -0
  245. package/assets/prompts/components/stepper.md +93 -0
  246. package/assets/prompts/components/switch.md +86 -0
  247. package/assets/prompts/components/table-cell.md +87 -0
  248. package/assets/prompts/components/table-header.md +157 -0
  249. package/assets/prompts/components/table-row-edit.md +129 -0
  250. package/assets/prompts/components/table-row-select.md +82 -0
  251. package/assets/prompts/components/table-row.md +156 -0
  252. package/assets/prompts/components/tabs.md +122 -0
  253. package/assets/prompts/components/textarea.md +105 -0
  254. package/assets/prompts/components/toast.md +149 -0
  255. package/assets/prompts/components/tooltip.md +71 -0
  256. package/assets/prompts/components/topbar.md +148 -0
  257. package/assets/prompts/primitives/emerge.md +43 -0
  258. package/assets/prompts/primitives/fade.md +31 -0
  259. package/assets/prompts/primitives/scale.md +32 -0
  260. package/assets/prompts/primitives/scroll.md +75 -0
  261. package/assets/prompts/primitives/slide.md +31 -0
  262. package/assets/prompts/themes/_SCHEMA.md +135 -0
  263. package/assets/prompts/themes/_TEMPLATE.md +73 -0
  264. package/assets/prompts/themes/expressive.md +118 -0
  265. package/assets/prompts/themes/pure.md +87 -0
  266. package/assets/prompts/themes/spare.md +73 -0
  267. package/assets/prompts/tokens/breakpoint.md +29 -0
  268. package/assets/prompts/tokens/color.md +78 -0
  269. package/assets/prompts/tokens/radius.md +33 -0
  270. package/assets/prompts/tokens/shadow.md +35 -0
  271. package/assets/prompts/tokens/spacing.md +86 -0
  272. package/assets/prompts/tokens/typography.md +76 -0
  273. package/assets/prompts/tokens/z-index.md +35 -0
  274. package/assets/public/expressive.png +0 -0
  275. package/assets/public/pure.png +0 -0
  276. package/assets/public/spare.png +0 -0
  277. package/assets/skill/SKILL.md +36 -0
  278. package/bin/ctds.js +76 -0
  279. package/package.json +35 -4
  280. package/src/commands/init.js +215 -0
  281. package/src/commands/update.js +275 -0
  282. package/src/utils/assets.js +78 -0
  283. package/src/utils/deps.js +51 -0
  284. package/src/utils/detect.js +47 -0
  285. package/src/utils/diff.js +52 -0
  286. package/src/utils/hash.js +16 -0
  287. package/src/utils/log.js +41 -0
  288. package/src/utils/meta.js +73 -0
  289. package/src/utils/prompt.js +28 -0
@@ -0,0 +1,157 @@
1
+ # TableHeader
2
+
3
+ ### 원칙
4
+ 데이터 그리드 상단의 columnheader 행. 시각 스타일(`line`/`plate`) × 두 기능(`resizable`/`reorderable`) 각각 on/off = 총 8조합을 하나의 컴포넌트로 담는다. 데이터 로딩·정렬 로직·행 렌더는 상위 Table이 책임지고, 이 컴포넌트는 columnheader 시각·상호작용만 담당한다.
5
+
6
+ `reorderable=on`이면 각 셀이 hover 배경 + `cursor: grab`으로 반응하고, `off`면 hover 반응이 사라진다. reorder는 @dnd-kit/sortable로 처리해 키보드/터치도 지원한다.
7
+
8
+ ### Do
9
+ - **variant 선택 기준**: 페이지 표면과 자연스럽게 이어지는 얇은 헤더는 `line`, 카드 안에서 위치가 명확해야 하는 헤더는 `plate`. `line`은 sticky 하단 hairline으로 흐름을 만들고, `plate`는 라운드 컨테이너로 시선을 가둔다.
10
+ - **정렬은 셀별 opt-in**: 컬럼마다 `sortable: true`로 개별 활성화. 정렬 인디케이터는 2-state (`asc` ↔ `desc`) — 라벨 옆 caret이 방향 표시.
11
+ - **컨트롤드 3종은 세트로**: `order`/`widths`/`sort` 중 하나라도 controlled로 넘기면 대응 `on*Change`도 반드시 함께. uncontrolled 사용도 지원(각 상태 내부 보관).
12
+ - **width 최소 하한**: 사용자가 resize로 아무리 줄여도 48px 밑으로는 안 내려간다(컴포넌트가 강제). 컬럼 라벨이 잘리기 시작하는 지점의 안전선.
13
+ - **checkbox 셀은 `selectable` prop**: 셀 순서 재정렬 대상에서 자동 제외되어 항상 맨 앞 고정.
14
+ - **컬럼 매니저 버튼은 `columnManager` prop**: 우측 끝 고정 IconButton (`Icon name="columns"`). show/hide 다이얼로그 트리거용.
15
+ - **sortable + reorderable 공존**: 라벨 클릭(sort)과 드래그(reorder)는 8px pointer 이동 threshold로 자동 분기 — 짧게 누르면 sort, 끌면 reorder.
16
+
17
+ ### Don't
18
+ - 이 컴포넌트 안에서 데이터를 정렬하지 않는다. `onSortChange`만 발화하고 정렬은 상위 Table의 책임.
19
+ - variant 시각을 `background`/`border` 직접 지정으로 커스터마이즈하지 않는다 — Palette·Mode 축 스왑이 깨진다. 색이 필요하면 축·프리셋에 정의.
20
+ - resize handle 폭을 CSS로 좁히지 않는다 — 8px 히트 영역이 접근성 최소치.
21
+ - 셀 안에 두 개 이상의 인터랙션 요소(라벨 정렬 + 아이콘 필터 등)를 넣지 않는다. 셀 = 하나의 columnheader.
22
+ - `reorderable=off`에서 hover 배경을 억지로 살리지 않는다 — "이동 불가한데 반응만 있으면" 의미 없는 시각 노이즈.
23
+ - 3-state sort(asc→desc→none)로 확장하지 않는다. 정렬 해제는 "다른 컬럼 클릭"으로 대체 — 클릭당 결과가 명확해야 함.
24
+
25
+ ### Fallback
26
+ - variant 애매하면 `line`. 페이지에 자연스럽게 얹히는 기본 형태.
27
+ - resizable/reorderable 애매하면 둘 다 `off`. 나중에 필요하면 켜는 게 안전.
28
+ - sort 초기값 애매하면 `null` (미정렬) — 다만 이후엔 2-state로 고정.
29
+
30
+ ### 관련
31
+ - [[components/checkbox]] — `selectable` 셀에서 사용
32
+ - [[components/icon-button]] — `columnManager` 버튼
33
+ - [[components/icon]] — sort caret / column manager icon
34
+ - [[axes/density]] — Density=`compact`일 때 헤더 높이와 셀 padding이 자동으로 좁아짐
35
+ - [[axes/shape]] — `plate` variant의 컨테이너 radius가 Shape 축에 반응
36
+ - [[tokens/color]] — `--hover`(reorderable hover) / `--border`(line tick / hairline) / `--fg-subtle`→`--fg-strong`(sort icon)
37
+
38
+ ### Props 시그니처
39
+ ```tsx
40
+ type TableHeaderColumn = {
41
+ key: string; // 안정적 id (reorder/width 상태 키)
42
+ label: ReactNode;
43
+ width?: number | string; // px number 또는 CSS 값
44
+ minWidth?: number | string;
45
+ flex?: boolean; // 남은 공간 차지
46
+ sortable?: boolean; // 라벨 옆 sort caret
47
+ align?: "start" | "center" | "end";
48
+ resizable?: boolean; // false면 상위 resizable=true여도 이 column handle 숨김. 기본 true
49
+ movable?: boolean; // false면 상위 reorderable=true여도 이 column drag 불가. 기본 true
50
+ pinned?: "left" | "right"; // 가로스크롤 시 좌/우 끝에 sticky. reorder 자동 제외
51
+ };
52
+
53
+ type TableHeaderSortState = { key: string; direction: "asc" | "desc" } | null;
54
+
55
+ <TableHeader
56
+ variant="line | plate" // default: line
57
+ columns={TableHeaderColumn[]}
58
+
59
+ resizable={boolean} // 전 셀 resize handle
60
+ reorderable={boolean} // 전 셀 drag reorder + hover
61
+
62
+ selectable={boolean} // 맨 앞 체크박스 셀
63
+ selectAllChecked={boolean}
64
+ selectAllIndeterminate={boolean}
65
+ onSelectAllChange={(next: boolean) => void}
66
+
67
+ order={string[]} // controlled column 순서
68
+ onOrderChange={(next: string[]) => void}
69
+ widths={Record<string, number>} // controlled column 폭
70
+ onWidthsChange={(next: Record<string, number>) => void}
71
+ sort={TableHeaderSortState} // controlled sort 상태
72
+ onSortChange={(next: TableHeaderSortState) => void}
73
+
74
+ columnManager={boolean} // 우측 끝 컬럼 관리 IconButton
75
+ onColumnManagerClick={() => void}
76
+ />
77
+ ```
78
+
79
+ ### Variant × Feature 매트릭스
80
+ | variant | resizable | reorderable | 시각 |
81
+ |---|---|---|---|
82
+ | `line` | off | off | 하단 hairline만 |
83
+ | `line` | on | off | 셀 좌측 세로 tick (눈금자) + resize handle |
84
+ | `line` | off | on | 하단 hairline + 셀 hover 배경, cursor-grab |
85
+ | `line` | on | on | 위 둘 조합 |
86
+ | `plate` | off | off | 라운드 컨테이너 (bg-muted) |
87
+ | `plate` | on | off | hover 시 셀 우측 세로선 (opacity 상승) |
88
+ | `plate` | off | on | 셀 hover 배경, cursor-grab |
89
+ | `plate` | on | on | 위 둘 조합 |
90
+
91
+ ### 상태 매트릭스
92
+ | 상태 | 시각 |
93
+ |---|---|
94
+ | default | variant 기본 색·배경 |
95
+ | cell hover (reorderable=on) | `--hover` 배경, `cursor: grab` |
96
+ | cell dragging | `--bg-muted` 배경, `cursor: grabbing`, z 상승 |
97
+ | resize handle hover (plate) | 우측 세로선 opacity 0 → 0.5 |
98
+ | sort inactive | caret-up-down 아이콘, `--fg-subtle` |
99
+ | sort asc | caret-up 아이콘, `--fg-strong` |
100
+ | sort desc | caret-down 아이콘, `--fg-strong` |
101
+
102
+ ### 조합 예시
103
+ ```tsx
104
+ // 가장 단순 — 라인 + 정렬 없는 기본 헤더
105
+ <TableHeader
106
+ columns={[
107
+ { key: "name", label: "이름", width: 200 },
108
+ { key: "email", label: "이메일", flex: true, minWidth: 240 },
109
+ { key: "role", label: "역할", width: 120 },
110
+ ]}
111
+ />
112
+
113
+ // 리서치 트레이스 뷰 — line + resize + 체크박스, 일부 컬럼만 정렬 가능
114
+ <TableHeader
115
+ variant="line"
116
+ resizable
117
+ selectable
118
+ selectAllChecked={allSelected}
119
+ onSelectAllChange={setAllSelected}
120
+ columns={[
121
+ { key: "timestamp", label: "Timestamp", width: 140, sortable: true },
122
+ { key: "flow", label: "Flow", width: 260 },
123
+ { key: "input", label: "Input", width: 220 },
124
+ { key: "output", label: "Output", flex: true, minWidth: 240 },
125
+ { key: "status", label: "Status", width: 92 },
126
+ { key: "duration", label: "Duration", width: 84, sortable: true },
127
+ { key: "cost", label: "Cost", width: 84, sortable: true },
128
+ ]}
129
+ sort={sort}
130
+ onSortChange={setSort}
131
+ />
132
+
133
+ // 법무 관리 뷰 — plate + 순서 변경 + 리사이즈 + 컬럼 매니저
134
+ <TableHeader
135
+ variant="plate"
136
+ resizable
137
+ reorderable
138
+ columnManager
139
+ onColumnManagerClick={() => setColManagerOpen(true)}
140
+ columns={[
141
+ { key: "type", label: "권리", width: 80 },
142
+ { key: "ourRef", label: "Our Ref", width: 150 },
143
+ { key: "status", label: "상태", width: 200, minWidth: 200 },
144
+ { key: "deadline", label: "법정 마감일", width: 170, minWidth: 170 },
145
+ { key: "client", label: "의뢰인", width: 140 },
146
+ { key: "applicant", label: "출원인", width: 140 },
147
+ { key: "title", label: "출원명", flex: true, minWidth: 300 },
148
+ { key: "history", label: "히스토리", width: 300 },
149
+ { key: "appNo", label: "출원번호", width: 160 },
150
+ { key: "owner", label: "담당자", width: 120 },
151
+ ]}
152
+ order={order}
153
+ onOrderChange={setOrder}
154
+ widths={widths}
155
+ onWidthsChange={setWidths}
156
+ />
157
+ ```
@@ -0,0 +1,129 @@
1
+ # TableRowEdit
2
+
3
+ ### 원칙
4
+ Notion·Excel 톤의 **edit 모드** 행. [[components/table-row]]가 "읽는 행"(행 전체 클릭)이라면 이 컴포넌트는 "편집 가능한 셀 그리드" — 상호작용 단위가 **행이 아니라 셀**이다. 세로 구분선이 **강제**되는 유일한 Row 타입으로, 사용자가 edit 모드임을 즉시 인지할 수 있게 한다.
5
+
6
+ **외곽은 line, 높이는 md로 고정** — 사이즈·variant 축이 없다. 밀도(sm/lg)나 pill(plate) 조합은 edit의 정체성(스프레드시트 톤, 세로선 그리드)과 어긋나 지원하지 않는다. 레이아웃·fade mask는 [[components/table-row]]의 CSS를 상속 — 헤더와의 폭·정렬 동기화 규칙은 동일. 셀 안쪽 콘텐츠는 [[components/table-cell]] 포맷터 또는 Input/Select/DatePicker 같은 편집 컨트롤을 호출 측에서 `columns[].render(data)`로 꽂아 쓴다.
7
+
8
+ ### Do
9
+ - **columns는 헤더와 1:1로 공유**: [[components/table-row]]와 동일한 `TableRowColumn<T>[]`. 폭·정렬·flex 동기화는 그대로 작동. 헤더는 반드시 `variant="line"`과 짝.
10
+ - **세로 구분선은 자동**: 첫 데이터 셀 왼쪽엔 선 없음, 그 뒤로 `--border` hairline이 자동으로 들어감. `selectable`이 있으면 체크박스 셀과 첫 데이터 셀 사이에도 선이 들어가 "선택 영역 ↔ 편집 영역" 구분.
11
+ - **`activeCellKey`로 '지금 편집 중인 셀' 단일화**: 호출 측에서 하나의 셀 key만 넘김. 포커스·클릭 시 `onCellActivate(key)`가 호출되므로 상위에서 그 값을 state로 받아 다시 돌려주는 패턴. 활성 셀은 `--primary-subtle` 배경 + 1px inset ring.
12
+ - **셀 콘텐츠는 `TableCell*` + 편집 컨트롤**: 표시 전엔 [[components/table-cell]] 포맷터, 활성 상태로 바뀌면 [[components/input]]·[[components/select]]·[[components/date-picker]] 등으로 교체하는 패턴. Row는 레이아웃만 관장.
13
+ - **selectable은 bulk 액션용**: 체크박스로 여러 행을 모아 복사/삭제. [[components/table-row-select]]와 선택 톤(`--primary-subtle`)이 동일 — 멀티셀렉션 신호 일관성.
14
+ - **읽기 전용 셀은 `columns[].editable: false`로 명시**: SKU·식별자·계산 파생값처럼 "값을 보여주지만 바꿀 수 없는" 셀. 커서는 default, hover/active 톤 없음, 포커스 불가, 세로 구분선 대상에서도 제외됨. 첫 "편집 가능" 셀 왼쪽엔 자동으로 선이 없음.
15
+ - **편집 컨트롤은 호출 측 책임**: Input은 `Enter` 커밋 / `Esc` 취소 / `Tab` 다음 셀로 쓰는 게 Notion/Excel 표준. Select는 `defaultOpen`으로 셀 활성화와 동시에 open해 한 번의 클릭으로 선택까지 끝나게. `autoFocus` 대신 `focus({ preventScroll: true })` + `select()`를 쓸 것 — autoFocus는 scrollIntoView로 페이지가 튐.
16
+
17
+ ### Don't
18
+ - 행 전체에 `onClick`/`href`를 걸지 않는다. edit 모드는 **셀**이 상호작용 단위 — 행 전체 핸들러가 있으면 셀 활성화와 충돌해 포커스/편집이 꼬인다. 행 라우팅이 필요하면 [[components/table-row]]를 쓸 것.
19
+ - 세로 구분선을 끄지 않는다. edit 모드의 **정체성** — 끄면 navigate 모드와 시각 구분이 사라져 사용자가 편집 가능 여부를 인지하지 못함.
20
+ - 셀 안에 또 다른 anchor/button을 중첩하지 않는다. 셀 자체가 포커스 타겟(`tabIndex=0`) — nested interactive는 접근성·키보드 탐색 파괴.
21
+ - `activeCellKey`를 여러 개로 동시에 유지하지 않는다. "지금 편집 중"은 한 번에 하나. 멀티셀 선택이 필요하면 별도 selection state로 분리(미래 기능).
22
+ - hover가 selected/active 셀을 덮지 않는다 — 컴포넌트 CSS가 보장([[feedback/hover-over-active]]).
23
+ - `background`를 직접 지정하지 않는다. selected/active는 축 토큰(`--primary-subtle`)으로 자동.
24
+ - 사이즈·variant를 쓰려 하지 않는다 — 애초에 prop이 없다. 밀도 다른 테이블이 필요하면 edit 대신 [[components/table-row]]의 size로 해결.
25
+
26
+ ### Fallback
27
+ - 셀 콘텐츠 애매하면 [[components/table-cell]] 중 `TableCellText`. 편집 시만 [[components/input]]으로 교체.
28
+ - `activeCellKey` 미지정 → 아무 셀도 활성 아님(모두 포커스 가능 상태만 유지).
29
+
30
+ ### 관련
31
+ - [[components/table-row]] — navigate 모드(행 전체 클릭). edit과 스펙 공유.
32
+ - [[components/table-row-select]] — select 모드(체크박스 bulk 선택). selected 톤 일관.
33
+ - [[components/table-header]] — columns·order·widths·selectable 세트 (variant=line과 짝)
34
+ - [[components/table-cell]] — 각 셀 콘텐츠 포맷터
35
+ - [[components/input]], [[components/select]], [[components/date-picker]] — 활성 셀의 편집 컨트롤로 꽂음
36
+ - [[axes/density]] — space-* 토큰을 통해 자동 반영 (명시적 size 축 없음)
37
+ - [[tokens/color]] — `--primary-subtle`/`--primary`(active cell), `--border`(세로 구분선), `--hover`(셀 hover), `--fg-disabled`
38
+ - [[feedback/hover-over-active]] — selected/active 위 hover가 상태 톤을 덮지 않음
39
+
40
+ ### Props 시그니처
41
+ ```tsx
42
+ type TableRowColumn<T> = TableHeaderColumn & {
43
+ render?: (data: T) => ReactNode; // 미지정 시 data[key]
44
+ editable?: boolean; // 기본 true. false면 read-only
45
+ resizable?: boolean; // 폭 drag 조절 opt-in (widths로 state 커밋)
46
+ movable?: boolean; // 순서 drag 변경 opt-in (order로 state 커밋)
47
+ removable?: boolean; // 헤더 액션으로 삭제 opt-in
48
+ pinned?: "left" | "right"; // 가로스크롤 시 좌/우 sticky
49
+ };
50
+
51
+ <TableRowEdit<T>
52
+ size="sm | md" // default: md (sm=36, md=40)
53
+ columns={TableRowColumn<T>[]} // 헤더와 동일 스펙 + render + editable + …
54
+ order={string[]} // 헤더와 공유
55
+ widths={Record<string, number>} // 헤더와 공유
56
+ data={T}
57
+
58
+ selectable={boolean} // 맨 앞 체크박스
59
+ selected={boolean}
60
+ onSelectedChange={(next: boolean) => void}
61
+
62
+ activeCellKey={string} // 지금 편집 중인 셀 key
63
+ onCellActivate={(key: string) => void} // 편집 가능 셀 클릭/Enter/Space 시
64
+
65
+ disabled={boolean}
66
+ />
67
+ ```
68
+
69
+ ### 상태 매트릭스
70
+ | 상태 | 시각 |
71
+ |---|---|
72
+ | default | 세로 구분선 + 행 하단 hairline |
73
+ | cell hover | 해당 셀만 `--hover` 배경 |
74
+ | cell active | `--primary-subtle` 배경 + 1px inset ring (`--primary`) |
75
+ | cell focus-visible (keyboard, not active) | `--ring-shadow` 외곽 ring |
76
+ | selected (행) | 행 전체 `--primary-subtle` 배경 |
77
+ | disabled | `--fg-disabled` 톤, 셀 포커스 불가 |
78
+
79
+ ### 조합 예시
80
+ ```tsx
81
+ // 가장 단순 — 셀 하나씩 활성화되는 스프레드시트 톤
82
+ const [activeCell, setActiveCell] = useState<string | undefined>();
83
+
84
+ <TableHeader variant="line" columns={headerCols} />
85
+ {rows.map((row) => (
86
+ <TableRowEdit
87
+ key={row.id}
88
+ columns={rowCols}
89
+ data={row}
90
+ activeCellKey={activeCell}
91
+ onCellActivate={setActiveCell}
92
+ />
93
+ ))}
94
+
95
+ // 활성 셀만 Input으로 교체하는 패턴
96
+ const makeEditColumn = <T,>(col: TableHeaderColumn, field: keyof T): TableRowColumn<T> => ({
97
+ ...col,
98
+ render: (d) =>
99
+ activeCell === col.key && focusedRowId === (d as { id: string }).id ? (
100
+ <Input defaultValue={String(d[field])} autoFocus onBlur={commit} />
101
+ ) : (
102
+ <TableCellText>{String(d[field])}</TableCellText>
103
+ ),
104
+ });
105
+
106
+ // 체크박스 selection + 셀 편집 동시 사용
107
+ <TableHeader
108
+ variant="line"
109
+ columns={headerCols}
110
+ selectable
111
+ selectAllChecked={allSelected}
112
+ onSelectAllChange={setAllSelected}
113
+ />
114
+ {rows.map((row) => (
115
+ <TableRowEdit
116
+ key={row.id}
117
+ columns={rowCols}
118
+ data={row}
119
+ selectable
120
+ selected={selected.has(row.id)}
121
+ onSelectedChange={(next) => toggleOne(row.id, next)}
122
+ activeCellKey={focusedRowId === row.id ? activeCell : undefined}
123
+ onCellActivate={(key) => {
124
+ setFocusedRowId(row.id);
125
+ setActiveCell(key);
126
+ }}
127
+ />
128
+ ))}
129
+ ```
@@ -0,0 +1,82 @@
1
+ # TableRowSelect
2
+
3
+ ### 원칙
4
+ "선택 모드" 데이터 행. [[components/table-row]]의 **navigate 모드**가 "클릭 = 라우팅/상세 열기"였다면, select 모드는 **"클릭 = 선택 토글"** 하나로 묶인다. 체크박스 UI는 **쓰지 않음** — 선택된 행 자체가 primary 톤의 옅은 배경으로 명확히 구분된다. 행의 어느 지점을 눌러도, 키보드 Space를 눌러도 전부 동일한 토글.
5
+
6
+ 레이아웃·사이즈·variant·fade mask·폭 공식은 [[components/table-row]]와 **완전히 공유** — 같은 `TableRowColumn<T>[]`를 받고, 같은 2단계(cellSlot→cell→cellInner) 구조로 렌더된다. 차이는 "선택 상태 시각"과 "클릭 핸들러의 의미"뿐.
7
+
8
+ ### Do
9
+ - **columns는 TableRow와 동일 스펙**: 같은 `TableRowColumn<T>[]`를 Header와 Row 모두에 전달. 폭·정렬·render가 자동 동기화.
10
+ - **체크박스 UI 없음**: 행 자체가 토글 영역 — 선택된 상태는 배경 톤으로만 표현. 헤더에도 `selectable=false` (기본).
11
+ - **선택 상태 배경 = hover 톤**: `--hover`(중성 알파). TableRow(navigate)의 active와 같은 토큰 체계 — "지금 포커스된/선택된 행" 신호가 컴포넌트 간 일관되게 유지됨.
12
+ - **hover가 selected를 덮지 않도록**: selected 위에서 hover해도 배경 톤 유지. [[feedback/hover-over-active]] 원칙과 정합.
13
+ - **키보드 Space로 토글**: Enter는 안 받음 — Finder·Explorer도 선택은 Space. 접근성 트리는 `role="row"` + `aria-selected`.
14
+ - **전체 선택/해제는 외부 UI로**: Row 자체엔 "select all" 개념이 없음. 상위 툴바에 버튼으로 두거나 헤더의 `selectable`을 켜고 TableRow(navigate)와 섞어 쓰는 패턴은 지양.
15
+
16
+ ### Don't
17
+ - select 모드 안에 navigate(`href`/`onClick 라우팅`)를 섞지 않는다. "클릭의 의미가 하나"라는 규칙이 이 컴포넌트의 핵심. 두 가지가 필요하면 [[components/table-row]]를 쓰고 체크박스는 selectable로 켠다.
18
+ - 체크박스 UI를 추가하지 않는다 — 이 컴포넌트의 정체성이 "체크박스 없이 행 자체로 선택". 체크박스가 필요하면 [[components/table-row]]의 `selectable` + `onSelectedChange`로.
19
+ - `active` prop 없음. "지금 보고 있는 상세 행"의 개념은 navigate 전용 — select 모드엔 "선택됨"만 있다.
20
+ - 행 한 줄 안에 여러 선택 가능한 영역을 두지 않는다. 선택 단위는 "행 하나" 고정.
21
+
22
+ ### Fallback
23
+ - 사이즈 애매하면 `md`. 파일 리스트 표준.
24
+ - variant 애매하면 `line`. 파일 매니저 톤.
25
+ - selected 초기값 애매하면 `false`.
26
+
27
+ ### 관련
28
+ - [[components/table-row]] — navigate 모드. columns 스펙·레이아웃·fade mask 전부 공유. 체크박스가 필요하면 이쪽의 `selectable` 사용
29
+ - [[components/table-header]] — 헤더는 selectable 끄고 사용 (이 컴포넌트는 체크박스 없음)
30
+ - [[tokens/color]] — `--hover`(선택 + hover 공통 중성 톤), `--fg-disabled`(disabled)
31
+
32
+ ### Props 시그니처
33
+ ```tsx
34
+ <TableRowSelect<T>
35
+ variant="line | plate" // default: line
36
+ size="sm | md | lg" // default: md
37
+ columns={TableRowColumn<T>[]} // TableRow와 동일 스펙
38
+ order={string[]} // Header와 공유
39
+ widths={Record<string, number>} // Header와 공유
40
+ data={T}
41
+
42
+ selected={boolean}
43
+ onSelectedChange={(next: boolean) => void} // 행 클릭 / Space 공통 콜백
44
+
45
+ disabled={boolean} // pointer-events + 키보드 차단
46
+ />
47
+ ```
48
+
49
+ ### 상태 매트릭스
50
+ | 상태 | 시각 |
51
+ |---|---|
52
+ | default | variant 기본 배경 |
53
+ | hover | `--hover` 중성 톤 |
54
+ | selected | `--hover` 배경 (TableRow active와 동일 토큰) |
55
+ | selected + hover | selected 톤 유지(hover 덮어쓰기 금지) |
56
+ | focus-visible (keyboard) | 외곽 ring(`--ring-shadow`) |
57
+ | disabled | `--fg-disabled` 톤, pointer-events 없음, Space 무시 |
58
+
59
+ ### 조합 예시
60
+ ```tsx
61
+ const [selected, setSelected] = useState<Set<string>>(new Set());
62
+
63
+ const toggle = (id: string, next: boolean) => {
64
+ setSelected((prev) => {
65
+ const n = new Set(prev);
66
+ if (next) n.add(id); else n.delete(id);
67
+ return n;
68
+ });
69
+ };
70
+
71
+ <TableHeader variant="line" columns={HEADER_COLUMNS} />
72
+ {rows.map((row) => (
73
+ <TableRowSelect
74
+ key={row.id}
75
+ variant="line"
76
+ columns={ROW_COLUMNS}
77
+ data={row}
78
+ selected={selected.has(row.id)}
79
+ onSelectedChange={(next) => toggle(row.id, next)}
80
+ />
81
+ ))}
82
+ ```
@@ -0,0 +1,156 @@
1
+ # TableRow
2
+
3
+ ### 원칙
4
+ 데이터 그리드의 한 행. [[components/table-header]]와 같은 `columns` 스펙을 공유해 폭·정렬·순서가 자동으로 맞도록 설계된다. 이번 버전은 **navigate 모드** — 행 전체가 클릭 요소가 되어 상세 라우팅·패널 오픈을 담당한다. select 모드(Google Drive 스타일 체크박스 토글), edit 모드(Notion·Excel 스타일 셀 편집)는 각각 [[components/table-row-select]], [[components/table-row-edit]]로 분리.
5
+
6
+ 행 자체는 레이아웃만 통제한다 — 셀 안쪽 콘텐츠는 [[components/table-cell]] 세트(`TableCellText`/`TableCellNumber`/`TableCellBadge`/`TableCellImage`/`TableCellIcon`/`TableCellAvatar`/`TableCellLink`/`TableCellMeter`)로 꽂아 쓴다. 콘텐츠 포맷터와 행 레이아웃을 분리해 셀 타입이 늘어도 Row API는 흔들리지 않는다.
7
+
8
+ ### Do
9
+ - **columns는 헤더와 1:1로 공유**: 같은 `TableHeaderColumn[]`에 `render(data)` 콜백을 얹은 `TableRowColumn<T>[]`로 전달. 폭·정렬·flex가 자동 동기화되어 "헤더-아이템은 항상 세트" 원칙이 유지됨.
10
+ - **`order` / `widths`도 헤더와 공유**: 헤더에서 drag reorder·resize한 결과를 그대로 Row에 넘기면 즉시 반영. uncontrolled면 columns 원본 순서·width 사용.
11
+ - **variant는 헤더와 짝 맞춤**: `line` 헤더 아래엔 `line` 행, `plate` 헤더 아래엔 `plate` 행. 섞으면 라운드/구분 규칙이 깨짐.
12
+ - **size로 밀도 조절**: `sm`(xs 폰트/control-h-sm) → 데이터 테이블·대시보드, `md`(sm 폰트/control-h-md) → 표준, `lg`(base 폰트/control-h-lg) → 콘텐츠 중심 리스트. padding은 세 사이즈 공유.
13
+ - **라우팅은 `href`, 액션은 `onClick`**: `href` 있으면 anchor로 렌더(네이티브 Cmd+click·prefetch 등 활용), 없으면 `role="button"` + Enter/Space 키보드 지원. 둘 다 없으면 단순 디스플레이 행.
14
+ - **`active`는 현재 보고 있는 상세의 반영**: 상세 패널이 열린 행에만 적용. hover가 active를 덮지 않도록 CSS가 보장.
15
+ - **체크박스 셀은 `selectable` prop**: 헤더 selectable과 짝. 체크박스 영역 클릭은 자동으로 행 클릭 전파 차단.
16
+ - **셀 안에 포맷터만**: `columns[].render` 안에는 `TableCell*` 포맷터 또는 1개의 레벨 안쪽 요소까지. 여러 인터랙션 요소(링크·버튼·메뉴)를 셀 안에 넣지 않는다 — 행 전체가 이미 클릭 요소.
17
+
18
+ ### Don't
19
+ - 행 안에 또 다른 anchor/button을 중첩하지 않는다. nested interactive는 접근성 깨지고 pointer 이벤트도 꼬임. 셀별 액션이 필요하면 edit 모드(별도 컴포넌트)로. `TableCellLink`는 anchor를 포함하므로 `href`/`onClick` 있는 navigate 행에서 쓰지 말 것 — display-only 행이나 [[components/table-row-edit]]에서만 허용.
20
+ - `line` 행에 `background`를 직접 지정해 활성/hover 색을 커스텀하지 않는다. `active` prop + 축 토큰(hover) 조합으로 해결.
21
+ - `size`로 셀 padding을 바꾸지 않는다 — 사이즈는 세로 폭·폰트만. 셀 안 요소(badge·chip)의 리듬이 깨짐.
22
+ - 셀 사이 세로 구분선을 넣지 않는다. navigate 모드는 "읽는 행" — 세로 구분선은 [[components/table-row-edit]](엑셀/Notion 스타일 셀 편집) 전용 시각이다.
23
+ - disabled 행에 onClick 로직을 걸지 않는다. 컴포넌트가 pointer-events·tabIndex 모두 차단하므로 상위에서 가드 불필요.
24
+ - Row 안에 긴 설명문을 두 줄 이상으로 넣지 않는다. 셀은 1-line ellipsis가 기본 — 긴 콘텐츠는 상세 패널로.
25
+
26
+ ### Fallback
27
+ - 사이즈 애매하면 `md`. 데이터 테이블 표준.
28
+ - variant 애매하면 `line`. 페이지 흐름에 자연스럽게 섞임.
29
+ - 셀 포맷터 애매하면 `TableCellText`. 톤만 default/strong/muted/subtle로 선택.
30
+ - 행 전체가 클릭 아닌 경우 — `href`/`onClick` 둘 다 생략하면 단순 디스플레이 행이 됨.
31
+
32
+ ### 관련
33
+ - [[components/table-header]] — columns·order·widths·selectable 세트
34
+ - [[components/table-cell]] — 각 셀 콘텐츠 포맷터 (Text/Number/Image/Tag/Badge/Icon/Avatar/Link/Meter)
35
+ - [[components/checkbox]] — selectable 셀
36
+ - [[axes/density]] — 세 사이즈 모두 Density 축 scale에 반응
37
+ - [[axes/shape]] — plate variant의 radius가 Shape 축에 반응
38
+ - [[tokens/color]] — `--hover`(행 hover 배경), `--border`(line hairline), `--fg-disabled`(disabled 톤)
39
+
40
+ ### Props 시그니처
41
+ ```tsx
42
+ type TableRowColumn<T> = TableHeaderColumn & {
43
+ render?: (data: T) => ReactNode; // 미지정 시 data[key]
44
+ editable?: boolean; // [[components/table-row-edit]] 전용. false면 read-only
45
+ resizable?: boolean; // 헤더 drag로 column 폭 조절 opt-in. widths로 state 커밋
46
+ movable?: boolean; // 헤더 drag로 column 순서 변경 opt-in. order로 state 커밋
47
+ removable?: boolean; // 헤더 context menu / × 액션 노출 opt-in
48
+ pinned?: "left" | "right"; // 가로스크롤 시 좌/우에 sticky. reorder/remove 자동 제외
49
+ };
50
+
51
+ <TableRow<T>
52
+ variant="line | plate" // default: line (헤더와 짝)
53
+ size="sm | md | lg" // default: md
54
+ columns={TableRowColumn<T>[]} // 헤더와 동일 스펙 + render
55
+ order={string[]} // 헤더와 공유
56
+ widths={Record<string, number>} // 헤더와 공유
57
+ data={T} // 행 데이터 — columns[].render(data)로 사용
58
+
59
+ selectable={boolean} // 맨 앞 체크박스
60
+ selected={boolean}
61
+ onSelectedChange={(next: boolean) => void}
62
+
63
+ active={boolean} // 상세가 열린 행 — hover 톤
64
+ disabled={boolean} // 톤 다운 + 상호작용 차단
65
+
66
+ // Navigate — 둘 중 하나만 (anchor OR button role)
67
+ href={string} // 있으면 <a>로 렌더
68
+ onClick={(e) => void} // 있으면 <div role="button">로 렌더
69
+ />
70
+ ```
71
+
72
+ ### Variant × Size 매트릭스
73
+ | variant | size | 세로 폭 | 폰트 | 사용처 |
74
+ |---|---|---|---|---|
75
+ | `line` | `sm` | control-h-sm | text-xs | 데이터 테이블 · 로그 뷰 |
76
+ | `line` | `md` | control-h-md | text-sm | 표준 리스트 뷰 (기본) |
77
+ | `line` | `lg` | control-h-lg | text-base | 콘텐츠 중심 리스트 |
78
+ | `plate` | `sm` | control-h-sm | text-xs | 밀도 높은 카드 안 리스트 |
79
+ | `plate` | `md` | control-h-md | text-sm | 카드 안 표준 리스트 |
80
+ | `plate` | `lg` | control-h-lg | text-base | 큰 썸네일 아이템 리스트 |
81
+
82
+ ### 상태 매트릭스
83
+ | 상태 | 시각 |
84
+ |---|---|
85
+ | default | variant 기본 색·배경 |
86
+ | hover (interactive) | 전체 행 배경 `--hover` |
87
+ | active | 전체 행 배경 `--hover` (hover가 덮지 않음) |
88
+ | focus-visible (keyboard) | `--ring-shadow` 외곽 ring |
89
+ | disabled | `--fg-disabled` 톤, pointer-events 없음 |
90
+
91
+ ### 조합 예시
92
+ ```tsx
93
+ // 가장 단순 — line / navigate / 체크박스 없음
94
+ <TableHeader variant="line" columns={headerCols} />
95
+ {rows.map((row) => (
96
+ <TableRow
97
+ key={row.id}
98
+ variant="line"
99
+ columns={rowCols}
100
+ data={row}
101
+ onClick={() => openDetail(row.id)}
102
+ active={row.id === activeId}
103
+ />
104
+ ))}
105
+
106
+ // Google Drive 스타일 — 체크박스로 선택, 나머지 영역은 라우팅
107
+ <TableHeader
108
+ variant="line"
109
+ columns={headerCols}
110
+ selectable
111
+ selectAllChecked={allSelected}
112
+ onSelectAllChange={setAllSelected}
113
+ />
114
+ {rows.map((row) => (
115
+ <TableRow
116
+ key={row.id}
117
+ variant="line"
118
+ columns={rowCols}
119
+ data={row}
120
+ selectable
121
+ selected={selected.has(row.id)}
122
+ onSelectedChange={(next) => toggleOne(row.id, next)}
123
+ href={`/trace/${row.id}`}
124
+ />
125
+ ))}
126
+
127
+ // 법무 관리 뷰 — plate + 큰 썸네일 + 태그 셀 + 통화 포맷터
128
+ <TableHeader variant="plate" columns={legalHeaderCols} />
129
+ {legalRows.map((row) => (
130
+ <TableRow
131
+ key={row.id}
132
+ variant="plate"
133
+ size="md"
134
+ columns={legalRowCols}
135
+ data={row}
136
+ onClick={() => router.push(`/legal/${row.id}`)}
137
+ />
138
+ ))}
139
+
140
+ // columns에 render 콜백을 심는 방식
141
+ const rowCols: TableRowColumn<LegalRow>[] = [
142
+ { ...headerCols[0], render: (d) => <TableCellBadge status={d.typeStatus}>{d.typeLabel}</TableCellBadge> },
143
+ { ...headerCols[1], render: (d) => <TableCellText tone="muted">{d.ourRef}</TableCellText> },
144
+ { ...headerCols[2], render: (d) => <TableCellText tone="strong">{d.title}</TableCellText> },
145
+ { ...headerCols[3], render: (d) => (
146
+ <span style={{ display: "inline-flex", gap: "var(--space-1)" }}>
147
+ {d.tags.map((t) => <TableCellBadge key={t} status="info">{t}</TableCellBadge>)}
148
+ </span>
149
+ ),
150
+ },
151
+ { ...headerCols[4], render: (d) => (
152
+ <TableCellNumber value={d.cost} format={{ style: "currency", currency: "KRW" }} locale="ko-KR" />
153
+ ),
154
+ },
155
+ ];
156
+ ```
@@ -0,0 +1,122 @@
1
+ # Tabs
2
+
3
+ ### 원칙
4
+ 동일 문맥 안에서 **한 번에 하나의 뷰만 보이는 여러 뷰**를 전환하는 컨트롤. 세 variant는 사용 맥락이 완전히 다르다 — 무게 순으로 `segmented > underline > text`.
5
+
6
+ - **`underline`** — 페이지·섹션 안의 **콘텐츠 전환**. 하단 인디케이터 바만 있어 무게가 가볍고, 콘텐츠 흐름을 방해하지 않는다. Overview / Details / Activity 같은 화면 단위 전환의 표준.
7
+ - **`segmented`** — 연한 컨테이너 안에 3~4개 pill. **필터·뷰 스위처** 같은 소수 옵션 즉시 선택. Day / Week / Month, List / Grid / Timeline, Preview / Code 등.
8
+ - **`text`** — 컨테이너·라인·인디케이터 전부 없음. 색과 weight만으로 위계 표현. **공간을 거의 안 먹어야 할 때** — 페이지 헤더 sub-nav, 카드 안 미니 스위처, 목록 상단 상태 필터.
9
+
10
+ ### Do
11
+ - **variant는 목적으로 고른다** — 콘텐츠 전환은 `underline`, 옵션 스위처는 `segmented`, 최소 무게 필터·sub-nav는 `text`. 예쁘다는 이유로 뒤바꾸지 말 것.
12
+ - **선택된 값은 URL query·라우터에 반영** (`?tab=activity`). 새로고침·공유·뒤로가기가 자연스러워짐.
13
+ - **아이콘은 라벨을 도울 때만**. 3자 이하 라벨 옆에 아이콘 없이도 인지될 때는 아이콘 없이. segmented는 좁으니 아이콘+긴 라벨 조합은 피한다.
14
+ - **패널 콘텐츠에 상태(폼, 스크롤 위치)가 있으면 `keepMounted`**. 탭 전환마다 리셋되는 폼은 아주 나쁜 경험.
15
+ - **키보드**: 컴포넌트가 ← → Home End 처리. Tab 키는 탭 리스트 밖으로 나가 활성 패널로 이동한다 (roving tabindex).
16
+ - **한 화면에 탭 계층은 하나까지**. 탭 안에 다시 탭을 두면 방향 감각이 무너진다.
17
+
18
+ ### Don't
19
+ - `underline` 하단 border/두께를 CSS로 덮어쓰지 않는다 — `--border` + `--space-0_5` 조합이 Density·Mode 축에 반응한다.
20
+ - `segmented`를 5개 이상으로 늘리지 않는다. 5개 이상이면 [[components/select]] 또는 [[components/dropdown-menu]].
21
+ - `segmented` 안 트리거에 **가변 너비 라벨** 금지 — flex 균등 분배가 무너져 pill이 어색해진다. 라벨 길이가 심하게 차이나면 `underline`으로.
22
+ - 패널 헤더(title + Tabs가 `space-between`으로 양쪽 배치되는 레이아웃)에 `segmented`를 넣지 않는다 — segmented는 라벨 합산 폭이 커서 반대편 title을 밀어내 세로로 쪼개 놓는다. 패널 헤더 안 뷰 스위처가 필요하면 `text` variant 또는 `underline` sm 쓰거나, title을 `flex-shrink: 0`·`white-space: nowrap`으로 고정하고 Tabs는 그 다음 자리에만.
23
+ - 페이지 이동(URL 자체가 바뀌는 라우팅)에 Tabs 사용 금지. 그건 [[components/sidebar]] 또는 상단 nav의 역할.
24
+ - 활성 색을 `--primary`로 덮지 않는다. 활성은 `--fg-strong`(무채) — 브랜드 컬러를 여기 쓰면 CTA와 위계가 충돌.
25
+ - `text`를 페이지 안 여러 자리에 반복해 쓰지 않는다. 장식이 없어서 "이게 탭인가 링크인가" 혼동이 잘 남 — 한 화면에 하나 정도로.
26
+
27
+ ### Fallback
28
+ variant 애매하면 `underline`, size 애매하면 `md`.
29
+
30
+ ### 관련
31
+ - [[components/button]] · [[components/split-button]] — 동일한 `--control-h-*` 사이즈 토큰 공유
32
+ - [[components/filter-button]] — 여러 개 동시 선택이면 FilterButton. Tabs는 항상 1개만 활성.
33
+ - [[axes/density]] — `--control-h-*`가 트리거 높이·segmented 컨테이너 높이에 매핑
34
+ - [[axes/shape]] — segmented 컨테이너/트리거 radius가 Shape 축에 반응
35
+ - [[axes/motion]] — 인디케이터 등장·hover 색 전환 duration
36
+ - [[tokens/color]] — `--fg-muted` → `--fg-strong` 대비, `--bg-muted` 컨테이너
37
+
38
+ ### Props 시그니처
39
+ ```tsx
40
+ <Tabs
41
+ variant="underline | segmented | text" // default: underline
42
+ size="sm | md | lg" // default: md
43
+ value={string} // controlled
44
+ defaultValue={string} // uncontrolled 초기값
45
+ onValueChange={(v) => void}
46
+ >
47
+ <Tabs.List aria-label="섹션 선택">
48
+ <Tabs.Trigger value="overview" leadingIcon={<Icon name="rectangle" />}>Overview</Tabs.Trigger>
49
+ <Tabs.Trigger value="activity">Activity</Tabs.Trigger>
50
+ <Tabs.Trigger value="settings" disabled>Settings</Tabs.Trigger>
51
+ </Tabs.List>
52
+
53
+ <Tabs.Panel value="overview">…</Tabs.Panel>
54
+ <Tabs.Panel value="activity" keepMounted>…</Tabs.Panel>
55
+ <Tabs.Panel value="settings">…</Tabs.Panel>
56
+ </Tabs>
57
+ ```
58
+
59
+ ### 상태 매트릭스
60
+ | 상태 | underline | segmented | text |
61
+ |---|---|---|---|
62
+ | default | `--fg-muted` 텍스트, 인디케이터 투명 | `--fg-muted` 텍스트, 트리거 배경 투명 | `--fg-muted` 텍스트, regular weight |
63
+ | hover | `--fg` 텍스트, 아이콘 opacity 1 | `--hover` 배경 + `--fg` 텍스트 | `--fg` 텍스트 (배경 없음) |
64
+ | active(선택) | `--fg-strong` 텍스트 + 하단 2px 인디케이터 | `--bg` 표면 + `shadow-xs` + `--fg-strong` | `--fg-strong` 텍스트 + semibold |
65
+ | focus-visible | `--ring-shadow` 링 (전체 트리거) | 동일 | 동일 |
66
+ | disabled | opacity 0.5, cursor not-allowed, tabIndex=-1 | 동일 | 동일 |
67
+
68
+ ### Size 가이드
69
+ | size | height (control-h) | 언제 |
70
+ |---|---|---|
71
+ | `sm` | control-h-sm | 밀도 높은 대시보드, 사이드 패널 안, 카드 헤더 |
72
+ | `md` | control-h-md | **기본.** 페이지 섹션 탭, 대부분의 스위처 |
73
+ | `lg` | control-h-lg | 랜딩·마케팅 상단, 태블릿·터치 |
74
+
75
+ ### 접근성
76
+ - 루트가 자동으로 `role=tablist` / `role=tab` / `role=tabpanel`과 `aria-selected` / `aria-controls` / `aria-labelledby` 를 붙인다.
77
+ - 활성 트리거만 `tabIndex=0`, 나머지는 `-1` (roving focus).
78
+ - ← → 로 순환 이동, Home / End 로 양 끝.
79
+ - 사용자에게 필요한 유일한 aria는 `<Tabs.List aria-label="…">` — 스크린리더가 이 탭이 무엇을 스위치하는지 알 수 있게.
80
+
81
+ ### 조합 예시
82
+ ```tsx
83
+ // 콘텐츠 전환 (underline) — URL query와 동기화
84
+ const [tab, setTab] = useQueryState("tab", { defaultValue: "overview" });
85
+ <Tabs value={tab} onValueChange={setTab}>
86
+ <Tabs.List aria-label="프로젝트 섹션">
87
+ <Tabs.Trigger value="overview" leadingIcon={<Icon name="rectangle" />}>Overview</Tabs.Trigger>
88
+ <Tabs.Trigger value="activity" leadingIcon={<Icon name="chart-line" />}>Activity</Tabs.Trigger>
89
+ <Tabs.Trigger value="members" leadingIcon={<Icon name="user" />}>Members</Tabs.Trigger>
90
+ </Tabs.List>
91
+ <Tabs.Panel value="overview"><Overview /></Tabs.Panel>
92
+ <Tabs.Panel value="activity" keepMounted><Activity /></Tabs.Panel>
93
+ <Tabs.Panel value="members"><Members /></Tabs.Panel>
94
+ </Tabs>
95
+
96
+ // 뷰 스위처 (segmented) — 즉시 반영, 페이지 이동 아님
97
+ <Tabs variant="segmented" size="sm" defaultValue="week" onValueChange={setRange}>
98
+ <Tabs.List aria-label="기간">
99
+ <Tabs.Trigger value="day">Day</Tabs.Trigger>
100
+ <Tabs.Trigger value="week">Week</Tabs.Trigger>
101
+ <Tabs.Trigger value="month">Month</Tabs.Trigger>
102
+ <Tabs.Trigger value="year">Year</Tabs.Trigger>
103
+ </Tabs.List>
104
+ </Tabs>
105
+
106
+ // Preview / Code 스위처 — 패널 없이 트리거만 써서 외부 state 제어
107
+ <Tabs variant="segmented" value={mode} onValueChange={setMode}>
108
+ <Tabs.List aria-label="뷰 모드">
109
+ <Tabs.Trigger value="preview" leadingIcon={<Icon name="eye" />}>Preview</Tabs.Trigger>
110
+ <Tabs.Trigger value="code" leadingIcon={<Icon name="brackets-square" />}>Code</Tabs.Trigger>
111
+ </Tabs.List>
112
+ </Tabs>
113
+
114
+ // 목록 상단 상태 필터 (text) — 장식 없이 색·weight만
115
+ <Tabs variant="text" size="sm" value={status} onValueChange={setStatus}>
116
+ <Tabs.List aria-label="이슈 상태">
117
+ <Tabs.Trigger value="all">전체</Tabs.Trigger>
118
+ <Tabs.Trigger value="open">열림</Tabs.Trigger>
119
+ <Tabs.Trigger value="closed">닫힘</Tabs.Trigger>
120
+ </Tabs.List>
121
+ </Tabs>
122
+ ```