@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,276 @@
|
|
|
1
|
+
# Menubar
|
|
2
|
+
|
|
3
|
+
> 메뉴 & 오버레이 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
|
|
4
|
+
|
|
5
|
+
데스크톱 앱처럼 파일 · 편집 · 보기 메뉴를 가로로 늘어놓은 막대. 누르는 순간 열리고(누른 채 항목으로 끌어 놓으면 실행), 하나가 열려 있는 동안은 옆 메뉴 이름에 마우스를 올리기만 해도 즉시 바뀌어요. 열린 메뉴 이름 뒤의 회색 채움은 메뉴를 바꿀 때 미끄러져 옮겨 가요. 키보드는 ←/→로 메뉴 사이를 이동(열린 메뉴 안에서도, 하위 메뉴가 아니면)하고 ↓ · Enter · Space로 열어요. 막대 전체가 Tab 한 번(roving tabindex). 항목 · 단축키 · 체크/라디오 · 하위 메뉴는 DropdownMenu와 같은 조각을 써요. 막대에는 테두리가 없어요.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Menubar, MenubarMenu, MenubarItem, MenubarCheckboxItem, MenubarRadioGroup, MenubarRadioItem, MenubarLabel, MenubarSeparator, MenubarSub } from '@iamfiro/design'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
### Menubar
|
|
14
|
+
|
|
15
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| `children` (필수) | `ReactNode` | | MenubarMenu들(순서대로). |
|
|
18
|
+
| `value / onValueChange` | `string \| null / (value) => void` | | 열린 메뉴의 value(null = 모두 닫힘). 밖에서 제어할 때. |
|
|
19
|
+
| `defaultValue` | `string \| null` | `null` | 처음 열린 메뉴(비제어). |
|
|
20
|
+
| `aria-label` | `string` | | 막대(role="menubar")의 접근성 이름. |
|
|
21
|
+
| `className` | `string` | | 막대에 붙일 클래스. |
|
|
22
|
+
|
|
23
|
+
### MenubarMenu
|
|
24
|
+
|
|
25
|
+
막대의 메뉴 하나(이름 버튼 + 패널). 항목은 DropdownMenu와 같아요: MenubarItem(= DropdownMenuItem) · MenubarCheckboxItem · MenubarRadioGroup · MenubarRadioItem · MenubarLabel · MenubarSeparator · MenubarSub.
|
|
26
|
+
|
|
27
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `label` (필수) | `ReactNode` | | 막대에 보이는 메뉴 이름(예: 파일). |
|
|
30
|
+
| `children` (필수) | `ReactNode` | | 항목, 라벨, 구분선, 하위 메뉴. |
|
|
31
|
+
| `value` | `string` | | Menubar value에서 이 메뉴를 가리키는 값. 없으면 내부 id. |
|
|
32
|
+
| `disabled` | `boolean` | | 비활성. 열리지 않고 키보드 이동에서 건너뛰어요. |
|
|
33
|
+
| `minWidth` | `number` | `220` | 패널 최소 폭(px). |
|
|
34
|
+
|
|
35
|
+
## 예제
|
|
36
|
+
|
|
37
|
+
예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
|
|
38
|
+
|
|
39
|
+
### 문서 편집기
|
|
40
|
+
|
|
41
|
+
메뉴 하나를 연 채로 옆 이름 위로 마우스를 옮겨 보세요. 메뉴가 바뀔 때는 패널이 새로 튀어나오지 않고 제자리에 바로 나타나며, 회색 채움만 미끄러져요. 막대를 완전히 닫을 때는 패널이 퇴장 애니메이션으로 사라져요. 하위 메뉴(최근 파일)는 DropdownMenu와 같이 머물면 열리고 →로도 열려요.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { useState } from 'react'
|
|
45
|
+
import {
|
|
46
|
+
ClipboardPaste,
|
|
47
|
+
Copy,
|
|
48
|
+
FilePlus,
|
|
49
|
+
FolderOpen,
|
|
50
|
+
History,
|
|
51
|
+
LogOut,
|
|
52
|
+
Printer,
|
|
53
|
+
Redo2,
|
|
54
|
+
Save,
|
|
55
|
+
Scissors,
|
|
56
|
+
Search,
|
|
57
|
+
Trash2,
|
|
58
|
+
Undo2,
|
|
59
|
+
} from 'lucide-react'
|
|
60
|
+
import { Stack, Text, Menubar, MenubarItem, MenubarMenu, MenubarSeparator, MenubarSub, toast } from '@iamfiro/design'
|
|
61
|
+
|
|
62
|
+
const RECENT = ['3분기 이사회 보고서.docx', '출시 일정.xlsx', '브랜드 가이드.fig']
|
|
63
|
+
|
|
64
|
+
export default function Example() {
|
|
65
|
+
const [last, setLast] = useState('없음')
|
|
66
|
+
const run = (label: string) => () => {
|
|
67
|
+
setLast(label)
|
|
68
|
+
toast(`‘${label}’ 실행`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Stack gap={3} style={{ width: '100%', maxWidth: 560 }}>
|
|
73
|
+
<Menubar aria-label="문서 편집기">
|
|
74
|
+
<MenubarMenu label="파일">
|
|
75
|
+
<MenubarItem icon={<FilePlus />} shortcut={['mod', 'N']} onSelect={run('새 문서')}>
|
|
76
|
+
새 문서
|
|
77
|
+
</MenubarItem>
|
|
78
|
+
<MenubarItem icon={<FolderOpen />} shortcut={['mod', 'O']} onSelect={run('열기')}>
|
|
79
|
+
열기…
|
|
80
|
+
</MenubarItem>
|
|
81
|
+
<MenubarSub label="최근 파일" icon={<History />}>
|
|
82
|
+
{RECENT.map((name) => (
|
|
83
|
+
<MenubarItem key={name} onSelect={run(name)}>
|
|
84
|
+
{name}
|
|
85
|
+
</MenubarItem>
|
|
86
|
+
))}
|
|
87
|
+
</MenubarSub>
|
|
88
|
+
<MenubarSeparator />
|
|
89
|
+
<MenubarItem icon={<Save />} shortcut={['mod', 'S']} onSelect={run('저장')}>
|
|
90
|
+
저장
|
|
91
|
+
</MenubarItem>
|
|
92
|
+
<MenubarItem icon={<Printer />} shortcut={['mod', 'P']} onSelect={run('인쇄')}>
|
|
93
|
+
인쇄…
|
|
94
|
+
</MenubarItem>
|
|
95
|
+
<MenubarSeparator />
|
|
96
|
+
<MenubarItem icon={<Trash2 />} danger onSelect={run('휴지통으로 이동')}>
|
|
97
|
+
휴지통으로 이동
|
|
98
|
+
</MenubarItem>
|
|
99
|
+
</MenubarMenu>
|
|
100
|
+
<MenubarMenu label="편집">
|
|
101
|
+
<MenubarItem icon={<Undo2 />} shortcut={['mod', 'Z']} onSelect={run('실행 취소')}>
|
|
102
|
+
실행 취소
|
|
103
|
+
</MenubarItem>
|
|
104
|
+
<MenubarItem icon={<Redo2 />} shortcut={['mod', 'shift', 'Z']} disabled>
|
|
105
|
+
다시 실행
|
|
106
|
+
</MenubarItem>
|
|
107
|
+
<MenubarSeparator />
|
|
108
|
+
<MenubarItem icon={<Scissors />} shortcut={['mod', 'X']} onSelect={run('잘라내기')}>
|
|
109
|
+
잘라내기
|
|
110
|
+
</MenubarItem>
|
|
111
|
+
<MenubarItem icon={<Copy />} shortcut={['mod', 'C']} onSelect={run('복사')}>
|
|
112
|
+
복사
|
|
113
|
+
</MenubarItem>
|
|
114
|
+
<MenubarItem icon={<ClipboardPaste />} shortcut={['mod', 'V']} onSelect={run('붙여넣기')}>
|
|
115
|
+
붙여넣기
|
|
116
|
+
</MenubarItem>
|
|
117
|
+
<MenubarSeparator />
|
|
118
|
+
<MenubarItem icon={<Search />} shortcut={['mod', 'F']} onSelect={run('찾기')}>
|
|
119
|
+
찾기…
|
|
120
|
+
</MenubarItem>
|
|
121
|
+
</MenubarMenu>
|
|
122
|
+
<MenubarMenu label="삽입">
|
|
123
|
+
<MenubarItem onSelect={run('표')}>표</MenubarItem>
|
|
124
|
+
<MenubarItem onSelect={run('이미지')}>이미지…</MenubarItem>
|
|
125
|
+
<MenubarItem onSelect={run('링크')} shortcut={['mod', 'K']}>
|
|
126
|
+
링크
|
|
127
|
+
</MenubarItem>
|
|
128
|
+
<MenubarSub label="구분 요소">
|
|
129
|
+
<MenubarItem onSelect={run('가로줄')}>가로줄</MenubarItem>
|
|
130
|
+
<MenubarItem onSelect={run('페이지 나누기')} shortcut={['mod', 'enter']}>
|
|
131
|
+
페이지 나누기
|
|
132
|
+
</MenubarItem>
|
|
133
|
+
</MenubarSub>
|
|
134
|
+
</MenubarMenu>
|
|
135
|
+
<MenubarMenu label="계정">
|
|
136
|
+
<MenubarItem description="minji@lumen.kr" onSelect={run('프로필')}>
|
|
137
|
+
김민지
|
|
138
|
+
</MenubarItem>
|
|
139
|
+
<MenubarSeparator />
|
|
140
|
+
<MenubarItem icon={<LogOut />} onSelect={run('로그아웃')}>
|
|
141
|
+
로그아웃
|
|
142
|
+
</MenubarItem>
|
|
143
|
+
</MenubarMenu>
|
|
144
|
+
</Menubar>
|
|
145
|
+
<Text variant="caption" tone="tertiary">
|
|
146
|
+
하나를 연 채로 옆 메뉴에 마우스를 올리면 바로 바뀌어요. 마지막 동작: {last}
|
|
147
|
+
</Text>
|
|
148
|
+
</Stack>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 체크 · 라디오
|
|
154
|
+
|
|
155
|
+
체크 항목과 라디오 그룹은 고른 뒤에도 메뉴가 열려 있어 여러 설정을 이어서 바꿀 수 있어요. MenubarLabel로 묶음 제목을 붙여요.
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
import { useState } from 'react'
|
|
159
|
+
import { PanelLeft, Ruler } from 'lucide-react'
|
|
160
|
+
import { Stack, Text, Menubar, MenubarCheckboxItem, MenubarItem, MenubarLabel, MenubarMenu, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, toast } from '@iamfiro/design'
|
|
161
|
+
|
|
162
|
+
export default function Example() {
|
|
163
|
+
const [sidebar, setSidebar] = useState(true)
|
|
164
|
+
const [ruler, setRuler] = useState(false)
|
|
165
|
+
const [zoom, setZoom] = useState('fit')
|
|
166
|
+
const [sort, setSort] = useState('updated')
|
|
167
|
+
|
|
168
|
+
const ZOOM: Record<string, string> = { fit: '화면에 맞추기', '100': '100%', '150': '150%' }
|
|
169
|
+
const SORT: Record<string, string> = { updated: '수정한 날짜', name: '이름', size: '크기' }
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<Stack gap={3} style={{ width: '100%', maxWidth: 520 }}>
|
|
173
|
+
<Menubar aria-label="보기 설정">
|
|
174
|
+
<MenubarMenu label="보기">
|
|
175
|
+
<MenubarCheckboxItem icon={<PanelLeft />} checked={sidebar} onCheckedChange={setSidebar} shortcut={['mod', '\\']}>
|
|
176
|
+
사이드바
|
|
177
|
+
</MenubarCheckboxItem>
|
|
178
|
+
<MenubarCheckboxItem icon={<Ruler />} checked={ruler} onCheckedChange={setRuler} shortcut={['shift', 'R']}>
|
|
179
|
+
눈금자
|
|
180
|
+
</MenubarCheckboxItem>
|
|
181
|
+
<MenubarSeparator />
|
|
182
|
+
<MenubarRadioGroup label="확대" value={zoom} onValueChange={setZoom}>
|
|
183
|
+
{Object.entries(ZOOM).map(([v, label]) => (
|
|
184
|
+
<MenubarRadioItem key={v} value={v}>
|
|
185
|
+
{label}
|
|
186
|
+
</MenubarRadioItem>
|
|
187
|
+
))}
|
|
188
|
+
</MenubarRadioGroup>
|
|
189
|
+
</MenubarMenu>
|
|
190
|
+
<MenubarMenu label="정렬">
|
|
191
|
+
<MenubarRadioGroup label="정렬 기준" value={sort} onValueChange={setSort}>
|
|
192
|
+
{Object.entries(SORT).map(([v, label]) => (
|
|
193
|
+
<MenubarRadioItem key={v} value={v}>
|
|
194
|
+
{label}
|
|
195
|
+
</MenubarRadioItem>
|
|
196
|
+
))}
|
|
197
|
+
</MenubarRadioGroup>
|
|
198
|
+
</MenubarMenu>
|
|
199
|
+
<MenubarMenu label="창">
|
|
200
|
+
<MenubarLabel>열린 창</MenubarLabel>
|
|
201
|
+
<MenubarItem onSelect={() => toast('‘10월 회의록’으로 전환했어요')}>10월 회의록</MenubarItem>
|
|
202
|
+
<MenubarItem onSelect={() => toast('‘디자인 리뷰’로 전환했어요')}>디자인 리뷰</MenubarItem>
|
|
203
|
+
<MenubarSeparator />
|
|
204
|
+
<MenubarItem shortcut={['mod', 'M']} onSelect={() => toast('창을 최소화했어요')}>
|
|
205
|
+
최소화
|
|
206
|
+
</MenubarItem>
|
|
207
|
+
</MenubarMenu>
|
|
208
|
+
</Menubar>
|
|
209
|
+
<Text variant="caption" tone="tertiary">
|
|
210
|
+
사이드바 {sidebar ? '켜짐' : '꺼짐'} · 눈금자 {ruler ? '켜짐' : '꺼짐'} · {ZOOM[zoom]} · {SORT[sort]} 순
|
|
211
|
+
</Text>
|
|
212
|
+
</Stack>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 키보드 · 제어
|
|
218
|
+
|
|
219
|
+
value · onValueChange로 열린 메뉴를 밖에서 읽거나 정해요(null = 닫힘). disabled 메뉴는 ←/→ 이동에서 건너뛰어요. 열린 메뉴 안에서 ←/→를 누르면 이웃 메뉴가 열리고 첫 항목이 강조돼요. Esc는 메뉴를 닫고 그 메뉴 이름으로 포커스를 돌려줘요.
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
import { useState } from 'react'
|
|
223
|
+
import { Inline, Kbd, Stack, Text, DescriptionList, Menubar, MenubarItem, MenubarMenu, MenubarSeparator, toast } from '@iamfiro/design'
|
|
224
|
+
|
|
225
|
+
const LABELS: Record<string, string> = { file: '파일', edit: '편집', share: '공유', help: '도움말' }
|
|
226
|
+
|
|
227
|
+
export default function Example() {
|
|
228
|
+
const [open, setOpen] = useState<string | null>(null)
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<Stack gap={4} style={{ width: '100%', maxWidth: 520 }}>
|
|
232
|
+
<Menubar aria-label="키보드 예제" value={open} onValueChange={setOpen}>
|
|
233
|
+
<MenubarMenu value="file" label="파일">
|
|
234
|
+
<MenubarItem shortcut={['mod', 'N']} onSelect={() => toast('새 문서를 만들었어요')}>
|
|
235
|
+
새 문서
|
|
236
|
+
</MenubarItem>
|
|
237
|
+
<MenubarItem shortcut={['mod', 'S']} onSelect={() => toast.success('저장했어요')}>
|
|
238
|
+
저장
|
|
239
|
+
</MenubarItem>
|
|
240
|
+
</MenubarMenu>
|
|
241
|
+
<MenubarMenu value="edit" label="편집">
|
|
242
|
+
<MenubarItem shortcut={['mod', 'Z']} onSelect={() => toast('실행을 취소했어요')}>
|
|
243
|
+
실행 취소
|
|
244
|
+
</MenubarItem>
|
|
245
|
+
<MenubarSeparator />
|
|
246
|
+
<MenubarItem shortcut={['mod', 'A']} onSelect={() => toast('모두 선택했어요')}>
|
|
247
|
+
모두 선택
|
|
248
|
+
</MenubarItem>
|
|
249
|
+
</MenubarMenu>
|
|
250
|
+
<MenubarMenu value="share" label="공유" disabled>
|
|
251
|
+
<MenubarItem>링크 복사</MenubarItem>
|
|
252
|
+
</MenubarMenu>
|
|
253
|
+
<MenubarMenu value="help" label="도움말">
|
|
254
|
+
<MenubarItem shortcut={['mod', '/']} onSelect={() => toast('단축키 목록을 열었어요')}>
|
|
255
|
+
단축키 보기
|
|
256
|
+
</MenubarItem>
|
|
257
|
+
<MenubarItem onSelect={() => toast('새 소식을 열었어요')}>새 소식</MenubarItem>
|
|
258
|
+
</MenubarMenu>
|
|
259
|
+
</Menubar>
|
|
260
|
+
<Text variant="caption" tone="tertiary">
|
|
261
|
+
열린 메뉴: {open ? LABELS[open] : '없음'} · ‘공유’는 비활성이라 건너뛰어요
|
|
262
|
+
</Text>
|
|
263
|
+
<DescriptionList
|
|
264
|
+
variant="soft"
|
|
265
|
+
layout="inline"
|
|
266
|
+
items={[
|
|
267
|
+
{ label: '메뉴 사이 이동', value: <Inline gap={1}><Kbd keys={['left']} size="sm" /><Kbd keys={['right']} size="sm" /></Inline> },
|
|
268
|
+
{ label: '열기 (첫 / 마지막 항목)', value: <Inline gap={1}><Kbd keys={['down']} size="sm" /><Kbd keys={['enter']} size="sm" /><Kbd keys={['up']} size="sm" /></Inline> },
|
|
269
|
+
{ label: '처음 · 끝 메뉴', value: <Inline gap={1}><Kbd keys={['Home']} size="sm" /><Kbd keys={['End']} size="sm" /></Inline> },
|
|
270
|
+
{ label: '닫고 메뉴 이름으로', value: <Kbd keys={['esc']} size="sm" /> },
|
|
271
|
+
]}
|
|
272
|
+
/>
|
|
273
|
+
</Stack>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
```
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# MultiSelect
|
|
2
|
+
|
|
3
|
+
> 선택 컨트롤 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
|
|
4
|
+
|
|
5
|
+
목록에서 여러 개를 고르는 셀렉트. 고르는 동안 목록이 열린 채로 유지되고, 고른 항목은 지울 수 있는 태그나 "n개 선택됨" 요약으로 보여줘요. 선택지 형태(ListOption)는 Select 페이지를 참고하세요.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { MultiSelect, type ListOption } from '@iamfiro/design'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
### MultiSelect
|
|
14
|
+
|
|
15
|
+
| prop | 타입 | 기본값 | 설명 |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| `options` (필수) | `ListOption<V>[]` | | 선택지 목록. ListOption은 Select 페이지 참고. |
|
|
18
|
+
| `value` | `V[]` | | 선택된 값들(제어). 항상 options 순서로 정렬돼요. |
|
|
19
|
+
| `defaultValue` | `V[]` | `[]` | 처음 선택값(비제어). |
|
|
20
|
+
| `onValueChange` | `(value: V[]) => void` | | 값이 바뀔 때. |
|
|
21
|
+
| `open / onOpenChange` | `boolean / (open) => void` | | 열림 상태를 밖에서 제어할 때. |
|
|
22
|
+
| `display` | `'tags' \| 'count'` | `'tags'` | 트리거에 태그로 보여줄지, 개수 요약으로 보여줄지. |
|
|
23
|
+
| `maxTags` | `number` | `2` | 보여줄 태그 수. 나머지는 +N으로 접혀요. |
|
|
24
|
+
| `formatCount` | `(count: number) => ReactNode` | ``${n}개 선택됨`` | display='count'일 때 문구. |
|
|
25
|
+
| `selectAll` | `boolean` | | 목록 맨 위에 전체 선택 행 추가(Ctrl/⌘ + A). |
|
|
26
|
+
| `selectAllLabel` | `ReactNode` | `'전체 선택'` | 전체 선택 행 문구. |
|
|
27
|
+
| `placeholder` | `ReactNode` | `'선택하세요'` | 선택 전 문구. |
|
|
28
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 높이 36 / 44 / 52px. |
|
|
29
|
+
| `invalid` | `boolean` | | 오류 표시. |
|
|
30
|
+
| `disabled` | `boolean` | | 비활성. 태그의 × 버튼도 숨겨져요. |
|
|
31
|
+
| `leading` | `ReactNode` | | 값 앞 아이콘. |
|
|
32
|
+
| `renderOption` | `(option, state) => ReactNode` | | 목록 행을 직접 그릴 때. |
|
|
33
|
+
| `name` | `string` | | 폼 제출용. 값마다 같은 name의 hidden input이 생겨요. |
|
|
34
|
+
| `maxHeight` | `number` | `300` | 팝오버 최대 높이. |
|
|
35
|
+
|
|
36
|
+
## 예제
|
|
37
|
+
|
|
38
|
+
예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
|
|
39
|
+
|
|
40
|
+
### 기본
|
|
41
|
+
|
|
42
|
+
고른 항목은 태그로 쌓이고 ×로 바로 지울 수 있어요. Space/Enter로 토글, 목록이 닫혀 있을 때 Backspace는 마지막 항목을 지워요.
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { useState } from 'react'
|
|
46
|
+
import { MultiSelect, type ListOption } from '@iamfiro/design'
|
|
47
|
+
|
|
48
|
+
const LANGUAGES: ListOption[] = [
|
|
49
|
+
{ value: 'ko', label: '한국어' },
|
|
50
|
+
{ value: 'en', label: '영어' },
|
|
51
|
+
{ value: 'ja', label: '일본어' },
|
|
52
|
+
{ value: 'zh', label: '중국어' },
|
|
53
|
+
{ value: 'es', label: '스페인어' },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
export default function Example() {
|
|
57
|
+
const [languages, setLanguages] = useState<string[]>(['ko', 'en'])
|
|
58
|
+
return (
|
|
59
|
+
<div style={{ width: 320 }}>
|
|
60
|
+
<MultiSelect options={LANGUAGES} value={languages} onValueChange={setLanguages} placeholder="언어 선택" aria-label="지원 언어" />
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 전체 선택 · 그룹
|
|
67
|
+
|
|
68
|
+
selectAll은 목록 맨 위에 "전체 선택" 행을 붙여요. 열린 상태에서 Ctrl/⌘ + A로도 돼요. 비활성 항목은 전체 선택에서 빠져요.
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { useState } from 'react'
|
|
72
|
+
import { Field, MultiSelect, type ListOption } from '@iamfiro/design'
|
|
73
|
+
|
|
74
|
+
const CHANNELS: ListOption[] = [
|
|
75
|
+
{ value: 'email', label: '이메일', group: '알림 채널' },
|
|
76
|
+
{ value: 'push', label: '푸시', group: '알림 채널' },
|
|
77
|
+
{ value: 'sms', label: '문자', group: '알림 채널' },
|
|
78
|
+
{ value: 'slack', label: 'Slack', description: '워크스페이스 연결 필요', group: '연동', disabled: true },
|
|
79
|
+
{ value: 'webhook', label: '웹훅', group: '연동' },
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
export default function Example() {
|
|
83
|
+
const [channels, setChannels] = useState<string[]>(['email', 'push'])
|
|
84
|
+
return (
|
|
85
|
+
<div style={{ width: 320 }}>
|
|
86
|
+
<Field label="알림 받을 채널" htmlFor="notify-channels">
|
|
87
|
+
<MultiSelect id="notify-channels" options={CHANNELS} value={channels} onValueChange={setChannels} selectAll />
|
|
88
|
+
</Field>
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 개수로 요약
|
|
95
|
+
|
|
96
|
+
display='count'는 태그 대신 개수만 보여줘요. 문구는 formatCount로 바꿔요.
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { Avatar, Field, MultiSelect, type ListOption } from '@iamfiro/design'
|
|
100
|
+
|
|
101
|
+
const MEMBERS: ListOption[] = [
|
|
102
|
+
{ value: 'minji', label: '김민지', description: '디자인' },
|
|
103
|
+
{ value: 'dohyun', label: '이도현', description: '프론트엔드' },
|
|
104
|
+
{ value: 'seoyeon', label: '최서연', description: '백엔드' },
|
|
105
|
+
{ value: 'woojin', label: '정우진', description: '프로덕트' },
|
|
106
|
+
{ value: 'jisu', label: '한지수', description: '데이터' },
|
|
107
|
+
].map((member) => ({ ...member, icon: <Avatar name={member.label} size={22} /> }))
|
|
108
|
+
|
|
109
|
+
export default function Example() {
|
|
110
|
+
return (
|
|
111
|
+
<div style={{ width: 300 }}>
|
|
112
|
+
<Field label="담당자" htmlFor="assignees">
|
|
113
|
+
<MultiSelect
|
|
114
|
+
id="assignees"
|
|
115
|
+
options={MEMBERS}
|
|
116
|
+
defaultValue={['minji', 'jisu', 'woojin']}
|
|
117
|
+
display="count"
|
|
118
|
+
formatCount={(n) => `담당자 ${n}명`}
|
|
119
|
+
placeholder="담당자 선택"
|
|
120
|
+
/>
|
|
121
|
+
</Field>
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 태그 수 · 크기 · 상태
|
|
128
|
+
|
|
129
|
+
maxTags를 넘는 태그는 +N으로 접혀요. size, invalid, disabled는 Select와 같아요.
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import { Field, MultiSelect, type ListOption } from '@iamfiro/design'
|
|
133
|
+
|
|
134
|
+
const STATUSES: ListOption[] = [
|
|
135
|
+
{ value: 'todo', label: '할 일' },
|
|
136
|
+
{ value: 'doing', label: '진행 중' },
|
|
137
|
+
{ value: 'review', label: '검토 중' },
|
|
138
|
+
{ value: 'done', label: '완료' },
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
export default function Example() {
|
|
142
|
+
return (
|
|
143
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 260px)', gap: 16 }}>
|
|
144
|
+
<Field label="태그 1개까지 (maxTags)" htmlFor="status-max">
|
|
145
|
+
<MultiSelect id="status-max" options={STATUSES} maxTags={1} defaultValue={['todo', 'doing', 'review']} />
|
|
146
|
+
</Field>
|
|
147
|
+
<Field label="작게 (sm)" htmlFor="status-sm">
|
|
148
|
+
<MultiSelect id="status-sm" size="sm" options={STATUSES} defaultValue={['doing']} />
|
|
149
|
+
</Field>
|
|
150
|
+
<Field label="오류" invalid hint="상태를 하나 이상 골라 주세요." htmlFor="status-err">
|
|
151
|
+
<MultiSelect id="status-err" options={STATUSES} invalid />
|
|
152
|
+
</Field>
|
|
153
|
+
<Field label="비활성" htmlFor="status-off">
|
|
154
|
+
<MultiSelect id="status-off" options={STATUSES} defaultValue={['done']} disabled />
|
|
155
|
+
</Field>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
```
|