@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,92 @@
1
+ # Sparkbar
2
+
3
+ > 차트 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
4
+
5
+ 축 없이 막대만 늘어놓은 작은 막대 줄. 합계 아래 일별 추이처럼 흐름만 보여 줄 때 써요. 바닥에서 자라고 데이터가 바뀌면 새 높이로 미끄러지며, 마우스·터치·←/→로 막대를 고르면 차트 툴팁 카드에 값이 떠요. null은 아직 기록이 없는 칸으로 납작한 자리 표시가 돼요.
6
+
7
+ ```tsx
8
+ import { Sparkbar } from '@iamfiro/design'
9
+ ```
10
+
11
+ ## 디자인 규칙
12
+
13
+ - 차트: 시리즈 색은 `--chart-1…6`을 순서대로, 그리드는 `--chart-grid`로 아주 옅게(세로선·축선 없음). 범례·툴팁의 색 표시는 `ColorDot`, 툴팁은 `Card raised` + `Separator`로 조립. 축·툴팁·범례·호버/키보드는 `components/Chart/`의 공용 조각(`ChartTooltip`, `ChartLegend`, `useChartInteraction`)을 쓴다. → `LineChart` · `BarChart` · `PieChart` · `Sparkbar`
14
+ - 표 칸 · KPI 카드의 작은 추이는 `SparkLine` · `SparkArea` · `Sparkbar`(같은 props, 하나로 고르면 `SparkChart type`). 축 없음, 실제 픽셀 크기로 그려 24–40px에서도 또렷하게, 값 범위는 0이 아니라 최솟값–최댓값. 색은 `tone="trend"` + `higherIs`(Delta와 같은 규칙), 마지막 값은 `endDot`. 긴 표에선 `tooltip={false}`로 이벤트 없는 정적 그림(`role="img"`). KPI 카드는 Card 안 `Stat` + `Delta` + `SparkArea`.
15
+
16
+ ## API
17
+
18
+ ### Sparkbar
19
+
20
+ | prop | 타입 | 기본값 | 설명 |
21
+ | --- | --- | --- | --- |
22
+ | `data` (필수) | `(number \| null)[]` | | 한 칸에 값 하나. null은 기록 없음(자리 표시). |
23
+ | `height` | `number` | `48` | 높이(px). |
24
+ | `max` | `number` | | 막대가 꽉 차는 값. 기본은 가장 큰 값. |
25
+ | `color` | `string` | `'--n5'` | 막대 색. CSS 변수 이름('--accent') 또는 색. |
26
+ | `tone / higherIs / worseTone` | `'accent' \| 'neutral' \| 'trend'` | | SparkLine과 같은 색 규칙. trend는 처음 대비 마지막 값의 방향으로 초록 / 주황. color가 우선이고, 둘 다 없으면 옅은 회색. |
27
+ | `formatLabel` | `(index) => string` | `'3일'` | 툴팁 제목. |
28
+ | `formatValue` | `(value) => string` | `'1,234'` | 툴팁 값 형식. |
29
+ | `valueLabel` | `string` | `'값'` | 툴팁 행 이름. |
30
+ | `tooltip` | `boolean` | `true` | 값 카드 표시. |
31
+ | `activeIndex / defaultActiveIndex / onActiveIndexChange` | `number \| null` | | 고른 막대(제어/비제어). |
32
+ | `aria-label` | `string` | `'막대 그래프'` | 이름. 고른 막대는 aria-live로 읽어요. |
33
+
34
+ ## 예제
35
+
36
+ 예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
37
+
38
+ ### 기본
39
+
40
+ 기본 막대는 옅은 회색이고, 고른 막대만 조금 진해져요. 포커스한 뒤 ←/→, Home/End, Esc로도 움직여요.
41
+
42
+ ```tsx
43
+ import { Sparkbar } from '@iamfiro/design'
44
+
45
+ const VISITS = [42, 58, 51, 66, 73, 40, 35, 61, 70, 68, 77, 82, 49, 44, 69, 88, 91, 76, 84, 52, 47, 79, 86, 93]
46
+
47
+ export default function Example() {
48
+ return (
49
+ <div style={{ width: '100%', maxWidth: 360 }}>
50
+ <Sparkbar data={VISITS} formatLabel={(i) => `${i}시`} valueLabel="방문" aria-label="시간대별 방문 수" />
51
+ </div>
52
+ )
53
+ }
54
+ ```
55
+
56
+ ### 자리 표시 · 색 · 데이터 바뀜
57
+
58
+ 앞으로 올 날은 null로 두면 바닥에 납작한 선으로 보여요. color로 막대 색을 바꾸고, 달을 바꾸면 막대가 새 높이로 미끄러져요.
59
+
60
+ ```tsx
61
+ import { useState } from 'react'
62
+ import { Button, Inline, Stack, Sparkbar } from '@iamfiro/design'
63
+
64
+ // 10월은 22일까지만 기록이 있어요. 남은 날은 null → 납작한 자리 표시.
65
+ const OCTOBER = [12, 31, 29, 25, 42, 36, 16, 14, 38, 35, 34, 44, 33, 17, 19, 48, 42, 38, 40, 47, 22, 23, ...Array<null>(9).fill(null)]
66
+ const SEPTEMBER = [26, 30, 28, 33, 31, 18, 15, 29, 35, 33, 30, 36, 20, 17, 31, 38, 34, 32, 40, 21, 19, 33, 36, 35, 31, 39, 22, 18, 30, 32]
67
+
68
+ export default function Example() {
69
+ const [month, setMonth] = useState<9 | 10>(10)
70
+ return (
71
+ <Stack gap={4} style={{ width: '100%', maxWidth: 360 }}>
72
+ <Sparkbar
73
+ data={month === 10 ? OCTOBER : SEPTEMBER}
74
+ height={56}
75
+ color="--accent"
76
+ formatLabel={(i) => `${month}월 ${i + 1}일`}
77
+ formatValue={(v) => `${v}건`}
78
+ valueLabel="주문"
79
+ aria-label={`${month}월 일별 주문`}
80
+ />
81
+ <Inline gap={2}>
82
+ <Button size="sm" variant={month === 9 ? 'primary' : 'secondary'} onClick={() => setMonth(9)}>
83
+ 9월
84
+ </Button>
85
+ <Button size="sm" variant={month === 10 ? 'primary' : 'secondary'} onClick={() => setMonth(10)}>
86
+ 10월
87
+ </Button>
88
+ </Inline>
89
+ </Stack>
90
+ )
91
+ }
92
+ ```
@@ -0,0 +1,123 @@
1
+ # Spinner
2
+
3
+ > 진행 표시 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
4
+
5
+ 옅은 고리 위로 호가 돌면서 늘었다 줄었다 하는 로딩 표시. 진행률을 알 수 없는 짧은 기다림에 써요. 기본 색은 주변 글자 색을 따라가고, 스크린 리더에는 label을 읽어 줘요.
6
+
7
+ ```tsx
8
+ import { Spinner } from '@iamfiro/design'
9
+ ```
10
+
11
+ ## API
12
+
13
+ ### Spinner
14
+
15
+ | prop | 타입 | 기본값 | 설명 |
16
+ | --- | --- | --- | --- |
17
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| number` | `'sm'` | 이름 있는 크기 또는 px 숫자. |
18
+ | `tone` | `'default' \| 'muted' \| 'accent' \| 'inverse' \| 'inherit'` | `'inherit'` | 색. inherit은 글자 색을 따라가요. |
19
+ | `label` | `string` | `'불러오는 중'` | 스크린 리더가 읽는 이름(role="status"). |
20
+ | `className / style` | `string / CSSProperties` | | 루트에 적용돼요. |
21
+
22
+ ## 예제
23
+
24
+ 예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
25
+
26
+ ### 크기
27
+
28
+ xs 12 · sm 14 · md 18 · lg 24 · xl 32px. 숫자를 넘기면 그 크기(px)로 그려요.
29
+
30
+ ```tsx
31
+ import { Spinner, Text, type SpinnerSize } from '@iamfiro/design'
32
+
33
+ const SIZES: SpinnerSize[] = ['xs', 'sm', 'md', 'lg', 'xl']
34
+
35
+ export default function Example() {
36
+ return (
37
+ <div style={{ display: 'flex', alignItems: 'flex-end', gap: 32 }}>
38
+ {SIZES.map((size) => (
39
+ <div key={size} style={{ display: 'grid', justifyItems: 'center', gap: 10 }}>
40
+ <Spinner size={size} tone="default" />
41
+ <Text variant="caption" tone="tertiary">
42
+ {size}
43
+ </Text>
44
+ </div>
45
+ ))}
46
+ <div style={{ display: 'grid', justifyItems: 'center', gap: 10 }}>
47
+ <Spinner size={48} tone="default" />
48
+ <Text variant="caption" tone="tertiary">
49
+ 48
50
+ </Text>
51
+ </div>
52
+ </div>
53
+ )
54
+ }
55
+ ```
56
+
57
+ ### 색
58
+
59
+ default · muted · accent는 정해진 색, inverse는 어두운 배경용이에요. 기본값 'inherit'은 currentColor를 써서 글자 색을 따라가요.
60
+
61
+ ```tsx
62
+ import { Spinner, Text } from '@iamfiro/design'
63
+
64
+ export default function Example() {
65
+ return (
66
+ <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'center', gap: 28 }}>
67
+ <Spinner size="lg" tone="default" label="기본" />
68
+ <Spinner size="lg" tone="muted" label="흐리게" />
69
+ <Spinner size="lg" tone="accent" label="강조" />
70
+ <div style={{ display: 'grid', placeItems: 'center', width: 48, height: 48, borderRadius: 12, background: 'var(--primary)' }}>
71
+ <Spinner size="lg" tone="inverse" label="어두운 배경" />
72
+ </div>
73
+ <Text tone="success" style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
74
+ <Spinner />
75
+ 글자 색을 따라가요
76
+ </Text>
77
+ </div>
78
+ )
79
+ }
80
+ ```
81
+
82
+ ### 버튼 · 인라인
83
+
84
+ Button의 loading을 켜면 아이콘 자리에 Spinner가 들어가고 눌리지 않아요. 짧은 상태 문구 옆에도 써요.
85
+
86
+ ```tsx
87
+ import { useState } from 'react'
88
+ import { RefreshCw, Save } from 'lucide-react'
89
+ import { Button, Spinner, Text } from '@iamfiro/design'
90
+
91
+ export default function Example() {
92
+ const [saving, setSaving] = useState(false)
93
+ const [syncing, setSyncing] = useState(false)
94
+
95
+ const save = () => {
96
+ setSaving(true)
97
+ setTimeout(() => setSaving(false), 1600)
98
+ }
99
+
100
+ const sync = () => {
101
+ setSyncing(true)
102
+ setTimeout(() => setSyncing(false), 2400)
103
+ }
104
+
105
+ return (
106
+ <div style={{ display: 'grid', justifyItems: 'center', gap: 20 }}>
107
+ <div style={{ display: 'flex', gap: 8 }}>
108
+ <Button variant="primary" icon={<Save />} loading={saving} onClick={save}>
109
+ {saving ? '저장 중…' : '저장'}
110
+ </Button>
111
+ <Button iconOnly icon={<RefreshCw />} loading={syncing} onClick={sync} aria-label="동기화" />
112
+ <Button size="sm" loading>
113
+ 작은 버튼
114
+ </Button>
115
+ </div>
116
+ <Text variant="body-sm" tone="tertiary" style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
117
+ {syncing && <Spinner size="xs" />}
118
+ {syncing ? '동기화 중…' : '2분 전에 동기화됨'}
119
+ </Text>
120
+ </div>
121
+ )
122
+ }
123
+ ```
@@ -0,0 +1,290 @@
1
+ # SplitView
2
+
3
+ > 컨테이너 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
4
+
5
+ 목록 – 상세(master–detail) 두 칸 레이아웃. 왼쪽 목록과 오른쪽 상세가 각자 스크롤되고, 목록은 옅은 회색 판 위에 놓여 ListItem variant="raised"로 고른 행이 흰 카드처럼 떠올라요. resizable이면 가운데 경계를 끌거나, 포커스한 뒤 ← → 로 목록 너비를 바꿔요. 높이는 부모가 정해요.
6
+
7
+ ```tsx
8
+ import { SplitView } from '@iamfiro/design'
9
+ ```
10
+
11
+ ## 디자인 규칙
12
+
13
+ - 목록–상세는 `SplitView`(칸마다 스크롤, 경계는 `role="separator"`). 회색 판 위의 선택 행은 `ListItem variant="raised"`.
14
+
15
+ ## API
16
+
17
+ ### SplitView
18
+
19
+ | prop | 타입 | 기본값 | 설명 |
20
+ | --- | --- | --- | --- |
21
+ | `list` (필수) | `ReactNode` | | 왼쪽 목록 칸. |
22
+ | `children` (필수) | `ReactNode` | | 오른쪽 상세 칸. 세로 flex라 ActionBar가 아래에 붙어요. |
23
+ | `listWidth` | `number` | | 목록 너비 px(제어). |
24
+ | `defaultListWidth` | `number` | `300` | 처음 너비(비제어). 더블 클릭 · Enter로 돌아가는 너비. |
25
+ | `onListWidthChange` | `(width: number) => void` | | 끌거나 키로 바꿀 때. |
26
+ | `minListWidth / maxListWidth` | `number` | `220 / 520` | 너비 범위. |
27
+ | `resizable` | `boolean` | | 경계를 끌어 너비 조절. role="separator"(aria-valuenow), ← → (Shift는 크게) · Home / End. |
28
+ | `listTone` | `'soft' \| 'plain'` | `'soft'` | soft: 목록 칸이 옅은 회색 판 · plain: 흰 바탕. |
29
+ | `listLabel / detailLabel` | `string` | `'목록' / '상세'` | 두 칸(section)의 접근성 이름. |
30
+ | `className / style` | `string / CSSProperties` | | 바깥 요소에 전달. 부모가 flex 세로면 style={{ flex: 1, minHeight: 0 }}. |
31
+
32
+ ## 예제
33
+
34
+ 예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
35
+
36
+ ### 검토 대기열
37
+
38
+ 위쪽은 제목 · 작은 진행 막대 · "26 / 33 검토" · 실행 취소(Z). 왼쪽은 개수가 붙은 Tabs와 요청 목록, 오른쪽은 요청 내용과 아래에 붙은 ActionBar(승인 A · 수정 E · 거절 R · 건너뛰기 S). 처리 중엔 버튼과 단축키가 잠기고, 끝나면 다음 요청이 골라져요.
39
+
40
+ ```tsx
41
+ import { useEffect, useState, type ReactNode } from 'react'
42
+ import { Check, ChevronsRight, CircleCheck, FileSignature, KeyRound, Pencil, Receipt, Undo2, UserPlus, X } from 'lucide-react'
43
+ import { Badge, Box, Button, Grid, IconTile, Inline, Progress, Stack, Text, ActionBar, Card, List, ListItem, SplitView, Tab, TabList, TabPanel, Tabs } from '@iamfiro/design'
44
+
45
+ interface ReviewItem {
46
+ id: string
47
+ icon: ReactNode
48
+ title: string
49
+ kind: string
50
+ time: string
51
+ from: string
52
+ risk?: boolean
53
+ note: string
54
+ amount: string
55
+ due: string
56
+ }
57
+
58
+ const REQUESTS: ReviewItem[] = [
59
+ { id: 'r1', icon: <Receipt />, title: '10월 법인 카드 정산', kind: '경비', time: '방금', from: '김민지', risk: true, note: '영수증 12장 중 2장의 금액이 카드 내역과 달라요.', amount: '1,248,000원', due: '10월 27일' },
60
+ { id: 'r2', icon: <UserPlus />, title: '이도현 님 관리자 권한 요청', kind: '권한', time: '14분', from: '이도현', note: '디자인팀 워크스페이스의 관리자 권한을 요청했어요.', amount: '—', due: '10월 26일' },
61
+ { id: 'r3', icon: <FileSignature />, title: '협력사 계약서 갱신', kind: '계약', time: '1시간', from: '김민지', note: '단가가 작년보다 4% 올랐어요. 나머지 조건은 같아요.', amount: '32,000,000원', due: '10월 31일' },
62
+ { id: 'r4', icon: <KeyRound />, title: '외부 공유 링크 허용', kind: '보안', time: '2시간', from: '이도현', risk: true, note: '고객사에 보낼 폴더를 링크로 공유하려고 해요.', amount: '—', due: '10월 25일' },
63
+ ]
64
+
65
+ const REVIEWED_BEFORE = 26
66
+ const TOTAL = REVIEWED_BEFORE + REQUESTS.length
67
+
68
+ export default function Example() {
69
+ const [queue, setQueue] = useState(REQUESTS)
70
+ const [done, setDone] = useState<ReviewItem[]>([])
71
+ const [tab, setTab] = useState('all')
72
+ const [selectedId, setSelectedId] = useState(REQUESTS[0].id)
73
+ const [pending, setPending] = useState(false)
74
+
75
+ const shown = tab === 'risk' ? queue.filter((r) => r.risk) : queue
76
+ const selected = queue.find((r) => r.id === selectedId) ?? shown[0]
77
+
78
+ // 데모용 처리: 0.6초 동안 잠갔다가 목록에서 빼고 다음 요청을 골라요.
79
+ useEffect(() => {
80
+ if (!pending || !selected) return
81
+ const id = window.setTimeout(() => {
82
+ const rest = queue.filter((r) => r.id !== selected.id)
83
+ setQueue(rest)
84
+ setDone((d) => [selected, ...d])
85
+ setSelectedId(rest[0]?.id ?? '')
86
+ setPending(false)
87
+ }, 600)
88
+ return () => window.clearTimeout(id)
89
+ }, [pending, selected, queue])
90
+
91
+ const undo = () => {
92
+ const [last, ...older] = done
93
+ if (!last) return
94
+ setDone(older)
95
+ setQueue((q) => REQUESTS.filter((r) => r.id === last.id || q.some((x) => x.id === r.id)))
96
+ setSelectedId(last.id)
97
+ }
98
+
99
+ const act = () => setPending(true)
100
+ const reviewed = REVIEWED_BEFORE + done.length
101
+
102
+ const rows = (items: ReviewItem[]) => (
103
+ <List density="compact" aria-label="검토할 요청">
104
+ {items.map((r) => (
105
+ <ListItem
106
+ key={r.id}
107
+ variant="raised"
108
+ selected={selected?.id === r.id}
109
+ onClick={() => setSelectedId(r.id)}
110
+ leading={<IconTile tone="surface">{r.icon}</IconTile>}
111
+ title={r.title}
112
+ description={r.kind}
113
+ meta={r.time}
114
+ />
115
+ ))}
116
+ </List>
117
+ )
118
+
119
+ return (
120
+ <Card padding="none" style={{ width: '100%', maxWidth: 880, height: 520 }}>
121
+ <Box paddingX={4} paddingY={3}>
122
+ <Inline justify="between" wrap={false}>
123
+ <Inline gap={2} wrap={false}>
124
+ <IconTile tone="accent" size={24}>
125
+ <CircleCheck />
126
+ </IconTile>
127
+ <Text variant="label-md">검토 센터</Text>
128
+ <Progress value={reviewed} max={TOTAL} size="sm" tone="accent" aria-label="검토 진행률" style={{ width: 72 }} />
129
+ <Text variant="label-sm" numeric>
130
+ {reviewed} / {TOTAL} 검토
131
+ </Text>
132
+ <Text variant="caption" tone="tertiary">
133
+ 가장 오래된 요청 2시간
134
+ </Text>
135
+ </Inline>
136
+ <Button variant="ghost" size="sm" icon={<Undo2 />} shortcut="Z" hotkey disabled={done.length === 0 || pending} onClick={undo}>
137
+ 실행 취소
138
+ </Button>
139
+ </Inline>
140
+ </Box>
141
+
142
+ <SplitView
143
+ resizable
144
+ defaultListWidth={300}
145
+ listLabel="요청 목록"
146
+ detailLabel="요청 내용"
147
+ style={{ flex: 1, minHeight: 0 }}
148
+ list={
149
+ <Tabs value={tab} onValueChange={setTab}>
150
+ <TabList size="sm" aria-label="요청 거르기">
151
+ <Tab value="all" count={queue.length}>
152
+ 전체
153
+ </Tab>
154
+ <Tab value="risk" count={queue.filter((r) => r.risk).length}>
155
+ 위험 높음
156
+ </Tab>
157
+ </TabList>
158
+ <TabPanel value="all">{rows(queue)}</TabPanel>
159
+ <TabPanel value="risk">{rows(queue.filter((r) => r.risk))}</TabPanel>
160
+ </Tabs>
161
+ }
162
+ >
163
+ {selected ? (
164
+ <>
165
+ <Box padding={5}>
166
+ <Stack gap={4}>
167
+ <Inline gap={2}>
168
+ <Text variant="label-md">{selected.from}</Text>
169
+ {selected.risk && (
170
+ <Badge tone="danger" size="sm">
171
+ 위험 높음
172
+ </Badge>
173
+ )}
174
+ <Text variant="caption" tone="tertiary">
175
+ {selected.kind} · {selected.time} 전
176
+ </Text>
177
+ </Inline>
178
+ <Text variant="heading-sm">{selected.title}</Text>
179
+ <Text variant="body-md" tone="secondary">
180
+ {selected.note}
181
+ </Text>
182
+ <Grid columns={2} gap={4} style={{ maxWidth: 360 }}>
183
+ <Stack gap={1}>
184
+ <Text variant="caption" tone="tertiary">
185
+ 금액
186
+ </Text>
187
+ <Text variant="label-md" numeric>
188
+ {selected.amount}
189
+ </Text>
190
+ </Stack>
191
+ <Stack gap={1}>
192
+ <Text variant="caption" tone="tertiary">
193
+ 기한
194
+ </Text>
195
+ <Text variant="label-md">{selected.due}</Text>
196
+ </Stack>
197
+ </Grid>
198
+ </Stack>
199
+ </Box>
200
+ <ActionBar pending={pending} aria-label="요청 처리">
201
+ <Button variant="primary" icon={<Check />} shortcut="A" hotkey loading={pending} onClick={act}>
202
+ 승인
203
+ </Button>
204
+ <Button variant="ghost" icon={<Pencil />} shortcut="E" hotkey onClick={act}>
205
+ 수정
206
+ </Button>
207
+ <Button variant="ghost" icon={<X />} shortcut="R" hotkey onClick={act}>
208
+ 거절
209
+ </Button>
210
+ <Button variant="ghost" icon={<ChevronsRight />} shortcut="S" hotkey onClick={act}>
211
+ 건너뛰기
212
+ </Button>
213
+ </ActionBar>
214
+ </>
215
+ ) : (
216
+ <Box padding={6}>
217
+ <Text variant="body-md" tone="tertiary" align="center">
218
+ 모든 요청을 검토했어요.
219
+ </Text>
220
+ </Box>
221
+ )}
222
+ </SplitView>
223
+ </Card>
224
+ )
225
+ }
226
+ ```
227
+
228
+ ### 너비 제어
229
+
230
+ listWidth / onListWidthChange로 제어해요. 경계를 끄는 동안엔 포인터를 그대로 따르고, 키보드 · 버튼으로 바꾸면 부드럽게 움직여요. 끄는 중 Esc는 원래 너비로, 더블 클릭 · Enter는 기본 너비로 돌아가요.
231
+
232
+ ```tsx
233
+ import { useState } from 'react'
234
+ import { Bell, Lock, User } from 'lucide-react'
235
+ import { Box, Button, Inline, Stack, Text, Card, List, ListItem, SplitView } from '@iamfiro/design'
236
+
237
+ const SECTIONS = [
238
+ { id: 'profile', icon: <User />, title: '프로필', body: '이름, 사진, 소개를 바꿔요. 김민지 님의 프로필은 팀원 모두에게 보여요.' },
239
+ { id: 'alarm', icon: <Bell />, title: '알림', body: '멘션과 승인 요청만 바로 알려 드리고, 나머지는 하루 한 번 모아서 보내요.' },
240
+ { id: 'security', icon: <Lock />, title: '보안', body: '2단계 인증이 켜져 있어요. 마지막 로그인은 10월 25일 오전 9시예요.' },
241
+ ]
242
+
243
+ export default function Example() {
244
+ const [width, setWidth] = useState(220)
245
+ const [selected, setSelected] = useState('profile')
246
+ const section = SECTIONS.find((s) => s.id === selected) ?? SECTIONS[0]
247
+ return (
248
+ <Stack gap={3} style={{ width: '100%', maxWidth: 640 }}>
249
+ <Inline gap={2}>
250
+ <Text variant="caption" tone="tertiary" numeric>
251
+ 목록 너비 {width}px
252
+ </Text>
253
+ <Button size="sm" variant="ghost" onClick={() => setWidth(180)}>
254
+ 좁게
255
+ </Button>
256
+ <Button size="sm" variant="ghost" onClick={() => setWidth(300)}>
257
+ 넓게
258
+ </Button>
259
+ </Inline>
260
+ <Card padding="none" style={{ height: 280 }}>
261
+ <SplitView
262
+ resizable
263
+ listWidth={width}
264
+ onListWidthChange={setWidth}
265
+ minListWidth={160}
266
+ maxListWidth={360}
267
+ listLabel="설정 목록"
268
+ detailLabel="설정 내용"
269
+ list={
270
+ <List aria-label="설정">
271
+ {SECTIONS.map((s) => (
272
+ <ListItem key={s.id} variant="raised" leading={s.icon} title={s.title} selected={selected === s.id} onClick={() => setSelected(s.id)} />
273
+ ))}
274
+ </List>
275
+ }
276
+ >
277
+ <Box padding={5}>
278
+ <Stack gap={2}>
279
+ <Text variant="heading-sm">{section.title}</Text>
280
+ <Text variant="body-md" tone="secondary">
281
+ {section.body}
282
+ </Text>
283
+ </Stack>
284
+ </Box>
285
+ </SplitView>
286
+ </Card>
287
+ </Stack>
288
+ )
289
+ }
290
+ ```
@@ -0,0 +1,138 @@
1
+ # Stat
2
+
3
+ > 데이터 표시 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
4
+
5
+ 라벨과 숫자 하나. 옅은 라벨, 굵은 값, 옅은 "/ 전체"나 단위, 그리고 증감(Delta) 자리로 이뤄져요. 한 줄(inline)로 행 안에 넣거나, 라벨 위 큰 숫자(stacked)로 지표 띠를 만들어요. StatGroup은 여러 Stat을 옅은 세로선으로 나눠 한 줄에 놓고, 좁아지면 줄을 바꿔요.
6
+
7
+ ```tsx
8
+ import { Stat, StatGroup } from '@iamfiro/design'
9
+ ```
10
+
11
+ ## 디자인 규칙
12
+
13
+ - 표 칸 · KPI 카드의 작은 추이는 `SparkLine` · `SparkArea` · `Sparkbar`(같은 props, 하나로 고르면 `SparkChart type`). 축 없음, 실제 픽셀 크기로 그려 24–40px에서도 또렷하게, 값 범위는 0이 아니라 최솟값–최댓값. 색은 `tone="trend"` + `higherIs`(Delta와 같은 규칙), 마지막 값은 `endDot`. 긴 표에선 `tooltip={false}`로 이벤트 없는 정적 그림(`role="img"`). KPI 카드는 Card 안 `Stat` + `Delta` + `SparkArea`.
14
+ - 라벨 + 숫자는 `Stat`(한 줄 "시간 1:52 / 2:55" 또는 오버라인 라벨 위 큰 숫자), 지표 띠는 `StatGroup`(칸 사이 `Separator`, 줄이 바뀌어도 줄 첫 칸 앞엔 선 없음).
15
+ - 단독 개수 타일("완료 137 / 실패 3")은 `Stat tile icon iconTone`. 카드 · 팝오버 안의 옅은 회색 패널은 모서리 `--r-panel`(12px) + 여백 12px.
16
+
17
+ ## API
18
+
19
+ ### Stat
20
+
21
+ | prop | 타입 | 기본값 | 설명 |
22
+ | --- | --- | --- | --- |
23
+ | `label` (필수) | `ReactNode` | | 무엇을 잰 값인지. |
24
+ | `value` (필수) | `ReactNode` | | 형식을 맞춘 값("1:52", "₩8,500"). |
25
+ | `total` | `ReactNode` | | 값이 속한 전체. 옅게 "/ 전체"로 붙어요. |
26
+ | `unit` | `ReactNode` | | 값 뒤 옅은 단위("초", "건"). |
27
+ | `delta` | `ReactNode` | | 값 뒤 증감 자리. 보통 <Delta>. |
28
+ | `caption` | `ReactNode` | | 값 아래 옅은 한 줄(stacked). |
29
+ | `layout` | `'stacked' \| 'inline'` | `'stacked'` | 라벨 위 큰 숫자 / 한 줄. |
30
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 값 크기(number-sm · md · lg 역할). |
31
+ | `tone` | `TextTone` | `'default'` | 값 색. 오류가 있으면 danger 같은 식으로. |
32
+ | `tile` | `boolean` | | 옅은 회색 패널(--surface-active, 모서리 12, 여백 12)로 감싼 단독 타일. |
33
+ | `icon` | `ReactNode` | | 라벨 앞 작은 아이콘. IconTile 네모 안에 들어가요. |
34
+ | `iconTone` | `'neutral' \| 'accent' \| 'success' \| 'warning' \| 'danger'` | `'neutral'` | 아이콘 네모의 색. |
35
+ | `className` | `string` | | 추가 클래스. |
36
+ | `style` | `CSSProperties` | | 인라인 스타일. |
37
+
38
+ ### StatGroup
39
+
40
+ | prop | 타입 | 기본값 | 설명 |
41
+ | --- | --- | --- | --- |
42
+ | `children` (필수) | `ReactNode` | | Stat들(또는 아무 칸). |
43
+ | `gap` | `Space` | `6` | 칸 사이 간격. 세로선은 그 가운데에 놓여요. |
44
+ | `minItemWidth` | `number` | `96` | 칸의 최소 폭(px). 모자라면 줄을 바꿔요. |
45
+ | `fill` | `boolean` | `true` | 칸이 폭을 똑같이 나눠 가질지, 내용 폭만 쓸지. |
46
+ | `dividers` | `boolean` | `true` | 칸 사이 옅은 세로선. |
47
+ | `aria-label` | `string` | | 묶음 이름("배포 지표"). |
48
+
49
+ ## 예제
50
+
51
+ 예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
52
+
53
+ ### 한 줄
54
+
55
+ layout="inline": "시간 1:52 / 2:55"처럼 행 · 카드 아래쪽에 넣는 짧은 지표.
56
+
57
+ ```tsx
58
+ import { Delta, Inline, Stack, Stat } from '@iamfiro/design'
59
+
60
+ export default function Example() {
61
+ return (
62
+ <Stack gap={4}>
63
+ <Inline gap={5}>
64
+ <Stat layout="inline" label="시간" value="1:52" total="2:55" />
65
+ <Stat layout="inline" label="비용" value="₩8,500" total="₩10,300" />
66
+ <Stat layout="inline" label="완료" value="7" total="12" />
67
+ </Inline>
68
+ <Inline gap={5}>
69
+ <Stat layout="inline" size="sm" label="빌드" value="48" unit="초" delta={<Delta value={-6} higherIs="worse" format={(n) => `${n}초`} size="sm" />} />
70
+ <Stat layout="inline" size="sm" label="경고" value="3" unit="건" tone="warning" />
71
+ </Inline>
72
+ </Stack>
73
+ )
74
+ }
75
+ ```
76
+
77
+ ### 라벨 위 숫자
78
+
79
+ layout="stacked"(기본): 오버라인 라벨 + 큰 숫자. size로 숫자 크기를, delta로 증감을 붙여요.
80
+
81
+ ```tsx
82
+ import { Delta, Grid, Stat } from '@iamfiro/design'
83
+
84
+ export default function Example() {
85
+ return (
86
+ <Grid minItemWidth={160} gap={6} style={{ width: '100%' }}>
87
+ <Stat size="sm" label="이번 주 배포" value="14" unit="회" />
88
+ <Stat label="평균 리뷰 시간" value="3.2" unit="시간" delta={<Delta value={-0.8} higherIs="worse" format={(n) => `${n}시간`} size="sm" />} />
89
+ <Stat size="lg" label="활성 사용자" value="12,480" delta={<Delta value={4.2} format={(n) => `${n}%`} arrow size="sm" />} caption="지난주 11,977명" />
90
+ </Grid>
91
+ )
92
+ }
93
+ ```
94
+
95
+ ### 타일
96
+
97
+ tile: 옅은 회색 패널(모서리 12, 촘촘한 여백) 안의 Stat. icon은 라벨 앞 색 네모(IconTile)로 들어가고, iconTone으로 상태색을 고르며, 숫자 색은 tone으로 따로 정해요.
98
+
99
+ ```tsx
100
+ import { Check, Clock, X } from 'lucide-react'
101
+ import { Grid, Stat } from '@iamfiro/design'
102
+
103
+ export default function Example() {
104
+ return (
105
+ <Grid columns={3} gap={2} style={{ width: '100%', maxWidth: 420 }}>
106
+ <Stat tile icon={<Check />} iconTone="success" label="완료" value="137" />
107
+ <Stat tile icon={<X />} iconTone="danger" label="실패" value="3" tone="danger" />
108
+ <Stat tile icon={<Clock />} label="대기" value="12" unit="건" />
109
+ </Grid>
110
+ )
111
+ }
112
+ ```
113
+
114
+ ### StatGroup
115
+
116
+ 지표 띠. 칸 사이 세로선은 Separator이고, 줄이 바뀌어도 각 줄의 첫 칸 앞에는 선이 생기지 않아요. 미리보기 폭을 줄여 보세요.
117
+
118
+ ```tsx
119
+ import { Delta, Card, CardBody, CardHeader, Stat, StatGroup } from '@iamfiro/design'
120
+
121
+ export default function Example() {
122
+ return (
123
+ <Card style={{ width: '100%' }}>
124
+ <CardHeader title="배포 #1284" description="main · 10월 25일 오후 2:14" />
125
+ <CardBody>
126
+ <StatGroup aria-label="배포 지표">
127
+ <Stat label="소요 시간" value="25" unit="초" />
128
+ <Stat label="비용" value="₩2,500" />
129
+ <Stat label="요청" value="128" />
130
+ <Stat label="캐시 적중" value="92%" />
131
+ <Stat label="재시도" value="3" delta={<Delta value={2} higherIs="worse" size="sm" />} />
132
+ <Stat label="오류" value="0" />
133
+ </StatGroup>
134
+ </CardBody>
135
+ </Card>
136
+ )
137
+ }
138
+ ```