@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,392 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ContextMenu ─ 우클릭/롱프레스로 열리는 컨텍스트 메뉴.
5
+ * Radix `@radix-ui/react-context-menu` 위에 얇게 얹은 compound 컴포넌트.
6
+ *
7
+ * 구조 원칙
8
+ * - 열림/닫힘·포커스 트랩·화살표 네비·서브메뉴 안전지연·뷰포트 flip은 Radix가 담당.
9
+ * - 시각·슬롯·상태(active/checked/destructive/hoverOnly)는 MenuItem이 담당.
10
+ * - 이 파일은 두 계층을 잇는 얇은 어댑터. 신규 시각 규칙 · 로직 · 상태를 여기서 만들지 않는다.
11
+ *
12
+ * 소비 표면
13
+ * <ContextMenu>
14
+ * <ContextMenu.Trigger>...</ContextMenu.Trigger>
15
+ * <ContextMenu.Content>
16
+ * <ContextMenu.Label>제목</ContextMenu.Label>
17
+ * <ContextMenu.Item leadingActions={[<Icon.../>]} shortcut="⌘C" onSelect={...}>
18
+ * 복사
19
+ * </ContextMenu.Item>
20
+ * <ContextMenu.Separator />
21
+ * <ContextMenu.CheckboxItem checked={grid} onCheckedChange={setGrid}>
22
+ * 그리드 표시
23
+ * </ContextMenu.CheckboxItem>
24
+ * <ContextMenu.RadioGroup value={view} onValueChange={setView}>
25
+ * <ContextMenu.RadioItem value="list">리스트</ContextMenu.RadioItem>
26
+ * <ContextMenu.RadioItem value="grid">그리드</ContextMenu.RadioItem>
27
+ * </ContextMenu.RadioGroup>
28
+ * <ContextMenu.Sub>
29
+ * <ContextMenu.SubTrigger>공유</ContextMenu.SubTrigger>
30
+ * <ContextMenu.SubContent>...</ContextMenu.SubContent>
31
+ * </ContextMenu.Sub>
32
+ * <ContextMenu.Item variant="destructive" onSelect={...}>삭제</ContextMenu.Item>
33
+ * </ContextMenu.Content>
34
+ * </ContextMenu>
35
+ */
36
+
37
+ import * as RCM from "@radix-ui/react-context-menu";
38
+ import {
39
+ createContext,
40
+ forwardRef,
41
+ useContext,
42
+ type ComponentPropsWithoutRef,
43
+ type ReactNode,
44
+ } from "react";
45
+
46
+ import { Icon, IconAxisProvider, useIconAxis } from "../Icon";
47
+ import { MenuItem, type MenuItemAction, type MenuItemSize, type MenuItemVariant } from "../MenuItem";
48
+ import styles from "./ContextMenu.module.css";
49
+
50
+ /* Size context ─────────────────────────────────────────────────────
51
+ Content/SubContent가 하위 Item·CheckboxItem·RadioItem·SubTrigger의 기본 size를 지정.
52
+ 개별 아이템의 size prop이 있으면 그 값이 우선.
53
+ 페이지에서 컨테이너 한 번만 size를 지정하면 모든 자식이 따라오도록. */
54
+ const SizeContext = createContext<MenuItemSize>("md");
55
+ function useMenuSize(override?: MenuItemSize): MenuItemSize {
56
+ const ctx = useContext(SizeContext);
57
+ return override ?? ctx;
58
+ }
59
+
60
+ /* Root · Trigger · Portal ─────────────────────────────────────────── */
61
+
62
+ type RootProps = ComponentPropsWithoutRef<typeof RCM.Root>;
63
+ const Root = (props: RootProps) => <RCM.Root {...props} />;
64
+
65
+ type TriggerProps = ComponentPropsWithoutRef<typeof RCM.Trigger>;
66
+ const Trigger = forwardRef<HTMLSpanElement, TriggerProps>(function ContextMenuTrigger(props, ref) {
67
+ return <RCM.Trigger ref={ref} {...props} />;
68
+ });
69
+
70
+ /* Content ─────────────────────────────────────────────────────────── */
71
+
72
+ export interface ContentProps extends ComponentPropsWithoutRef<typeof RCM.Content> {
73
+ /**
74
+ * Content 안의 모든 Item · CheckboxItem · RadioItem · SubTrigger 기본 size.
75
+ * 개별 아이템에서 size를 지정하면 그 값이 우선.
76
+ */
77
+ size?: MenuItemSize;
78
+ }
79
+
80
+ const Content = forwardRef<HTMLDivElement, ContentProps>(function ContextMenuContent(
81
+ { className, size = "md", collisionPadding = 8, children, ...rest },
82
+ ref,
83
+ ) {
84
+ /* Radix Portal이 Content를 document.body로 옮기며 React 트리 부모의 Provider가
85
+ 흘러가지 않는다. 상위 트리의 Icon 축 값을 캡처해 Portal 하위에 재주입. */
86
+ const iconAxis = useIconAxis();
87
+ return (
88
+ <RCM.Portal>
89
+ <RCM.Content
90
+ ref={ref}
91
+ className={[styles.content, "m-float", className ?? ""].filter(Boolean).join(" ")}
92
+ collisionPadding={collisionPadding}
93
+ data-size={size}
94
+ {...rest}
95
+ >
96
+ <IconAxisProvider value={iconAxis}>
97
+ <SizeContext.Provider value={size}>{children}</SizeContext.Provider>
98
+ </IconAxisProvider>
99
+ </RCM.Content>
100
+ </RCM.Portal>
101
+ );
102
+ });
103
+
104
+ /* SubContent ──────────────────────────────────────────────────────── */
105
+
106
+ export interface SubContentProps extends ComponentPropsWithoutRef<typeof RCM.SubContent> {
107
+ size?: MenuItemSize;
108
+ }
109
+
110
+ const SubContent = forwardRef<HTMLDivElement, SubContentProps>(function ContextMenuSubContent(
111
+ { className, size, collisionPadding = 8, children, ...rest },
112
+ ref,
113
+ ) {
114
+ /* SubContent는 별도 지정이 없으면 부모 Content의 size를 그대로 이어받는다.
115
+ 서브메뉴만 다른 사이즈를 쓰는 일이 없어 자연스러운 기본. */
116
+ const parentSize = useContext(SizeContext);
117
+ const effective = size ?? parentSize;
118
+ const iconAxis = useIconAxis();
119
+ return (
120
+ <RCM.Portal>
121
+ <RCM.SubContent
122
+ ref={ref}
123
+ className={[styles.content, "m-float", className ?? ""].filter(Boolean).join(" ")}
124
+ collisionPadding={collisionPadding}
125
+ data-size={effective}
126
+ {...rest}
127
+ >
128
+ <IconAxisProvider value={iconAxis}>
129
+ <SizeContext.Provider value={effective}>{children}</SizeContext.Provider>
130
+ </IconAxisProvider>
131
+ </RCM.SubContent>
132
+ </RCM.Portal>
133
+ );
134
+ });
135
+
136
+ /* Label ───────────────────────────────────────────────────────────── */
137
+
138
+ const Label = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<typeof RCM.Label>>(
139
+ function ContextMenuLabel({ className, children, ...rest }, ref) {
140
+ const size = useMenuSize();
141
+ /* MenuItem variant="section"을 감싸서 사이드바 그룹 헤더와 시각 통일.
142
+ Radix Label의 접근성/select 방지는 유지. */
143
+ return (
144
+ <RCM.Label ref={ref} asChild {...rest}>
145
+ <MenuItem variant="section" size={size} className={className}>
146
+ {children}
147
+ </MenuItem>
148
+ </RCM.Label>
149
+ );
150
+ },
151
+ );
152
+
153
+ /* Separator ───────────────────────────────────────────────────────── */
154
+
155
+ const Separator = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<typeof RCM.Separator>>(
156
+ function ContextMenuSeparator({ className, ...rest }, ref) {
157
+ return (
158
+ <RCM.Separator
159
+ ref={ref}
160
+ className={[styles.separator, className ?? ""].filter(Boolean).join(" ")}
161
+ {...rest}
162
+ />
163
+ );
164
+ },
165
+ );
166
+
167
+ /* Group · RadioGroup · Sub ───────────────────────────────────────── */
168
+
169
+ const Group = RCM.Group;
170
+ const RadioGroup = RCM.RadioGroup;
171
+ const Sub = RCM.Sub;
172
+
173
+ /* Item ─────────────────────────────────────────────────────────────
174
+ MenuItem을 Radix ContextMenu.Item에 asChild로 위임한다.
175
+ - Radix가 [data-highlighted]/[data-disabled]를 부여 → MenuItem CSS가 그대로 반응.
176
+ - onSelect(Radix)를 노출: 선택 후 자동 닫힘 + 이벤트 preventDefault로 열림 유지 가능. */
177
+
178
+ type ItemPassthrough = {
179
+ size?: MenuItemSize;
180
+ variant?: MenuItemVariant;
181
+ leadingActions?: MenuItemAction[];
182
+ trailingActions?: MenuItemAction[];
183
+ shortcut?: string;
184
+ disabled?: boolean;
185
+ children?: ReactNode;
186
+ };
187
+
188
+ export interface ItemProps
189
+ extends Omit<ComponentPropsWithoutRef<typeof RCM.Item>, "asChild" | "children">,
190
+ ItemPassthrough {}
191
+
192
+ const Item = forwardRef<HTMLDivElement, ItemProps>(function ContextMenuItem(
193
+ {
194
+ size,
195
+ variant,
196
+ leadingActions,
197
+ trailingActions,
198
+ shortcut,
199
+ disabled,
200
+ children,
201
+ ...rest
202
+ },
203
+ ref,
204
+ ) {
205
+ const effectiveSize = useMenuSize(size);
206
+ return (
207
+ <RCM.Item asChild disabled={disabled} {...rest}>
208
+ <MenuItem
209
+ ref={ref}
210
+ size={effectiveSize}
211
+ variant={variant}
212
+ leadingActions={leadingActions}
213
+ trailingActions={trailingActions}
214
+ shortcut={shortcut}
215
+ disabled={disabled}
216
+ >
217
+ {children}
218
+ </MenuItem>
219
+ </RCM.Item>
220
+ );
221
+ });
222
+
223
+ /* CheckboxItem ────────────────────────────────────────────────────── */
224
+
225
+ export interface CheckboxItemProps
226
+ extends Omit<ComponentPropsWithoutRef<typeof RCM.CheckboxItem>, "asChild" | "children">,
227
+ ItemPassthrough {}
228
+
229
+ const CheckboxItem = forwardRef<HTMLDivElement, CheckboxItemProps>(function ContextMenuCheckboxItem(
230
+ {
231
+ size,
232
+ variant,
233
+ leadingActions = [],
234
+ trailingActions,
235
+ shortcut,
236
+ disabled,
237
+ checked,
238
+ onCheckedChange,
239
+ children,
240
+ ...rest
241
+ },
242
+ ref,
243
+ ) {
244
+ const effectiveSize = useMenuSize(size);
245
+ /* leading 맨 앞에 Checkbox 컴포넌트의 시각 인디케이터를 얹는다.
246
+ Radix CheckboxItem이 상태·포커스·키보드를 이미 관리하므로 여기선 시각만 필요 —
247
+ Checkbox 컴포넌트의 <label><input> 접근성 구조는 중첩되면 방해되므로 재사용하지 않고,
248
+ `.indicator` 스타일만 composes로 가져와 span 하나로 재현한다.
249
+ 안에 항상 check Icon을 두되, Checkbox CSS의 `color: transparent → var(--bg)`가
250
+ checked일 때만 색을 드러낸다(data-checked). */
251
+ const indicatorSlot: MenuItemAction = {
252
+ node: (
253
+ <span
254
+ className={styles.checkIndicator}
255
+ data-checked={checked === true ? "true" : undefined}
256
+ aria-hidden
257
+ >
258
+ <Icon name="check" />
259
+ </span>
260
+ ),
261
+ key: "__checkbox__",
262
+ };
263
+ const composedLeading: MenuItemAction[] = [indicatorSlot, ...leadingActions];
264
+ return (
265
+ <RCM.CheckboxItem
266
+ asChild
267
+ disabled={disabled}
268
+ checked={checked}
269
+ onCheckedChange={onCheckedChange}
270
+ {...rest}
271
+ >
272
+ <MenuItem
273
+ ref={ref}
274
+ size={effectiveSize}
275
+ variant={variant}
276
+ leadingActions={composedLeading}
277
+ trailingActions={trailingActions}
278
+ shortcut={shortcut}
279
+ disabled={disabled}
280
+ >
281
+ {children}
282
+ </MenuItem>
283
+ </RCM.CheckboxItem>
284
+ );
285
+ });
286
+
287
+ /* RadioItem ───────────────────────────────────────────────────────── */
288
+
289
+ export interface RadioItemProps
290
+ extends Omit<ComponentPropsWithoutRef<typeof RCM.RadioItem>, "asChild" | "children">,
291
+ ItemPassthrough {}
292
+
293
+ const RadioItem = forwardRef<HTMLDivElement, RadioItemProps>(function ContextMenuRadioItem(
294
+ {
295
+ size,
296
+ variant,
297
+ leadingActions = [],
298
+ trailingActions,
299
+ shortcut,
300
+ disabled,
301
+ children,
302
+ ...rest
303
+ },
304
+ ref,
305
+ ) {
306
+ const effectiveSize = useMenuSize(size);
307
+ /* leading 맨 앞에 Radio 컴포넌트의 시각 인디케이터를 얹는다.
308
+ Radix RadioItem이 상태·포커스·키보드를 이미 관리하므로 여기선 시각만 필요.
309
+ `.indicator` 스타일만 composes로 가져와 span 하나로 재현. checked 여부는
310
+ Radix가 asChild 대상 MenuItem에 붙이는 data-state="checked"를 CSS 컨텍스트
311
+ 셀렉터로 잡아 자손 indicator에 전파한다 (module.css 참고). */
312
+ const composedLeading: MenuItemAction[] = [
313
+ { node: <span className={styles.radioIndicator} aria-hidden />, key: "__radio__" },
314
+ ...leadingActions,
315
+ ];
316
+ return (
317
+ <RCM.RadioItem asChild disabled={disabled} {...rest}>
318
+ <MenuItem
319
+ ref={ref}
320
+ size={effectiveSize}
321
+ variant={variant}
322
+ leadingActions={composedLeading}
323
+ trailingActions={trailingActions}
324
+ shortcut={shortcut}
325
+ disabled={disabled}
326
+ >
327
+ {children}
328
+ </MenuItem>
329
+ </RCM.RadioItem>
330
+ );
331
+ });
332
+
333
+ /* SubTrigger ──────────────────────────────────────────────────────── */
334
+
335
+ export interface SubTriggerProps
336
+ extends Omit<ComponentPropsWithoutRef<typeof RCM.SubTrigger>, "asChild" | "children">,
337
+ Omit<ItemPassthrough, "shortcut"> {}
338
+
339
+ const SubTrigger = forwardRef<HTMLDivElement, SubTriggerProps>(function ContextMenuSubTrigger(
340
+ { size, variant, leadingActions, trailingActions, disabled, children, ...rest },
341
+ ref,
342
+ ) {
343
+ const effectiveSize = useMenuSize(size);
344
+ return (
345
+ <RCM.SubTrigger asChild disabled={disabled} {...rest}>
346
+ <MenuItem
347
+ ref={ref}
348
+ size={effectiveSize}
349
+ variant={variant}
350
+ leadingActions={leadingActions}
351
+ trailingActions={trailingActions}
352
+ hasSubmenu
353
+ disabled={disabled}
354
+ >
355
+ {children}
356
+ </MenuItem>
357
+ </RCM.SubTrigger>
358
+ );
359
+ });
360
+
361
+ /* Compound export ─────────────────────────────────────────────────── */
362
+
363
+ type ContextMenuComponent = typeof Root & {
364
+ Trigger: typeof Trigger;
365
+ Content: typeof Content;
366
+ Item: typeof Item;
367
+ CheckboxItem: typeof CheckboxItem;
368
+ RadioGroup: typeof RadioGroup;
369
+ RadioItem: typeof RadioItem;
370
+ Separator: typeof Separator;
371
+ Label: typeof Label;
372
+ Group: typeof Group;
373
+ Sub: typeof Sub;
374
+ SubTrigger: typeof SubTrigger;
375
+ SubContent: typeof SubContent;
376
+ };
377
+
378
+ const ContextMenu = Root as ContextMenuComponent;
379
+ ContextMenu.Trigger = Trigger;
380
+ ContextMenu.Content = Content;
381
+ ContextMenu.Item = Item;
382
+ ContextMenu.CheckboxItem = CheckboxItem;
383
+ ContextMenu.RadioGroup = RadioGroup;
384
+ ContextMenu.RadioItem = RadioItem;
385
+ ContextMenu.Separator = Separator;
386
+ ContextMenu.Label = Label;
387
+ ContextMenu.Group = Group;
388
+ ContextMenu.Sub = Sub;
389
+ ContextMenu.SubTrigger = SubTrigger;
390
+ ContextMenu.SubContent = SubContent;
391
+
392
+ export { ContextMenu };
@@ -0,0 +1,9 @@
1
+ export { ContextMenu } from "./ContextMenu";
2
+ export type {
3
+ ContentProps as ContextMenuContentProps,
4
+ SubContentProps as ContextMenuSubContentProps,
5
+ ItemProps as ContextMenuItemProps,
6
+ CheckboxItemProps as ContextMenuCheckboxItemProps,
7
+ RadioItemProps as ContextMenuRadioItemProps,
8
+ SubTriggerProps as ContextMenuSubTriggerProps,
9
+ } from "./ContextMenu";
@@ -0,0 +1,234 @@
1
+ "use client";
2
+
3
+ import { memo, useMemo } from "react";
4
+ import { IconButton } from "../IconButton";
5
+ import { Icon } from "../Icon";
6
+ import styles from "./DatePicker.module.css";
7
+ import {
8
+ addMonths,
9
+ buildMonthGrid,
10
+ isSameDay,
11
+ isSameMonth,
12
+ isWithin,
13
+ MONTH_LABELS_KO,
14
+ WEEKDAY_LABELS_KO,
15
+ type DateRange,
16
+ } from "./utils";
17
+
18
+ interface MonthProps {
19
+ month: Date;
20
+ selectedFrom: Date | null;
21
+ selectedTo: Date | null;
22
+ hoverDate: Date | null;
23
+ rangeInProgress: boolean;
24
+ mode: "single" | "range";
25
+ onSelectDay: (day: Date) => void;
26
+ onHoverDay: (day: Date | null) => void;
27
+ minDate?: Date;
28
+ maxDate?: Date;
29
+ weekStartsOn: number;
30
+ showHeader: boolean;
31
+ onPrev?: () => void;
32
+ onNext?: () => void;
33
+ showPrev?: boolean;
34
+ showNext?: boolean;
35
+ }
36
+
37
+ const MonthView = memo(function MonthView({
38
+ month,
39
+ selectedFrom,
40
+ selectedTo,
41
+ hoverDate,
42
+ rangeInProgress,
43
+ mode,
44
+ onSelectDay,
45
+ onHoverDay,
46
+ minDate,
47
+ maxDate,
48
+ weekStartsOn,
49
+ showHeader,
50
+ onPrev,
51
+ onNext,
52
+ showPrev,
53
+ showNext,
54
+ }: MonthProps) {
55
+ const cells = useMemo(
56
+ () => buildMonthGrid(month, weekStartsOn),
57
+ [month, weekStartsOn]
58
+ );
59
+ const weekdays = useMemo(() => {
60
+ const list = [...WEEKDAY_LABELS_KO];
61
+ for (let i = 0; i < weekStartsOn; i += 1) list.push(list.shift() as string);
62
+ return list;
63
+ }, [weekStartsOn]);
64
+
65
+ const previewTo = rangeInProgress && selectedFrom && !selectedTo ? hoverDate : null;
66
+ const rangeStart = selectedFrom;
67
+ const rangeEnd = selectedTo ?? previewTo;
68
+ const normalizedStart =
69
+ rangeStart && rangeEnd && rangeStart.getTime() > rangeEnd.getTime() ? rangeEnd : rangeStart;
70
+ const normalizedEnd =
71
+ rangeStart && rangeEnd && rangeStart.getTime() > rangeEnd.getTime() ? rangeStart : rangeEnd;
72
+
73
+ return (
74
+ <div className={styles.monthCol}>
75
+ {showHeader && (
76
+ <div className={styles.monthHeader}>
77
+ <IconButton
78
+ variant="outline"
79
+ size="md"
80
+ icon={<Icon name="caret-left" />}
81
+ onClick={onPrev}
82
+ aria-label="이전 달"
83
+ style={{ visibility: showPrev ? "visible" : "hidden" }}
84
+ />
85
+ <div className={styles.monthTitle}>
86
+ {month.getFullYear()}년 {MONTH_LABELS_KO[month.getMonth()]}
87
+ </div>
88
+ <IconButton
89
+ variant="outline"
90
+ size="md"
91
+ icon={<Icon name="caret-right" />}
92
+ onClick={onNext}
93
+ aria-label="다음 달"
94
+ style={{ visibility: showNext ? "visible" : "hidden" }}
95
+ />
96
+ </div>
97
+ )}
98
+
99
+ <div className={styles.weekdays} role="row">
100
+ {weekdays.map((w) => (
101
+ <div key={w} role="columnheader" className={styles.weekday}>
102
+ {w}
103
+ </div>
104
+ ))}
105
+ </div>
106
+
107
+ <div className={styles.grid} role="grid">
108
+ {cells.map((day) => {
109
+ const outside = !isSameMonth(day, month);
110
+ const disabled =
111
+ outside ||
112
+ (minDate && day.getTime() < minDate.setHours(0, 0, 0, 0)) ||
113
+ (maxDate && day.getTime() > maxDate.setHours(23, 59, 59, 999));
114
+
115
+ const isFrom = mode === "range" && isSameDay(day, normalizedStart);
116
+ const isTo = mode === "range" && isSameDay(day, normalizedEnd);
117
+ const isSelected =
118
+ mode === "single"
119
+ ? isSameDay(day, selectedFrom)
120
+ : isFrom || isTo;
121
+ const inRange =
122
+ mode === "range" &&
123
+ !outside &&
124
+ normalizedStart &&
125
+ normalizedEnd &&
126
+ isWithin(day, normalizedStart, normalizedEnd) &&
127
+ !isFrom &&
128
+ !isTo;
129
+ const isToday = !outside && isSameDay(day, new Date());
130
+
131
+ return (
132
+ <button
133
+ type="button"
134
+ key={day.toISOString()}
135
+ className={styles.dayCell}
136
+ data-outside={outside || undefined}
137
+ data-today={isToday || undefined}
138
+ data-selected={isSelected || undefined}
139
+ data-in-range={inRange || undefined}
140
+ data-range-start={isFrom || undefined}
141
+ data-range-end={isTo || undefined}
142
+ disabled={disabled || undefined}
143
+ onClick={() => onSelectDay(day)}
144
+ onMouseEnter={() => onHoverDay(day)}
145
+ onMouseLeave={() => onHoverDay(null)}
146
+ aria-selected={isSelected}
147
+ role="gridcell"
148
+ >
149
+ <span className={styles.dayLabel}>{day.getDate()}</span>
150
+ </button>
151
+ );
152
+ })}
153
+ </div>
154
+ </div>
155
+ );
156
+ });
157
+
158
+ export interface CalendarProps {
159
+ mode: "single" | "range";
160
+ months: 1 | 2;
161
+ viewMonth: Date;
162
+ onViewMonthChange: (m: Date) => void;
163
+ selectedFrom: Date | null;
164
+ selectedTo: Date | null;
165
+ hoverDate: Date | null;
166
+ onHoverDate: (d: Date | null) => void;
167
+ rangeInProgress: boolean;
168
+ onSelectDay: (day: Date) => void;
169
+ minDate?: Date;
170
+ maxDate?: Date;
171
+ weekStartsOn?: number;
172
+ }
173
+
174
+ export function Calendar({
175
+ mode,
176
+ months,
177
+ viewMonth,
178
+ onViewMonthChange,
179
+ selectedFrom,
180
+ selectedTo,
181
+ hoverDate,
182
+ onHoverDate,
183
+ rangeInProgress,
184
+ onSelectDay,
185
+ minDate,
186
+ maxDate,
187
+ weekStartsOn = 0,
188
+ }: CalendarProps) {
189
+ const secondMonth = addMonths(viewMonth, 1);
190
+ return (
191
+ <div className={styles.calendarWrap} data-months={months}>
192
+ <MonthView
193
+ month={viewMonth}
194
+ selectedFrom={selectedFrom}
195
+ selectedTo={selectedTo}
196
+ hoverDate={hoverDate}
197
+ rangeInProgress={rangeInProgress}
198
+ mode={mode}
199
+ onSelectDay={onSelectDay}
200
+ onHoverDay={onHoverDate}
201
+ minDate={minDate}
202
+ maxDate={maxDate}
203
+ weekStartsOn={weekStartsOn}
204
+ showHeader
205
+ onPrev={() => onViewMonthChange(addMonths(viewMonth, -1))}
206
+ onNext={() => onViewMonthChange(addMonths(viewMonth, 1))}
207
+ showPrev
208
+ showNext={months === 1}
209
+ />
210
+ {months === 2 && (
211
+ <MonthView
212
+ month={secondMonth}
213
+ selectedFrom={selectedFrom}
214
+ selectedTo={selectedTo}
215
+ hoverDate={hoverDate}
216
+ rangeInProgress={rangeInProgress}
217
+ mode={mode}
218
+ onSelectDay={onSelectDay}
219
+ onHoverDay={onHoverDate}
220
+ minDate={minDate}
221
+ maxDate={maxDate}
222
+ weekStartsOn={weekStartsOn}
223
+ showHeader
224
+ onPrev={() => onViewMonthChange(addMonths(viewMonth, -1))}
225
+ onNext={() => onViewMonthChange(addMonths(viewMonth, 1))}
226
+ showPrev={false}
227
+ showNext
228
+ />
229
+ )}
230
+ </div>
231
+ );
232
+ }
233
+
234
+ export type { DateRange };