@iamfiro/design 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.
Files changed (264) hide show
  1. package/AGENTS.md +81 -0
  2. package/README.md +15 -2
  3. package/agents/INDEX.md +156 -0
  4. package/agents/accordion/AGENTS.md +279 -0
  5. package/agents/action-bar/AGENTS.md +90 -0
  6. package/agents/alert/AGENTS.md +212 -0
  7. package/agents/alert-dialog/AGENTS.md +193 -0
  8. package/agents/app-shell/AGENTS.md +409 -0
  9. package/agents/area-chart/AGENTS.md +183 -0
  10. package/agents/aspect-ratio/AGENTS.md +143 -0
  11. package/agents/avatar/AGENTS.md +130 -0
  12. package/agents/badge/AGENTS.md +217 -0
  13. package/agents/bar-chart/AGENTS.md +386 -0
  14. package/agents/bar-list/AGENTS.md +223 -0
  15. package/agents/brand-icon/AGENTS.md +82 -0
  16. package/agents/breadcrumb/AGENTS.md +133 -0
  17. package/agents/button/AGENTS.md +195 -0
  18. package/agents/card/AGENTS.md +141 -0
  19. package/agents/card-frame/AGENTS.md +227 -0
  20. package/agents/chat-bubble/AGENTS.md +266 -0
  21. package/agents/chat-composer/AGENTS.md +251 -0
  22. package/agents/checkbox/AGENTS.md +113 -0
  23. package/agents/code-block/AGENTS.md +144 -0
  24. package/agents/color-dot/AGENTS.md +120 -0
  25. package/agents/color-picker/AGENTS.md +224 -0
  26. package/agents/colors/AGENTS.md +168 -0
  27. package/agents/combobox/AGENTS.md +218 -0
  28. package/agents/command-menu/AGENTS.md +169 -0
  29. package/agents/comment/AGENTS.md +206 -0
  30. package/agents/context-menu/AGENTS.md +303 -0
  31. package/agents/copy-button/AGENTS.md +84 -0
  32. package/agents/count-badge/AGENTS.md +115 -0
  33. package/agents/countdown/AGENTS.md +101 -0
  34. package/agents/data-grid/AGENTS.md +460 -0
  35. package/agents/date-picker/AGENTS.md +197 -0
  36. package/agents/delta/AGENTS.md +114 -0
  37. package/agents/description-list/AGENTS.md +251 -0
  38. package/agents/dialog/AGENTS.md +391 -0
  39. package/agents/drawer/AGENTS.md +471 -0
  40. package/agents/dropdown-menu/AGENTS.md +322 -0
  41. package/agents/dropzone/AGENTS.md +116 -0
  42. package/agents/elapsed/AGENTS.md +160 -0
  43. package/agents/empty-state/AGENTS.md +203 -0
  44. package/agents/file-chip/AGENTS.md +79 -0
  45. package/agents/file-icon/AGENTS.md +66 -0
  46. package/agents/file-upload/AGENTS.md +168 -0
  47. package/agents/filter-chips/AGENTS.md +167 -0
  48. package/agents/flag/AGENTS.md +67 -0
  49. package/agents/funnel-chart/AGENTS.md +131 -0
  50. package/agents/highlight/AGENTS.md +119 -0
  51. package/agents/hover-card/AGENTS.md +271 -0
  52. package/agents/icon-stack/AGENTS.md +120 -0
  53. package/agents/icon-tile/AGENTS.md +94 -0
  54. package/agents/inline-diff/AGENTS.md +211 -0
  55. package/agents/input/AGENTS.md +261 -0
  56. package/agents/kanban/AGENTS.md +352 -0
  57. package/agents/kbd/AGENTS.md +160 -0
  58. package/agents/layout/AGENTS.md +351 -0
  59. package/agents/line-chart/AGENTS.md +393 -0
  60. package/agents/link/AGENTS.md +53 -0
  61. package/agents/list/AGENTS.md +392 -0
  62. package/agents/marker/AGENTS.md +142 -0
  63. package/agents/masked-text/AGENTS.md +127 -0
  64. package/agents/menubar/AGENTS.md +276 -0
  65. package/agents/multi-select/AGENTS.md +159 -0
  66. package/agents/navigation-menu/AGENTS.md +315 -0
  67. package/agents/notice-stack/AGENTS.md +101 -0
  68. package/agents/number-stepper/AGENTS.md +96 -0
  69. package/agents/otp-input/AGENTS.md +162 -0
  70. package/agents/pagination/AGENTS.md +189 -0
  71. package/agents/phone-input/AGENTS.md +131 -0
  72. package/agents/pie-chart/AGENTS.md +243 -0
  73. package/agents/popover/AGENTS.md +190 -0
  74. package/agents/progress/AGENTS.md +273 -0
  75. package/agents/progress-tracker/AGENTS.md +305 -0
  76. package/agents/rating/AGENTS.md +165 -0
  77. package/agents/relative-time/AGENTS.md +103 -0
  78. package/agents/resizable/AGENTS.md +390 -0
  79. package/agents/scaled-preview/AGENTS.md +111 -0
  80. package/agents/scatter-chart/AGENTS.md +160 -0
  81. package/agents/scroll-area/AGENTS.md +271 -0
  82. package/agents/search-input/AGENTS.md +105 -0
  83. package/agents/segmented-control/AGENTS.md +153 -0
  84. package/agents/select/AGENTS.md +159 -0
  85. package/agents/selector-button/AGENTS.md +122 -0
  86. package/agents/sidebar/AGENTS.md +201 -0
  87. package/agents/skeleton/AGENTS.md +166 -0
  88. package/agents/slider/AGENTS.md +181 -0
  89. package/agents/spark-chart/AGENTS.md +257 -0
  90. package/agents/sparkbar/AGENTS.md +92 -0
  91. package/agents/spinner/AGENTS.md +123 -0
  92. package/agents/split-view/AGENTS.md +290 -0
  93. package/agents/stat/AGENTS.md +138 -0
  94. package/agents/status-icon/AGENTS.md +243 -0
  95. package/agents/step-list/AGENTS.md +313 -0
  96. package/agents/table/AGENTS.md +286 -0
  97. package/agents/tabs/AGENTS.md +305 -0
  98. package/agents/tag-input/AGENTS.md +246 -0
  99. package/agents/time-picker/AGENTS.md +228 -0
  100. package/agents/timeline/AGENTS.md +1018 -0
  101. package/agents/toast/AGENTS.md +266 -0
  102. package/agents/toggle-group/AGENTS.md +216 -0
  103. package/agents/toolbar/AGENTS.md +277 -0
  104. package/agents/tooltip/AGENTS.md +154 -0
  105. package/agents/tracker/AGENTS.md +158 -0
  106. package/agents/tree-diagram/AGENTS.md +260 -0
  107. package/agents/tree-view/AGENTS.md +329 -0
  108. package/agents/typography/AGENTS.md +396 -0
  109. package/lib/index.js +17914 -0
  110. package/lib/style.css +2 -0
  111. package/lib/types/components/Accordion/Accordion.d.ts +57 -0
  112. package/lib/types/components/ActionBar/ActionBar.d.ts +24 -0
  113. package/lib/types/components/Alert/Alert.d.ts +36 -0
  114. package/lib/types/components/AppShell/AppShell.d.ts +38 -0
  115. package/lib/types/components/AreaChart/AreaChart.d.ts +17 -0
  116. package/lib/types/components/BarChart/BarChart.d.ts +57 -0
  117. package/lib/types/components/BarList/BarList.d.ts +56 -0
  118. package/lib/types/components/Breadcrumb/Breadcrumb.d.ts +15 -0
  119. package/lib/types/components/Calendar/Calendar.d.ts +34 -0
  120. package/lib/types/components/Calendar/DatePicker.d.ts +45 -0
  121. package/lib/types/components/Calendar/date.d.ts +32 -0
  122. package/lib/types/components/Card/Card.d.ts +64 -0
  123. package/lib/types/components/CardFrame/CardFrame.d.ts +15 -0
  124. package/lib/types/components/Chart/ChartParts.d.ts +38 -0
  125. package/lib/types/components/Chart/chart.d.ts +46 -0
  126. package/lib/types/components/Chart/useChartInteraction.d.ts +39 -0
  127. package/lib/types/components/ChatBubble/ChatBubble.d.ts +61 -0
  128. package/lib/types/components/ChatBubble/formatChatTime.d.ts +2 -0
  129. package/lib/types/components/ChatComposer/ChatComposer.d.ts +42 -0
  130. package/lib/types/components/CodeBlock/CodeBlock.d.ts +18 -0
  131. package/lib/types/components/CodeBlock/highlight.d.ts +10 -0
  132. package/lib/types/components/ColorPicker/ColorPicker.d.ts +61 -0
  133. package/lib/types/components/ColorPicker/color.d.ts +48 -0
  134. package/lib/types/components/ColorPicker/presets.d.ts +7 -0
  135. package/lib/types/components/Combobox/Combobox.d.ts +48 -0
  136. package/lib/types/components/CommandMenu/CommandMenu.d.ts +51 -0
  137. package/lib/types/components/Comment/Comment.d.ts +45 -0
  138. package/lib/types/components/ContextMenu/ContextMenu.d.ts +40 -0
  139. package/lib/types/components/DataGrid/DataGrid.d.ts +43 -0
  140. package/lib/types/components/DataGrid/GridPopover.d.ts +43 -0
  141. package/lib/types/components/DataGrid/anchor.d.ts +5 -0
  142. package/lib/types/components/DataGrid/model.d.ts +61 -0
  143. package/lib/types/components/DataGrid/panels.d.ts +63 -0
  144. package/lib/types/components/DataGrid/typeMeta.d.ts +6 -0
  145. package/lib/types/components/DataGrid/types.d.ts +77 -0
  146. package/lib/types/components/DescriptionList/DescriptionList.d.ts +77 -0
  147. package/lib/types/components/Dialog/AlertDialog.d.ts +26 -0
  148. package/lib/types/components/Dialog/Dialog.d.ts +42 -0
  149. package/lib/types/components/Dialog/modal.d.ts +38 -0
  150. package/lib/types/components/Drawer/Drawer.d.ts +62 -0
  151. package/lib/types/components/DropdownMenu/DropdownMenu.d.ts +97 -0
  152. package/lib/types/components/DropdownMenu/menuCore.d.ts +23 -0
  153. package/lib/types/components/Dropzone/Dropzone.d.ts +30 -0
  154. package/lib/types/components/EmptyState/EmptyState.d.ts +26 -0
  155. package/lib/types/components/FileChip/FileChip.d.ts +32 -0
  156. package/lib/types/components/FileUpload/FileUpload.d.ts +52 -0
  157. package/lib/types/components/FilterChips/FilterChips.d.ts +37 -0
  158. package/lib/types/components/FunnelChart/FunnelChart.d.ts +38 -0
  159. package/lib/types/components/FunnelChart/funnel.d.ts +25 -0
  160. package/lib/types/components/Highlight/Highlight.d.ts +35 -0
  161. package/lib/types/components/HoverCard/HoverCard.d.ts +47 -0
  162. package/lib/types/components/InlineDiff/InlineDiff.d.ts +36 -0
  163. package/lib/types/components/InlineDiff/diff.d.ts +42 -0
  164. package/lib/types/components/InlineDiff/useDiffReview.d.ts +57 -0
  165. package/lib/types/components/Kanban/Kanban.d.ts +64 -0
  166. package/lib/types/components/Kanban/KanbanCard.d.ts +35 -0
  167. package/lib/types/components/Kanban/move.d.ts +25 -0
  168. package/lib/types/components/LineChart/LineChart.d.ts +85 -0
  169. package/lib/types/components/List/List.d.ts +62 -0
  170. package/lib/types/components/Listbox/Listbox.d.ts +60 -0
  171. package/lib/types/components/Marker/Marker.d.ts +28 -0
  172. package/lib/types/components/Menubar/Menubar.d.ts +50 -0
  173. package/lib/types/components/MultiSelect/MultiSelect.d.ts +38 -0
  174. package/lib/types/components/NavigationMenu/NavigationMenu.d.ts +101 -0
  175. package/lib/types/components/NoticeStack/NoticeStack.d.ts +41 -0
  176. package/lib/types/components/NumberStepper/NumberStepper.d.ts +32 -0
  177. package/lib/types/components/OtpInput/OtpInput.d.ts +31 -0
  178. package/lib/types/components/Pagination/Pagination.d.ts +54 -0
  179. package/lib/types/components/Pagination/paginationRange.d.ts +7 -0
  180. package/lib/types/components/PhoneInput/PhoneInput.d.ts +47 -0
  181. package/lib/types/components/PieChart/PieChart.d.ts +67 -0
  182. package/lib/types/components/PieChart/arc.d.ts +26 -0
  183. package/lib/types/components/PieChart/useArcTween.d.ts +15 -0
  184. package/lib/types/components/Popover/Popover.d.ts +28 -0
  185. package/lib/types/components/ProgressTracker/ProgressTracker.d.ts +64 -0
  186. package/lib/types/components/Rating/Rating.d.ts +42 -0
  187. package/lib/types/components/Resizable/Resizable.d.ts +50 -0
  188. package/lib/types/components/Resizable/layout.d.ts +31 -0
  189. package/lib/types/components/ScaledPreview/ScaledPreview.d.ts +38 -0
  190. package/lib/types/components/ScatterChart/ScatterChart.d.ts +81 -0
  191. package/lib/types/components/ScatterChart/scatter.d.ts +14 -0
  192. package/lib/types/components/ScrollArea/ScrollArea.d.ts +34 -0
  193. package/lib/types/components/Select/Select.d.ts +39 -0
  194. package/lib/types/components/Sidebar/Sidebar.d.ts +39 -0
  195. package/lib/types/components/Skeleton/Skeleton.d.ts +47 -0
  196. package/lib/types/components/Slider/Slider.d.ts +36 -0
  197. package/lib/types/components/SparkChart/SparkChart.d.ts +55 -0
  198. package/lib/types/components/SparkChart/spark.d.ts +16 -0
  199. package/lib/types/components/Sparkbar/Sparkbar.d.ts +39 -0
  200. package/lib/types/components/SplitView/SplitView.d.ts +31 -0
  201. package/lib/types/components/Stat/Stat.d.ts +58 -0
  202. package/lib/types/components/StepList/StepList.d.ts +61 -0
  203. package/lib/types/components/Table/Table.d.ts +39 -0
  204. package/lib/types/components/Tabs/Tabs.d.ts +47 -0
  205. package/lib/types/components/TagInput/TagInput.d.ts +46 -0
  206. package/lib/types/components/TimePicker/TimePicker.d.ts +43 -0
  207. package/lib/types/components/TimePicker/time.d.ts +15 -0
  208. package/lib/types/components/Timeline/Timeline.d.ts +154 -0
  209. package/lib/types/components/Timeline/scale.d.ts +22 -0
  210. package/lib/types/components/Toast/Toaster.d.ts +19 -0
  211. package/lib/types/components/Toast/index.d.ts +4 -0
  212. package/lib/types/components/Toast/store.d.ts +75 -0
  213. package/lib/types/components/ToggleGroup/ToggleGroup.d.ts +28 -0
  214. package/lib/types/components/Toolbar/Toolbar.d.ts +70 -0
  215. package/lib/types/components/Tracker/Tracker.d.ts +43 -0
  216. package/lib/types/components/TreeDiagram/TreeDiagram.d.ts +68 -0
  217. package/lib/types/components/TreeView/TreeView.d.ts +107 -0
  218. package/lib/types/components/index.d.ts +186 -0
  219. package/lib/types/hooks/index.d.ts +17 -0
  220. package/lib/types/hooks/timing.d.ts +29 -0
  221. package/lib/types/hooks/useControllableState.d.ts +5 -0
  222. package/lib/types/hooks/useCountdown.d.ts +19 -0
  223. package/lib/types/hooks/useDigitInput.d.ts +24 -0
  224. package/lib/types/hooks/useDismiss.d.ts +3 -0
  225. package/lib/types/hooks/useElapsed.d.ts +28 -0
  226. package/lib/types/hooks/useFloating.d.ts +21 -0
  227. package/lib/types/hooks/useFocusTrap.d.ts +7 -0
  228. package/lib/types/hooks/usePresence.d.ts +8 -0
  229. package/lib/types/hooks/usePressToOpen.d.ts +20 -0
  230. package/lib/types/hooks/useScrollLock.d.ts +2 -0
  231. package/lib/types/hooks/useSelection.d.ts +29 -0
  232. package/lib/types/index.d.ts +5 -0
  233. package/lib/types/primitives/AspectRatio.d.ts +14 -0
  234. package/lib/types/primitives/BrandIcon.d.ts +12 -0
  235. package/lib/types/primitives/ColorDot.d.ts +20 -0
  236. package/lib/types/primitives/CopyButton.d.ts +12 -0
  237. package/lib/types/primitives/CountBadge.d.ts +28 -0
  238. package/lib/types/primitives/Countdown.d.ts +21 -0
  239. package/lib/types/primitives/Delta.d.ts +33 -0
  240. package/lib/types/primitives/Elapsed.d.ts +23 -0
  241. package/lib/types/primitives/FileIcon.d.ts +17 -0
  242. package/lib/types/primitives/Flag.d.ts +15 -0
  243. package/lib/types/primitives/IconStack.d.ts +27 -0
  244. package/lib/types/primitives/IconTile.d.ts +24 -0
  245. package/lib/types/primitives/Kbd.d.ts +19 -0
  246. package/lib/types/primitives/Layout.d.ts +64 -0
  247. package/lib/types/primitives/Link.d.ts +12 -0
  248. package/lib/types/primitives/MaskedText.d.ts +28 -0
  249. package/lib/types/primitives/Progress.d.ts +52 -0
  250. package/lib/types/primitives/RelativeTime.d.ts +22 -0
  251. package/lib/types/primitives/SearchInput.d.ts +8 -0
  252. package/lib/types/primitives/SelectorButton.d.ts +23 -0
  253. package/lib/types/primitives/Spinner.d.ts +15 -0
  254. package/lib/types/primitives/StatusIcon.d.ts +40 -0
  255. package/lib/types/primitives/Swatch.d.ts +11 -0
  256. package/lib/types/primitives/Tooltip.d.ts +17 -0
  257. package/lib/types/primitives/Typography.d.ts +48 -0
  258. package/lib/types/primitives/formatCountdown.d.ts +2 -0
  259. package/lib/types/primitives/formatElapsed.d.ts +8 -0
  260. package/lib/types/primitives/formatRelativeTime.d.ts +16 -0
  261. package/lib/types/primitives/hotkey.d.ts +10 -0
  262. package/lib/types/primitives/index.d.ts +188 -0
  263. package/lib/types/primitives/maskValue.d.ts +24 -0
  264. package/package.json +54 -4
