@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,85 @@
1
+ 'use client';
2
+
3
+ import './date-picker.css';
4
+ import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode, Ref } from 'react';
5
+ import { Icon, type IconName } from '../../internal/icon/Icon';
6
+ import { FieldLabel, FieldMessage } from '../../primitives/input/field-chrome';
7
+ import type { DisableableAnimation } from '../../../motion/timing';
8
+ import type { DataAttributes } from '../../../dom-props';
9
+ import { cx } from '../../internal/utils/cx';
10
+
11
+ export interface DateFieldBaseProps {
12
+ /** Field label rendered above the trigger. */
13
+ label?: string;
14
+ /** Asterisk on the label. @default false */
15
+ required?: boolean;
16
+ /** Danger border + message color (.fld is-error). @default false */
17
+ invalid?: boolean;
18
+ /** Helper / error text under the field. */
19
+ message?: string;
20
+ /** Disable the field. @default false */
21
+ disabled?: boolean;
22
+ /** Extra class on the field shell root. */
23
+ className?: string;
24
+ /** Standard <div> attributes (style, data-*, aria-*, ...) forwarded to the field shell root. */
25
+ htmlProps?: HTMLAttributes<HTMLDivElement> & DataAttributes;
26
+ /** Popover open/close timing - motion tokens only, or `null` to disable. (Not used by TimeField, which is inline.) */
27
+ animation?: DisableableAnimation;
28
+ }
29
+
30
+ export interface FieldShellProps {
31
+ variant: string;
32
+ label?: string;
33
+ required?: boolean;
34
+ invalid?: boolean;
35
+ message?: ReactNode;
36
+ icon: IconName;
37
+ className?: string;
38
+ htmlProps?: HTMLAttributes<HTMLDivElement> & DataAttributes;
39
+ children?: ReactNode;
40
+ }
41
+
42
+ export function FieldShell({
43
+ variant,
44
+ label,
45
+ required,
46
+ invalid,
47
+ message,
48
+ icon,
49
+ className,
50
+ htmlProps,
51
+ children,
52
+ }: FieldShellProps) {
53
+ const cls = cx('fld', variant, 'fld--has-lead', invalid && 'is-error', className, htmlProps?.className);
54
+ return (
55
+ <div {...htmlProps} className={cls}>
56
+ <FieldLabel label={label} required={required} />
57
+ <div className="fld__control">
58
+ {children}
59
+ <span className="fld__icon fld__icon--lead" aria-hidden="true">
60
+ <Icon name={icon} size="md" />
61
+ </span>
62
+ </div>
63
+ <FieldMessage message={message ? <span>{message}</span> : null} />
64
+ </div>
65
+ );
66
+ }
67
+
68
+ export function FieldTrigger({
69
+ display,
70
+ placeholder,
71
+ disabled,
72
+ ...rest
73
+ }: { display: string | null; placeholder: string; disabled?: boolean } & ButtonHTMLAttributes<HTMLButtonElement> & {
74
+ ref?: Ref<HTMLButtonElement>;
75
+ }) {
76
+ return (
77
+ <button type="button" className="fld__input dtf__trigger" disabled={disabled} {...rest}>
78
+ {display ? (
79
+ <span className="dtf__value">{display}</span>
80
+ ) : (
81
+ <span className="dtf__placeholder">{placeholder}</span>
82
+ )}
83
+ </button>
84
+ );
85
+ }
@@ -0,0 +1,363 @@
1
+ 'use client';
2
+
3
+ import './date-picker.css';
4
+ import { Fragment, useEffect, useId, useRef, useState, type KeyboardEvent } from 'react';
5
+ import { UIMotion } from '../../../tokens/motion-tokens';
6
+ import { Icon } from '../../internal/icon/Icon';
7
+ import { Button } from '../../primitives/button/Button';
8
+ import { GlidePill, useGlide } from '../../../motion/glide';
9
+ import { useSharedFlip } from '../../../motion/flip';
10
+ import { useDayFocus } from './use-day-focus';
11
+ import { MONTHS, DOW, pad, toKey, parse, today, add, col, grid, tzLabel, within } from './date-utils';
12
+
13
+ /** A date range as wall-clock 'YYYY-MM-DD' endpoints, inclusive. */
14
+ export interface DateRange {
15
+ /** Range start, inclusive - `'YYYY-MM-DD'`. */
16
+ start: string;
17
+ /** Range end, inclusive - `'YYYY-MM-DD'`. Never before `start`. */
18
+ end: string;
19
+ }
20
+
21
+ interface DrpPreset {
22
+ id: string;
23
+ label: string;
24
+ start: string;
25
+ end: string;
26
+ }
27
+
28
+ function drpDisplay(key: string, withYear: boolean): string {
29
+ const d = parse(key);
30
+ const mon = MONTHS[d.getMonth()].slice(0, 3);
31
+ return mon + ' ' + pad(d.getDate()) + (withYear ? ', ' + d.getFullYear() : '');
32
+ }
33
+ export function drpRangeText(start: string, end: string): string {
34
+ const sameYear = parse(start).getFullYear() === parse(end).getFullYear();
35
+ const curYear = new Date().getFullYear();
36
+ const startYr = !sameYear || parse(start).getFullYear() !== curYear;
37
+ const endYr = parse(end).getFullYear() !== curYear;
38
+ return drpDisplay(start, startYr) + ' - ' + drpDisplay(end, endYr);
39
+ }
40
+ const drpDays = (start: string, end: string): number => Math.round((+parse(end) - +parse(start)) / 86400000) + 1;
41
+
42
+ function drpPresets(): DrpPreset[] {
43
+ const t = today();
44
+ const now = new Date();
45
+ const firstThis = toKey(new Date(now.getFullYear(), now.getMonth(), 1));
46
+ const firstPrev = toKey(new Date(now.getFullYear(), now.getMonth() - 1, 1));
47
+ const lastPrev = toKey(new Date(now.getFullYear(), now.getMonth(), 0));
48
+ const jan1 = toKey(new Date(now.getFullYear(), 0, 1));
49
+ return [
50
+ { id: 'today', label: 'Today', start: t, end: t },
51
+ { id: 'yest', label: 'Yesterday', start: add(t, -1), end: add(t, -1) },
52
+ { id: '7d', label: 'Last 7 days', start: add(t, -6), end: t },
53
+ { id: '30d', label: 'Last 30 days', start: add(t, -29), end: t },
54
+ { id: 'mtd', label: 'This month', start: firstThis, end: t },
55
+ { id: 'lastm', label: 'Last month', start: firstPrev, end: lastPrev },
56
+ { id: '90d', label: 'Last 90 days', start: add(t, -89), end: t },
57
+ { id: 'ytd', label: 'Year to date', start: jan1, end: t },
58
+ ];
59
+ }
60
+
61
+ interface DrpPanelProps {
62
+ value: DateRange | null;
63
+ commit: (value: DateRange) => void;
64
+ close: () => void;
65
+ min?: string;
66
+ max?: string;
67
+ timezone?: string;
68
+ label?: string;
69
+ months: number;
70
+ layout: 'popover' | 'sheet';
71
+ }
72
+
73
+ export function DrpPanel({ value, commit, close, min, max, timezone, label, months, layout }: DrpPanelProps) {
74
+ const seedKey = (value && value.start) || today();
75
+ const seed = parse(seedKey);
76
+ const [view, setView] = useState<{ y: number; m: number }>({ y: seed.getFullYear(), m: seed.getMonth() });
77
+
78
+ const [anchor, setAnchor] = useState<string | null>(null);
79
+ const [hoverKey, setHoverKey] = useState<string | null>(null);
80
+ const [focusKey, setFocusKey] = useState<string>(seedKey);
81
+
82
+ const daysRef = useRef<HTMLDivElement>(null);
83
+ const presetsRef = useRef<HTMLDivElement>(null);
84
+ const viewIdx = view.y * 12 + view.m;
85
+ const armFocus = useDayFocus(daysRef, focusKey, viewIdx);
86
+
87
+ const uid = useId();
88
+ const gridGlide = useGlide(daysRef);
89
+ const presetGlide = useGlide(presetsRef);
90
+ const capLoRef = useSharedFlip<HTMLSpanElement>('drp-cap-lo-' + uid, { timing: UIMotion.t.settle });
91
+ const capHiRef = useSharedFlip<HTMLSpanElement>('drp-cap-hi-' + uid, { timing: UIMotion.t.settle });
92
+
93
+ const inBounds = (key: string): boolean => within(key, min, max);
94
+
95
+ let lo: string | null = null,
96
+ hi: string | null = null,
97
+ provisional = false;
98
+ if (anchor) {
99
+ const other = hoverKey || anchor;
100
+ lo = anchor < other ? anchor : other;
101
+ hi = anchor < other ? other : anchor;
102
+ provisional = true;
103
+ } else if (value && value.start && value.end) {
104
+ lo = value.start;
105
+ hi = value.end;
106
+ }
107
+
108
+ function pickDay(key: string) {
109
+ if (!inBounds(key)) return;
110
+ if (!anchor) {
111
+ setAnchor(key);
112
+ setHoverKey(key);
113
+ } else {
114
+ const start = anchor < key ? anchor : key;
115
+ const end = anchor < key ? key : anchor;
116
+ setAnchor(null);
117
+ setHoverKey(null);
118
+ commit({ start: start, end: end });
119
+ }
120
+ }
121
+
122
+ function applyPreset(p: DrpPreset) {
123
+ setAnchor(null);
124
+ setHoverKey(null);
125
+ const d = parse(p.start);
126
+ setView({ y: d.getFullYear(), m: d.getMonth() });
127
+ setFocusKey(p.start);
128
+ commit({ start: p.start, end: p.end });
129
+ }
130
+
131
+ function nav(dir: number) {
132
+ setView((v) => {
133
+ const d = new Date(v.y, v.m + dir, 1);
134
+ return { y: d.getFullYear(), m: d.getMonth() };
135
+ });
136
+ }
137
+
138
+ const prevViewIdxRef = useRef(viewIdx);
139
+ const navDir = prevViewIdxRef.current === viewIdx ? 0 : viewIdx > prevViewIdxRef.current ? 1 : -1;
140
+ useEffect(() => {
141
+ prevViewIdxRef.current = viewIdx;
142
+ }, [viewIdx]);
143
+
144
+ function moveFocus(deltaDays: number, deltaMonths = 0) {
145
+ const d = parse(focusKey);
146
+ if (deltaMonths) d.setMonth(d.getMonth() + deltaMonths);
147
+ if (deltaDays) d.setDate(d.getDate() + deltaDays);
148
+ const key = toKey(d);
149
+ armFocus();
150
+ setFocusKey(key);
151
+ if (anchor) setHoverKey(key);
152
+ const idx = d.getFullYear() * 12 + d.getMonth();
153
+ const last = viewIdx + (months - 1);
154
+ if (idx < viewIdx) setView({ y: d.getFullYear(), m: d.getMonth() });
155
+ else if (idx > last) {
156
+ const back = new Date(d.getFullYear(), d.getMonth() - (months - 1), 1);
157
+ setView({ y: back.getFullYear(), m: back.getMonth() });
158
+ }
159
+ }
160
+ function onGridKeyDown(e: KeyboardEvent<HTMLDivElement>) {
161
+ const k = e.key;
162
+ if (k === 'ArrowLeft') moveFocus(-1);
163
+ else if (k === 'ArrowRight') moveFocus(1);
164
+ else if (k === 'ArrowUp') moveFocus(-7);
165
+ else if (k === 'ArrowDown') moveFocus(7);
166
+ else if (k === 'PageUp') moveFocus(0, -1);
167
+ else if (k === 'PageDown') moveFocus(0, 1);
168
+ else if (k === 'Enter' || k === ' ') pickDay(focusKey);
169
+ else if (k === 'Escape' && anchor) {
170
+ setAnchor(null);
171
+ setHoverKey(null);
172
+ } else return;
173
+ e.preventDefault();
174
+ }
175
+
176
+ const todayKey = today();
177
+ function renderDay(d: Date, viewMonth: number) {
178
+ const key = toKey(d);
179
+ const out = d.getMonth() !== viewMonth;
180
+ const disabled = !inBounds(key);
181
+ const dayCol = col(d);
182
+
183
+ const isLo = lo && key === lo;
184
+ const isHi = hi && key === hi;
185
+ const single = lo && hi && lo === hi;
186
+ const inRange = lo && hi && key >= lo && key <= hi;
187
+ const loGhost = provisional && isLo && lo !== anchor;
188
+ const hiGhost = provisional && isHi && !single && hi !== anchor;
189
+
190
+ const band = inRange && !single;
191
+ const extL = band && !isLo && dayCol !== 0;
192
+ const extR = band && !isHi && dayCol !== 6;
193
+
194
+ const capHere = months === 1 || !out;
195
+ const loCap = isLo && capHere;
196
+ const hiCap = isHi && !single && capHere;
197
+
198
+ const cls = ['dtp__day'];
199
+ if (out) cls.push('is-out');
200
+ if (inRange) cls.push('is-in-range');
201
+ if ((loGhost || hiGhost) && capHere) cls.push('is-cap-ghost');
202
+ else if (loCap || hiCap) cls.push('is-cap');
203
+
204
+ const bandCls = ['drp__band'];
205
+ if (extL) bandCls.push('drp__band--extL');
206
+ if (extR) bandCls.push('drp__band--extR');
207
+ if (!extL) bandCls.push('drp__band--roundL');
208
+ if (!extR) bandCls.push('drp__band--roundR');
209
+
210
+ return (
211
+ <button
212
+ key={key}
213
+ type="button"
214
+ role="gridcell"
215
+ data-key={key}
216
+ className={cls.join(' ')}
217
+ disabled={disabled}
218
+ tabIndex={key === focusKey ? 0 : -1}
219
+ aria-label={MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear()}
220
+ aria-selected={isLo || isHi || undefined}
221
+ onClick={() => pickDay(key)}
222
+ onPointerEnter={(e) => {
223
+ setHoverKey(key);
224
+ if (!anchor && !disabled) gridGlide.enter(e.currentTarget);
225
+ else gridGlide.leave();
226
+ }}
227
+ >
228
+ {band ? <span className={bandCls.join(' ')} aria-hidden="true"></span> : null}
229
+ {loCap ? (
230
+ <span className={'drp__cap' + (loGhost ? ' drp__cap--ghost' : '')} aria-hidden="true" ref={capLoRef}></span>
231
+ ) : null}
232
+ {hiCap ? (
233
+ <span className={'drp__cap' + (hiGhost ? ' drp__cap--ghost' : '')} aria-hidden="true" ref={capHiRef}></span>
234
+ ) : null}
235
+ <span className="dtp__num">{d.getDate()}</span>
236
+ {key === todayKey ? <span className="dtp__dot" aria-hidden="true"></span> : null}
237
+ </button>
238
+ );
239
+ }
240
+
241
+ function renderMonth(offset: number, withPrev: boolean, withNext: boolean) {
242
+ const base = new Date(view.y, view.m + offset, 1);
243
+ const y = base.getFullYear(),
244
+ m = base.getMonth();
245
+ const cells = grid(y, m);
246
+ const prevEnd = toKey(new Date(y, m, 0));
247
+ const nextStart = toKey(new Date(y, m + 1, 1));
248
+ return (
249
+ <div className="dtp__cal" key={offset}>
250
+ <div className={'drp__mhead' + (withNext && !withPrev ? ' drp__mhead--right' : '')}>
251
+ {withPrev ? (
252
+ <button
253
+ type="button"
254
+ className="dtp__nav"
255
+ aria-label="Previous month"
256
+ disabled={min && prevEnd < min}
257
+ onClick={() => nav(-1)}
258
+ >
259
+ <Icon name="caret-left" size="sm" />
260
+ </button>
261
+ ) : null}
262
+ <span className="dtp__month">
263
+ {MONTHS[m]} <span className="dtp__year">{y}</span>
264
+ </span>
265
+ {withNext ? (
266
+ <button
267
+ type="button"
268
+ className="dtp__nav"
269
+ aria-label="Next month"
270
+ disabled={max && nextStart > max}
271
+ onClick={() => nav(1)}
272
+ >
273
+ <Icon name="caret-right" size="sm" />
274
+ </button>
275
+ ) : null}
276
+ </div>
277
+ <div className="dtp__dow" aria-hidden="true">
278
+ {DOW.map((d) => (
279
+ <span key={d}>{d}</span>
280
+ ))}
281
+ </div>
282
+ <div
283
+ className="dtp__days"
284
+ role="grid"
285
+ aria-label={MONTHS[m] + ' ' + y}
286
+ key={'days-' + viewIdx + '-' + offset}
287
+ data-enter={navDir || undefined}
288
+ >
289
+ {cells.map((d) => renderDay(d, m))}
290
+ </div>
291
+ </div>
292
+ );
293
+ }
294
+
295
+ const presets = drpPresets();
296
+ function presetActive(p: DrpPreset): boolean {
297
+ return value && value.start === p.start && value.end === p.end;
298
+ }
299
+
300
+ return (
301
+ <div
302
+ className={'drp' + (layout === 'sheet' ? ' drp--sheet' : '')}
303
+ role="dialog"
304
+ aria-label={label || 'Pick a date range'}
305
+ >
306
+ <div className="drp__body">
307
+ <div
308
+ className="drp__presets"
309
+ role="group"
310
+ aria-label="Quick ranges"
311
+ ref={presetsRef}
312
+ onPointerLeave={presetGlide.leave}
313
+ >
314
+ {presets.map((p) => (
315
+ <button
316
+ key={p.id}
317
+ type="button"
318
+ className={'drp__preset' + (presetActive(p) ? ' is-active' : '')}
319
+ onPointerEnter={(e) => presetGlide.enter(e.currentTarget)}
320
+ onClick={() => applyPreset(p)}
321
+ >
322
+ {p.label}
323
+ </button>
324
+ ))}
325
+ <GlidePill className="drp__presetGlide" glide={presetGlide} />
326
+ </div>
327
+ <div
328
+ className="drp__months"
329
+ ref={daysRef}
330
+ onKeyDown={onGridKeyDown}
331
+ onPointerLeave={() => {
332
+ if (anchor) setHoverKey(anchor);
333
+ gridGlide.leave();
334
+ }}
335
+ >
336
+ {months === 2 ? [renderMonth(0, true, false), renderMonth(1, false, true)] : renderMonth(0, true, true)}
337
+ <GlidePill className="dtp__hover" glide={gridGlide} />
338
+ </div>
339
+ </div>
340
+ <div className="drp__foot">
341
+ <span className={'drp__readout' + (lo && hi ? '' : ' is-empty')}>
342
+ {lo && hi ? (
343
+ <Fragment>
344
+ {drpRangeText(lo, hi)}{' '}
345
+ <span className="drp__count">
346
+ - {drpDays(lo, hi)} {drpDays(lo, hi) === 1 ? 'day' : 'days'}
347
+ </span>
348
+ </Fragment>
349
+ ) : anchor ? (
350
+ 'Pick the end date'
351
+ ) : (
352
+ 'Pick a start date'
353
+ )}
354
+ </span>
355
+ {timezone ? <span className="drp__tz">{tzLabel(timezone)}</span> : null}
356
+ <span className="drp__footSpacer"></span>
357
+ <Button variant="primary" size="sm" onClick={close}>
358
+ Done
359
+ </Button>
360
+ </div>
361
+ </div>
362
+ );
363
+ }