@conscience-technology/ctds 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 (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 +241 -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,387 @@
1
+ "use client";
2
+
3
+ import * as RadixPopover from "@radix-ui/react-popover";
4
+ import {
5
+ forwardRef,
6
+ useCallback,
7
+ useMemo,
8
+ useState,
9
+ type ReactNode,
10
+ } from "react";
11
+
12
+ import { Input, type InputSize } from "../Input";
13
+ import { Icon } from "../Icon";
14
+ import { Button } from "../Button";
15
+ import styles from "./DatePicker.module.css";
16
+ import { Calendar } from "./Calendar";
17
+ import { TimeSelector } from "./TimeSelector";
18
+ import {
19
+ defaultRangePresets,
20
+ defaultSinglePresets,
21
+ type RangePreset,
22
+ type SinglePreset,
23
+ } from "./presets";
24
+ import {
25
+ combineDateTime,
26
+ formatDate,
27
+ formatHM,
28
+ startOfMonth,
29
+ type DateFormat,
30
+ } from "./utils";
31
+
32
+ export type DatePickerMode = "single" | "range";
33
+
34
+ export interface DatePickerProps {
35
+ mode?: DatePickerMode;
36
+ /** 달력을 몇 개 나란히 보일지. range에서 2를 권장. */
37
+ months?: 1 | 2;
38
+
39
+ /** single 모드 값 */
40
+ value?: Date | null;
41
+ /** range 모드 값 */
42
+ rangeValue?: { from: Date | null; to: Date | null };
43
+
44
+ onChange?: (v: Date | null) => void;
45
+ onRangeChange?: (v: { from: Date | null; to: Date | null }) => void;
46
+
47
+ /** 시간 선택 포함 여부 */
48
+ withTime?: boolean;
49
+
50
+ /** 퀵 프리셋 표시 */
51
+ withPresets?: boolean;
52
+ /** 커스텀 프리셋 (mode에 맞춰 전달). 지정 시 기본 프리셋 대체 */
53
+ presets?: SinglePreset[] | RangePreset[];
54
+
55
+ /** 하단 액션 바 표시 (재설정 / 확인). withTime일 때 자동 true. */
56
+ withFooter?: boolean;
57
+ /** footer 재설정 클릭 — 값을 null로 초기화 */
58
+ onReset?: () => void;
59
+ /** footer 확인 클릭 — Popover 닫기 트리거. inline일 땐 무시. */
60
+ onConfirm?: () => void;
61
+ /** footer 재설정 버튼 라벨 (기본 "재설정") */
62
+ resetLabel?: string;
63
+ /** footer 확인 버튼 라벨 (기본 "확인") */
64
+ confirmLabel?: string;
65
+
66
+ minDate?: Date;
67
+ maxDate?: Date;
68
+ /** 주 시작 요일 (0=일 ... 6=토). 기본 0(일). */
69
+ weekStartsOn?: number;
70
+
71
+ /** 트리거 인풋 표시 형식.
72
+ * - "short": 25-01-15
73
+ * - "iso": 2025-01-15 (기본)
74
+ * - "medium": 2025년 1월 15일
75
+ * - "long": 2025년 1월 15일 (수) */
76
+ dateFormat?: DateFormat;
77
+
78
+ placeholder?: string;
79
+ disabled?: boolean;
80
+ size?: InputSize;
81
+ /** popover 없이 인라인으로 노출. 트리거 input이 사라짐. */
82
+ inline?: boolean;
83
+ /** 인풋 커스텀 렌더링. asChild 형태로 rendered element를 감쌈. */
84
+ trigger?: ReactNode;
85
+
86
+ className?: string;
87
+ id?: string;
88
+ "aria-label"?: string;
89
+ }
90
+
91
+ function displayValue(props: {
92
+ mode: DatePickerMode;
93
+ value: Date | null;
94
+ from: Date | null;
95
+ to: Date | null;
96
+ withTime: boolean;
97
+ dateFormat: DateFormat;
98
+ }): string {
99
+ const { mode, value, from, to, withTime, dateFormat } = props;
100
+ const fmt = (d: Date) =>
101
+ withTime ? `${formatDate(d, dateFormat)} ${formatHM(d)}` : formatDate(d, dateFormat);
102
+ if (mode === "single") {
103
+ if (!value) return "";
104
+ return fmt(value);
105
+ }
106
+ const f = from ? fmt(from) : "";
107
+ const t = to ? fmt(to) : "";
108
+ if (!f && !t) return "";
109
+ return `${f} ~ ${t}`;
110
+ }
111
+
112
+ export const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(
113
+ (
114
+ {
115
+ mode = "single",
116
+ months = mode === "range" ? 2 : 1,
117
+ value = null,
118
+ rangeValue = { from: null, to: null },
119
+ onChange,
120
+ onRangeChange,
121
+ withTime = false,
122
+ withPresets = false,
123
+ presets,
124
+ withFooter,
125
+ onReset,
126
+ onConfirm,
127
+ resetLabel = "재설정",
128
+ confirmLabel = "확인",
129
+ minDate,
130
+ maxDate,
131
+ weekStartsOn = 0,
132
+ dateFormat = "iso",
133
+ placeholder = "날짜 선택",
134
+ disabled = false,
135
+ size = "md",
136
+ inline = false,
137
+ trigger,
138
+ className,
139
+ id,
140
+ "aria-label": ariaLabel,
141
+ },
142
+ ref
143
+ ) => {
144
+ const [open, setOpen] = useState(false);
145
+ const anchorDate = value ?? rangeValue.from ?? new Date();
146
+ const [viewMonth, setViewMonth] = useState<Date>(startOfMonth(anchorDate));
147
+ const [hoverDate, setHoverDate] = useState<Date | null>(null);
148
+ const [rangeInProgress, setRangeInProgress] = useState(false);
149
+
150
+ const activePresets = useMemo(() => {
151
+ if (presets) return presets;
152
+ return mode === "single" ? defaultSinglePresets : defaultRangePresets;
153
+ }, [presets, mode]);
154
+
155
+ const handleSelectDay = useCallback(
156
+ (day: Date) => {
157
+ if (mode === "single") {
158
+ const next = withTime && value
159
+ ? combineDateTime(day, value.getHours(), value.getMinutes())
160
+ : day;
161
+ onChange?.(next);
162
+ if (!withTime && !(withFooter ?? withTime)) setOpen(false);
163
+ return;
164
+ }
165
+
166
+ // range
167
+ const { from, to } = rangeValue;
168
+ const firstPick = !from || (from && to) || rangeInProgress === false;
169
+ if (firstPick) {
170
+ const nextFrom = withTime && from
171
+ ? combineDateTime(day, from.getHours(), from.getMinutes())
172
+ : day;
173
+ onRangeChange?.({ from: nextFrom, to: null });
174
+ setRangeInProgress(true);
175
+ setHoverDate(day);
176
+ return;
177
+ }
178
+ // second pick
179
+ let start = from!;
180
+ let end = day;
181
+ if (end.getTime() < start.getTime()) [start, end] = [end, start];
182
+ if (withTime) {
183
+ if (from) start = combineDateTime(start, from.getHours(), from.getMinutes());
184
+ if (to) end = combineDateTime(end, to.getHours(), to.getMinutes());
185
+ }
186
+ onRangeChange?.({ from: start, to: end });
187
+ setRangeInProgress(false);
188
+ setHoverDate(null);
189
+ if (!withTime && !(withFooter ?? withTime)) setOpen(false);
190
+ },
191
+ [mode, value, rangeValue, withTime, withFooter, onChange, onRangeChange, rangeInProgress]
192
+ );
193
+
194
+ const handleTimeChange = useCallback(
195
+ (which: "single" | "from" | "to", t: { hours: number; minutes: number }) => {
196
+ if (which === "single") {
197
+ const base = value ?? new Date();
198
+ onChange?.(combineDateTime(base, t.hours, t.minutes));
199
+ return;
200
+ }
201
+ const { from, to } = rangeValue;
202
+ if (which === "from") {
203
+ const base = from ?? new Date();
204
+ onRangeChange?.({ from: combineDateTime(base, t.hours, t.minutes), to });
205
+ } else {
206
+ const base = to ?? from ?? new Date();
207
+ onRangeChange?.({ from, to: combineDateTime(base, t.hours, t.minutes) });
208
+ }
209
+ },
210
+ [value, rangeValue, onChange, onRangeChange]
211
+ );
212
+
213
+ const applyPreset = useCallback(
214
+ (preset: SinglePreset | RangePreset) => {
215
+ if (mode === "single") {
216
+ const d = (preset as SinglePreset).getValue();
217
+ onChange?.(d);
218
+ setViewMonth(startOfMonth(d));
219
+ if (!withTime && !(withFooter ?? withTime)) setOpen(false);
220
+ } else {
221
+ const { from, to } = (preset as RangePreset).getValue();
222
+ onRangeChange?.({ from, to });
223
+ setViewMonth(startOfMonth(from));
224
+ setRangeInProgress(false);
225
+ if (!withTime && !(withFooter ?? withTime)) setOpen(false);
226
+ }
227
+ },
228
+ [mode, withTime, withFooter, onChange, onRangeChange]
229
+ );
230
+
231
+ const display = displayValue({
232
+ mode,
233
+ value,
234
+ from: rangeValue.from,
235
+ to: rangeValue.to,
236
+ withTime,
237
+ dateFormat,
238
+ });
239
+
240
+ const showFooter = withFooter ?? withTime;
241
+
242
+ const handleReset = () => {
243
+ if (mode === "single") onChange?.(null);
244
+ else onRangeChange?.({ from: null, to: null });
245
+ setRangeInProgress(false);
246
+ onReset?.();
247
+ };
248
+
249
+ const handleConfirm = () => {
250
+ onConfirm?.();
251
+ if (!inline) setOpen(false);
252
+ };
253
+
254
+ const panel = (
255
+ <div className={styles.panel} data-with-presets={withPresets || undefined}>
256
+ {withPresets && (
257
+ <div className={styles.presets} role="listbox" aria-label="빠른 선택">
258
+ {activePresets.map((p) => (
259
+ <Button
260
+ key={p.label}
261
+ size="sm"
262
+ variant="outline"
263
+ className={styles.presetChip}
264
+ onClick={() => applyPreset(p)}
265
+ >
266
+ {p.label}
267
+ </Button>
268
+ ))}
269
+ </div>
270
+ )}
271
+
272
+ <div className={styles.panelMain} data-with-time={withTime || undefined}>
273
+ <Calendar
274
+ mode={mode}
275
+ months={months}
276
+ viewMonth={viewMonth}
277
+ onViewMonthChange={setViewMonth}
278
+ selectedFrom={mode === "single" ? value : rangeValue.from}
279
+ selectedTo={mode === "single" ? null : rangeValue.to}
280
+ hoverDate={hoverDate}
281
+ onHoverDate={setHoverDate}
282
+ rangeInProgress={rangeInProgress}
283
+ onSelectDay={handleSelectDay}
284
+ minDate={minDate}
285
+ maxDate={maxDate}
286
+ weekStartsOn={weekStartsOn}
287
+ />
288
+
289
+ {withTime && (
290
+ <div className={styles.timeArea} data-mode={mode}>
291
+ {mode === "single" ? (
292
+ <TimeSelector
293
+ value={
294
+ value
295
+ ? { hours: value.getHours(), minutes: value.getMinutes() }
296
+ : null
297
+ }
298
+ onChange={(t) => handleTimeChange("single", t)}
299
+ />
300
+ ) : (
301
+ <>
302
+ <TimeSelector
303
+ label="시작 시간"
304
+ value={
305
+ rangeValue.from
306
+ ? { hours: rangeValue.from.getHours(), minutes: rangeValue.from.getMinutes() }
307
+ : null
308
+ }
309
+ onChange={(t) => handleTimeChange("from", t)}
310
+ />
311
+ <TimeSelector
312
+ label="종료 시간"
313
+ value={
314
+ rangeValue.to
315
+ ? { hours: rangeValue.to.getHours(), minutes: rangeValue.to.getMinutes() }
316
+ : null
317
+ }
318
+ onChange={(t) => handleTimeChange("to", t)}
319
+ />
320
+ </>
321
+ )}
322
+ </div>
323
+ )}
324
+ </div>
325
+
326
+ {showFooter && (
327
+ <div className={styles.footer}>
328
+ <Button variant="ghost" size="sm" onClick={handleReset}>
329
+ {resetLabel}
330
+ </Button>
331
+ <Button variant="primary" size="sm" onClick={handleConfirm}>
332
+ {confirmLabel}
333
+ </Button>
334
+ </div>
335
+ )}
336
+ </div>
337
+ );
338
+
339
+ if (inline) {
340
+ return (
341
+ <div className={[styles.inlineWrap, className ?? ""].filter(Boolean).join(" ")} id={id}>
342
+ {panel}
343
+ </div>
344
+ );
345
+ }
346
+
347
+ const calendarIcon = <Icon name="calendar-blank" />;
348
+
349
+ return (
350
+ <RadixPopover.Root open={open} onOpenChange={setOpen}>
351
+ <RadixPopover.Trigger asChild>
352
+ {trigger ?? (
353
+ <Input
354
+ ref={ref}
355
+ id={id}
356
+ size={size}
357
+ disabled={disabled}
358
+ value={display}
359
+ placeholder={placeholder}
360
+ rightIcon={calendarIcon}
361
+ className={[styles.triggerInput, className ?? ""].filter(Boolean).join(" ")}
362
+ aria-label={ariaLabel ?? placeholder}
363
+ aria-haspopup="dialog"
364
+ role="combobox"
365
+ aria-expanded={open}
366
+ style={{ cursor: "pointer" }}
367
+ />
368
+ )}
369
+ </RadixPopover.Trigger>
370
+ <RadixPopover.Portal>
371
+ <RadixPopover.Content
372
+ className={[styles.popoverContent, "m-float"].join(" ")}
373
+ sideOffset={6}
374
+ align="start"
375
+ collisionPadding={8}
376
+ >
377
+ {panel}
378
+ </RadixPopover.Content>
379
+ </RadixPopover.Portal>
380
+ </RadixPopover.Root>
381
+ );
382
+ }
383
+ );
384
+
385
+ DatePicker.displayName = "DatePicker";
386
+
387
+ export type { SinglePreset, RangePreset };
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { MenuItem } from "../MenuItem";
5
+ import styles from "./DatePicker.module.css";
6
+
7
+ function formatHM12(h: number, m: number): string {
8
+ const suffix = h < 12 ? "AM" : "PM";
9
+ const hour12 = h % 12 === 0 ? 12 : h % 12;
10
+ return `${hour12}:${String(m).padStart(2, "0")} ${suffix}`;
11
+ }
12
+
13
+ export interface TimeSelectorProps {
14
+ /** null이면 시간이 아직 미설정. */
15
+ value: { hours: number; minutes: number } | null;
16
+ onChange: (v: { hours: number; minutes: number }) => void;
17
+ /** 표시 시작 시(포함). 기본 0 */
18
+ startHour?: number;
19
+ /** 표시 종료 시(제외). 기본 24 */
20
+ endHour?: number;
21
+ /** 라벨 (from/to 구분 등) */
22
+ label?: string;
23
+ }
24
+
25
+ export function TimeSelector({
26
+ value,
27
+ onChange,
28
+ startHour = 0,
29
+ endHour = 24,
30
+ label,
31
+ }: TimeSelectorProps) {
32
+ const slots = useMemo(() => {
33
+ const list: { h: number; m: number }[] = [];
34
+ for (let h = startHour; h < endHour; h += 1) {
35
+ list.push({ h, m: 0 });
36
+ list.push({ h, m: 30 });
37
+ }
38
+ return list;
39
+ }, [startHour, endHour]);
40
+
41
+ return (
42
+ <div className={styles.timeSection}>
43
+ {label && <div className={styles.timeLabel}>{label}</div>}
44
+ <div
45
+ className={`${styles.timeList} scroll-fade-y`}
46
+ role="listbox"
47
+ aria-label="시간 선택"
48
+ >
49
+ <div className={styles.timeListTrack}>
50
+ {slots.map(({ h, m }) => {
51
+ const active = !!(value && value.hours === h && value.minutes === m);
52
+ return (
53
+ <MenuItem
54
+ key={`${h}-${m}`}
55
+ size="sm"
56
+ active={active}
57
+ role="option"
58
+ aria-selected={active}
59
+ onClick={() => onChange({ hours: h, minutes: m })}
60
+ >
61
+ {formatHM12(h, m)}
62
+ </MenuItem>
63
+ );
64
+ })}
65
+ </div>
66
+ </div>
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1,8 @@
1
+ export { DatePicker } from "./DatePicker";
2
+ export type {
3
+ DatePickerProps,
4
+ DatePickerMode,
5
+ SinglePreset,
6
+ RangePreset,
7
+ } from "./DatePicker";
8
+ export { defaultSinglePresets, defaultRangePresets } from "./presets";
@@ -0,0 +1,80 @@
1
+ import { addDays, startOfDay, endOfDay } from "./utils";
2
+
3
+ export interface SinglePreset {
4
+ label: string;
5
+ getValue: () => Date;
6
+ }
7
+
8
+ export interface RangePreset {
9
+ label: string;
10
+ getValue: () => { from: Date; to: Date };
11
+ }
12
+
13
+ /** 요일 index: 0=일 ... 6=토 */
14
+ function nextWeekday(from: Date, weekday: number): Date {
15
+ const d = startOfDay(from);
16
+ const diff = (weekday - d.getDay() + 7) % 7 || 7;
17
+ return addDays(d, diff);
18
+ }
19
+
20
+ export const defaultSinglePresets: SinglePreset[] = [
21
+ { label: "오늘", getValue: () => startOfDay(new Date()) },
22
+ { label: "내일", getValue: () => addDays(startOfDay(new Date()), 1) },
23
+ { label: "다음 월요일", getValue: () => nextWeekday(new Date(), 1) },
24
+ ];
25
+
26
+ export const defaultRangePresets: RangePreset[] = [
27
+ {
28
+ label: "오늘",
29
+ getValue: () => {
30
+ const now = new Date();
31
+ return { from: startOfDay(now), to: endOfDay(now) };
32
+ },
33
+ },
34
+ {
35
+ label: "어제",
36
+ getValue: () => {
37
+ const y = addDays(startOfDay(new Date()), -1);
38
+ return { from: y, to: endOfDay(y) };
39
+ },
40
+ },
41
+ {
42
+ label: "이번 주",
43
+ getValue: () => {
44
+ const today = startOfDay(new Date());
45
+ const day = today.getDay();
46
+ const monday = addDays(today, day === 0 ? -6 : 1 - day);
47
+ return { from: monday, to: endOfDay(addDays(monday, 6)) };
48
+ },
49
+ },
50
+ {
51
+ label: "지난 7일",
52
+ getValue: () => ({
53
+ from: addDays(startOfDay(new Date()), -6),
54
+ to: endOfDay(new Date()),
55
+ }),
56
+ },
57
+ {
58
+ label: "이번 달",
59
+ getValue: () => {
60
+ const now = new Date();
61
+ const from = new Date(now.getFullYear(), now.getMonth(), 1);
62
+ const to = endOfDay(new Date(now.getFullYear(), now.getMonth() + 1, 0));
63
+ return { from, to };
64
+ },
65
+ },
66
+ {
67
+ label: "지난 30일",
68
+ getValue: () => ({
69
+ from: addDays(startOfDay(new Date()), -29),
70
+ to: endOfDay(new Date()),
71
+ }),
72
+ },
73
+ {
74
+ label: "올해",
75
+ getValue: () => {
76
+ const y = new Date().getFullYear();
77
+ return { from: new Date(y, 0, 1), to: endOfDay(new Date(y, 11, 31)) };
78
+ },
79
+ },
80
+ ];
@@ -0,0 +1,148 @@
1
+ export type DateValue = Date | null;
2
+ export type DateRange = { from: Date | null; to: Date | null };
3
+
4
+ export const MS_DAY = 86_400_000;
5
+
6
+ export function startOfDay(d: Date): Date {
7
+ const x = new Date(d);
8
+ x.setHours(0, 0, 0, 0);
9
+ return x;
10
+ }
11
+
12
+ export function endOfDay(d: Date): Date {
13
+ const x = new Date(d);
14
+ x.setHours(23, 59, 59, 999);
15
+ return x;
16
+ }
17
+
18
+ export function isSameDay(a: Date | null, b: Date | null): boolean {
19
+ if (!a || !b) return false;
20
+ return (
21
+ a.getFullYear() === b.getFullYear() &&
22
+ a.getMonth() === b.getMonth() &&
23
+ a.getDate() === b.getDate()
24
+ );
25
+ }
26
+
27
+ export function isSameMonth(a: Date, b: Date): boolean {
28
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
29
+ }
30
+
31
+ export function addMonths(d: Date, n: number): Date {
32
+ const x = new Date(d);
33
+ x.setMonth(x.getMonth() + n);
34
+ return x;
35
+ }
36
+
37
+ export function addDays(d: Date, n: number): Date {
38
+ const x = new Date(d);
39
+ x.setDate(x.getDate() + n);
40
+ return x;
41
+ }
42
+
43
+ export function startOfMonth(d: Date): Date {
44
+ return new Date(d.getFullYear(), d.getMonth(), 1);
45
+ }
46
+
47
+ export function endOfMonth(d: Date): Date {
48
+ return new Date(d.getFullYear(), d.getMonth() + 1, 0);
49
+ }
50
+
51
+ export function startOfWeek(d: Date, weekStartsOn: number): Date {
52
+ const x = startOfDay(d);
53
+ const day = x.getDay();
54
+ const diff = (day - weekStartsOn + 7) % 7;
55
+ return addDays(x, -diff);
56
+ }
57
+
58
+ export function isBefore(a: Date, b: Date): boolean {
59
+ return a.getTime() < b.getTime();
60
+ }
61
+
62
+ export function isAfter(a: Date, b: Date): boolean {
63
+ return a.getTime() > b.getTime();
64
+ }
65
+
66
+ export function isWithin(d: Date, from: Date, to: Date): boolean {
67
+ const t = d.getTime();
68
+ return t >= from.getTime() && t <= to.getTime();
69
+ }
70
+
71
+ export function clampDate(d: Date, min?: Date, max?: Date): Date {
72
+ if (min && isBefore(d, min)) return min;
73
+ if (max && isAfter(d, max)) return max;
74
+ return d;
75
+ }
76
+
77
+ /** yyyy-MM-dd */
78
+ export function formatISODate(d: Date): string {
79
+ const y = d.getFullYear();
80
+ const m = String(d.getMonth() + 1).padStart(2, "0");
81
+ const day = String(d.getDate()).padStart(2, "0");
82
+ return `${y}-${m}-${day}`;
83
+ }
84
+
85
+ /** HH:mm */
86
+ export function formatHM(d: Date): string {
87
+ const h = String(d.getHours()).padStart(2, "0");
88
+ const m = String(d.getMinutes()).padStart(2, "0");
89
+ return `${h}:${m}`;
90
+ }
91
+
92
+ /** 표시 포맷.
93
+ * - "short": 25-01-15
94
+ * - "iso": 2025-01-15 (기본)
95
+ * - "medium": 2025년 1월 15일
96
+ * - "long": 2025년 1월 15일 (수)
97
+ */
98
+ export type DateFormat = "short" | "iso" | "medium" | "long";
99
+
100
+ const KO_WEEKDAY = ["일", "월", "화", "수", "목", "금", "토"] as const;
101
+
102
+ export function formatDate(d: Date, format: DateFormat = "iso"): string {
103
+ const y = d.getFullYear();
104
+ const m = d.getMonth() + 1;
105
+ const day = d.getDate();
106
+ const mm = String(m).padStart(2, "0");
107
+ const dd = String(day).padStart(2, "0");
108
+ const yy = String(y).slice(-2);
109
+ const w = KO_WEEKDAY[d.getDay()];
110
+
111
+ switch (format) {
112
+ case "short":
113
+ return `${yy}-${mm}-${dd}`;
114
+ case "medium":
115
+ return `${y}년 ${m}월 ${day}일`;
116
+ case "long":
117
+ return `${y}년 ${m}월 ${day}일 (${w})`;
118
+ case "iso":
119
+ default:
120
+ return `${y}-${mm}-${dd}`;
121
+ }
122
+ }
123
+
124
+ export function combineDateTime(date: Date, hours: number, minutes: number): Date {
125
+ const x = new Date(date);
126
+ x.setHours(hours, minutes, 0, 0);
127
+ return x;
128
+ }
129
+
130
+ /** month의 6행 x 7열 그리드에 들어갈 42일 배열 */
131
+ export function buildMonthGrid(month: Date, weekStartsOn: number): Date[] {
132
+ const first = startOfMonth(month);
133
+ const last = endOfMonth(month);
134
+ const gridStart = startOfWeek(first, weekStartsOn);
135
+ const totalDays = Math.ceil(
136
+ (last.getTime() - gridStart.getTime()) / (24 * 60 * 60 * 1000)
137
+ ) + 1;
138
+ const weeks = Math.ceil(totalDays / 7);
139
+ const cells: Date[] = [];
140
+ for (let i = 0; i < weeks * 7; i += 1) cells.push(addDays(gridStart, i));
141
+ return cells;
142
+ }
143
+
144
+ export const WEEKDAY_LABELS_KO = ["일", "월", "화", "수", "목", "금", "토"];
145
+ export const MONTH_LABELS_KO = [
146
+ "1월", "2월", "3월", "4월", "5월", "6월",
147
+ "7월", "8월", "9월", "10월", "11월", "12월",
148
+ ];