@passui/react 0.0.0-stage → 0.1.14

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 (275) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -2
  3. package/package.json +96 -5
  4. package/src/appearance/index.ts +2 -0
  5. package/src/appearance/platform.tsx +123 -0
  6. package/src/appearance/theme.tsx +535 -0
  7. package/src/components/ambient/ambient.docs.tsx +68 -0
  8. package/src/components/ambient/ambient.tsx +43 -0
  9. package/src/components/ambient/examples/ambient-images.tsx +30 -0
  10. package/src/components/ambient/examples/ambient-now-playing.tsx +55 -0
  11. package/src/components/ambient/examples/ambient-showcase.tsx +33 -0
  12. package/src/components/ambient/examples/ambient-usage.tsx +19 -0
  13. package/src/components/ambient/index.ts +1 -0
  14. package/src/components/button/button-styles.ts +60 -0
  15. package/src/components/button/button.docs.tsx +116 -0
  16. package/src/components/button/button.tsx +91 -0
  17. package/src/components/button/examples/button-disabled.tsx +19 -0
  18. package/src/components/button/examples/button-icon.tsx +39 -0
  19. package/src/components/button/examples/button-link.tsx +12 -0
  20. package/src/components/button/examples/button-pending.tsx +27 -0
  21. package/src/components/button/examples/button-showcase.tsx +115 -0
  22. package/src/components/button/examples/button-sizes.tsx +12 -0
  23. package/src/components/button/examples/button-usage.tsx +12 -0
  24. package/src/components/button/examples/button-variants.tsx +15 -0
  25. package/src/components/button/examples/button-with-icon.tsx +32 -0
  26. package/src/components/button/examples/icon-button-player.tsx +38 -0
  27. package/src/components/button/examples/icon-button-showcase.tsx +83 -0
  28. package/src/components/button/examples/icon-button-usage.tsx +11 -0
  29. package/src/components/button/examples/icon-button-variants.tsx +39 -0
  30. package/src/components/button/icon-button.docs.tsx +68 -0
  31. package/src/components/button/icon-button.tsx +73 -0
  32. package/src/components/button/pending-spinner.tsx +14 -0
  33. package/src/components/button/use-pending-indicator.ts +27 -0
  34. package/src/components/chip/chip.docs.tsx +37 -0
  35. package/src/components/chip/chip.tsx +35 -0
  36. package/src/components/chip/examples/chip-in-rows.tsx +19 -0
  37. package/src/components/chip/examples/chip-showcase.tsx +51 -0
  38. package/src/components/chip/examples/chip-tones.tsx +13 -0
  39. package/src/components/chip/examples/chip-usage.tsx +5 -0
  40. package/src/components/compact-date-picker/calendar-month.ts +131 -0
  41. package/src/components/compact-date-picker/compact-date-picker.docs.tsx +81 -0
  42. package/src/components/compact-date-picker/compact-date-picker.tsx +438 -0
  43. package/src/components/compact-date-picker/examples/compact-date-picker-showcase.tsx +35 -0
  44. package/src/components/compact-date-picker/examples/compact-date-picker-usage.tsx +22 -0
  45. package/src/components/compact-time-picker/compact-time-picker.docs.tsx +80 -0
  46. package/src/components/compact-time-picker/compact-time-picker.tsx +432 -0
  47. package/src/components/compact-time-picker/examples/compact-time-picker-showcase.tsx +31 -0
  48. package/src/components/compact-time-picker/examples/compact-time-picker-usage.tsx +21 -0
  49. package/src/components/compact-time-picker/time-value.ts +67 -0
  50. package/src/components/content-view/content-view.docs.tsx +61 -0
  51. package/src/components/content-view/content-view.tsx +197 -0
  52. package/src/components/content-view/examples/content-view-header-action.tsx +33 -0
  53. package/src/components/content-view/examples/content-view-showcase.tsx +65 -0
  54. package/src/components/content-view/examples/content-view-stats.tsx +35 -0
  55. package/src/components/content-view/examples/content-view-usage.tsx +14 -0
  56. package/src/components/content-view/index.ts +1 -0
  57. package/src/components/context-menu/conceal-menu-source.ts +33 -0
  58. package/src/components/context-menu/context-menu-host.tsx +445 -0
  59. package/src/components/context-menu/context-menu-store.ts +165 -0
  60. package/src/components/context-menu/context-menu.docs.tsx +92 -0
  61. package/src/components/context-menu/examples/context-menu-checked.tsx +37 -0
  62. package/src/components/context-menu/examples/context-menu-list.tsx +85 -0
  63. package/src/components/context-menu/examples/context-menu-showcase.tsx +56 -0
  64. package/src/components/context-menu/examples/context-menu-toolbar.tsx +63 -0
  65. package/src/components/context-menu/examples/context-menu-usage.tsx +24 -0
  66. package/src/components/context-menu/menu-position.ts +55 -0
  67. package/src/components/context-menu/menu-reveal.ts +75 -0
  68. package/src/components/context-menu/use-context-menu.ts +90 -0
  69. package/src/components/conversation/conversation.docs.tsx +88 -0
  70. package/src/components/conversation/conversation.tsx +279 -0
  71. package/src/components/conversation/examples/conversation-composer.tsx +36 -0
  72. package/src/components/conversation/examples/conversation-days.tsx +48 -0
  73. package/src/components/conversation/examples/conversation-showcase.tsx +71 -0
  74. package/src/components/conversation/examples/conversation-usage.tsx +28 -0
  75. package/src/components/conversation/index.ts +1 -0
  76. package/src/components/date-picker/date-picker.docs.tsx +88 -0
  77. package/src/components/date-picker/date-picker.tsx +103 -0
  78. package/src/components/date-picker/date-value.ts +37 -0
  79. package/src/components/date-picker/examples/date-picker-locales.tsx +41 -0
  80. package/src/components/date-picker/examples/date-picker-past-only.tsx +29 -0
  81. package/src/components/date-picker/examples/date-picker-showcase.tsx +49 -0
  82. package/src/components/date-picker/examples/date-picker-usage.tsx +22 -0
  83. package/src/components/dialog/dialog-host.tsx +156 -0
  84. package/src/components/dialog/dialog-store.ts +56 -0
  85. package/src/components/dialog/dialog.docs.tsx +114 -0
  86. package/src/components/dialog/dialog.tsx +200 -0
  87. package/src/components/dialog/examples/dialog-confirm.tsx +24 -0
  88. package/src/components/dialog/examples/dialog-form.tsx +46 -0
  89. package/src/components/dialog/examples/dialog-inline.tsx +48 -0
  90. package/src/components/dialog/examples/dialog-showcase.tsx +53 -0
  91. package/src/components/dialog/examples/dialog-usage.tsx +31 -0
  92. package/src/components/dialog/index.ts +1 -0
  93. package/src/components/labeled-content/examples/labeled-content-descriptions.tsx +37 -0
  94. package/src/components/labeled-content/examples/labeled-content-showcase.tsx +54 -0
  95. package/src/components/labeled-content/examples/labeled-content-usage.tsx +22 -0
  96. package/src/components/labeled-content/examples/labeled-content-values.tsx +26 -0
  97. package/src/components/labeled-content/examples/labeled-content-wide.tsx +46 -0
  98. package/src/components/labeled-content/labeled-content.docs.tsx +69 -0
  99. package/src/components/labeled-content/labeled-content.tsx +66 -0
  100. package/src/components/locale-switcher/examples/locale-switcher-native-names.tsx +30 -0
  101. package/src/components/locale-switcher/examples/locale-switcher-showcase.tsx +46 -0
  102. package/src/components/locale-switcher/examples/locale-switcher-translated-page.tsx +35 -0
  103. package/src/components/locale-switcher/examples/locale-switcher-usage.tsx +20 -0
  104. package/src/components/locale-switcher/index.ts +1 -0
  105. package/src/components/locale-switcher/locale-switcher.docs.tsx +91 -0
  106. package/src/components/locale-switcher/locale-switcher.tsx +252 -0
  107. package/src/components/picker/examples/picker-details.tsx +44 -0
  108. package/src/components/picker/examples/picker-searchable.tsx +48 -0
  109. package/src/components/picker/examples/picker-showcase.tsx +66 -0
  110. package/src/components/picker/examples/picker-usage.tsx +20 -0
  111. package/src/components/picker/picker.docs.tsx +68 -0
  112. package/src/components/picker/picker.tsx +493 -0
  113. package/src/components/popover/examples/popover-help.tsx +57 -0
  114. package/src/components/popover/examples/popover-menu.tsx +51 -0
  115. package/src/components/popover/examples/popover-showcase.tsx +61 -0
  116. package/src/components/popover/examples/popover-usage.tsx +31 -0
  117. package/src/components/popover/popover.docs.tsx +84 -0
  118. package/src/components/popover/popover.tsx +138 -0
  119. package/src/components/profile-image/examples/profile-image-fallbacks.tsx +29 -0
  120. package/src/components/profile-image/examples/profile-image-group.tsx +34 -0
  121. package/src/components/profile-image/examples/profile-image-showcase.tsx +30 -0
  122. package/src/components/profile-image/examples/profile-image-usage.tsx +14 -0
  123. package/src/components/profile-image/profile-image.docs.tsx +81 -0
  124. package/src/components/profile-image/profile-image.tsx +80 -0
  125. package/src/components/search-field/examples/search-field-filter.tsx +53 -0
  126. package/src/components/search-field/examples/search-field-showcase.tsx +56 -0
  127. package/src/components/search-field/examples/search-field-toolbar.tsx +27 -0
  128. package/src/components/search-field/examples/search-field-usage.tsx +18 -0
  129. package/src/components/search-field/search-field.docs.tsx +58 -0
  130. package/src/components/search-field/search-field.tsx +29 -0
  131. package/src/components/search-list/examples/search-list-dropdown.tsx +53 -0
  132. package/src/components/search-list/examples/search-list-groups.tsx +64 -0
  133. package/src/components/search-list/examples/search-list-showcase.tsx +67 -0
  134. package/src/components/search-list/examples/search-list-usage.tsx +27 -0
  135. package/src/components/search-list/search-list.docs.tsx +87 -0
  136. package/src/components/search-list/search-list.tsx +265 -0
  137. package/src/components/section/examples/section-actions.tsx +35 -0
  138. package/src/components/section/examples/section-groups.tsx +41 -0
  139. package/src/components/section/examples/section-showcase.tsx +63 -0
  140. package/src/components/section/examples/section-usage.tsx +19 -0
  141. package/src/components/section/section.docs.tsx +51 -0
  142. package/src/components/section/section.tsx +41 -0
  143. package/src/components/secure-field/examples/secure-field-new-password.tsx +56 -0
  144. package/src/components/secure-field/examples/secure-field-reveal.tsx +20 -0
  145. package/src/components/secure-field/examples/secure-field-showcase.tsx +62 -0
  146. package/src/components/secure-field/examples/secure-field-usage.tsx +19 -0
  147. package/src/components/secure-field/secure-field.docs.tsx +72 -0
  148. package/src/components/secure-field/secure-field.tsx +10 -0
  149. package/src/components/segmented-control/examples/segmented-control-overflow.tsx +28 -0
  150. package/src/components/segmented-control/examples/segmented-control-showcase.tsx +59 -0
  151. package/src/components/segmented-control/examples/segmented-control-usage.tsx +20 -0
  152. package/src/components/segmented-control/examples/segmented-control-views.tsx +32 -0
  153. package/src/components/segmented-control/segmented-control.docs.tsx +61 -0
  154. package/src/components/segmented-control/segmented-control.tsx +122 -0
  155. package/src/components/sheet/examples/sheet-choices.tsx +58 -0
  156. package/src/components/sheet/examples/sheet-share.tsx +51 -0
  157. package/src/components/sheet/examples/sheet-showcase.tsx +74 -0
  158. package/src/components/sheet/examples/sheet-usage.tsx +32 -0
  159. package/src/components/sheet/modal-environment.ts +113 -0
  160. package/src/components/sheet/sheet-portal.tsx +89 -0
  161. package/src/components/sheet/sheet.docs.tsx +75 -0
  162. package/src/components/sheet/sheet.tsx +112 -0
  163. package/src/components/sheet/use-sheet-focus.ts +130 -0
  164. package/src/components/sidebar/examples/sidebar-groups.tsx +133 -0
  165. package/src/components/sidebar/examples/sidebar-showcase.tsx +123 -0
  166. package/src/components/sidebar/examples/sidebar-unread.tsx +66 -0
  167. package/src/components/sidebar/examples/sidebar-usage.tsx +52 -0
  168. package/src/components/sidebar/sidebar.docs.tsx +135 -0
  169. package/src/components/sidebar/sidebar.tsx +69 -0
  170. package/src/components/stat-tile/examples/stat-tile-filters.tsx +32 -0
  171. package/src/components/stat-tile/examples/stat-tile-grid.tsx +21 -0
  172. package/src/components/stat-tile/examples/stat-tile-showcase.tsx +52 -0
  173. package/src/components/stat-tile/examples/stat-tile-usage.tsx +13 -0
  174. package/src/components/stat-tile/index.ts +1 -0
  175. package/src/components/stat-tile/stat-tile.docs.tsx +60 -0
  176. package/src/components/stat-tile/stat-tile.tsx +68 -0
  177. package/src/components/status-indicator/examples/status-indicator-live.tsx +34 -0
  178. package/src/components/status-indicator/examples/status-indicator-showcase.tsx +39 -0
  179. package/src/components/status-indicator/examples/status-indicator-states.tsx +17 -0
  180. package/src/components/status-indicator/examples/status-indicator-usage.tsx +9 -0
  181. package/src/components/status-indicator/status-indicator.docs.tsx +43 -0
  182. package/src/components/status-indicator/status-indicator.tsx +35 -0
  183. package/src/components/swatch-picker/examples/swatch-picker-accents.tsx +42 -0
  184. package/src/components/swatch-picker/examples/swatch-picker-labels.tsx +45 -0
  185. package/src/components/swatch-picker/examples/swatch-picker-showcase.tsx +70 -0
  186. package/src/components/swatch-picker/examples/swatch-picker-usage.tsx +39 -0
  187. package/src/components/swatch-picker/swatch-picker.docs.tsx +60 -0
  188. package/src/components/swatch-picker/swatch-picker.tsx +96 -0
  189. package/src/components/tab-bar/examples/tab-bar-five-tabs.tsx +66 -0
  190. package/src/components/tab-bar/examples/tab-bar-minimize.tsx +71 -0
  191. package/src/components/tab-bar/examples/tab-bar-showcase.tsx +82 -0
  192. package/src/components/tab-bar/examples/tab-bar-usage.tsx +54 -0
  193. package/src/components/tab-bar/tab-bar.docs.tsx +136 -0
  194. package/src/components/tab-bar/tab-bar.tsx +148 -0
  195. package/src/components/tab-view/examples/tab-view-keyboard.tsx +39 -0
  196. package/src/components/tab-view/examples/tab-view-settings.tsx +56 -0
  197. package/src/components/tab-view/examples/tab-view-showcase.tsx +53 -0
  198. package/src/components/tab-view/examples/tab-view-usage.tsx +23 -0
  199. package/src/components/tab-view/tab-view.docs.tsx +77 -0
  200. package/src/components/tab-view/tab-view.tsx +117 -0
  201. package/src/components/table/examples/table-empty.tsx +53 -0
  202. package/src/components/table/examples/table-invoices.tsx +102 -0
  203. package/src/components/table/examples/table-selection.tsx +150 -0
  204. package/src/components/table/examples/table-showcase.tsx +115 -0
  205. package/src/components/table/examples/table-usage.tsx +67 -0
  206. package/src/components/table/examples/table-wide.tsx +75 -0
  207. package/src/components/table/table.docs.tsx +131 -0
  208. package/src/components/table/table.tsx +149 -0
  209. package/src/components/text-editor/examples/text-editor-comment.tsx +48 -0
  210. package/src/components/text-editor/examples/text-editor-count.tsx +38 -0
  211. package/src/components/text-editor/examples/text-editor-showcase.tsx +58 -0
  212. package/src/components/text-editor/examples/text-editor-usage.tsx +25 -0
  213. package/src/components/text-editor/text-editor.docs.tsx +61 -0
  214. package/src/components/text-editor/text-editor.tsx +19 -0
  215. package/src/components/text-field/examples/text-field-icons.tsx +72 -0
  216. package/src/components/text-field/examples/text-field-showcase.tsx +50 -0
  217. package/src/components/text-field/examples/text-field-usage.tsx +18 -0
  218. package/src/components/text-field/examples/text-field-validation.tsx +27 -0
  219. package/src/components/text-field/text-field.docs.tsx +118 -0
  220. package/src/components/text-field/text-field.tsx +135 -0
  221. package/src/components/toast/examples/toast-action.tsx +32 -0
  222. package/src/components/toast/examples/toast-promise.tsx +45 -0
  223. package/src/components/toast/examples/toast-showcase.tsx +49 -0
  224. package/src/components/toast/examples/toast-usage.tsx +13 -0
  225. package/src/components/toast/toast-store.ts +166 -0
  226. package/src/components/toast/toast.docs.tsx +79 -0
  227. package/src/components/toast/toaster.tsx +374 -0
  228. package/src/components/toggle/examples/toggle-checkboxes.tsx +43 -0
  229. package/src/components/toggle/examples/toggle-settings.tsx +31 -0
  230. package/src/components/toggle/examples/toggle-showcase.tsx +59 -0
  231. package/src/components/toggle/examples/toggle-usage.tsx +13 -0
  232. package/src/components/toggle/toggle.docs.tsx +69 -0
  233. package/src/components/toggle/toggle.tsx +85 -0
  234. package/src/components/tooltip/examples/tooltip-icon-buttons.tsx +27 -0
  235. package/src/components/tooltip/examples/tooltip-shortcuts.tsx +38 -0
  236. package/src/components/tooltip/examples/tooltip-showcase.tsx +44 -0
  237. package/src/components/tooltip/examples/tooltip-usage.tsx +13 -0
  238. package/src/components/tooltip/tooltip-host.tsx +156 -0
  239. package/src/components/tooltip/tooltip.docs.tsx +56 -0
  240. package/src/components/view-controller/examples/view-controller-action.tsx +27 -0
  241. package/src/components/view-controller/examples/view-controller-settings.tsx +31 -0
  242. package/src/components/view-controller/examples/view-controller-showcase.tsx +45 -0
  243. package/src/components/view-controller/examples/view-controller-usage.tsx +19 -0
  244. package/src/components/view-controller/index.ts +1 -0
  245. package/src/components/view-controller/view-controller.docs.tsx +71 -0
  246. package/src/components/view-controller/view-controller.tsx +81 -0
  247. package/src/docs/index.ts +3 -0
  248. package/src/docs/registry.ts +113 -0
  249. package/src/docs/snippet.ts +17 -0
  250. package/src/docs/types.ts +180 -0
  251. package/src/hooks/scroll/minimize-decision.ts +13 -0
  252. package/src/hooks/use-anchored-placement.ts +43 -0
  253. package/src/hooks/use-dismiss.ts +58 -0
  254. package/src/hooks/use-media-query.ts +106 -0
  255. package/src/hooks/use-minimize-on-scroll.ts +92 -0
  256. package/src/hooks/use-visual-viewport.ts +76 -0
  257. package/src/hooks/viewport/types.ts +19 -0
  258. package/src/hooks/viewport/viewport-metrics.ts +25 -0
  259. package/src/icons/icons.tsx +298 -0
  260. package/src/index.ts +78 -0
  261. package/src/lib/anchored-placement.ts +92 -0
  262. package/src/lib/class-names.ts +21 -0
  263. package/src/lib/date.ts +96 -0
  264. package/src/lib/floating-layers.ts +28 -0
  265. package/src/lib/haptics.ts +25 -0
  266. package/src/lib/language.ts +21 -0
  267. package/src/lib/screen-edges.ts +45 -0
  268. package/src/lib/shapes.ts +70 -0
  269. package/src/motion/transitions.ts +30 -0
  270. package/src/styles/base.css +1 -0
  271. package/src/styles/styles.css +3 -0
  272. package/src/styles/theme.css +1 -0
  273. package/src/styles/typeset.css +1 -0
  274. package/src/styles/utilities.css +1 -0
  275. package/src/tokens.ts +1 -0
