@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,279 @@
1
+ import { IconButton } from "@passui/react/icon-button"
2
+ import { ArrowUp02Icon } from "@hugeicons/core-free-icons"
3
+ import { HugeiconsIcon } from "@hugeicons/react"
4
+ import type { ReactNode } from "react"
5
+ import { useState } from "react"
6
+ import { formatTime } from "../../lib/date"
7
+ import { cn } from "../../lib/class-names"
8
+
9
+ export type ConversationMessage = {
10
+ id: string
11
+ body: string
12
+ /** Written by whoever is looking: drawn on the right. */
13
+ mine: boolean
14
+ author?: string
15
+ /** Shown beside the other side's messages, like their avatar. */
16
+ avatar?: ReactNode
17
+ createdAt: string
18
+ }
19
+
20
+ const GROUP_GAP_MS = 5 * 60 * 1000
21
+
22
+ /**
23
+ * The day line: today and yesterday in the reader's own words, from
24
+ * Intl rather than built-in English, else the full date.
25
+ */
26
+ function dayLabel(iso: string, locale?: string) {
27
+ const date = new Date(iso)
28
+ const today = new Date()
29
+ const yesterday = new Date()
30
+ yesterday.setDate(today.getDate() - 1)
31
+ const offset =
32
+ date.toDateString() === today.toDateString()
33
+ ? 0
34
+ : date.toDateString() === yesterday.toDateString()
35
+ ? -1
36
+ : null
37
+ if (offset === null)
38
+ return date.toLocaleDateString(locale, {
39
+ weekday: "long",
40
+ month: "long",
41
+ day: "numeric",
42
+ })
43
+ const word = new Intl.RelativeTimeFormat(locale, {
44
+ numeric: "auto",
45
+ }).format(offset, "day")
46
+ return (
47
+ word.charAt(0).toLocaleUpperCase(locale) +
48
+ word.slice(1)
49
+ )
50
+ }
51
+
52
+ const clock = (iso: string, locale?: string) =>
53
+ new Date(iso).toLocaleTimeString(locale, {
54
+ hour: "numeric",
55
+ minute: "2-digit",
56
+ })
57
+
58
+ /**
59
+ * A thread of plain-text messages like Messages on iPhone: a day line when
60
+ * the date changes, back-to-back messages from one side grouped under one
61
+ * name, theirs on the left with an avatar, yours on the right.
62
+ */
63
+ function Messages({
64
+ messages,
65
+ locale,
66
+ }: {
67
+ messages: ConversationMessage[]
68
+ /** For day names and times; the reader's own by default. */
69
+ locale?: string
70
+ }) {
71
+ return (
72
+ <ol data-not-typeset className="flex flex-col">
73
+ {messages.map((message, index) => {
74
+ const prev = messages[index - 1]
75
+ const next = messages[index + 1]
76
+ const newDay =
77
+ !prev ||
78
+ new Date(
79
+ prev.createdAt
80
+ ).toDateString() !==
81
+ new Date(
82
+ message.createdAt
83
+ ).toDateString()
84
+ const startsGroup =
85
+ newDay ||
86
+ prev.mine !== message.mine ||
87
+ new Date(message.createdAt).getTime() -
88
+ new Date(prev.createdAt).getTime() >
89
+ GROUP_GAP_MS
90
+ const endsGroup =
91
+ !next ||
92
+ next.mine !== message.mine ||
93
+ new Date(next.createdAt).getTime() -
94
+ new Date(
95
+ message.createdAt
96
+ ).getTime() >
97
+ GROUP_GAP_MS
98
+
99
+ return (
100
+ <li
101
+ key={message.id}
102
+ className="flex flex-col"
103
+ >
104
+ {newDay && (
105
+ // the reader's own day and clock, which a server can't know
106
+ <span
107
+ suppressHydrationWarning
108
+ className="my-4 text-center text-caption2 font-medium text-label-secondary"
109
+ >
110
+ {dayLabel(
111
+ message.createdAt,
112
+ locale
113
+ )}
114
+ </span>
115
+ )}
116
+ <div
117
+ className={cn(
118
+ "flex items-end gap-2",
119
+ message.mine
120
+ ? "justify-end"
121
+ : "justify-start",
122
+ startsGroup
123
+ ? "mt-3"
124
+ : "mt-0.5"
125
+ )}
126
+ >
127
+ {!message.mine && (
128
+ <span className="flex size-7 shrink-0 items-center justify-center">
129
+ {endsGroup &&
130
+ message.avatar}
131
+ </span>
132
+ )}
133
+ <div
134
+ className={cn(
135
+ "flex max-w-[78%] flex-col gap-1",
136
+ message.mine
137
+ ? "items-end"
138
+ : "items-start"
139
+ )}
140
+ >
141
+ {startsGroup &&
142
+ message.author && (
143
+ <span className="px-3 text-caption2 text-label-secondary">
144
+ {message.author}
145
+ </span>
146
+ )}
147
+ {/* text only: a message can never become markup */}
148
+ <p
149
+ suppressHydrationWarning
150
+ title={formatTime(
151
+ message.createdAt
152
+ )}
153
+ className={cn(
154
+ "rounded-4xl px-3.5 py-2 text-subheadline leading-snug whitespace-pre-wrap wrap-break-word",
155
+ message.mine
156
+ ? "bg-inverse text-on-inverse"
157
+ : "bg-fill text-label",
158
+ endsGroup &&
159
+ (message.mine
160
+ ? "rounded-br-md"
161
+ : "rounded-bl-md")
162
+ )}
163
+ >
164
+ {message.body}
165
+ </p>
166
+ {endsGroup && (
167
+ <time
168
+ suppressHydrationWarning
169
+ dateTime={
170
+ message.createdAt
171
+ }
172
+ className="px-3 text-caption2 text-label-secondary tabular-nums"
173
+ >
174
+ {clock(
175
+ message.createdAt,
176
+ locale
177
+ )}
178
+ </time>
179
+ )}
180
+ </div>
181
+ </div>
182
+ </li>
183
+ )
184
+ })}
185
+ </ol>
186
+ )
187
+ }
188
+
189
+ type ComposerProps = {
190
+ onSend: (body: string) => Promise<unknown>
191
+ pending?: boolean
192
+ maxLength: number
193
+ placeholder: string
194
+ /** Extra controls under the field, like a status picker. */
195
+ actions?: ReactNode
196
+ /** Names the round Send button for screen readers. */
197
+ sendLabel: string
198
+ /**
199
+ * Words for the characters left, shown near the limit, such as
200
+ * (count) => `${count} left`. Without it, just the number shows.
201
+ */
202
+ remaining?: (count: number) => string
203
+ }
204
+
205
+ /** A reply field with a round send button; Ctrl/Cmd+Enter sends, and it clears only once the send worked. */
206
+ function Composer({
207
+ onSend,
208
+ pending = false,
209
+ maxLength,
210
+ placeholder,
211
+ actions,
212
+ sendLabel,
213
+ remaining,
214
+ }: ComposerProps) {
215
+ const [body, setBody] = useState("")
216
+ const ready = body.trim().length > 0 && !pending
217
+ const send = async () => {
218
+ if (!ready) return
219
+ await onSend(body.trim())
220
+ setBody("")
221
+ }
222
+
223
+ return (
224
+ <form
225
+ data-not-typeset
226
+ className="flex flex-col gap-2"
227
+ onSubmit={(event) => {
228
+ event.preventDefault()
229
+ void send()
230
+ }}
231
+ >
232
+ <div className="flex items-end gap-2 rounded-5xl border border-separator bg-elevated py-1.5 pr-1.5 pl-4 transition-colors focus-within:border-accent-text/60">
233
+ <textarea
234
+ value={body}
235
+ onChange={(event) =>
236
+ setBody(event.target.value)
237
+ }
238
+ onKeyDown={(event) => {
239
+ if (
240
+ event.key === "Enter" &&
241
+ (event.metaKey || event.ctrlKey)
242
+ ) {
243
+ event.preventDefault()
244
+ void send()
245
+ }
246
+ }}
247
+ maxLength={maxLength}
248
+ placeholder={placeholder}
249
+ rows={1}
250
+ className="field-sizing-content max-h-48 min-h-9 flex-1 resize-none bg-transparent py-1.5 text-subheadline text-label outline-none placeholder:text-label-secondary"
251
+ />
252
+ <IconButton
253
+ type="submit"
254
+ size="icon-xs"
255
+ disabled={!ready}
256
+ label={sendLabel}
257
+ className="size-8 shrink-0 rounded-full"
258
+ >
259
+ <HugeiconsIcon
260
+ icon={ArrowUp02Icon}
261
+ strokeWidth={2.5}
262
+ />
263
+ </IconButton>
264
+ </div>
265
+ <div className="flex min-h-6 items-center gap-2 px-1">
266
+ {actions}
267
+ <span className="ml-auto text-caption2 text-label-secondary tabular-nums">
268
+ {body.length > maxLength * 0.8 &&
269
+ (remaining?.(
270
+ maxLength - body.length
271
+ ) ??
272
+ maxLength - body.length)}
273
+ </span>
274
+ </div>
275
+ </form>
276
+ )
277
+ }
278
+
279
+ export const Conversation = { Messages, Composer }
@@ -0,0 +1,36 @@
1
+ import { Conversation } from "@passui/react/conversation"
2
+ import { useState } from "react"
3
+
4
+ export function ConversationComposer() {
5
+ const [sent, setSent] = useState<string[]>([])
6
+
7
+ return (
8
+ <div className="flex w-full max-w-sm flex-col gap-3">
9
+ <ul className="m-0 flex list-none flex-col gap-1 p-0 text-footnote text-label-secondary">
10
+ {sent.length === 0 ? (
11
+ <li>
12
+ Type a reply, then press Send or
13
+ Ctrl/Cmd+Enter.
14
+ </li>
15
+ ) : (
16
+ sent.map((body, index) => (
17
+ <li key={index}>Sent: {body}</li>
18
+ ))
19
+ )}
20
+ </ul>
21
+ <Conversation.Composer
22
+ placeholder="Write a reply"
23
+ maxLength={280}
24
+ sendLabel="Send"
25
+ remaining={(count) => `${count} left`}
26
+ onSend={async (body) => {
27
+ // the field clears only once this resolves
28
+ await new Promise((resolve) =>
29
+ setTimeout(resolve, 400)
30
+ )
31
+ setSent((current) => [...current, body])
32
+ }}
33
+ />
34
+ </div>
35
+ )
36
+ }
@@ -0,0 +1,48 @@
1
+ import { Conversation } from "@passui/react/conversation"
2
+
3
+ const hoursAgo = (hours: number) =>
4
+ new Date(Date.now() - hours * 3_600_000).toISOString()
5
+
6
+ export function ConversationDays() {
7
+ return (
8
+ <div className="w-full max-w-sm rounded-4xl bg-elevated p-4 shadow-sm ring-1 ring-separator/60">
9
+ <Conversation.Messages
10
+ messages={[
11
+ {
12
+ id: "1",
13
+ mine: false,
14
+ author: "Mira",
15
+ body: "Did the release go out?",
16
+ createdAt: hoursAgo(26),
17
+ },
18
+ {
19
+ id: "2",
20
+ mine: true,
21
+ body: "Not yet, tomorrow morning.",
22
+ createdAt: hoursAgo(25.9),
23
+ },
24
+ {
25
+ id: "3",
26
+ mine: true,
27
+ body: "It's live!",
28
+ createdAt: hoursAgo(1),
29
+ },
30
+ {
31
+ id: "4",
32
+ mine: false,
33
+ author: "Mira",
34
+ body: "Amazing, well done.",
35
+ createdAt: hoursAgo(0.9),
36
+ },
37
+ {
38
+ id: "5",
39
+ mine: false,
40
+ author: "Mira",
41
+ body: "Celebrating at lunch?",
42
+ createdAt: hoursAgo(0.88),
43
+ },
44
+ ]}
45
+ />
46
+ </div>
47
+ )
48
+ }
@@ -0,0 +1,71 @@
1
+ import { CustomerSupportIcon } from "@hugeicons/core-free-icons"
2
+ import { HugeiconsIcon } from "@hugeicons/react"
3
+ import {
4
+ Conversation,
5
+ type ConversationMessage,
6
+ } from "@passui/react/conversation"
7
+ import { useState } from "react"
8
+
9
+ const minutesAgo = (minutes: number) =>
10
+ new Date(Date.now() - minutes * 60_000).toISOString()
11
+
12
+ const support = {
13
+ mine: false,
14
+ author: "Pass support",
15
+ avatar: (
16
+ <span className="flex size-7 items-center justify-center rounded-full bg-accent-soft text-accent-text">
17
+ <HugeiconsIcon
18
+ icon={CustomerSupportIcon}
19
+ size={16}
20
+ />
21
+ </span>
22
+ ),
23
+ }
24
+
25
+ export function ConversationShowcase() {
26
+ const [messages, setMessages] = useState<
27
+ ConversationMessage[]
28
+ >(() => [
29
+ {
30
+ id: "1",
31
+ mine: true,
32
+ body: "Hi, I lost my phone and can't get my sign-in codes.",
33
+ createdAt: minutesAgo(26),
34
+ },
35
+ {
36
+ id: "2",
37
+ ...support,
38
+ body: "We can help. Do you still have your recovery key?",
39
+ createdAt: minutesAgo(24),
40
+ },
41
+ {
42
+ id: "3",
43
+ ...support,
44
+ body: "It's the 24-letter key you saved when you turned on two-step verification.",
45
+ createdAt: minutesAgo(24),
46
+ },
47
+ ])
48
+
49
+ return (
50
+ <div className="flex w-full max-w-sm flex-col gap-4 rounded-4xl bg-elevated p-4 shadow-sm ring-1 ring-separator/60">
51
+ <Conversation.Messages messages={messages} />
52
+ <Conversation.Composer
53
+ placeholder="Write a reply"
54
+ maxLength={500}
55
+ sendLabel="Send"
56
+ onSend={async (body) => {
57
+ setMessages((current) => [
58
+ ...current,
59
+ {
60
+ id: String(current.length + 1),
61
+ mine: true,
62
+ body,
63
+ createdAt:
64
+ new Date().toISOString(),
65
+ },
66
+ ])
67
+ }}
68
+ />
69
+ </div>
70
+ )
71
+ }
@@ -0,0 +1,28 @@
1
+ import { Conversation } from "@passui/react/conversation"
2
+
3
+ const minutesAgo = (minutes: number) =>
4
+ new Date(Date.now() - minutes * 60_000).toISOString()
5
+
6
+ export function ConversationUsage() {
7
+ return (
8
+ <div className="w-full max-w-sm">
9
+ <Conversation.Messages
10
+ messages={[
11
+ {
12
+ id: "1",
13
+ mine: false,
14
+ author: "Kenji",
15
+ body: "Are we still on for lunch?",
16
+ createdAt: minutesAgo(4),
17
+ },
18
+ {
19
+ id: "2",
20
+ mine: true,
21
+ body: "Yes! 12:30 at the usual place.",
22
+ createdAt: minutesAgo(2),
23
+ },
24
+ ]}
25
+ />
26
+ </div>
27
+ )
28
+ }
@@ -0,0 +1 @@
1
+ export * from "./conversation"
@@ -0,0 +1,88 @@
1
+ import type { ComponentProps } from "react"
2
+ import { useState } from "react"
3
+ import { defineDocs } from "../../docs/types"
4
+ import { DatePicker } from "./date-picker"
5
+ import { DatePickerLocales } from "./examples/date-picker-locales"
6
+ import { DatePickerPastOnly } from "./examples/date-picker-past-only"
7
+ import { DatePickerShowcase } from "./examples/date-picker-showcase"
8
+ import { DatePickerUsage } from "./examples/date-picker-usage"
9
+
10
+ type DatePickerProps = ComponentProps<typeof DatePicker>
11
+
12
+ // A handful of locales whose field order differs, so the playground
13
+ // shows the order following the locale.
14
+ type DemoProps = Pick<DatePickerProps, "label"> & {
15
+ locale: "en-US" | "en-GB" | "fr-FR" | "zh-CN" | "ja-JP"
16
+ }
17
+
18
+ const labels = {
19
+ day: "Day",
20
+ month: "Month",
21
+ year: "Year",
22
+ invalid: "Enter a real date.",
23
+ }
24
+
25
+ function DatePickerDemo({ locale, label }: DemoProps) {
26
+ const [birthday, setBirthday] = useState("")
27
+ return (
28
+ <div className="w-full max-w-xs">
29
+ <DatePicker
30
+ key={locale}
31
+ label={label}
32
+ labels={labels}
33
+ locale={locale}
34
+ value={birthday}
35
+ onChange={setBirthday}
36
+ />
37
+ </div>
38
+ )
39
+ }
40
+
41
+ export const datePickerDocs = defineDocs<DemoProps>()({
42
+ slug: "date-picker",
43
+ name: "DatePicker",
44
+ importPath: "@passui/react/date-picker",
45
+ controls: {
46
+ label: {
47
+ kind: "text",
48
+ default: "Birthday",
49
+ required: true,
50
+ },
51
+ locale: {
52
+ kind: "select",
53
+ options: {
54
+ "en-US": true,
55
+ "en-GB": true,
56
+ "fr-FR": true,
57
+ "zh-CN": true,
58
+ "ja-JP": true,
59
+ },
60
+ default: "en-US",
61
+ },
62
+ },
63
+ render: (values) => <DatePickerDemo {...values} />,
64
+ childrenSnippet: ({ locale }) => ({
65
+ imports: ['import { useState } from "react"'],
66
+ setup: [
67
+ 'const [birthday, setBirthday] = useState("")',
68
+ ],
69
+ children: "",
70
+ omit: ["locale"],
71
+ extraProps: { locale },
72
+ expressionProps: {
73
+ labels: '{ day: "Day", month: "Month", year: "Year", invalid: "Enter a real date." }',
74
+ value: "birthday",
75
+ onChange: "setBirthday",
76
+ },
77
+ }),
78
+ usage: DatePickerUsage,
79
+ showcase: DatePickerShowcase,
80
+ examples: [
81
+ {
82
+ id: "locales",
83
+ component: DatePickerLocales,
84
+ playground: { locale: "zh-CN" },
85
+ },
86
+ { id: "past-only", component: DatePickerPastOnly },
87
+ ],
88
+ })
@@ -0,0 +1,103 @@
1
+ import { useState } from "react"
2
+ import { TextField } from "../text-field/text-field"
3
+ import { dateValue } from "./date-value"
4
+
5
+ type DatePickerProps = {
6
+ value: string
7
+ onChange: (value: string) => void
8
+ label: string
9
+ labels: {
10
+ day: string
11
+ month: string
12
+ year: string
13
+ invalid: string
14
+ }
15
+ locale: string
16
+ max?: string
17
+ }
18
+
19
+ /** Segmented date entry suited to birthdays, without a native calendar dialog. */
20
+ export function DatePicker({
21
+ value,
22
+ onChange,
23
+ label,
24
+ labels,
25
+ locale,
26
+ max,
27
+ }: DatePickerProps) {
28
+ const [year, month, day] = value.split("-")
29
+ const [parts, setParts] = useState({
30
+ year: year ?? "",
31
+ month: month ?? "",
32
+ day: day ?? "",
33
+ })
34
+ const complete =
35
+ parts.year.length === 4 &&
36
+ Boolean(parts.month && parts.day)
37
+ const invalid =
38
+ complete &&
39
+ !dateValue(parts.year, parts.month, parts.day, max)
40
+ const order = new Intl.DateTimeFormat(locale, {
41
+ year: "numeric",
42
+ month: "2-digit",
43
+ day: "2-digit",
44
+ timeZone: "UTC",
45
+ })
46
+ .formatToParts(new Date("2000-01-02T00:00:00Z"))
47
+ .filter(
48
+ (
49
+ part
50
+ ): part is Intl.DateTimeFormatPart & {
51
+ type: "year" | "month" | "day"
52
+ } =>
53
+ part.type === "year" ||
54
+ part.type === "month" ||
55
+ part.type === "day"
56
+ )
57
+ return (
58
+ <fieldset className="flex min-w-0 flex-col gap-3">
59
+ <legend className="mb-3 text-subheadline font-medium">
60
+ {label}
61
+ </legend>
62
+ <div className="grid grid-cols-3 gap-3">
63
+ {order.map(({ type }) => (
64
+ <TextField
65
+ key={type}
66
+ label={labels[type]}
67
+ inputMode="numeric"
68
+ autoComplete={`bday-${type}`}
69
+ maxLength={type === "year" ? 4 : 2}
70
+ value={parts[type]}
71
+ aria-invalid={invalid || undefined}
72
+ onChange={(event) => {
73
+ const next = {
74
+ ...parts,
75
+ [type]: event.target.value.replace(
76
+ /\D/g,
77
+ ""
78
+ ),
79
+ }
80
+ setParts(next)
81
+ onChange(
82
+ dateValue(
83
+ next.year,
84
+ next.month,
85
+ next.day,
86
+ max
87
+ )
88
+ )
89
+ }}
90
+ />
91
+ ))}
92
+ </div>
93
+ {invalid && (
94
+ <p
95
+ role="status"
96
+ className="text-footnote text-danger"
97
+ >
98
+ {labels.invalid}
99
+ </p>
100
+ )}
101
+ </fieldset>
102
+ )
103
+ }