@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,821 @@
1
+ "use client";
2
+
3
+ import * as RadixPopover from "@radix-ui/react-popover";
4
+ import {
5
+ forwardRef,
6
+ useCallback,
7
+ useEffect,
8
+ useId,
9
+ useLayoutEffect,
10
+ useRef,
11
+ useState,
12
+ type KeyboardEvent as ReactKeyboardEvent,
13
+ } from "react";
14
+
15
+ import inputStyles from "../Input/Input.module.css";
16
+ import { Icon } from "../Icon";
17
+ import styles from "./TimePicker.module.css";
18
+ import {
19
+ clampStep,
20
+ isSameTime,
21
+ toTotalMinutes,
22
+ type TimeFormat,
23
+ type TimeValue,
24
+ } from "./utils";
25
+
26
+ export type TimePickerSize = "sm" | "md" | "lg";
27
+ export type TimePickerVariant = "outline" | "filled" | "line";
28
+
29
+ export interface TimePickerProps {
30
+ value?: TimeValue | null;
31
+ onChange?: (v: TimeValue | null) => void;
32
+
33
+ /** 리스트 간격(분). 5 | 10 | 15 | 30 | 60. 기본 30. */
34
+ step?: number;
35
+ /** 표시 포맷. "24h"(기본) 또는 "12h". */
36
+ format?: TimeFormat;
37
+
38
+ /** "HH:MM" (24h). 포함, 이 시간 이전은 비활성. */
39
+ minTime?: string;
40
+ /** "HH:MM" (24h). 포함, 이 시간 이후는 비활성. */
41
+ maxTime?: string;
42
+
43
+ disabled?: boolean;
44
+ size?: TimePickerSize;
45
+ variant?: TimePickerVariant;
46
+
47
+ /** popover 없이 패널만. */
48
+ inline?: boolean;
49
+
50
+ /** 패널 레이아웃.
51
+ * - "columns"(기본): 시 | 분 세로 스크롤 2열. popover·inline 둘 다 사용.
52
+ * - "grid": step 간격 전체 슬롯을 격자로 깔아놓음. inline 전제, 예약 슬롯 톤. */
53
+ panelLayout?: "columns" | "grid";
54
+
55
+ /** panelLayout="grid"에서 그리드 열 수. 기본 6. */
56
+ gridCols?: 4 | 6 | 8;
57
+
58
+ /** panelLayout="grid"에서 오전/오후 섹션 분리. true면 AM/PM 두 섹션으로 자름. */
59
+ splitMeridiem?: boolean;
60
+
61
+ /** 상단 상대 시간 칩 노출. ["now", "+15m", "+30m", "+1h"]. 기본 false. */
62
+ quickOptions?: QuickOption[] | boolean;
63
+
64
+ className?: string;
65
+ id?: string;
66
+ "aria-label"?: string;
67
+ }
68
+
69
+ type QuickOption =
70
+ | { label: string; minutes: number }
71
+ | "now";
72
+
73
+ const DEFAULT_QUICK: QuickOption[] = [
74
+ "now",
75
+ { label: "+15m", minutes: 15 },
76
+ { label: "+30m", minutes: 30 },
77
+ { label: "+1h", minutes: 60 },
78
+ ];
79
+
80
+ function parseHHMM(s: string | undefined): number | null {
81
+ if (!s) return null;
82
+ const m = s.match(/^(\d{1,2}):(\d{1,2})$/);
83
+ if (!m) return null;
84
+ const h = Number(m[1]);
85
+ const mi = Number(m[2]);
86
+ if (h < 0 || h > 23 || mi < 0 || mi > 59) return null;
87
+ return h * 60 + mi;
88
+ }
89
+
90
+ function clampToRange(
91
+ t: TimeValue,
92
+ minMin: number | null,
93
+ maxMin: number | null
94
+ ): TimeValue {
95
+ const total = toTotalMinutes(t);
96
+ if (minMin != null && total < minMin) {
97
+ return { hours: Math.floor(minMin / 60), minutes: minMin % 60 };
98
+ }
99
+ if (maxMin != null && total > maxMin) {
100
+ return { hours: Math.floor(maxMin / 60), minutes: maxMin % 60 };
101
+ }
102
+ return t;
103
+ }
104
+
105
+ /* 세그먼트 입력 ─────────────────────────────────────────────
106
+ 한 세그먼트는 2자리 숫자(또는 AM/PM 토글). focus 받으면 다음 숫자키 입력에 따라
107
+ buffer를 쌓고, 2자리 완성 or 범위 초과 시 다음 세그먼트로 이동. */
108
+
109
+ type SegmentKind = "hour" | "minute" | "meridiem";
110
+
111
+ interface SegmentButtonProps {
112
+ kind: SegmentKind;
113
+ value: number | null; // hour: 0-23 (24h) or 1-12 (12h); minute: 0-59; meridiem: 0=AM 1=PM
114
+ display: string;
115
+ placeholder: string;
116
+ focused: boolean;
117
+ disabled?: boolean;
118
+ onFocus: () => void;
119
+ onKey: (e: ReactKeyboardEvent<HTMLButtonElement>) => void;
120
+ "aria-label": string;
121
+ }
122
+
123
+ const SegmentButton = forwardRef<HTMLButtonElement, SegmentButtonProps>(
124
+ function SegmentButton(
125
+ { value, display, placeholder, focused, disabled, onFocus, onKey, ...rest },
126
+ ref
127
+ ) {
128
+ const empty = value == null;
129
+ return (
130
+ <button
131
+ ref={ref}
132
+ type="button"
133
+ role="spinbutton"
134
+ className={[
135
+ styles.segment,
136
+ empty ? styles.segmentPlaceholder : "",
137
+ rest["aria-label"].includes("오전") ? styles.segmentMeridiem : "",
138
+ ]
139
+ .filter(Boolean)
140
+ .join(" ")}
141
+ data-focused={focused || undefined}
142
+ disabled={disabled}
143
+ onFocus={onFocus}
144
+ onKeyDown={onKey}
145
+ tabIndex={disabled ? -1 : 0}
146
+ aria-label={rest["aria-label"]}
147
+ >
148
+ {empty ? placeholder : display}
149
+ </button>
150
+ );
151
+ }
152
+ );
153
+
154
+ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
155
+ (
156
+ {
157
+ value = null,
158
+ onChange,
159
+ step = 30,
160
+ format = "24h",
161
+ minTime,
162
+ maxTime,
163
+ disabled = false,
164
+ size = "md",
165
+ variant = "outline",
166
+ inline = false,
167
+ panelLayout = "columns",
168
+ gridCols = 6,
169
+ splitMeridiem = false,
170
+ quickOptions = false,
171
+ className,
172
+ id,
173
+ "aria-label": ariaLabel,
174
+ },
175
+ ref
176
+ ) => {
177
+ const [open, setOpen] = useState(false);
178
+ const stepMinutes = clampStep(step);
179
+ const minMin = parseHHMM(minTime);
180
+ const maxMin = parseHHMM(maxTime);
181
+ const is12h = format === "12h";
182
+
183
+ const autoId = useId();
184
+ const triggerId = id ?? autoId;
185
+
186
+ /* 세그먼트 포커스 상태 */
187
+ const [focusedSeg, setFocusedSeg] = useState<SegmentKind | null>(null);
188
+ const hourRef = useRef<HTMLButtonElement>(null);
189
+ const minuteRef = useRef<HTMLButtonElement>(null);
190
+ const meridiemRef = useRef<HTMLButtonElement>(null);
191
+
192
+ /* 숫자 타이핑 버퍼 — 세그먼트별로 2자리 완성 추적. */
193
+ const bufferRef = useRef<string>("");
194
+ useEffect(() => {
195
+ bufferRef.current = "";
196
+ }, [focusedSeg]);
197
+
198
+ const commit = useCallback(
199
+ (next: TimeValue | null) => {
200
+ if (!next) {
201
+ onChange?.(null);
202
+ return;
203
+ }
204
+ onChange?.(clampToRange(next, minMin, maxMin));
205
+ },
206
+ [onChange, minMin, maxMin]
207
+ );
208
+
209
+ const focusSeg = (seg: SegmentKind) => {
210
+ setFocusedSeg(seg);
211
+ bufferRef.current = "";
212
+ if (seg === "hour") hourRef.current?.focus();
213
+ else if (seg === "minute") minuteRef.current?.focus();
214
+ else meridiemRef.current?.focus();
215
+ };
216
+
217
+ /* hour/minute 세그먼트 키보드 처리. */
218
+ const handleSegmentKey = (seg: "hour" | "minute") =>
219
+ (e: ReactKeyboardEvent<HTMLButtonElement>) => {
220
+ if (disabled) return;
221
+ const key = e.key;
222
+
223
+ if (/^[0-9]$/.test(key)) {
224
+ e.preventDefault();
225
+ const prevBuf = bufferRef.current;
226
+ const nextBuf = (prevBuf + key).slice(-2);
227
+ bufferRef.current = nextBuf;
228
+ const n = Number(nextBuf);
229
+
230
+ const cur = value ?? { hours: 0, minutes: 0 };
231
+ if (seg === "hour") {
232
+ const maxH = is12h ? 12 : 23;
233
+ // 두 자리면 그대로, 한 자리인데 다음 숫자 추가 시 10초과면 한 자리로 즉시 적용
234
+ let h = n;
235
+ if (prevBuf.length === 0 && Number(key) > (is12h ? 1 : 2)) {
236
+ h = Number(key);
237
+ bufferRef.current = "";
238
+ if (is12h) {
239
+ const mer = cur.hours >= 12 ? 12 : 0;
240
+ const h24 = (h % 12) + mer;
241
+ commit({ hours: h24, minutes: cur.minutes });
242
+ } else {
243
+ commit({ hours: h, minutes: cur.minutes });
244
+ }
245
+ focusSeg("minute");
246
+ return;
247
+ }
248
+ if (h > maxH) h = Number(key);
249
+ if (is12h) {
250
+ const mer = cur.hours >= 12 ? 12 : 0;
251
+ const h24 = (h % 12) + mer;
252
+ commit({ hours: h24, minutes: cur.minutes });
253
+ } else {
254
+ commit({ hours: h, minutes: cur.minutes });
255
+ }
256
+ if (bufferRef.current.length === 2) focusSeg("minute");
257
+ return;
258
+ }
259
+
260
+ // minute
261
+ let m = n;
262
+ if (prevBuf.length === 0 && Number(key) > 5) {
263
+ m = Number(key);
264
+ bufferRef.current = "";
265
+ commit({ hours: cur.hours, minutes: m });
266
+ if (is12h) focusSeg("meridiem");
267
+ return;
268
+ }
269
+ if (m > 59) m = Number(key);
270
+ commit({ hours: cur.hours, minutes: m });
271
+ if (bufferRef.current.length === 2 && is12h) focusSeg("meridiem");
272
+ return;
273
+ }
274
+
275
+ if (key === "ArrowUp" || key === "ArrowDown") {
276
+ e.preventDefault();
277
+ const dir = key === "ArrowUp" ? 1 : -1;
278
+ const cur = value ?? { hours: 0, minutes: 0 };
279
+ if (seg === "hour") {
280
+ const nextH = ((cur.hours + dir) % 24 + 24) % 24;
281
+ commit({ hours: nextH, minutes: cur.minutes });
282
+ } else {
283
+ const stepM = stepMinutes;
284
+ const total = cur.hours * 60 + cur.minutes + dir * stepM;
285
+ const wrapped = ((total % (24 * 60)) + 24 * 60) % (24 * 60);
286
+ commit({
287
+ hours: Math.floor(wrapped / 60),
288
+ minutes: wrapped % 60,
289
+ });
290
+ }
291
+ return;
292
+ }
293
+
294
+ if (key === "ArrowLeft") {
295
+ e.preventDefault();
296
+ if (seg === "minute") focusSeg("hour");
297
+ else if (seg === "hour" && is12h) focusSeg("meridiem");
298
+ return;
299
+ }
300
+ if (key === "ArrowRight") {
301
+ e.preventDefault();
302
+ if (seg === "hour") focusSeg("minute");
303
+ else if (seg === "minute" && is12h) focusSeg("meridiem");
304
+ return;
305
+ }
306
+
307
+ if (key === "Backspace" || key === "Delete") {
308
+ e.preventDefault();
309
+ commit(null);
310
+ bufferRef.current = "";
311
+ return;
312
+ }
313
+
314
+ if (key === "Enter" || key === " ") {
315
+ e.preventDefault();
316
+ if (!inline) setOpen((o) => !o);
317
+ return;
318
+ }
319
+
320
+ if (key === "Escape" && open) {
321
+ e.preventDefault();
322
+ setOpen(false);
323
+ }
324
+ };
325
+
326
+ const handleMeridiemKey = (e: ReactKeyboardEvent<HTMLButtonElement>) => {
327
+ if (disabled) return;
328
+ const key = e.key;
329
+ if (
330
+ key === "a" ||
331
+ key === "A" ||
332
+ key === "p" ||
333
+ key === "P" ||
334
+ key === "ArrowUp" ||
335
+ key === "ArrowDown" ||
336
+ key === " "
337
+ ) {
338
+ e.preventDefault();
339
+ const cur = value ?? { hours: 0, minutes: 0 };
340
+ const toAM = key === "a" || key === "A";
341
+ const toPM = key === "p" || key === "P";
342
+ let nextMer: "AM" | "PM";
343
+ if (toAM) nextMer = "AM";
344
+ else if (toPM) nextMer = "PM";
345
+ else nextMer = cur.hours >= 12 ? "AM" : "PM";
346
+ const h12 = cur.hours % 12;
347
+ const h24 = nextMer === "PM" ? h12 + 12 : h12;
348
+ commit({ hours: h24, minutes: cur.minutes });
349
+ return;
350
+ }
351
+ if (key === "ArrowLeft") {
352
+ e.preventDefault();
353
+ focusSeg("minute");
354
+ return;
355
+ }
356
+ if (key === "Backspace" || key === "Delete") {
357
+ e.preventDefault();
358
+ commit(null);
359
+ return;
360
+ }
361
+ if (key === "Enter") {
362
+ e.preventDefault();
363
+ if (!inline) setOpen((o) => !o);
364
+ }
365
+ };
366
+
367
+ /* 표시용 ─────────────────────────────────────────────── */
368
+ const hourDisplay = (() => {
369
+ if (!value) return "";
370
+ if (is12h) {
371
+ const h12 = value.hours % 12 === 0 ? 12 : value.hours % 12;
372
+ return String(h12).padStart(2, "0");
373
+ }
374
+ return String(value.hours).padStart(2, "0");
375
+ })();
376
+ const minuteDisplay = value ? String(value.minutes).padStart(2, "0") : "";
377
+ const meridiemDisplay = value ? (value.hours < 12 ? "am" : "pm") : "";
378
+
379
+ /* 그리드 데이터 ─────────────────────────────────────── */
380
+ const hours = (() => {
381
+ if (is12h) return Array.from({ length: 12 }, (_, i) => i + 1);
382
+ return Array.from({ length: 24 }, (_, i) => i);
383
+ })();
384
+ const minutes = Array.from(
385
+ { length: Math.floor(60 / stepMinutes) },
386
+ (_, i) => i * stepMinutes
387
+ );
388
+
389
+ const activeMeridiem: "AM" | "PM" | null = value
390
+ ? value.hours >= 12
391
+ ? "PM"
392
+ : "AM"
393
+ : null;
394
+
395
+ /* 그리드 셀 선택 */
396
+ const selectHour = (h12or24: number) => {
397
+ const cur = value ?? { hours: 0, minutes: 0 };
398
+ let h24: number;
399
+ if (is12h) {
400
+ const h12 = h12or24 % 12;
401
+ const mer = activeMeridiem ?? (cur.hours >= 12 ? "PM" : "AM");
402
+ h24 = mer === "PM" ? h12 + 12 : h12;
403
+ } else {
404
+ h24 = h12or24;
405
+ }
406
+ commit({ hours: h24, minutes: cur.minutes });
407
+ // hour 선택 후 minute 열로 포커스 유도
408
+ setFocusedSeg("minute");
409
+ };
410
+ const selectMinute = (m: number) => {
411
+ const cur = value ?? { hours: 0, minutes: 0 };
412
+ commit({ hours: cur.hours, minutes: m });
413
+ if (!inline) setOpen(false);
414
+ };
415
+ const selectMeridiem = (mer: "AM" | "PM") => {
416
+ const cur = value ?? { hours: 0, minutes: 0 };
417
+ const h12 = cur.hours % 12;
418
+ const h24 = mer === "PM" ? h12 + 12 : h12;
419
+ commit({ hours: h24, minutes: cur.minutes });
420
+ };
421
+
422
+ /* 셀 비활성 판정 */
423
+ const hourDisabled = (hourVal: number): boolean => {
424
+ // 24h: hourVal 그대로, 12h: 현재 meridiem과 결합해 모든 분이 범위 밖일 때만 비활성
425
+ const h24s = is12h
426
+ ? (() => {
427
+ const h12 = hourVal % 12;
428
+ const mer = activeMeridiem ?? "AM";
429
+ return [mer === "PM" ? h12 + 12 : h12];
430
+ })()
431
+ : [hourVal];
432
+ return h24s.every((h) => {
433
+ const start = h * 60;
434
+ const end = h * 60 + 59;
435
+ if (maxMin != null && start > maxMin) return true;
436
+ if (minMin != null && end < minMin) return true;
437
+ return false;
438
+ });
439
+ };
440
+ const minuteDisabled = (mVal: number): boolean => {
441
+ if (!value) return false; // hour 미선택이면 모두 활성
442
+ const total = value.hours * 60 + mVal;
443
+ if (minMin != null && total < minMin) return true;
444
+ if (maxMin != null && total > maxMin) return true;
445
+ return false;
446
+ };
447
+
448
+ /* Quick ────────────────────────────────────────────── */
449
+ const quicks = Array.isArray(quickOptions)
450
+ ? quickOptions
451
+ : quickOptions
452
+ ? DEFAULT_QUICK
453
+ : [];
454
+
455
+ const applyQuick = (q: QuickOption) => {
456
+ const base = (() => {
457
+ if (q === "now") {
458
+ const d = new Date();
459
+ return { hours: d.getHours(), minutes: d.getMinutes() };
460
+ }
461
+ const cur = value ?? (() => {
462
+ const d = new Date();
463
+ return { hours: d.getHours(), minutes: d.getMinutes() };
464
+ })();
465
+ const total = cur.hours * 60 + cur.minutes + q.minutes;
466
+ const wrapped = ((total % (24 * 60)) + 24 * 60) % (24 * 60);
467
+ return { hours: Math.floor(wrapped / 60), minutes: wrapped % 60 };
468
+ })();
469
+ commit(base);
470
+ };
471
+
472
+ /* 세그먼트 트리거 — Input 시각 토큰 재사용(.root/variant/size) + 전용 .trigger */
473
+ const sizeClass =
474
+ size === "sm"
475
+ ? inputStyles.sizeSm
476
+ : size === "lg"
477
+ ? inputStyles.sizeLg
478
+ : inputStyles.sizeMd;
479
+ const variantClass =
480
+ variant === "filled"
481
+ ? inputStyles.filled
482
+ : variant === "line"
483
+ ? inputStyles.line
484
+ : inputStyles.outline;
485
+
486
+ const triggerClass = [
487
+ inputStyles.root,
488
+ variantClass,
489
+ sizeClass,
490
+ styles.trigger,
491
+ className ?? "",
492
+ ]
493
+ .filter(Boolean)
494
+ .join(" ");
495
+
496
+ const triggerNode = (
497
+ <div
498
+ ref={ref as React.Ref<HTMLDivElement>}
499
+ id={triggerId}
500
+ className={triggerClass}
501
+ data-size={size}
502
+ data-disabled={disabled || undefined}
503
+ role="group"
504
+ aria-label={ariaLabel ?? "시간 입력"}
505
+ >
506
+ <div className={styles.segments}>
507
+ <SegmentButton
508
+ ref={hourRef}
509
+ kind="hour"
510
+ value={value?.hours ?? null}
511
+ display={hourDisplay}
512
+ placeholder="HH"
513
+ focused={focusedSeg === "hour"}
514
+ disabled={disabled}
515
+ onFocus={() => setFocusedSeg("hour")}
516
+ onKey={handleSegmentKey("hour")}
517
+ aria-label="시"
518
+ />
519
+ <span className={styles.separator} aria-hidden>
520
+ :
521
+ </span>
522
+ <SegmentButton
523
+ ref={minuteRef}
524
+ kind="minute"
525
+ value={value?.minutes ?? null}
526
+ display={minuteDisplay}
527
+ placeholder="MM"
528
+ focused={focusedSeg === "minute"}
529
+ disabled={disabled}
530
+ onFocus={() => setFocusedSeg("minute")}
531
+ onKey={handleSegmentKey("minute")}
532
+ aria-label="분"
533
+ />
534
+ {is12h && (
535
+ <SegmentButton
536
+ ref={meridiemRef}
537
+ kind="meridiem"
538
+ value={value ? (value.hours >= 12 ? 1 : 0) : null}
539
+ display={meridiemDisplay}
540
+ placeholder="am"
541
+ focused={focusedSeg === "meridiem"}
542
+ disabled={disabled}
543
+ onFocus={() => setFocusedSeg("meridiem")}
544
+ onKey={handleMeridiemKey}
545
+ aria-label="오전 오후"
546
+ />
547
+ )}
548
+ </div>
549
+ {!inline && (
550
+ <RadixPopover.Trigger asChild>
551
+ <button
552
+ type="button"
553
+ className={styles.triggerIconBtn}
554
+ disabled={disabled}
555
+ aria-label="시간 선택 열기"
556
+ aria-haspopup="dialog"
557
+ aria-expanded={open}
558
+ >
559
+ <Icon name="clock" />
560
+ </button>
561
+ </RadixPopover.Trigger>
562
+ )}
563
+ </div>
564
+ );
565
+
566
+ const hourLaneRef = useRef<HTMLDivElement>(null);
567
+ const minuteLaneRef = useRef<HTMLDivElement>(null);
568
+ const hourSelectedRef = useRef<HTMLButtonElement>(null);
569
+ const minuteSelectedRef = useRef<HTMLButtonElement>(null);
570
+
571
+ useLayoutEffect(() => {
572
+ if (!open && !inline) return;
573
+ ([
574
+ [hourLaneRef, hourSelectedRef],
575
+ [minuteLaneRef, minuteSelectedRef],
576
+ ] as const).forEach(([laneR, cellR]) => {
577
+ const lane = laneR.current;
578
+ const cell = cellR.current;
579
+ if (!lane || !cell) return;
580
+ const target =
581
+ cell.offsetTop - lane.clientHeight / 2 + cell.clientHeight / 2;
582
+ lane.scrollTop = Math.max(0, target);
583
+ });
584
+ }, [open, inline, value, stepMinutes, is12h]);
585
+
586
+ const panel = (
587
+ <div className={styles.panel}>
588
+ <div
589
+ className={styles.column}
590
+ data-focus={(!value || focusedSeg === "hour") || undefined}
591
+ >
592
+ <div className={styles.columnLabel}>시</div>
593
+ <div
594
+ ref={hourLaneRef}
595
+ className={`${styles.lane} scroll-fade-y`}
596
+ role="listbox"
597
+ aria-label="시 선택"
598
+ >
599
+ <div className={styles.laneTrack}>
600
+ {hours.map((h) => {
601
+ const selected = (() => {
602
+ if (!value) return false;
603
+ if (is12h) {
604
+ const curH12 = value.hours % 12 === 0 ? 12 : value.hours % 12;
605
+ return curH12 === h;
606
+ }
607
+ return value.hours === h;
608
+ })();
609
+ return (
610
+ <button
611
+ key={h}
612
+ ref={selected ? hourSelectedRef : undefined}
613
+ type="button"
614
+ role="option"
615
+ aria-selected={selected}
616
+ className={styles.cell}
617
+ data-selected={selected || undefined}
618
+ disabled={hourDisabled(h)}
619
+ onClick={() => selectHour(h)}
620
+ >
621
+ {String(h).padStart(2, "0")}
622
+ </button>
623
+ );
624
+ })}
625
+ </div>
626
+ </div>
627
+ </div>
628
+
629
+ <div
630
+ className={styles.column}
631
+ data-focus={(value && focusedSeg === "minute") || undefined}
632
+ >
633
+ <div className={styles.columnLabel}>분</div>
634
+ <div
635
+ ref={minuteLaneRef}
636
+ className={`${styles.lane} scroll-fade-y`}
637
+ role="listbox"
638
+ aria-label="분 선택"
639
+ >
640
+ <div className={styles.laneTrack}>
641
+ {minutes.map((m) => {
642
+ const selected = isSameTime(value, {
643
+ hours: value?.hours ?? -1,
644
+ minutes: m,
645
+ });
646
+ return (
647
+ <button
648
+ key={m}
649
+ ref={selected ? minuteSelectedRef : undefined}
650
+ type="button"
651
+ role="option"
652
+ aria-selected={selected}
653
+ className={styles.cell}
654
+ data-selected={selected || undefined}
655
+ disabled={minuteDisabled(m)}
656
+ onClick={() => selectMinute(m)}
657
+ >
658
+ {String(m).padStart(2, "0")}
659
+ </button>
660
+ );
661
+ })}
662
+ </div>
663
+ </div>
664
+ </div>
665
+
666
+ {is12h && (
667
+ <div
668
+ className={`${styles.column} ${styles.meridiemColumn}`}
669
+ data-focus={focusedSeg === "meridiem" || undefined}
670
+ >
671
+ <div className={styles.columnLabel}>&nbsp;</div>
672
+ <div className={styles.lane}>
673
+ <div className={styles.laneTrack}>
674
+ {(["AM", "PM"] as const).map((mer) => {
675
+ const selected = activeMeridiem === mer;
676
+ return (
677
+ <button
678
+ key={mer}
679
+ type="button"
680
+ className={styles.cell}
681
+ data-selected={selected || undefined}
682
+ onClick={() => selectMeridiem(mer)}
683
+ >
684
+ {mer}
685
+ </button>
686
+ );
687
+ })}
688
+ </div>
689
+ </div>
690
+ </div>
691
+ )}
692
+ </div>
693
+ );
694
+
695
+ /* Grid panel — step 간격으로 전체 슬롯을 격자. */
696
+ const gridSlots = (() => {
697
+ const list: TimeValue[] = [];
698
+ const total = Math.floor((24 * 60) / stepMinutes);
699
+ for (let i = 0; i < total; i += 1) {
700
+ const t = i * stepMinutes;
701
+ list.push({ hours: Math.floor(t / 60), minutes: t % 60 });
702
+ }
703
+ return list;
704
+ })();
705
+
706
+ const renderGridButton = (t: TimeValue) => {
707
+ const total = t.hours * 60 + t.minutes;
708
+ const dis =
709
+ (minMin != null && total < minMin) ||
710
+ (maxMin != null && total > maxMin);
711
+ const selected = isSameTime(value, t);
712
+ const label = (() => {
713
+ if (!is12h) return `${String(t.hours).padStart(2, "0")}:${String(t.minutes).padStart(2, "0")}`;
714
+ const h12 = t.hours % 12 === 0 ? 12 : t.hours % 12;
715
+ const mm = String(t.minutes).padStart(2, "0");
716
+ const suf = t.hours < 12 ? "AM" : "PM";
717
+ return `${h12}:${mm} ${suf}`;
718
+ })();
719
+ return (
720
+ <button
721
+ key={`${t.hours}:${t.minutes}`}
722
+ type="button"
723
+ className={styles.gridCell}
724
+ data-selected={selected || undefined}
725
+ disabled={dis}
726
+ onClick={() => commit(t)}
727
+ >
728
+ {label}
729
+ </button>
730
+ );
731
+ };
732
+
733
+ const gridPanel = splitMeridiem ? (
734
+ <div className={styles.gridWrap}>
735
+ <div className={styles.gridSection}>
736
+ <div className={styles.gridSectionLabel}>오전</div>
737
+ <div className={styles.gridPanel} data-cols={String(gridCols)}>
738
+ {gridSlots.filter((t) => t.hours < 12).map(renderGridButton)}
739
+ </div>
740
+ </div>
741
+ <div className={styles.gridSection}>
742
+ <div className={styles.gridSectionLabel}>오후</div>
743
+ <div className={styles.gridPanel} data-cols={String(gridCols)}>
744
+ {gridSlots.filter((t) => t.hours >= 12).map(renderGridButton)}
745
+ </div>
746
+ </div>
747
+ </div>
748
+ ) : (
749
+ <div className={styles.gridPanel} data-cols={String(gridCols)}>
750
+ {gridSlots.map(renderGridButton)}
751
+ </div>
752
+ );
753
+
754
+ const activePanel = panelLayout === "grid" ? gridPanel : panel;
755
+
756
+ const panelWithQuick = quicks.length > 0 ? (
757
+ <div style={{ display: "flex", flexDirection: "column" }}>
758
+ <div className={styles.quickRow}>
759
+ {quicks.map((q) => (
760
+ <button
761
+ key={typeof q === "string" ? q : q.label}
762
+ type="button"
763
+ className={styles.cell}
764
+ onClick={() => applyQuick(q)}
765
+ style={{ paddingInline: "var(--space-3)" }}
766
+ >
767
+ {typeof q === "string" ? "지금" : q.label}
768
+ </button>
769
+ ))}
770
+ </div>
771
+ {activePanel}
772
+ </div>
773
+ ) : (
774
+ activePanel
775
+ );
776
+
777
+ if (inline) {
778
+ // grid 레이아웃은 트리거 없이 패널만.
779
+ if (panelLayout === "grid") {
780
+ return (
781
+ <div
782
+ className={[styles.inlineWrap, className ?? ""].filter(Boolean).join(" ")}
783
+ >
784
+ {panelWithQuick}
785
+ </div>
786
+ );
787
+ }
788
+ return (
789
+ <div
790
+ className={[styles.inlineWrap, className ?? ""].filter(Boolean).join(" ")}
791
+ >
792
+ {triggerNode}
793
+ <div style={{ borderTop: "var(--hairline) solid var(--border)" }}>
794
+ {panelWithQuick}
795
+ </div>
796
+ </div>
797
+ );
798
+ }
799
+
800
+ return (
801
+ <RadixPopover.Root open={open} onOpenChange={setOpen}>
802
+ {triggerNode}
803
+ <RadixPopover.Portal>
804
+ <RadixPopover.Content
805
+ className={[styles.popoverContent, "m-float"].join(" ")}
806
+ sideOffset={6}
807
+ align="start"
808
+ collisionPadding={8}
809
+ onOpenAutoFocus={(e) => e.preventDefault()}
810
+ >
811
+ {panelWithQuick}
812
+ </RadixPopover.Content>
813
+ </RadixPopover.Portal>
814
+ </RadixPopover.Root>
815
+ );
816
+ }
817
+ );
818
+
819
+ TimePicker.displayName = "TimePicker";
820
+
821
+ export type { TimeValue, TimeFormat };