@passui/react 0.0.0-stage → 0.1.15

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 +130 -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,166 @@
1
+ /**
2
+ * App-wide toasts: short messages that confirm, report progress or explain
3
+ * a failure. Call `toast` from anywhere; render one `<Toaster />`.
4
+ */
5
+ export type ToastTone =
6
+ | "neutral"
7
+ | "success"
8
+ | "error"
9
+ | "loading"
10
+
11
+ export interface ToastAction {
12
+ label: string
13
+ onClick: () => void
14
+ }
15
+
16
+ export interface Toast {
17
+ id: number
18
+ tone: ToastTone
19
+ title: string
20
+ description?: string
21
+ action?: ToastAction
22
+ /** Milliseconds before it leaves on its own; `null` stays until dismissed. */
23
+ duration: number | null
24
+ createdAt: number
25
+ }
26
+
27
+ export interface ToastOptions {
28
+ description?: string
29
+ action?: ToastAction
30
+ duration?: number | null
31
+ }
32
+
33
+ const DEFAULT_DURATION = 4200
34
+ const MAX_VISIBLE = 3
35
+
36
+ let toasts: Toast[] = []
37
+ let nextId = 1
38
+ const listeners = new Set<() => void>()
39
+
40
+ const emit = () => {
41
+ for (const listener of listeners) listener()
42
+ }
43
+
44
+ function show(
45
+ tone: ToastTone,
46
+ title: string,
47
+ options: ToastOptions = {}
48
+ ): number {
49
+ const id = nextId++
50
+ const duration =
51
+ options.duration !== undefined
52
+ ? options.duration
53
+ : tone === "loading"
54
+ ? null
55
+ : DEFAULT_DURATION
56
+ toasts = [
57
+ {
58
+ id,
59
+ tone,
60
+ title,
61
+ description: options.description,
62
+ action: options.action,
63
+ duration,
64
+ createdAt: Date.now(),
65
+ },
66
+ ...toasts,
67
+ ].slice(0, MAX_VISIBLE + 2)
68
+ emit()
69
+ return id
70
+ }
71
+
72
+ /** Changes a toast in place; it morphs to its new state. */
73
+ function update(
74
+ id: number,
75
+ tone: ToastTone,
76
+ title: string,
77
+ options: ToastOptions = {}
78
+ ) {
79
+ if (!toasts.some((t) => t.id === id))
80
+ return show(tone, title, options)
81
+ toasts = toasts.map((t) =>
82
+ t.id === id
83
+ ? {
84
+ ...t,
85
+ tone,
86
+ title,
87
+ description: options.description,
88
+ action: options.action,
89
+ duration:
90
+ options.duration !== undefined
91
+ ? options.duration
92
+ : tone === "loading"
93
+ ? null
94
+ : DEFAULT_DURATION,
95
+ createdAt: Date.now(),
96
+ }
97
+ : t
98
+ )
99
+ emit()
100
+ return id
101
+ }
102
+
103
+ function dismiss(id: number) {
104
+ toasts = toasts.filter((t) => t.id !== id)
105
+ emit()
106
+ }
107
+
108
+ interface PromiseMessages<T> {
109
+ loading: string
110
+ success: string | ((value: T) => string)
111
+ error: string | ((error: unknown) => string)
112
+ }
113
+
114
+ export const toast = Object.assign(
115
+ (title: string, options?: ToastOptions) =>
116
+ show("neutral", title, options),
117
+ {
118
+ success: (title: string, options?: ToastOptions) =>
119
+ show("success", title, options),
120
+ error: (title: string, options?: ToastOptions) =>
121
+ show("error", title, options),
122
+ loading: (title: string, options?: ToastOptions) =>
123
+ show("loading", title, options),
124
+ update,
125
+ dismiss,
126
+ /** Shows progress, then morphs into the outcome. Returns the promise. */
127
+ promise<T>(
128
+ promise: Promise<T>,
129
+ messages: PromiseMessages<T>
130
+ ): Promise<T> {
131
+ const id = show("loading", messages.loading)
132
+ promise.then(
133
+ (value) =>
134
+ update(
135
+ id,
136
+ "success",
137
+ typeof messages.success === "function"
138
+ ? messages.success(value)
139
+ : messages.success
140
+ ),
141
+ (error: unknown) =>
142
+ update(
143
+ id,
144
+ "error",
145
+ typeof messages.error === "function"
146
+ ? messages.error(error)
147
+ : messages.error
148
+ )
149
+ )
150
+ return promise
151
+ },
152
+ }
153
+ )
154
+
155
+ export function subscribeToasts(listener: () => void) {
156
+ listeners.add(listener)
157
+ return () => {
158
+ listeners.delete(listener)
159
+ }
160
+ }
161
+
162
+ export function getToasts(): Toast[] {
163
+ return toasts
164
+ }
165
+
166
+ export const toastLimits = { visible: MAX_VISIBLE } as const
@@ -0,0 +1,79 @@
1
+ import { Button } from "../button/button"
2
+ import { jsxText } from "../../docs/snippet"
3
+ import { defineDocs } from "../../docs/types"
4
+ import { ToastAction } from "./examples/toast-action"
5
+ import { ToastPromise } from "./examples/toast-promise"
6
+ import { ToastShowcase } from "./examples/toast-showcase"
7
+ import { ToastUsage } from "./examples/toast-usage"
8
+ import { type ToastTone, toast } from "./toast-store"
9
+
10
+ // toast is a function, not an element: the playground fires one from a
11
+ // button, and the snippet writes that button.
12
+ type DemoProps = {
13
+ tone: ToastTone
14
+ title: string
15
+ description: string
16
+ }
17
+
18
+ function show({ tone, title, description }: DemoProps) {
19
+ const options = description ? { description } : undefined
20
+ if (tone === "neutral") toast(title, options)
21
+ else toast[tone](title, options)
22
+ }
23
+
24
+ export const toastDocs = defineDocs<DemoProps>()({
25
+ slug: "toast",
26
+ name: "toast",
27
+ importPath: "@passui/react/toast",
28
+ controls: {
29
+ tone: {
30
+ kind: "select",
31
+ options: {
32
+ neutral: true,
33
+ success: true,
34
+ error: true,
35
+ loading: true,
36
+ },
37
+ default: "success",
38
+ },
39
+ title: {
40
+ kind: "text",
41
+ default: "Changes saved",
42
+ },
43
+ description: { kind: "text", default: "" },
44
+ },
45
+ render: (values) => (
46
+ <Button
47
+ variant="secondary"
48
+ onClick={() => show(values)}
49
+ >
50
+ Show toast
51
+ </Button>
52
+ ),
53
+ childrenSnippet: ({ tone, title, description }) => {
54
+ const call =
55
+ tone === "neutral" ? "toast" : `toast.${tone}`
56
+ const options = description
57
+ ? `, { description: ${JSON.stringify(description)} }`
58
+ : ""
59
+ return {
60
+ imports: [
61
+ 'import { Button } from "@passui/react/button"',
62
+ ],
63
+ element: "Button",
64
+ omit: ["tone", "title", "description"],
65
+ extraProps: { variant: "secondary" },
66
+ expressionProps: {
67
+ onClick: `() => ${call}(${JSON.stringify(title)}${options})`,
68
+ },
69
+ children: jsxText("Show toast"),
70
+ }
71
+ },
72
+ usage: ToastUsage,
73
+ showcase: ToastShowcase,
74
+ examples: [
75
+ { id: "action", component: ToastAction },
76
+ { id: "promise", component: ToastPromise },
77
+ ],
78
+ previewHeight: 240,
79
+ })
@@ -0,0 +1,374 @@
1
+ import { AnimatePresence, motion } from "motion/react"
2
+ import {
3
+ useEffect,
4
+ useLayoutEffect,
5
+ useRef,
6
+ useState,
7
+ useSyncExternalStore,
8
+ } from "react"
9
+ import { CheckIcon, CloseIcon } from "../../icons/icons"
10
+ import { cn } from "../../lib/class-names"
11
+ import { spring } from "../../motion/transitions"
12
+ import {
13
+ getToasts,
14
+ subscribeToasts,
15
+ type Toast,
16
+ type ToastTone,
17
+ toast as toastApi,
18
+ toastLimits,
19
+ } from "./toast-store"
20
+
21
+ const PILL = 40
22
+ const GOO_FILTER_ID = "nt-toast-goo"
23
+ const EMPTY: Toast[] = []
24
+ /** A little loose, so the body overshoots and settles like a drop of honey. */
25
+ const BODY_SPRING = {
26
+ type: "spring",
27
+ stiffness: 260,
28
+ damping: 19,
29
+ mass: 0.9,
30
+ } as const
31
+
32
+ /**
33
+ * Toasts drop from the top centre like an island: a small pill carrying the
34
+ * icon, out of which the message flows. Both shapes sit under an SVG "goo"
35
+ * filter, so where they meet looks liquid while they grow, morph between
36
+ * states and shrink away. Text is drawn above the filter to stay crisp.
37
+ */
38
+ export function Toaster({
39
+ className,
40
+ }: {
41
+ className?: string
42
+ }) {
43
+ const toasts = useSyncExternalStore(
44
+ subscribeToasts,
45
+ getToasts,
46
+ () => EMPTY
47
+ )
48
+ const [hovered, setHovered] = useState(false)
49
+ const visible = toasts.slice(0, toastLimits.visible)
50
+
51
+ return (
52
+ <>
53
+ <GooFilter />
54
+ <section
55
+ aria-label="Notifications"
56
+ aria-live="polite"
57
+ className={cn(
58
+ "pointer-events-none fixed inset-x-0 top-[max(10px,env(safe-area-inset-top))] z-[60] flex justify-center px-3",
59
+ className
60
+ )}
61
+ >
62
+ <ol
63
+ className="relative flex w-full max-w-[420px] flex-col items-center"
64
+ onMouseEnter={() => setHovered(true)}
65
+ onMouseLeave={() => setHovered(false)}
66
+ >
67
+ <AnimatePresence initial={false}>
68
+ {visible.map((item, index) => (
69
+ <ToastItem
70
+ key={item.id}
71
+ toast={item}
72
+ index={index}
73
+ expanded={hovered}
74
+ paused={hovered}
75
+ />
76
+ ))}
77
+ </AnimatePresence>
78
+ </ol>
79
+ </section>
80
+ </>
81
+ )
82
+ }
83
+
84
+ function GooFilter() {
85
+ return (
86
+ <svg
87
+ aria-hidden="true"
88
+ className="pointer-events-none absolute size-0"
89
+ >
90
+ <defs>
91
+ <filter id={GOO_FILTER_ID}>
92
+ <feGaussianBlur
93
+ in="SourceGraphic"
94
+ stdDeviation="7"
95
+ result="blur"
96
+ />
97
+ <feColorMatrix
98
+ in="blur"
99
+ mode="matrix"
100
+ values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 22 -10"
101
+ result="goo"
102
+ />
103
+ <feComposite
104
+ in="SourceGraphic"
105
+ in2="goo"
106
+ operator="atop"
107
+ />
108
+ </filter>
109
+ </defs>
110
+ </svg>
111
+ )
112
+ }
113
+
114
+ function ToastItem({
115
+ toast,
116
+ index,
117
+ expanded,
118
+ paused,
119
+ }: {
120
+ toast: Toast
121
+ index: number
122
+ expanded: boolean
123
+ paused: boolean
124
+ }) {
125
+ const content = useRef<HTMLDivElement>(null)
126
+ const [size, setSize] = useState({
127
+ width: PILL,
128
+ height: PILL,
129
+ })
130
+ const [open, setOpen] = useState(false)
131
+
132
+ // The body flows out of the pill a beat after it lands.
133
+ useEffect(() => {
134
+ const timer = setTimeout(() => setOpen(true), 140)
135
+ return () => clearTimeout(timer)
136
+ }, [])
137
+
138
+ useLayoutEffect(() => {
139
+ const element = content.current
140
+ if (!element) return
141
+ const measure = () =>
142
+ setSize({
143
+ width: Math.ceil(element.offsetWidth),
144
+ height: Math.ceil(element.offsetHeight),
145
+ })
146
+ measure()
147
+ const observer = new ResizeObserver(measure)
148
+ observer.observe(element)
149
+ return () => observer.disconnect()
150
+ }, [])
151
+
152
+ useEffect(() => {
153
+ if (toast.duration === null || paused) return
154
+ const timer = setTimeout(
155
+ () => toastApi.dismiss(toast.id),
156
+ toast.duration
157
+ )
158
+ return () => clearTimeout(timer)
159
+ }, [toast.id, toast.duration, paused])
160
+
161
+ const width = open ? size.width : PILL
162
+ const height = open ? size.height : PILL
163
+ // Older toasts tuck in behind the newest, and fan out on hover.
164
+ const stacked = !expanded && index > 0
165
+
166
+ return (
167
+ <motion.li
168
+ layout
169
+ className="pointer-events-none absolute inset-x-0 top-0 flex justify-center"
170
+ style={{ zIndex: 10 - index }}
171
+ initial={{ opacity: 0, y: -24, scale: 0.6 }}
172
+ animate={{
173
+ opacity: stacked ? 1 - index * 0.25 : 1,
174
+ y: stacked
175
+ ? index * 9
176
+ : index * (PILL + 14) + index * 6,
177
+ scale: stacked ? 1 - index * 0.05 : 1,
178
+ }}
179
+ exit={{
180
+ opacity: 0,
181
+ y: -18,
182
+ scale: 0.6,
183
+ transition: { duration: 0.22 },
184
+ }}
185
+ transition={spring.bouncy}
186
+ drag="y"
187
+ dragConstraints={{ top: 0, bottom: 0 }}
188
+ dragElastic={{ top: 0.6, bottom: 0.1 }}
189
+ onDragEnd={(_, info) => {
190
+ if (
191
+ info.offset.y < -30 ||
192
+ info.velocity.y < -300
193
+ )
194
+ toastApi.dismiss(toast.id)
195
+ }}
196
+ >
197
+ <motion.div
198
+ role="status"
199
+ className="pointer-events-auto relative"
200
+ initial={{ width: PILL, height: PILL }}
201
+ animate={{ width, height }}
202
+ transition={BODY_SPRING}
203
+ >
204
+ {/* Shapes, under the goo filter; padded so the blur isn't clipped. */}
205
+ <div
206
+ aria-hidden="true"
207
+ className="absolute -inset-3"
208
+ style={{
209
+ filter: `url(#${GOO_FILTER_ID})`,
210
+ }}
211
+ >
212
+ <motion.span
213
+ className="absolute top-3 left-3 rounded-full bg-inverse shadow-xl"
214
+ style={{ width: PILL, height: PILL }}
215
+ animate={{ scale: open ? 1 : 1.06 }}
216
+ transition={spring.bouncy}
217
+ />
218
+ <motion.span
219
+ className="absolute top-3 left-3 bg-inverse"
220
+ initial={{
221
+ width: PILL,
222
+ height: PILL,
223
+ borderRadius: PILL / 2,
224
+ }}
225
+ animate={{
226
+ width,
227
+ height,
228
+ borderRadius: Math.min(
229
+ PILL / 2,
230
+ height / 2,
231
+ 22
232
+ ),
233
+ }}
234
+ transition={BODY_SPRING}
235
+ />
236
+ </div>
237
+
238
+ {/* Content, crisp above the shapes. */}
239
+ <motion.div
240
+ ref={content}
241
+ className="absolute top-0 left-0 flex w-max max-w-[min(420px,calc(100vw-24px))] items-start gap-2.5 py-[10px] pr-4 pl-[10px] text-on-inverse"
242
+ style={{ minHeight: PILL }}
243
+ animate={{ opacity: open ? 1 : 0 }}
244
+ transition={{
245
+ duration: open ? 0.22 : 0.1,
246
+ delay: open ? 0.08 : 0,
247
+ }}
248
+ >
249
+ <ToneIcon tone={toast.tone} />
250
+ <div className="flex min-w-0 flex-col py-px">
251
+ <AnimatePresence
252
+ mode="popLayout"
253
+ initial={false}
254
+ >
255
+ <motion.span
256
+ key={toast.title}
257
+ className="text-subheadline leading-5 font-semibold"
258
+ initial={{
259
+ opacity: 0,
260
+ y: 6,
261
+ filter: "blur(4px)",
262
+ }}
263
+ animate={{
264
+ opacity: 1,
265
+ y: 0,
266
+ filter: "blur(0px)",
267
+ }}
268
+ exit={{
269
+ opacity: 0,
270
+ y: -6,
271
+ filter: "blur(4px)",
272
+ }}
273
+ transition={spring.snappy}
274
+ >
275
+ {toast.title}
276
+ </motion.span>
277
+ </AnimatePresence>
278
+ {toast.description && (
279
+ <span className="text-footnote leading-snug text-on-inverse/70">
280
+ {toast.description}
281
+ </span>
282
+ )}
283
+ </div>
284
+ {toast.action && (
285
+ <button
286
+ type="button"
287
+ onClick={() => {
288
+ toast.action?.onClick()
289
+ toastApi.dismiss(toast.id)
290
+ }}
291
+ className="ml-1 shrink-0 self-center rounded-full bg-on-inverse/12 px-3 py-1 text-footnote font-semibold text-on-inverse transition-colors hover:bg-on-inverse/20"
292
+ >
293
+ {toast.action.label}
294
+ </button>
295
+ )}
296
+ </motion.div>
297
+ </motion.div>
298
+ </motion.li>
299
+ )
300
+ }
301
+
302
+ const toneColors: Record<ToastTone, string> = {
303
+ neutral: "bg-transparent",
304
+ success: "bg-success text-white",
305
+ error: "bg-danger text-white",
306
+ loading: "bg-on-inverse/15 text-on-inverse",
307
+ }
308
+
309
+ function ToneIcon({ tone }: { tone: ToastTone }) {
310
+ return (
311
+ <AnimatePresence mode="popLayout" initial={false}>
312
+ <motion.span
313
+ key={tone}
314
+ className={cn(
315
+ "flex size-5 shrink-0 items-center justify-center rounded-full",
316
+ toneColors[tone]
317
+ )}
318
+ initial={{
319
+ scale: 0.3,
320
+ opacity: 0,
321
+ rotate: -40,
322
+ }}
323
+ animate={{ scale: 1, opacity: 1, rotate: 0 }}
324
+ exit={{ scale: 0.3, opacity: 0 }}
325
+ transition={spring.bouncy}
326
+ >
327
+ {tone === "success" && (
328
+ <CheckIcon size={13} strokeWidth={3} />
329
+ )}
330
+ {tone === "error" && (
331
+ <CloseIcon size={12} strokeWidth={3} />
332
+ )}
333
+ {/* The full stop from the Notables mark. */}
334
+ {tone === "neutral" && (
335
+ <span className="size-2 rounded-full bg-[#EE8A2A]" />
336
+ )}
337
+ {tone === "loading" && <Spinner />}
338
+ </motion.span>
339
+ </AnimatePresence>
340
+ )
341
+ }
342
+
343
+ function Spinner() {
344
+ return (
345
+ <motion.svg
346
+ viewBox="0 0 20 20"
347
+ className="size-4"
348
+ animate={{ rotate: 360 }}
349
+ transition={{
350
+ repeat: Number.POSITIVE_INFINITY,
351
+ duration: 0.8,
352
+ ease: "linear",
353
+ }}
354
+ aria-hidden="true"
355
+ >
356
+ <circle
357
+ cx="10"
358
+ cy="10"
359
+ r="7"
360
+ fill="none"
361
+ stroke="currentColor"
362
+ strokeOpacity="0.25"
363
+ strokeWidth="2.4"
364
+ />
365
+ <path
366
+ d="M10 3a7 7 0 0 1 7 7"
367
+ fill="none"
368
+ stroke="currentColor"
369
+ strokeWidth="2.4"
370
+ strokeLinecap="round"
371
+ />
372
+ </motion.svg>
373
+ )
374
+ }
@@ -0,0 +1,43 @@
1
+ import { Toggle } from "@passui/react/toggle"
2
+ import { useState } from "react"
3
+
4
+ const tasks = ["Pick a palette", "Review copy", "Ship it"]
5
+
6
+ export function ToggleCheckboxes() {
7
+ const [done, setDone] = useState([true, false, false])
8
+ const all = done.every(Boolean)
9
+ const some = done.some(Boolean)
10
+
11
+ return (
12
+ <div className="flex flex-col gap-3">
13
+ <Toggle
14
+ style="checkbox"
15
+ label="Launch list"
16
+ checked={all}
17
+ mixed={some && !all}
18
+ onChange={(checked) =>
19
+ setDone(tasks.map(() => checked))
20
+ }
21
+ />
22
+ <div className="flex flex-col gap-3 ps-8">
23
+ {tasks.map((task, index) => (
24
+ <Toggle
25
+ key={task}
26
+ style="checkbox"
27
+ label={task}
28
+ checked={done[index]}
29
+ onChange={(checked) =>
30
+ setDone((current) =>
31
+ current.map((value, at) =>
32
+ at === index
33
+ ? checked
34
+ : value
35
+ )
36
+ )
37
+ }
38
+ />
39
+ ))}
40
+ </div>
41
+ </div>
42
+ )
43
+ }
@@ -0,0 +1,31 @@
1
+ import { LabeledContent, Section } from "@passui/react"
2
+ import { Toggle } from "@passui/react/toggle"
3
+ import { useState } from "react"
4
+
5
+ export function ToggleSettings() {
6
+ const [sync, setSync] = useState(true)
7
+ const [sounds, setSounds] = useState(false)
8
+
9
+ return (
10
+ <div className="w-full max-w-sm">
11
+ <Section title="Notifications">
12
+ <LabeledContent label="Sync across devices">
13
+ <Toggle
14
+ label="Sync"
15
+ labelHidden
16
+ checked={sync}
17
+ onChange={setSync}
18
+ />
19
+ </LabeledContent>
20
+ <LabeledContent label="Sounds">
21
+ <Toggle
22
+ label="Sounds"
23
+ labelHidden
24
+ checked={sounds}
25
+ onChange={setSounds}
26
+ />
27
+ </LabeledContent>
28
+ </Section>
29
+ </div>
30
+ )
31
+ }