@zyncat/ui 0.8.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 (334) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +133 -0
  3. package/dist/alert.css +132 -0
  4. package/dist/alert.d.ts +46 -0
  5. package/dist/alert.js +80 -0
  6. package/dist/alert.js.map +1 -0
  7. package/dist/avatar-group.d.ts +24 -0
  8. package/dist/avatar-group.js +23 -0
  9. package/dist/avatar-group.js.map +1 -0
  10. package/dist/avatar.css +200 -0
  11. package/dist/avatar.d.ts +35 -0
  12. package/dist/avatar.js +67 -0
  13. package/dist/avatar.js.map +1 -0
  14. package/dist/badge.css +182 -0
  15. package/dist/badge.d.ts +34 -0
  16. package/dist/badge.js +4 -0
  17. package/dist/badge.js.map +1 -0
  18. package/dist/button.css +269 -0
  19. package/dist/button.d.ts +44 -0
  20. package/dist/button.js +4 -0
  21. package/dist/button.js.map +1 -0
  22. package/dist/checkbox.css +177 -0
  23. package/dist/checkbox.d.ts +35 -0
  24. package/dist/checkbox.js +5 -0
  25. package/dist/checkbox.js.map +1 -0
  26. package/dist/chunk-2ERBBFZS.js +12 -0
  27. package/dist/chunk-2ERBBFZS.js.map +1 -0
  28. package/dist/chunk-2HORSLXJ.js +191 -0
  29. package/dist/chunk-2HORSLXJ.js.map +1 -0
  30. package/dist/chunk-36WVAQKL.js +40 -0
  31. package/dist/chunk-36WVAQKL.js.map +1 -0
  32. package/dist/chunk-3SBCBYB7.js +575 -0
  33. package/dist/chunk-3SBCBYB7.js.map +1 -0
  34. package/dist/chunk-4R2GQO43.js +125 -0
  35. package/dist/chunk-4R2GQO43.js.map +1 -0
  36. package/dist/chunk-64UU4LGU.js +258 -0
  37. package/dist/chunk-64UU4LGU.js.map +1 -0
  38. package/dist/chunk-7SZCJ7WI.js +35 -0
  39. package/dist/chunk-7SZCJ7WI.js.map +1 -0
  40. package/dist/chunk-BIDVNLJO.js +38 -0
  41. package/dist/chunk-BIDVNLJO.js.map +1 -0
  42. package/dist/chunk-CONSWNIV.js +17 -0
  43. package/dist/chunk-CONSWNIV.js.map +1 -0
  44. package/dist/chunk-EQ32F2QJ.js +75 -0
  45. package/dist/chunk-EQ32F2QJ.js.map +1 -0
  46. package/dist/chunk-GY7JOPUQ.js +111 -0
  47. package/dist/chunk-GY7JOPUQ.js.map +1 -0
  48. package/dist/chunk-I4XQ4PV2.js +30 -0
  49. package/dist/chunk-I4XQ4PV2.js.map +1 -0
  50. package/dist/chunk-JSKXZMLH.js +67 -0
  51. package/dist/chunk-JSKXZMLH.js.map +1 -0
  52. package/dist/chunk-L3Q3VDIW.js +295 -0
  53. package/dist/chunk-L3Q3VDIW.js.map +1 -0
  54. package/dist/chunk-LDC3RIYA.js +62 -0
  55. package/dist/chunk-LDC3RIYA.js.map +1 -0
  56. package/dist/chunk-MKIQFK7S.js +43 -0
  57. package/dist/chunk-MKIQFK7S.js.map +1 -0
  58. package/dist/chunk-MMRIQNEN.js +20 -0
  59. package/dist/chunk-MMRIQNEN.js.map +1 -0
  60. package/dist/chunk-MXOPCFWR.js +59 -0
  61. package/dist/chunk-MXOPCFWR.js.map +1 -0
  62. package/dist/chunk-NEF5XAHG.js +17 -0
  63. package/dist/chunk-NEF5XAHG.js.map +1 -0
  64. package/dist/chunk-NNXJCLTV.js +22 -0
  65. package/dist/chunk-NNXJCLTV.js.map +1 -0
  66. package/dist/chunk-O4N2OBTR.js +94 -0
  67. package/dist/chunk-O4N2OBTR.js.map +1 -0
  68. package/dist/chunk-PMECHR3A.js +38 -0
  69. package/dist/chunk-PMECHR3A.js.map +1 -0
  70. package/dist/chunk-QDK66S4G.js +44 -0
  71. package/dist/chunk-QDK66S4G.js.map +1 -0
  72. package/dist/chunk-QGNC7LH5.js +8 -0
  73. package/dist/chunk-QGNC7LH5.js.map +1 -0
  74. package/dist/chunk-QMSJZCK3.js +398 -0
  75. package/dist/chunk-QMSJZCK3.js.map +1 -0
  76. package/dist/chunk-S5ZTZANS.js +57 -0
  77. package/dist/chunk-S5ZTZANS.js.map +1 -0
  78. package/dist/chunk-S7B34VGN.js +14 -0
  79. package/dist/chunk-S7B34VGN.js.map +1 -0
  80. package/dist/chunk-TCUCYFT7.js +25 -0
  81. package/dist/chunk-TCUCYFT7.js.map +1 -0
  82. package/dist/chunk-TYHR4VNM.js +193 -0
  83. package/dist/chunk-TYHR4VNM.js.map +1 -0
  84. package/dist/chunk-U5QQZC3U.js +56 -0
  85. package/dist/chunk-U5QQZC3U.js.map +1 -0
  86. package/dist/chunk-UE63OOVG.js +13 -0
  87. package/dist/chunk-UE63OOVG.js.map +1 -0
  88. package/dist/chunk-VTJTGVZZ.js +22 -0
  89. package/dist/chunk-VTJTGVZZ.js.map +1 -0
  90. package/dist/chunk-W3SHPKSB.js +49 -0
  91. package/dist/chunk-W3SHPKSB.js.map +1 -0
  92. package/dist/chunk-XZAEIQDR.js +188 -0
  93. package/dist/chunk-XZAEIQDR.js.map +1 -0
  94. package/dist/chunk-YGGCQNNU.js +92 -0
  95. package/dist/chunk-YGGCQNNU.js.map +1 -0
  96. package/dist/collapse.css +88 -0
  97. package/dist/collapse.d.ts +46 -0
  98. package/dist/collapse.js +5 -0
  99. package/dist/collapse.js.map +1 -0
  100. package/dist/count-badge.d.ts +13 -0
  101. package/dist/count-badge.js +13 -0
  102. package/dist/count-badge.js.map +1 -0
  103. package/dist/date-field.d.ts +25 -0
  104. package/dist/date-field.js +87 -0
  105. package/dist/date-field.js.map +1 -0
  106. package/dist/date-picker.css +595 -0
  107. package/dist/date-range-field.d.ts +33 -0
  108. package/dist/date-range-field.js +404 -0
  109. package/dist/date-range-field.js.map +1 -0
  110. package/dist/datetime-field.d.ts +29 -0
  111. package/dist/datetime-field.js +155 -0
  112. package/dist/datetime-field.js.map +1 -0
  113. package/dist/dialog.css +180 -0
  114. package/dist/dialog.d.ts +42 -0
  115. package/dist/dialog.js +128 -0
  116. package/dist/dialog.js.map +1 -0
  117. package/dist/dom-props-C0jrllBw.d.ts +5 -0
  118. package/dist/field-chrome-CMPOQSLs.d.ts +28 -0
  119. package/dist/field-shell-Cuda-feL.d.ts +24 -0
  120. package/dist/glide.d.ts +15 -0
  121. package/dist/glide.js +5 -0
  122. package/dist/glide.js.map +1 -0
  123. package/dist/icon.css +33 -0
  124. package/dist/input.css +333 -0
  125. package/dist/mcp.js +359 -0
  126. package/dist/motion-devtools.css +265 -0
  127. package/dist/motion-devtools.d.ts +51 -0
  128. package/dist/motion-devtools.js +354 -0
  129. package/dist/motion-devtools.js.map +1 -0
  130. package/dist/motion-scale-Canq4stZ.d.ts +18 -0
  131. package/dist/motion-tokens.d.ts +28 -0
  132. package/dist/motion-tokens.js +3 -0
  133. package/dist/motion-tokens.js.map +1 -0
  134. package/dist/multi-select.d.ts +44 -0
  135. package/dist/multi-select.js +109 -0
  136. package/dist/multi-select.js.map +1 -0
  137. package/dist/number-field.d.ts +36 -0
  138. package/dist/number-field.js +112 -0
  139. package/dist/number-field.js.map +1 -0
  140. package/dist/odometer.css +62 -0
  141. package/dist/otp-field.d.ts +31 -0
  142. package/dist/otp-field.js +98 -0
  143. package/dist/otp-field.js.map +1 -0
  144. package/dist/overlay.css +23 -0
  145. package/dist/pagination.css +43 -0
  146. package/dist/pagination.d.ts +29 -0
  147. package/dist/pagination.js +107 -0
  148. package/dist/pagination.js.map +1 -0
  149. package/dist/popover.css +69 -0
  150. package/dist/popover.d.ts +35 -0
  151. package/dist/popover.js +15 -0
  152. package/dist/popover.js.map +1 -0
  153. package/dist/radio-group.css +300 -0
  154. package/dist/radio-group.d.ts +52 -0
  155. package/dist/radio-group.js +115 -0
  156. package/dist/radio-group.js.map +1 -0
  157. package/dist/select.css +416 -0
  158. package/dist/select.d.ts +49 -0
  159. package/dist/select.js +99 -0
  160. package/dist/select.js.map +1 -0
  161. package/dist/sheet.css +26 -0
  162. package/dist/sheet.d.ts +31 -0
  163. package/dist/sheet.js +14 -0
  164. package/dist/sheet.js.map +1 -0
  165. package/dist/status-badge.d.ts +19 -0
  166. package/dist/status-badge.js +72 -0
  167. package/dist/status-badge.js.map +1 -0
  168. package/dist/table.css +348 -0
  169. package/dist/table.d.ts +75 -0
  170. package/dist/table.js +262 -0
  171. package/dist/table.js.map +1 -0
  172. package/dist/tabs.css +208 -0
  173. package/dist/tabs.d.ts +71 -0
  174. package/dist/tabs.js +181 -0
  175. package/dist/tabs.js.map +1 -0
  176. package/dist/tag.css +248 -0
  177. package/dist/tag.d.ts +52 -0
  178. package/dist/tag.js +72 -0
  179. package/dist/tag.js.map +1 -0
  180. package/dist/text-field.d.ts +28 -0
  181. package/dist/text-field.js +69 -0
  182. package/dist/text-field.js.map +1 -0
  183. package/dist/textarea.css +219 -0
  184. package/dist/textarea.d.ts +38 -0
  185. package/dist/textarea.js +144 -0
  186. package/dist/textarea.js.map +1 -0
  187. package/dist/time-field.d.ts +28 -0
  188. package/dist/time-field.js +66 -0
  189. package/dist/time-field.js.map +1 -0
  190. package/dist/timing-7JA_zQHP.d.ts +27 -0
  191. package/dist/toast-store.d.ts +86 -0
  192. package/dist/toast-store.js +3 -0
  193. package/dist/toast-store.js.map +1 -0
  194. package/dist/toast.css +257 -0
  195. package/dist/toast.d.ts +12 -0
  196. package/dist/toast.js +310 -0
  197. package/dist/toast.js.map +1 -0
  198. package/dist/toggle-tag.d.ts +34 -0
  199. package/dist/toggle-tag.js +57 -0
  200. package/dist/toggle-tag.js.map +1 -0
  201. package/dist/toggle.css +152 -0
  202. package/dist/toggle.d.ts +31 -0
  203. package/dist/toggle.js +75 -0
  204. package/dist/toggle.js.map +1 -0
  205. package/dist/tooltip.css +86 -0
  206. package/dist/tooltip.d.ts +30 -0
  207. package/dist/tooltip.js +268 -0
  208. package/dist/tooltip.js.map +1 -0
  209. package/dist/types-SGMShK7R.d.ts +22 -0
  210. package/llms.txt +196 -0
  211. package/next.d.mts +7 -0
  212. package/next.mjs +59 -0
  213. package/package.json +257 -0
  214. package/src/components/composites/alert/Alert.tsx +141 -0
  215. package/src/components/composites/alert/alert.css +132 -0
  216. package/src/components/composites/date-picker/DateField.tsx +80 -0
  217. package/src/components/composites/date-picker/DateRangeField.tsx +110 -0
  218. package/src/components/composites/date-picker/DateTimeField.tsx +162 -0
  219. package/src/components/composites/date-picker/TimeField.tsx +78 -0
  220. package/src/components/composites/date-picker/calendar-panel.tsx +216 -0
  221. package/src/components/composites/date-picker/date-picker.css +595 -0
  222. package/src/components/composites/date-picker/date-utils.ts +56 -0
  223. package/src/components/composites/date-picker/field-shell.tsx +85 -0
  224. package/src/components/composites/date-picker/range-panel.tsx +363 -0
  225. package/src/components/composites/date-picker/time-core.tsx +290 -0
  226. package/src/components/composites/date-picker/use-day-focus.ts +35 -0
  227. package/src/components/composites/dialog/Dialog.tsx +198 -0
  228. package/src/components/composites/dialog/dialog.css +180 -0
  229. package/src/components/composites/pagination/Pagination.tsx +131 -0
  230. package/src/components/composites/pagination/pagination.css +43 -0
  231. package/src/components/composites/popover/Popover.tsx +141 -0
  232. package/src/components/composites/popover/popover.css +69 -0
  233. package/src/components/composites/select/MultiSelect.tsx +133 -0
  234. package/src/components/composites/select/Select.tsx +122 -0
  235. package/src/components/composites/select/core/index.ts +4 -0
  236. package/src/components/composites/select/core/menu.tsx +100 -0
  237. package/src/components/composites/select/core/panel.tsx +154 -0
  238. package/src/components/composites/select/core/trigger.tsx +74 -0
  239. package/src/components/composites/select/core/types.ts +39 -0
  240. package/src/components/composites/select/core/use-listbox.ts +182 -0
  241. package/src/components/composites/select/select.css +416 -0
  242. package/src/components/composites/sheet/Sheet.tsx +135 -0
  243. package/src/components/composites/sheet/sheet.css +26 -0
  244. package/src/components/composites/sheet/use-sheet-drag.ts +126 -0
  245. package/src/components/composites/table/Table.tsx +377 -0
  246. package/src/components/composites/table/table.css +348 -0
  247. package/src/components/composites/tabs/Tabs.tsx +278 -0
  248. package/src/components/composites/tabs/tabs.css +208 -0
  249. package/src/components/composites/toast/Toast.tsx +380 -0
  250. package/src/components/composites/toast/toast-store.ts +295 -0
  251. package/src/components/composites/toast/toast.css +257 -0
  252. package/src/components/composites/tooltip/Tooltip.tsx +110 -0
  253. package/src/components/composites/tooltip/tooltip-host.tsx +184 -0
  254. package/src/components/composites/tooltip/tooltip-store.ts +81 -0
  255. package/src/components/composites/tooltip/tooltip.css +86 -0
  256. package/src/components/dev/MotionDevtools.tsx +200 -0
  257. package/src/components/dev/motion-devtools.css +265 -0
  258. package/src/components/dev/slowmo-engine.ts +230 -0
  259. package/src/components/internal/glass/glass.css +100 -0
  260. package/src/components/internal/glass/glint.ts +9 -0
  261. package/src/components/internal/hooks/use-controllable.ts +21 -0
  262. package/src/components/internal/hooks/use-scroll-edges.ts +46 -0
  263. package/src/components/internal/icon/Icon.tsx +66 -0
  264. package/src/components/internal/icon/IconSlot.tsx +10 -0
  265. package/src/components/internal/icon/icon.css +33 -0
  266. package/src/components/internal/overlay/focus.ts +68 -0
  267. package/src/components/internal/overlay/layer.tsx +149 -0
  268. package/src/components/internal/overlay/modal.tsx +141 -0
  269. package/src/components/internal/overlay/overlay.css +23 -0
  270. package/src/components/internal/overlay/position.ts +103 -0
  271. package/src/components/internal/utils/cx.ts +3 -0
  272. package/src/components/internal/utils/token-px.ts +8 -0
  273. package/src/components/primitives/avatar/Avatar.tsx +120 -0
  274. package/src/components/primitives/avatar/AvatarGroup.tsx +56 -0
  275. package/src/components/primitives/avatar/avatar.css +200 -0
  276. package/src/components/primitives/badge/Badge.tsx +75 -0
  277. package/src/components/primitives/badge/CountBadge.tsx +20 -0
  278. package/src/components/primitives/badge/StatusBadge.tsx +111 -0
  279. package/src/components/primitives/badge/badge.css +182 -0
  280. package/src/components/primitives/badge/odometer.css +62 -0
  281. package/src/components/primitives/badge/odometer.tsx +36 -0
  282. package/src/components/primitives/button/Button.tsx +102 -0
  283. package/src/components/primitives/button/button.css +269 -0
  284. package/src/components/primitives/checkbox/Checkbox.tsx +76 -0
  285. package/src/components/primitives/checkbox/check-glyph.tsx +28 -0
  286. package/src/components/primitives/checkbox/checkbox.css +177 -0
  287. package/src/components/primitives/collapse/Collapse.tsx +86 -0
  288. package/src/components/primitives/collapse/collapse.css +88 -0
  289. package/src/components/primitives/collapse/collapse.typecheck.tsx +29 -0
  290. package/src/components/primitives/input/NumberField.tsx +139 -0
  291. package/src/components/primitives/input/OtpField.tsx +134 -0
  292. package/src/components/primitives/input/TextField.tsx +110 -0
  293. package/src/components/primitives/input/field-chrome.tsx +83 -0
  294. package/src/components/primitives/input/input.css +333 -0
  295. package/src/components/primitives/radio-group/RadioGroup.tsx +183 -0
  296. package/src/components/primitives/radio-group/radio-group.css +300 -0
  297. package/src/components/primitives/tag/Tag.tsx +143 -0
  298. package/src/components/primitives/tag/ToggleTag.tsx +97 -0
  299. package/src/components/primitives/tag/tag.css +248 -0
  300. package/src/components/primitives/textarea/Textarea.tsx +190 -0
  301. package/src/components/primitives/textarea/textarea.css +219 -0
  302. package/src/components/primitives/toggle/Toggle.tsx +105 -0
  303. package/src/components/primitives/toggle/toggle.css +152 -0
  304. package/src/css.d.ts +1 -0
  305. package/src/dom-props.ts +1 -0
  306. package/src/engine/animate.ts +141 -0
  307. package/src/engine/drag.ts +53 -0
  308. package/src/engine/ease.ts +155 -0
  309. package/src/engine/flip.ts +59 -0
  310. package/src/engine/index.ts +13 -0
  311. package/src/mcp/server.ts +420 -0
  312. package/src/motion/element.tsx +53 -0
  313. package/src/motion/flip.tsx +87 -0
  314. package/src/motion/glide.tsx +47 -0
  315. package/src/motion/motion-timing.ts +33 -0
  316. package/src/motion/presence-context.ts +18 -0
  317. package/src/motion/presence.tsx +91 -0
  318. package/src/motion/timing.ts +49 -0
  319. package/src/motion/use-motion.ts +64 -0
  320. package/src/styles.css +26 -0
  321. package/src/tokens/avatar.css +20 -0
  322. package/src/tokens/color.css +58 -0
  323. package/src/tokens/elevation.css +22 -0
  324. package/src/tokens/fonts.css +3 -0
  325. package/src/tokens/glass.css +20 -0
  326. package/src/tokens/icons.css +7 -0
  327. package/src/tokens/layers.css +6 -0
  328. package/src/tokens/motion-scale.ts +17 -0
  329. package/src/tokens/motion-tokens.ts +92 -0
  330. package/src/tokens/motion.css +49 -0
  331. package/src/tokens/radius.css +11 -0
  332. package/src/tokens/semantic.css +65 -0
  333. package/src/tokens/spacing.css +26 -0
  334. package/src/tokens/typography.css +52 -0