@@ -0,0 +1,286 @@
1
+ # Table
2
+
3
+ > 데이터 표시 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
4
+
5
+ 격자선 없는 데이터 표. 머리글은 작은 회색 글씨로, 행은 선 대신 옅은 채움으로 구분해요. 열마다 render로 셀을 직접 그리고, 숫자 열은 자릿수가 맞게 정렬해요. 가로로 넘치면 표만 스크롤돼요.
6
+
7
+ ```tsx
8
+ import { Table, type TableColumn } from '@iamfiro/design'
9
+ ```
10
+
11
+ ## 디자인 규칙
12
+
13
+ - 차트에서 한 칸을 고르는 화면(피벗)은 `BarChart selected` + `Table selectedKey`를 같은 상태로 묶는다. 고르지 않은 칸은 옅은 색, 고른 칸의 x축 아이콘(`xIcon`)은 흰 칩으로 펼친다. 임계치·이상 구간은 `LineChart referenceLines` · `bands`, 실시간 데이터는 `streaming`(모양 변형 대신 한 칸 미끄러짐) + `endDot="pulse"`.
14
+ - 표: 보여 주기만 하면 `Table`, 편집·정렬·필터·범위 선택이 필요하면 `DataGrid`. 셀 편집·키 입력·클립보드·한글 IME는 셀 위에 놓인 textarea 하나가 모두 받는다(조합 중에 포커스를 옮기면 한글이 깨진다).
15
+
16
+ ## API
17
+
18
+ ### Table
19
+
20
+ | prop | 타입 | 기본값 | 설명 |
21
+ | --- | --- | --- | --- |
22
+ | `columns` (필수) | `TableColumn<Row>[]` | | 열 정의. |
23
+ | `rows` (필수) | `Row[]` | | 데이터 행. |
24
+ | `rowKey` | `(row, index) => string \| number` | | 행마다 고유한 key. 생략하면 index. |
25
+ | `density` | `'default' \| 'compact'` | `'default'` | 행 높이. |
26
+ | `striped` | `boolean` | `true` | 행을 번갈아 채워 구분해요. |
27
+ | `headerVariant` | `'label' \| 'overline'` | `'label'` | 머리글 모양. overline은 작은 대문자. |
28
+ | `selectedKey` | `string \| number \| null` | | rowKey가 같은 행을 옅은 회색으로 채워요. |
29
+ | `onRowClick` | `(row, index) => void` | | 행을 누를 때(클릭 · Enter/Space). 주면 hover 채움과 포커스가 생겨요. |
30
+ | `empty` | `ReactNode` | | 행이 없을 때 보여 줄 내용. |
31
+ | `caption` | `ReactNode` | | 표 위 작은 제목. |
32
+ | `className / style` | `string / CSSProperties` | | 스크롤 래퍼에 적용돼요. |
33
+
34
+ ### TableColumn
35
+
36
+ | prop | 타입 | 기본값 | 설명 |
37
+ | --- | --- | --- | --- |
38
+ | `key` (필수) | `string` | | 열 id. render가 없으면 row[key]를 보여 줘요. |
39
+ | `header` (필수) | `ReactNode` | | 머리글. |
40
+ | `headerLeading` | `ReactNode` | | 머리글 앞 요소(시리즈 열의 ColorDot). |
41
+ | `render` | `(row, index) => ReactNode` | | 셀 내용을 직접 그릴 때. |
42
+ | `leading` | `(row, index) => ReactNode` | | 셀 내용 앞 요소(첫 열 아이콘). |
43
+ | `width` | `number \| string` | | 열 너비. |
44
+ | `align` | `'start' \| 'center' \| 'end'` | `numeric ? 'end' : 'start'` | 머리글과 셀 정렬. |
45
+ | `numeric` | `boolean` | | 숫자 열. 자릿수가 맞는 tabular 숫자에 기본 오른쪽 정렬. |
46
+
47
+ ## 예제
48
+
49
+ 예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
50
+
51
+ ### 기본
52
+
53
+ columns와 rows를 넘겨요. render가 없으면 row[key]를 그대로 보여 주고, numeric 열은 tabular 숫자로, align="end"면 오른쪽 정렬돼요.
54
+
55
+ ```tsx
56
+ import { Avatar, Badge, Text, type Tone, Table, type TableColumn } from '@iamfiro/design'
57
+
58
+ interface Member {
59
+ id: string
60
+ name: string
61
+ email: string
62
+ role: string
63
+ status: 'active' | 'invited' | 'suspended'
64
+ lastSeen: string
65
+ storage: number
66
+ }
67
+
68
+ const MEMBERS: Member[] = [
69
+ { id: 'u1', name: '김민지', email: 'minji@lumen.app', role: '관리자', status: 'active', lastSeen: '2분 전', storage: 12.4 },
70
+ { id: 'u2', name: '이도현', email: 'dohyun@lumen.app', role: '편집자', status: 'active', lastSeen: '1시간 전', storage: 8.15 },
71
+ { id: 'u3', name: '박서연', email: 'seoyeon@lumen.app', role: '편집자', status: 'invited', lastSeen: '—', storage: 0 },
72
+ { id: 'u4', name: '최준호', email: 'junho@lumen.app', role: '뷰어', status: 'suspended', lastSeen: '10월 3일', storage: 1.02 },
73
+ ]
74
+
75
+ const STATUS: Record<Member['status'], { label: string; tone: Tone }> = {
76
+ active: { label: '활성', tone: 'success' },
77
+ invited: { label: '초대됨', tone: 'info' },
78
+ suspended: { label: '정지', tone: 'danger' },
79
+ }
80
+
81
+ const COLUMNS: TableColumn<Member>[] = [
82
+ {
83
+ key: 'name',
84
+ header: '멤버',
85
+ render: (m) => (
86
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
87
+ <Avatar name={m.name} />
88
+ <div>
89
+ <Text variant="label-md" as="div">
90
+ {m.name}
91
+ </Text>
92
+ <Text variant="caption" tone="tertiary" as="div">
93
+ {m.email}
94
+ </Text>
95
+ </div>
96
+ </div>
97
+ ),
98
+ },
99
+ { key: 'role', header: '역할' },
100
+ {
101
+ key: 'status',
102
+ header: '상태',
103
+ render: (m) => (
104
+ <Badge tone={STATUS[m.status].tone} dot>
105
+ {STATUS[m.status].label}
106
+ </Badge>
107
+ ),
108
+ },
109
+ { key: 'lastSeen', header: '최근 활동' },
110
+ { key: 'storage', header: '사용량', align: 'end', numeric: true, render: (m) => `${m.storage.toFixed(2)} GB` },
111
+ ]
112
+
113
+ export default function Example() {
114
+ return <Table caption="멤버 4명" columns={COLUMNS} rows={MEMBERS} rowKey={(m) => m.id} style={{ width: 640 }} />
115
+ }
116
+ ```
117
+
118
+ ### 간격 · 줄무늬
119
+
120
+ density="compact"는 행 높이를 줄여요. striped를 끄면 행 구분 채움이 사라져요.
121
+
122
+ ```tsx
123
+ import { useState } from 'react'
124
+ import { SegmentedControl, Switch, Table, type TableColumn } from '@iamfiro/design'
125
+
126
+ interface Invoice {
127
+ id: string
128
+ date: string
129
+ plan: string
130
+ seats: number
131
+ amount: number
132
+ }
133
+
134
+ const INVOICES: Invoice[] = [
135
+ { id: 'INV-2610', date: '10월 1일', plan: '팀', seats: 12, amount: 180000 },
136
+ { id: 'INV-2609', date: '9월 1일', plan: '팀', seats: 10, amount: 150000 },
137
+ { id: 'INV-2608', date: '8월 1일', plan: '팀', seats: 10, amount: 150000 },
138
+ { id: 'INV-2607', date: '7월 1일', plan: '스타터', seats: 4, amount: 36000 },
139
+ { id: 'INV-2606', date: '6월 1일', plan: '스타터', seats: 3, amount: 27000 },
140
+ ]
141
+
142
+ const COLUMNS: TableColumn<Invoice>[] = [
143
+ { key: 'id', header: '청구서' },
144
+ { key: 'date', header: '발행일' },
145
+ { key: 'plan', header: '플랜' },
146
+ { key: 'seats', header: '좌석', align: 'end', numeric: true },
147
+ { key: 'amount', header: '금액', align: 'end', numeric: true, render: (r) => `${r.amount.toLocaleString('ko-KR')}원` },
148
+ ]
149
+
150
+ export default function Example() {
151
+ const [density, setDensity] = useState<'default' | 'compact'>('default')
152
+ const [striped, setStriped] = useState(true)
153
+
154
+ return (
155
+ <div style={{ display: 'grid', gap: 16, width: 560 }}>
156
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
157
+ <SegmentedControl
158
+ value={density}
159
+ onChange={setDensity}
160
+ ariaLabel="행 간격"
161
+ options={[
162
+ { value: 'default', label: '기본' },
163
+ { value: 'compact', label: '촘촘하게' },
164
+ ]}
165
+ />
166
+ <Switch label="줄무늬" checked={striped} onChange={(e) => setStriped(e.target.checked)} />
167
+ </div>
168
+ <Table columns={COLUMNS} rows={INVOICES} rowKey={(r) => r.id} density={density} striped={striped} />
169
+ </div>
170
+ )
171
+ }
172
+ ```
173
+
174
+ ### 빈 상태
175
+
176
+ 행이 없으면 empty에 넣은 내용(보통 EmptyState)을 표 너비 전체에 보여 줘요.
177
+
178
+ ```tsx
179
+ import { useState } from 'react'
180
+ import { SearchX } from 'lucide-react'
181
+ import { Button, SearchInput, EmptyState, Table, type TableColumn } from '@iamfiro/design'
182
+
183
+ interface Project {
184
+ name: string
185
+ owner: string
186
+ updated: string
187
+ }
188
+
189
+ const PROJECTS: Project[] = [
190
+ { name: '웹사이트 리뉴얼', owner: '김민지', updated: '2분 전' },
191
+ { name: '모바일 앱 2.0', owner: '이도현', updated: '어제' },
192
+ { name: '디자인 시스템', owner: '박서연', updated: '10월 21일' },
193
+ ]
194
+
195
+ const COLUMNS: TableColumn<Project>[] = [
196
+ { key: 'name', header: '프로젝트' },
197
+ { key: 'owner', header: '담당자' },
198
+ { key: 'updated', header: '수정', align: 'end' },
199
+ ]
200
+
201
+ export default function Example() {
202
+ const [query, setQuery] = useState('결제')
203
+ const rows = PROJECTS.filter((p) => p.name.includes(query.trim()))
204
+
205
+ return (
206
+ <div style={{ display: 'grid', gap: 16, width: 520 }}>
207
+ <SearchInput size="sm" placeholder="프로젝트 검색" value={query} onChange={(e) => setQuery(e.target.value)} aria-label="프로젝트 검색" />
208
+ <Table
209
+ columns={COLUMNS}
210
+ rows={rows}
211
+ rowKey={(p) => p.name}
212
+ empty={
213
+ <EmptyState
214
+ size="sm"
215
+ icon={<SearchX />}
216
+ title="검색 결과가 없어요"
217
+ description={`‘${query}’와 일치하는 프로젝트가 없어요.`}
218
+ actions={
219
+ <Button size="sm" onClick={() => setQuery('')}>
220
+ 검색어 지우기
221
+ </Button>
222
+ }
223
+ />
224
+ }
225
+ />
226
+ </div>
227
+ )
228
+ }
229
+ ```
230
+
231
+ ### 행 선택 · 오버라인 머리글
232
+
233
+ onRowClick을 주면 행에 hover 채움이 생기고 Tab · Enter/Space로도 눌러요. selectedKey와 같은 행은 옅은 회색으로 채워요. headerVariant="overline"은 작은 대문자 머리글, headerLeading은 머리글 앞 ColorDot, leading은 첫 열 아이콘이에요. numeric 열은 오른쪽 정렬 + tabular 숫자예요. 차트와 연동하는 예는 BarChart의 "표와 연동"을 보세요.
234
+
235
+ ```tsx
236
+ import { useState, type ReactNode } from 'react'
237
+ import { CreditCard, Globe, Search, Server, Smartphone } from 'lucide-react'
238
+ import { Table, type TableColumn, ColorDot, Stack, Text } from '@iamfiro/design'
239
+
240
+ interface Service {
241
+ id: string
242
+ name: string
243
+ icon: ReactNode
244
+ success: number
245
+ rollback: number
246
+ failed: number
247
+ }
248
+
249
+ const SERVICES: Service[] = [
250
+ { id: 'web', name: '웹', icon: <Globe size={16} color="var(--text-3)" />, success: 42, rollback: 3, failed: 2 },
251
+ { id: 'mobile', name: '모바일', icon: <Smartphone size={16} color="var(--text-3)" />, success: 18, rollback: 1, failed: 1 },
252
+ { id: 'api', name: 'API', icon: <Server size={16} color="var(--text-3)" />, success: 57, rollback: 5, failed: 4 },
253
+ { id: 'billing', name: '결제', icon: <CreditCard size={16} color="var(--text-3)" />, success: 12, rollback: 2, failed: 0 },
254
+ { id: 'search', name: '검색', icon: <Search size={16} color="var(--text-3)" />, success: 25, rollback: 0, failed: 1 },
255
+ ]
256
+
257
+ const COLUMNS: TableColumn<Service>[] = [
258
+ { key: 'name', header: '서비스', leading: (s) => s.icon },
259
+ { key: 'total', header: '배포', numeric: true, render: (s) => s.success + s.rollback + s.failed },
260
+ { key: 'success', header: '성공', headerLeading: <ColorDot color="--chart-3" />, numeric: true },
261
+ { key: 'rollback', header: '롤백', headerLeading: <ColorDot color="--warning" />, numeric: true },
262
+ { key: 'failed', header: '실패', headerLeading: <ColorDot color="--danger" />, numeric: true },
263
+ ]
264
+
265
+ export default function Example() {
266
+ const [selected, setSelected] = useState<string | null>('api')
267
+ const service = SERVICES.find((s) => s.id === selected)
268
+
269
+ return (
270
+ <Stack gap={3} style={{ width: 560 }}>
271
+ <Table
272
+ columns={COLUMNS}
273
+ rows={SERVICES}
274
+ rowKey={(s) => s.id}
275
+ headerVariant="overline"
276
+ striped={false}
277
+ selectedKey={selected}
278
+ onRowClick={(s) => setSelected((cur) => (cur === s.id ? null : s.id))}
279
+ />
280
+ <Text variant="body-sm" tone="secondary">
281
+ {service ? `${service.name} 서비스를 골랐어요. 다시 누르면 선택이 풀려요.` : '행을 누르거나 Tab으로 옮겨 Enter를 눌러 보세요.'}
282
+ </Text>
283
+ </Stack>
284
+ )
285
+ }
286
+ ```
@@ -0,0 +1,305 @@
1
+ # Tabs
2
+
3
+ > 내비게이션 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
4
+
5
+ 같은 자리에서 여러 패널을 바꿔 보여줘요. Tabs 안에 TabList · Tab · TabPanel을 조합하고, 선택 표시는 탭 사이를 미끄러지듯 이동해요. ← → · Home / End로 탭을 옮기고, 무거운 패널은 manual 활성화로 Enter / Space를 누를 때만 바꿔요.
6
+
7
+ ```tsx
8
+ import { Tab, TabList, TabPanel, Tabs } from '@iamfiro/design'
9
+ ```
10
+
11
+ ## 디자인 규칙
12
+
13
+ - 탭은 `Tabs`(패널 연결). `SegmentedControl`은 같은 화면 안의 보기 전환에만.
14
+
15
+ ## API
16
+
17
+ ### Tabs
18
+
19
+ | prop | 타입 | 기본값 | 설명 |
20
+ | --- | --- | --- | --- |
21
+ | `value` | `string` | | 선택된 탭(제어). |
22
+ | `defaultValue` | `string` | `''` | 처음 선택된 탭(비제어). |
23
+ | `onValueChange` | `(value: string) => void` | | 선택이 바뀔 때. |
24
+ | `activation` | `'auto' \| 'manual'` | `'auto'` | auto: 화살표로 옮기면 바로 선택 · manual: 화살표는 포커스만, Enter / Space로 선택. |
25
+ | `className` | `string` | | 바깥 요소에 전달. |
26
+
27
+ ### TabList
28
+
29
+ Tabs.List로도 쓸 수 있어요.
30
+
31
+ | prop | 타입 | 기본값 | 설명 |
32
+ | --- | --- | --- | --- |
33
+ | `variant` | `'underline' \| 'pill'` | `'underline'` | underline: 선택 탭 아래 선 · pill: 회색 트랙 위 흰 칩. |
34
+ | `size` | `'sm' \| 'md'` | `'md'` | 탭 크기. |
35
+ | `fitted` | `boolean` | | 탭이 줄 전체를 같은 폭으로 채워요. |
36
+ | `aria-label` | `string` | | 탭 목록의 접근성 이름. |
37
+ | `className` | `string` | | tablist 요소에 전달. |
38
+
39
+ ### Tab
40
+
41
+ Tabs.Tab으로도 쓸 수 있어요.
42
+
43
+ | prop | 타입 | 기본값 | 설명 |
44
+ | --- | --- | --- | --- |
45
+ | `value` (필수) | `string` | | 이 탭의 값. 같은 value의 TabPanel과 연결돼요. |
46
+ | `children` (필수) | `ReactNode` | | 탭 이름. |
47
+ | `icon` | `ReactNode` | | 이름 앞 아이콘. |
48
+ | `count` | `number` | | 이름 뒤 작은 숫자. |
49
+ | `disabled` | `boolean` | | 비활성. 키보드 이동에서도 건너뛰어요. |
50
+
51
+ ### TabPanel
52
+
53
+ Tabs.Panel로도 쓸 수 있어요.
54
+
55
+ | prop | 타입 | 기본값 | 설명 |
56
+ | --- | --- | --- | --- |
57
+ | `value` (필수) | `string` | | 연결할 Tab의 value. |
58
+ | `children` (필수) | `ReactNode` | | 패널 내용. |
59
+ | `keepMounted` | `boolean` | | 숨겨져도 DOM에 남겨 폼 상태·스크롤을 유지해요. |
60
+ | `className` | `string` | | tabpanel 요소에 전달. |
61
+
62
+ ## 예제
63
+
64
+ 예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
65
+
66
+ ### 기본 (underline)
67
+
68
+ 선택된 탭 아래 선이 움직여요. value를 생략하면 defaultValue로 비제어 사용.
69
+
70
+ ```tsx
71
+ import { Box, Stack, Text, Tab, TabList, TabPanel, Tabs } from '@iamfiro/design'
72
+
73
+ export default function Example() {
74
+ return (
75
+ <Tabs defaultValue="overview">
76
+ <Box style={{ width: 480 }}>
77
+ <TabList aria-label="프로젝트">
78
+ <Tab value="overview">개요</Tab>
79
+ <Tab value="activity">활동</Tab>
80
+ <Tab value="settings">설정</Tab>
81
+ </TabList>
82
+ <TabPanel value="overview">
83
+ <Stack gap={1}>
84
+ <Text variant="label-md">Lumen 리디자인</Text>
85
+ <Text variant="body-sm" tone="secondary">
86
+ 10월 31일 출시를 목표로 디자인 시스템과 랜딩 페이지를 새로 만들고 있어요.
87
+ </Text>
88
+ </Stack>
89
+ </TabPanel>
90
+ <TabPanel value="activity">
91
+ <Text variant="body-sm" tone="secondary">
92
+ 이도현님이 2분 전에 ‘버튼 상태 정리’ 작업을 완료했어요.
93
+ </Text>
94
+ </TabPanel>
95
+ <TabPanel value="settings">
96
+ <Text variant="body-sm" tone="secondary">
97
+ 프로젝트 이름, 공개 범위, 보관 설정을 바꿀 수 있어요.
98
+ </Text>
99
+ </TabPanel>
100
+ </Box>
101
+ </Tabs>
102
+ )
103
+ }
104
+ ```
105
+
106
+ ### pill
107
+
108
+ 회색 트랙 위 흰 칩. 화면 안의 작은 보기 전환에 어울려요. size="sm"으로 더 작게.
109
+
110
+ ```tsx
111
+ import { Box, Stack, Text, Tab, TabList, TabPanel, Tabs } from '@iamfiro/design'
112
+
113
+ const RANGES = [
114
+ { value: 'day', label: '오늘', visitors: '1,284' },
115
+ { value: 'week', label: '이번 주', visitors: '8,903' },
116
+ { value: 'month', label: '이번 달', visitors: '32,410' },
117
+ ]
118
+
119
+ export default function Example() {
120
+ return (
121
+ <Stack gap={8} align="start">
122
+ <Tabs defaultValue="week">
123
+ <Box>
124
+ <TabList variant="pill" aria-label="기간">
125
+ {RANGES.map((r) => (
126
+ <Tab key={r.value} value={r.value}>
127
+ {r.label}
128
+ </Tab>
129
+ ))}
130
+ </TabList>
131
+ {RANGES.map((r) => (
132
+ <TabPanel key={r.value} value={r.value}>
133
+ <Stack gap={1}>
134
+ <Text variant="caption" tone="tertiary">
135
+ {r.label} 방문자
136
+ </Text>
137
+ <Text variant="number-lg">{r.visitors}</Text>
138
+ </Stack>
139
+ </TabPanel>
140
+ ))}
141
+ </Box>
142
+ </Tabs>
143
+ <Tabs defaultValue="code">
144
+ <TabList variant="pill" size="sm" aria-label="보기">
145
+ <Tab value="preview">미리 보기</Tab>
146
+ <Tab value="code">코드</Tab>
147
+ </TabList>
148
+ </Tabs>
149
+ </Stack>
150
+ )
151
+ }
152
+ ```
153
+
154
+ ### 아이콘과 개수
155
+
156
+ icon과 count로 탭 옆에 아이콘·숫자를 붙여요. disabled 탭은 키보드 이동에서도 건너뛰어요.
157
+
158
+ ```tsx
159
+ import { Archive, CircleDot, GitPullRequest, Inbox } from 'lucide-react'
160
+ import { Box, Text, Tab, TabList, TabPanel, Tabs } from '@iamfiro/design'
161
+
162
+ export default function Example() {
163
+ return (
164
+ <Tabs defaultValue="inbox">
165
+ <Box style={{ width: 520 }}>
166
+ <TabList aria-label="받은 항목">
167
+ <Tab value="inbox" icon={<Inbox />} count={12}>
168
+ 받은 편지함
169
+ </Tab>
170
+ <Tab value="issues" icon={<CircleDot />} count={4}>
171
+ 이슈
172
+ </Tab>
173
+ <Tab value="reviews" icon={<GitPullRequest />} count={2}>
174
+ 리뷰 요청
175
+ </Tab>
176
+ <Tab value="archive" icon={<Archive />} disabled>
177
+ 보관함
178
+ </Tab>
179
+ </TabList>
180
+ <TabPanel value="inbox">
181
+ <Text variant="body-sm" tone="secondary">
182
+ 읽지 않은 알림 12개가 있어요.
183
+ </Text>
184
+ </TabPanel>
185
+ <TabPanel value="issues">
186
+ <Text variant="body-sm" tone="secondary">
187
+ 나에게 배정된 열린 이슈 4개.
188
+ </Text>
189
+ </TabPanel>
190
+ <TabPanel value="reviews">
191
+ <Text variant="body-sm" tone="secondary">
192
+ 김민지님과 박서연님이 리뷰를 요청했어요.
193
+ </Text>
194
+ </TabPanel>
195
+ </Box>
196
+ </Tabs>
197
+ )
198
+ }
199
+ ```
200
+
201
+ ### 꽉 채우기
202
+
203
+ fitted를 주면 탭이 줄 전체를 같은 폭으로 나눠 가져요.
204
+
205
+ ```tsx
206
+ import { Stack, Tab, TabList, Tabs } from '@iamfiro/design'
207
+
208
+ export default function Example() {
209
+ return (
210
+ <Stack gap={6} style={{ width: 400 }}>
211
+ <Tabs defaultValue="login">
212
+ <TabList fitted aria-label="계정">
213
+ <Tab value="login">로그인</Tab>
214
+ <Tab value="signup">회원가입</Tab>
215
+ </TabList>
216
+ </Tabs>
217
+ <Tabs defaultValue="monthly">
218
+ <TabList variant="pill" fitted aria-label="결제 주기">
219
+ <Tab value="monthly">월간</Tab>
220
+ <Tab value="yearly">연간</Tab>
221
+ <Tab value="custom">맞춤</Tab>
222
+ </TabList>
223
+ </Tabs>
224
+ </Stack>
225
+ )
226
+ }
227
+ ```
228
+
229
+ ### 수동 활성화
230
+
231
+ activation="manual"이면 화살표는 포커스만 옮기고 Enter / Space로 선택해요. 패널을 그리는 비용이 클 때. 제어(value / onValueChange) 예시이기도 해요.
232
+
233
+ ```tsx
234
+ import { useState } from 'react'
235
+ import { Box, Kbd, Stack, Text, Tab, TabList, TabPanel, Tabs } from '@iamfiro/design'
236
+
237
+ const REPORTS = [
238
+ { value: 'sales', label: '매출', summary: '10월 매출은 지난달보다 12% 늘었어요.' },
239
+ { value: 'users', label: '사용자', summary: '주간 활성 사용자는 8,903명이에요.' },
240
+ { value: 'retention', label: '리텐션', summary: '4주 리텐션은 41%예요.' },
241
+ ]
242
+
243
+ export default function Example() {
244
+ const [report, setReport] = useState('sales')
245
+ return (
246
+ <Tabs value={report} onValueChange={setReport} activation="manual">
247
+ <Stack gap={5} style={{ width: 460 }}>
248
+ <Box>
249
+ <TabList aria-label="리포트">
250
+ {REPORTS.map((r) => (
251
+ <Tab key={r.value} value={r.value}>
252
+ {r.label}
253
+ </Tab>
254
+ ))}
255
+ </TabList>
256
+ {REPORTS.map((r) => (
257
+ <TabPanel key={r.value} value={r.value}>
258
+ <Text variant="body-sm">{r.summary}</Text>
259
+ </TabPanel>
260
+ ))}
261
+ </Box>
262
+ <Text variant="caption" tone="tertiary">
263
+ <Kbd>←</Kbd> <Kbd>→</Kbd>로 포커스를 옮기고 <Kbd>Enter</Kbd>로 열어요.
264
+ </Text>
265
+ </Stack>
266
+ </Tabs>
267
+ )
268
+ }
269
+ ```
270
+
271
+ ### 숨겨도 상태 유지
272
+
273
+ keepMounted 패널은 숨겨져도 DOM에 남아서, 탭을 오가도 입력한 내용이 지워지지 않아요.
274
+
275
+ ```tsx
276
+ import { Box, Field, Input, Stack, Textarea, Tab, TabList, TabPanel, Tabs } from '@iamfiro/design'
277
+
278
+ export default function Example() {
279
+ return (
280
+ <Tabs defaultValue="basic">
281
+ <Box style={{ width: 440 }}>
282
+ <TabList aria-label="새 일정">
283
+ <Tab value="basic">기본 정보</Tab>
284
+ <Tab value="detail">상세 설명</Tab>
285
+ </TabList>
286
+ <TabPanel value="basic" keepMounted>
287
+ <Stack gap={4}>
288
+ <Field label="일정 이름" htmlFor="event-name">
289
+ <Input id="event-name" placeholder="예: 디자인 리뷰" />
290
+ </Field>
291
+ <Field label="날짜" htmlFor="event-date">
292
+ <Input id="event-date" placeholder="10월 25일" />
293
+ </Field>
294
+ </Stack>
295
+ </TabPanel>
296
+ <TabPanel value="detail" keepMounted>
297
+ <Field label="설명" htmlFor="event-desc">
298
+ <Textarea id="event-desc" rows={4} placeholder="탭을 바꿔도 입력한 내용이 남아 있어요." />
299
+ </Field>
300
+ </TabPanel>
301
+ </Box>
302
+ </Tabs>
303
+ )
304
+ }
305
+ ```