@@ -0,0 +1,81 @@
1
+ import type { ComponentProps } from "react"
2
+ import { useState } from "react"
3
+ import { defineDocs } from "../../docs/types"
4
+ import { CompactDatePicker } from "./compact-date-picker"
5
+ import { CompactDatePickerShowcase } from "./examples/compact-date-picker-showcase"
6
+ import { CompactDatePickerUsage } from "./examples/compact-date-picker-usage"
7
+
8
+ type CompactDatePickerProps = ComponentProps<
9
+ typeof CompactDatePicker
10
+ >
11
+
12
+ // Locales whose weeks start on different days, so the playground shows it.
13
+ type DemoProps = Pick<CompactDatePickerProps, "label"> & {
14
+ locale: "en-US" | "en-GB" | "fr-FR" | "ar-EG" | "zh-CN"
15
+ }
16
+
17
+ const labels = {
18
+ previousMonth: "Previous month",
19
+ nextMonth: "Next month",
20
+ clear: "Remove date",
21
+ }
22
+
23
+ function CompactDatePickerDemo({ locale, label }: DemoProps) {
24
+ const [date, setDate] = useState("")
25
+ return (
26
+ <div
27
+ className="w-full max-w-xs"
28
+ dir={locale === "ar-EG" ? "rtl" : undefined}
29
+ >
30
+ <CompactDatePicker
31
+ key={locale}
32
+ label={label}
33
+ labels={labels}
34
+ locale={locale}
35
+ placeholder="None"
36
+ value={date}
37
+ onChange={setDate}
38
+ />
39
+ </div>
40
+ )
41
+ }
42
+
43
+ export const compactDatePickerDocs = defineDocs<DemoProps>()({
44
+ slug: "compact-date-picker",
45
+ name: "CompactDatePicker",
46
+ importPath: "@passui/react/compact-date-picker",
47
+ controls: {
48
+ label: {
49
+ kind: "text",
50
+ default: "Starts",
51
+ required: true,
52
+ },
53
+ locale: {
54
+ kind: "select",
55
+ options: {
56
+ "en-US": true,
57
+ "en-GB": true,
58
+ "fr-FR": true,
59
+ "ar-EG": true,
60
+ "zh-CN": true,
61
+ },
62
+ default: "en-US",
63
+ },
64
+ },
65
+ render: (values) => <CompactDatePickerDemo {...values} />,
66
+ childrenSnippet: ({ locale }) => ({
67
+ imports: ['import { useState } from "react"'],
68
+ setup: ['const [date, setDate] = useState("")'],
69
+ children: "",
70
+ omit: ["locale"],
71
+ extraProps: { locale },
72
+ expressionProps: {
73
+ labels: '{ previousMonth: "Previous month", nextMonth: "Next month", clear: "Remove date" }',
74
+ value: "date",
75
+ onChange: "setDate",
76
+ },
77
+ }),
78
+ usage: CompactDatePickerUsage,
79
+ showcase: CompactDatePickerShowcase,
80
+ examples: [],
81
+ })
@@ -0,0 +1,438 @@
1
+ import {
2
+ AnimatePresence,
3
+ motion,
4
+ useReducedMotion,
5
+ } from "motion/react"
6
+ import {
7
+ type KeyboardEvent,
8
+ useEffect,
9
+ useId,
10
+ useLayoutEffect,
11
+ useMemo,
12
+ useRef,
13
+ useState,
14
+ } from "react"
15
+ import { createPortal } from "react-dom"
16
+ import { FLOATING_LAYER } from "../../hooks/use-dismiss"
17
+ import {
18
+ CalendarIcon,
19
+ ChevronLeftIcon,
20
+ ChevronRightIcon,
21
+ } from "../../icons/icons"
22
+ import { cn } from "../../lib/class-names"
23
+ import { registerFloatingLayer } from "../../lib/floating-layers"
24
+ import { haptic } from "../../lib/haptics"
25
+ import { useAnchoredPlacement } from "../../hooks/use-anchored-placement"
26
+ import { spring } from "../../motion/transitions"
27
+ import { IconButton } from "../button/icon-button"
28
+ import {
29
+ addDays,
30
+ addMonths,
31
+ type CalendarDay,
32
+ firstWeekday,
33
+ formatDay,
34
+ monthGrid,
35
+ parseDay,
36
+ withinBounds,
37
+ } from "./calendar-month"
38
+
39
+ export interface CompactDatePickerProps {
40
+ /** `YYYY-MM-DD`, or "" when no date is chosen. */
41
+ value: string
42
+ onChange: (value: string) => void
43
+ /** Names the button and the calendar for assistive technology. */
44
+ label: string
45
+ locale: string
46
+ min?: string
47
+ max?: string
48
+ /** Shown when no date is chosen. */
49
+ placeholder?: string
50
+ labels: {
51
+ previousMonth: string
52
+ nextMonth: string
53
+ /** Offers to remove the date, for optional dates. */
54
+ clear?: string
55
+ }
56
+ className?: string
57
+ }
58
+
59
+ const WIDTH = 308
60
+
61
+ const toDate = ({ year, month, day }: CalendarDay) =>
62
+ new Date(Date.UTC(year, month - 1, day))
63
+
64
+ function todayDay(): CalendarDay {
65
+ const now = new Date()
66
+ return {
67
+ year: now.getFullYear(),
68
+ month: now.getMonth() + 1,
69
+ day: now.getDate(),
70
+ }
71
+ }
72
+
73
+ /**
74
+ * A date as a compact button that opens a month calendar, like SwiftUI's
75
+ * compact DatePicker: for dates on a calendar, such as an event or an
76
+ * invoice. For dates people know by heart, like a birthday, use DatePicker.
77
+ */
78
+ export function CompactDatePicker({
79
+ value,
80
+ onChange,
81
+ label,
82
+ locale,
83
+ min,
84
+ max,
85
+ placeholder,
86
+ labels,
87
+ className,
88
+ }: CompactDatePickerProps) {
89
+ const [open, setOpen] = useState(false)
90
+ const trigger = useRef<HTMLButtonElement>(null)
91
+ const calendarId = useId()
92
+ const chosen = parseDay(value)
93
+ const shown = chosen
94
+ ? new Intl.DateTimeFormat(locale, {
95
+ dateStyle: "medium",
96
+ timeZone: "UTC",
97
+ }).format(toDate(chosen))
98
+ : (placeholder ?? "")
99
+
100
+ const close = () => {
101
+ setOpen(false)
102
+ trigger.current?.focus()
103
+ }
104
+
105
+ return (
106
+ <>
107
+ <button
108
+ ref={trigger}
109
+ type="button"
110
+ aria-label={
111
+ chosen ? `${label}, ${shown}` : label
112
+ }
113
+ aria-haspopup="dialog"
114
+ aria-expanded={open}
115
+ aria-controls={open ? calendarId : undefined}
116
+ onClick={() => setOpen((current) => !current)}
117
+ className={cn(
118
+ "flex w-full min-w-0 items-center gap-2 rounded-lg control-field px-3 py-2 text-start text-subheadline text-label",
119
+ className
120
+ )}
121
+ >
122
+ <span
123
+ className={cn(
124
+ "min-w-0 grow truncate tabular-nums",
125
+ !chosen && "text-label-tertiary"
126
+ )}
127
+ >
128
+ {shown}
129
+ </span>
130
+ <CalendarIcon
131
+ size={16}
132
+ className="shrink-0 text-label-tertiary"
133
+ />
134
+ </button>
135
+ {typeof document !== "undefined" &&
136
+ createPortal(
137
+ <AnimatePresence>
138
+ {open && trigger.current && (
139
+ <MonthCalendar
140
+ id={calendarId}
141
+ anchor={trigger.current}
142
+ label={label}
143
+ locale={locale}
144
+ value={value}
145
+ min={min}
146
+ max={max}
147
+ labels={labels}
148
+ onChoose={(next) => {
149
+ haptic("selection")
150
+ onChange(next)
151
+ close()
152
+ }}
153
+ onClose={close}
154
+ />
155
+ )}
156
+ </AnimatePresence>,
157
+ document.body
158
+ )}
159
+ </>
160
+ )
161
+ }
162
+
163
+ function MonthCalendar({
164
+ id,
165
+ anchor,
166
+ label,
167
+ locale,
168
+ value,
169
+ min,
170
+ max,
171
+ labels,
172
+ onChoose,
173
+ onClose,
174
+ }: {
175
+ id: string
176
+ anchor: HTMLElement
177
+ label: string
178
+ locale: string
179
+ value: string
180
+ min?: string
181
+ max?: string
182
+ labels: CompactDatePickerProps["labels"]
183
+ onChoose: (value: string) => void
184
+ onClose: () => void
185
+ }) {
186
+ const root = useRef<HTMLDivElement>(null)
187
+ const reducedMotion = useReducedMotion()
188
+ const placement = useAnchoredPlacement(anchor, WIDTH, 360)
189
+ const today = todayDay()
190
+ const [focused, setFocused] = useState<CalendarDay>(
191
+ () => parseDay(value) ?? today
192
+ )
193
+ const firstDay = useMemo(
194
+ () => firstWeekday(locale),
195
+ [locale]
196
+ )
197
+ const days = monthGrid(
198
+ focused.year,
199
+ focused.month,
200
+ firstDay
201
+ )
202
+ const title = new Intl.DateTimeFormat(locale, {
203
+ month: "long",
204
+ year: "numeric",
205
+ timeZone: "UTC",
206
+ }).format(toDate({ ...focused, day: 1 }))
207
+ const weekdays = days.slice(0, 7).map((day) =>
208
+ new Intl.DateTimeFormat(locale, {
209
+ weekday: "narrow",
210
+ timeZone: "UTC",
211
+ }).format(toDate(day))
212
+ )
213
+ const fullDate = new Intl.DateTimeFormat(locale, {
214
+ dateStyle: "full",
215
+ timeZone: "UTC",
216
+ })
217
+
218
+ useLayoutEffect(() => {
219
+ if (!root.current) return
220
+ return registerFloatingLayer(root.current, anchor)
221
+ }, [anchor])
222
+
223
+ // The focused day takes keyboard focus as it moves.
224
+ useEffect(() => {
225
+ root.current
226
+ ?.querySelector<HTMLButtonElement>(
227
+ `[data-day="${formatDay(focused)}"]`
228
+ )
229
+ ?.focus()
230
+ }, [focused])
231
+
232
+ useEffect(() => {
233
+ const onPointer = (event: PointerEvent) => {
234
+ const target = event.target as Node
235
+ if (
236
+ !root.current?.contains(target) &&
237
+ !anchor.contains(target)
238
+ )
239
+ onClose()
240
+ }
241
+ document.addEventListener("pointerdown", onPointer)
242
+ return () =>
243
+ document.removeEventListener(
244
+ "pointerdown",
245
+ onPointer
246
+ )
247
+ }, [anchor, onClose])
248
+
249
+ const move = (next: CalendarDay) => setFocused(next)
250
+
251
+ const onKeyDown = (
252
+ event: KeyboardEvent<HTMLDivElement>
253
+ ) => {
254
+ const rtl =
255
+ getComputedStyle(event.currentTarget)
256
+ .direction === "rtl"
257
+ ? -1
258
+ : 1
259
+ const keys: Record<string, () => void> = {
260
+ ArrowLeft: () => move(addDays(focused, -rtl)),
261
+ ArrowRight: () => move(addDays(focused, rtl)),
262
+ ArrowUp: () => move(addDays(focused, -7)),
263
+ ArrowDown: () => move(addDays(focused, 7)),
264
+ PageUp: () =>
265
+ move(
266
+ addMonths(
267
+ focused,
268
+ event.shiftKey ? -12 : -1
269
+ )
270
+ ),
271
+ PageDown: () =>
272
+ move(
273
+ addMonths(
274
+ focused,
275
+ event.shiftKey ? 12 : 1
276
+ )
277
+ ),
278
+ Escape: onClose,
279
+ }
280
+ const action = keys[event.key]
281
+ if (!action) return
282
+ event.preventDefault()
283
+ event.stopPropagation()
284
+ action()
285
+ }
286
+
287
+ return (
288
+ <motion.div
289
+ ref={root}
290
+ id={id}
291
+ role="dialog"
292
+ aria-label={label}
293
+ initial={
294
+ reducedMotion
295
+ ? { opacity: 0 }
296
+ : {
297
+ opacity: 0,
298
+ scale: 0.96,
299
+ y: placement.above ? 6 : -6,
300
+ }
301
+ }
302
+ animate={{ opacity: 1, scale: 1, y: 0 }}
303
+ exit={
304
+ reducedMotion
305
+ ? { opacity: 0 }
306
+ : {
307
+ opacity: 0,
308
+ scale: 0.98,
309
+ transition: { duration: 0.1 },
310
+ }
311
+ }
312
+ transition={
313
+ reducedMotion
314
+ ? { duration: 0 }
315
+ : spring.smooth
316
+ }
317
+ style={{
318
+ left: placement.left,
319
+ width: placement.width,
320
+ top: placement.top,
321
+ bottom: placement.bottom,
322
+ maxHeight: placement.maxHeight,
323
+ overflowY: placement.maxHeight
324
+ ? "auto"
325
+ : undefined,
326
+ transformOrigin: placement.above
327
+ ? "50% 100%"
328
+ : "50% 0%",
329
+ }}
330
+ className="glass-menu fixed z-[80] flex flex-col gap-2 rounded-3xl p-3"
331
+ {...{ [FLOATING_LAYER]: "" }}
332
+ onKeyDown={onKeyDown}
333
+ >
334
+ <div className="flex items-center justify-between gap-2 ps-2">
335
+ <p
336
+ className="text-headline font-semibold"
337
+ aria-live="polite"
338
+ >
339
+ {title}
340
+ </p>
341
+ <div className="flex items-center rtl:flex-row-reverse">
342
+ <IconButton
343
+ variant="ghost"
344
+ size="icon-sm"
345
+ label={labels.previousMonth}
346
+ onClick={() =>
347
+ move(addMonths(focused, -1))
348
+ }
349
+ className="text-accent-text"
350
+ >
351
+ <ChevronLeftIcon size={20} />
352
+ </IconButton>
353
+ <IconButton
354
+ variant="ghost"
355
+ size="icon-sm"
356
+ label={labels.nextMonth}
357
+ onClick={() =>
358
+ move(addMonths(focused, 1))
359
+ }
360
+ className="text-accent-text"
361
+ >
362
+ <ChevronRightIcon size={20} />
363
+ </IconButton>
364
+ </div>
365
+ </div>
366
+ <div
367
+ role="grid"
368
+ aria-label={title}
369
+ className="grid grid-cols-7 gap-y-0.5"
370
+ >
371
+ <div role="row" className="contents">
372
+ {weekdays.map((weekday, index) => (
373
+ <span
374
+ key={index}
375
+ role="columnheader"
376
+ className="pb-1 text-center text-caption2 font-semibold text-label-tertiary"
377
+ >
378
+ {weekday}
379
+ </span>
380
+ ))}
381
+ </div>
382
+ {days.map((day) => {
383
+ const key = formatDay(day)
384
+ const selected = key === value
385
+ const isToday = key === formatDay(today)
386
+ const isFocused =
387
+ key === formatDay(focused)
388
+ const allowed = withinBounds(
389
+ key,
390
+ min,
391
+ max
392
+ )
393
+ return (
394
+ <button
395
+ key={key}
396
+ type="button"
397
+ role="gridcell"
398
+ data-day={key}
399
+ tabIndex={isFocused ? 0 : -1}
400
+ disabled={!allowed}
401
+ aria-selected={selected}
402
+ aria-current={
403
+ isToday ? "date" : undefined
404
+ }
405
+ aria-label={fullDate.format(
406
+ toDate(day)
407
+ )}
408
+ onClick={() => onChoose(key)}
409
+ className={cn(
410
+ "mx-auto flex size-10 items-center justify-center rounded-full text-body tabular-nums transition-colors",
411
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/70",
412
+ selected
413
+ ? "bg-accent font-semibold text-on-accent"
414
+ : isToday
415
+ ? "font-semibold text-accent-text hover:bg-fill/80"
416
+ : day.inMonth
417
+ ? "text-label hover:bg-fill/80"
418
+ : "text-label-tertiary hover:bg-fill/60",
419
+ !allowed && "opacity-30"
420
+ )}
421
+ >
422
+ {day.day}
423
+ </button>
424
+ )
425
+ })}
426
+ </div>
427
+ {labels.clear && value && (
428
+ <button
429
+ type="button"
430
+ onClick={() => onChoose("")}
431
+ className="self-center rounded-full px-3 py-1.5 text-subheadline text-accent-text hover:bg-fill/80"
432
+ >
433
+ {labels.clear}
434
+ </button>
435
+ )}
436
+ </motion.div>
437
+ )
438
+ }
@@ -0,0 +1,35 @@
1
+ import { CompactDatePicker } from "@passui/react/compact-date-picker"
2
+ import { useState } from "react"
3
+
4
+ const labels = {
5
+ previousMonth: "Previous month",
6
+ nextMonth: "Next month",
7
+ clear: "Remove date",
8
+ }
9
+
10
+ export function CompactDatePickerShowcase() {
11
+ const [starts, setStarts] = useState("2026-10-15")
12
+ const [ends, setEnds] = useState("")
13
+ return (
14
+ <div className="grid w-full max-w-sm grid-cols-2 gap-3">
15
+ <CompactDatePicker
16
+ label="Starts"
17
+ labels={labels}
18
+ locale="en-GB"
19
+ value={starts}
20
+ onChange={(value) =>
21
+ value && setStarts(value)
22
+ }
23
+ />
24
+ <CompactDatePicker
25
+ label="Ends"
26
+ placeholder="None"
27
+ labels={labels}
28
+ locale="en-GB"
29
+ min={starts}
30
+ value={ends}
31
+ onChange={setEnds}
32
+ />
33
+ </div>
34
+ )
35
+ }
@@ -0,0 +1,22 @@
1
+ import { CompactDatePicker } from "@passui/react/compact-date-picker"
2
+ import { useState } from "react"
3
+
4
+ export function CompactDatePickerUsage() {
5
+ const [due, setDue] = useState("")
6
+ return (
7
+ <div className="w-full max-w-xs">
8
+ <CompactDatePicker
9
+ label="Due"
10
+ placeholder="None"
11
+ labels={{
12
+ previousMonth: "Previous month",
13
+ nextMonth: "Next month",
14
+ clear: "Remove date",
15
+ }}
16
+ locale="en-US"
17
+ value={due}
18
+ onChange={setDue}
19
+ />
20
+ </div>
21
+ )
22
+ }
@@ -0,0 +1,80 @@
1
+ import type { ComponentProps } from "react"
2
+ import { useState } from "react"
3
+ import { defineDocs } from "../../docs/types"
4
+ import { CompactTimePicker } from "./compact-time-picker"
5
+ import { CompactTimePickerShowcase } from "./examples/compact-time-picker-showcase"
6
+ import { CompactTimePickerUsage } from "./examples/compact-time-picker-usage"
7
+
8
+ type CompactTimePickerProps = ComponentProps<
9
+ typeof CompactTimePicker
10
+ >
11
+
12
+ // Locales on 12 and 24 hour clocks, so the playground shows both.
13
+ type DemoProps = Pick<CompactTimePickerProps, "label"> & {
14
+ locale: "en-US" | "en-GB" | "fr-FR" | "ar-EG" | "zh-CN"
15
+ }
16
+
17
+ const labels = {
18
+ hour: "Hour",
19
+ minute: "Minute",
20
+ period: "AM or PM",
21
+ }
22
+
23
+ function CompactTimePickerDemo({ locale, label }: DemoProps) {
24
+ const [time, setTime] = useState("18:30")
25
+ return (
26
+ <div
27
+ className="w-full max-w-xs"
28
+ dir={locale === "ar-EG" ? "rtl" : undefined}
29
+ >
30
+ <CompactTimePicker
31
+ key={locale}
32
+ label={label}
33
+ labels={labels}
34
+ locale={locale}
35
+ value={time}
36
+ onChange={setTime}
37
+ />
38
+ </div>
39
+ )
40
+ }
41
+
42
+ export const compactTimePickerDocs = defineDocs<DemoProps>()({
43
+ slug: "compact-time-picker",
44
+ name: "CompactTimePicker",
45
+ importPath: "@passui/react/compact-time-picker",
46
+ controls: {
47
+ label: {
48
+ kind: "text",
49
+ default: "Time",
50
+ required: true,
51
+ },
52
+ locale: {
53
+ kind: "select",
54
+ options: {
55
+ "en-US": true,
56
+ "en-GB": true,
57
+ "fr-FR": true,
58
+ "ar-EG": true,
59
+ "zh-CN": true,
60
+ },
61
+ default: "en-US",
62
+ },
63
+ },
64
+ render: (values) => <CompactTimePickerDemo {...values} />,
65
+ childrenSnippet: ({ locale }) => ({
66
+ imports: ['import { useState } from "react"'],
67
+ setup: ['const [time, setTime] = useState("18:30")'],
68
+ children: "",
69
+ omit: ["locale"],
70
+ extraProps: { locale },
71
+ expressionProps: {
72
+ labels: '{ hour: "Hour", minute: "Minute", period: "AM or PM" }',
73
+ value: "time",
74
+ onChange: "setTime",
75
+ },
76
+ }),
77
+ usage: CompactTimePickerUsage,
78
+ showcase: CompactTimePickerShowcase,
79
+ examples: [],
80
+ })