@iamfiro/design 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/AGENTS.md +81 -0
  2. package/README.md +15 -2
  3. package/agents/INDEX.md +156 -0
  4. package/agents/accordion/AGENTS.md +279 -0
  5. package/agents/action-bar/AGENTS.md +90 -0
  6. package/agents/alert/AGENTS.md +212 -0
  7. package/agents/alert-dialog/AGENTS.md +193 -0
  8. package/agents/app-shell/AGENTS.md +409 -0
  9. package/agents/area-chart/AGENTS.md +183 -0
  10. package/agents/aspect-ratio/AGENTS.md +143 -0
  11. package/agents/avatar/AGENTS.md +130 -0
  12. package/agents/badge/AGENTS.md +217 -0
  13. package/agents/bar-chart/AGENTS.md +386 -0
  14. package/agents/bar-list/AGENTS.md +223 -0
  15. package/agents/brand-icon/AGENTS.md +82 -0
  16. package/agents/breadcrumb/AGENTS.md +133 -0
  17. package/agents/button/AGENTS.md +195 -0
  18. package/agents/card/AGENTS.md +141 -0
  19. package/agents/card-frame/AGENTS.md +227 -0
  20. package/agents/chat-bubble/AGENTS.md +266 -0
  21. package/agents/chat-composer/AGENTS.md +251 -0
  22. package/agents/checkbox/AGENTS.md +113 -0
  23. package/agents/code-block/AGENTS.md +144 -0
  24. package/agents/color-dot/AGENTS.md +120 -0
  25. package/agents/color-picker/AGENTS.md +224 -0
  26. package/agents/colors/AGENTS.md +168 -0
  27. package/agents/combobox/AGENTS.md +218 -0
  28. package/agents/command-menu/AGENTS.md +169 -0
  29. package/agents/comment/AGENTS.md +206 -0
  30. package/agents/context-menu/AGENTS.md +303 -0
  31. package/agents/copy-button/AGENTS.md +84 -0
  32. package/agents/count-badge/AGENTS.md +115 -0
  33. package/agents/countdown/AGENTS.md +101 -0
  34. package/agents/data-grid/AGENTS.md +460 -0
  35. package/agents/date-picker/AGENTS.md +197 -0
  36. package/agents/delta/AGENTS.md +114 -0
  37. package/agents/description-list/AGENTS.md +251 -0
  38. package/agents/dialog/AGENTS.md +391 -0
  39. package/agents/drawer/AGENTS.md +471 -0
  40. package/agents/dropdown-menu/AGENTS.md +322 -0
  41. package/agents/dropzone/AGENTS.md +116 -0
  42. package/agents/elapsed/AGENTS.md +160 -0
  43. package/agents/empty-state/AGENTS.md +203 -0
  44. package/agents/file-chip/AGENTS.md +79 -0
  45. package/agents/file-icon/AGENTS.md +66 -0
  46. package/agents/file-upload/AGENTS.md +168 -0
  47. package/agents/filter-chips/AGENTS.md +167 -0
  48. package/agents/flag/AGENTS.md +67 -0
  49. package/agents/funnel-chart/AGENTS.md +131 -0
  50. package/agents/highlight/AGENTS.md +119 -0
  51. package/agents/hover-card/AGENTS.md +271 -0
  52. package/agents/icon-stack/AGENTS.md +120 -0
  53. package/agents/icon-tile/AGENTS.md +94 -0
  54. package/agents/inline-diff/AGENTS.md +211 -0
  55. package/agents/input/AGENTS.md +261 -0
  56. package/agents/kanban/AGENTS.md +352 -0
  57. package/agents/kbd/AGENTS.md +160 -0
  58. package/agents/layout/AGENTS.md +351 -0
  59. package/agents/line-chart/AGENTS.md +393 -0
  60. package/agents/link/AGENTS.md +53 -0
  61. package/agents/list/AGENTS.md +392 -0
  62. package/agents/marker/AGENTS.md +142 -0
  63. package/agents/masked-text/AGENTS.md +127 -0
  64. package/agents/menubar/AGENTS.md +276 -0
  65. package/agents/multi-select/AGENTS.md +159 -0
  66. package/agents/navigation-menu/AGENTS.md +315 -0
  67. package/agents/notice-stack/AGENTS.md +101 -0
  68. package/agents/number-stepper/AGENTS.md +96 -0
  69. package/agents/otp-input/AGENTS.md +162 -0
  70. package/agents/pagination/AGENTS.md +189 -0
  71. package/agents/phone-input/AGENTS.md +131 -0
  72. package/agents/pie-chart/AGENTS.md +243 -0
  73. package/agents/popover/AGENTS.md +190 -0
  74. package/agents/progress/AGENTS.md +273 -0
  75. package/agents/progress-tracker/AGENTS.md +305 -0
  76. package/agents/rating/AGENTS.md +165 -0
  77. package/agents/relative-time/AGENTS.md +103 -0
  78. package/agents/resizable/AGENTS.md +390 -0
  79. package/agents/scaled-preview/AGENTS.md +111 -0
  80. package/agents/scatter-chart/AGENTS.md +160 -0
  81. package/agents/scroll-area/AGENTS.md +271 -0
  82. package/agents/search-input/AGENTS.md +105 -0
  83. package/agents/segmented-control/AGENTS.md +153 -0
  84. package/agents/select/AGENTS.md +159 -0
  85. package/agents/selector-button/AGENTS.md +122 -0
  86. package/agents/sidebar/AGENTS.md +201 -0
  87. package/agents/skeleton/AGENTS.md +166 -0
  88. package/agents/slider/AGENTS.md +181 -0
  89. package/agents/spark-chart/AGENTS.md +257 -0
  90. package/agents/sparkbar/AGENTS.md +92 -0
  91. package/agents/spinner/AGENTS.md +123 -0
  92. package/agents/split-view/AGENTS.md +290 -0
  93. package/agents/stat/AGENTS.md +138 -0
  94. package/agents/status-icon/AGENTS.md +243 -0
  95. package/agents/step-list/AGENTS.md +313 -0
  96. package/agents/table/AGENTS.md +286 -0
  97. package/agents/tabs/AGENTS.md +305 -0
  98. package/agents/tag-input/AGENTS.md +246 -0
  99. package/agents/time-picker/AGENTS.md +228 -0
  100. package/agents/timeline/AGENTS.md +1018 -0
  101. package/agents/toast/AGENTS.md +266 -0
  102. package/agents/toggle-group/AGENTS.md +216 -0
  103. package/agents/toolbar/AGENTS.md +277 -0
  104. package/agents/tooltip/AGENTS.md +154 -0
  105. package/agents/tracker/AGENTS.md +158 -0
  106. package/agents/tree-diagram/AGENTS.md +260 -0
  107. package/agents/tree-view/AGENTS.md +329 -0
  108. package/agents/typography/AGENTS.md +396 -0
  109. package/lib/index.js +17914 -0
  110. package/lib/style.css +2 -0
  111. package/lib/types/components/Accordion/Accordion.d.ts +57 -0
  112. package/lib/types/components/ActionBar/ActionBar.d.ts +24 -0
  113. package/lib/types/components/Alert/Alert.d.ts +36 -0
  114. package/lib/types/components/AppShell/AppShell.d.ts +38 -0
  115. package/lib/types/components/AreaChart/AreaChart.d.ts +17 -0
  116. package/lib/types/components/BarChart/BarChart.d.ts +57 -0
  117. package/lib/types/components/BarList/BarList.d.ts +56 -0
  118. package/lib/types/components/Breadcrumb/Breadcrumb.d.ts +15 -0
  119. package/lib/types/components/Calendar/Calendar.d.ts +34 -0
  120. package/lib/types/components/Calendar/DatePicker.d.ts +45 -0
  121. package/lib/types/components/Calendar/date.d.ts +32 -0
  122. package/lib/types/components/Card/Card.d.ts +64 -0
  123. package/lib/types/components/CardFrame/CardFrame.d.ts +15 -0
  124. package/lib/types/components/Chart/ChartParts.d.ts +38 -0
  125. package/lib/types/components/Chart/chart.d.ts +46 -0
  126. package/lib/types/components/Chart/useChartInteraction.d.ts +39 -0
  127. package/lib/types/components/ChatBubble/ChatBubble.d.ts +61 -0
  128. package/lib/types/components/ChatBubble/formatChatTime.d.ts +2 -0
  129. package/lib/types/components/ChatComposer/ChatComposer.d.ts +42 -0
  130. package/lib/types/components/CodeBlock/CodeBlock.d.ts +18 -0
  131. package/lib/types/components/CodeBlock/highlight.d.ts +10 -0
  132. package/lib/types/components/ColorPicker/ColorPicker.d.ts +61 -0
  133. package/lib/types/components/ColorPicker/color.d.ts +48 -0
  134. package/lib/types/components/ColorPicker/presets.d.ts +7 -0
  135. package/lib/types/components/Combobox/Combobox.d.ts +48 -0
  136. package/lib/types/components/CommandMenu/CommandMenu.d.ts +51 -0
  137. package/lib/types/components/Comment/Comment.d.ts +45 -0
  138. package/lib/types/components/ContextMenu/ContextMenu.d.ts +40 -0
  139. package/lib/types/components/DataGrid/DataGrid.d.ts +43 -0
  140. package/lib/types/components/DataGrid/GridPopover.d.ts +43 -0
  141. package/lib/types/components/DataGrid/anchor.d.ts +5 -0
  142. package/lib/types/components/DataGrid/model.d.ts +61 -0
  143. package/lib/types/components/DataGrid/panels.d.ts +63 -0
  144. package/lib/types/components/DataGrid/typeMeta.d.ts +6 -0
  145. package/lib/types/components/DataGrid/types.d.ts +77 -0
  146. package/lib/types/components/DescriptionList/DescriptionList.d.ts +77 -0
  147. package/lib/types/components/Dialog/AlertDialog.d.ts +26 -0
  148. package/lib/types/components/Dialog/Dialog.d.ts +42 -0
  149. package/lib/types/components/Dialog/modal.d.ts +38 -0
  150. package/lib/types/components/Drawer/Drawer.d.ts +62 -0
  151. package/lib/types/components/DropdownMenu/DropdownMenu.d.ts +97 -0
  152. package/lib/types/components/DropdownMenu/menuCore.d.ts +23 -0
  153. package/lib/types/components/Dropzone/Dropzone.d.ts +30 -0
  154. package/lib/types/components/EmptyState/EmptyState.d.ts +26 -0
  155. package/lib/types/components/FileChip/FileChip.d.ts +32 -0
  156. package/lib/types/components/FileUpload/FileUpload.d.ts +52 -0
  157. package/lib/types/components/FilterChips/FilterChips.d.ts +37 -0
  158. package/lib/types/components/FunnelChart/FunnelChart.d.ts +38 -0
  159. package/lib/types/components/FunnelChart/funnel.d.ts +25 -0
  160. package/lib/types/components/Highlight/Highlight.d.ts +35 -0
  161. package/lib/types/components/HoverCard/HoverCard.d.ts +47 -0
  162. package/lib/types/components/InlineDiff/InlineDiff.d.ts +36 -0
  163. package/lib/types/components/InlineDiff/diff.d.ts +42 -0
  164. package/lib/types/components/InlineDiff/useDiffReview.d.ts +57 -0
  165. package/lib/types/components/Kanban/Kanban.d.ts +64 -0
  166. package/lib/types/components/Kanban/KanbanCard.d.ts +35 -0
  167. package/lib/types/components/Kanban/move.d.ts +25 -0
  168. package/lib/types/components/LineChart/LineChart.d.ts +85 -0
  169. package/lib/types/components/List/List.d.ts +62 -0
  170. package/lib/types/components/Listbox/Listbox.d.ts +60 -0
  171. package/lib/types/components/Marker/Marker.d.ts +28 -0
  172. package/lib/types/components/Menubar/Menubar.d.ts +50 -0
  173. package/lib/types/components/MultiSelect/MultiSelect.d.ts +38 -0
  174. package/lib/types/components/NavigationMenu/NavigationMenu.d.ts +101 -0
  175. package/lib/types/components/NoticeStack/NoticeStack.d.ts +41 -0
  176. package/lib/types/components/NumberStepper/NumberStepper.d.ts +32 -0
  177. package/lib/types/components/OtpInput/OtpInput.d.ts +31 -0
  178. package/lib/types/components/Pagination/Pagination.d.ts +54 -0
  179. package/lib/types/components/Pagination/paginationRange.d.ts +7 -0
  180. package/lib/types/components/PhoneInput/PhoneInput.d.ts +47 -0
  181. package/lib/types/components/PieChart/PieChart.d.ts +67 -0
  182. package/lib/types/components/PieChart/arc.d.ts +26 -0
  183. package/lib/types/components/PieChart/useArcTween.d.ts +15 -0
  184. package/lib/types/components/Popover/Popover.d.ts +28 -0
  185. package/lib/types/components/ProgressTracker/ProgressTracker.d.ts +64 -0
  186. package/lib/types/components/Rating/Rating.d.ts +42 -0
  187. package/lib/types/components/Resizable/Resizable.d.ts +50 -0
  188. package/lib/types/components/Resizable/layout.d.ts +31 -0
  189. package/lib/types/components/ScaledPreview/ScaledPreview.d.ts +38 -0
  190. package/lib/types/components/ScatterChart/ScatterChart.d.ts +81 -0
  191. package/lib/types/components/ScatterChart/scatter.d.ts +14 -0
  192. package/lib/types/components/ScrollArea/ScrollArea.d.ts +34 -0
  193. package/lib/types/components/Select/Select.d.ts +39 -0
  194. package/lib/types/components/Sidebar/Sidebar.d.ts +39 -0
  195. package/lib/types/components/Skeleton/Skeleton.d.ts +47 -0
  196. package/lib/types/components/Slider/Slider.d.ts +36 -0
  197. package/lib/types/components/SparkChart/SparkChart.d.ts +55 -0
  198. package/lib/types/components/SparkChart/spark.d.ts +16 -0
  199. package/lib/types/components/Sparkbar/Sparkbar.d.ts +39 -0
  200. package/lib/types/components/SplitView/SplitView.d.ts +31 -0
  201. package/lib/types/components/Stat/Stat.d.ts +58 -0
  202. package/lib/types/components/StepList/StepList.d.ts +61 -0
  203. package/lib/types/components/Table/Table.d.ts +39 -0
  204. package/lib/types/components/Tabs/Tabs.d.ts +47 -0
  205. package/lib/types/components/TagInput/TagInput.d.ts +46 -0
  206. package/lib/types/components/TimePicker/TimePicker.d.ts +43 -0
  207. package/lib/types/components/TimePicker/time.d.ts +15 -0
  208. package/lib/types/components/Timeline/Timeline.d.ts +154 -0
  209. package/lib/types/components/Timeline/scale.d.ts +22 -0
  210. package/lib/types/components/Toast/Toaster.d.ts +19 -0
  211. package/lib/types/components/Toast/index.d.ts +4 -0
  212. package/lib/types/components/Toast/store.d.ts +75 -0
  213. package/lib/types/components/ToggleGroup/ToggleGroup.d.ts +28 -0
  214. package/lib/types/components/Toolbar/Toolbar.d.ts +70 -0
  215. package/lib/types/components/Tracker/Tracker.d.ts +43 -0
  216. package/lib/types/components/TreeDiagram/TreeDiagram.d.ts +68 -0
  217. package/lib/types/components/TreeView/TreeView.d.ts +107 -0
  218. package/lib/types/components/index.d.ts +186 -0
  219. package/lib/types/hooks/index.d.ts +17 -0
  220. package/lib/types/hooks/timing.d.ts +29 -0
  221. package/lib/types/hooks/useControllableState.d.ts +5 -0
  222. package/lib/types/hooks/useCountdown.d.ts +19 -0
  223. package/lib/types/hooks/useDigitInput.d.ts +24 -0
  224. package/lib/types/hooks/useDismiss.d.ts +3 -0
  225. package/lib/types/hooks/useElapsed.d.ts +28 -0
  226. package/lib/types/hooks/useFloating.d.ts +21 -0
  227. package/lib/types/hooks/useFocusTrap.d.ts +7 -0
  228. package/lib/types/hooks/usePresence.d.ts +8 -0
  229. package/lib/types/hooks/usePressToOpen.d.ts +20 -0
  230. package/lib/types/hooks/useScrollLock.d.ts +2 -0
  231. package/lib/types/hooks/useSelection.d.ts +29 -0
  232. package/lib/types/index.d.ts +5 -0
  233. package/lib/types/primitives/AspectRatio.d.ts +14 -0
  234. package/lib/types/primitives/BrandIcon.d.ts +12 -0
  235. package/lib/types/primitives/ColorDot.d.ts +20 -0
  236. package/lib/types/primitives/CopyButton.d.ts +12 -0
  237. package/lib/types/primitives/CountBadge.d.ts +28 -0
  238. package/lib/types/primitives/Countdown.d.ts +21 -0
  239. package/lib/types/primitives/Delta.d.ts +33 -0
  240. package/lib/types/primitives/Elapsed.d.ts +23 -0
  241. package/lib/types/primitives/FileIcon.d.ts +17 -0
  242. package/lib/types/primitives/Flag.d.ts +15 -0
  243. package/lib/types/primitives/IconStack.d.ts +27 -0
  244. package/lib/types/primitives/IconTile.d.ts +24 -0
  245. package/lib/types/primitives/Kbd.d.ts +19 -0
  246. package/lib/types/primitives/Layout.d.ts +64 -0
  247. package/lib/types/primitives/Link.d.ts +12 -0
  248. package/lib/types/primitives/MaskedText.d.ts +28 -0
  249. package/lib/types/primitives/Progress.d.ts +52 -0
  250. package/lib/types/primitives/RelativeTime.d.ts +22 -0
  251. package/lib/types/primitives/SearchInput.d.ts +8 -0
  252. package/lib/types/primitives/SelectorButton.d.ts +23 -0
  253. package/lib/types/primitives/Spinner.d.ts +15 -0
  254. package/lib/types/primitives/StatusIcon.d.ts +40 -0
  255. package/lib/types/primitives/Swatch.d.ts +11 -0
  256. package/lib/types/primitives/Tooltip.d.ts +17 -0
  257. package/lib/types/primitives/Typography.d.ts +48 -0
  258. package/lib/types/primitives/formatCountdown.d.ts +2 -0
  259. package/lib/types/primitives/formatElapsed.d.ts +8 -0
  260. package/lib/types/primitives/formatRelativeTime.d.ts +16 -0
  261. package/lib/types/primitives/hotkey.d.ts +10 -0
  262. package/lib/types/primitives/index.d.ts +188 -0
  263. package/lib/types/primitives/maskValue.d.ts +24 -0
  264. package/package.json +54 -4
