@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.
- package/AGENTS.md +81 -0
- package/README.md +15 -2
- package/agents/INDEX.md +156 -0
- package/agents/accordion/AGENTS.md +279 -0
- package/agents/action-bar/AGENTS.md +90 -0
- package/agents/alert/AGENTS.md +212 -0
- package/agents/alert-dialog/AGENTS.md +193 -0
- package/agents/app-shell/AGENTS.md +409 -0
- package/agents/area-chart/AGENTS.md +183 -0
- package/agents/aspect-ratio/AGENTS.md +143 -0
- package/agents/avatar/AGENTS.md +130 -0
- package/agents/badge/AGENTS.md +217 -0
- package/agents/bar-chart/AGENTS.md +386 -0
- package/agents/bar-list/AGENTS.md +223 -0
- package/agents/brand-icon/AGENTS.md +82 -0
- package/agents/breadcrumb/AGENTS.md +133 -0
- package/agents/button/AGENTS.md +195 -0
- package/agents/card/AGENTS.md +141 -0
- package/agents/card-frame/AGENTS.md +227 -0
- package/agents/chat-bubble/AGENTS.md +266 -0
- package/agents/chat-composer/AGENTS.md +251 -0
- package/agents/checkbox/AGENTS.md +113 -0
- package/agents/code-block/AGENTS.md +144 -0
- package/agents/color-dot/AGENTS.md +120 -0
- package/agents/color-picker/AGENTS.md +224 -0
- package/agents/colors/AGENTS.md +168 -0
- package/agents/combobox/AGENTS.md +218 -0
- package/agents/command-menu/AGENTS.md +169 -0
- package/agents/comment/AGENTS.md +206 -0
- package/agents/context-menu/AGENTS.md +303 -0
- package/agents/copy-button/AGENTS.md +84 -0
- package/agents/count-badge/AGENTS.md +115 -0
- package/agents/countdown/AGENTS.md +101 -0
- package/agents/data-grid/AGENTS.md +460 -0
- package/agents/date-picker/AGENTS.md +197 -0
- package/agents/delta/AGENTS.md +114 -0
- package/agents/description-list/AGENTS.md +251 -0
- package/agents/dialog/AGENTS.md +391 -0
- package/agents/drawer/AGENTS.md +471 -0
- package/agents/dropdown-menu/AGENTS.md +322 -0
- package/agents/dropzone/AGENTS.md +116 -0
- package/agents/elapsed/AGENTS.md +160 -0
- package/agents/empty-state/AGENTS.md +203 -0
- package/agents/file-chip/AGENTS.md +79 -0
- package/agents/file-icon/AGENTS.md +66 -0
- package/agents/file-upload/AGENTS.md +168 -0
- package/agents/filter-chips/AGENTS.md +167 -0
- package/agents/flag/AGENTS.md +67 -0
- package/agents/funnel-chart/AGENTS.md +131 -0
- package/agents/highlight/AGENTS.md +119 -0
- package/agents/hover-card/AGENTS.md +271 -0
- package/agents/icon-stack/AGENTS.md +120 -0
- package/agents/icon-tile/AGENTS.md +94 -0
- package/agents/inline-diff/AGENTS.md +211 -0
- package/agents/input/AGENTS.md +261 -0
- package/agents/kanban/AGENTS.md +352 -0
- package/agents/kbd/AGENTS.md +160 -0
- package/agents/layout/AGENTS.md +351 -0
- package/agents/line-chart/AGENTS.md +393 -0
- package/agents/link/AGENTS.md +53 -0
- package/agents/list/AGENTS.md +392 -0
- package/agents/marker/AGENTS.md +142 -0
- package/agents/masked-text/AGENTS.md +127 -0
- package/agents/menubar/AGENTS.md +276 -0
- package/agents/multi-select/AGENTS.md +159 -0
- package/agents/navigation-menu/AGENTS.md +315 -0
- package/agents/notice-stack/AGENTS.md +101 -0
- package/agents/number-stepper/AGENTS.md +96 -0
- package/agents/otp-input/AGENTS.md +162 -0
- package/agents/pagination/AGENTS.md +189 -0
- package/agents/phone-input/AGENTS.md +131 -0
- package/agents/pie-chart/AGENTS.md +243 -0
- package/agents/popover/AGENTS.md +190 -0
- package/agents/progress/AGENTS.md +273 -0
- package/agents/progress-tracker/AGENTS.md +305 -0
- package/agents/rating/AGENTS.md +165 -0
- package/agents/relative-time/AGENTS.md +103 -0
- package/agents/resizable/AGENTS.md +390 -0
- package/agents/scaled-preview/AGENTS.md +111 -0
- package/agents/scatter-chart/AGENTS.md +160 -0
- package/agents/scroll-area/AGENTS.md +271 -0
- package/agents/search-input/AGENTS.md +105 -0
- package/agents/segmented-control/AGENTS.md +153 -0
- package/agents/select/AGENTS.md +159 -0
- package/agents/selector-button/AGENTS.md +122 -0
- package/agents/sidebar/AGENTS.md +201 -0
- package/agents/skeleton/AGENTS.md +166 -0
- package/agents/slider/AGENTS.md +181 -0
- package/agents/spark-chart/AGENTS.md +257 -0
- package/agents/sparkbar/AGENTS.md +92 -0
- package/agents/spinner/AGENTS.md +123 -0
- package/agents/split-view/AGENTS.md +290 -0
- package/agents/stat/AGENTS.md +138 -0
- package/agents/status-icon/AGENTS.md +243 -0
- package/agents/step-list/AGENTS.md +313 -0
- package/agents/table/AGENTS.md +286 -0
- package/agents/tabs/AGENTS.md +305 -0
- package/agents/tag-input/AGENTS.md +246 -0
- package/agents/time-picker/AGENTS.md +228 -0
- package/agents/timeline/AGENTS.md +1018 -0
- package/agents/toast/AGENTS.md +266 -0
- package/agents/toggle-group/AGENTS.md +216 -0
- package/agents/toolbar/AGENTS.md +277 -0
- package/agents/tooltip/AGENTS.md +154 -0
- package/agents/tracker/AGENTS.md +158 -0
- package/agents/tree-diagram/AGENTS.md +260 -0
- package/agents/tree-view/AGENTS.md +329 -0
- package/agents/typography/AGENTS.md +396 -0
- package/lib/index.js +17914 -0
- package/lib/style.css +2 -0
- package/lib/types/components/Accordion/Accordion.d.ts +57 -0
- package/lib/types/components/ActionBar/ActionBar.d.ts +24 -0
- package/lib/types/components/Alert/Alert.d.ts +36 -0
- package/lib/types/components/AppShell/AppShell.d.ts +38 -0
- package/lib/types/components/AreaChart/AreaChart.d.ts +17 -0
- package/lib/types/components/BarChart/BarChart.d.ts +57 -0
- package/lib/types/components/BarList/BarList.d.ts +56 -0
- package/lib/types/components/Breadcrumb/Breadcrumb.d.ts +15 -0
- package/lib/types/components/Calendar/Calendar.d.ts +34 -0
- package/lib/types/components/Calendar/DatePicker.d.ts +45 -0
- package/lib/types/components/Calendar/date.d.ts +32 -0
- package/lib/types/components/Card/Card.d.ts +64 -0
- package/lib/types/components/CardFrame/CardFrame.d.ts +15 -0
- package/lib/types/components/Chart/ChartParts.d.ts +38 -0
- package/lib/types/components/Chart/chart.d.ts +46 -0
- package/lib/types/components/Chart/useChartInteraction.d.ts +39 -0
- package/lib/types/components/ChatBubble/ChatBubble.d.ts +61 -0
- package/lib/types/components/ChatBubble/formatChatTime.d.ts +2 -0
- package/lib/types/components/ChatComposer/ChatComposer.d.ts +42 -0
- package/lib/types/components/CodeBlock/CodeBlock.d.ts +18 -0
- package/lib/types/components/CodeBlock/highlight.d.ts +10 -0
- package/lib/types/components/ColorPicker/ColorPicker.d.ts +61 -0
- package/lib/types/components/ColorPicker/color.d.ts +48 -0
- package/lib/types/components/ColorPicker/presets.d.ts +7 -0
- package/lib/types/components/Combobox/Combobox.d.ts +48 -0
- package/lib/types/components/CommandMenu/CommandMenu.d.ts +51 -0
- package/lib/types/components/Comment/Comment.d.ts +45 -0
- package/lib/types/components/ContextMenu/ContextMenu.d.ts +40 -0
- package/lib/types/components/DataGrid/DataGrid.d.ts +43 -0
- package/lib/types/components/DataGrid/GridPopover.d.ts +43 -0
- package/lib/types/components/DataGrid/anchor.d.ts +5 -0
- package/lib/types/components/DataGrid/model.d.ts +61 -0
- package/lib/types/components/DataGrid/panels.d.ts +63 -0
- package/lib/types/components/DataGrid/typeMeta.d.ts +6 -0
- package/lib/types/components/DataGrid/types.d.ts +77 -0
- package/lib/types/components/DescriptionList/DescriptionList.d.ts +77 -0
- package/lib/types/components/Dialog/AlertDialog.d.ts +26 -0
- package/lib/types/components/Dialog/Dialog.d.ts +42 -0
- package/lib/types/components/Dialog/modal.d.ts +38 -0
- package/lib/types/components/Drawer/Drawer.d.ts +62 -0
- package/lib/types/components/DropdownMenu/DropdownMenu.d.ts +97 -0
- package/lib/types/components/DropdownMenu/menuCore.d.ts +23 -0
- package/lib/types/components/Dropzone/Dropzone.d.ts +30 -0
- package/lib/types/components/EmptyState/EmptyState.d.ts +26 -0
- package/lib/types/components/FileChip/FileChip.d.ts +32 -0
- package/lib/types/components/FileUpload/FileUpload.d.ts +52 -0
- package/lib/types/components/FilterChips/FilterChips.d.ts +37 -0
- package/lib/types/components/FunnelChart/FunnelChart.d.ts +38 -0
- package/lib/types/components/FunnelChart/funnel.d.ts +25 -0
- package/lib/types/components/Highlight/Highlight.d.ts +35 -0
- package/lib/types/components/HoverCard/HoverCard.d.ts +47 -0
- package/lib/types/components/InlineDiff/InlineDiff.d.ts +36 -0
- package/lib/types/components/InlineDiff/diff.d.ts +42 -0
- package/lib/types/components/InlineDiff/useDiffReview.d.ts +57 -0
- package/lib/types/components/Kanban/Kanban.d.ts +64 -0
- package/lib/types/components/Kanban/KanbanCard.d.ts +35 -0
- package/lib/types/components/Kanban/move.d.ts +25 -0
- package/lib/types/components/LineChart/LineChart.d.ts +85 -0
- package/lib/types/components/List/List.d.ts +62 -0
- package/lib/types/components/Listbox/Listbox.d.ts +60 -0
- package/lib/types/components/Marker/Marker.d.ts +28 -0
- package/lib/types/components/Menubar/Menubar.d.ts +50 -0
- package/lib/types/components/MultiSelect/MultiSelect.d.ts +38 -0
- package/lib/types/components/NavigationMenu/NavigationMenu.d.ts +101 -0
- package/lib/types/components/NoticeStack/NoticeStack.d.ts +41 -0
- package/lib/types/components/NumberStepper/NumberStepper.d.ts +32 -0
- package/lib/types/components/OtpInput/OtpInput.d.ts +31 -0
- package/lib/types/components/Pagination/Pagination.d.ts +54 -0
- package/lib/types/components/Pagination/paginationRange.d.ts +7 -0
- package/lib/types/components/PhoneInput/PhoneInput.d.ts +47 -0
- package/lib/types/components/PieChart/PieChart.d.ts +67 -0
- package/lib/types/components/PieChart/arc.d.ts +26 -0
- package/lib/types/components/PieChart/useArcTween.d.ts +15 -0
- package/lib/types/components/Popover/Popover.d.ts +28 -0
- package/lib/types/components/ProgressTracker/ProgressTracker.d.ts +64 -0
- package/lib/types/components/Rating/Rating.d.ts +42 -0
- package/lib/types/components/Resizable/Resizable.d.ts +50 -0
- package/lib/types/components/Resizable/layout.d.ts +31 -0
- package/lib/types/components/ScaledPreview/ScaledPreview.d.ts +38 -0
- package/lib/types/components/ScatterChart/ScatterChart.d.ts +81 -0
- package/lib/types/components/ScatterChart/scatter.d.ts +14 -0
- package/lib/types/components/ScrollArea/ScrollArea.d.ts +34 -0
- package/lib/types/components/Select/Select.d.ts +39 -0
- package/lib/types/components/Sidebar/Sidebar.d.ts +39 -0
- package/lib/types/components/Skeleton/Skeleton.d.ts +47 -0
- package/lib/types/components/Slider/Slider.d.ts +36 -0
- package/lib/types/components/SparkChart/SparkChart.d.ts +55 -0
- package/lib/types/components/SparkChart/spark.d.ts +16 -0
- package/lib/types/components/Sparkbar/Sparkbar.d.ts +39 -0
- package/lib/types/components/SplitView/SplitView.d.ts +31 -0
- package/lib/types/components/Stat/Stat.d.ts +58 -0
- package/lib/types/components/StepList/StepList.d.ts +61 -0
- package/lib/types/components/Table/Table.d.ts +39 -0
- package/lib/types/components/Tabs/Tabs.d.ts +47 -0
- package/lib/types/components/TagInput/TagInput.d.ts +46 -0
- package/lib/types/components/TimePicker/TimePicker.d.ts +43 -0
- package/lib/types/components/TimePicker/time.d.ts +15 -0
- package/lib/types/components/Timeline/Timeline.d.ts +154 -0
- package/lib/types/components/Timeline/scale.d.ts +22 -0
- package/lib/types/components/Toast/Toaster.d.ts +19 -0
- package/lib/types/components/Toast/index.d.ts +4 -0
- package/lib/types/components/Toast/store.d.ts +75 -0
- package/lib/types/components/ToggleGroup/ToggleGroup.d.ts +28 -0
- package/lib/types/components/Toolbar/Toolbar.d.ts +70 -0
- package/lib/types/components/Tracker/Tracker.d.ts +43 -0
- package/lib/types/components/TreeDiagram/TreeDiagram.d.ts +68 -0
- package/lib/types/components/TreeView/TreeView.d.ts +107 -0
- package/lib/types/components/index.d.ts +186 -0
- package/lib/types/hooks/index.d.ts +17 -0
- package/lib/types/hooks/timing.d.ts +29 -0
- package/lib/types/hooks/useControllableState.d.ts +5 -0
- package/lib/types/hooks/useCountdown.d.ts +19 -0
- package/lib/types/hooks/useDigitInput.d.ts +24 -0
- package/lib/types/hooks/useDismiss.d.ts +3 -0
- package/lib/types/hooks/useElapsed.d.ts +28 -0
- package/lib/types/hooks/useFloating.d.ts +21 -0
- package/lib/types/hooks/useFocusTrap.d.ts +7 -0
- package/lib/types/hooks/usePresence.d.ts +8 -0
- package/lib/types/hooks/usePressToOpen.d.ts +20 -0
- package/lib/types/hooks/useScrollLock.d.ts +2 -0
- package/lib/types/hooks/useSelection.d.ts +29 -0
- package/lib/types/index.d.ts +5 -0
- package/lib/types/primitives/AspectRatio.d.ts +14 -0
- package/lib/types/primitives/BrandIcon.d.ts +12 -0
- package/lib/types/primitives/ColorDot.d.ts +20 -0
- package/lib/types/primitives/CopyButton.d.ts +12 -0
- package/lib/types/primitives/CountBadge.d.ts +28 -0
- package/lib/types/primitives/Countdown.d.ts +21 -0
- package/lib/types/primitives/Delta.d.ts +33 -0
- package/lib/types/primitives/Elapsed.d.ts +23 -0
- package/lib/types/primitives/FileIcon.d.ts +17 -0
- package/lib/types/primitives/Flag.d.ts +15 -0
- package/lib/types/primitives/IconStack.d.ts +27 -0
- package/lib/types/primitives/IconTile.d.ts +24 -0
- package/lib/types/primitives/Kbd.d.ts +19 -0
- package/lib/types/primitives/Layout.d.ts +64 -0
- package/lib/types/primitives/Link.d.ts +12 -0
- package/lib/types/primitives/MaskedText.d.ts +28 -0
- package/lib/types/primitives/Progress.d.ts +52 -0
- package/lib/types/primitives/RelativeTime.d.ts +22 -0
- package/lib/types/primitives/SearchInput.d.ts +8 -0
- package/lib/types/primitives/SelectorButton.d.ts +23 -0
- package/lib/types/primitives/Spinner.d.ts +15 -0
- package/lib/types/primitives/StatusIcon.d.ts +40 -0
- package/lib/types/primitives/Swatch.d.ts +11 -0
- package/lib/types/primitives/Tooltip.d.ts +17 -0
- package/lib/types/primitives/Typography.d.ts +48 -0
- package/lib/types/primitives/formatCountdown.d.ts +2 -0
- package/lib/types/primitives/formatElapsed.d.ts +8 -0
- package/lib/types/primitives/formatRelativeTime.d.ts +16 -0
- package/lib/types/primitives/hotkey.d.ts +10 -0
- package/lib/types/primitives/index.d.ts +188 -0
- package/lib/types/primitives/maskValue.d.ts +24 -0
- package/package.json +54 -4
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# DropdownMenu
|
|
2
|
+
|
|
3
|
+
> 메뉴 & 오버레이 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
|
|
4
|
+
|
|
5
|
+
버튼에서 펼쳐지는 동작 메뉴. 마우스를 누르는 순간 열리고, 누른 채 항목으로 끌어 놓으면 바로 실행돼요(press-drag-release). 그냥 클릭하면 열린 채로 남아요. 키보드는 ↑↓ · Home/End · 글자 입력으로 이동, Enter/Space로 실행, Esc로 닫고 트리거로 포커스가 돌아가요. 하위 메뉴는 잠깐 머물러야 열리고, 하위 패널 쪽으로 대각선으로 움직이는 동안은 지나치는 행이 강조를 빼앗지 않아요.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { DropdownMenu, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub } from '@iamfiro/design'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 디자인 규칙
|
|
12
|
+
|
|
13
|
+
- 행 앞 타일: 사람은 `Avatar`, 도구·사이트·문서는 `IconTile`(회색 채움 + 아이콘/글자). 선택된 것을 보여 주고 메뉴로 바꾸는 버튼(워크스페이스·계정 전환, 드롭다운 제목)은 `SelectorButton`을 `DropdownMenu` trigger에.
|
|
14
|
+
- 팝오버는 `ListboxPopover` / `Popover` / `DropdownMenu`(portal + fixed, 공간 부족 시 위로, `useFloating`의 `align`). 부모 `overflow: hidden`에 잘리면 안 된다.
|
|
15
|
+
|
|
16
|
+
## API
|
|
17
|
+
|
|
18
|
+
### DropdownMenu
|
|
19
|
+
|
|
20
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
21
|
+
| --- | --- | --- | --- |
|
|
22
|
+
| `trigger` (필수) | `ReactNode` | | 여는 버튼. |
|
|
23
|
+
| `children` (필수) | `ReactNode` | | 항목, 라벨, 구분선, 하위 메뉴. |
|
|
24
|
+
| `align` | `'start' \| 'center' \| 'end'` | `'start'` | 트리거 기준 가로 정렬. 오른쪽 끝의 "⋯" 버튼은 end. |
|
|
25
|
+
| `minWidth` | `number` | `200` | 패널 최소 폭(px). |
|
|
26
|
+
| `open / onOpenChange` | `boolean / (open) => void` | | 열림 상태를 밖에서 제어할 때. |
|
|
27
|
+
| `defaultOpen` | `boolean` | `false` | 처음 열림 상태(비제어). |
|
|
28
|
+
| `aria-label` | `string` | | 메뉴(role="menu")의 접근성 이름. |
|
|
29
|
+
|
|
30
|
+
### DropdownMenuItem
|
|
31
|
+
|
|
32
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| `children` (필수) | `ReactNode` | | 항목 이름. |
|
|
35
|
+
| `onSelect` | `() => void` | | 클릭 · Enter/Space · 끌어 놓기로 실행될 때. 메뉴가 먼저 닫혀요. |
|
|
36
|
+
| `icon` | `ReactNode` | | 앞 아이콘. |
|
|
37
|
+
| `description` | `ReactNode` | | 이름 아래 회색 설명. |
|
|
38
|
+
| `shortcut` | `string[]` | | 단축키 힌트. 예: ['mod', 'D']. 표시만 하고 단축키를 등록하지는 않아요. |
|
|
39
|
+
| `danger` | `boolean` | | 빨간 글자(파괴적 동작). |
|
|
40
|
+
| `disabled` | `boolean` | | 비활성. 키보드 이동에서 건너뛰어요. |
|
|
41
|
+
| `keepOpen` | `boolean` | | 실행 뒤에도 메뉴를 열어 둬요(반복 동작용). |
|
|
42
|
+
|
|
43
|
+
### DropdownMenuCheckboxItem
|
|
44
|
+
|
|
45
|
+
icon · description · shortcut · disabled도 받아요.
|
|
46
|
+
|
|
47
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
48
|
+
| --- | --- | --- | --- |
|
|
49
|
+
| `checked` (필수) | `boolean` | | 켜짐 여부. |
|
|
50
|
+
| `onCheckedChange` (필수) | `(checked: boolean) => void` | | 토글될 때. |
|
|
51
|
+
| `closeOnSelect` | `boolean` | `false` | 고르면 메뉴를 닫아요. |
|
|
52
|
+
|
|
53
|
+
### DropdownMenuRadioGroup
|
|
54
|
+
|
|
55
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
56
|
+
| --- | --- | --- | --- |
|
|
57
|
+
| `value` (필수) | `string` | | 선택된 값. |
|
|
58
|
+
| `onValueChange` (필수) | `(value: string) => void` | | 값이 바뀔 때. 메뉴는 열린 채로 남아요. |
|
|
59
|
+
| `label` | `ReactNode` | | 그룹 제목(문자열이면 aria-label). |
|
|
60
|
+
|
|
61
|
+
### DropdownMenuRadioItem
|
|
62
|
+
|
|
63
|
+
icon · description · shortcut · disabled도 받아요.
|
|
64
|
+
|
|
65
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
66
|
+
| --- | --- | --- | --- |
|
|
67
|
+
| `value` (필수) | `string` | | 이 항목의 값. |
|
|
68
|
+
|
|
69
|
+
### DropdownMenuSub
|
|
70
|
+
|
|
71
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
72
|
+
| --- | --- | --- | --- |
|
|
73
|
+
| `label` (필수) | `ReactNode` | | 하위 메뉴를 여는 행의 이름. |
|
|
74
|
+
| `children` (필수) | `ReactNode` | | 하위 패널의 항목. |
|
|
75
|
+
| `icon` | `ReactNode` | | 앞 아이콘. |
|
|
76
|
+
| `disabled` | `boolean` | | 비활성. |
|
|
77
|
+
|
|
78
|
+
### DropdownMenuLabel
|
|
79
|
+
|
|
80
|
+
그룹 제목(선택 불가). 묶음 사이 구분선은 props 없는 <DropdownMenuSeparator />.
|
|
81
|
+
|
|
82
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
83
|
+
| --- | --- | --- | --- |
|
|
84
|
+
| `children` (필수) | `ReactNode` | | 제목. |
|
|
85
|
+
|
|
86
|
+
## 예제
|
|
87
|
+
|
|
88
|
+
예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
|
|
89
|
+
|
|
90
|
+
### 동작 메뉴
|
|
91
|
+
|
|
92
|
+
아이콘과 단축키 힌트(shortcut={['mod', 'D']}, OS에 맞게 ⌘/Ctrl로 표시)를 붙여요. 되돌릴 수 있는 "보관"은 바로 실행하고 toast.undo()로 실행 취소를 주고, 되돌릴 수 없는 "영구 삭제"만 AlertDialog로 확인해요. 메뉴 안에서 연 대화상자는 메뉴가 닫혀도 남도록 메뉴 바깥에 두고 open으로 제어해요.
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import { useState } from 'react'
|
|
96
|
+
import { Archive, Copy, FileText, Link2, MoreHorizontal, Pencil, Trash2 } from 'lucide-react'
|
|
97
|
+
import { Box, Button, AlertDialog, Card, CardHeader, DropdownMenu, DropdownMenuItem, DropdownMenuSeparator, toast } from '@iamfiro/design'
|
|
98
|
+
|
|
99
|
+
export default function Example() {
|
|
100
|
+
const [archived, setArchived] = useState(false)
|
|
101
|
+
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
102
|
+
|
|
103
|
+
const archive = () => {
|
|
104
|
+
setArchived(true)
|
|
105
|
+
toast.undo('‘3분기 이사회 보고서’를 보관했어요', {
|
|
106
|
+
onUndo: () => setArchived(false),
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Box style={{ width: 380 }}>
|
|
112
|
+
<Card padding="sm">
|
|
113
|
+
<CardHeader
|
|
114
|
+
icon={<FileText />}
|
|
115
|
+
title="3분기 이사회 보고서"
|
|
116
|
+
description={archived ? '보관됨 · 김민지' : '10월 25일 수정 · 김민지'}
|
|
117
|
+
actions={
|
|
118
|
+
<DropdownMenu
|
|
119
|
+
align="end"
|
|
120
|
+
aria-label="문서 메뉴"
|
|
121
|
+
trigger={<Button iconOnly variant="ghost" size="sm" icon={<MoreHorizontal />} aria-label="더 보기" />}
|
|
122
|
+
>
|
|
123
|
+
<DropdownMenuItem icon={<Pencil />} shortcut={['F2']} onSelect={() => toast('이름 바꾸기를 열었어요')}>
|
|
124
|
+
이름 바꾸기
|
|
125
|
+
</DropdownMenuItem>
|
|
126
|
+
<DropdownMenuItem icon={<Copy />} shortcut={['mod', 'D']} onSelect={() => toast.success('사본을 만들었어요')}>
|
|
127
|
+
복제
|
|
128
|
+
</DropdownMenuItem>
|
|
129
|
+
<DropdownMenuItem icon={<Link2 />} shortcut={['mod', 'L']} onSelect={() => toast.success('링크를 복사했어요')}>
|
|
130
|
+
링크 복사
|
|
131
|
+
</DropdownMenuItem>
|
|
132
|
+
<DropdownMenuSeparator />
|
|
133
|
+
<DropdownMenuItem icon={<Archive />} onSelect={archive} disabled={archived}>
|
|
134
|
+
보관
|
|
135
|
+
</DropdownMenuItem>
|
|
136
|
+
<DropdownMenuItem icon={<Trash2 />} danger onSelect={() => setConfirmDelete(true)}>
|
|
137
|
+
영구 삭제
|
|
138
|
+
</DropdownMenuItem>
|
|
139
|
+
</DropdownMenu>
|
|
140
|
+
}
|
|
141
|
+
/>
|
|
142
|
+
</Card>
|
|
143
|
+
<AlertDialog
|
|
144
|
+
open={confirmDelete}
|
|
145
|
+
onOpenChange={setConfirmDelete}
|
|
146
|
+
tone="danger"
|
|
147
|
+
title="문서를 영구 삭제할까요?"
|
|
148
|
+
description="‘3분기 이사회 보고서’와 댓글 18개가 함께 삭제돼요. 휴지통을 거치지 않아 되돌릴 수 없어요."
|
|
149
|
+
confirmLabel="삭제"
|
|
150
|
+
onConfirm={() => {
|
|
151
|
+
toast.success('문서를 삭제했어요')
|
|
152
|
+
}}
|
|
153
|
+
/>
|
|
154
|
+
</Box>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 체크 항목
|
|
160
|
+
|
|
161
|
+
DropdownMenuCheckboxItem은 기본적으로 고른 뒤에도 메뉴가 열려 있어 여러 개를 한 번에 켜고 끌 수 있어요. 바로 닫으려면 closeOnSelect.
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
import { useState } from 'react'
|
|
165
|
+
import { SlidersHorizontal } from 'lucide-react'
|
|
166
|
+
import { Button, Stack, Text, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuLabel, DropdownMenuSeparator } from '@iamfiro/design'
|
|
167
|
+
|
|
168
|
+
const COLUMNS = [
|
|
169
|
+
{ id: 'assignee', label: '담당자' },
|
|
170
|
+
{ id: 'due', label: '마감일' },
|
|
171
|
+
{ id: 'priority', label: '우선순위' },
|
|
172
|
+
{ id: 'tags', label: '태그' },
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
export default function Example() {
|
|
176
|
+
const [visible, setVisible] = useState<string[]>(['assignee', 'due'])
|
|
177
|
+
const [showDone, setShowDone] = useState(false)
|
|
178
|
+
|
|
179
|
+
const toggle = (id: string, on: boolean) => setVisible((ids) => (on ? [...ids, id] : ids.filter((x) => x !== id)))
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<Stack gap={3} align="start">
|
|
183
|
+
<DropdownMenu aria-label="보기 옵션" trigger={<Button icon={<SlidersHorizontal />}>보기</Button>}>
|
|
184
|
+
<DropdownMenuLabel>표시할 열</DropdownMenuLabel>
|
|
185
|
+
{COLUMNS.map((c) => (
|
|
186
|
+
<DropdownMenuCheckboxItem key={c.id} checked={visible.includes(c.id)} onCheckedChange={(on) => toggle(c.id, on)}>
|
|
187
|
+
{c.label}
|
|
188
|
+
</DropdownMenuCheckboxItem>
|
|
189
|
+
))}
|
|
190
|
+
<DropdownMenuSeparator />
|
|
191
|
+
<DropdownMenuCheckboxItem checked={showDone} onCheckedChange={setShowDone} description="지난 30일 안에 끝난 작업">
|
|
192
|
+
완료된 작업 표시
|
|
193
|
+
</DropdownMenuCheckboxItem>
|
|
194
|
+
</DropdownMenu>
|
|
195
|
+
<Text variant="body-sm" tone="tertiary">
|
|
196
|
+
표시 중: {COLUMNS.filter((c) => visible.includes(c.id)).map((c) => c.label).join(', ') || '없음'}
|
|
197
|
+
{showDone && ' · 완료 포함'}
|
|
198
|
+
</Text>
|
|
199
|
+
</Stack>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 라디오 그룹
|
|
205
|
+
|
|
206
|
+
하나만 고르는 옵션(정렬 기준, 순서). label을 주면 그룹 제목이 붙고 접근성 이름이 돼요.
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
import { useState } from 'react'
|
|
210
|
+
import { ArrowDownWideNarrow } from 'lucide-react'
|
|
211
|
+
import { Button, DropdownMenu, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator } from '@iamfiro/design'
|
|
212
|
+
|
|
213
|
+
const SORTS = [
|
|
214
|
+
{ value: 'updated', label: '최근 수정순' },
|
|
215
|
+
{ value: 'created', label: '만든 날짜순' },
|
|
216
|
+
{ value: 'name', label: '이름순' },
|
|
217
|
+
{ value: 'size', label: '크기순' },
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
export default function Example() {
|
|
221
|
+
const [sort, setSort] = useState('updated')
|
|
222
|
+
const [order, setOrder] = useState('desc')
|
|
223
|
+
|
|
224
|
+
const current = SORTS.find((s) => s.value === sort)?.label
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<DropdownMenu aria-label="정렬" trigger={<Button icon={<ArrowDownWideNarrow />}>{current}</Button>}>
|
|
228
|
+
<DropdownMenuRadioGroup label="정렬 기준" value={sort} onValueChange={setSort}>
|
|
229
|
+
{SORTS.map((s) => (
|
|
230
|
+
<DropdownMenuRadioItem key={s.value} value={s.value}>
|
|
231
|
+
{s.label}
|
|
232
|
+
</DropdownMenuRadioItem>
|
|
233
|
+
))}
|
|
234
|
+
</DropdownMenuRadioGroup>
|
|
235
|
+
<DropdownMenuSeparator />
|
|
236
|
+
<DropdownMenuRadioGroup label="순서" value={order} onValueChange={setOrder}>
|
|
237
|
+
<DropdownMenuRadioItem value="desc">내림차순</DropdownMenuRadioItem>
|
|
238
|
+
<DropdownMenuRadioItem value="asc">오름차순</DropdownMenuRadioItem>
|
|
239
|
+
</DropdownMenuRadioGroup>
|
|
240
|
+
</DropdownMenu>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 하위 메뉴
|
|
246
|
+
|
|
247
|
+
행에 200ms 머물면 열리고(→ · Enter도 가능, ←·Esc로 닫고 행으로 복귀), 벗어나도 300ms 유예가 있어요. 열린 하위 패널로 대각선으로 이동하는 동안에는 지나가는 "공유" 행이 강조를 가져가지 않아요 — 출발점과 하위 패널 가장자리로 만든 삼각형 안이면 "가는 중"으로 봐요.
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
import { useState } from 'react'
|
|
251
|
+
import { ExternalLink, Folder, FolderInput, Link2, Mail, MoreHorizontal, Share2 } from 'lucide-react'
|
|
252
|
+
import { Button, Inline, Text, DropdownMenu, DropdownMenuItem, DropdownMenuSub, toast } from '@iamfiro/design'
|
|
253
|
+
|
|
254
|
+
const FOLDERS = ['디자인', '마케팅', '회의록', '보관함']
|
|
255
|
+
|
|
256
|
+
export default function Example() {
|
|
257
|
+
const [folder, setFolder] = useState('회의록')
|
|
258
|
+
|
|
259
|
+
const move = (to: string) => {
|
|
260
|
+
const from = folder
|
|
261
|
+
setFolder(to)
|
|
262
|
+
toast.undo(`‘${to}’ 폴더로 옮겼어요`, { onUndo: () => setFolder(from) })
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<Inline gap={3}>
|
|
267
|
+
<DropdownMenu aria-label="파일 메뉴" trigger={<Button iconOnly variant="ghost" icon={<MoreHorizontal />} aria-label="더 보기" />}>
|
|
268
|
+
<DropdownMenuItem icon={<ExternalLink />} onSelect={() => toast('새 탭에서 열었어요')}>
|
|
269
|
+
새 탭에서 열기
|
|
270
|
+
</DropdownMenuItem>
|
|
271
|
+
<DropdownMenuSub label="이동" icon={<FolderInput />}>
|
|
272
|
+
{FOLDERS.map((f) => (
|
|
273
|
+
<DropdownMenuItem key={f} icon={<Folder />} disabled={f === folder} description={f === folder ? '현재 폴더' : undefined} onSelect={() => move(f)}>
|
|
274
|
+
{f}
|
|
275
|
+
</DropdownMenuItem>
|
|
276
|
+
))}
|
|
277
|
+
</DropdownMenuSub>
|
|
278
|
+
<DropdownMenuSub label="공유" icon={<Share2 />}>
|
|
279
|
+
<DropdownMenuItem icon={<Link2 />} onSelect={() => toast.success('링크를 복사했어요')}>
|
|
280
|
+
링크 복사
|
|
281
|
+
</DropdownMenuItem>
|
|
282
|
+
<DropdownMenuItem icon={<Mail />} onSelect={() => toast('메일 작성 창을 열었어요')}>
|
|
283
|
+
이메일로 보내기
|
|
284
|
+
</DropdownMenuItem>
|
|
285
|
+
</DropdownMenuSub>
|
|
286
|
+
</DropdownMenu>
|
|
287
|
+
<Text variant="body-sm" tone="secondary">
|
|
288
|
+
10월 회의록.docx · {folder}
|
|
289
|
+
</Text>
|
|
290
|
+
</Inline>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### 비활성 항목
|
|
296
|
+
|
|
297
|
+
비활성 항목은 키보드 이동에서 건너뛰고 눌러도 아무 일도 없어요. description으로 이유를 알려 주세요. 하위 메뉴도 disabled를 받을 수 있어요.
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
import { ChevronDown, Download, FileSpreadsheet, Printer, Users } from 'lucide-react'
|
|
301
|
+
import { Button, DropdownMenu, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, toast } from '@iamfiro/design'
|
|
302
|
+
|
|
303
|
+
export default function Example() {
|
|
304
|
+
return (
|
|
305
|
+
<DropdownMenu aria-label="내보내기" trigger={<Button iconRight={<ChevronDown />}>내보내기</Button>}>
|
|
306
|
+
<DropdownMenuItem icon={<Download />} onSelect={() => toast.success('PDF로 내보냈어요')}>
|
|
307
|
+
PDF로 다운로드
|
|
308
|
+
</DropdownMenuItem>
|
|
309
|
+
<DropdownMenuItem icon={<FileSpreadsheet />} disabled description="표가 있는 문서에서만 쓸 수 있어요">
|
|
310
|
+
엑셀로 다운로드
|
|
311
|
+
</DropdownMenuItem>
|
|
312
|
+
<DropdownMenuItem icon={<Printer />} shortcut={['mod', 'P']} onSelect={() => toast('인쇄 미리보기를 준비하고 있어요')}>
|
|
313
|
+
인쇄
|
|
314
|
+
</DropdownMenuItem>
|
|
315
|
+
<DropdownMenuSeparator />
|
|
316
|
+
<DropdownMenuSub label="팀에 공유" icon={<Users />} disabled>
|
|
317
|
+
<DropdownMenuItem>디자인 팀</DropdownMenuItem>
|
|
318
|
+
</DropdownMenuSub>
|
|
319
|
+
</DropdownMenu>
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
```
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Dropzone
|
|
2
|
+
|
|
3
|
+
> 파일 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
|
|
4
|
+
|
|
5
|
+
파일을 끌어 놓거나 눌러서 고르는 영역. 입력류라서 점선 윤곽을 가져요. 파일이 위에 오면 파랗게 바뀌고 종이가 부채처럼 펼쳐지며, accept에 맞지 않는 파일이 섞이면 영역이 흔들려요. Enter / Space로도 열려요. 목록과 진행률까지 필요하면 FileUpload를 쓰세요.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Dropzone, matchesAccept } from '@iamfiro/design'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
### Dropzone
|
|
14
|
+
|
|
15
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| `onFiles` | `(accepted: File[], rejected: File[]) => void` | | 끌어 놓거나 고른 파일. accept에 맞지 않으면 rejected로. |
|
|
18
|
+
| `accept` | `string` | | <input accept>와 같은 문법(.pdf, image/*). 끌어 놓은 파일에도 적용돼요. |
|
|
19
|
+
| `multiple` | `boolean` | `true` | 여러 개 받기. false면 첫 파일만. |
|
|
20
|
+
| `size` | `'sm' \| 'md'` | `'md'` | md: 그림 + 가운데 · sm: 한 줄. |
|
|
21
|
+
| `title` | `ReactNode` | `여기에 파일을 끌어 놓거나 찾아보기` | 첫 줄. |
|
|
22
|
+
| `description` | `ReactNode` | | 회색 둘째 줄(형식, 용량). |
|
|
23
|
+
| `invalid` | `boolean` | | 빨간 윤곽 + 연한 빨강 배경. |
|
|
24
|
+
| `disabled` | `boolean` | | 받지 않음. |
|
|
25
|
+
| `name` | `string` | | 폼 전송용 숨은 input 이름. |
|
|
26
|
+
| `aria-label` | `string` | `파일 올리기` | 접근성 이름. |
|
|
27
|
+
|
|
28
|
+
### matchesAccept
|
|
29
|
+
|
|
30
|
+
파일이 accept 문자열에 맞는지 확인하는 함수.
|
|
31
|
+
|
|
32
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| `file` (필수) | `File` | | 확인할 파일. |
|
|
35
|
+
| `accept` | `string` | | 비어 있으면 모두 통과. |
|
|
36
|
+
|
|
37
|
+
## 예제
|
|
38
|
+
|
|
39
|
+
예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
|
|
40
|
+
|
|
41
|
+
### 기본
|
|
42
|
+
|
|
43
|
+
onFiles(accepted, rejected)로 받아요. 이미지가 아닌 파일을 끌어 놓아 보세요.
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import { useState } from 'react'
|
|
47
|
+
import { FileIcon, Stack, Text, Dropzone, List, ListItem, formatBytes } from '@iamfiro/design'
|
|
48
|
+
|
|
49
|
+
export default function Example() {
|
|
50
|
+
const [files, setFiles] = useState<File[]>([])
|
|
51
|
+
const [rejected, setRejected] = useState(0)
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Stack gap={3} style={{ width: 420 }}>
|
|
55
|
+
<Dropzone
|
|
56
|
+
accept="image/*"
|
|
57
|
+
description="PNG, JPG, WEBP 이미지만"
|
|
58
|
+
onFiles={(accepted, wrong) => {
|
|
59
|
+
setFiles((f) => [...f, ...accepted])
|
|
60
|
+
setRejected(wrong.length)
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
{rejected > 0 && (
|
|
64
|
+
<Text variant="body-sm" tone="danger">
|
|
65
|
+
이미지가 아닌 파일 {rejected}개는 뺐어요.
|
|
66
|
+
</Text>
|
|
67
|
+
)}
|
|
68
|
+
{files.length > 0 && (
|
|
69
|
+
<List density="compact" aria-label="고른 이미지">
|
|
70
|
+
{files.map((f, i) => (
|
|
71
|
+
<ListItem key={i} leading={<FileIcon name={f.name} />} title={f.name} meta={formatBytes(f.size)} />
|
|
72
|
+
))}
|
|
73
|
+
</List>
|
|
74
|
+
)}
|
|
75
|
+
</Stack>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 크기
|
|
81
|
+
|
|
82
|
+
md는 그림과 함께 가운데 정렬, sm은 폼 안에 들어가는 한 줄.
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { Stack, Dropzone } from '@iamfiro/design'
|
|
86
|
+
|
|
87
|
+
export default function Example() {
|
|
88
|
+
return (
|
|
89
|
+
<Stack gap={4} style={{ width: 420 }}>
|
|
90
|
+
<Dropzone description="PDF, PNG, JPG 등 · 파일당 최대 25MB" />
|
|
91
|
+
<Dropzone size="sm" title="첨부 파일 추가" description="끌어 놓거나 눌러서 골라요" />
|
|
92
|
+
</Stack>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 오류와 비활성
|
|
98
|
+
|
|
99
|
+
Field로 라벨과 안내를 붙여요. invalid는 연한 빨강, disabled는 회색 + 금지 커서.
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { Field, Stack, Dropzone } from '@iamfiro/design'
|
|
103
|
+
|
|
104
|
+
export default function Example() {
|
|
105
|
+
return (
|
|
106
|
+
<Stack gap={4} style={{ width: 420 }}>
|
|
107
|
+
<Field label="사업자등록증" hint="필수 서류예요. PDF로 올려 주세요." invalid>
|
|
108
|
+
<Dropzone size="sm" accept=".pdf" invalid title="PDF 파일을 끌어 놓거나 찾아보기" aria-label="사업자등록증 올리기" />
|
|
109
|
+
</Field>
|
|
110
|
+
<Field label="계약서" hint="검토가 끝나 더 올릴 수 없어요.">
|
|
111
|
+
<Dropzone size="sm" disabled title="파일 올리기가 잠겨 있어요" aria-label="계약서 올리기" />
|
|
112
|
+
</Field>
|
|
113
|
+
</Stack>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
```
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Elapsed
|
|
2
|
+
|
|
3
|
+
> 진행 표시 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
|
|
4
|
+
|
|
5
|
+
시작한 뒤 지난 시간을 "17:40" · "17분 40초"처럼 글자로 올려 세요. Countdown의 반대 방향이에요. startedAt을 주면 그 시각부터의 실제 시간을, 없으면 스톱워치처럼 initial부터 세요. running={false}면 그 자리에서 멈추고, 일이 끝나면 endedAt으로 최종 시간에 고정해요. 렌더 중엔 시계를 읽지 않고, 매 초 경계에 맞춰 다시 읽어 밀리지 않아요.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Elapsed, formatElapsed, useElapsed } from '@iamfiro/design'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 디자인 규칙
|
|
12
|
+
|
|
13
|
+
- 지난 시간("17:40", "17분 40초")은 `Elapsed` / `useElapsed`(`startedAt` · 스톱워치 · `running` 멈춤 · 끝나면 `endedAt`), 글자는 `formatElapsed`. 시작 시각은 클릭 핸들러 · effect에서만 잡는다.
|
|
14
|
+
|
|
15
|
+
## API
|
|
16
|
+
|
|
17
|
+
### Elapsed
|
|
18
|
+
|
|
19
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
20
|
+
| --- | --- | --- | --- |
|
|
21
|
+
| `startedAt` | `Date \| number \| string` | | 시작 시각. 없으면 마운트(또는 initial)부터 세는 스톱워치. |
|
|
22
|
+
| `running` | `boolean` | `true` | false면 그 자리에서 멈춰요. startedAt이 있으면 다시 켤 때 실제 시간으로 따라잡아요. |
|
|
23
|
+
| `endedAt` | `Date \| number \| string` | | 끝난 시각. startedAt과 함께면 최종 시간에 고정되고 더 세지 않아요. |
|
|
24
|
+
| `initial` | `number` | `0` | startedAt 없을 때 시작 초. |
|
|
25
|
+
| `format` | `(elapsed: number) => ReactNode` | `formatElapsed` | 지난 초를 글자로. 기본 "17:40" / "1:02:05". |
|
|
26
|
+
| `className / style` | `string / CSSProperties` | | span(role="timer")에 전달. |
|
|
27
|
+
|
|
28
|
+
### useElapsed(startedAt?, options)
|
|
29
|
+
|
|
30
|
+
반환값 { elapsed, running, reset() }. reset은 스톱워치(startedAt 없음)에서만 의미가 있어요.
|
|
31
|
+
|
|
32
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| `startedAt` | `Date \| number \| string` | | 시작 시각. 없으면 스톱워치. |
|
|
35
|
+
| `options.running` | `boolean` | `true` | 멈춤 / 이어 세기. |
|
|
36
|
+
| `options.endedAt` | `Date \| number \| string` | | 끝난 시각에 고정. |
|
|
37
|
+
| `options.initial` | `number` | `0` | 스톱워치 시작 초. |
|
|
38
|
+
|
|
39
|
+
### formatElapsed(seconds, style?)
|
|
40
|
+
|
|
41
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
42
|
+
| --- | --- | --- | --- |
|
|
43
|
+
| `style` | `'clock' \| 'text' \| 'short'` | `'clock'` | clock "17:40" · text "17분 40초" · short "17m 40s". text · short는 큰 단위 두 개까지만. |
|
|
44
|
+
|
|
45
|
+
## 예제
|
|
46
|
+
|
|
47
|
+
예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
|
|
48
|
+
|
|
49
|
+
### 머리글 · Badge
|
|
50
|
+
|
|
51
|
+
시작 시각 옆 경과 시간, Badge 안의 글자. format으로 "1분 35초"(text) · "1h 2m"(short)를 골라요.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { Clock } from 'lucide-react'
|
|
55
|
+
import { Badge, Elapsed, Inline, Stack, Text, formatElapsed } from '@iamfiro/design'
|
|
56
|
+
|
|
57
|
+
export default function Example() {
|
|
58
|
+
return (
|
|
59
|
+
<Stack gap={4}>
|
|
60
|
+
<Inline gap={1} wrap={false}>
|
|
61
|
+
<Text as="span" tone="tertiary" style={{ display: 'inline-flex' }}>
|
|
62
|
+
<Clock size={14} aria-hidden />
|
|
63
|
+
</Text>
|
|
64
|
+
<Text variant="label-sm" tone="tertiary" numeric>
|
|
65
|
+
시작 오후 9:12 · <Elapsed initial={1060} />
|
|
66
|
+
</Text>
|
|
67
|
+
</Inline>
|
|
68
|
+
<Inline gap={2}>
|
|
69
|
+
<Badge tone="accent" spinner>
|
|
70
|
+
배포 중 · <Elapsed initial={95} format={(s) => formatElapsed(s, 'text')} />
|
|
71
|
+
</Badge>
|
|
72
|
+
<Badge>
|
|
73
|
+
<Elapsed initial={3725} format={(s) => formatElapsed(s, 'short')} />
|
|
74
|
+
</Badge>
|
|
75
|
+
</Inline>
|
|
76
|
+
</Stack>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 시작 · 끝 시각
|
|
82
|
+
|
|
83
|
+
시작 시각(startedAt)은 클릭 핸들러에서 Date.now()로 잡아요. 끝내면 endedAt을 넘겨 걸린 시간에 고정돼요.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { useState } from 'react'
|
|
87
|
+
import { Button, Elapsed, Inline, Stack, StatusIcon, Text, formatElapsed } from '@iamfiro/design'
|
|
88
|
+
|
|
89
|
+
export default function Example() {
|
|
90
|
+
// Real timestamps, taken in click handlers (never during render).
|
|
91
|
+
const [startedAt, setStartedAt] = useState<number>()
|
|
92
|
+
const [endedAt, setEndedAt] = useState<number>()
|
|
93
|
+
const status = startedAt === undefined ? 'pending' : endedAt === undefined ? 'active' : 'done'
|
|
94
|
+
return (
|
|
95
|
+
<Stack gap={4} style={{ width: 320 }}>
|
|
96
|
+
<Inline gap={3} justify="between" wrap={false}>
|
|
97
|
+
<Inline gap={2} wrap={false}>
|
|
98
|
+
<StatusIcon status={status} decorative />
|
|
99
|
+
<Text variant="label-md">월간 보고서 내보내기</Text>
|
|
100
|
+
</Inline>
|
|
101
|
+
<Text variant="label-sm" tone="tertiary">
|
|
102
|
+
{startedAt === undefined ? '아직 시작 전' : <Elapsed startedAt={startedAt} endedAt={endedAt} format={(s) => formatElapsed(s, 'text')} />}
|
|
103
|
+
</Text>
|
|
104
|
+
</Inline>
|
|
105
|
+
<Inline gap={2}>
|
|
106
|
+
<Button
|
|
107
|
+
size="sm"
|
|
108
|
+
variant="primary"
|
|
109
|
+
disabled={status === 'active'}
|
|
110
|
+
onClick={() => {
|
|
111
|
+
setStartedAt(Date.now())
|
|
112
|
+
setEndedAt(undefined)
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{status === 'done' ? '다시 시작' : '시작'}
|
|
116
|
+
</Button>
|
|
117
|
+
<Button size="sm" disabled={status !== 'active'} onClick={() => setEndedAt(Date.now())}>
|
|
118
|
+
끝내기
|
|
119
|
+
</Button>
|
|
120
|
+
</Inline>
|
|
121
|
+
</Stack>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### useElapsed
|
|
127
|
+
|
|
128
|
+
startedAt 없이 쓰면 스톱워치예요. running으로 멈추고 이어 세고, reset()으로 0부터 다시.
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { useState } from 'react'
|
|
132
|
+
import { Pause, Play, RotateCcw } from 'lucide-react'
|
|
133
|
+
import { Button, Inline, Stack, Text, formatElapsed, useElapsed } from '@iamfiro/design'
|
|
134
|
+
|
|
135
|
+
export default function Example() {
|
|
136
|
+
const [running, setRunning] = useState(true)
|
|
137
|
+
const { elapsed, reset } = useElapsed(undefined, { running })
|
|
138
|
+
return (
|
|
139
|
+
<Stack gap={3} style={{ width: 320 }}>
|
|
140
|
+
<Inline justify="between" wrap={false}>
|
|
141
|
+
<Text variant="label-md">회의 기록</Text>
|
|
142
|
+
<Text variant="number-md" tone={running ? 'default' : 'tertiary'}>
|
|
143
|
+
{formatElapsed(elapsed)}
|
|
144
|
+
</Text>
|
|
145
|
+
</Inline>
|
|
146
|
+
<Text variant="caption" tone="tertiary">
|
|
147
|
+
{running ? '기록 중' : '일시 정지'} · {formatElapsed(elapsed, 'text')}
|
|
148
|
+
</Text>
|
|
149
|
+
<Inline gap={2}>
|
|
150
|
+
<Button size="sm" icon={running ? <Pause /> : <Play />} onClick={() => setRunning((r) => !r)}>
|
|
151
|
+
{running ? '일시 정지' : '이어서'}
|
|
152
|
+
</Button>
|
|
153
|
+
<Button size="sm" variant="ghost" icon={<RotateCcw />} onClick={reset}>
|
|
154
|
+
처음부터
|
|
155
|
+
</Button>
|
|
156
|
+
</Inline>
|
|
157
|
+
</Stack>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
```
|