@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,246 @@
1
+ # TagInput
2
+
3
+ > 텍스트 입력 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
4
+
5
+ 자유롭게 친 글자를 태그로 모으는 입력 칸. Enter · 쉼표 · Tab으로 추가하고, 붙여넣은 글은 구분자로 나눠 한꺼번에 넣어요. 빈 칸에서 Backspace는 처음엔 마지막 태그를 표시만 하고 두 번째에 지워서, 한 번 잘못 눌러 지워지는 일이 없어요. 중복·규칙 위반은 흔들어서 알리고, 지운 태그는 폭이 접히며 나머지가 미끄러져 와요. 한글 조합 중의 키는 무시해요. 태그 모양·여백은 MultiSelect · Combobox와 같아요(왼쪽 8, 높이 28, 간격 6).
6
+
7
+ ```tsx
8
+ import { TagInput, type TagRejectReason } from '@iamfiro/design'
9
+ ```
10
+
11
+ ## API
12
+
13
+ ### TagInput
14
+
15
+ | prop | 타입 | 기본값 | 설명 |
16
+ | --- | --- | --- | --- |
17
+ | `value` | `string[]` | | 태그 목록(제어). |
18
+ | `defaultValue` | `string[]` | `[]` | 처음 태그(비제어). |
19
+ | `onValueChange` | `(tags: string[]) => void` | | 추가 · 삭제 · 수정할 때. |
20
+ | `delimiters` | `string[]` | `['Enter', 'Tab', ',']` | 태그를 만드는 키. 'Enter' · 'Tab' 또는 한 글자(',' ';' ' '). 붙여넣기는 이 글자(와 줄바꿈 · 탭)로 나눠요. |
21
+ | `validate` | `(tag, tags) => boolean \| string` | | true면 통과, false나 메시지면 거절(칸이 흔들림). |
22
+ | `allowInvalid` | `boolean` | | validate에 걸린 태그도 빨간 톤으로 남겨요. |
23
+ | `allowDuplicates` | `boolean` | `false` | 중복 허용. 기본은 대소문자 · 앞뒤 공백 무시하고 거절, 기존 태그가 흔들려요. |
24
+ | `transform` | `(text: string) => string` | | 태그가 되기 전에 다듬기(앞의 # 제거, 소문자 등). |
25
+ | `max` | `number` | | 최대 개수. 넘치면 칸이 흔들려요. |
26
+ | `onReject` | `(text, reason: 'duplicate' \| 'invalid' \| 'max', message?) => void` | | 거절될 때마다. 이유를 hint에 보여줄 때. |
27
+ | `suggestions` | `string[]` | | 치는 동안 보여줄 추천. ↓로 골라 Enter, 그냥 Enter는 친 글자 그대로. |
28
+ | `editable` | `boolean` | `true` | 더블클릭 · 표시 후 Enter로 그 자리 수정. 비우고 확정하면 삭제. |
29
+ | `placeholder` | `string` | `'입력 후 Enter'` | 태그가 없을 때 문구. |
30
+ | `leading` | `ReactNode` | | 앞쪽 아이콘. |
31
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 최소 높이 36 / 44 / 52px. 태그가 많으면 줄을 바꿔 늘어나요. |
32
+ | `invalid / disabled` | `boolean` | | 오류 · 비활성. |
33
+ | `name` | `string` | | 폼 제출용. 태그마다 hidden input. |
34
+
35
+ ## 예제
36
+
37
+ 예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
38
+
39
+ ### 기본
40
+
41
+ 제어 모드(value + onValueChange). ×로 지우고, 태그를 한 번 누르면 표시, 더블클릭하면 그 자리에서 고쳐요.
42
+
43
+ ```tsx
44
+ import { useState } from 'react'
45
+ import { Field, Stack, Text, TagInput } from '@iamfiro/design'
46
+
47
+ export default function Example() {
48
+ const [tags, setTags] = useState(['디자인 시스템', '접근성', '모션'])
49
+ return (
50
+ <Stack gap={2} style={{ width: 360 }}>
51
+ <Field label="관심 주제" htmlFor="topics" hint="Enter · 쉼표 · Tab으로 추가해요. 쉼표로 이어 붙여넣어도 나눠져요.">
52
+ <TagInput id="topics" value={tags} onValueChange={setTags} placeholder="주제 입력 후 Enter" />
53
+ </Field>
54
+ <Text variant="caption" tone="tertiary">
55
+ {tags.length}개: {tags.join(' · ') || '없음'}
56
+ </Text>
57
+ </Stack>
58
+ )
59
+ }
60
+ ```
61
+
62
+ ### 키보드
63
+
64
+ 입력 칸에 포커스를 둔 채 태그를 오가요. 표시된 태그는 한 단계 진한 회색이에요.
65
+
66
+ ```tsx
67
+ import { Field, Inline, Kbd, Stack, Text, TagInput } from '@iamfiro/design'
68
+
69
+ const KEYS: { keys: string[]; label: string }[] = [
70
+ { keys: ['backspace'], label: '빈 칸에서 한 번: 마지막 태그 표시 · 한 번 더: 삭제' },
71
+ { keys: ['left', 'right'], label: '태그 사이 이동 (맨 오른쪽 다음은 입력 칸)' },
72
+ { keys: ['enter'], label: '표시한 태그 수정 · 더블클릭도 같아요' },
73
+ { keys: ['esc'], label: '표시 해제 · 수정 취소' },
74
+ ]
75
+
76
+ export default function Example() {
77
+ return (
78
+ <Stack gap={4} style={{ width: 400 }}>
79
+ <Field label="라벨" htmlFor="labels" hint="한글을 조합하는 중의 Enter · 쉼표는 글자를 끝내는 데만 쓰여요.">
80
+ <TagInput id="labels" defaultValue={['버그', '긴급', '디자인 검토', '백엔드']} placeholder="라벨 추가" />
81
+ </Field>
82
+ <Stack gap={2}>
83
+ {KEYS.map((k) => (
84
+ <Inline key={k.label} gap={2} align="center">
85
+ <Kbd keys={k.keys} size="sm" />
86
+ <Text variant="body-sm" tone="secondary">
87
+ {k.label}
88
+ </Text>
89
+ </Inline>
90
+ ))}
91
+ </Stack>
92
+ </Stack>
93
+ )
94
+ }
95
+ ```
96
+
97
+ ### 규칙 · 최대 개수
98
+
99
+ validate · max · 중복 거절. onReject로 이유를 받아 Field hint에 보여줘요.
100
+
101
+ ```tsx
102
+ import { useState } from 'react'
103
+ import { Field, TagInput, type TagRejectReason } from '@iamfiro/design'
104
+
105
+ const MAX = 5
106
+
107
+ const MESSAGES: Record<TagRejectReason, string> = {
108
+ duplicate: '이미 있는 키워드예요.',
109
+ invalid: '키워드는 12자까지 쓸 수 있어요.',
110
+ max: `키워드는 ${MAX}개까지예요.`,
111
+ }
112
+
113
+ export default function Example() {
114
+ const [keywords, setKeywords] = useState(['봄 신상', '린넨'])
115
+ const [error, setError] = useState<string | null>(null)
116
+ return (
117
+ <div style={{ width: 380 }}>
118
+ <Field
119
+ label={`검색 키워드 (${keywords.length}/${MAX})`}
120
+ htmlFor="keywords"
121
+ invalid={!!error}
122
+ hint={error ?? '같은 키워드를 다시 넣으면 기존 태그가 흔들려요. 12자를 넘거나 5개를 넘기면 칸이 흔들려요.'}
123
+ >
124
+ <TagInput
125
+ id="keywords"
126
+ value={keywords}
127
+ onValueChange={(next) => {
128
+ setKeywords(next)
129
+ setError(null)
130
+ }}
131
+ max={MAX}
132
+ validate={(text) => text.length <= 12}
133
+ onReject={(_, reason) => setError(MESSAGES[reason])}
134
+ placeholder="키워드 입력"
135
+ />
136
+ </Field>
137
+ </div>
138
+ )
139
+ }
140
+ ```
141
+
142
+ ### 형식이 틀린 태그 남기기
143
+
144
+ allowInvalid면 validate에 걸린 태그도 빨간 톤으로 남겨요(받는 사람 칸). 마우스를 올리면 이유가 보여요.
145
+
146
+ ```tsx
147
+ import { Field, TagInput } from '@iamfiro/design'
148
+ import { Mail } from 'lucide-react'
149
+
150
+ const isEmail = (text: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text) || '이메일 형식이 아니에요'
151
+
152
+ export default function Example() {
153
+ return (
154
+ <div style={{ width: 420 }}>
155
+ <Field label="받는 사람" htmlFor="recipients" hint="스페이스 · 쉼표 · 세미콜론으로 나눠요. 형식이 틀린 주소는 빨간 태그로 남아 바로 고칠 수 있어요(더블클릭).">
156
+ <TagInput
157
+ id="recipients"
158
+ leading={<Mail />}
159
+ defaultValue={['minji.kim@lumen.app', 'dohyun.lee@lumen', 'seoyeon@lumen.app']}
160
+ delimiters={['Enter', 'Tab', ',', ';', ' ']}
161
+ validate={isEmail}
162
+ allowInvalid
163
+ transform={(text) => text.toLowerCase()}
164
+ placeholder="이메일 주소"
165
+ />
166
+ </Field>
167
+ </div>
168
+ )
169
+ }
170
+ ```
171
+
172
+ ### 추천 목록
173
+
174
+ suggestions를 주면 치는 동안 Listbox가 떠요. 이미 넣은 것은 빠져요.
175
+
176
+ ```tsx
177
+ import { Field, TagInput } from '@iamfiro/design'
178
+
179
+ const SKILLS = ['React', 'React Native', 'TypeScript', 'JavaScript', 'Next.js', 'Vite', 'Node.js', 'GraphQL', 'Figma', 'Storybook', '접근성', '애니메이션', '디자인 토큰', '테스트 자동화']
180
+
181
+ export default function Example() {
182
+ return (
183
+ <div style={{ width: 380 }}>
184
+ <Field label="기술 스택" htmlFor="skills" hint="치는 동안 추천이 떠요. ↓로 골라 Enter, 그냥 Enter는 친 글자 그대로 추가해요. 앞의 #은 지워져요.">
185
+ <TagInput
186
+ id="skills"
187
+ suggestions={SKILLS}
188
+ defaultValue={['React', 'TypeScript']}
189
+ transform={(text) => text.replace(/^#+/, '')}
190
+ placeholder="#태그 또는 이름"
191
+ />
192
+ </Field>
193
+ </div>
194
+ )
195
+ }
196
+ ```
197
+
198
+ ### 크기 · 상태 · 폼
199
+
200
+ size · invalid · disabled. name을 주면 태그마다 hidden input으로 제출돼요.
201
+
202
+ ```tsx
203
+ import { useState, type FormEvent } from 'react'
204
+ import { Button, Field, Grid, Stack, Text, TagInput } from '@iamfiro/design'
205
+
206
+ export default function Example() {
207
+ const [sent, setSent] = useState<string[] | null>(null)
208
+ const submit = (e: FormEvent<HTMLFormElement>) => {
209
+ e.preventDefault()
210
+ setSent(new FormData(e.currentTarget).getAll('tags').map(String))
211
+ }
212
+ return (
213
+ <Stack gap={5} style={{ width: 520 }}>
214
+ <Grid columns={2} gap={4}>
215
+ <Field label="작게 (sm)" htmlFor="ti-sm">
216
+ <TagInput id="ti-sm" size="sm" defaultValue={['기획', '리서치']} />
217
+ </Field>
218
+ <Field label="크게 (lg)" htmlFor="ti-lg">
219
+ <TagInput id="ti-lg" size="lg" defaultValue={['출시']} />
220
+ </Field>
221
+ <Field label="태그" invalid hint="태그를 하나 이상 넣어 주세요." htmlFor="ti-err">
222
+ <TagInput id="ti-err" invalid />
223
+ </Field>
224
+ <Field label="잠긴 태그" htmlFor="ti-off">
225
+ <TagInput id="ti-off" defaultValue={['보관됨', '읽기 전용']} disabled />
226
+ </Field>
227
+ </Grid>
228
+ <form onSubmit={submit}>
229
+ <Stack gap={2}>
230
+ <Field label="폼으로 보내기 (name=&quot;tags&quot;)" htmlFor="ti-form">
231
+ <TagInput id="ti-form" name="tags" defaultValue={['서울', '부산']} />
232
+ </Field>
233
+ <Button type="submit" size="sm" style={{ alignSelf: 'flex-start' }}>
234
+ 제출
235
+ </Button>
236
+ {sent && (
237
+ <Text variant="caption" tone="tertiary">
238
+ 보낸 값: {sent.join(', ') || '없음'}
239
+ </Text>
240
+ )}
241
+ </Stack>
242
+ </form>
243
+ </Stack>
244
+ )
245
+ }
246
+ ```
@@ -0,0 +1,228 @@
1
+ # TimePicker
2
+
3
+ > 선택 컨트롤 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
4
+
5
+ 시각 입력. DatePicker와 같은 입력 칸이라 '14:30', '오후 2시 반'처럼 바로 쳐도 되고, 누르면 오전·오후 / 시 / 분 열이 열려요. 열마다 따로 스크롤되고 한 칸씩 맞춰 멈춰요. 고르는 동안은 칸에 미리 보여주기만 하고, Enter · 확인 · 바깥 클릭에서 확정, Esc는 열기 전 시각으로 되돌려요. 값은 언제나 'HH:mm'(24시간) 문자열이에요. 일정처럼 정해진 칸에서 고를 땐 mode="list".
6
+
7
+ ```tsx
8
+ import { TimePicker, formatTime, parseTimeInput } from '@iamfiro/design'
9
+ ```
10
+
11
+ ## API
12
+
13
+ ### TimePicker
14
+
15
+ | prop | 타입 | 기본값 | 설명 |
16
+ | --- | --- | --- | --- |
17
+ | `value` | `string \| null` | | 'HH:mm'(제어). |
18
+ | `defaultValue` | `string \| null` | `null` | 처음 값(비제어). |
19
+ | `onValueChange` | `(time: string \| null) => void` | | 확정하거나 지울 때. |
20
+ | `hourCycle` | `12 \| 24` | `12` | '오후 2:30' + 오전·오후 열, 또는 '14:30'. |
21
+ | `minuteStep` | `number` | `5 (list: 30)` | 분 열 간격 · 목록 칸 간격. |
22
+ | `min / max` | `string` | | 고를 수 있는 처음 · 마지막 시각('HH:mm'). |
23
+ | `isTimeDisabled` | `(time: string) => boolean` | | 추가로 막을 시각(점심시간, 예약된 칸). |
24
+ | `mode` | `'columns' \| 'list'` | `'columns'` | 열 팝오버 또는 Listbox 목록. |
25
+ | `durationFrom` | `string` | | 목록 모드: 이 시각부터의 길이를 설명으로 보여주고, 이전 칸은 막아요(종료 시간). |
26
+ | `formatValue` | `(time: string) => string` | `formatTime` | 칸에 쓰는 형식. |
27
+ | `placeholder` | `string` | `'시간 입력 또는 선택'` | 비었을 때 문구. |
28
+ | `clearable` | `boolean` | `true` | × 지우기. |
29
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 높이 36 / 44 / 52px. |
30
+ | `invalid / disabled` | `boolean` | | 오류 · 비활성. |
31
+ | `align` | `'start' \| 'end'` | `'start'` | 팝오버 정렬. |
32
+ | `name` | `string` | | 폼 제출용 hidden input('HH:mm'). |
33
+
34
+ ### 도우미 함수
35
+
36
+ | prop | 타입 | 기본값 | 설명 |
37
+ | --- | --- | --- | --- |
38
+ | `formatTime(time, hourCycle?)` | `string` | | '14:30' → '오후 2:30' / '14:30'. |
39
+ | `parseTimeInput(text)` | `string \| null` | | '오후 2시 반' → '14:30'. 못 읽으면 null. |
40
+ | `timeToMinutes / minutesToTime` | `number · string` | | 'HH:mm' ↔ 자정부터의 분. |
41
+ | `formatDuration(minutes)` | `string` | | 90 → '1시간 30분'. |
42
+
43
+ ## 예제
44
+
45
+ 예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
46
+
47
+ ### 기본
48
+
49
+ '오후 2:30'처럼 보이고 값은 '14:30'. ↓로 열에 들어가 ↑↓로 바꾸고, ←→ · Tab으로 열을 옮겨요. Enter 확정, Esc 취소.
50
+
51
+ ```tsx
52
+ import { useState } from 'react'
53
+ import { Field, Stack, Text, TimePicker } from '@iamfiro/design'
54
+
55
+ export default function Example() {
56
+ const [time, setTime] = useState<string | null>('14:30')
57
+ return (
58
+ <Stack gap={2} style={{ width: 260 }}>
59
+ <Field label="알림 시간" htmlFor="remind-at">
60
+ <TimePicker id="remind-at" value={time} onValueChange={setTime} />
61
+ </Field>
62
+ <Text variant="caption" tone="tertiary">
63
+ 값: {time ?? '없음'}
64
+ </Text>
65
+ </Stack>
66
+ )
67
+ }
68
+ ```
69
+
70
+ ### 직접 입력
71
+
72
+ 완전한 시각이 되면 열이 따라와요. 못 알아들으면 빨갛게, 칸을 벗어나면 원래 값으로. 오전·오후가 없으면 24시간으로 읽어요.
73
+
74
+ ```tsx
75
+ import { useState } from 'react'
76
+ import { Field, Stack, Text, TimePicker } from '@iamfiro/design'
77
+
78
+ export default function Example() {
79
+ const [time, setTime] = useState<string | null>(null)
80
+ return (
81
+ <Stack gap={2} style={{ width: 260 }}>
82
+ <Field label="퇴근 시간" htmlFor="leave-at" hint="예) 18:30 · 1830 · 오후 6시 반 · 6:30pm · 정오">
83
+ <TimePicker id="leave-at" value={time} onValueChange={setTime} />
84
+ </Field>
85
+ <Text variant="caption" tone="tertiary">
86
+ 값: {time ?? '없음'}
87
+ </Text>
88
+ </Stack>
89
+ )
90
+ }
91
+ ```
92
+
93
+ ### 12시간 · 24시간 · 간격
94
+
95
+ hourCycle과 minuteStep. 간격에 안 맞게 친 분(09:07)도 열에 끼워 보여줘요.
96
+
97
+ ```tsx
98
+ import { Field, Grid, TimePicker } from '@iamfiro/design'
99
+
100
+ export default function Example() {
101
+ return (
102
+ <Grid columns={2} gap={4} style={{ width: 480 }}>
103
+ <Field label="12시간 · 5분 단위" htmlFor="tp-12">
104
+ <TimePicker id="tp-12" defaultValue="09:05" />
105
+ </Field>
106
+ <Field label="24시간 · 15분 단위" htmlFor="tp-24">
107
+ <TimePicker id="tp-24" hourCycle={24} minuteStep={15} defaultValue="21:45" />
108
+ </Field>
109
+ </Grid>
110
+ )
111
+ }
112
+ ```
113
+
114
+ ### 고를 수 있는 시간 제한
115
+
116
+ min · max · isTimeDisabled. 모든 분이 막힌 시와 오전·오후도 함께 막혀요.
117
+
118
+ ```tsx
119
+ import { Field, TimePicker, timeToMinutes } from '@iamfiro/design'
120
+
121
+ // 점심시간(12:00 – 12:59)은 예약할 수 없어요.
122
+ const isLunch = (time: string) => timeToMinutes(time) >= 12 * 60 && timeToMinutes(time) < 13 * 60
123
+
124
+ export default function Example() {
125
+ return (
126
+ <div style={{ width: 260 }}>
127
+ <Field label="방문 예약" htmlFor="visit-at" hint="09:00 – 18:00, 점심시간 제외. 막힌 시간은 취소선이에요.">
128
+ <TimePicker id="visit-at" min="09:00" max="18:00" minuteStep={10} isTimeDisabled={isLunch} />
129
+ </Field>
130
+ </div>
131
+ )
132
+ }
133
+ ```
134
+
135
+ ### 목록 모드(일정)
136
+
137
+ mode="list"는 30분 간격 목록. 종료 칸은 durationFrom으로 길이('1시간 30분')를 보여주고 시작 이전은 막아요. 입력 칸에서 ↑↓로 옮기며 계속 칠 수 있어요.
138
+
139
+ ```tsx
140
+ import { useState } from 'react'
141
+ import { Field, Grid, TimePicker, minutesToTime, timeToMinutes } from '@iamfiro/design'
142
+
143
+ export default function Example() {
144
+ const [start, setStart] = useState<string | null>('10:00')
145
+ const [end, setEnd] = useState<string | null>('11:00')
146
+ return (
147
+ <Grid columns={2} gap={4} style={{ width: 480 }}>
148
+ <Field label="시작" htmlFor="meet-start">
149
+ <TimePicker
150
+ id="meet-start"
151
+ mode="list"
152
+ min="08:00"
153
+ max="20:00"
154
+ value={start}
155
+ onValueChange={(next) => {
156
+ setStart(next)
157
+ // 시작을 옮기면 끝도 같은 길이만큼 따라가요.
158
+ if (next && start && end) {
159
+ const length = timeToMinutes(end) - timeToMinutes(start)
160
+ setEnd(minutesToTime(Math.min(timeToMinutes(next) + length, 21 * 60)))
161
+ }
162
+ }}
163
+ />
164
+ </Field>
165
+ <Field label="종료" htmlFor="meet-end">
166
+ <TimePicker id="meet-end" mode="list" min="08:00" max="21:00" durationFrom={start ?? undefined} value={end} onValueChange={setEnd} />
167
+ </Field>
168
+ </Grid>
169
+ )
170
+ }
171
+ ```
172
+
173
+ ### DatePicker와 함께
174
+
175
+ 날짜와 시각을 따로 받아 하나의 Date로 합쳐요.
176
+
177
+ ```tsx
178
+ import { useState } from 'react'
179
+ import { Field, Grid, Stack, Text, DatePicker, TimePicker, formatDate, formatTime, timeToMinutes } from '@iamfiro/design'
180
+
181
+ export default function Example() {
182
+ const [date, setDate] = useState<Date | null>(new Date(2026, 9, 28))
183
+ const [time, setTime] = useState<string | null>('15:00')
184
+ const at = date && time ? new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, timeToMinutes(time)) : null
185
+ return (
186
+ <Stack gap={2} style={{ width: 480 }}>
187
+ <Grid columns={2} gap={3}>
188
+ <Field label="게시 날짜" htmlFor="publish-date">
189
+ <DatePicker id="publish-date" value={date} onValueChange={setDate} />
190
+ </Field>
191
+ <Field label="게시 시간" htmlFor="publish-time">
192
+ <TimePicker id="publish-time" value={time} onValueChange={setTime} minuteStep={15} />
193
+ </Field>
194
+ </Grid>
195
+ <Text variant="caption" tone="tertiary">
196
+ {at && time ? `${formatDate(at)} ${formatTime(time)}에 게시돼요` : '날짜와 시간을 골라 주세요'}
197
+ </Text>
198
+ </Stack>
199
+ )
200
+ }
201
+ ```
202
+
203
+ ### 크기와 상태
204
+
205
+ size · invalid · disabled. Field와 함께 쓰면 라벨이 연결돼요.
206
+
207
+ ```tsx
208
+ import { Field, Grid, TimePicker } from '@iamfiro/design'
209
+
210
+ export default function Example() {
211
+ return (
212
+ <Grid columns={2} gap={4} style={{ width: 480 }}>
213
+ <Field label="작게 (sm)" htmlFor="tp-sm">
214
+ <TimePicker id="tp-sm" size="sm" defaultValue="08:30" />
215
+ </Field>
216
+ <Field label="크게 (lg)" htmlFor="tp-lg">
217
+ <TimePicker id="tp-lg" size="lg" />
218
+ </Field>
219
+ <Field label="마감 시간" invalid hint="마감 시간을 골라 주세요." htmlFor="tp-err">
220
+ <TimePicker id="tp-err" invalid />
221
+ </Field>
222
+ <Field label="정산 시간" htmlFor="tp-off">
223
+ <TimePicker id="tp-off" defaultValue="00:00" disabled />
224
+ </Field>
225
+ </Grid>
226
+ )
227
+ }
228
+ ```