@conscience-technology/ctds 0.0.0-stage → 0.1.1

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 (289) hide show
  1. package/README.md +128 -2
  2. package/assets/components/AppFrame/AppFrame.module.css +177 -0
  3. package/assets/components/AppFrame/AppFrame.tsx +93 -0
  4. package/assets/components/AppFrame/index.ts +6 -0
  5. package/assets/components/AttachmentChip/AttachmentChip.module.css +71 -0
  6. package/assets/components/AttachmentChip/AttachmentChip.tsx +74 -0
  7. package/assets/components/AttachmentChip/index.ts +2 -0
  8. package/assets/components/AttachmentThumb/AttachmentThumb.module.css +107 -0
  9. package/assets/components/AttachmentThumb/AttachmentThumb.tsx +91 -0
  10. package/assets/components/AttachmentThumb/index.ts +2 -0
  11. package/assets/components/Avatar/Avatar.module.css +167 -0
  12. package/assets/components/Avatar/Avatar.tsx +228 -0
  13. package/assets/components/Avatar/index.ts +2 -0
  14. package/assets/components/AvatarUpload/AvatarUpload.module.css +130 -0
  15. package/assets/components/AvatarUpload/AvatarUpload.tsx +139 -0
  16. package/assets/components/AvatarUpload/index.ts +2 -0
  17. package/assets/components/Badge/Badge.module.css +76 -0
  18. package/assets/components/Badge/Badge.tsx +68 -0
  19. package/assets/components/Badge/index.ts +2 -0
  20. package/assets/components/Breadcrumb/Breadcrumb.module.css +213 -0
  21. package/assets/components/Breadcrumb/Breadcrumb.tsx +279 -0
  22. package/assets/components/Breadcrumb/index.ts +8 -0
  23. package/assets/components/Button/Button.module.css +216 -0
  24. package/assets/components/Button/Button.tsx +149 -0
  25. package/assets/components/Button/index.ts +2 -0
  26. package/assets/components/Callout/Callout.module.css +146 -0
  27. package/assets/components/Callout/Callout.tsx +138 -0
  28. package/assets/components/Callout/index.ts +2 -0
  29. package/assets/components/ChatInput/ChatInput.module.css +254 -0
  30. package/assets/components/ChatInput/ChatInput.tsx +322 -0
  31. package/assets/components/ChatInput/ModelSelector.tsx +40 -0
  32. package/assets/components/ChatInput/SendButton.tsx +41 -0
  33. package/assets/components/ChatInput/index.ts +10 -0
  34. package/assets/components/Checkbox/Checkbox.module.css +66 -0
  35. package/assets/components/Checkbox/Checkbox.tsx +36 -0
  36. package/assets/components/Checkbox/index.ts +2 -0
  37. package/assets/components/Chip/Chip.module.css +128 -0
  38. package/assets/components/Chip/Chip.tsx +103 -0
  39. package/assets/components/Chip/index.ts +2 -0
  40. package/assets/components/ChoiceCard/ChoiceCard.module.css +142 -0
  41. package/assets/components/ChoiceCard/ChoiceCard.tsx +132 -0
  42. package/assets/components/ChoiceCard/index.ts +2 -0
  43. package/assets/components/ContextMenu/ContextMenu.module.css +102 -0
  44. package/assets/components/ContextMenu/ContextMenu.tsx +392 -0
  45. package/assets/components/ContextMenu/index.ts +9 -0
  46. package/assets/components/DatePicker/Calendar.tsx +234 -0
  47. package/assets/components/DatePicker/DESIGN_NOTES.md +106 -0
  48. package/assets/components/DatePicker/DESIGN_NOTES_TIME.md +125 -0
  49. package/assets/components/DatePicker/DatePicker.module.css +295 -0
  50. package/assets/components/DatePicker/DatePicker.tsx +387 -0
  51. package/assets/components/DatePicker/TimeSelector.tsx +69 -0
  52. package/assets/components/DatePicker/index.ts +8 -0
  53. package/assets/components/DatePicker/presets.ts +80 -0
  54. package/assets/components/DatePicker/utils.ts +148 -0
  55. package/assets/components/Disclosure/Disclosure.module.css +73 -0
  56. package/assets/components/Disclosure/Disclosure.tsx +129 -0
  57. package/assets/components/Disclosure/index.ts +6 -0
  58. package/assets/components/Drawer/Drawer.module.css +257 -0
  59. package/assets/components/Drawer/Drawer.tsx +126 -0
  60. package/assets/components/Drawer/index.ts +7 -0
  61. package/assets/components/EmptyState/EmptyState.module.css +102 -0
  62. package/assets/components/EmptyState/EmptyState.tsx +80 -0
  63. package/assets/components/EmptyState/index.ts +2 -0
  64. package/assets/components/FileIcon/FileIcon.module.css +66 -0
  65. package/assets/components/FileIcon/FileIcon.tsx +84 -0
  66. package/assets/components/FileIcon/fileExtMap.ts +112 -0
  67. package/assets/components/FileIcon/index.ts +9 -0
  68. package/assets/components/FilterButton/FilterButton.module.css +130 -0
  69. package/assets/components/FilterButton/FilterButton.tsx +94 -0
  70. package/assets/components/FilterButton/index.ts +2 -0
  71. package/assets/components/GalleryItem/GalleryItem.module.css +220 -0
  72. package/assets/components/GalleryItem/GalleryItem.tsx +163 -0
  73. package/assets/components/GalleryItem/index.ts +7 -0
  74. package/assets/components/Icon/Icon.tsx +616 -0
  75. package/assets/components/Icon/context.tsx +39 -0
  76. package/assets/components/Icon/index.ts +4 -0
  77. package/assets/components/IconButton/IconButton.module.css +109 -0
  78. package/assets/components/IconButton/IconButton.tsx +110 -0
  79. package/assets/components/IconButton/index.ts +2 -0
  80. package/assets/components/Input/Input.module.css +338 -0
  81. package/assets/components/Input/Input.tsx +371 -0
  82. package/assets/components/Input/index.ts +2 -0
  83. package/assets/components/ListItem/ListItem.module.css +318 -0
  84. package/assets/components/ListItem/ListItem.tsx +262 -0
  85. package/assets/components/ListItem/index.ts +9 -0
  86. package/assets/components/MenuItem/MenuItem.module.css +437 -0
  87. package/assets/components/MenuItem/MenuItem.tsx +529 -0
  88. package/assets/components/MenuItem/index.ts +8 -0
  89. package/assets/components/MetricCard/MetricCard.module.css +153 -0
  90. package/assets/components/MetricCard/MetricCard.tsx +140 -0
  91. package/assets/components/MetricCard/index.ts +2 -0
  92. package/assets/components/Modal/Modal.module.css +175 -0
  93. package/assets/components/Modal/Modal.tsx +98 -0
  94. package/assets/components/Modal/index.ts +2 -0
  95. package/assets/components/Onboarding/DocsIndex.tsx +82 -0
  96. package/assets/components/Onboarding/LayoutBuilder.tsx +160 -0
  97. package/assets/components/Onboarding/Onboarding.module.css +1394 -0
  98. package/assets/components/Onboarding/OnboardingAxisPanel.tsx +515 -0
  99. package/assets/components/Onboarding/OnboardingPage.tsx +1193 -0
  100. package/assets/components/Onboarding/PresetsGallery.tsx +65 -0
  101. package/assets/components/Onboarding/actions.ts +250 -0
  102. package/assets/components/Onboarding/index.ts +8 -0
  103. package/assets/components/Onboarding/palette.css +40 -0
  104. package/assets/components/Onboarding/wizardData.ts +265 -0
  105. package/assets/components/Pagination/Pagination.module.css +82 -0
  106. package/assets/components/Pagination/Pagination.tsx +248 -0
  107. package/assets/components/Pagination/index.ts +6 -0
  108. package/assets/components/ProgressBar/ProgressBar.module.css +64 -0
  109. package/assets/components/ProgressBar/ProgressBar.tsx +54 -0
  110. package/assets/components/ProgressBar/index.ts +2 -0
  111. package/assets/components/QuotationBar/QuotationBar.module.css +61 -0
  112. package/assets/components/QuotationBar/QuotationBar.tsx +48 -0
  113. package/assets/components/QuotationBar/index.ts +2 -0
  114. package/assets/components/Radio/Radio.module.css +63 -0
  115. package/assets/components/Radio/Radio.tsx +33 -0
  116. package/assets/components/Radio/index.ts +2 -0
  117. package/assets/components/Select/Select.module.css +151 -0
  118. package/assets/components/Select/Select.tsx +425 -0
  119. package/assets/components/Select/index.ts +10 -0
  120. package/assets/components/SelectableCard/SelectableCard.module.css +177 -0
  121. package/assets/components/SelectableCard/SelectableCard.tsx +83 -0
  122. package/assets/components/SelectableCard/index.ts +2 -0
  123. package/assets/components/Shortcut/Shortcut.module.css +56 -0
  124. package/assets/components/Shortcut/Shortcut.tsx +156 -0
  125. package/assets/components/Shortcut/index.ts +2 -0
  126. package/assets/components/Sidebar/Sidebar.module.css +242 -0
  127. package/assets/components/Sidebar/Sidebar.tsx +264 -0
  128. package/assets/components/Sidebar/index.ts +7 -0
  129. package/assets/components/SidebarSplit/SidebarSplit.module.css +126 -0
  130. package/assets/components/SidebarSplit/SidebarSplit.tsx +222 -0
  131. package/assets/components/SidebarSplit/index.ts +6 -0
  132. package/assets/components/Slider/Slider.module.css +120 -0
  133. package/assets/components/Slider/Slider.tsx +88 -0
  134. package/assets/components/Slider/index.ts +2 -0
  135. package/assets/components/SplitButton/SplitButton.module.css +190 -0
  136. package/assets/components/SplitButton/SplitButton.tsx +145 -0
  137. package/assets/components/SplitButton/index.ts +6 -0
  138. package/assets/components/Stepper/Stepper.module.css +182 -0
  139. package/assets/components/Stepper/Stepper.tsx +185 -0
  140. package/assets/components/Stepper/index.ts +2 -0
  141. package/assets/components/Switch/Switch.module.css +116 -0
  142. package/assets/components/Switch/Switch.tsx +58 -0
  143. package/assets/components/Switch/index.ts +2 -0
  144. package/assets/components/TableCell/TableCell.module.css +137 -0
  145. package/assets/components/TableCell/TableCellAvatar.tsx +42 -0
  146. package/assets/components/TableCell/TableCellBadge.tsx +23 -0
  147. package/assets/components/TableCell/TableCellIcon.tsx +39 -0
  148. package/assets/components/TableCell/TableCellImage.tsx +49 -0
  149. package/assets/components/TableCell/TableCellLink.tsx +33 -0
  150. package/assets/components/TableCell/TableCellMeter.tsx +55 -0
  151. package/assets/components/TableCell/TableCellNumber.tsx +43 -0
  152. package/assets/components/TableCell/TableCellText.tsx +30 -0
  153. package/assets/components/TableCell/index.ts +23 -0
  154. package/assets/components/TableHeader/TableHeader.module.css +287 -0
  155. package/assets/components/TableHeader/TableHeader.tsx +590 -0
  156. package/assets/components/TableHeader/index.ts +9 -0
  157. package/assets/components/TableRow/TableRow.module.css +290 -0
  158. package/assets/components/TableRow/TableRow.tsx +253 -0
  159. package/assets/components/TableRow/index.ts +8 -0
  160. package/assets/components/TableRowEdit/TableRowEdit.module.css +126 -0
  161. package/assets/components/TableRowEdit/TableRowEdit.tsx +247 -0
  162. package/assets/components/TableRowEdit/index.ts +2 -0
  163. package/assets/components/TableRowSelect/TableRowSelect.module.css +23 -0
  164. package/assets/components/TableRowSelect/TableRowSelect.tsx +175 -0
  165. package/assets/components/TableRowSelect/index.ts +2 -0
  166. package/assets/components/Tabs/Tabs.module.css +234 -0
  167. package/assets/components/Tabs/Tabs.tsx +426 -0
  168. package/assets/components/Tabs/index.ts +9 -0
  169. package/assets/components/Textarea/Textarea.module.css +318 -0
  170. package/assets/components/Textarea/Textarea.tsx +426 -0
  171. package/assets/components/Textarea/index.ts +7 -0
  172. package/assets/components/ThemeController.tsx +271 -0
  173. package/assets/components/TimePicker/TimePicker.module.css +317 -0
  174. package/assets/components/TimePicker/TimePicker.tsx +821 -0
  175. package/assets/components/TimePicker/index.ts +8 -0
  176. package/assets/components/TimePicker/utils.ts +115 -0
  177. package/assets/components/Toast/Toast.module.css +264 -0
  178. package/assets/components/Toast/Toast.tsx +243 -0
  179. package/assets/components/Toast/index.ts +15 -0
  180. package/assets/components/Toolbar/Toolbar.module.css +72 -0
  181. package/assets/components/Toolbar/Toolbar.tsx +75 -0
  182. package/assets/components/Toolbar/index.ts +2 -0
  183. package/assets/components/Tooltip/Tooltip.module.css +42 -0
  184. package/assets/components/Tooltip/Tooltip.tsx +332 -0
  185. package/assets/components/Tooltip/index.ts +2 -0
  186. package/assets/components/Topbar/Topbar.module.css +26 -0
  187. package/assets/components/Topbar/Topbar.tsx +47 -0
  188. package/assets/components/Topbar/index.ts +2 -0
  189. package/assets/components/_cn.ts +3 -0
  190. package/assets/components/_color.ts +48 -0
  191. package/assets/components/_data.ts +659 -0
  192. package/assets/components/_utils.css +19 -0
  193. package/assets/components/axisOptions.ts +24 -0
  194. package/assets/components/tokens.css +742 -0
  195. package/assets/prompts/GUIDE.md +251 -0
  196. package/assets/prompts/OVERRIDE.md +170 -0
  197. package/assets/prompts/PRINCIPLES.md +130 -0
  198. package/assets/prompts/axes/density.md +38 -0
  199. package/assets/prompts/axes/font.md +36 -0
  200. package/assets/prompts/axes/hue.md +36 -0
  201. package/assets/prompts/axes/icon.md +48 -0
  202. package/assets/prompts/axes/mode.md +50 -0
  203. package/assets/prompts/axes/motion.md +77 -0
  204. package/assets/prompts/axes/palette.md +35 -0
  205. package/assets/prompts/axes/shape.md +33 -0
  206. package/assets/prompts/axes/voice.md +54 -0
  207. package/assets/prompts/components/README.md +84 -0
  208. package/assets/prompts/components/app-frame.md +94 -0
  209. package/assets/prompts/components/attachment-chip.md +46 -0
  210. package/assets/prompts/components/attachment-thumb.md +49 -0
  211. package/assets/prompts/components/avatar.md +86 -0
  212. package/assets/prompts/components/badge.md +104 -0
  213. package/assets/prompts/components/breadcrumb.md +77 -0
  214. package/assets/prompts/components/button.md +99 -0
  215. package/assets/prompts/components/callout.md +81 -0
  216. package/assets/prompts/components/chat-input.md +84 -0
  217. package/assets/prompts/components/checkbox.md +46 -0
  218. package/assets/prompts/components/chip.md +84 -0
  219. package/assets/prompts/components/choice-card.md +127 -0
  220. package/assets/prompts/components/context-menu.md +145 -0
  221. package/assets/prompts/components/date-picker.md +104 -0
  222. package/assets/prompts/components/drawer.md +152 -0
  223. package/assets/prompts/components/empty-state.md +69 -0
  224. package/assets/prompts/components/file-icon.md +73 -0
  225. package/assets/prompts/components/filter-button.md +42 -0
  226. package/assets/prompts/components/form-field.md +74 -0
  227. package/assets/prompts/components/gallery-item.md +158 -0
  228. package/assets/prompts/components/icon-button.md +82 -0
  229. package/assets/prompts/components/icon.md +68 -0
  230. package/assets/prompts/components/input.md +95 -0
  231. package/assets/prompts/components/list-item.md +235 -0
  232. package/assets/prompts/components/menu-item.md +234 -0
  233. package/assets/prompts/components/metric-card.md +132 -0
  234. package/assets/prompts/components/modal.md +131 -0
  235. package/assets/prompts/components/pagination.md +92 -0
  236. package/assets/prompts/components/progress-bar.md +61 -0
  237. package/assets/prompts/components/quotation-bar.md +39 -0
  238. package/assets/prompts/components/radio.md +41 -0
  239. package/assets/prompts/components/select.md +104 -0
  240. package/assets/prompts/components/selectable-card.md +67 -0
  241. package/assets/prompts/components/shortcut.md +76 -0
  242. package/assets/prompts/components/sidebar.md +107 -0
  243. package/assets/prompts/components/slider.md +89 -0
  244. package/assets/prompts/components/split-button.md +71 -0
  245. package/assets/prompts/components/stepper.md +93 -0
  246. package/assets/prompts/components/switch.md +86 -0
  247. package/assets/prompts/components/table-cell.md +87 -0
  248. package/assets/prompts/components/table-header.md +157 -0
  249. package/assets/prompts/components/table-row-edit.md +129 -0
  250. package/assets/prompts/components/table-row-select.md +82 -0
  251. package/assets/prompts/components/table-row.md +156 -0
  252. package/assets/prompts/components/tabs.md +122 -0
  253. package/assets/prompts/components/textarea.md +105 -0
  254. package/assets/prompts/components/toast.md +149 -0
  255. package/assets/prompts/components/tooltip.md +71 -0
  256. package/assets/prompts/components/topbar.md +148 -0
  257. package/assets/prompts/primitives/emerge.md +43 -0
  258. package/assets/prompts/primitives/fade.md +31 -0
  259. package/assets/prompts/primitives/scale.md +32 -0
  260. package/assets/prompts/primitives/scroll.md +75 -0
  261. package/assets/prompts/primitives/slide.md +31 -0
  262. package/assets/prompts/themes/_SCHEMA.md +135 -0
  263. package/assets/prompts/themes/_TEMPLATE.md +73 -0
  264. package/assets/prompts/themes/expressive.md +118 -0
  265. package/assets/prompts/themes/pure.md +87 -0
  266. package/assets/prompts/themes/spare.md +73 -0
  267. package/assets/prompts/tokens/breakpoint.md +29 -0
  268. package/assets/prompts/tokens/color.md +78 -0
  269. package/assets/prompts/tokens/radius.md +33 -0
  270. package/assets/prompts/tokens/shadow.md +35 -0
  271. package/assets/prompts/tokens/spacing.md +86 -0
  272. package/assets/prompts/tokens/typography.md +76 -0
  273. package/assets/prompts/tokens/z-index.md +35 -0
  274. package/assets/public/expressive.png +0 -0
  275. package/assets/public/pure.png +0 -0
  276. package/assets/public/spare.png +0 -0
  277. package/assets/skill/SKILL.md +36 -0
  278. package/bin/ctds.js +76 -0
  279. package/package.json +35 -4
  280. package/src/commands/init.js +215 -0
  281. package/src/commands/update.js +275 -0
  282. package/src/utils/assets.js +78 -0
  283. package/src/utils/deps.js +51 -0
  284. package/src/utils/detect.js +47 -0
  285. package/src/utils/diff.js +52 -0
  286. package/src/utils/hash.js +16 -0
  287. package/src/utils/log.js +41 -0
  288. package/src/utils/meta.js +73 -0
  289. package/src/utils/prompt.js +28 -0