@@ -0,0 +1,110 @@
1
+ 'use client';
2
+
3
+ import './date-picker.css';
4
+ import { useEffect, useState } from 'react';
5
+ import { Popover } from '../popover/Popover';
6
+ import { Sheet } from '../sheet/Sheet';
7
+ import { FieldShell, FieldTrigger, type DateFieldBaseProps } from './field-shell';
8
+ import { useControllable } from '../../internal/hooks/use-controllable';
9
+ import { DrpPanel, drpRangeText, type DateRange } from './range-panel';
10
+
11
+ export type { DateRange };
12
+
13
+ function useNarrowViewport(query?: string): boolean {
14
+ const q = query || '(max-width: 640px)';
15
+ const [narrow, setNarrow] = useState(() => (typeof matchMedia === 'function' ? matchMedia(q).matches : false));
16
+ useEffect(() => {
17
+ const mq = matchMedia(q);
18
+ const fn = (e: MediaQueryListEvent) => setNarrow(e.matches);
19
+ mq.addEventListener('change', fn);
20
+ setNarrow(mq.matches);
21
+ return () => mq.removeEventListener('change', fn);
22
+ }, [q]);
23
+ return narrow;
24
+ }
25
+
26
+ export interface DateRangeFieldProps extends DateFieldBaseProps {
27
+ /** Controlled value - both endpoints, or null when empty. */
28
+ value?: DateRange | null;
29
+ /** Uncontrolled initial range. Use instead of `value`. @default null */
30
+ defaultValue?: DateRange | null;
31
+ /** Fires only on a COMPLETE range (a lone anchor never commits). */
32
+ onChange?: (value: DateRange) => void;
33
+ /** Trigger text shown when no range is picked. @default 'Pick a date range' */
34
+ placeholder?: string;
35
+ /** IANA timezone (e.g. 'Europe/Riga') - display context, shown in the footer. */
36
+ timezone?: string;
37
+ /** Earliest pickable date, 'YYYY-MM-DD', inclusive. */
38
+ min?: string;
39
+ /** Latest pickable date, 'YYYY-MM-DD', inclusive. */
40
+ max?: string;
41
+ }
42
+
43
+ export function DateRangeField({
44
+ value,
45
+ defaultValue = null,
46
+ onChange,
47
+ label,
48
+ placeholder = 'Pick a date range',
49
+ timezone,
50
+ min,
51
+ max,
52
+ required = false,
53
+ invalid = false,
54
+ message,
55
+ disabled = false,
56
+ className = '',
57
+ htmlProps,
58
+ animation,
59
+ }: DateRangeFieldProps) {
60
+ const [val, commit] = useControllable(value, defaultValue, onChange);
61
+
62
+ const narrow = useNarrowViewport();
63
+ const [pickerOpen, setPickerOpen] = useState(false);
64
+
65
+ const display = val && val.start && val.end ? drpRangeText(val.start, val.end) : null;
66
+ const trigger = <FieldTrigger display={display} placeholder={placeholder} disabled={disabled} />;
67
+ const panel = (
68
+ <DrpPanel
69
+ value={val}
70
+ commit={commit}
71
+ close={() => setPickerOpen(false)}
72
+ min={min}
73
+ max={max}
74
+ timezone={timezone}
75
+ label={label}
76
+ months={narrow ? 1 : 2}
77
+ layout={narrow ? 'sheet' : 'popover'}
78
+ />
79
+ );
80
+
81
+ return (
82
+ <FieldShell
83
+ variant="dtf"
84
+ label={label}
85
+ required={required}
86
+ invalid={invalid}
87
+ message={message}
88
+ icon="calendar"
89
+ className={className}
90
+ htmlProps={htmlProps}
91
+ >
92
+ {narrow ? (
93
+ <Sheet trigger={trigger} side="bottom" open={pickerOpen} onOpenChange={setPickerOpen} animation={animation}>
94
+ {panel}
95
+ </Sheet>
96
+ ) : (
97
+ <Popover
98
+ trigger={trigger}
99
+ side="bottom"
100
+ align="start"
101
+ open={pickerOpen}
102
+ onOpenChange={setPickerOpen}
103
+ animation={animation}
104
+ >
105
+ {panel}
106
+ </Popover>
107
+ )}
108
+ </FieldShell>
109
+ );
110
+ }
@@ -0,0 +1,162 @@
1
+ 'use client';
2
+
3
+ import './date-picker.css';
4
+ import { useState, useEffect } from 'react';
5
+ import { Popover } from '../popover/Popover';
6
+ import { FieldShell, FieldTrigger, type DateFieldBaseProps } from './field-shell';
7
+ import { useControllable } from '../../internal/hooks/use-controllable';
8
+ import { TimeSegments, disp12 } from './time-core';
9
+ import { DtpPanel } from './calendar-panel';
10
+ import { pad, displayDay } from './date-utils';
11
+
12
+ interface DateTimeParts {
13
+ date: string | null;
14
+ time: string | null;
15
+ }
16
+
17
+ function displayTime(t: string, format?: '24h' | '12h'): string {
18
+ if (format !== '12h') return t;
19
+ const p = t.split(':').map(Number);
20
+ const mer = p[0] >= 12 ? 'PM' : 'AM';
21
+ return disp12(p[0]) + ':' + pad(p[1]) + ' ' + mer;
22
+ }
23
+ function dttfSplit(v: string | null | undefined): DateTimeParts {
24
+ if (!v) return { date: null, time: null };
25
+ const i = v.indexOf('T');
26
+ return i < 0 ? { date: v, time: null } : { date: v.slice(0, i), time: v.slice(i + 1) };
27
+ }
28
+
29
+ export interface DateTimeFieldProps extends DateFieldBaseProps {
30
+ /** Controlled value, 'YYYY-MM-DDTHH:mm'. */
31
+ value?: string | null;
32
+ /** Uncontrolled initial value, 'YYYY-MM-DDTHH:mm'. Use instead of `value`. @default null */
33
+ defaultValue?: string | null;
34
+ /** Fires once both date and time are set, with 'YYYY-MM-DDTHH:mm' (an incomplete half never commits). */
35
+ onChange?: (value: string) => void;
36
+ /** Trigger text shown when no value is picked. @default 'Pick date & time' */
37
+ placeholder?: string;
38
+ /** IANA timezone (e.g. 'Europe/Riga') - display context, shown in the footer. */
39
+ timezone?: string;
40
+ /** Lower bound - 'YYYY-MM-DD' or 'YYYY-MM-DDTHH:mm', inclusive. */
41
+ min?: string;
42
+ /** Upper bound - 'YYYY-MM-DD' or 'YYYY-MM-DDTHH:mm', inclusive. */
43
+ max?: string;
44
+ /** Time display only; storage stays 24h. Default '24h'. */
45
+ format?: '24h' | '12h';
46
+ /** ↑/↓ step granularity in minutes (typing is exact). Default 5. */
47
+ minuteStep?: number;
48
+ }
49
+
50
+ export function DateTimeField({
51
+ value,
52
+ defaultValue = null,
53
+ onChange,
54
+ label,
55
+ placeholder = 'Pick date & time',
56
+ timezone,
57
+ min,
58
+ max,
59
+ format = '24h',
60
+ minuteStep = 5,
61
+ required = false,
62
+ invalid = false,
63
+ message,
64
+ disabled = false,
65
+ className = '',
66
+ htmlProps,
67
+ animation,
68
+ }: DateTimeFieldProps) {
69
+ const [val, commit] = useControllable<string | null>(
70
+ value,
71
+ defaultValue,
72
+ onChange as ((next: string | null) => void) | undefined,
73
+ );
74
+ const parts = dttfSplit(val);
75
+
76
+ const [pickerOpen, setPickerOpen] = useState(false);
77
+ const [pendDate, setPendDate] = useState<string | null>(null);
78
+ const [pendTime, setPendTime] = useState<string | null>(null);
79
+ useEffect(() => {
80
+ setPendDate(null);
81
+ setPendTime(null);
82
+ }, [val]);
83
+
84
+ const date = pendDate != null ? pendDate : parts.date;
85
+ const time = pendTime != null ? pendTime : parts.time;
86
+
87
+ const minL = dttfSplit(min);
88
+ const maxL = dttfSplit(max);
89
+ const minTime = date && date === minL.date ? minL.time : null;
90
+ const maxTime = date && date === maxL.date ? maxL.time : null;
91
+
92
+ function commitIf(d: string | null, t: string | null) {
93
+ if (!d || !t) return;
94
+ let tt = t;
95
+ if (minL.time && d === minL.date && tt < minL.time) tt = minL.time;
96
+ if (maxL.time && d === maxL.date && tt > maxL.time) tt = maxL.time;
97
+ if (tt !== t) setPendTime(tt);
98
+ const next = d + 'T' + tt;
99
+ if (next !== val) commit(next);
100
+ }
101
+ function handleDate(d: string) {
102
+ setPendDate(d);
103
+ commitIf(d, time);
104
+ }
105
+ function handleTime(t: string) {
106
+ setPendTime(t);
107
+ commitIf(date, t);
108
+ }
109
+
110
+ const display = date ? displayDay(date) + ', ' + (time ? displayTime(time, format) : '--:--') : null;
111
+ const trigger = <FieldTrigger display={display} placeholder={placeholder} disabled={disabled} />;
112
+
113
+ return (
114
+ <FieldShell
115
+ variant="dtf"
116
+ label={label}
117
+ required={required}
118
+ invalid={invalid}
119
+ message={message}
120
+ icon="calendar"
121
+ className={className}
122
+ htmlProps={htmlProps}
123
+ >
124
+ <Popover
125
+ trigger={trigger}
126
+ side="bottom"
127
+ align="start"
128
+ animation={animation}
129
+ open={pickerOpen}
130
+ onOpenChange={setPickerOpen}
131
+ >
132
+ <DtpPanel
133
+ val={date}
134
+ commit={handleDate}
135
+ close={() => setPickerOpen(false)}
136
+ min={minL.date}
137
+ max={maxL.date}
138
+ timezone={timezone}
139
+ label={label}
140
+ slot={
141
+ <div className="dtp__time">
142
+ <span className="dtp__timeLabel">Time</span>
143
+ <span className="dtp__timeHint" aria-hidden="true">
144
+ <kbd>↑</kbd>
145
+ <kbd>↓</kbd>
146
+ </span>
147
+ <TimeSegments
148
+ value={time}
149
+ onCommit={handleTime}
150
+ format={format}
151
+ minuteStep={minuteStep}
152
+ min={minTime}
153
+ max={maxTime}
154
+ ariaLabel="Time"
155
+ />
156
+ </div>
157
+ }
158
+ />
159
+ </Popover>
160
+ </FieldShell>
161
+ );
162
+ }
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+
3
+ import './date-picker.css';
4
+ import type { PointerEvent } from 'react';
5
+ import { FieldShell, type DateFieldBaseProps } from './field-shell';
6
+ import { useControllable } from '../../internal/hooks/use-controllable';
7
+ import { TimeSegments } from './time-core';
8
+
9
+ export interface TimeFieldProps extends DateFieldBaseProps {
10
+ /** Controlled value, canonical 'HH:mm' (24h). */
11
+ value?: string | null;
12
+ /** Uncontrolled initial value, 'HH:mm' (24h). Use instead of `value`. @default null */
13
+ defaultValue?: string | null;
14
+ /** Fires live - the instant both segments exist. */
15
+ onChange?: (value: string) => void;
16
+ /** Field label rendered above the box; also the segments' `aria-label` fallback. */
17
+ label?: string;
18
+ /** Display only; storage stays 24h. Default '24h'. */
19
+ format?: '24h' | '12h';
20
+ /** ↑/↓ step granularity in minutes (typing is exact). Default 5. */
21
+ minuteStep?: number;
22
+ /** Lower bound 'HH:mm' - saturates, never errors. */
23
+ min?: string;
24
+ /** Upper bound 'HH:mm' - saturates, never errors. */
25
+ max?: string;
26
+ }
27
+
28
+ export function TimeField({
29
+ value, // controlled: 'HH:mm' | null
30
+ defaultValue = null,
31
+ onChange,
32
+ label,
33
+ format = '24h',
34
+ minuteStep = 5,
35
+ min,
36
+ max,
37
+ required = false,
38
+ invalid = false,
39
+ message,
40
+ disabled = false,
41
+ className = '',
42
+ htmlProps,
43
+ }: TimeFieldProps) {
44
+ const [val, commit] = useControllable(value, defaultValue, onChange);
45
+
46
+ function onBoxPointerDown(e: PointerEvent<HTMLDivElement>) {
47
+ if (disabled || (e.target as HTMLElement).closest('.tsg__seg')) return;
48
+ e.preventDefault();
49
+ const seg = e.currentTarget.querySelector('.tsg__seg') as HTMLElement | null;
50
+ if (seg) seg.focus();
51
+ }
52
+
53
+ return (
54
+ <FieldShell
55
+ variant="tfd"
56
+ label={label}
57
+ required={required}
58
+ invalid={invalid}
59
+ message={message}
60
+ icon="clock"
61
+ className={className}
62
+ htmlProps={htmlProps}
63
+ >
64
+ <div className={'fld__input tfd__box' + (disabled ? ' is-disabled' : '')} onPointerDown={onBoxPointerDown}>
65
+ <TimeSegments
66
+ value={val}
67
+ onCommit={commit}
68
+ format={format}
69
+ minuteStep={minuteStep}
70
+ min={min}
71
+ max={max}
72
+ disabled={disabled}
73
+ ariaLabel={label || 'Time'}
74
+ />
75
+ </div>
76
+ </FieldShell>
77
+ );
78
+ }
@@ -0,0 +1,216 @@
1
+ 'use client';
2
+
3
+ import './date-picker.css';
4
+ import { useEffect, useId, useRef, useState, type KeyboardEvent, type ReactNode } from 'react';
5
+ import { animate } from '../../../engine';
6
+ import { UIMotion } from '../../../tokens/motion-tokens';
7
+ import { Icon } from '../../internal/icon/Icon';
8
+ import { Button } from '../../primitives/button/Button';
9
+ import { GlidePill, useGlide } from '../../../motion/glide';
10
+ import { useSharedFlip } from '../../../motion/flip';
11
+ import { useDayFocus } from './use-day-focus';
12
+ import { MONTHS, DOW, toKey, parse, col, today, grid, tzLabel, within } from './date-utils';
13
+ import { cx } from '../../internal/utils/cx';
14
+
15
+ interface DtpPanelProps {
16
+ val: string | null;
17
+ commit: (key: string) => void;
18
+ close: () => void;
19
+ min?: string;
20
+ max?: string;
21
+ timezone?: string;
22
+ label?: string;
23
+ slot?: ReactNode;
24
+ }
25
+
26
+ export function DtpPanel({ val, commit, min, max, timezone, label, close, slot }: DtpPanelProps) {
27
+ const seed = val ? parse(val) : new Date();
28
+ const [view, setView] = useState<{ y: number; m: number }>({ y: seed.getFullYear(), m: seed.getMonth() });
29
+ const [focusKey, setFocusKey] = useState<string>(val || today());
30
+
31
+ const daysRef = useRef<HTMLDivElement>(null);
32
+ const prevViewRef = useRef<number | null>(null);
33
+ const uid = useId();
34
+ const pillId = 'dtp-pill-' + uid;
35
+
36
+ const glide = useGlide(daysRef);
37
+ const pillRef = useSharedFlip<HTMLSpanElement>(pillId, { timing: UIMotion.t.settle });
38
+
39
+ const inRange = (key: string): boolean => within(key, min, max);
40
+
41
+ function pickDay(key: string) {
42
+ if (!inRange(key)) return;
43
+ armFocus();
44
+ setFocusKey(key);
45
+ const d = parse(key);
46
+ goToMonth(d.getFullYear(), d.getMonth());
47
+ commit(key);
48
+ }
49
+
50
+ function goToMonth(y: number, m: number) {
51
+ setView((v) => (v.y === y && v.m === m ? v : { y: y, m: m }));
52
+ }
53
+ function nav(dir: number) {
54
+ const d = new Date(view.y, view.m + dir, 1);
55
+ goToMonth(d.getFullYear(), d.getMonth());
56
+ }
57
+ const now = new Date();
58
+ const viewIsCurrent = view.y === now.getFullYear() && view.m === now.getMonth();
59
+ function goToToday() {
60
+ const key = today();
61
+ const d = parse(key);
62
+ armFocus();
63
+ setFocusKey(key);
64
+ goToMonth(d.getFullYear(), d.getMonth());
65
+ }
66
+ const viewIdx = view.y * 12 + view.m;
67
+ const armFocus = useDayFocus(daysRef, focusKey, viewIdx, '[tabindex="0"]:not(:disabled)');
68
+ useEffect(() => {
69
+ const prev = prevViewRef.current;
70
+ prevViewRef.current = viewIdx;
71
+ const el = daysRef.current;
72
+ if (prev == null || prev === viewIdx || !el) return;
73
+ const dir = viewIdx > prev ? 1 : -1;
74
+ animate(el, { x: [dir * 16, 0], opacity: [0, 1], timing: UIMotion.t.enter });
75
+ }, [viewIdx]);
76
+
77
+ function moveFocus(deltaDays: number, deltaMonths: number) {
78
+ const d = parse(focusKey);
79
+ if (deltaMonths) d.setMonth(d.getMonth() + deltaMonths);
80
+ if (deltaDays) d.setDate(d.getDate() + deltaDays);
81
+ const key = toKey(d);
82
+ armFocus();
83
+ setFocusKey(key);
84
+ goToMonth(d.getFullYear(), d.getMonth());
85
+ }
86
+ function onGridKeyDown(e: KeyboardEvent<HTMLDivElement>) {
87
+ const k = e.key;
88
+ if (k === 'ArrowLeft') moveFocus(-1, 0);
89
+ else if (k === 'ArrowRight') moveFocus(1, 0);
90
+ else if (k === 'ArrowUp') moveFocus(-7, 0);
91
+ else if (k === 'ArrowDown') moveFocus(7, 0);
92
+ else if (k === 'PageUp') moveFocus(0, -1);
93
+ else if (k === 'PageDown') moveFocus(0, 1);
94
+ else if (k === 'Home') moveFocus(-col(parse(focusKey)), 0);
95
+ else if (k === 'End') moveFocus(6 - col(parse(focusKey)), 0);
96
+ else if (k === 'Enter' || k === ' ') pickDay(focusKey);
97
+ else return;
98
+ e.preventDefault();
99
+ }
100
+
101
+ const days = grid(view.y, view.m);
102
+ const todayKey = today();
103
+ const selKey = val || null;
104
+ const gridKeys = days.map(toKey);
105
+ const tabKey =
106
+ gridKeys.indexOf(focusKey) >= 0
107
+ ? focusKey
108
+ : selKey && gridKeys.indexOf(selKey) >= 0
109
+ ? selKey
110
+ : gridKeys.indexOf(todayKey) >= 0
111
+ ? todayKey
112
+ : toKey(new Date(view.y, view.m, 1));
113
+
114
+ const prevEnd = toKey(new Date(view.y, view.m, 0));
115
+ const nextStart = toKey(new Date(view.y, view.m + 1, 1));
116
+ const canPrev = !min || prevEnd >= min;
117
+ const canNext = !max || nextStart <= max;
118
+
119
+ const weeks: Date[][] = [];
120
+ for (let w = 0; w < 6; w++) weeks.push(days.slice(w * 7, w * 7 + 7));
121
+
122
+ return (
123
+ <div className="dtp" role="dialog" aria-label={label || 'Pick a date'}>
124
+ <div className="dtp__cal">
125
+ <div className="dtp__head">
126
+ <span className="dtp__month" aria-live="polite">
127
+ {MONTHS[view.m]} <span className="dtp__year">{view.y}</span>
128
+ </span>
129
+ <div className="dtp__navs">
130
+ <button
131
+ type="button"
132
+ className="dtp__nav"
133
+ aria-label="Previous month"
134
+ disabled={!canPrev}
135
+ onClick={() => nav(-1)}
136
+ >
137
+ <Icon name="caret-left" size="sm" />
138
+ </button>
139
+ <button
140
+ type="button"
141
+ className="dtp__nav"
142
+ aria-label="Go to today"
143
+ disabled={viewIsCurrent}
144
+ onClick={goToToday}
145
+ >
146
+ <Icon name="calendar-dot" size="sm" />
147
+ </button>
148
+ <button
149
+ type="button"
150
+ className="dtp__nav"
151
+ aria-label="Next month"
152
+ disabled={!canNext}
153
+ onClick={() => nav(1)}
154
+ >
155
+ <Icon name="caret-right" size="sm" />
156
+ </button>
157
+ </div>
158
+ </div>
159
+ <div className="dtp__dow" aria-hidden="true">
160
+ {DOW.map((d) => (
161
+ <span key={d}>{d}</span>
162
+ ))}
163
+ </div>
164
+ <div
165
+ className="dtp__days"
166
+ ref={daysRef}
167
+ role="grid"
168
+ aria-label="Calendar"
169
+ onKeyDown={onGridKeyDown}
170
+ onPointerLeave={glide.leave}
171
+ >
172
+ {weeks.map((week, wi) => (
173
+ <div key={wi} role="row" className="dtp__row">
174
+ {week.map((d) => {
175
+ const key = toKey(d);
176
+ const out = d.getMonth() !== view.m;
177
+ const sel = key === selKey;
178
+ const cls = cx('dtp__day', out && 'is-out', sel && 'is-selected', key === todayKey && 'is-today');
179
+ return (
180
+ <button
181
+ key={key}
182
+ type="button"
183
+ role="gridcell"
184
+ data-key={key}
185
+ className={cls}
186
+ disabled={!inRange(key)}
187
+ tabIndex={key === tabKey ? 0 : -1}
188
+ aria-selected={sel || undefined}
189
+ aria-label={MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear()}
190
+ onClick={() => pickDay(key)}
191
+ onPointerEnter={(e) => glide.enter(e.currentTarget)}
192
+ >
193
+ {sel ? <span className="dtp__pill" aria-hidden="true" ref={pillRef}></span> : null}
194
+ <span className="dtp__num">{d.getDate()}</span>
195
+ {key === todayKey ? <span className="dtp__dot" aria-hidden="true"></span> : null}
196
+ </button>
197
+ );
198
+ })}
199
+ </div>
200
+ ))}
201
+ <GlidePill className="dtp__hover" glide={glide} />
202
+ </div>
203
+ </div>
204
+
205
+ {slot || null}
206
+
207
+ <div className="dtp__foot">
208
+ {timezone ? <span className="dtp__tz">{tzLabel(timezone, selKey)}</span> : null}
209
+ <span className="dtp__footSpacer"></span>
210
+ <Button variant="primary" size="sm" onClick={close}>
211
+ Done
212
+ </Button>
213
+ </div>
214
+ </div>
215
+ );
216
+ }