@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,260 @@
1
+ # TreeDiagram
2
+
3
+ > 데이터 표시 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
4
+
5
+ 조직도 · 작업 분해처럼 위계를 상자와 연결선으로 그려요. 루트 아래 자식이 한 줄로 놓이고, 깊은 단계는 부모 아래 들여쓴 목록으로 매달 수 있어요. 연결선은 실제 노드 상자를 재서 그리기 때문에 상자 크기가 바뀌거나 접고 펼 때도 따라가요. 노드 모양은 renderNode로 직접(보통 Card) 그리고, 넓어지면 가로로 스크롤돼요.
6
+
7
+ ```tsx
8
+ import { TreeDiagram, TreeDiagramToggle } from '@iamfiro/design'
9
+ ```
10
+
11
+ ## 디자인 규칙
12
+
13
+ - 위계(부모 → 자식): 들여쓴 행이면 `TreeView`(WAI-ARIA tree, roving tabindex, typeahead는 `TIMING.typeahead`), 상자 + 연결선이면 `TreeDiagram`(노드는 `renderNode`로 Card 조립, 접기 토글은 `state.toggle`).
14
+
15
+ ## API
16
+
17
+ ### TreeDiagram
18
+
19
+ | prop | 타입 | 기본값 | 설명 |
20
+ | --- | --- | --- | --- |
21
+ | `items` (필수) | `TreeDiagramNode<T>[]` | | 루트 노드(보통 하나). 여러 개면 나란히 놓여요. |
22
+ | `renderNode` (필수) | `(node, state: TreeDiagramNodeState) => ReactNode` | | 노드 상자. 보통 Card + Avatar · Text · Badge와 state.toggle. |
23
+ | `collapsed / defaultCollapsed` | `string[]` | | 접힌 노드 id(제어 / 비제어). |
24
+ | `onCollapsedChange` | `(ids: string[]) => void` | | 접힘이 바뀔 때. |
25
+ | `indentFrom` | `number` | | 이 깊이 이상인 노드는 하위를 들여쓴 목록으로(0 = 루트). 노드의 childLayout이 우선해요. |
26
+ | `gap` | `Space` | `4` | 한 줄에 놓인 형제 사이 간격. 들여쓴 목록은 그 절반. |
27
+ | `levelGap` | `Space` | `8` | 부모와 아래 줄 사이 세로 간격. |
28
+ | `indent` | `Space` | `8` | 들여쓴 목록이 부모보다 오른쪽으로 들어가는 폭. |
29
+ | `nodeWidth` | `number \| string` | | 모든 노드 상자의 고정 폭. 줄이 반듯해져요. |
30
+ | `aria-label` | `string` | | 다이어그램의 접근성 이름. |
31
+ | `className / style` | `string / CSSProperties` | | 스크롤되는 바깥 요소에 전달. |
32
+
33
+ ### TreeDiagramNode<T>
34
+
35
+ | prop | 타입 | 기본값 | 설명 |
36
+ | --- | --- | --- | --- |
37
+ | `id` (필수) | `string` | | 다이어그램 전체에서 고유. |
38
+ | `data` | `T` | | renderNode에서 쓸 값. |
39
+ | `children` | `TreeDiagramNode<T>[]` | | 하위 노드. |
40
+ | `childLayout` | `'row' \| 'indented'` | | 하위 배치. row: 한 단계 아래 한 줄로(가로 연결선) · indented: 아래로 들여쓴 목록(왼쪽 꺾은 연결선). indented 노드의 하위도 기본으로 indented. |
41
+
42
+ ### TreeDiagramNodeState
43
+
44
+ renderNode의 두 번째 인자.
45
+
46
+ | prop | 타입 | 기본값 | 설명 |
47
+ | --- | --- | --- | --- |
48
+ | `depth` | `number` | | 깊이(루트 0). |
49
+ | `childCount` | `number` | | 직계 하위 수. |
50
+ | `collapsed / setCollapsed` | `boolean / (next: boolean) => void` | | 이 노드의 접힘 상태와 변경 함수. |
51
+ | `toggle` | `ReactNode` | | 이 노드에 연결된 "3 ⌄" 토글. 하위가 없으면 null. |
52
+ | `childLayout` | `'row' \| 'indented'` | | 이 노드의 하위 배치. |
53
+
54
+ ### TreeDiagramToggle
55
+
56
+ 하위 수 + 셰브런. state.toggle로 충분하지 않을 때 직접 배치해요. 눌러도 감싼 카드의 클릭으로 번지지 않아요.
57
+
58
+ | prop | 타입 | 기본값 | 설명 |
59
+ | --- | --- | --- | --- |
60
+ | `count` (필수) | `number` | | 하위 수. |
61
+ | `collapsed` (필수) | `boolean` | | 접힘 상태(셰브런 방향 · aria-expanded). |
62
+ | `onToggle` (필수) | `() => void` | | 눌렀을 때. |
63
+ | `aria-controls / aria-label` | `string` | | 접히는 영역 id / 접근성 이름(기본 "하위 3개 접기"). |
64
+
65
+ ## 예제
66
+
67
+ 예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
68
+
69
+ ### 조직도
70
+
71
+ 대표 아래 팀장이 한 줄로, indentFrom={1}로 팀원은 팀장 아래 목록으로 매달려요. state.toggle("3 ⌄")을 CardHeader actions에 넣으면 하위를 접고 펴요. defaultCollapsed로 처음부터 접어 둘 수 있어요.
72
+
73
+ ```tsx
74
+ import { Avatar, Inline, Stack, Text, Card, CardHeader, TreeDiagram, type TreeDiagramNode } from '@iamfiro/design'
75
+
76
+ interface Person {
77
+ name: string
78
+ role: string
79
+ }
80
+
81
+ const p = (id: string, name: string, role: string, children?: TreeDiagramNode<Person>[]): TreeDiagramNode<Person> => ({ id, data: { name, role }, children })
82
+
83
+ const ORG = [
84
+ p('ceo', '최유진', '대표', [
85
+ p('design', '김민지', '디자인 팀장', [p('d1', '박서연', '프로덕트 디자이너'), p('d2', '정하늘', 'UX 리서처'), p('d3', '윤채원', '브랜드 디자이너')]),
86
+ p('dev', '이도현', '개발 팀장', [p('e1', '한지우', '프론트엔드 개발'), p('e2', '오세훈', '백엔드 개발')]),
87
+ p('biz', '장예린', '사업 팀장', [p('b1', '강태오', '영업'), p('b2', '서지민', '마케팅'), p('b3', '문가은', '고객 지원')]),
88
+ ]),
89
+ ]
90
+
91
+ export default function Example() {
92
+ return (
93
+ <TreeDiagram
94
+ aria-label="조직도"
95
+ items={ORG}
96
+ indentFrom={1}
97
+ nodeWidth={220}
98
+ defaultCollapsed={['biz']}
99
+ renderNode={(node, state) => (
100
+ <Card padding="sm">
101
+ <CardHeader actions={state.toggle}>
102
+ <Inline gap={3} wrap={false}>
103
+ <Avatar name={node.data?.name ?? ''} size={32} />
104
+ <Stack gap={0} style={{ minWidth: 0 }}>
105
+ <Text variant="label-md" truncate>
106
+ {node.data?.name}
107
+ </Text>
108
+ <Text variant="caption" tone="tertiary" truncate>
109
+ {node.data?.role}
110
+ </Text>
111
+ </Stack>
112
+ </Inline>
113
+ </CardHeader>
114
+ </Card>
115
+ )}
116
+ />
117
+ )
118
+ }
119
+ ```
120
+
121
+ ### 작업 분해 (제어)
122
+
123
+ collapsed / onCollapsedChange로 제어해 모두 펼치기 · 단계만 보기 버튼을 만들었어요. 상태는 Badge, 깊이에 따라 제목 크기를 바꿨어요.
124
+
125
+ ```tsx
126
+ import { useState } from 'react'
127
+ import { Badge, Button, Inline, Stack, Text, type Tone, Card, CardHeader, TreeDiagram, type TreeDiagramNode } from '@iamfiro/design'
128
+
129
+ interface Task {
130
+ title: string
131
+ owner: string
132
+ status: '완료' | '진행 중' | '대기'
133
+ }
134
+
135
+ const TONE: Record<Task['status'], Tone> = { 완료: 'success', '진행 중': 'accent', 대기: 'neutral' }
136
+
137
+ const t = (id: string, title: string, owner: string, status: Task['status'], children?: TreeDiagramNode<Task>[]): TreeDiagramNode<Task> => ({
138
+ id,
139
+ data: { title, owner, status },
140
+ children,
141
+ })
142
+
143
+ const PROJECT = [
144
+ t('app', '모바일 앱 리뉴얼', '김민지', '진행 중', [
145
+ t('plan', '기획', '최유진', '완료', [t('plan-1', '요구사항 정리', '최유진', '완료'), t('plan-2', '경쟁 서비스 분석', '장예린', '완료')]),
146
+ t('design', '디자인', '박서연', '진행 중', [t('design-1', '와이어프레임', '박서연', '완료'), t('design-2', '시각 디자인', '윤채원', '진행 중'), t('design-3', '사용성 테스트', '정하늘', '대기')]),
147
+ t('build', '개발', '이도현', '대기', [t('build-1', '화면 개발', '한지우', '대기'), t('build-2', 'API 연동', '오세훈', '대기')]),
148
+ ]),
149
+ ]
150
+
151
+ const PARENTS = ['app', 'plan', 'design', 'build']
152
+
153
+ export default function Example() {
154
+ const [collapsed, setCollapsed] = useState<string[]>(['plan'])
155
+ return (
156
+ <Stack gap={3}>
157
+ <Inline justify="between">
158
+ <Text variant="label-md">작업 분해</Text>
159
+ <Inline gap={1}>
160
+ <Button variant="ghost" size="sm" onClick={() => setCollapsed([])}>
161
+ 모두 펼치기
162
+ </Button>
163
+ <Button variant="ghost" size="sm" onClick={() => setCollapsed(PARENTS.slice(1))}>
164
+ 단계만 보기
165
+ </Button>
166
+ </Inline>
167
+ </Inline>
168
+ <TreeDiagram
169
+ aria-label="모바일 앱 리뉴얼 작업 분해"
170
+ items={PROJECT}
171
+ collapsed={collapsed}
172
+ onCollapsedChange={setCollapsed}
173
+ indentFrom={1}
174
+ nodeWidth={200}
175
+ renderNode={(node, state) => {
176
+ const task = node.data
177
+ if (!task) return null
178
+ return (
179
+ <Card padding="sm">
180
+ <CardHeader actions={state.toggle}>
181
+ <Stack gap={1}>
182
+ <Text variant={state.depth === 0 ? 'heading-xs' : 'label-md'} truncate>
183
+ {task.title}
184
+ </Text>
185
+ <Inline gap={2} wrap={false}>
186
+ <Badge tone={TONE[task.status]} dot>
187
+ {task.status}
188
+ </Badge>
189
+ <Text variant="caption" tone="tertiary" truncate>
190
+ {task.owner}
191
+ </Text>
192
+ </Inline>
193
+ </Stack>
194
+ </CardHeader>
195
+ </Card>
196
+ )
197
+ }}
198
+ />
199
+ </Stack>
200
+ )
201
+ }
202
+ ```
203
+
204
+ ### 노드별 배치 · 크기 변화
205
+
206
+ childLayout: 'indented'를 준 노드만 하위를 목록으로 매달아요. 페이지를 추가하면 상자와 연결선이 함께 자라요.
207
+
208
+ ```tsx
209
+ import { useState } from 'react'
210
+ import { Plus } from 'lucide-react'
211
+ import { Button, Stack, Text, Card, CardHeader, TreeDiagram, type TreeDiagramNode } from '@iamfiro/design'
212
+
213
+ interface Page {
214
+ title: string
215
+ path: string
216
+ }
217
+
218
+ const page = (id: string, title: string, path: string, children?: TreeDiagramNode<Page>[]): TreeDiagramNode<Page> => ({ id, data: { title, path }, children })
219
+
220
+ const EXTRA = ['공지사항', '이용 안내', '환불 정책', '제휴 문의']
221
+
222
+ export default function Example() {
223
+ const [added, setAdded] = useState(0)
224
+ const support = [page('faq', '자주 묻는 질문', '/support/faq'), ...EXTRA.slice(0, added).map((title, i) => page(`extra-${i}`, title, `/support/${i + 1}`))]
225
+ const site = [
226
+ page('home', '홈', '/', [
227
+ page('products', '제품', '/products', [page('new', '신제품', '/products/new'), page('best', '인기 상품', '/products/best')]),
228
+ // This one hangs its pages as a list, the others stay in rows.
229
+ { ...page('support', '고객 지원', '/support', support), childLayout: 'indented' as const },
230
+ page('about', '회사 소개', '/about', [page('team', '팀', '/about/team'), page('careers', '채용', '/about/careers')]),
231
+ ]),
232
+ ]
233
+ return (
234
+ <Stack gap={3} align="start">
235
+ <Button size="sm" icon={<Plus />} disabled={added === EXTRA.length} onClick={() => setAdded((n) => n + 1)}>
236
+ 고객 지원에 페이지 추가
237
+ </Button>
238
+ <TreeDiagram
239
+ aria-label="사이트맵"
240
+ items={site}
241
+ gap={3}
242
+ levelGap={6}
243
+ style={{ alignSelf: 'stretch' }}
244
+ renderNode={(node, state) => (
245
+ <Card padding="sm" style={{ minWidth: 140 }}>
246
+ <CardHeader actions={state.toggle}>
247
+ <Stack gap={0}>
248
+ <Text variant="label-md">{node.data?.title}</Text>
249
+ <Text variant="code" tone="tertiary">
250
+ {node.data?.path}
251
+ </Text>
252
+ </Stack>
253
+ </CardHeader>
254
+ </Card>
255
+ )}
256
+ />
257
+ </Stack>
258
+ )
259
+ }
260
+ ```
@@ -0,0 +1,329 @@
1
+ # TreeView
2
+
3
+ > 내비게이션 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
4
+
5
+ 단계별로 들여쓴 행을 접고 펴는 트리. 폴더 · 파일, 작업 분해, 사이트맵처럼 위아래 관계가 있는 행에 써요. 행 모양은 List와 같고(앞 아이콘 · 굵은 이름 · 잘리는 회색 설명 · 뒤 자리), 하위 항목은 실제 높이만큼 부드럽게 펼쳐져요. 행 높이가 고정이라 옆에 일정 막대 같은 것을 행 단위로 맞춰 그릴 수 있어요.
6
+
7
+ ```tsx
8
+ import { TreeItem, TreeView, flattenTree, TREE_ROW_HEIGHT } from '@iamfiro/design'
9
+ ```
10
+
11
+ ## 디자인 규칙
12
+
13
+ - 위계(부모 → 자식): 들여쓴 행이면 `TreeView`(WAI-ARIA tree, roving tabindex, typeahead는 `TIMING.typeahead`), 상자 + 연결선이면 `TreeDiagram`(노드는 `renderNode`로 Card 조립, 접기 토글은 `state.toggle`).
14
+
15
+ ## API
16
+
17
+ ### TreeView
18
+
19
+ 키보드: ↑ ↓ 이동 · → 펼치기 / 첫 하위로 · ← 접기 / 부모로 · Home / End · Enter / Space 선택 · * 형제 모두 펼치기 · 글자 입력으로 이동(typeahead). 탭 순서에는 한 행만 들어가요(roving tabindex).
20
+
21
+ | prop | 타입 | 기본값 | 설명 |
22
+ | --- | --- | --- | --- |
23
+ | `items` | `TreeNode<T>[]` | | 데이터로 그릴 노드. 없으면 children의 TreeItem을 읽어요. |
24
+ | `children` | `ReactNode` | | items 대신 중첩한 TreeItem. |
25
+ | `expanded / defaultExpanded` | `string[]` | | 펼친 노드 id(제어 / 비제어). |
26
+ | `onExpandedChange` | `(ids: string[]) => void` | | 펼침이 바뀔 때. |
27
+ | `selected / defaultSelected` | `string \| null` | | 선택한 노드 id(제어 / 비제어). null = 제어 중, 선택 없음. |
28
+ | `onSelectedChange` | `(id: string \| null) => void` | | 선택이 바뀔 때. |
29
+ | `selectionMode` | `'single' \| 'none'` | `'single'` | none이면 선택 없이 펼치고 접기만 해요. |
30
+ | `expandOnClick` | `boolean` | `selectionMode === 'none'` | 부모 행을 누르면 펼침도 바꿔요(파일 탐색기). 아니면 셰브런으로만. |
31
+ | `onAction` | `(node: TreeNode<T>) => void` | | 행을 누르거나 Enter / Space를 눌렀을 때. |
32
+ | `density` | `'compact' \| 'default' \| 'comfortable'` | `'default'` | 행 높이 32 / 40 / 48. |
33
+ | `rowHeight` | `number` | | density의 행 높이를 덮어써요(px). |
34
+ | `indent` | `Space` | `6` | 단계마다 들여쓰는 폭(4px 단위). 기본 24px이면 하위 셰브런이 부모 아이콘 아래 와요. |
35
+ | `guides` | `boolean` | | 열린 부모 아래 옅은 세로 안내선. |
36
+ | `animated` | `boolean` | `true` | 하위 항목 접고 펴기 애니메이션. 옆 차트가 행 위치를 바로 따라가야 하면 끄세요. |
37
+ | `renderItem` | `(node, state: TreeItemState) => ReactNode` | | 이름 + 설명 자리를 직접 그려요. leading / trailing은 노드 값 그대로. |
38
+ | `onVisibleChange` | `(rows: TreeRow<T>[]) => void` | | 보이는 행이 바뀔 때마다 순서대로(펼침 · 접힘 · 데이터 변경). |
39
+ | `aria-label / aria-labelledby` | `string` | | 트리의 접근성 이름. |
40
+ | `className / style` | `string / CSSProperties` | | 바깥 ul에 전달. |
41
+
42
+ ### TreeNode<T> · TreeItem
43
+
44
+ items의 노드 모양. TreeItem은 같은 props(+ 중첩 children)를 JSX로 받아요.
45
+
46
+ | prop | 타입 | 기본값 | 설명 |
47
+ | --- | --- | --- | --- |
48
+ | `id` (필수) | `string` | | 트리 전체에서 고유. |
49
+ | `label` (필수) | `ReactNode` | | 굵은 이름. |
50
+ | `description` | `ReactNode` | | 이름 뒤 회색 글자. 자리가 모자라면 먼저 잘려요. |
51
+ | `leading` | `ReactNode` | | 이름 앞 아이콘 · 아바타 · 파일 아이콘. |
52
+ | `trailing` | `ReactNode` | | 오른쪽: 기간, 개수, Spinner 등. |
53
+ | `disabled` | `boolean` | | 흐리게. 포커스는 되지만 선택 · 펼침은 안 돼요. |
54
+ | `textValue` | `string` | | label이 문자열이 아닐 때 typeahead가 읽을 글자. |
55
+ | `children` | `TreeNode<T>[]` | | 하위 노드. |
56
+ | `data` | `T` | | renderItem · 콜백에서 다시 꺼내 쓸 값. |
57
+
58
+ ### flattenTree · TreeRow · TREE_ROW_HEIGHT
59
+
60
+ flattenTree(items, expanded)는 보이는 행을 위에서부터 돌려줘요. TREE_ROW_HEIGHT는 density별 행 높이 { compact: 32, default: 40, comfortable: 48 }.
61
+
62
+ | prop | 타입 | 기본값 | 설명 |
63
+ | --- | --- | --- | --- |
64
+ | `id / node` | `string / TreeNode<T>` | | 행의 노드. |
65
+ | `index` | `number` | | 보이는 목록에서의 위치(0부터). × 행 높이 = 행의 top. |
66
+ | `depth` | `number` | | 단계(맨 위 0). |
67
+ | `parentId` | `string \| null` | | 부모 id. |
68
+ | `hasChildren / expanded` | `boolean` | | 하위가 있는지 / 펼쳐져 있는지. |
69
+ | `posinset / setsize` | `number` | | 형제 중 순서(1부터) / 형제 수. |
70
+
71
+ ## 예제
72
+
73
+ 예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
74
+
75
+ ### 파일 탐색기
76
+
77
+ expandOnClick으로 폴더 행을 누르면 펼쳐져요. guides는 열린 폴더 아래 옅은 세로선을 그려요. 펼침과 선택을 모두 제어(expanded · selected)해 폴더 아이콘을 바꿨어요.
78
+
79
+ ```tsx
80
+ import { useState } from 'react'
81
+ import { Folder, FolderOpen } from 'lucide-react'
82
+ import { FileIcon, Stack, Text, TreeView, type TreeNode } from '@iamfiro/design'
83
+
84
+ const file = (id: string, name: string, meta: string): TreeNode => ({ id, label: name, leading: <FileIcon name={name} size={16} />, trailing: meta })
85
+
86
+ const ITEMS: TreeNode[] = [
87
+ {
88
+ id: 'design',
89
+ label: '디자인',
90
+ children: [
91
+ file('design/landing', '랜딩 페이지 시안.png', '5.1MB'),
92
+ file('design/guide', '브랜드 가이드.pdf', '12MB'),
93
+ {
94
+ id: 'design/icons',
95
+ label: '아이콘',
96
+ children: [file('design/icons/set', '아이콘 세트.zip', '3.2MB'), file('design/icons/note', '사용 규칙.docx', '84KB')],
97
+ },
98
+ ],
99
+ },
100
+ {
101
+ id: 'plan',
102
+ label: '기획',
103
+ children: [file('plan/q4', '4분기 로드맵.pptx', '8.4MB'), file('plan/research', '사용자 인터뷰 정리.docx', '220KB')],
104
+ },
105
+ {
106
+ id: 'finance',
107
+ label: '재무',
108
+ children: [file('finance/sales', '10월 매출 정리.xlsx', '860KB'), file('finance/budget', '2027 예산안.xlsx', '1.1MB')],
109
+ },
110
+ file('readme', '폴더 안내.txt', '2KB'),
111
+ ]
112
+
113
+ // Open folders show an open-folder icon.
114
+ const withFolderIcons = (nodes: TreeNode[], open: string[]): TreeNode[] =>
115
+ nodes.map((n) => (n.children ? { ...n, leading: open.includes(n.id) ? <FolderOpen /> : <Folder />, children: withFolderIcons(n.children, open) } : n))
116
+
117
+ export default function Example() {
118
+ const [expanded, setExpanded] = useState<string[]>(['design'])
119
+ const [selected, setSelected] = useState<string | null>('design/landing')
120
+ return (
121
+ <Stack gap={3} style={{ width: 420 }}>
122
+ <TreeView
123
+ aria-label="공유 폴더"
124
+ items={withFolderIcons(ITEMS, expanded)}
125
+ expanded={expanded}
126
+ onExpandedChange={setExpanded}
127
+ selected={selected}
128
+ onSelectedChange={setSelected}
129
+ expandOnClick
130
+ guides
131
+ />
132
+ <Text variant="caption" tone="tertiary">
133
+ 선택: {selected ?? '없음'}
134
+ </Text>
135
+ </Stack>
136
+ )
137
+ }
138
+ ```
139
+
140
+ ### 작업 분해
141
+
142
+ trailing에 기간이나 Spinner, description에 잘리는 설명. 아직 시작하지 않은 작업은 disabled로 흐리게 — 포커스는 가지만 선택 · 펼침은 안 돼요.
143
+
144
+ ```tsx
145
+ import { CircleCheck, CircleDashed, CircleDot } from 'lucide-react'
146
+ import { Spinner, TreeView, type TreeNode } from '@iamfiro/design'
147
+
148
+ const done = <CircleCheck color="var(--success)" />
149
+ const doing = <CircleDot color="var(--accent)" />
150
+ const todo = <CircleDashed />
151
+
152
+ const ITEMS: TreeNode[] = [
153
+ {
154
+ id: 'launch',
155
+ label: '신규 서비스 출시',
156
+ description: '10월 1일 – 11월 7일',
157
+ leading: doing,
158
+ trailing: '38일',
159
+ children: [
160
+ {
161
+ id: 'research',
162
+ label: '사용자 조사',
163
+ description: '인터뷰 12명, 설문 340건 분석',
164
+ leading: done,
165
+ trailing: '6일',
166
+ children: [
167
+ { id: 'interview', label: '심층 인터뷰', description: '김민지 · 박서연', leading: done, trailing: '4일' },
168
+ { id: 'survey', label: '설문 분석', description: '응답 340건, 주요 불편 5가지 정리', leading: done, trailing: '2일' },
169
+ ],
170
+ },
171
+ {
172
+ id: 'design',
173
+ label: '디자인',
174
+ description: '와이어프레임부터 최종 시안까지',
175
+ leading: doing,
176
+ trailing: '12일',
177
+ children: [
178
+ { id: 'wire', label: '와이어프레임', description: '핵심 화면 8개', leading: done, trailing: '3일' },
179
+ { id: 'visual', label: '시각 디자인', description: '디자인 시스템 컴포넌트로 조립 중', leading: doing, trailing: <Spinner size={14} label="진행 중" /> },
180
+ ],
181
+ },
182
+ {
183
+ id: 'build',
184
+ label: '개발',
185
+ description: '디자인 확정 후 시작',
186
+ leading: todo,
187
+ trailing: '14일',
188
+ disabled: true,
189
+ children: [
190
+ { id: 'front', label: '프론트엔드', leading: todo, trailing: '9일', disabled: true },
191
+ { id: 'back', label: '백엔드 API', leading: todo, trailing: '5일', disabled: true },
192
+ ],
193
+ },
194
+ { id: 'qa', label: 'QA · 출시', description: '11월 4일부터', leading: todo, trailing: '4일', disabled: true },
195
+ ],
196
+ },
197
+ ]
198
+
199
+ export default function Example() {
200
+ return <TreeView aria-label="출시 작업" items={ITEMS} defaultExpanded={['launch', 'design']} defaultSelected="visual" style={{ width: 520 }} />
201
+ }
202
+ ```
203
+
204
+ ### TreeItem으로 쓰기 · 모두 펼치기
205
+
206
+ items 대신 TreeItem을 중첩해 JSX로 적을 수 있어요. selectionMode="none"이면 행을 눌러 펼치고 접기만 해요.
207
+
208
+ ```tsx
209
+ import { useState } from 'react'
210
+ import { FileText, House, Settings, ShoppingBag, Users } from 'lucide-react'
211
+ import { Button, Inline, Stack, Text, TreeItem, TreeView } from '@iamfiro/design'
212
+
213
+ const PARENTS = ['home', 'products', 'about', 'support']
214
+
215
+ export default function Example() {
216
+ const [expanded, setExpanded] = useState<string[]>(['home'])
217
+ const allOpen = expanded.length === PARENTS.length
218
+ return (
219
+ <Stack gap={3} style={{ width: 440 }}>
220
+ <Inline justify="between">
221
+ <Text variant="label-md">사이트맵</Text>
222
+ <Button variant="ghost" size="sm" onClick={() => setExpanded(allOpen ? [] : PARENTS)}>
223
+ {allOpen ? '모두 접기' : '모두 펼치기'}
224
+ </Button>
225
+ </Inline>
226
+ <TreeView aria-label="사이트맵" expanded={expanded} onExpandedChange={setExpanded} selectionMode="none" density="compact">
227
+ <TreeItem id="home" label="홈" description="/" leading={<House />}>
228
+ <TreeItem id="products" label="제품" description="/products" leading={<ShoppingBag />}>
229
+ <TreeItem id="products/new" label="신제품" description="/products/new" leading={<FileText />} />
230
+ <TreeItem id="products/best" label="인기 상품" description="/products/best" leading={<FileText />} />
231
+ <TreeItem id="products/sale" label="할인 행사" description="/products/sale · 11월 1일 공개" leading={<FileText />} disabled />
232
+ </TreeItem>
233
+ <TreeItem id="about" label="회사 소개" description="/about" leading={<Users />}>
234
+ <TreeItem id="about/team" label="팀" description="/about/team" leading={<FileText />} />
235
+ <TreeItem id="about/careers" label="채용" description="/about/careers" leading={<FileText />} />
236
+ </TreeItem>
237
+ <TreeItem id="support" label="고객 지원" description="/support" leading={<Settings />}>
238
+ <TreeItem id="support/faq" label="자주 묻는 질문" description="/support/faq" leading={<FileText />} />
239
+ <TreeItem id="support/contact" label="문의하기" description="/support/contact" leading={<FileText />} />
240
+ </TreeItem>
241
+ </TreeItem>
242
+ </TreeView>
243
+ </Stack>
244
+ )
245
+ }
246
+ ```
247
+
248
+ ### 밀도
249
+
250
+ density로 행 높이를 compact(32) · default(40) · comfortable(48) 중에 골라요. 행 좌우 여백은 List와 같아요.
251
+
252
+ ```tsx
253
+ import { useState } from 'react'
254
+ import { Avatar, SegmentedControl, Stack, TreeView, type TreeDensity, type TreeNode } from '@iamfiro/design'
255
+
256
+ const person = (id: string, name: string, role: string, children?: TreeNode[]): TreeNode => ({
257
+ id,
258
+ label: name,
259
+ description: role,
260
+ leading: <Avatar name={name} size={20} />,
261
+ children,
262
+ })
263
+
264
+ const ITEMS = [
265
+ person('ceo', '최유진', '대표', [
266
+ person('lead-1', '김민지', '디자인 팀장', [person('m-1', '박서연', '프로덕트 디자이너'), person('m-2', '정하늘', 'UX 리서처')]),
267
+ person('lead-2', '이도현', '개발 팀장', [person('m-3', '한지우', '프론트엔드'), person('m-4', '오세훈', '백엔드')]),
268
+ ]),
269
+ ]
270
+
271
+ export default function Example() {
272
+ const [density, setDensity] = useState<TreeDensity>('default')
273
+ return (
274
+ <Stack gap={4} align="start" style={{ width: 420 }}>
275
+ <SegmentedControl
276
+ ariaLabel="행 높이"
277
+ value={density}
278
+ onChange={setDensity}
279
+ options={[
280
+ { value: 'compact', label: 'compact' },
281
+ { value: 'default', label: 'default' },
282
+ { value: 'comfortable', label: 'comfortable' },
283
+ ]}
284
+ />
285
+ <TreeView aria-label="조직" items={ITEMS} density={density} defaultExpanded={['ceo', 'lead-1']} guides style={{ alignSelf: 'stretch' }} />
286
+ </Stack>
287
+ )
288
+ }
289
+ ```
290
+
291
+ ### 보이는 행 (옆에 차트 맞추기)
292
+
293
+ onVisibleChange는 보이는 행을 순서대로 알려 줘요. 행 높이(TREE_ROW_HEIGHT 또는 rowHeight) × index가 그 행의 위치예요. 데이터만 있을 땐 flattenTree(items, expanded)로 같은 목록을 얻어요.
294
+
295
+ ```tsx
296
+ import { useState } from 'react'
297
+ import { Stack, Text, TREE_ROW_HEIGHT, TreeView, type TreeNode, type TreeRow } from '@iamfiro/design'
298
+
299
+ const ITEMS: TreeNode[] = [
300
+ {
301
+ id: 'web',
302
+ label: '웹사이트 개편',
303
+ trailing: '21일',
304
+ children: [
305
+ { id: 'web/ia', label: '정보 구조 정리', trailing: '3일' },
306
+ { id: 'web/design', label: '화면 디자인', trailing: '8일', children: [{ id: 'web/design/main', label: '메인', trailing: '3일' }, { id: 'web/design/sub', label: '하위 페이지', trailing: '5일' }] },
307
+ { id: 'web/dev', label: '개발', trailing: '10일' },
308
+ ],
309
+ },
310
+ { id: 'app', label: '앱 업데이트', trailing: '9일', children: [{ id: 'app/push', label: '알림 설정 개선', trailing: '4일' }, { id: 'app/login', label: '간편 로그인', trailing: '5일' }] },
311
+ ]
312
+
313
+ export default function Example() {
314
+ const [rows, setRows] = useState<TreeRow[]>([])
315
+ const h = TREE_ROW_HEIGHT.default
316
+ return (
317
+ <Stack gap={3} style={{ width: 460 }}>
318
+ {/* animated={false}: rows move at once, so whatever is drawn beside them can follow without lagging. */}
319
+ <TreeView aria-label="프로젝트 일정" items={ITEMS} defaultExpanded={['web']} animated={false} onVisibleChange={setRows} />
320
+ <Text variant="caption" tone="tertiary" numeric>
321
+ 보이는 행 {rows.length}개 · 행 높이 {h}px · 전체 {rows.length * h}px
322
+ </Text>
323
+ <Text variant="caption" tone="tertiary" numeric>
324
+ {rows.map((r) => `${r.index * h}px ${'·'.repeat(r.depth)}${typeof r.node.label === 'string' ? r.node.label : r.id}`).join(' / ')}
325
+ </Text>
326
+ </Stack>
327
+ )
328
+ }
329
+ ```