@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,203 @@
|
|
|
1
|
+
# EmptyState
|
|
2
|
+
|
|
3
|
+
> 알림 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
|
|
4
|
+
|
|
5
|
+
보여 줄 내용이 없을 때 이유와 다음 행동을 알려 주는 자리. 처음 사용, 검색 결과 없음, 오류, 권한 없음처럼 상황에 맞게 아이콘 · 문구 · 버튼을 바꿔 써요. 나타날 때 아이콘부터 버튼까지 차례로 떠올라요.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { EmptyState } from '@iamfiro/design'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
### EmptyState
|
|
14
|
+
|
|
15
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| `title` (필수) | `ReactNode` | | 한 줄 요약. |
|
|
18
|
+
| `description` | `ReactNode` | | 이유와 다음 행동 설명. |
|
|
19
|
+
| `icon` | `ReactNode` | | 둥근 배지 안에 보이는 아이콘. |
|
|
20
|
+
| `media` | `ReactNode` | | 아이콘 배지 대신 쓸 일러스트 · 이미지. |
|
|
21
|
+
| `actions` | `ReactNode` | | 버튼. 보통 첫 번째가 주요 행동이에요. |
|
|
22
|
+
| `footer` | `ReactNode` | | 버튼 아래 작은 줄(도움말 링크, 단축키 안내). |
|
|
23
|
+
| `tone` | `'default' \| 'accent' \| 'danger' \| 'warning' \| 'success'` | `'default'` | 아이콘 배지 색. |
|
|
24
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 아이콘 36 / 48 / 60px과 여백. |
|
|
25
|
+
| `align` | `'center' \| 'start'` | `'center'` | start는 왼쪽 정렬(패널·표 안). |
|
|
26
|
+
| `className / style` | `string / CSSProperties` | | 루트에 적용돼요. |
|
|
27
|
+
|
|
28
|
+
## 예제
|
|
29
|
+
|
|
30
|
+
예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
|
|
31
|
+
|
|
32
|
+
### 처음 사용
|
|
33
|
+
|
|
34
|
+
무엇을 할 수 있는지 알려 주고 첫 행동을 primary 버튼으로 제안해요. footer에는 작은 도움말을 넣어요.
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { FolderKanban, Plus } from 'lucide-react'
|
|
38
|
+
import { Button, EmptyState, toast } from '@iamfiro/design'
|
|
39
|
+
|
|
40
|
+
export default function Example() {
|
|
41
|
+
return (
|
|
42
|
+
<EmptyState
|
|
43
|
+
tone="accent"
|
|
44
|
+
icon={<FolderKanban />}
|
|
45
|
+
title="첫 프로젝트를 만들어 보세요"
|
|
46
|
+
description="프로젝트에서 팀원과 할 일, 파일, 일정을 한곳에서 관리할 수 있어요."
|
|
47
|
+
actions={
|
|
48
|
+
<>
|
|
49
|
+
<Button variant="primary" icon={<Plus />} onClick={() => toast.success('새 프로젝트를 만들었어요')}>
|
|
50
|
+
새 프로젝트
|
|
51
|
+
</Button>
|
|
52
|
+
<Button variant="ghost">템플릿 둘러보기</Button>
|
|
53
|
+
</>
|
|
54
|
+
}
|
|
55
|
+
footer="팀원이 프로젝트에 초대하면 여기에 바로 보여요."
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 검색 결과 없음
|
|
62
|
+
|
|
63
|
+
검색어를 그대로 보여 주고, 검색어를 지우는 버튼을 함께 둬요. 좁은 곳에서는 size="sm".
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { useState } from 'react'
|
|
67
|
+
import { SearchX } from 'lucide-react'
|
|
68
|
+
import { Button, SearchInput, EmptyState } from '@iamfiro/design'
|
|
69
|
+
|
|
70
|
+
export default function Example() {
|
|
71
|
+
const [query, setQuery] = useState('분기 매출 보고서')
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div style={{ display: 'grid', gap: 8, width: 360 }}>
|
|
75
|
+
<SearchInput placeholder="문서 검색" value={query} onChange={(e) => setQuery(e.target.value)} aria-label="문서 검색" />
|
|
76
|
+
{query ? (
|
|
77
|
+
<EmptyState
|
|
78
|
+
size="sm"
|
|
79
|
+
icon={<SearchX />}
|
|
80
|
+
title="검색 결과가 없어요"
|
|
81
|
+
description={`‘${query}’와 일치하는 문서가 없어요. 철자를 확인하거나 다른 단어로 찾아보세요.`}
|
|
82
|
+
actions={
|
|
83
|
+
<Button size="sm" onClick={() => setQuery('')}>
|
|
84
|
+
검색어 지우기
|
|
85
|
+
</Button>
|
|
86
|
+
}
|
|
87
|
+
/>
|
|
88
|
+
) : (
|
|
89
|
+
<EmptyState size="sm" title="찾을 문서 이름을 입력하세요" />
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 오류 · 다시 시도
|
|
97
|
+
|
|
98
|
+
tone="danger"로 아이콘을 빨갛게 하고, 다시 시도 버튼에는 loading으로 진행 중임을 보여 줘요.
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import { useState } from 'react'
|
|
102
|
+
import { CloudOff, RefreshCw } from 'lucide-react'
|
|
103
|
+
import { Button, EmptyState } from '@iamfiro/design'
|
|
104
|
+
|
|
105
|
+
export default function Example() {
|
|
106
|
+
const [retrying, setRetrying] = useState(false)
|
|
107
|
+
const [attempts, setAttempts] = useState(0)
|
|
108
|
+
|
|
109
|
+
const retry = () => {
|
|
110
|
+
setRetrying(true)
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
setRetrying(false)
|
|
113
|
+
setAttempts((n) => n + 1)
|
|
114
|
+
}, 1400)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<EmptyState
|
|
119
|
+
tone="danger"
|
|
120
|
+
icon={<CloudOff />}
|
|
121
|
+
title="목록을 불러오지 못했어요"
|
|
122
|
+
description="서버와 연결이 끊겼어요. 잠시 후 다시 시도해 주세요."
|
|
123
|
+
actions={
|
|
124
|
+
<Button icon={<RefreshCw />} loading={retrying} onClick={retry}>
|
|
125
|
+
다시 시도
|
|
126
|
+
</Button>
|
|
127
|
+
}
|
|
128
|
+
footer={attempts > 0 ? `${attempts}번 다시 시도했어요 · 오류 코드 503` : '오류 코드 503'}
|
|
129
|
+
/>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 권한 없음
|
|
135
|
+
|
|
136
|
+
tone="warning". 왜 볼 수 없는지와 누구에게 요청하면 되는지 적어요.
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import { useState } from 'react'
|
|
140
|
+
import { Lock } from 'lucide-react'
|
|
141
|
+
import { Button, EmptyState, toast } from '@iamfiro/design'
|
|
142
|
+
|
|
143
|
+
export default function Example() {
|
|
144
|
+
const [requested, setRequested] = useState(false)
|
|
145
|
+
|
|
146
|
+
const request = () => {
|
|
147
|
+
setRequested(true)
|
|
148
|
+
toast.success('접근 권한을 요청했어요', { description: '김민지님이 승인하면 알려 드릴게요.' })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<EmptyState
|
|
153
|
+
tone="warning"
|
|
154
|
+
icon={<Lock />}
|
|
155
|
+
title="이 폴더를 볼 권한이 없어요"
|
|
156
|
+
description="‘재무 · 2026 예산’ 폴더는 재무팀만 볼 수 있어요. 관리자에게 권한을 요청하세요."
|
|
157
|
+
actions={
|
|
158
|
+
<Button variant="primary" disabled={requested} onClick={request}>
|
|
159
|
+
{requested ? '요청 보냄' : '권한 요청'}
|
|
160
|
+
</Button>
|
|
161
|
+
}
|
|
162
|
+
/>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 크기 · 정렬
|
|
168
|
+
|
|
169
|
+
size는 sm / md / lg로 아이콘과 여백이 함께 바뀌어요. 패널이나 표 안에서는 align="start"로 왼쪽에 맞춰요.
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import { Bell, CheckCheck, Inbox } from 'lucide-react'
|
|
173
|
+
import { Button, Card, CardHeader, EmptyState } from '@iamfiro/design'
|
|
174
|
+
|
|
175
|
+
export default function Example() {
|
|
176
|
+
return (
|
|
177
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 16, width: '100%', maxWidth: 720 }}>
|
|
178
|
+
<Card>
|
|
179
|
+
<CardHeader title="알림" />
|
|
180
|
+
<EmptyState
|
|
181
|
+
size="sm"
|
|
182
|
+
align="start"
|
|
183
|
+
tone="success"
|
|
184
|
+
icon={<CheckCheck />}
|
|
185
|
+
title="모두 확인했어요"
|
|
186
|
+
description="새 알림이 오면 여기에 보여요."
|
|
187
|
+
style={{ paddingInline: 0 }}
|
|
188
|
+
/>
|
|
189
|
+
</Card>
|
|
190
|
+
<Card>
|
|
191
|
+
<CardHeader title="받은 편지함" />
|
|
192
|
+
<EmptyState
|
|
193
|
+
size="lg"
|
|
194
|
+
icon={<Inbox />}
|
|
195
|
+
title="받은 메일이 없어요"
|
|
196
|
+
description="새 메일이 도착하면 알려 드릴게요."
|
|
197
|
+
actions={<Button icon={<Bell />}>알림 설정</Button>}
|
|
198
|
+
/>
|
|
199
|
+
</Card>
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# FileChip
|
|
2
|
+
|
|
3
|
+
> 파일 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
|
|
4
|
+
|
|
5
|
+
첨부 파일을 작게 보여 주는 링크형 칩. 클립(또는 색 FileIcon), 파일 이름, 선택적인 크기로 이뤄져요. 이름이 길면 가운데가 줄어 확장자는 항상 보여요. href면 링크, onClick이면 버튼, 둘 다 없으면 글자예요. 업로드 진행 · 재시도가 필요하면 FileUpload를 쓰세요.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { FileChip } from '@iamfiro/design'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 디자인 규칙
|
|
12
|
+
|
|
13
|
+
- 민감한 값은 `MaskedText`(양 끝만, 점 한 묶음이라 길이를 드러내지 않음, 화면 읽기는 "가려진 값"), 글 속 강조 + 메모 말풍선은 `Highlight note`, 첨부 파일 링크는 `FileChip`(확장자는 줄이지 않음).
|
|
14
|
+
|
|
15
|
+
## API
|
|
16
|
+
|
|
17
|
+
### FileChip
|
|
18
|
+
|
|
19
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
20
|
+
| --- | --- | --- | --- |
|
|
21
|
+
| `name` (필수) | `string` | | 파일 이름. 확장자는 줄어들지 않아요. |
|
|
22
|
+
| `size` | `number \| string` | | 바이트(formatBytes로 표시) 또는 문자열("1.2MB"). |
|
|
23
|
+
| `href` | `string` | | 링크로 만들어요. |
|
|
24
|
+
| `download` | `boolean \| string` | | href와 함께: 이동 대신 내려받기(파일 이름 지정 가능). |
|
|
25
|
+
| `target` | `string` | | 링크 대상. '_blank'면 rel=noreferrer. |
|
|
26
|
+
| `onClick` | `(event) => void` | | 버튼으로 만들어요(미리보기 열기 등). |
|
|
27
|
+
| `icon` | `'clip' \| 'file' \| ReactNode` | `'clip'` | 클립 / 색 FileIcon / 직접 넣은 아이콘. |
|
|
28
|
+
| `variant` | `'ghost' \| 'soft'` | `'ghost'` | 채움 없음(링크형) / 회색 칩. |
|
|
29
|
+
| `maxWidth` | `number` | `220` | 이름이 이 폭(px)을 넘으면 줄어요. |
|
|
30
|
+
| `disabled` | `boolean` | | 누를 수 없게. |
|
|
31
|
+
| `aria-label` | `string` | | 이름 대신 읽을 접근성 이름. |
|
|
32
|
+
|
|
33
|
+
## 예제
|
|
34
|
+
|
|
35
|
+
예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
|
|
36
|
+
|
|
37
|
+
### 기본
|
|
38
|
+
|
|
39
|
+
ghost(기본)는 채움 없이 링크처럼, hover에서만 옅게 채워져요. size가 숫자면 formatBytes로 바꿔요.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { Inline, Stack, Text, FileChip, toast } from '@iamfiro/design'
|
|
43
|
+
|
|
44
|
+
export default function Example() {
|
|
45
|
+
return (
|
|
46
|
+
<Stack gap={2} align="start">
|
|
47
|
+
<Text variant="body-sm" tone="secondary">
|
|
48
|
+
김민지 님이 청구서를 첨부했어요.
|
|
49
|
+
</Text>
|
|
50
|
+
<Inline gap={1}>
|
|
51
|
+
<FileChip name="INV-2207.pdf" size={248_000} onClick={() => toast('INV-2207.pdf 미리보기를 열었어요')} />
|
|
52
|
+
<FileChip name="10월-정산-내역.xlsx" size="1.2MB" onClick={() => toast('10월-정산-내역.xlsx 미리보기를 열었어요')} />
|
|
53
|
+
<FileChip name="계약서-사본.pdf" />
|
|
54
|
+
</Inline>
|
|
55
|
+
</Stack>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 아이콘 · 채움 · 줄임
|
|
61
|
+
|
|
62
|
+
icon="file"은 종류별 색 FileIcon, variant="soft"는 회색 칩. maxWidth를 넘는 이름은 확장자 앞에서 줄어요.
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { Stack, FileChip, toast } from '@iamfiro/design'
|
|
66
|
+
|
|
67
|
+
const open = (name: string) => () => toast(`${name} 미리보기를 열었어요`)
|
|
68
|
+
|
|
69
|
+
export default function Example() {
|
|
70
|
+
return (
|
|
71
|
+
<Stack gap={2} align="start">
|
|
72
|
+
<FileChip icon="file" name="배포-체크리스트.docx" size={52_300} onClick={open('배포-체크리스트.docx')} />
|
|
73
|
+
<FileChip icon="file" variant="soft" name="결제-화면-시안.png" size={3_400_000} onClick={open('결제-화면-시안.png')} />
|
|
74
|
+
<FileChip variant="soft" name="2026년-4분기-거래처별-미수금-정리-최종본-이도현-검토.xlsx" maxWidth={180} size="840KB" />
|
|
75
|
+
<FileChip name="만료된-링크.zip" onClick={open('만료된-링크.zip')} disabled />
|
|
76
|
+
</Stack>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# FileIcon
|
|
2
|
+
|
|
3
|
+
> 파일 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
|
|
4
|
+
|
|
5
|
+
파일 종류를 색 띠로 알려 주는 종이 아이콘. 이름 · 확장자 · MIME 타입에서 종류를 추측해요(PDF 빨강, 이미지 보라, 시트 초록…). 색은 tokens.css의 --file-* 토큰이에요.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { FileIcon, fileKind } from '@iamfiro/design'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
### FileIcon
|
|
14
|
+
|
|
15
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| `name` | `string` | | 파일 이름, 확장자 또는 MIME 타입. |
|
|
18
|
+
| `kind` | `'pdf' \| 'image' \| 'sheet' \| 'doc' \| 'slide' \| 'video' \| 'audio' \| 'archive' \| 'code' \| 'other'` | | name에서 추측한 종류 대신 직접 지정. |
|
|
19
|
+
| `size` | `number` | `20` | 크기(px). |
|
|
20
|
+
|
|
21
|
+
## 예제
|
|
22
|
+
|
|
23
|
+
예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
|
|
24
|
+
|
|
25
|
+
### 종류
|
|
26
|
+
|
|
27
|
+
확장자에서 고른 색. 모르는 형식은 회색.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { FileIcon, Inline, Stack, Text } from '@iamfiro/design'
|
|
31
|
+
|
|
32
|
+
const FILES = ['보고서.pdf', '시안.png', '예산.xlsx', '기획서.docx', '발표.pptx', '데모.mp4', '녹음.m4a', '자료.zip', 'index.ts', '메모']
|
|
33
|
+
|
|
34
|
+
export default function Example() {
|
|
35
|
+
return (
|
|
36
|
+
<Inline gap={6}>
|
|
37
|
+
{FILES.map((name) => (
|
|
38
|
+
<Stack key={name} gap={2} align="center">
|
|
39
|
+
<FileIcon name={name} size={28} />
|
|
40
|
+
<Text variant="caption" tone="tertiary">
|
|
41
|
+
{name}
|
|
42
|
+
</Text>
|
|
43
|
+
</Stack>
|
|
44
|
+
))}
|
|
45
|
+
</Inline>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 크기
|
|
51
|
+
|
|
52
|
+
size로 px를 정해요. 목록 행에서는 기본 20.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { FileIcon, Inline } from '@iamfiro/design'
|
|
56
|
+
|
|
57
|
+
export default function Example() {
|
|
58
|
+
return (
|
|
59
|
+
<Inline gap={4} align="end">
|
|
60
|
+
{[16, 20, 24, 32, 40].map((size) => (
|
|
61
|
+
<FileIcon key={size} kind="pdf" size={size} />
|
|
62
|
+
))}
|
|
63
|
+
</Inline>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
```
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# FileUpload
|
|
2
|
+
|
|
3
|
+
> 파일 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
|
|
4
|
+
|
|
5
|
+
파일을 끌어 놓거나 골라서 올리고, 줄마다 진행률과 남은 시간을 보여 줘요. 파운데이션(Card · Dropzone · List · FileIcon · Progress · Text)만 조립해 만들었어요. ×는 보내는 중이면 요청을 끊고, 줄은 높이를 접으며 빠져요. 형식 · 크기 · 개수 제한에 걸린 파일은 빨간 글자로 이유를 남겨요.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { FileUpload, type UploadHandler, type UploadItem } from '@iamfiro/design'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
### FileUpload
|
|
14
|
+
|
|
15
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| `upload` | `(file, { onProgress, signal }) => Promise` | | 파일 하나를 보내요. 끝나면 resolve, 실패하면 reject(메시지가 줄에 나와요). ×를 누르면 signal이 끊겨요. 없으면 고른 파일을 목록에만 넣어요. |
|
|
18
|
+
| `items` | `UploadItem[]` | | 목록(제어). |
|
|
19
|
+
| `defaultItems` | `UploadItem[]` | | 처음 목록(비제어). uploading이고 file이 있으면 바로 보내기 시작해요. |
|
|
20
|
+
| `onItemsChange` | `(items: UploadItem[]) => void` | | 추가 · 진행 · 완료 · 삭제 때마다. |
|
|
21
|
+
| `onRemove` | `(item: UploadItem) => void` | | ×로 뺄 때. 서버에 올라간 파일을 지우는 곳. |
|
|
22
|
+
| `accept` | `string` | | <input accept>와 같은 문법. 끌어 놓은 파일에도 적용돼요. |
|
|
23
|
+
| `maxSize` | `number` | | 파일당 최대 바이트. |
|
|
24
|
+
| `maxFiles` | `number` | | 한 번에 둘 수 있는 파일 수(실패한 줄은 세지 않아요). |
|
|
25
|
+
| `multiple` | `boolean` | `true` | 여러 개 고르기. |
|
|
26
|
+
| `disabled` | `boolean` | | 새 파일을 받지 않아요. |
|
|
27
|
+
| `title` | `ReactNode` | | 드롭 영역 첫 줄. |
|
|
28
|
+
| `description` | `ReactNode` | | 드롭 영역 회색 줄. 기본은 accept · maxSize로 만들어요. |
|
|
29
|
+
| `showSummary` | `boolean` | `true` | 아래 "3개 중 2개 업로드 중 · 1.2 / 8.1MB" 줄. |
|
|
30
|
+
|
|
31
|
+
### UploadItem
|
|
32
|
+
|
|
33
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
34
|
+
| --- | --- | --- | --- |
|
|
35
|
+
| `id` (필수) | `string` | | 고유 id. |
|
|
36
|
+
| `name` (필수) | `string` | | 파일 이름. 확장자로 아이콘 색이 정해져요. |
|
|
37
|
+
| `size` (필수) | `number` | | 바이트. |
|
|
38
|
+
| `status` (필수) | `'uploading' \| 'done' \| 'error'` | | 상태. |
|
|
39
|
+
| `loaded` | `number` | | 보낸 바이트. |
|
|
40
|
+
| `error` | `ReactNode` | | 실패 이유(빨간 글자). |
|
|
41
|
+
| `file` | `File` | | 원본 파일. 있어야 보내고 다시 시도할 수 있어요. |
|
|
42
|
+
|
|
43
|
+
## 예제
|
|
44
|
+
|
|
45
|
+
예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
|
|
46
|
+
|
|
47
|
+
### 기본
|
|
48
|
+
|
|
49
|
+
upload 함수가 onProgress로 보낸 바이트를 알려 주면 막대와 남은 시간이 따라와요. 파일을 더 끌어 놓아 보세요.
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { useState } from 'react'
|
|
53
|
+
import { FileUpload, type UploadHandler, type UploadItem } from '@iamfiro/design'
|
|
54
|
+
|
|
55
|
+
const MB = 1024 * 1024
|
|
56
|
+
|
|
57
|
+
// 실제 앱에서는 fetch · XHR로 보내고 진행 이벤트를 onProgress로 넘겨요. signal이 끊기면 요청도 멈춰요.
|
|
58
|
+
const fakeUpload: UploadHandler = (file, { onProgress, signal }) =>
|
|
59
|
+
new Promise((resolve) => {
|
|
60
|
+
let loaded = 0
|
|
61
|
+
const timer = setInterval(() => {
|
|
62
|
+
loaded = Math.min(file.size, loaded + file.size * (0.02 + Math.random() * 0.04))
|
|
63
|
+
onProgress(loaded)
|
|
64
|
+
if (loaded >= file.size) {
|
|
65
|
+
clearInterval(timer)
|
|
66
|
+
resolve(undefined)
|
|
67
|
+
}
|
|
68
|
+
}, 200)
|
|
69
|
+
signal.addEventListener('abort', () => clearInterval(timer))
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const sample = (name: string, mb: number) => new File([new Uint8Array(Math.round(mb * MB))], name)
|
|
73
|
+
|
|
74
|
+
export default function Example() {
|
|
75
|
+
// 올리고 있던 파일로 시작해요. status가 'uploading'이고 file이 있으면 바로 보내기 시작해요.
|
|
76
|
+
const [initial] = useState<UploadItem[]>(() => [
|
|
77
|
+
{ id: 'q3', name: '3분기 이사회 보고서.pdf', size: 4.2 * MB, status: 'uploading', file: sample('3분기 이사회 보고서.pdf', 4.2) },
|
|
78
|
+
{ id: 'hero', name: '메인 이미지.png', size: 2.6 * MB, status: 'uploading', file: sample('메인 이미지.png', 2.6) },
|
|
79
|
+
{ id: 'budget', name: '2027 예산안.xlsx', size: 1.3 * MB, status: 'uploading', file: sample('2027 예산안.xlsx', 1.3) },
|
|
80
|
+
{ id: 'demo', name: '제품 데모.mp4', size: 48 * MB, status: 'error', error: '너무 커요 · 최대 25MB' },
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<FileUpload
|
|
85
|
+
upload={fakeUpload}
|
|
86
|
+
defaultItems={initial}
|
|
87
|
+
maxSize={25 * MB}
|
|
88
|
+
description="PDF, PNG, JPG 등 · 파일당 최대 25MB"
|
|
89
|
+
style={{ width: 460 }}
|
|
90
|
+
/>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 제한과 실패
|
|
96
|
+
|
|
97
|
+
accept · maxSize · maxFiles에 걸리면 이유를 적은 줄이 생겨요(형식이 다르면 영역이 흔들려요). 보내다 실패하면 ↻로 다시 시도해요.
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { FileUpload, type UploadHandler } from '@iamfiro/design'
|
|
101
|
+
|
|
102
|
+
const MB = 1024 * 1024
|
|
103
|
+
|
|
104
|
+
// 셋 중 하나는 절반쯤 가다가 실패해요. 실패한 줄의 ↻로 다시 보내요.
|
|
105
|
+
const flakyUpload: UploadHandler = (file, { onProgress, signal }) =>
|
|
106
|
+
new Promise((resolve, reject) => {
|
|
107
|
+
const fail = Math.random() < 0.34
|
|
108
|
+
let loaded = 0
|
|
109
|
+
const timer = setInterval(() => {
|
|
110
|
+
loaded = Math.min(file.size, loaded + file.size * 0.08)
|
|
111
|
+
onProgress(loaded)
|
|
112
|
+
if (fail && loaded > file.size / 2) {
|
|
113
|
+
clearInterval(timer)
|
|
114
|
+
reject(new Error('연결이 끊겼어요 · 다시 시도해 주세요'))
|
|
115
|
+
} else if (loaded >= file.size) {
|
|
116
|
+
clearInterval(timer)
|
|
117
|
+
resolve(undefined)
|
|
118
|
+
}
|
|
119
|
+
}, 180)
|
|
120
|
+
signal.addEventListener('abort', () => clearInterval(timer))
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
export default function Example() {
|
|
124
|
+
return (
|
|
125
|
+
<FileUpload
|
|
126
|
+
upload={flakyUpload}
|
|
127
|
+
accept="image/*,.pdf"
|
|
128
|
+
maxSize={5 * MB}
|
|
129
|
+
maxFiles={3}
|
|
130
|
+
title="증빙 서류를 끌어 놓거나 찾아보기"
|
|
131
|
+
style={{ width: 460 }}
|
|
132
|
+
/>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 고르기만 하기 (제어)
|
|
138
|
+
|
|
139
|
+
upload 없이 items로 목록만 관리하다가 폼을 보낼 때 한꺼번에 처리해요.
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
import { useState } from 'react'
|
|
143
|
+
import { Send } from 'lucide-react'
|
|
144
|
+
import { Button, Stack, FileUpload, toast, type UploadItem } from '@iamfiro/design'
|
|
145
|
+
|
|
146
|
+
export default function Example() {
|
|
147
|
+
// upload 없이 쓰면 고른 파일을 목록으로만 보여 줘요. 실제 전송은 폼을 보낼 때.
|
|
148
|
+
const [items, setItems] = useState<UploadItem[]>([])
|
|
149
|
+
const ready = items.filter((it) => it.status === 'done')
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<Stack gap={3} style={{ width: 460 }}>
|
|
153
|
+
<FileUpload items={items} onItemsChange={setItems} accept=".pdf,.docx,.hwp" maxFiles={5} showSummary={false} />
|
|
154
|
+
<Button
|
|
155
|
+
variant="primary"
|
|
156
|
+
icon={<Send />}
|
|
157
|
+
disabled={ready.length === 0}
|
|
158
|
+
onClick={() => {
|
|
159
|
+
toast.success(`파일 ${ready.length}개를 보냈어요`)
|
|
160
|
+
setItems([])
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
{ready.length > 0 ? `${ready.length}개 보내기` : '보내기'}
|
|
164
|
+
</Button>
|
|
165
|
+
</Stack>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
```
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# FilterChips
|
|
2
|
+
|
|
3
|
+
> 선택 컨트롤 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
|
|
4
|
+
|
|
5
|
+
목록을 좁히는 필터 칩. FilterChipGroup으로 묶어 하나 또는 여러 개를 고르거나, FilterChip 하나만 켜고 끄는 토글로 써요. 개수 표시, 초기화 버튼, 제거 가능한 적용 필터를 지원해요.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { FilterChipGroup, FilterChip } from '@iamfiro/design'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
### FilterChipGroup
|
|
14
|
+
|
|
15
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| `type` (필수) | `'single' \| 'multiple'` | | 하나만 / 여러 개. |
|
|
18
|
+
| `value` | `V \| null · V[] (multiple)` | | 선택된 값(제어). V는 string \| number. |
|
|
19
|
+
| `defaultValue` | `V \| null · V[] (multiple)` | | 처음 선택값(비제어). |
|
|
20
|
+
| `onValueChange` | `(value: V \| null) => void · (value: V[]) => void` | | 값이 바뀔 때. |
|
|
21
|
+
| `required` | `boolean` | | type='single' 전용. 선택된 칩을 다시 눌러도 해제되지 않아요. |
|
|
22
|
+
| `variant` | `'solid' \| 'soft'` | `'solid'` | solid: 선택 시 진한 색 · soft: 선택 시 파란 톤. 칩에 그대로 전달돼요. |
|
|
23
|
+
| `size` | `'sm' \| 'md'` | `'md'` | 칩 크기. |
|
|
24
|
+
| `clearable` | `boolean` | | 하나라도 선택되면 초기화 버튼 표시. |
|
|
25
|
+
| `clearLabel` | `ReactNode` | `'초기화'` | 초기화 버튼 문구. |
|
|
26
|
+
| `disabled` | `boolean` | | 모든 칩 비활성. |
|
|
27
|
+
| `aria-label` | `string` | | 그룹 이름(스크린 리더). |
|
|
28
|
+
|
|
29
|
+
### FilterChip
|
|
30
|
+
|
|
31
|
+
나머지 button 속성도 받아요. FilterChipGroup.Chip으로도 쓸 수 있어요.
|
|
32
|
+
|
|
33
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
34
|
+
| --- | --- | --- | --- |
|
|
35
|
+
| `value` | `string \| number` | | 그룹 안에서 이 칩이 켜고 끄는 값. |
|
|
36
|
+
| `selected` | `boolean` | | 단독 사용 시 선택 상태(제어). |
|
|
37
|
+
| `onSelectedChange` | `(selected: boolean) => void` | | 단독 사용 시 눌렀을 때. |
|
|
38
|
+
| `onRemove` | `() => void` | | 있으면 × 버튼을 보여주고, 누르면 호출. |
|
|
39
|
+
| `removeLabel` | `string` | `'필터 제거'` | × 버튼의 aria-label. |
|
|
40
|
+
| `icon` | `ReactNode` | | 앞 아이콘. 선택되면 체크로 바뀌어요. |
|
|
41
|
+
| `count` | `number` | | 오른쪽 개수. |
|
|
42
|
+
| `variant` | `'solid' \| 'soft'` | `그룹 값 또는 'solid'` | 선택 스타일. |
|
|
43
|
+
| `size` | `'sm' \| 'md'` | `그룹 값 또는 'md'` | 크기. |
|
|
44
|
+
| `disabled` | `boolean` | | 비활성. |
|
|
45
|
+
|
|
46
|
+
## 예제
|
|
47
|
+
|
|
48
|
+
예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
|
|
49
|
+
|
|
50
|
+
### 여러 개 선택 · 초기화
|
|
51
|
+
|
|
52
|
+
count로 항목 수를 보여줘요. clearable이면 하나라도 고른 뒤에 초기화 버튼이 나타나요.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { useState } from 'react'
|
|
56
|
+
import { FilterChip, FilterChipGroup } from '@iamfiro/design'
|
|
57
|
+
|
|
58
|
+
export default function Example() {
|
|
59
|
+
const [teams, setTeams] = useState<string[]>(['design'])
|
|
60
|
+
return (
|
|
61
|
+
<FilterChipGroup type="multiple" value={teams} onValueChange={setTeams} clearable aria-label="팀">
|
|
62
|
+
<FilterChip value="design" count={12}>
|
|
63
|
+
디자인
|
|
64
|
+
</FilterChip>
|
|
65
|
+
<FilterChip value="dev" count={8}>
|
|
66
|
+
개발
|
|
67
|
+
</FilterChip>
|
|
68
|
+
<FilterChip value="marketing" count={5}>
|
|
69
|
+
마케팅
|
|
70
|
+
</FilterChip>
|
|
71
|
+
<FilterChip value="pm" count={3}>
|
|
72
|
+
기획
|
|
73
|
+
</FilterChip>
|
|
74
|
+
<FilterChip value="legal" count={0} disabled>
|
|
75
|
+
법무
|
|
76
|
+
</FilterChip>
|
|
77
|
+
</FilterChipGroup>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 하나만 선택 · soft
|
|
83
|
+
|
|
84
|
+
variant='soft'는 선택된 칩을 파란 톤으로 칠해요. required면 선택을 끌 수 없어요.
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { useState } from 'react'
|
|
88
|
+
import { FilterChip, FilterChipGroup } from '@iamfiro/design'
|
|
89
|
+
|
|
90
|
+
export default function Example() {
|
|
91
|
+
const [sort, setSort] = useState<string | null>('recent')
|
|
92
|
+
return (
|
|
93
|
+
<FilterChipGroup type="single" variant="soft" required value={sort} onValueChange={setSort} aria-label="정렬">
|
|
94
|
+
<FilterChip value="recent">최신순</FilterChip>
|
|
95
|
+
<FilterChip value="popular">인기순</FilterChip>
|
|
96
|
+
<FilterChip value="rating">평점순</FilterChip>
|
|
97
|
+
<FilterChip value="price">낮은 가격순</FilterChip>
|
|
98
|
+
</FilterChipGroup>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 적용된 필터
|
|
104
|
+
|
|
105
|
+
onRemove를 주면 오른쪽에 × 버튼이 생겨요. removeLabel에 무엇을 지우는지 적어 주세요.
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import { useState } from 'react'
|
|
109
|
+
import { Button, Text, FilterChip } from '@iamfiro/design'
|
|
110
|
+
|
|
111
|
+
const DEFAULT_FILTERS = [
|
|
112
|
+
{ id: 'status', label: '상태: 진행 중' },
|
|
113
|
+
{ id: 'owner', label: '담당자: 김민지' },
|
|
114
|
+
{ id: 'due', label: '마감: 이번 주' },
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
export default function Example() {
|
|
118
|
+
const [filters, setFilters] = useState(DEFAULT_FILTERS)
|
|
119
|
+
|
|
120
|
+
const remove = (id: string) => setFilters((list) => list.filter((f) => f.id !== id))
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
|
|
124
|
+
{filters.map((f) => (
|
|
125
|
+
<FilterChip key={f.id} variant="soft" selected removeLabel={`${f.label} 필터 제거`} onRemove={() => remove(f.id)}>
|
|
126
|
+
{f.label}
|
|
127
|
+
</FilterChip>
|
|
128
|
+
))}
|
|
129
|
+
{filters.length === 0 && (
|
|
130
|
+
<>
|
|
131
|
+
<Text variant="body-sm" tone="tertiary">
|
|
132
|
+
적용된 필터가 없어요
|
|
133
|
+
</Text>
|
|
134
|
+
<Button variant="ghost" size="sm" onClick={() => setFilters(DEFAULT_FILTERS)}>
|
|
135
|
+
기본 필터 복원
|
|
136
|
+
</Button>
|
|
137
|
+
</>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 단독 사용
|
|
145
|
+
|
|
146
|
+
그룹 없이 selected / onSelectedChange로 켜고 꺼요. icon은 선택되면 체크로 바뀌어요.
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
import { useState } from 'react'
|
|
150
|
+
import { Paperclip, Star } from 'lucide-react'
|
|
151
|
+
import { FilterChip } from '@iamfiro/design'
|
|
152
|
+
|
|
153
|
+
export default function Example() {
|
|
154
|
+
const [starred, setStarred] = useState(false)
|
|
155
|
+
const [withFiles, setWithFiles] = useState(true)
|
|
156
|
+
return (
|
|
157
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
158
|
+
<FilterChip size="sm" icon={<Star />} selected={starred} onSelectedChange={setStarred}>
|
|
159
|
+
즐겨찾기만
|
|
160
|
+
</FilterChip>
|
|
161
|
+
<FilterChip size="sm" icon={<Paperclip />} selected={withFiles} onSelectedChange={setWithFiles}>
|
|
162
|
+
첨부파일 있음
|
|
163
|
+
</FilterChip>
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
```
|