@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,460 @@
1
+ # DataGrid
2
+
3
+ > 데이터 표시 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
4
+
5
+ Notion 데이터베이스처럼 타입이 있는 속성과 보기(정렬·필터·그룹·검색)를, Google Sheets·Numbers처럼 범위 선택·키보드 편집·복사/붙여넣기·채우기 핸들을 갖춘 표. 열은 끌어서 순서·너비를 바꾸고 고정·숨기기·타입 변경까지 머리글 메뉴에서 할 수 있어요. 모든 데이터 변경은 실행 취소(⌘/Ctrl+Z)할 수 있고, 행이 많으면 보이는 줄만 그려요. 단순히 보여 주기만 하는 표는 Table을 쓰세요.
6
+
7
+ ```tsx
8
+ import { DataGrid, type GridColumn, type GridRow } from '@iamfiro/design'
9
+ ```
10
+
11
+ ## 디자인 규칙
12
+
13
+ - 표: 보여 주기만 하면 `Table`, 편집·정렬·필터·범위 선택이 필요하면 `DataGrid`. 셀 편집·키 입력·클립보드·한글 IME는 셀 위에 놓인 textarea 하나가 모두 받는다(조합 중에 포커스를 옮기면 한글이 깨진다).
14
+
15
+ ## API
16
+
17
+ ### DataGrid
18
+
19
+ | prop | 타입 | 기본값 | 설명 |
20
+ | --- | --- | --- | --- |
21
+ | `rows / defaultRows / onRowsChange` | `R[] / R[] / (rows) => void` | | 행 데이터(제어/비제어). 행마다 고유한 id가 있어야 해요. |
22
+ | `columns / defaultColumns / onColumnsChange` | `GridColumn<R>[]` | | 열 정의. 너비·순서·고정·숨김·타입 변경이 여기에 반영돼요. |
23
+ | `view / defaultView / onViewChange` | `GridView / Partial<GridView>` | | 정렬·필터·그룹·검색·행 높이 같은 보기 상태. |
24
+ | `selectedRowIds / onSelectedRowIdsChange` | `string[]` | | 체크된 행(일괄 작업 대상). |
25
+ | `createRow` | `(values) => R` | | 새 행을 만들 때. values에는 현재 필터·그룹에 맞는 값이 들어 있어요. |
26
+ | `onCellEdit` | `({ row, column, value, previous }) => void` | | 셀이 확정될 때마다(입력·붙여넣기·채우기·지우기). |
27
+ | `readOnly` | `boolean` | `false` | 데이터 변경 금지. 보기 조작과 복사는 돼요. |
28
+ | `columnEditing` | `boolean` | `true` | 사용자가 속성을 추가·이름 변경·타입 변경·삭제할 수 있는지. |
29
+ | `toolbar` | `boolean` | `true` | 검색·필터·정렬·그룹·속성·보기 설정·실행 취소·새로 만들기 막대. |
30
+ | `title / actions` | `ReactNode` | | 툴바 왼쪽 제목 / 오른쪽 끝에 붙일 버튼. |
31
+ | `footer` | `boolean` | `true` | 열마다 계산을 고르는 아래 줄. |
32
+ | `height` | `number \| string` | `520` | 스크롤 영역 최대 높이. |
33
+ | `emptyState` | `ReactNode` | | 보일 행이 없을 때 대신 보여 줄 내용. |
34
+
35
+ ### GridColumn
36
+
37
+ | prop | 타입 | 기본값 | 설명 |
38
+ | --- | --- | --- | --- |
39
+ | `key / name` (필수) | `string` | | 값을 읽고 쓰는 필드 / 머리글 이름. |
40
+ | `type` | `'text' \| 'number' \| 'select' \| 'multiSelect' \| 'checkbox' \| 'date' \| 'url'` | `'text'` | 표시·편집·정렬·필터·붙여넣기 해석이 타입을 따라요. |
41
+ | `width / minWidth` | `number` | `180 / 64` | 열 너비(px). 경계를 더블클릭하면 내용에 맞춰요. |
42
+ | `pinned / hidden / wrap` | `boolean` | | 왼쪽 고정 / 숨김 / 줄바꿈(켜면 행 가상화가 꺼져요). |
43
+ | `options / creatable` | `GridOption[] / boolean` | | 선택지({ label, tone }). 없는 값을 입력하면 새로 만들어요(creatable: false면 거절). |
44
+ | `format / decimals` | `'plain' \| 'comma' \| 'percent' \| 'won' \| 'usd'` | `'comma'` | 숫자 표시 형식. |
45
+ | `aggregate` | `GridAggregate` | | 아래 줄 계산('sum', 'avg', 'percentChecked', 'earliest' …). |
46
+ | `readOnly / locked` | `boolean` | | 셀 편집 금지 / 속성 삭제·타입 변경 금지. |
47
+ | `align` | `'start' \| 'center' \| 'end'` | | 정렬. 숫자는 기본이 오른쪽. |
48
+ | `render` | `(value, row) => ReactNode` | | 셀 모양을 직접 그릴 때. |
49
+ | `validate` | `(value, row) => string \| null` | | 메시지를 돌려주면 그 편집을 거절해요. |
50
+
51
+ ### 키보드
52
+
53
+ | prop | 타입 | 기본값 | 설명 |
54
+ | --- | --- | --- | --- |
55
+ | `↑ ↓ ← → · Tab` | `이동` | | Shift를 같이 누르면 범위 확장, ⌘/Ctrl은 끝까지. |
56
+ | `Enter · F2 · 더블클릭` | `편집` | | 바로 글자를 쳐도 편집이 시작돼요(한글 입력 포함). Enter 확정 후 아래로, Tab은 오른쪽, Esc 취소. |
57
+ | `Shift+Enter · Alt+Enter` | `줄바꿈` | | 텍스트 셀 안에서 새 줄. |
58
+ | `Delete · Backspace` | `지우기` | | 선택 범위의 값을 지워요. |
59
+ | `Space` | `체크` | | 체크박스 열에서 범위를 한꺼번에 토글. |
60
+ | `⌘/Ctrl + C · X · V` | `클립보드` | | TSV로 주고받아 Sheets·Excel·Numbers와 호환돼요. |
61
+ | `⌘/Ctrl + D` | `아래로 채우기` | | 범위의 첫 행 값을 아래로 복사(숫자는 이어서). |
62
+ | `⌘/Ctrl + A · Z · Shift+Z` | `전체 선택 · 실행 취소 · 다시 실행` | | |
63
+ | `Esc (끄는 중)` | `취소` | | 열 너비·순서, 행 순서, 채우기를 끄는 도중 Esc면 원래대로. |
64
+
65
+ ## 예제
66
+
67
+ 예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
68
+
69
+ ### 데이터베이스 (Notion)
70
+
71
+ 텍스트·숫자·선택·다중 선택·날짜·체크박스·URL 속성. 머리글의 ⌄ 메뉴(또는 우클릭)에서 이름·타입·정렬·필터·그룹·고정·줄바꿈·삽입·삭제를 하고, 맨 오른쪽 +로 속성을 추가해요. 행 왼쪽 손잡이로 순서를 바꾸고, 체크하면 아래에 일괄 작업 막대가 떠요. 맨 아래 칸을 누르면 합계·평균·체크 비율 같은 계산을 고를 수 있어요.
72
+
73
+ ```tsx
74
+ import { DataGrid, type GridColumn, type GridRow } from '@iamfiro/design'
75
+
76
+ interface Task extends GridRow {
77
+ title: string
78
+ status: string | null
79
+ owner: string | null
80
+ tags: string[]
81
+ due: string | null
82
+ budget: number | null
83
+ progress: number | null
84
+ done: boolean
85
+ link: string | null
86
+ }
87
+
88
+ const COLUMNS: GridColumn<Task>[] = [
89
+ { key: 'title', name: '작업', type: 'text', width: 240, pinned: true, aggregate: 'count', locked: true },
90
+ {
91
+ key: 'status',
92
+ name: '상태',
93
+ type: 'select',
94
+ width: 130,
95
+ options: [
96
+ { label: '시작 전', tone: 'neutral' },
97
+ { label: '진행 중', tone: 'info' },
98
+ { label: '검토', tone: 'warning' },
99
+ { label: '완료', tone: 'success' },
100
+ ],
101
+ },
102
+ {
103
+ key: 'owner',
104
+ name: '담당자',
105
+ type: 'select',
106
+ width: 120,
107
+ options: [{ label: '김민지' }, { label: '이도현' }, { label: '박서연' }, { label: '최준호' }],
108
+ },
109
+ {
110
+ key: 'tags',
111
+ name: '태그',
112
+ type: 'multiSelect',
113
+ width: 200,
114
+ options: [
115
+ { label: '디자인', tone: 'accent' },
116
+ { label: '개발', tone: 'info' },
117
+ { label: '마케팅', tone: 'warning' },
118
+ { label: '긴급', tone: 'danger' },
119
+ ],
120
+ },
121
+ { key: 'due', name: '마감일', type: 'date', width: 130, aggregate: 'earliest' },
122
+ { key: 'budget', name: '예산', type: 'number', format: 'won', width: 140, aggregate: 'sum' },
123
+ { key: 'progress', name: '진행률', type: 'number', format: 'percent', width: 110, aggregate: 'avg' },
124
+ { key: 'done', name: '완료', type: 'checkbox', width: 90, aggregate: 'percentChecked' },
125
+ { key: 'link', name: '문서', type: 'url', width: 200 },
126
+ ]
127
+
128
+ const ROWS: Task[] = [
129
+ { id: 't1', title: '랜딩 페이지 리뉴얼', status: '진행 중', owner: '김민지', tags: ['디자인', '마케팅'], due: '2026-11-03', budget: 4200000, progress: 0.6, done: false, link: 'figma.com/lumen-landing' },
130
+ { id: 't2', title: '결제 모듈 교체', status: '검토', owner: '이도현', tags: ['개발', '긴급'], due: '2026-10-28', budget: 8800000, progress: 0.9, done: false, link: 'github.com/lumen/pay' },
131
+ { id: 't3', title: '10월 뉴스레터', status: '완료', owner: '박서연', tags: ['마케팅'], due: '2026-10-20', budget: 600000, progress: 1, done: true, link: null },
132
+ { id: 't4', title: '디자인 토큰 정리', status: '진행 중', owner: '김민지', tags: ['디자인', '개발'], due: '2026-11-10', budget: 1500000, progress: 0.35, done: false, link: 'lumen.app/tokens' },
133
+ { id: 't5', title: '온보딩 인터뷰 5건', status: '시작 전', owner: '최준호', tags: ['디자인'], due: '2026-11-14', budget: 500000, progress: 0, done: false, link: null },
134
+ { id: 't6', title: '모바일 앱 2.0 QA', status: '진행 중', owner: '이도현', tags: ['개발'], due: '2026-11-07', budget: 3200000, progress: 0.45, done: false, link: null },
135
+ { id: 't7', title: '파트너 웨비나', status: '시작 전', owner: '박서연', tags: ['마케팅'], due: '2026-11-21', budget: 2000000, progress: 0.1, done: false, link: null },
136
+ { id: 't8', title: '접근성 감사', status: '완료', owner: '최준호', tags: ['디자인', '개발'], due: '2026-10-15', budget: 1200000, progress: 1, done: true, link: 'lumen.app/a11y' },
137
+ { id: 't9', title: '요금제 페이지 A/B 테스트', status: '검토', owner: '김민지', tags: ['마케팅', '긴급'], due: '2026-10-30', budget: 900000, progress: 0.8, done: false, link: null },
138
+ { id: 't10', title: '고객 지원 매크로 정비', status: '시작 전', owner: null, tags: [], due: null, budget: null, progress: null, done: false, link: null },
139
+ ]
140
+
141
+ export default function Example() {
142
+ return <DataGrid title="4분기 로드맵" defaultColumns={COLUMNS} defaultRows={ROWS} height={460} style={{ width: '100%' }} />
143
+ }
144
+ ```
145
+
146
+ ### 스프레드시트 (Sheets · Numbers)
147
+
148
+ 셀을 끌어 범위를 고르고, 선택 오른쪽 아래 점(채우기 핸들)을 끌면 1, 2 → 3, 4처럼 이어서 채워요. Sheets·Excel에서 복사한 범위를 그대로 붙여 넣을 수 있고, 범위를 넘으면 행이 새로 생겨요. 합계 열은 readOnly로 두고 onRowsChange에서 다시 계산해요.
149
+
150
+ ```tsx
151
+ import { useState } from 'react'
152
+ import { DataGrid, type GridColumn, type GridRow } from '@iamfiro/design'
153
+
154
+ interface Line extends GridRow {
155
+ item: string
156
+ oct: number | null
157
+ nov: number | null
158
+ dec: number | null
159
+ total: number
160
+ }
161
+
162
+ const MONTHS = ['oct', 'nov', 'dec'] as const
163
+
164
+ const COLUMNS: GridColumn<Line>[] = [
165
+ { key: 'item', name: '항목', width: 180, pinned: true },
166
+ { key: 'oct', name: '10월', type: 'number', format: 'comma', width: 120, aggregate: 'sum' },
167
+ { key: 'nov', name: '11월', type: 'number', format: 'comma', width: 120, aggregate: 'sum' },
168
+ { key: 'dec', name: '12월', type: 'number', format: 'comma', width: 120, aggregate: 'sum' },
169
+ { key: 'total', name: '합계', type: 'number', format: 'comma', width: 140, aggregate: 'sum', readOnly: true, locked: true },
170
+ ]
171
+
172
+ // 합계 열은 직접 고칠 수 없고, 월별 값이 바뀔 때마다 다시 계산해요.
173
+ const withTotal = (l: Line): Line => ({ ...l, total: MONTHS.reduce((s, m) => s + (l[m] ?? 0), 0) })
174
+
175
+ const START: Line[] = [
176
+ { id: 'l1', item: '클라우드 서버', oct: 1820000, nov: 1910000, dec: 2000000, total: 0 },
177
+ { id: 'l2', item: '디자인 툴 구독', oct: 360000, nov: 360000, dec: 360000, total: 0 },
178
+ { id: 'l3', item: '광고', oct: 2500000, nov: 3000000, dec: 3500000, total: 0 },
179
+ { id: 'l4', item: '외주 번역', oct: 800000, nov: null, dec: 450000, total: 0 },
180
+ { id: 'l5', item: '사무용품', oct: 120000, nov: 95000, dec: 130000, total: 0 },
181
+ ].map(withTotal)
182
+
183
+ export default function Example() {
184
+ const [rows, setRows] = useState(START)
185
+ return (
186
+ <DataGrid
187
+ title="4분기 비용"
188
+ rows={rows}
189
+ onRowsChange={(next) => setRows(next.map(withTotal))}
190
+ defaultColumns={COLUMNS}
191
+ defaultView={{ gridlines: true, density: 'compact' }}
192
+ createRow={(values) => withTotal({ id: crypto.randomUUID(), item: '', oct: null, nov: null, dec: null, total: 0, ...values })}
193
+ height={360}
194
+ style={{ width: '100%' }}
195
+ />
196
+ )
197
+ }
198
+ ```
199
+
200
+ ### 저장된 보기
201
+
202
+ view / onViewChange로 정렬·필터·그룹·접힘 상태를 밖에서 들고 있어요. 탭마다 다른 보기를 저장하고, 표에서 바꾼 내용도 그 탭에 남아요. 그룹 머리의 +로 그 그룹 값이 채워진 행을 만들어요.
203
+
204
+ ```tsx
205
+ import { useState } from 'react'
206
+ import { SegmentedControl, DEFAULT_GRID_VIEW, DataGrid, type GridColumn, type GridRow, type GridView } from '@iamfiro/design'
207
+
208
+ interface Ticket extends GridRow {
209
+ title: string
210
+ status: string
211
+ priority: string
212
+ owner: string
213
+ due: string
214
+ }
215
+
216
+ const COLUMNS: GridColumn<Ticket>[] = [
217
+ { key: 'title', name: '제목', width: 260 },
218
+ {
219
+ key: 'status',
220
+ name: '상태',
221
+ type: 'select',
222
+ width: 120,
223
+ options: [
224
+ { label: '접수', tone: 'neutral' },
225
+ { label: '처리 중', tone: 'info' },
226
+ { label: '해결', tone: 'success' },
227
+ ],
228
+ },
229
+ {
230
+ key: 'priority',
231
+ name: '우선순위',
232
+ type: 'select',
233
+ width: 110,
234
+ options: [
235
+ { label: '높음', tone: 'danger' },
236
+ { label: '보통', tone: 'warning' },
237
+ { label: '낮음', tone: 'neutral' },
238
+ ],
239
+ },
240
+ { key: 'owner', name: '담당자', type: 'select', width: 110, options: [{ label: '김민지' }, { label: '이도현' }, { label: '박서연' }] },
241
+ { key: 'due', name: '기한', type: 'date', width: 120 },
242
+ ]
243
+
244
+ const ROWS: Ticket[] = [
245
+ { id: 'k1', title: '로그인 후 빈 화면이 떠요', status: '처리 중', priority: '높음', owner: '이도현', due: '2026-10-26' },
246
+ { id: 'k2', title: '영수증 이메일이 늦게 와요', status: '접수', priority: '보통', owner: '박서연', due: '2026-10-29' },
247
+ { id: 'k3', title: '다크 모드 대비가 낮아요', status: '해결', priority: '낮음', owner: '김민지', due: '2026-10-21' },
248
+ { id: 'k4', title: 'CSV 내보내기 한글 깨짐', status: '처리 중', priority: '높음', owner: '김민지', due: '2026-10-27' },
249
+ { id: 'k5', title: '팀 초대 링크 만료 기간', status: '접수', priority: '낮음', owner: '이도현', due: '2026-11-04' },
250
+ { id: 'k6', title: '알림 설정이 저장되지 않아요', status: '처리 중', priority: '보통', owner: '박서연', due: '2026-10-31' },
251
+ ]
252
+
253
+ type ViewId = 'all' | 'open' | 'byOwner' | 'urgent'
254
+
255
+ // 탭마다 보기(정렬·필터·그룹)를 따로 저장해요. 표에서 바꾼 내용도 그 탭에 남아요.
256
+ const PRESETS: Record<ViewId, GridView> = {
257
+ all: { ...DEFAULT_GRID_VIEW },
258
+ open: { ...DEFAULT_GRID_VIEW, filters: [{ id: 'f1', key: 'status', op: 'isNot', value: '해결' }], sorts: [{ key: 'due', dir: 'asc' }] },
259
+ byOwner: { ...DEFAULT_GRID_VIEW, groupBy: 'owner' },
260
+ urgent: { ...DEFAULT_GRID_VIEW, groupBy: 'priority', collapsed: ['낮음'], sorts: [{ key: 'due', dir: 'asc' }] },
261
+ }
262
+
263
+ export default function Example() {
264
+ const [tab, setTab] = useState<ViewId>('open')
265
+ const [views, setViews] = useState(PRESETS)
266
+
267
+ return (
268
+ <div style={{ display: 'grid', gap: 12, width: '100%' }}>
269
+ <SegmentedControl
270
+ variant="underline"
271
+ value={tab}
272
+ onChange={setTab}
273
+ ariaLabel="저장된 보기"
274
+ options={[
275
+ { value: 'all', label: '전체' },
276
+ { value: 'open', label: '미해결' },
277
+ { value: 'byOwner', label: '담당자별' },
278
+ { value: 'urgent', label: '우선순위별' },
279
+ ]}
280
+ />
281
+ <DataGrid
282
+ toolbar
283
+ defaultColumns={COLUMNS}
284
+ defaultRows={ROWS}
285
+ view={views[tab]}
286
+ onViewChange={(view) => setViews((v) => ({ ...v, [tab]: view }))}
287
+ height={420}
288
+ aria-label="지원 티켓"
289
+ />
290
+ </div>
291
+ )
292
+ }
293
+ ```
294
+
295
+ ### 1만 행
296
+
297
+ 보이는 줄만 그려서 1만 행도 부드럽게 스크롤돼요. 검색·정렬·필터·하단 계산은 전체 행 기준이에요.
298
+
299
+ ```tsx
300
+ import { useMemo } from 'react'
301
+ import { DataGrid, type GridColumn, type GridRow } from '@iamfiro/design'
302
+
303
+ interface Order extends GridRow {
304
+ no: number
305
+ customer: string
306
+ region: string
307
+ amount: number
308
+ ordered: string
309
+ paid: boolean
310
+ }
311
+
312
+ const NAMES = ['김민지', '이도현', '박서연', '최준호', '정하은', '강지우', '윤서준', '한예린']
313
+ const REGIONS = ['서울', '부산', '대구', '인천', '광주', '대전']
314
+
315
+ const COLUMNS: GridColumn<Order>[] = [
316
+ { key: 'no', name: '주문 번호', type: 'number', format: 'plain', width: 120, readOnly: true, aggregate: 'count' },
317
+ { key: 'customer', name: '고객', width: 140 },
318
+ { key: 'region', name: '지역', type: 'select', width: 110, options: REGIONS.map((label) => ({ label })) },
319
+ { key: 'amount', name: '금액', type: 'number', format: 'won', width: 140, aggregate: 'sum' },
320
+ { key: 'ordered', name: '주문일', type: 'date', width: 130, aggregate: 'latest' },
321
+ { key: 'paid', name: '결제', type: 'checkbox', width: 90, aggregate: 'percentChecked' },
322
+ ]
323
+
324
+ /** 1만 행을 만들어요(같은 결과가 나오도록 간단한 의사 난수). */
325
+ function makeOrders(count: number): Order[] {
326
+ let seed = 7
327
+ const rand = () => (seed = (seed * 16807) % 2147483647) / 2147483647
328
+ return Array.from({ length: count }, (_, i) => {
329
+ const day = new Date(2026, 0, 1 + Math.floor(rand() * 298))
330
+ return {
331
+ id: `o${i}`,
332
+ no: 100001 + i,
333
+ customer: NAMES[Math.floor(rand() * NAMES.length)],
334
+ region: REGIONS[Math.floor(rand() * REGIONS.length)],
335
+ amount: Math.round(rand() * 400) * 1000 + 9000,
336
+ ordered: `${day.getFullYear()}-${String(day.getMonth() + 1).padStart(2, '0')}-${String(day.getDate()).padStart(2, '0')}`,
337
+ paid: rand() > 0.2,
338
+ }
339
+ })
340
+ }
341
+
342
+ export default function Example() {
343
+ const rows = useMemo(() => makeOrders(10000), [])
344
+ return <DataGrid title="주문 10,000건" defaultRows={rows} defaultColumns={COLUMNS} defaultView={{ density: 'compact' }} height={440} style={{ width: '100%' }} />
345
+ }
346
+ ```
347
+
348
+ ### 커스텀 셀 · 검증
349
+
350
+ render로 셀 모양을 바꾸고(편집은 type을 따라요), validate로 잘못된 값을 막아요(칸이 흔들리고 편집이 유지돼요). creatable: false인 선택 열은 새 선택지를 만들 수 없고, readOnly 열은 고칠 수 없어요. onCellEdit으로 모든 변경을 받아요.
351
+
352
+ ```tsx
353
+ import { Avatar, Text, DataGrid, toast, type GridColumn, type GridRow } from '@iamfiro/design'
354
+
355
+ interface Member extends GridRow {
356
+ name: string
357
+ email: string
358
+ role: string
359
+ seats: number
360
+ joined: string
361
+ }
362
+
363
+ const COLUMNS: GridColumn<Member>[] = [
364
+ {
365
+ key: 'name',
366
+ name: '이름',
367
+ width: 180,
368
+ // render는 보이는 모습만 바꿔요. 편집·복사·정렬은 여전히 type(텍스트)을 따라요.
369
+ render: (value) => (
370
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
371
+ <Avatar name={String(value ?? '')} size={22} />
372
+ <Text variant="label-md">{String(value ?? '')}</Text>
373
+ </span>
374
+ ),
375
+ },
376
+ {
377
+ key: 'email',
378
+ name: '이메일',
379
+ width: 220,
380
+ validate: (v) => (v && !/^\S+@\S+\.\S+$/.test(String(v)) ? '이메일 형식이 아니에요' : null),
381
+ },
382
+ {
383
+ key: 'role',
384
+ name: '역할',
385
+ type: 'select',
386
+ width: 120,
387
+ creatable: false,
388
+ options: [
389
+ { label: '관리자', tone: 'accent' },
390
+ { label: '편집자', tone: 'success' },
391
+ { label: '뷰어', tone: 'neutral' },
392
+ ],
393
+ },
394
+ {
395
+ key: 'seats',
396
+ name: '좌석',
397
+ type: 'number',
398
+ format: 'plain',
399
+ width: 90,
400
+ validate: (v) => (typeof v === 'number' && (v < 1 || v > 50 || !Number.isInteger(v)) ? '1–50 사이 정수만 돼요' : null),
401
+ },
402
+ { key: 'joined', name: '가입일', type: 'date', width: 130, readOnly: true },
403
+ ]
404
+
405
+ const ROWS: Member[] = [
406
+ { id: 'm1', name: '김민지', email: 'minji@lumen.app', role: '관리자', seats: 12, joined: '2025-03-02' },
407
+ { id: 'm2', name: '이도현', email: 'dohyun@lumen.app', role: '편집자', seats: 4, joined: '2025-07-18' },
408
+ { id: 'm3', name: '박서연', email: 'seoyeon@lumen.app', role: '편집자', seats: 2, joined: '2026-01-09' },
409
+ { id: 'm4', name: '최준호', email: 'junho@lumen.app', role: '뷰어', seats: 1, joined: '2026-09-30' },
410
+ ]
411
+
412
+ export default function Example() {
413
+ return (
414
+ <DataGrid
415
+ title="멤버"
416
+ defaultColumns={COLUMNS}
417
+ defaultRows={ROWS}
418
+ columnEditing={false}
419
+ footer={false}
420
+ defaultView={{ rowNumbers: false }}
421
+ onCellEdit={({ row, column, value }) => toast.success(`${row.name} · ${column.name} → ${String(value ?? '비움')}`)}
422
+ height={320}
423
+ style={{ width: '100%' }}
424
+ />
425
+ )
426
+ }
427
+ ```
428
+
429
+ ### 읽기 전용
430
+
431
+ readOnly면 데이터는 바뀌지 않지만 선택·복사·정렬·너비 조절은 그대로 돼요. toolbar·footer를 끄면 작은 표로도 써요.
432
+
433
+ ```tsx
434
+ import { DataGrid, type GridColumn, type GridRow } from '@iamfiro/design'
435
+
436
+ interface Release extends GridRow {
437
+ version: string
438
+ date: string
439
+ changes: number
440
+ stable: boolean
441
+ }
442
+
443
+ const COLUMNS: GridColumn<Release>[] = [
444
+ { key: 'version', name: '버전', width: 120 },
445
+ { key: 'date', name: '배포일', type: 'date', width: 130 },
446
+ { key: 'changes', name: '변경', type: 'number', width: 100 },
447
+ { key: 'stable', name: '안정판', type: 'checkbox', width: 90 },
448
+ ]
449
+
450
+ const ROWS: Release[] = [
451
+ { id: 'v5', version: 'v2.4.0', date: '2026-10-21', changes: 38, stable: true },
452
+ { id: 'v4', version: 'v2.4.0-rc.2', date: '2026-10-14', changes: 12, stable: false },
453
+ { id: 'v3', version: 'v2.3.2', date: '2026-09-30', changes: 7, stable: true },
454
+ { id: 'v2', version: 'v2.3.1', date: '2026-09-18', changes: 4, stable: true },
455
+ ]
456
+
457
+ export default function Example() {
458
+ return <DataGrid readOnly toolbar={false} footer={false} defaultColumns={COLUMNS} defaultRows={ROWS} defaultView={{ rowNumbers: false }} style={{ width: 460 }} />
459
+ }
460
+ ```
@@ -0,0 +1,197 @@
1
+ # DatePicker · Calendar
2
+
3
+ > 선택 컨트롤 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
4
+
5
+ 날짜 입력. 입력창처럼 생긴 필드를 누르면 Popover 안에 달력이 열리고, 고르면 닫혀요. 화살표로 하루씩, PageUp/PageDown으로 한 달씩(Shift는 1년) 움직이고, 달을 넘기면 그 방향으로 미끄러져요. 위의 "2026년 10월"을 누르면 한 화면에서 연도(왼쪽 스크롤 목록)와 월(오른쪽)을 고를 수 있고, 월을 누르면 바로 그 달로 돌아와요. Esc는 날짜 화면으로 돌아가요. 기간 선택은 마우스를 올린 날까지 범위를 미리 보여줘요.
6
+
7
+ ```tsx
8
+ import { Calendar, DatePicker, DateRangePicker, type DateRange } from '@iamfiro/design'
9
+ ```
10
+
11
+ ## API
12
+
13
+ ### DatePicker
14
+
15
+ | prop | 타입 | 기본값 | 설명 |
16
+ | --- | --- | --- | --- |
17
+ | `value` | `Date \| null` | | 선택한 날짜(제어). |
18
+ | `defaultValue` | `Date \| null` | `null` | 처음 값(비제어). |
19
+ | `onValueChange` | `(date: Date \| null) => void` | | 고르거나 지울 때. |
20
+ | `formatValue` | `(date: Date) => string` | `"10월 25일"` | 필드에 쓰는 형식. |
21
+ | `placeholder` | `string` | `'날짜 입력 또는 선택'` | 비었을 때 문구. |
22
+ | `clearable` | `boolean` | `true` | × 지우기 버튼. |
23
+ | `min / max` | `Date` | | 고를 수 있는 처음·마지막 날. |
24
+ | `isDateDisabled` | `(date: Date) => boolean` | | 추가로 막을 날(공휴일, 주말…). |
25
+ | `weekStartsOn` | `0 \| 1` | `0` | 0 = 일요일 시작, 1 = 월요일 시작. |
26
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 필드 높이 36 / 44 / 52px. |
27
+ | `invalid / disabled` | `boolean` | | 오류 · 비활성. |
28
+ | `align` | `'start' \| 'end'` | `'start'` | 달력이 필드 어느 쪽에 맞춰 열릴지. |
29
+
30
+ ### DateRangePicker
31
+
32
+ DatePicker와 같은 props에 아래가 다르거나 추가돼요.
33
+
34
+ | prop | 타입 | 기본값 | 설명 |
35
+ | --- | --- | --- | --- |
36
+ | `value / defaultValue` | `DateRange` | | { from: Date \| null, to: Date \| null } |
37
+ | `onValueChange` | `(range: DateRange) => void` | | 시작·끝을 고를 때마다. 두 날짜가 다 고르면 닫혀요. |
38
+ | `presets` | `DatePreset[]` | `DEFAULT_DATE_PRESETS` | { label, range: () => DateRange }[]. []면 숨김. |
39
+
40
+ ### Calendar
41
+
42
+ | prop | 타입 | 기본값 | 설명 |
43
+ | --- | --- | --- | --- |
44
+ | `mode` | `'single' \| 'range'` | `'single'` | 하루 또는 기간. |
45
+ | `value / onValueChange` | `Date \| null · DateRange` | | mode에 맞는 값. |
46
+ | `defaultMonth` | `Date` | | 처음 보여줄 달. 생략하면 선택한 날 또는 오늘. |
47
+ | `min / max / isDateDisabled / weekStartsOn` | `…` | | DatePicker와 같아요. |
48
+ | `autoFocus` | `boolean` | | 처음에 선택한 날(또는 오늘)로 포커스. 팝오버 안에서 써요. |
49
+ | `showFooter` | `boolean` | `true` | 아래 줄("오늘" 버튼, 기간 선택 안내). |
50
+
51
+ ## 예제
52
+
53
+ 예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
54
+
55
+ ### 날짜 선택
56
+
57
+ 값이 올해면 연도는 생략해요("10월 25일"). ×로 지울 수 있어요.
58
+
59
+ ```tsx
60
+ import { useState } from 'react'
61
+ import { Stack, Text, DatePicker, formatDate } from '@iamfiro/design'
62
+
63
+ export default function Example() {
64
+ const [date, setDate] = useState<Date | null>(() => new Date())
65
+ return (
66
+ <Stack gap={2} style={{ width: 260 }}>
67
+ <DatePicker value={date} onValueChange={setDate} aria-label="마감일" />
68
+ <Text variant="caption" tone="tertiary">
69
+ 선택: {date ? formatDate(date, true) : '없음'}
70
+ </Text>
71
+ </Stack>
72
+ )
73
+ }
74
+ ```
75
+
76
+ ### 직접 입력
77
+
78
+ "10/25", "2026.10.25", "10월 25일", "내일"처럼 쳐도 돼요. 완전한 날짜가 되면 달력이 따라오고, Enter로 확정, ↓로 달력에 들어가요. 못 알아들으면 빨갛게 표시하고, 칸을 벗어나면 원래 값으로 돌아가요.
79
+
80
+ ```tsx
81
+ import { useState } from 'react'
82
+ import { Field, Stack, Text, DatePicker, formatDate } from '@iamfiro/design'
83
+
84
+ export default function Example() {
85
+ const [date, setDate] = useState<Date | null>(null)
86
+ return (
87
+ <Stack gap={2} style={{ width: 280 }}>
88
+ <Field label="출발일" htmlFor="depart" hint="예) 10/25 · 2026.10.25 · 10월 25일 · 내일">
89
+ <DatePicker id="depart" value={date} onValueChange={setDate} />
90
+ </Field>
91
+ <Text variant="caption" tone="tertiary">
92
+ 값: {date ? formatDate(date, true) : '없음'}
93
+ </Text>
94
+ </Stack>
95
+ )
96
+ }
97
+ ```
98
+
99
+ ### 기간 선택
100
+
101
+ 두 날짜를 차례로 누르거나, 왼쪽의 빠른 선택(오늘·최근 7일…)을 써요. presets로 바꾸거나 []로 숨겨요.
102
+
103
+ ```tsx
104
+ import { useState } from 'react'
105
+ import { Stack, Text, DateRangePicker, formatDate, type DateRange } from '@iamfiro/design'
106
+
107
+ export default function Example() {
108
+ const [range, setRange] = useState<DateRange>({ from: null, to: null })
109
+ const days = range.from && range.to ? Math.round((range.to.getTime() - range.from.getTime()) / 86400000) + 1 : 0
110
+ return (
111
+ <Stack gap={2} style={{ width: 300 }}>
112
+ <DateRangePicker value={range} onValueChange={setRange} aria-label="조회 기간" />
113
+ <Text variant="caption" tone="tertiary">
114
+ {range.from && range.to ? `${formatDate(range.from)} ~ ${formatDate(range.to)} · ${days}일` : '두 날짜를 고르거나 빠른 선택을 눌러 보세요'}
115
+ </Text>
116
+ </Stack>
117
+ )
118
+ }
119
+ ```
120
+
121
+ ### 선택 가능한 날짜 제한
122
+
123
+ min · max와 isDateDisabled로 지난 날짜와 주말을 막아요. 막힌 날은 취소선으로 보여요.
124
+
125
+ ```tsx
126
+ import { Field, DatePicker } from '@iamfiro/design'
127
+
128
+ const today = new Date()
129
+ const inThreeMonths = new Date(today.getFullYear(), today.getMonth() + 3, today.getDate())
130
+ const isWeekend = (d: Date) => d.getDay() === 0 || d.getDay() === 6
131
+
132
+ export default function Example() {
133
+ return (
134
+ <Field label="미팅 날짜 · 평일, 오늘부터 3개월 안" htmlFor="meeting-date">
135
+ <div style={{ width: 280 }}>
136
+ <DatePicker id="meeting-date" min={today} max={inThreeMonths} isDateDisabled={isWeekend} placeholder="평일을 고르세요" />
137
+ </div>
138
+ </Field>
139
+ )
140
+ }
141
+ ```
142
+
143
+ ### Calendar 단독
144
+
145
+ 팝오버 없이 화면에 바로 두는 달력. 일요일은 빨강, 토요일은 파랑 톤. 아래 "오늘"로 바로 이동하고, 월요일 시작(weekStartsOn={1})도 돼요.
146
+
147
+ ```tsx
148
+ import { useState } from 'react'
149
+ import { Card, CardBody, Calendar, Inline } from '@iamfiro/design'
150
+
151
+ export default function Example() {
152
+ const [date, setDate] = useState<Date | null>(() => new Date())
153
+ return (
154
+ <Inline gap={4} align="start">
155
+ <Card>
156
+ <CardBody>
157
+ <Calendar value={date} onValueChange={setDate} />
158
+ </CardBody>
159
+ </Card>
160
+ <Card>
161
+ <CardBody>
162
+ <Calendar value={date} onValueChange={setDate} weekStartsOn={1} />
163
+ </CardBody>
164
+ </Card>
165
+ </Inline>
166
+ )
167
+ }
168
+ ```
169
+
170
+ ### 크기와 상태
171
+
172
+ Field와 함께 쓰면 라벨이 연결돼요. size · invalid · disabled.
173
+
174
+ ```tsx
175
+ import { Field, Grid, DatePicker } from '@iamfiro/design'
176
+
177
+ const TODAY = new Date()
178
+
179
+ export default function Example() {
180
+ return (
181
+ <Grid columns={2} gap={4} style={{ width: 480 }}>
182
+ <Field label="작게 (sm)" htmlFor="dp-sm">
183
+ <DatePicker id="dp-sm" size="sm" defaultValue={TODAY} />
184
+ </Field>
185
+ <Field label="크게 (lg)" htmlFor="dp-lg">
186
+ <DatePicker id="dp-lg" size="lg" />
187
+ </Field>
188
+ <Field label="시작일" invalid hint="시작일을 골라 주세요." htmlFor="dp-err">
189
+ <DatePicker id="dp-err" invalid />
190
+ </Field>
191
+ <Field label="계약일" htmlFor="dp-off">
192
+ <DatePicker id="dp-off" defaultValue={new Date(2026, 0, 2)} disabled />
193
+ </Field>
194
+ </Grid>
195
+ )
196
+ }
197
+ ```