@@ -0,0 +1,169 @@
1
+ # CommandMenu
2
+
3
+ > 메뉴 & 오버레이 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
4
+
5
+ ⌘K(Windows는 Ctrl+K)로 어디서든 여는 명령 팔레트. 검색칸 아래에 섹션별 명령이 있고, 입력하면 이름 · 힌트 · 키워드로 바로 걸러져요. 화살표로 움직이고 Enter로 실행, Tab은 다음 섹션의 첫 명령으로 건너뛰어요. 명령마다 단축키를 달면 메뉴가 열린 상태에서 바로 실행돼요. Dialog(bare, top) · Input(bare) · Listbox · Kbd로 조립했어요.
6
+
7
+ ```tsx
8
+ import { CommandMenu } from '@iamfiro/design'
9
+ ```
10
+
11
+ ## API
12
+
13
+ ### CommandMenu
14
+
15
+ | prop | 타입 | 기본값 | 설명 |
16
+ | --- | --- | --- | --- |
17
+ | `groups` (필수) | `CommandGroup[]` | | 섹션 목록. { heading, items } |
18
+ | `open / defaultOpen / onOpenChange` | `boolean / boolean / (open) => void` | | 제어 · 비제어 열림 상태. |
19
+ | `trigger` | `ReactElement` | | 누르면 메뉴를 여는 버튼. |
20
+ | `onSelect` | `(item: CommandItem) => void` | | 명령이 실행될 때마다(항목의 onSelect 다음에) 불려요. |
21
+ | `shortcutKey` | `string \| null` | `'k'` | ⌘/Ctrl과 함께 눌러 어디서든 열고 닫는 글자. null이면 꺼요. |
22
+ | `placeholder` | `string` | `'명령어 또는 검색어를 입력하세요'` | 검색칸 안내 문구. |
23
+ | `filter` | `(item, query) => boolean` | `defaultCommandFilter` | 걸러내기 규칙. 기본은 label · 문자열 hint · keywords에 포함되는지. |
24
+ | `emptyText` | `(query) => ReactNode` | | 결과가 없을 때 문구. |
25
+ | `footer` | `ReactNode \| false` | | 하단 안내 줄. 기본은 Tab 섹션 이동 · ⌘K 열기 · 닫기. false면 숨겨요. |
26
+ | `closeOnSelect` | `boolean` | `true` | 실행 후 닫기. |
27
+ | `title` | `string` | `'명령 메뉴'` | 화면에는 안 보이는 대화상자 이름(스크린 리더용). |
28
+
29
+ ### CommandItem
30
+
31
+ | prop | 타입 | 기본값 | 설명 |
32
+ | --- | --- | --- | --- |
33
+ | `id` (필수) | `string` | | 고유 값. |
34
+ | `label` (필수) | `string` | | 명령 이름. |
35
+ | `hint` | `ReactNode` | | 이름 뒤 회색 글자(앱, 채널, 경로). 문자열이면 검색에도 쓰여요. |
36
+ | `icon` | `ReactNode` | | 앞 타일. IconTile에 아이콘이나 BrandIcon을 담아요. |
37
+ | `shortcut` | `string[]` | | 메뉴가 열려 있을 때 바로 실행하는 키. 예: ['mod', '1'] |
38
+ | `keywords` | `string[]` | | 추가 검색어(별칭, 영어 이름). |
39
+ | `disabled` | `boolean` | | 보이지만 고를 수 없어요. 키보드 이동도 건너뛰어요. |
40
+ | `onSelect` | `() => void` | | 실행할 동작. |
41
+
42
+ ## 예제
43
+
44
+ 예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
45
+
46
+ ### 기본
47
+
48
+ 버튼이나 ⌘K로 열어요. 앱 로고는 흰 IconTile(tone="surface")에 BrandIcon을 담고, 오른쪽에는 단축키 키캡이 붙어요. 열린 상태에서 ⌘1 … ⌘5를 누르면 해당 명령이 바로 실행돼요. "메일", "slack"처럼 키워드로도 찾아져요.
49
+
50
+ ```tsx
51
+ import { CalendarPlus, FilePlus, FolderOpen, Settings, Sparkles } from 'lucide-react'
52
+ import { BrandIcon, Button, IconTile, Inline, Kbd, Text, CommandMenu, toast, type CommandGroup, type CommandItem } from '@iamfiro/design'
53
+
54
+ const logo = (name: Parameters<typeof BrandIcon>[0]['name']) => (
55
+ <IconTile tone="surface" size={26}>
56
+ <BrandIcon name={name} size={15} />
57
+ </IconTile>
58
+ )
59
+
60
+ const sections: { heading: string; items: Omit<CommandItem, 'onSelect'>[] }[] = [
61
+ {
62
+ heading: '추천',
63
+ items: [
64
+ { id: 'email', label: '이메일 쓰기', hint: 'Gmail', icon: logo('gmail'), shortcut: ['mod', '1'], keywords: ['메일', 'email'] },
65
+ {
66
+ id: 'assistant',
67
+ label: '어시스턴트에게 묻기',
68
+ hint: '요약, 초안, 답변',
69
+ icon: (
70
+ <IconTile tone="surface" size={26}>
71
+ <Sparkles color="var(--accent)" />
72
+ </IconTile>
73
+ ),
74
+ shortcut: ['mod', '2'],
75
+ keywords: ['ai'],
76
+ },
77
+ { id: 'task', label: '할 일 만들기', hint: 'ClickUp', icon: logo('clickup'), shortcut: ['mod', '3'], keywords: ['task', '태스크'] },
78
+ { id: 'crm', label: 'CRM에 연락처 추가', hint: 'HubSpot', icon: logo('hubspot'), shortcut: ['mod', '4'], keywords: ['contact'] },
79
+ { id: 'meeting', label: '회의 예약', hint: 'Google Calendar', icon: logo('google-calendar'), shortcut: ['mod', '5'], keywords: ['일정', 'meeting'] },
80
+ ],
81
+ },
82
+ {
83
+ heading: '공유',
84
+ items: [
85
+ { id: 'post', label: '업데이트 올리기', hint: '#team-updates', icon: logo('slack'), keywords: ['slack'] },
86
+ { id: 'page', label: '페이지로 저장', hint: 'Notion · 회의록', icon: logo('notion'), keywords: ['notion'] },
87
+ ],
88
+ },
89
+ {
90
+ heading: '일반',
91
+ items: [
92
+ { id: 'doc', label: '새 문서', icon: <IconTile size={26}><FilePlus /></IconTile>, shortcut: ['n'] },
93
+ { id: 'open', label: '최근 파일 열기', icon: <IconTile size={26}><FolderOpen /></IconTile> },
94
+ { id: 'event', label: '일정 추가', icon: <IconTile size={26}><CalendarPlus /></IconTile> },
95
+ { id: 'settings', label: '설정', icon: <IconTile size={26}><Settings /></IconTile>, keywords: ['settings', '환경설정'] },
96
+ ],
97
+ },
98
+ ]
99
+
100
+ const groups: CommandGroup[] = sections.map((g) => ({
101
+ ...g,
102
+ items: g.items.map((it) => ({ ...it, onSelect: () => toast.success(`'${it.label}' 실행`) })),
103
+ }))
104
+
105
+ export default function Example() {
106
+ return (
107
+ <Inline gap={3}>
108
+ <CommandMenu groups={groups} trigger={<Button>명령 메뉴 열기</Button>} />
109
+ <Inline gap={2}>
110
+ <Text variant="body-sm" tone="tertiary">
111
+ 또는
112
+ </Text>
113
+ <Kbd keys={['mod', 'k']} />
114
+ </Inline>
115
+ </Inline>
116
+ )
117
+ }
118
+ ```
119
+
120
+ ### 제어 · 비활성 · 하단 숨김
121
+
122
+ open과 onOpenChange로 바깥에서 열고, onSelect로 실행된 명령을 받아요. 한 화면에 메뉴가 여럿이면 shortcutKey={null}로 전역 단축키를 하나만 남겨요. 비활성 명령은 키보드 이동에서 건너뛰고, footer={false}는 하단 안내 줄을 숨겨요.
123
+
124
+ ```tsx
125
+ import { useState } from 'react'
126
+ import { Archive, Copy, Link, Pencil, Trash2 } from 'lucide-react'
127
+ import { Button, IconTile, Stack, Text, CommandMenu, type CommandGroup } from '@iamfiro/design'
128
+
129
+ const groups: CommandGroup[] = [
130
+ {
131
+ heading: '문서',
132
+ items: [
133
+ { id: 'rename', label: '이름 바꾸기', icon: <IconTile size={26}><Pencil /></IconTile>, keywords: ['rename'] },
134
+ { id: 'duplicate', label: '복제', icon: <IconTile size={26}><Copy /></IconTile>, keywords: ['copy'] },
135
+ { id: 'link', label: '링크 복사', hint: 'docs.lumen.app/…', icon: <IconTile size={26}><Link /></IconTile> },
136
+ ],
137
+ },
138
+ {
139
+ heading: '정리',
140
+ items: [
141
+ { id: 'archive', label: '보관', icon: <IconTile size={26}><Archive /></IconTile> },
142
+ { id: 'delete', label: '휴지통으로 이동', hint: '편집 권한 필요', icon: <IconTile size={26}><Trash2 /></IconTile>, disabled: true },
143
+ ],
144
+ },
145
+ ]
146
+
147
+ export default function Example() {
148
+ const [open, setOpen] = useState(false)
149
+ const [last, setLast] = useState<string | null>(null)
150
+
151
+ return (
152
+ <Stack gap={3} align="start">
153
+ <Button onClick={() => setOpen(true)}>문서 작업</Button>
154
+ <Text variant="body-sm" tone="tertiary">
155
+ 마지막 실행: {last ?? '없음'}
156
+ </Text>
157
+ <CommandMenu
158
+ open={open}
159
+ onOpenChange={setOpen}
160
+ groups={groups}
161
+ onSelect={(it) => setLast(it.label)}
162
+ shortcutKey={null}
163
+ footer={false}
164
+ placeholder="이 문서에서 할 작업"
165
+ />
166
+ </Stack>
167
+ )
168
+ }
169
+ ```
@@ -0,0 +1,206 @@
1
+ # Comment
2
+
3
+ > 대화 & 활동 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
4
+
5
+ 댓글 · 토론의 메시지 한 개. 아바타, 굵은 이름, 오른쪽의 상대 시간(RelativeTime), 본문, 마우스를 올리면 나타나는 액션으로 이루어져요. CommentThread로 묶으면 선 없이 여백으로 구분되고, 같은 사람이 이어 쓴 메시지(continued)는 더 가깝게 붙어 한 묶음으로 읽혀요.
6
+
7
+ ```tsx
8
+ import { Comment, CommentThread } from '@iamfiro/design'
9
+ ```
10
+
11
+ ## 디자인 규칙
12
+
13
+ - 메시지·댓글은 `Comment` + `CommentThread`(선 없이 여백, 같은 사람의 연속 메시지는 `continued`로 가깝게). 상대 시간은 직접 만들지 말고 `RelativeTime` / `formatRelativeTime`.
14
+
15
+ ## API
16
+
17
+ ### Comment
18
+
19
+ | prop | 타입 | 기본값 | 설명 |
20
+ | --- | --- | --- | --- |
21
+ | `author` (필수) | `ReactNode` | | 작성자. 문자열이면 기본 Avatar도 그려요. |
22
+ | `children` | `ReactNode` | | 본문. Text variant="code" 같은 인라인 요소를 섞을 수 있어요. |
23
+ | `date` | `Date \| string \| number` | | 작성 시각. RelativeTime으로 "2분 전"처럼 보이고, 올리면 전체 날짜가 나와요. |
24
+ | `avatar` | `ReactNode` | | 기본 아바타 대신 쓸 요소. |
25
+ | `meta` | `ReactNode` | | 이름 뒤 작은 요소(배지, "수정됨"). |
26
+ | `actions` | `ReactNode` | | 오른쪽 버튼들. hover · focus 때 나타나요. |
27
+ | `footer` | `ReactNode` | | 본문 아래(첨부, 상태 줄, 반응). |
28
+ | `continued` | `boolean` | | 같은 사람의 이어 쓴 메시지. 아바타 · 이름 없이 본문만, 위 메시지에 가깝게. |
29
+ | `avatarSize` | `number` | `28` | 아바타 크기(px). 본문 들여쓰기도 따라가요. |
30
+ | `className / style` | `string / CSSProperties` | | 바깥 요소에 전달. |
31
+
32
+ ### CommentThread
33
+
34
+ Comment.Thread로도 쓸 수 있어요.
35
+
36
+ | prop | 타입 | 기본값 | 설명 |
37
+ | --- | --- | --- | --- |
38
+ | `children` (필수) | `ReactNode` | | Comment들(오래된 것부터). |
39
+ | `aria-label` | `string` | | 주면 role="group"과 함께 붙어요. |
40
+ | `className / style` | `string / CSSProperties` | | 바깥 요소에 전달. |
41
+
42
+ ## 예제
43
+
44
+ 예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
45
+
46
+ ### 댓글 카드
47
+
48
+ Card + CardHeader 안에 CommentThread와 ChatComposer size="sm"을 조립했어요. 본문 앞에는 Text variant="code" 칩, footer에는 Collapsible variant="inline" 상태 줄을 넣었어요. 답글을 보내면 아래에 추가돼요.
49
+
50
+ ```tsx
51
+ import { useState } from 'react'
52
+ import { CircleCheck, X } from 'lucide-react'
53
+ import { Button, Stack, Text, Card, CardBody, CardHeader, ChatComposer, Collapsible, Comment, CommentThread } from '@iamfiro/design'
54
+
55
+ const minutesAgo = (m: number) => new Date(Date.now() - m * 60_000)
56
+ const ASKED = minutesAgo(12)
57
+ const ANSWERED = minutesAgo(10)
58
+
59
+ interface Message {
60
+ id: string
61
+ author: string
62
+ date: Date
63
+ text: string
64
+ }
65
+
66
+ export default function Example() {
67
+ const [messages, setMessages] = useState<Message[]>([])
68
+
69
+ return (
70
+ <Card padding="sm" style={{ width: 380 }}>
71
+ <CardHeader
72
+ title={
73
+ <Text as="span" variant="label-md">
74
+ 댓글{' '}
75
+ <Text as="span" variant="label-md" tone="tertiary">
76
+ 메시지 {messages.length + 2}개
77
+ </Text>
78
+ </Text>
79
+ }
80
+ actions={<Button variant="ghost" size="sm" iconOnly icon={<X />} aria-label="댓글 닫기" />}
81
+ />
82
+ <CardBody>
83
+ <Stack gap={4}>
84
+ <CommentThread aria-label="댓글">
85
+ <Comment author="이도현" date={ASKED}>
86
+ <Text variant="code">/수정</Text> 소개 문단을 더 짧고 분명하게 다듬어 주세요.
87
+ </Comment>
88
+ <Comment
89
+ author="김민지"
90
+ date={ANSWERED}
91
+ footer={
92
+ <Collapsible variant="inline" icon={<CircleCheck />} iconTone="success" label="수정 완료">
93
+ 소개 문단 · 3문장 → 2문장, 128자 줄임
94
+ </Collapsible>
95
+ }
96
+ >
97
+ 다듬었어요. 확인해 주세요.
98
+ </Comment>
99
+ {messages.map((m) => (
100
+ <Comment key={m.id} author={m.author} date={m.date}>
101
+ {m.text}
102
+ </Comment>
103
+ ))}
104
+ </CommentThread>
105
+ <ChatComposer
106
+ size="sm"
107
+ placeholder="답글을 쓰거나 @로 멤버를 불러 보세요"
108
+ aria-label="답글"
109
+ onSubmit={(text) => setMessages((prev) => [...prev, { id: String(Date.now()), author: '이도현', date: new Date(), text }])}
110
+ />
111
+ </Stack>
112
+ </CardBody>
113
+ </Card>
114
+ )
115
+ }
116
+ ```
117
+
118
+ ### 이어 쓴 메시지 묶기
119
+
120
+ 같은 사람이 5분 안에 이어 쓰면 continued로 아바타와 이름을 생략해요. 본문은 위 메시지와 같은 선에 맞춰져요.
121
+
122
+ ```tsx
123
+ import { Badge, Comment, CommentThread } from '@iamfiro/design'
124
+
125
+ const at = (minutesAgo: number) => new Date(Date.now() - minutesAgo * 60_000)
126
+
127
+ const MESSAGES = [
128
+ { id: '1', author: '김민지', date: at(64), text: '디자인 시안 2안 올렸어요.' },
129
+ { id: '2', author: '김민지', date: at(63), text: '색은 아직 확정 전이라 회색으로 두었어요.' },
130
+ { id: '3', author: '김민지', date: at(61), text: '내일 오전까지 의견 주시면 반영할게요.' },
131
+ { id: '4', author: '이도현', date: at(25), text: '2안이 좋아요. 버튼 문구만 "저장"으로 바꿔 주세요.' },
132
+ { id: '5', author: '박서연', date: at(3), text: '저도 2안이요!' },
133
+ ]
134
+
135
+ /** Same author within 5 minutes of their previous message → shown as a follow-up. */
136
+ const isContinued = (i: number) => {
137
+ const prev = MESSAGES[i - 1]
138
+ const cur = MESSAGES[i]
139
+ return !!prev && prev.author === cur.author && cur.date.getTime() - prev.date.getTime() < 5 * 60_000
140
+ }
141
+
142
+ export default function Example() {
143
+ return (
144
+ <CommentThread aria-label="토론" style={{ width: 400 }}>
145
+ {MESSAGES.map((m, i) => (
146
+ <Comment key={m.id} author={m.author} date={m.date} continued={isContinued(i)} meta={m.author === '김민지' ? <Badge>작성자</Badge> : undefined}>
147
+ {m.text}
148
+ </Comment>
149
+ ))}
150
+ </CommentThread>
151
+ )
152
+ }
153
+ ```
154
+
155
+ ### 액션 · 메타
156
+
157
+ actions는 마우스를 올리거나 포커스가 들어오면 나타나요(터치 기기에선 항상). meta 자리엔 배지나 "수정됨"을 넣어요.
158
+
159
+ ```tsx
160
+ import { useState } from 'react'
161
+ import { MoreHorizontal, Reply, SmilePlus } from 'lucide-react'
162
+ import { Button, Text, Comment, CommentThread, DropdownMenu, DropdownMenuItem } from '@iamfiro/design'
163
+
164
+ const POSTED = new Date(Date.now() - 2 * 24 * 60 * 60_000)
165
+ const EDITED = new Date(Date.now() - 90_000)
166
+
167
+ export default function Example() {
168
+ const [replies, setReplies] = useState(0)
169
+ const actions = (
170
+ <>
171
+ <Button variant="ghost" size="sm" iconOnly icon={<SmilePlus />} aria-label="반응 남기기" />
172
+ <Button variant="ghost" size="sm" iconOnly icon={<Reply />} aria-label="답글" onClick={() => setReplies((n) => n + 1)} />
173
+ <DropdownMenu align="end" trigger={<Button variant="ghost" size="sm" iconOnly icon={<MoreHorizontal />} aria-label="더 보기" />}>
174
+ <DropdownMenuItem>링크 복사</DropdownMenuItem>
175
+ <DropdownMenuItem>수정</DropdownMenuItem>
176
+ </DropdownMenu>
177
+ </>
178
+ )
179
+ return (
180
+ <CommentThread style={{ width: 420 }}>
181
+ <Comment author="이도현" date={POSTED} actions={actions}>
182
+ 3분기 회고 문서 초안이에요. 각자 맡은 부분에 의견 남겨 주세요.
183
+ </Comment>
184
+ <Comment
185
+ author="김민지"
186
+ date={EDITED}
187
+ meta={
188
+ <Text variant="caption" tone="tertiary">
189
+ 수정됨
190
+ </Text>
191
+ }
192
+ actions={actions}
193
+ footer={
194
+ replies > 0 ? (
195
+ <Text variant="caption" tone="accent">
196
+ 답글 {replies}개
197
+ </Text>
198
+ ) : undefined
199
+ }
200
+ >
201
+ 목표 대비 결과 표는 제가 채울게요.
202
+ </Comment>
203
+ </CommentThread>
204
+ )
205
+ }
206
+ ```
@@ -0,0 +1,303 @@
1
+ # ContextMenu
2
+
3
+ > 메뉴 & 오버레이 · `@iamfiro/design` · 직접 만들지 말고 이 컴포넌트를 쓴다.
4
+
5
+ 마우스 오른쪽 버튼을 누른 자리에서 열리는 메뉴. 터치에서는 손가락을 움직이지 않고 500ms(TIMING.longPress) 누르고 있으면 열리고, 키보드는 영역에 포커스한 뒤 Shift+F10 또는 메뉴 키로 포커스한 요소 아래에 열어요. 항목 · 단축키 · 체크/라디오 · 하위 메뉴 · 구분선은 DropdownMenu와 같은 조각을 그대로 써요. 화면 가장자리에 가까우면 왼쪽 · 위로 뒤집히고 화면 안으로 밀려 들어와요. Esc · 바깥 클릭 · 항목 실행으로 닫히고(닫힐 때도 애니메이션), 포커스는 열기 전 자리로 돌아가요. 스크롤하거나 창 크기가 바뀌면 누른 자리가 어긋나므로 닫혀요.
6
+
7
+ ```tsx
8
+ import { ContextMenu, ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuSub } from '@iamfiro/design'
9
+ ```
10
+
11
+ ## API
12
+
13
+ ### ContextMenu
14
+
15
+ 항목은 DropdownMenu와 같아요: ContextMenuItem(= DropdownMenuItem) · ContextMenuCheckboxItem · ContextMenuRadioGroup · ContextMenuRadioItem · ContextMenuLabel · ContextMenuSeparator · ContextMenuSub. props는 DropdownMenu 문서를 보세요.
16
+
17
+ | prop | 타입 | 기본값 | 설명 |
18
+ | --- | --- | --- | --- |
19
+ | `trigger` (필수) | `ReactNode` | | 오른쪽 클릭 · 길게 누르기 · Shift+F10을 받는 영역. 감싸는 요소는 display: contents라 레이아웃에 끼어들지 않아요. 키보드로 열려면 포커스할 수 있어야 해요(tabIndex 또는 버튼). |
20
+ | `children` (필수) | `ReactNode` | | 항목, 라벨, 구분선, 하위 메뉴. |
21
+ | `disabled` | `boolean` | | 가로채지 않고 브라우저 기본 메뉴를 그대로 둬요. |
22
+ | `minWidth` | `number` | `200` | 패널 최소 폭(px). |
23
+ | `open / onOpenChange` | `boolean / (open) => void` | | 열림 상태를 밖에서 제어할 때. |
24
+ | `defaultOpen` | `boolean` | `false` | 처음 열림 상태(비제어). |
25
+ | `aria-label` | `string` | | 메뉴(role="menu")의 접근성 이름. |
26
+
27
+ ## 예제
28
+
29
+ 예제를 감싼 `style={{ ... }}` div는 미리보기 배치용이다. 실제 화면 배치는 `Stack` · `Inline` · `Grid`로 한다.
30
+
31
+ ### 파일 목록
32
+
33
+ 항목마다 ContextMenu로 감싸면 그 항목에 대한 메뉴가 돼요. 휴지통 · 이동처럼 되돌릴 수 있는 동작은 바로 실행하고 toast.undo()로 실행 취소를 줘요. 하위 메뉴는 DropdownMenu와 같이 잠깐 머물면 열리고, 대각선으로 이동하는 동안은 지나는 행이 강조를 빼앗지 않아요.
34
+
35
+ ```tsx
36
+ import { useState } from 'react'
37
+ import { Copy, Download, ExternalLink, Folder, FolderInput, Link2, Mail, Pencil, Share2, Star, Trash2 } from 'lucide-react'
38
+ import { FileIcon, Grid, Stack, Text, Card, CardBody, ContextMenu, ContextMenuItem, ContextMenuSeparator, ContextMenuSub, toast } from '@iamfiro/design'
39
+
40
+ interface FileRow {
41
+ id: string
42
+ name: string
43
+ meta: string
44
+ folder: string
45
+ }
46
+
47
+ const INITIAL: FileRow[] = [
48
+ { id: '1', name: '3분기 이사회 보고서.pdf', meta: '2.4MB · 10월 25일', folder: '회의록' },
49
+ { id: '2', name: '브랜드 가이드.fig', meta: '18MB · 10월 21일', folder: '디자인' },
50
+ { id: '3', name: '고객 인터뷰.mp4', meta: '312MB · 10월 18일', folder: '마케팅' },
51
+ { id: '4', name: '예산안_v3.xlsx', meta: '86KB · 10월 12일', folder: '회의록' },
52
+ { id: '5', name: '출시 일정.docx', meta: '41KB · 10월 9일', folder: '마케팅' },
53
+ { id: '6', name: '로고 원본.png', meta: '1.2MB · 9월 30일', folder: '디자인' },
54
+ ]
55
+ const FOLDERS = ['디자인', '마케팅', '회의록', '보관함']
56
+
57
+ export default function Example() {
58
+ const [files, setFiles] = useState(INITIAL)
59
+ const [starred, setStarred] = useState<string[]>(['2'])
60
+
61
+ const remove = (file: FileRow) => {
62
+ const before = files
63
+ setFiles((list) => list.filter((f) => f.id !== file.id))
64
+ toast.undo(`‘${file.name}’을 휴지통으로 옮겼어요`, { onUndo: () => setFiles(before) })
65
+ }
66
+ const move = (file: FileRow, to: string) => {
67
+ const from = file.folder
68
+ setFiles((list) => list.map((f) => (f.id === file.id ? { ...f, folder: to } : f)))
69
+ toast.undo(`‘${to}’ 폴더로 옮겼어요`, {
70
+ onUndo: () => setFiles((list) => list.map((f) => (f.id === file.id ? { ...f, folder: from } : f))),
71
+ })
72
+ }
73
+
74
+ return (
75
+ <Stack gap={3} style={{ width: '100%', maxWidth: 560 }}>
76
+ <Grid columns={3} gap={3}>
77
+ {files.map((file) => {
78
+ const isStarred = starred.includes(file.id)
79
+ return (
80
+ <ContextMenu
81
+ key={file.id}
82
+ aria-label={`${file.name} 메뉴`}
83
+ trigger={
84
+ <Card
85
+ as="button"
86
+ type="button"
87
+ padding="sm"
88
+ interactive
89
+ style={{ textAlign: 'left' }}
90
+ onDoubleClick={() => toast(`‘${file.name}’을 열었어요`)}
91
+ >
92
+ <CardBody>
93
+ <Stack gap={2} align="start">
94
+ <FileIcon name={file.name} size={28} />
95
+ <Stack gap={0} align="start" style={{ minWidth: 0, width: '100%' }}>
96
+ <Text variant="label-sm" truncate style={{ maxWidth: '100%' }}>
97
+ {file.name}
98
+ </Text>
99
+ <Text variant="caption" tone="tertiary">
100
+ {file.folder} · {file.meta}
101
+ </Text>
102
+ </Stack>
103
+ </Stack>
104
+ </CardBody>
105
+ </Card>
106
+ }
107
+ >
108
+ <ContextMenuItem icon={<ExternalLink />} shortcut={['enter']} onSelect={() => toast(`‘${file.name}’을 열었어요`)}>
109
+ 열기
110
+ </ContextMenuItem>
111
+ <ContextMenuItem icon={<Pencil />} shortcut={['F2']} onSelect={() => toast('이름 바꾸기를 열었어요')}>
112
+ 이름 바꾸기
113
+ </ContextMenuItem>
114
+ <ContextMenuItem icon={<Copy />} shortcut={['mod', 'D']} onSelect={() => toast.success('사본을 만들었어요')}>
115
+ 사본 만들기
116
+ </ContextMenuItem>
117
+ <ContextMenuItem
118
+ icon={<Star />}
119
+ onSelect={() => setStarred((ids) => (isStarred ? ids.filter((x) => x !== file.id) : [...ids, file.id]))}
120
+ >
121
+ {isStarred ? '별표 해제' : '별표 표시'}
122
+ </ContextMenuItem>
123
+ <ContextMenuSeparator />
124
+ <ContextMenuSub label="이동" icon={<FolderInput />}>
125
+ {FOLDERS.map((f) => (
126
+ <ContextMenuItem
127
+ key={f}
128
+ icon={<Folder />}
129
+ disabled={f === file.folder}
130
+ description={f === file.folder ? '현재 폴더' : undefined}
131
+ onSelect={() => move(file, f)}
132
+ >
133
+ {f}
134
+ </ContextMenuItem>
135
+ ))}
136
+ </ContextMenuSub>
137
+ <ContextMenuSub label="공유" icon={<Share2 />}>
138
+ <ContextMenuItem icon={<Link2 />} onSelect={() => toast.success('링크를 복사했어요')}>
139
+ 링크 복사
140
+ </ContextMenuItem>
141
+ <ContextMenuItem icon={<Mail />} onSelect={() => toast('메일 작성 창을 열었어요')}>
142
+ 이메일로 보내기
143
+ </ContextMenuItem>
144
+ </ContextMenuSub>
145
+ <ContextMenuItem icon={<Download />} onSelect={() => toast('다운로드를 시작했어요')}>
146
+ 다운로드
147
+ </ContextMenuItem>
148
+ <ContextMenuSeparator />
149
+ <ContextMenuItem icon={<Trash2 />} danger shortcut={['delete']} onSelect={() => remove(file)}>
150
+ 휴지통으로 이동
151
+ </ContextMenuItem>
152
+ </ContextMenu>
153
+ )
154
+ })}
155
+ </Grid>
156
+ <Text variant="caption" tone="tertiary">
157
+ 파일을 마우스 오른쪽 버튼으로 클릭하거나 길게 누르세요. 별표 {starred.length}개
158
+ </Text>
159
+ </Stack>
160
+ )
161
+ }
162
+ ```
163
+
164
+ ### 캔버스 · 보기 옵션
165
+
166
+ 빈 영역의 메뉴. 체크 항목과 라디오 그룹은 고른 뒤에도 메뉴가 열려 있어요. 영역에 tabIndex를 주면 키보드로 포커스한 뒤 Shift+F10으로 열 수 있고, 이때는 첫 항목이 바로 강조돼요. 오른쪽 아래 구석에서 열어 보면 패널이 뒤집혀요.
167
+
168
+ ```tsx
169
+ import { useState } from 'react'
170
+ import { ClipboardPaste, Plus, Square, StickyNote, Type } from 'lucide-react'
171
+ import { Box, Inline, Kbd, Stack, Text, ContextMenu, ContextMenuCheckboxItem, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuSub, toast } from '@iamfiro/design'
172
+
173
+ export default function Example() {
174
+ const [ruler, setRuler] = useState(true)
175
+ const [snap, setSnap] = useState(false)
176
+ const [zoom, setZoom] = useState('100')
177
+ const [shapes, setShapes] = useState(0)
178
+
179
+ const add = (name: string) => {
180
+ setShapes((n) => n + 1)
181
+ toast(`${name}을 추가했어요`)
182
+ }
183
+
184
+ return (
185
+ <Stack gap={3} style={{ width: '100%', maxWidth: 520 }}>
186
+ <ContextMenu
187
+ aria-label="캔버스 메뉴"
188
+ trigger={
189
+ <Box
190
+ tabIndex={0}
191
+ background="subtle"
192
+ radius="lg"
193
+ padding={6}
194
+ style={{ height: 220, display: 'grid', placeItems: 'center' }}
195
+ aria-label="캔버스"
196
+ >
197
+ <Stack gap={2} align="center">
198
+ <Text variant="label-md" tone="secondary">
199
+ 빈 곳을 마우스 오른쪽 버튼으로 클릭하세요
200
+ </Text>
201
+ <Inline gap={1} align="center">
202
+ <Text variant="caption" tone="tertiary">
203
+ 키보드는 포커스한 뒤
204
+ </Text>
205
+ <Kbd keys={['shift', 'F10']} size="sm" />
206
+ </Inline>
207
+ </Stack>
208
+ </Box>
209
+ }
210
+ >
211
+ <ContextMenuItem icon={<ClipboardPaste />} shortcut={['mod', 'V']} disabled description="클립보드가 비어 있어요">
212
+ 붙여넣기
213
+ </ContextMenuItem>
214
+ <ContextMenuSub label="새로 만들기" icon={<Plus />}>
215
+ <ContextMenuItem icon={<Square />} shortcut={['R']} onSelect={() => add('사각형')}>
216
+ 사각형
217
+ </ContextMenuItem>
218
+ <ContextMenuItem icon={<Type />} shortcut={['T']} onSelect={() => add('텍스트')}>
219
+ 텍스트
220
+ </ContextMenuItem>
221
+ <ContextMenuItem icon={<StickyNote />} shortcut={['N']} onSelect={() => add('메모')}>
222
+ 메모
223
+ </ContextMenuItem>
224
+ </ContextMenuSub>
225
+ <ContextMenuSeparator />
226
+ <ContextMenuLabel>보기</ContextMenuLabel>
227
+ <ContextMenuCheckboxItem checked={ruler} onCheckedChange={setRuler} shortcut={['shift', 'R']}>
228
+ 눈금자 표시
229
+ </ContextMenuCheckboxItem>
230
+ <ContextMenuCheckboxItem checked={snap} onCheckedChange={setSnap}>
231
+ 격자에 맞추기
232
+ </ContextMenuCheckboxItem>
233
+ <ContextMenuSeparator />
234
+ <ContextMenuRadioGroup label="확대" value={zoom} onValueChange={setZoom}>
235
+ <ContextMenuRadioItem value="50">50%</ContextMenuRadioItem>
236
+ <ContextMenuRadioItem value="100">100%</ContextMenuRadioItem>
237
+ <ContextMenuRadioItem value="200">200%</ContextMenuRadioItem>
238
+ </ContextMenuRadioGroup>
239
+ </ContextMenu>
240
+ <Text variant="caption" tone="tertiary">
241
+ 눈금자 {ruler ? '켜짐' : '꺼짐'} · 격자 맞춤 {snap ? '켜짐' : '꺼짐'} · {zoom}% · 도형 {shapes}개
242
+ </Text>
243
+ </Stack>
244
+ )
245
+ }
246
+ ```
247
+
248
+ ### 비활성 · 제어
249
+
250
+ disabled면 브라우저 기본 메뉴가 그대로 떠요(글을 복사해야 하는 화면 등). open · onOpenChange로 열림 상태를 밖에서 읽거나 정할 수 있어요. 비활성 항목은 description으로 이유를 알려 주세요.
251
+
252
+ ```tsx
253
+ import { useState } from 'react'
254
+ import { Copy, Highlighter, Link2, MessageSquarePlus, Search } from 'lucide-react'
255
+ import { Stack, Switch, Text, Card, CardBody, ContextMenu, ContextMenuItem, ContextMenuSeparator, toast } from '@iamfiro/design'
256
+
257
+ export default function Example() {
258
+ const [native, setNative] = useState(false)
259
+ const [open, setOpen] = useState(false)
260
+
261
+ return (
262
+ <Stack gap={3} style={{ width: '100%', maxWidth: 460 }}>
263
+ <ContextMenu
264
+ aria-label="문단 메뉴"
265
+ disabled={native}
266
+ open={open}
267
+ onOpenChange={setOpen}
268
+ trigger={
269
+ <Card padding="sm" tabIndex={0}>
270
+ <CardBody>
271
+ <Text as="p" variant="body-md">
272
+ 다음 분기에는 온보딩 흐름을 세 단계로 줄이고, 첫 주 안에 팀원을 초대한 워크스페이스의 비율을 40%까지 올리는 것을 목표로
273
+ 해요.
274
+ </Text>
275
+ </CardBody>
276
+ </Card>
277
+ }
278
+ >
279
+ <ContextMenuItem icon={<Copy />} shortcut={['mod', 'C']} onSelect={() => toast.success('문단을 복사했어요')}>
280
+ 복사
281
+ </ContextMenuItem>
282
+ <ContextMenuItem icon={<Link2 />} onSelect={() => toast.success('이 문단의 링크를 복사했어요')}>
283
+ 문단 링크 복사
284
+ </ContextMenuItem>
285
+ <ContextMenuItem icon={<Search />} onSelect={() => toast('문서에서 검색했어요')}>
286
+ 문서에서 찾기
287
+ </ContextMenuItem>
288
+ <ContextMenuSeparator />
289
+ <ContextMenuItem icon={<MessageSquarePlus />} shortcut={['mod', 'alt', 'M']} onSelect={() => toast('댓글 입력 창을 열었어요')}>
290
+ 댓글 달기
291
+ </ContextMenuItem>
292
+ <ContextMenuItem icon={<Highlighter />} disabled description="읽기 전용 문서예요">
293
+ 하이라이트
294
+ </ContextMenuItem>
295
+ </ContextMenu>
296
+ <Switch label="브라우저 기본 메뉴 쓰기 (disabled)" checked={native} onChange={(e) => setNative(e.target.checked)} />
297
+ <Text variant="caption" tone="tertiary">
298
+ 메뉴 {open ? '열림' : '닫힘'} — open · onOpenChange로 상태를 밖에서 읽을 수 있어요.
299
+ </Text>
300
+ </Stack>
301
+ )
302
+ }
303
+ ```