@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,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
+ }
@@ -0,0 +1,59 @@
1
+ import { LabeledContent, Section } from "@passui/react"
2
+ import { Toggle } from "@passui/react/toggle"
3
+ import { useState } from "react"
4
+
5
+ export function ToggleShowcase() {
6
+ const [airplane, setAirplane] = useState(false)
7
+ const [wifi, setWifi] = useState(true)
8
+ const [bluetooth, setBluetooth] = useState(true)
9
+ const [focus, setFocus] = useState(false)
10
+
11
+ return (
12
+ <div className="flex w-full max-w-sm flex-col gap-6">
13
+ <Section title="Connections">
14
+ <LabeledContent label="Airplane mode">
15
+ <Toggle
16
+ label="Airplane mode"
17
+ labelHidden
18
+ checked={airplane}
19
+ onChange={setAirplane}
20
+ />
21
+ </LabeledContent>
22
+ <LabeledContent
23
+ label="Wi-Fi"
24
+ description={wifi ? "Home" : "Off"}
25
+ >
26
+ <Toggle
27
+ label="Wi-Fi"
28
+ labelHidden
29
+ checked={wifi}
30
+ disabled={airplane}
31
+ onChange={setWifi}
32
+ />
33
+ </LabeledContent>
34
+ <LabeledContent label="Bluetooth">
35
+ <Toggle
36
+ label="Bluetooth"
37
+ labelHidden
38
+ checked={bluetooth}
39
+ disabled={airplane}
40
+ onChange={setBluetooth}
41
+ />
42
+ </LabeledContent>
43
+ </Section>
44
+ <Section
45
+ title="Focus"
46
+ footer="Silences calls and alerts until you turn it off."
47
+ >
48
+ <LabeledContent label="Do not disturb">
49
+ <Toggle
50
+ label="Do not disturb"
51
+ labelHidden
52
+ checked={focus}
53
+ onChange={setFocus}
54
+ />
55
+ </LabeledContent>
56
+ </Section>
57
+ </div>
58
+ )
59
+ }
@@ -0,0 +1,13 @@
1
+ import { Toggle } from "@passui/react/toggle"
2
+ import { useState } from "react"
3
+
4
+ export function ToggleUsage() {
5
+ const [enabled, setEnabled] = useState(true)
6
+ return (
7
+ <Toggle
8
+ label="Wi-Fi"
9
+ checked={enabled}
10
+ onChange={setEnabled}
11
+ />
12
+ )
13
+ }
@@ -0,0 +1,69 @@
1
+ import type { ComponentProps } from "react"
2
+ import { useState } from "react"
3
+ import { defineDocs } from "../../docs/types"
4
+ import { ToggleCheckboxes } from "./examples/toggle-checkboxes"
5
+ import { ToggleSettings } from "./examples/toggle-settings"
6
+ import { ToggleShowcase } from "./examples/toggle-showcase"
7
+ import { ToggleUsage } from "./examples/toggle-usage"
8
+ import { Toggle } from "./toggle"
9
+
10
+ type ToggleProps = ComponentProps<typeof Toggle>
11
+ type DemoProps = Omit<ToggleProps, "checked" | "onChange">
12
+
13
+ /** Holds its own state so the playground can flip it. */
14
+ function ToggleDemo(props: DemoProps) {
15
+ const [checked, setChecked] = useState(true)
16
+ return (
17
+ <Toggle
18
+ {...props}
19
+ checked={checked}
20
+ onChange={setChecked}
21
+ />
22
+ )
23
+ }
24
+
25
+ export const toggleDocs = defineDocs<ToggleProps>()({
26
+ slug: "toggle",
27
+ name: "Toggle",
28
+ importPath: "@passui/react/toggle",
29
+ controls: {
30
+ style: {
31
+ kind: "select",
32
+ options: { switch: true, checkbox: true },
33
+ default: "switch",
34
+ },
35
+ disabled: { kind: "boolean", default: false },
36
+ mixed: { kind: "boolean", default: false },
37
+ labelHidden: { kind: "boolean", default: false },
38
+ label: {
39
+ kind: "text",
40
+ default: "Wi-Fi",
41
+ required: true,
42
+ },
43
+ },
44
+ render: (values) => <ToggleDemo {...values} />,
45
+ childrenSnippet: () => ({
46
+ imports: ['import { useState } from "react"'],
47
+ setup: [
48
+ "const [checked, setChecked] = useState(true)",
49
+ ],
50
+ children: "",
51
+ expressionProps: {
52
+ checked: "checked",
53
+ onChange: "setChecked",
54
+ },
55
+ }),
56
+ usage: ToggleUsage,
57
+ showcase: ToggleShowcase,
58
+ examples: [
59
+ { id: "settings", component: ToggleSettings },
60
+ {
61
+ id: "checkboxes",
62
+ component: ToggleCheckboxes,
63
+ playground: {
64
+ style: "checkbox",
65
+ label: "Remember me",
66
+ },
67
+ },
68
+ ],
69
+ })