@@ -0,0 +1,235 @@
1
+ # ListItem
2
+
3
+ ### 원칙
4
+ 피드·알림·인박스·활동 로그에 반복해서 등장하는 "누가 뭘 했다" 한 줄의 최소 단위. GitHub Timeline · Linear Activity · Jira Issue history · Slack Notification · Notion Updates가 모두 이 형태다. 시각 구조와 상태 신호(unread/urgent)만 담당하고, 그룹핑·필터·무한스크롤·mark-as-read 같은 흐름은 상위 컨테이너가 책임진다.
5
+
6
+ 같은 anatomy 안에서 **variant로 자리의 성격만 바꾼다** — `activity`(과거 서술), `notification`(처리 필요), `mention`(인용), `system`(사람 아님). variant를 늘리기 전에 slot(`leading`/`trailing`/`hoverActions`)으로 조립이 되는지 먼저 본다.
7
+
8
+ 컴포넌트 자신은 **단일 row**다. border/divider/카드 chrome을 자체적으로 지지 않는다 — 리스트로 묶는 건 부모 레이아웃이 결정한다. 운영 콘솔은 border로, pure 좌표는 hairline으로, 카드 안 리스트는 Callout이 담당. ([[components/metric-card]]의 `bare` variant와 같은 원리.)
9
+
10
+ **단, "chrome 없음"은 완전한 공간 없음을 뜻하지 않는다** — ListItem은 `padding-block: var(--space-3) + margin-block: -var(--space-3)`의 **self-contained 상쇄 공식**을 가진다. 부모가 세로 padding을 안 걸어도 row hover hit area가 보장되게 하려는 안전장치다. 하지만 flex item 레이아웃에서는 음의 margin이 레이아웃 박스를 축소하지 못해 **ListItem이 "상쇄된 자리"를 실제로 차지한다** — 부모 li가 padding을 또 걸면 두 번 공간이 쌓여 과하게 보인다. 자세한 공식·완화법은 아래 "조립 — 부모 wrapper 공식" 섹션.
11
+
12
+ ### Do
13
+ - **anatomy는 네 자리로 고정**: `[leading] [who] [body / verb+target] [meta/trailing]`. 어떤 variant든 이 뼈대를 깨지 않는다.
14
+ - **layout은 두 형태**: `inline`(한 줄, 기본) · `stacked`(두 줄 — from이 1줄, body가 2줄). 서술 모드 둘 다에서 작동 — action 모드 stacked는 "from · verb · target" + body 2줄째, 서술 모드 stacked는 from + body 2줄째. 긴 서술/인용/멘션은 stacked.
15
+ - **서술은 두 모드 중 하나로**: 자유 서술은 `from` + `body`("회계팀 — 외주비 결재 요청"), 액션 서술은 `from` + `verb` + `target`("박지훈 · 댓글 남김 · ERP 2.0 사이드바"). 둘 다 넘기지 않는다.
16
+ - **unread는 텍스트 톤으로만 신호** (dot/badge 없음): `unread={true}`면 body/from이 가장 진하고 meta는 `--primary` 컬러로 "지금"을 강조. `unread={false}`면 전체 톤이 한두 단계 뒤로 물러나 "이미 처리됨"을 시사. `unread` 생략(undefined)이면 중립 — **리드/언리드 개념 자체가 없는 자리**(활동 로그·히스토리·타임라인)에서 자유롭게 사용.
17
+ - **surface는 작은 inline 토큰**: `surface="GitHub"` 같은 서비스명은 target 뒤 작게 inline. [[components/badge]] variant="soft"로 렌더되지만 text-only로 유지.
18
+ - **meta는 상대시간 한 토큰**: "방금", "12분", "어제". ISO 날짜는 Tooltip으로 보조.
19
+ - **hover-only 액션은 명시적으로**: `hoverActions={[<IconButton name="archive"/>, ...]}` — Linear Inbox의 archive/snooze/mark-read 패턴. 평상시 숨김, row hover/focus/focus-within에서 등장. [[components/menu-item]]의 hoverOnly와 같은 메커니즘.
20
+ - **row 전체 클릭은 `href`로**: 라우팅 아이템이면 asChild로 `<Link>`에 위임. target만 링크로 쓰고 row는 비활성인 자리도 OK.
21
+ - **leading 정책은 리스트 단위로 통일**: 일부엔 Avatar, 일부엔 Icon처럼 섞으면 "누구"의 자리가 어긋난다. system 메시지가 섞이는 리스트면 전부 Avatar fallback(이니셜) + system은 서비스 로고 Avatar.
22
+ - **leading 타입은 세 종류**: Avatar(사람·팀), Icon(단색 글리프), `ListItemLeadingIcon`(배경색 + 아이콘 마크). 세 번째는 "서비스/시스템이 상태를 색으로 신호"하는 자리 — tone 프리셋(`primary · success · warning · danger · info · neutral`)으로 bg/fg 쌍을 한 번에, 혹은 `--leading-icon-bg` · `--leading-icon-fg` 변수로 세밀 조절. Avatar와 같은 사이즈 트랙(xs/sm/md/lg)이라 섞여도 정렬 유지. 추가로 `size="auto"`는 `layout="stacked"`와 짝 — content 두 줄 전체 높이에 맞춰 정사각으로 stretch, 상태 마크를 크게 보여야 하는 알림/시스템 메시지 자리.
23
+ - **density는 컨테이너에 맡긴다**: ListItem 자체는 size prop 없이, 부모 리스트의 density(또는 Density 축) 토큰을 상속한다.
24
+
25
+ ### Don't
26
+ - `verb` + `body` 동시 사용 금지 — 서술 중복. 액션 모드와 서술 모드는 배타.
27
+ - 채팅뷰처럼 긴 메시지 본문을 그대로 노출하지 않는다 — ListItem은 "누가 어디서 뭘 했다" 요약 라인이지, 메시지 내용의 복사본이 아니다. 멘션/댓글이어도 `verb` + `target`("어디/뭐에")로 축약하고 상세는 target 클릭 뒤 전체 뷰에서. 인용부호를 둘러친 긴 body도 금지.
28
+ - `leading`을 생략하고 unread dot만 두지 않는다 — dot 혼자 떠서 "누구"의 자리가 사라진다. system 메시지는 서비스 로고/아이콘 Avatar를 세운다.
29
+ - body를 두 줄 이상으로 늘리지 않는다 — 길면 `layout="stacked"`로 자동 승격해 target 라인과 서술 라인을 분리하거나, [[components/callout]] 영역으로 넘긴다.
30
+ - `trailing` + `hoverActions`를 둘 다 넘기지 않는다 — 메타 자리 하나뿐. 상시 메타(카운트·시간)와 hover 액션이 공존해야 하면 `trailing`에 배열로.
31
+ - unread/urgent를 색으로 중복 신호하지 않는다 — dot만. body 색을 fg-strong으로 올리거나 배경을 깔면 과잉.
32
+ - 자체적으로 padding·border·bg는 두지 않지만 **hover bg는 예외** — row 레벨 상호작용 피드백은 "가두는 chrome"이 아니라 "지금 이 줄" 신호이므로 default로 켜져 있다. pure 좌표에서 끄려면 색상 훅 `--list-item-hover-bg: transparent`.
33
+ - "댓글 남김" 같은 동사를 긴 서술로 늘이지 않는다. verb는 **동사 토큰** 하나. 긴 설명은 target 아래 2줄 body로 승격.
34
+
35
+ ### Fallback
36
+ variant 애매하면 `activity`. layout 애매하면 `inline`. leading 애매하면 `from` 기반 Avatar(이니셜).
37
+
38
+ ### 색상 훅 (컨테이너 단위 톤 조절)
39
+ MenuItem과 같은 패턴. 리스트 wrapper에 변수를 얹으면 안쪽 ListItem이 일괄 반응.
40
+
41
+ | 훅 | 대상 | 폴백 |
42
+ |---|---|---|
43
+ | `--list-item-fg` | 본문 서술 색 | `--fg` |
44
+ | `--list-item-from-fg` | from(누구) 색 | `--fg-strong` |
45
+ | `--list-item-meta-fg` | meta/상대시간 색 | `--fg-muted` |
46
+ | `--list-item-hover-bg` | row hover bg | `--hover` |
47
+
48
+ ```css
49
+ /* 예: 조용한 활동 로그 — 메타·본문 전부 한 단계 뒤로 */
50
+ .quiet-activity {
51
+ --list-item-fg: var(--fg-muted);
52
+ --list-item-meta-fg: var(--fg-subtle);
53
+ }
54
+
55
+ /* 예: 긴급 알림 리스트 — unread dot을 warning 톤으로 */
56
+ .urgent-notifications {
57
+ --list-item-unread: var(--warning);
58
+ }
59
+ ```
60
+
61
+ ### Props 시그니처
62
+ ```tsx
63
+ <ListItem
64
+ // 왼쪽
65
+ leading={ReactNode} // Avatar | Icon | 커스텀. 생략 가능 (system에서도 가능하면 Avatar fallback 권장)
66
+ unread={boolean} // 명시해야 톤 신호. undefined=중립, true=강조, false=뒤로
67
+
68
+ // 본문 — 두 모드
69
+ from={ReactNode} // "회계팀" / "박지훈" / "ERP"
70
+ body={ReactNode} // 자유 서술 모드
71
+ verb={ReactNode} // "댓글 남김" — 액션 모드
72
+ target={ReactNode} // 대상. 링크 자리로 자주 쓰임
73
+ surface={ReactNode} // "Figma" · "GitHub" — target 뒤 작게 inline
74
+
75
+ // 메타
76
+ meta={ReactNode} // "방금" · "12분". 오른쪽 끝 default
77
+ trailing={ReactNode} // meta 자리 커스텀 (ReactNode 또는 배열)
78
+ hoverActions={ReactNode[]} // row hover 시 등장. archive/snooze/⋯
79
+
80
+ // 레이아웃·상호작용
81
+ variant="activity | notification | mention | system" // default: activity
82
+ layout="inline | stacked" // default: inline. stacked는 body 2줄.
83
+ href={string} // row 전체 링크. asChild로 Link 위임.
84
+ onClick={(e) => void} // row 선택 핸들러
85
+ asChild={boolean} // <Link> 등에 위임
86
+ />
87
+ ```
88
+
89
+ ### Variant 매트릭스
90
+ 같은 anatomy, 다른 **기본 성격**. slot으로 못 바꾸는 자리만 variant로 바꾼다.
91
+ `unread`는 variant가 강제하지 않는다 — 명시해야 신호. 리드 개념이 없는 자리면 생략.
92
+
93
+ | variant | leading 기본 | hoverActions 전제 | 서술 모드 선호 | 쓰임 |
94
+ |---|---|---|---|---|
95
+ | `activity` | Avatar(from) | 없음 | verb+target | 최근 활동·히스토리 로그. GitHub/Linear timeline. 보통 unread 생략 |
96
+ | `notification` | Avatar(from) | archive/mark-read | from+body | 알림 센터·bell 드롭다운. unread 명시 권장 |
97
+ | `mention` | Avatar(from) | reply/resolve | from+body(인용) | 멘션·댓글 피드. body에 인용부호 자연스러움 |
98
+ | `system` | Icon(서비스 로고) | 없음 | from+body | ERP/CI/빌드 등 시스템 이벤트. 보통 unread 생략 |
99
+
100
+ ### 금지 조합
101
+ - `verb` + `body` 동시 — 서술 중복
102
+ - `leading=null` + `unread=true` — dot이 혼자 떠서 "누구" 자리가 사라짐
103
+ - `trailing` + `hoverActions` — 메타 자리 충돌 (상시+hover면 trailing에 배열)
104
+ - `layout="inline"` + body 2줄 이상 — stacked로 승격
105
+ - `variant="system"` + Avatar(사람 이름) — system은 서비스 아이덴티티
106
+ - 한 리스트 안에 variant 혼용 — 리스트 톤은 하나로. 섞어야 하면 섹션으로 분리
107
+
108
+ ### 조합 예시
109
+
110
+ ```tsx
111
+ // 1) 최근 활동 피드 — verb+target 액션 서술
112
+ <ListItem
113
+ leading={<Avatar name="박지훈" />}
114
+ from="박지훈"
115
+ verb="댓글 남김"
116
+ target="ERP 2.0 사이드바 구조 확정"
117
+ surface="Figma"
118
+ meta="방금"
119
+ />
120
+
121
+ // 2) 알림 센터 — unread + hoverActions
122
+ <ListItem
123
+ variant="notification"
124
+ leading={<Avatar name="회계팀" />}
125
+ from="회계팀"
126
+ body="외주비 세금계산서 결재 요청"
127
+ meta="방금"
128
+ unread
129
+ hoverActions={[
130
+ <IconButton name="archive" aria-label="보관" />,
131
+ <IconButton name="check" aria-label="읽음 처리" />,
132
+ ]}
133
+ />
134
+
135
+ // 3) 멘션 — body에 인용 (stacked로 2줄)
136
+ <ListItem
137
+ variant="mention"
138
+ layout="stacked"
139
+ leading={<Avatar name="장대표" />}
140
+ from="장대표"
141
+ body={<>&ldquo;@김희영 1:1 전에 자료 공유 부탁&rdquo;</>}
142
+ meta="오전 9:12"
143
+ unread
144
+ />
145
+
146
+ // 4) 시스템 이벤트 — Icon leading, 사람 아님
147
+ <ListItem
148
+ variant="system"
149
+ leading={<Icon name="chart-line" />}
150
+ from="ERP"
151
+ body="주간 리포트 생성 완료"
152
+ meta="3시간"
153
+ />
154
+
155
+ // 4-b) 상태 색을 띤 아이콘 마크 leading — 성공/경고/실패 신호
156
+ <ListItem
157
+ variant="system"
158
+ leading={
159
+ <ListItemLeadingIcon tone="success">
160
+ <Icon name="seal-check" />
161
+ </ListItemLeadingIcon>
162
+ }
163
+ from="CI"
164
+ body="main 브랜치 배포 성공"
165
+ meta="방금"
166
+ />
167
+
168
+ // 5) pure 좌표 — 부모 리스트가 hairline으로 분리 (조립 공식 "패턴 C" 참고)
169
+ // li에 세로 padding + ListItem self-space 중화 둘 다 필요하다.
170
+ <ul className="pureFeed">
171
+ {feed.map((f) => (
172
+ <li key={f.id}>
173
+ <ListItem
174
+ leading={<Avatar name={f.from} />}
175
+ from={f.from}
176
+ body={f.body}
177
+ meta={f.ago}
178
+ unread={f.unread}
179
+ />
180
+ </li>
181
+ ))}
182
+ </ul>
183
+ ```
184
+
185
+ ### 조립 — 부모 wrapper 공식
186
+ ListItem은 **self-contained 상쇄 공식**(`padding-block: --space-3` + `margin-block: -(--space-3)`)을 들고 있다. 부모가 공간을 안 걸어도 보장되는 안전장치지만, 조립할 때 아래 세 패턴을 혼동하면 세로가 과하거나 콘텐츠가 선에 붙는다.
187
+
188
+ **패턴 A — 부모가 간격 아예 안 걸음 (단독 사용):**
189
+ 그대로 둬도 ListItem이 자체 공간을 가진다. hover hit area는 보장되지만 **리스트처럼 세로로 쌓이면 아이템끼리 바로 붙는다** — 리스트 자리면 패턴 B/C로.
190
+
191
+ **패턴 B — 부모 li에 padding만 걸고 ListItem self-space는 그대로:**
192
+ flex item 레이아웃에서 ListItem의 음의 margin이 레이아웃 박스를 축소시키지 못해 **공간이 두 번 쌓인다** (li padding + ListItem self-padding). 겉보기엔 세로가 과하게 커진다 — hairline과 콘텐츠 사이 호흡은 보이지만 리스트가 전반적으로 숨 막혀 보이면 이 상태다.
193
+
194
+ **패턴 C — li padding + ListItem self-space 중화 (권장, pure 좌표):**
195
+ 부모 wrapper가 세로 공식을 완전히 책임지는 자리에서는 자식 선택자로 ListItem self-padding·margin을 둘 다 0으로 눌러 공식을 **하나로 통일**한다. 이게 "콘텐츠가 hairline에 붙지 않으면서도 세로가 과하지 않은" 유일한 조립.
196
+
197
+ ```css
198
+ /* pure 좌표 — hairline 리스트 */
199
+ .activityList {
200
+ list-style: none;
201
+ margin: 0;
202
+ padding: 0;
203
+ display: flex;
204
+ flex-direction: column;
205
+ --list-item-hover-bg: transparent; /* 가두는 chrome 회피 */
206
+ }
207
+ .activityList > li {
208
+ padding-block: var(--space-3); /* 세로 호흡은 li가 전담 */
209
+ border-top: 1px solid var(--border-subtle, var(--border));
210
+ }
211
+ .activityList > li:last-child {
212
+ border-bottom: 1px solid var(--border-subtle, var(--border));
213
+ }
214
+ /* ListItem self-space 중화 — 자식 선택자로 */
215
+ .activityList > li > * {
216
+ padding-block: 0;
217
+ margin-block: 0;
218
+ }
219
+ ```
220
+
221
+ **조립 체크리스트 — 리스트에 쓰기 전 아래를 순서대로:**
222
+ 1. 리스트인가? (하나만이면 그대로 둬도 OK)
223
+ 2. 부모 wrapper가 세로 padding을 책임질 것인가? (hairline 리스트면 반드시 YES)
224
+ 3. YES면 자식 선택자로 ListItem self-padding·margin을 중화했는가?
225
+ 4. 완성 후 콘텐츠가 hairline/테두리에 **padding 없이 붙어 있지 않은가** 눈으로 확인.
226
+
227
+ ### 관련
228
+ - [[components/avatar]] — leading의 default 렌더. system variant에서는 서비스 로고 Avatar.
229
+ - [[components/menu-item]] — hoverOnly 패턴을 동일한 메커니즘으로 재사용.
230
+ - [[components/badge]] — surface가 inline soft Badge로 렌더되는 경우.
231
+ - [[components/callout]] — body가 2줄을 넘고 CTA가 필요하면 Callout 영역.
232
+ - [[components/metric-card]] — 자신이 chrome을 지지 않고 부모가 분리를 담당하는 원칙(`bare` variant)과 같은 설계 철학.
233
+ - [[axes/density]] — compact에선 수직 padding/gap이 줄어 timeline이 조밀해짐.
234
+ - [[tokens/color]] — `--primary`(unread meta) · `--fg` · `--fg-strong` · `--fg-muted` · `--fg-subtle`만 사용.
235
+ - [[feedback_no_uppercase]] — verb·surface·meta 어디에도 uppercase 금지.
@@ -0,0 +1,234 @@
1
+ # MenuItem
2
+
3
+ ### 원칙
4
+ 메뉴/리스트 안 개별 행의 최소 단위. Dropdown Menu · Context Menu · Sidebar · Command Palette · 일반 List 아이템이 모두 이 컴포넌트를 공유한다. 시각 구조와 상태만 담당하고, 열림/닫힘·포커스 트랩·화살표 네비게이션은 상위 컨테이너(Radix DropdownMenu/ContextMenu, Sidebar `<nav>` 등)가 책임진다.
5
+
6
+ root는 `<div role="menuitem">`이다 — `leadingActions`/`trailingActions`에 `IconButton` 같은 인터랙션 요소를 자유롭게 넣기 위해 nested `<button>` 스펙 위반을 피했다. Enter/Space 키 처리는 컴포넌트가 직접 한다.
7
+
8
+ ### Do
9
+ - **슬롯은 배열로**: `leadingActions` / `trailingActions`에 아이콘·IconButton·배지·카운트 등 뭐든 넣는다. 각 슬롯 아이템은 `flex-gap: --space-1.5`로 나열됨.
10
+ - **hover-only 액션은 명시적으로**: `{ node: <IconButton .../>, hoverOnly: true }` — 평상시 숨김, MenuItem hover/focus/focus-within에서 드러남. Notion 페이지 리스트 `+`/`⋯`, Linear 이슈 `⋯`, VSCode Explorer `+`/`✏️`/`↻`, Slack 채널 `⋯` 표준 패턴.
11
+ - **항상 보이는 메타 + hover 액션 공존**: `trailingActions={[unreadCount, { node: menuBtn, hoverOnly: true }]}` — Slack 채널 언리드 카운트 + hover ⋯가 이 형태.
12
+ - **내부 액션 클릭은 반드시 `e.stopPropagation()`**: 아니면 root의 `onClick`(=아이템 자체 선택)도 함께 발화한다. IconButton `onClick` 안에서 처리.
13
+ - **checked=true**면 `leadingActions` 맨 앞에 자동으로 checkmark. 기존 leading 아이콘과 공존 가능(체크된 파일 옆에 파일 타입 아이콘도 함께).
14
+ - **한 메뉴 안 leading 정책은 통일**: 어느 아이템에 leadingActions이 있으면 나머지도 유지 — 아니면 라벨 시작점이 어긋난다.
15
+ - **size는 컨텍스트에 맞춘다**: Dropdown/Context Menu · Sidebar = `md`(기본), Command Palette · 밀도 높은 관리 UI = `sm`, 터치·모바일 시트 = `lg`.
16
+ - **단축키는 실제 심볼**: `⌘K`, `⇧⌘P`. uppercase(`CTRL+K`) 금지 — [[feedback_no_uppercase]].
17
+ - **destructive는 hover-only 빨강**: 평상시 fg, hover/focus에서만 빨강. 위계상 "약한" 위험 표시. 최종 확인은 [[components/alert-dialog]].
18
+
19
+ ### Don't
20
+ - MenuItem 안에 두 개 이상의 &ldquo;주된 액션&rdquo;을 두지 않는다 — 아이템 전체가 하나의 selection. 부가 액션(즐겨찾기 토글·⋯메뉴)만 slot에 얹는다.
21
+ - 라벨을 두 줄 이상 넣지 않는다. 설명이 필요하면 [[components/command-palette]] description 슬롯이나 [[components/tooltip]].
22
+ - 페이지 이동에 root click 시맨틱 금지. 라우팅 아이템이면 `asChild`로 `<Link>`/`<a>`에 위임.
23
+ - height를 CSS로 덮어쓰지 않는다. `size` prop 경유 (Density 축이 스왑).
24
+ - hover-only 액션을 &ldquo;시각적으로 숨김&rdquo;으로만 처리하지 않는다 — `opacity: 0` + `pointer-events: none`이 짝. 안 그러면 안 보이는 버튼이 클릭됨.
25
+
26
+ ### Fallback
27
+ size 애매하면 `md`, variant 애매하면 `default`.
28
+
29
+ ### 색상 훅 (컨테이너 단위 톤 조절)
30
+ MenuItem은 세 개의 CSS 변수로 색을 열어둔다. **부모 컨테이너에 값을 얹으면 그 안의 모든 MenuItem이 일괄 반응**한다 — variant를 늘리지 말고 이 훅을 쓰는 게 원칙.
31
+
32
+ | 훅 | 대상 | 폴백 |
33
+ |---|---|---|
34
+ | `--menu-fg` | 기본 라벨 색 | `--fg` |
35
+ | `--menu-active-bg` | active 배경 (fill 스타일) | `--bg-muted` |
36
+ | `--menu-active-fg` | active 라벨·아이콘·accent-bar 바 | fill=`--fg-strong` / accent-bar=`--primary` |
37
+
38
+ ```css
39
+ /* 예: 사이드바 라벨을 연하게 + active는 브랜드 컬러로 */
40
+ .brand-nav {
41
+ --menu-fg: var(--fg-muted);
42
+ --menu-active-bg: var(--primary);
43
+ --menu-active-fg: var(--bg);
44
+ }
45
+
46
+ /* 예: 조용한 리스트 — 평소 아주 연하게, active는 기본 강조만 */
47
+ .quiet-list {
48
+ --menu-fg: var(--fg-subtle);
49
+ }
50
+ ```
51
+
52
+ Do
53
+ - 훅은 **컨테이너 단위**로 지정한다 — 개별 아이템마다 다른 톤을 쓰면 시각 리듬이 깨진다.
54
+ - primary·secondary·destructive 같은 **시맨틱 토큰만** 훅에 넣는다. hex/oklch 직접 금지 ([[PRINCIPLES]] §1).
55
+
56
+ Don't
57
+ - 이 훅으로 hover/focus 색을 바꾸려 하지 않는다 — hover는 `--hover`로 별도 관리되며 원칙적으로 통일 유지.
58
+ - disabled/destructive 색은 훅으로 열려 있지 않다 — 접근성·위계 규칙이 붙어 있어 컴포넌트가 통제한다.
59
+
60
+ ### 관련
61
+ - [[components/button]] · [[components/icon-button]] — trailing/leading에 흔히 들어감. hover 표면·destructive 위계 원리 공유.
62
+ - [[components/dropdown-menu]] · [[components/context-menu]] — 이 컴포넌트를 감싸는 상위 컨테이너
63
+ - [[components/sidebar]] · [[components/command-palette]] — 같은 MenuItem을 다른 컨텍스트에서 사용
64
+ - [[axes/density]] — `--control-h-*`가 size에 매핑
65
+ - [[axes/shape]] — radius가 Shape 축에 반응 (Button과 동일)
66
+ - [[tokens/color]] — `--hover`/`--bg-muted`/`--destructive` 매핑
67
+
68
+ ### Props 시그니처
69
+ ```tsx
70
+ type MenuItemAction =
71
+ | ReactNode
72
+ | { node: ReactNode; hoverOnly?: boolean; key?: string };
73
+
74
+ <MenuItem
75
+ size="sm | md | lg" // default: md
76
+ variant="default | destructive" // default: default
77
+ leadingActions={MenuItemAction[]} // 왼쪽 슬롯 (아이콘, IconButton, 배지, ...)
78
+ trailingActions={MenuItemAction[]} // 오른쪽 슬롯 (카운트, ⋯, hover-only 액션, ...)
79
+ shortcut="⌘K" // trailingActions 맨 앞에 자동 삽입
80
+ hasSubmenu={boolean} // trailingActions 맨 뒤에 caret 자동 삽입
81
+ checked={boolean} // leadingActions 맨 앞에 checkmark 자동 삽입
82
+ active={boolean} // 현재 페이지/선택 아이템 강조
83
+ disabled={boolean}
84
+ asChild={boolean} // Radix Slot — <Link>/<a>/DropdownMenu.Item 등에 위임
85
+ onClick={(e) => void}
86
+ />
87
+ ```
88
+
89
+ ### 상태 매트릭스
90
+ | 상태 | 시각 |
91
+ |---|---|
92
+ | default | fg 텍스트, 투명 배경 |
93
+ | hover · focus (`:focus-visible` / Radix `[data-highlighted]`) | `--hover` 배경, `fg-strong` 텍스트 |
94
+ | active(눌림) | `--bg-muted` 배경 |
95
+ | active(현재 페이지/선택) | `--bg-muted` 배경, `fg-strong` + medium weight |
96
+ | checked | leading 맨 앞에 checkmark, 색 `fg-strong` |
97
+ | disabled | `fg-disabled` 텍스트, cursor not-allowed, tabIndex=-1 |
98
+ | destructive · hover | destructive 8% 배경, destructive 텍스트/아이콘 |
99
+ | hoverOnly 액션 | 기본 opacity 0 → hover/focus/focus-within 시 opacity 1 (터치는 항상 노출) |
100
+
101
+ ### Size 가이드
102
+ | size | height | icon | 언제 |
103
+ |---|---|---|---|
104
+ | `sm` | 32 | 14 | 밀도 높은 관리 UI, Command Palette |
105
+ | `md` | 36 | 16 | **기본.** Dropdown / Context Menu / Sidebar |
106
+ | `lg` | 40 | 18 | 터치·모바일 시트, 랜딩용 큰 메뉴 |
107
+
108
+ ### 조합 예시
109
+ ```tsx
110
+ // 가장 단순 — 아이콘 + 라벨 + 단축키
111
+ <MenuItem
112
+ leadingActions={[<Icon name="user" />]}
113
+ shortcut="⌘,"
114
+ >계정 설정</MenuItem>
115
+
116
+ // 서브메뉴 트리거
117
+ <MenuItem
118
+ leadingActions={[<Icon name="star" />]}
119
+ hasSubmenu
120
+ >즐겨찾기로 이동</MenuItem>
121
+
122
+ // 체크박스 메뉴 (Radix CheckboxItem + [[components/checkbox]])
123
+ // 표준: leading 슬롯에 <Checkbox> 컴포넌트를 그대로 얹는다.
124
+ // - MenuItem의 checked prop(✓ 자동 삽입)에 의존하지 않는다 — 시각 톤을 폼과 통일하기 위함.
125
+ // - Checkbox는 pointer-events:none + tabIndex=-1로 시각 마커화하고, 실제 토글은
126
+ // Radix가 관리한다(onCheckedChange). raw MenuItem이면 onClick에서.
127
+ <DropdownMenu.CheckboxItem asChild checked={showGrid} onCheckedChange={setShowGrid}>
128
+ <MenuItem
129
+ leadingActions={[
130
+ <Checkbox size="sm" checked={showGrid} readOnly tabIndex={-1} className="menu-form-marker" />,
131
+ ]}
132
+ shortcut="⌘G"
133
+ >그리드 표시</MenuItem>
134
+ </DropdownMenu.CheckboxItem>
135
+
136
+ // 라디오 메뉴 (Radix RadioGroup + RadioItem + [[components/radio]])
137
+ // 표준: leading 슬롯에 <Radio> 컴포넌트를 그대로 얹는다.
138
+ // - 미선택도 "빈 원(○)" 자리를 유지 → 라벨 좌측 시작점 흔들림 없음.
139
+ // - Radio도 마커화(pointer-events:none, tabIndex=-1). 같은 name으로 native 그룹 보장.
140
+ <DropdownMenu.RadioGroup value={density} onValueChange={setDensity}>
141
+ {["compact", "cozy", "comfortable"].map((value) => (
142
+ <DropdownMenu.RadioItem asChild key={value} value={value}>
143
+ <MenuItem
144
+ leadingActions={[
145
+ <Radio
146
+ size="sm"
147
+ name="density"
148
+ value={value}
149
+ checked={density === value}
150
+ readOnly
151
+ tabIndex={-1}
152
+ className="menu-form-marker"
153
+ />,
154
+ ]}
155
+ active={density === value}
156
+ >{value}</MenuItem>
157
+ </DropdownMenu.RadioItem>
158
+ ))}
159
+ </DropdownMenu.RadioGroup>
160
+
161
+ // 참고: ContextMenu/DropdownMenu의 <CheckboxItem>/<RadioItem> 래퍼는
162
+ // 내부에서 이미 dot/check placeholder를 leading에 얹고 CSS로 그린다.
163
+ // 그 래퍼를 쓰면 위처럼 leadingActions를 직접 넘길 필요 없음.
164
+ // 여기 예시는 raw MenuItem 위에 폼 컴포넌트로 직접 조합하는 방식.
165
+
166
+ // Notion 페이지 리스트: leading 페이지 아이콘, 항상 보이는 카운트, hover 시 +/⋯
167
+ <MenuItem
168
+ leadingActions={[<Icon name="star" />]}
169
+ trailingActions={[
170
+ <span className="badge">12</span>,
171
+ {
172
+ node: (
173
+ <IconButton
174
+ variant="ghost"
175
+ size="sm"
176
+ icon={<Icon name="arrow-right" />}
177
+ aria-label="새 하위 페이지"
178
+ onClick={(e) => { e.stopPropagation(); addChild(); }}
179
+ />
180
+ ),
181
+ hoverOnly: true,
182
+ },
183
+ {
184
+ node: (
185
+ <IconButton
186
+ variant="ghost"
187
+ size="sm"
188
+ icon={<Icon name="gear" />}
189
+ aria-label="더 보기"
190
+ onClick={(e) => { e.stopPropagation(); openMenu(); }}
191
+ />
192
+ ),
193
+ hoverOnly: true,
194
+ },
195
+ ]}
196
+ onClick={() => openPage()}
197
+ >디자인 시스템</MenuItem>
198
+
199
+ // Figma 파일 리스트: 항상 보이는 별표(즐겨찾기 토글), hover ⋯
200
+ <MenuItem
201
+ leadingActions={[
202
+ <IconButton
203
+ variant="ghost"
204
+ size="sm"
205
+ icon={<Icon name="star" weight={starred ? "fill" : "regular"} />}
206
+ aria-label={starred ? "즐겨찾기 해제" : "즐겨찾기"}
207
+ onClick={(e) => { e.stopPropagation(); toggleStar(); }}
208
+ />,
209
+ ]}
210
+ trailingActions={[
211
+ <span className="date">2일 전</span>,
212
+ {
213
+ node: (
214
+ <IconButton size="sm" variant="ghost" aria-label="더 보기" icon={<Icon name="gear" />}
215
+ onClick={(e) => { e.stopPropagation(); openMenu(); }} />
216
+ ),
217
+ hoverOnly: true,
218
+ },
219
+ ]}
220
+ onClick={() => openFile()}
221
+ >Untitled</MenuItem>
222
+
223
+ // Sidebar 라우팅: asChild로 <Link>에 위임 + active
224
+ <MenuItem asChild active={pathname === "/tokens/color"} leadingActions={[<Icon name="star" />]}>
225
+ <Link href="/tokens/color">Color</Link>
226
+ </MenuItem>
227
+
228
+ // 위험 액션
229
+ <MenuItem
230
+ leadingActions={[<Icon name="trash" />]}
231
+ variant="destructive"
232
+ onClick={() => confirmDelete()}
233
+ >삭제</MenuItem>
234
+ ```
@@ -0,0 +1,132 @@
1
+ # MetricCard
2
+
3
+ ### 원칙
4
+ 대시보드 상단에 조밀하게 늘어서는 KPI/Stat 카드. 라벨 → 큰 숫자 → delta Badge 로 "지금 숫자 하나" 를 전달한다. 숫자 뒤에 추세 신호를 색으로 겹치지만, 숫자 자체는 중립 톤으로 유지해 여러 장이 나란히 서도 색이 튀지 않는다. 긴 설명이 필요하거나 액션이 있으면 [[components/callout]] 영역이고, 리스트/표의 반복 레코드는 [[components/table-row]] 영역이다.
5
+
6
+ ### Do
7
+ - 한 카드엔 숫자 하나. `value`는 **짧게** — "72%", "4건", "₩1.2M" 처럼 단위 포함 한 토큰.
8
+ - 추세는 `trend`로 넘기고 숫자는 `deltaLabel`에만. Badge는 자동으로 생성(상승=success+▲ / 하락=destructive+▼ / 유지=info+–).
9
+ - 그리드로 늘어세울 때는 4~5열, 모든 카드 `variant` 동일. 섞으면 집중도가 깨진다.
10
+ - 숨은 drill-down 링크로 쓰고 싶으면 `href` 넘긴다 — hover 배경 + focus ring이 자동 활성.
11
+ - `description`은 "vs 지난주" 같은 비교 축만. 두 줄 이상이면 Callout 영역.
12
+
13
+ ### Don't
14
+ - `value`에 두 줄 텍스트, 긴 서술, 통화·단위를 라벨로 분리해 큰 숫자만 외롭게 두지 않는다.
15
+ - `trend`와 `delta`를 둘 다 넘기지 않는다 — `delta`를 넘기면 자동 매핑이 무시된다.
16
+ - `trend="down"`인데 숫자상 "좋은" 하락(예: 미결 결재 ↓)일 때도 색을 success로 뒤집지 않는다. 추세 색은 **방향**만 신호한다. 좋음/나쁨 해석은 `description`이나 라벨에서 한다.
17
+ - 그림자로 depth 쌓지 않는다. Shape 축이 shadow를 쥐었다면 간접 반영만.
18
+ - 아이콘을 숫자 옆에 두지 않는다. 아이콘은 라벨 왼쪽 보조 신호.
19
+
20
+ ### Fallback
21
+ `variant` 애매하면 `line`. `trend` 애매하면 생략(delta Badge 숨김). `href`/`onClick` 없이도 완결된다.
22
+
23
+ `bare`는 "부모가 분리를 담당하는" 자리에서만 — 혼자 쓰면 안 됨. pure 좌표에서 상단 hairline + 수직 hairline으로 strip을 만든 자리, 또는 섹션 안 한 줄 수평 나열에서 사용.
24
+
25
+ ### 관련
26
+ - [[components/callout]] — 긴 설명·CTA가 있는 공지. MetricCard는 단일 수치, Callout은 서술.
27
+ - [[components/badge]] — Badge pill로는 delta를 쓰지 않지만, 수동 `delta` slot에 Badge를 넣을 수 있음.
28
+ - [[components/table-row]] — 숫자 반복 레코드. 4개 이상이고 비교·정렬이 필요하면 테이블.
29
+ - [[axes/shape]] — radius 성향을 흡수. 자체 리터럴 없음.
30
+ - [[axes/density]] — compact에선 그리드 열 수 ↑, comfortable에선 ↓.
31
+ - [[tokens/color]] — `--bg` · `--bg-subtle` · `--border` · `--fg` · `--fg-muted` 만 사용.
32
+
33
+ ### Props 시그니처
34
+ ```tsx
35
+ <MetricCard
36
+ label={ReactNode} // required — 캡션
37
+ value={ReactNode} // required — 큰 숫자
38
+ icon={IconName} // 선택 — 라벨 왼쪽
39
+ trend="up | down | flat" // 추세 방향. 색 자동 매핑
40
+ deltaLabel={ReactNode} // trend와 짝. "+8%p"
41
+ delta={ReactNode} // 수동 override. trend/deltaLabel 무시
42
+ description={ReactNode} // 선택 — value 아래 보조 설명
43
+ variant="line | soft | bare" // default: line
44
+ href={string} // 있으면 <a>로 렌더. hover/focus 활성
45
+ />
46
+ ```
47
+
48
+ ### Variant 매트릭스
49
+
50
+ | variant | 표면 | 경계 | padding | 용도 |
51
+ |---|---|---|---|---|
52
+ | `line` | `--bg` | `1px solid --border` | 있음 | 운영 콘솔·조밀 그리드. hairline만. 기본값 |
53
+ | `soft` | `--bg-subtle` | 투명 | 있음 | 서술형 대시보드·분위기 있는 레이아웃. 경계 대신 표면 톤으로 depth |
54
+ | `bare` | 투명 | 없음 | 없음 | pure 좌표 전용. 부모가 hairline/톤으로 분리를 담당하는 strip 안에서만. 혼자 쓰지 않는다 |
55
+
56
+ **trend × 색 매핑 (자동)**
57
+
58
+ delta는 Badge pill이 아니라 **inline 텍스트 + 아이콘** 조합. 색은 토큰 그대로 입힌다 — 배경 없음.
59
+
60
+ | trend | Color | Icon |
61
+ |---|---|---|
62
+ | `up` | `--success` | `trend-up` |
63
+ | `down` | `--destructive` | `trend-down` |
64
+ | `flat` | `--fg-muted` | `minus` |
65
+
66
+ **금지 조합**
67
+ - `variant=soft` + 그림자 추가 — 표면 톤이 depth를 쥐고 있는데 그림자까지 쌓이면 Shape 축 신호가 깨진다.
68
+ - `trend` 매핑을 "좋음=green/나쁨=red" 가치판단으로 뒤집기 — 색은 **방향**만. 의미 반전은 라벨/설명에서.
69
+ - 한 그리드 안에 `line` + `soft` + `bare` 혼용 — depth 레일이 깨진다. 그리드 단위로 variant 통일.
70
+ - `value`에 Badge나 Chip 삽입 — 숫자 토큰은 하나여야 한다.
71
+ - `bare`를 분리 장치 없는 자리에 혼자 사용 — 부모 strip(상단 hairline + 수직 hairline, 또는 bg 톤 교대)이 없으면 카드가 서로 뭉쳐 "덜 가둠"이 아니라 "덜 담음"으로 보인다.
72
+ - `bare` + 부모 셀에 padding 생략 — bare는 자체 padding을 비웠으므로 **부모 셀(strip 안의 각 cell)이 좌우 padding을 반드시 책임져야** 숫자가 hairline에 붙지 않는다. [[components/list-item]] "조립 — 부모 wrapper 공식"과 같은 원리로, chrome 없는 variant는 부모가 공간 공식을 전담.
73
+
74
+ ### 조합 예시
75
+
76
+ ```tsx
77
+ // 운영 콘솔 — 조밀한 KPI 4카드
78
+ <section className="grid grid-cols-4 gap-3">
79
+ {metrics.map((m) => (
80
+ <MetricCard
81
+ key={m.id}
82
+ variant="line"
83
+ label={m.label}
84
+ value={m.value}
85
+ trend={m.trend}
86
+ deltaLabel={m.delta}
87
+ />
88
+ ))}
89
+ </section>
90
+
91
+ // 아이콘 + 설명이 붙은 "분위기" 대시보드
92
+ <MetricCard
93
+ variant="soft"
94
+ icon="chart-line"
95
+ label="이번 주 완료율"
96
+ value="72%"
97
+ trend="up"
98
+ deltaLabel="+8%p"
99
+ description="vs 지난주 평균"
100
+ />
101
+
102
+ // 수동 delta (Badge 자체를 커스텀) — trend 자동 매핑 off
103
+ <MetricCard
104
+ label="대기 중 결재"
105
+ value="4건"
106
+ delta={<Badge variant="soft" status="warning">주의</Badge>}
107
+ />
108
+
109
+ // drill-down 링크 카드
110
+ <MetricCard
111
+ href="/approvals"
112
+ label="대기 중 결재"
113
+ value="4건"
114
+ trend="down"
115
+ deltaLabel="-2건"
116
+ />
117
+
118
+ // pure 좌표 — bare variant를 hairline strip 안에서.
119
+ // 부모가 상단 border-top + 자식 간 수직 border-right으로 분리를 담당.
120
+ <section className="metricsStrip"> {/* border-top: 1px solid border-subtle */}
121
+ {metrics.map((m) => (
122
+ <MetricCard
123
+ key={m.id}
124
+ variant="bare"
125
+ label={m.label}
126
+ value={m.value}
127
+ trend={m.trend}
128
+ deltaLabel={m.delta}
129
+ />
130
+ ))}
131
+ </section>
132
+ ```