@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,66 @@
1
+ import type { ReactNode } from "react"
2
+ import { cn } from "../../lib/class-names"
3
+
4
+ /**
5
+ * A row: a label, an optional description, and its control at the trailing
6
+ * end, as SwiftUI's `LabeledContent`. Wide controls move under the label
7
+ * when there is no room beside it, rather than squeezing.
8
+ */
9
+ export function LabeledContent({
10
+ label,
11
+ description,
12
+ children,
13
+ stacked,
14
+ wide,
15
+ }: {
16
+ label: string
17
+ description?: string
18
+ children: ReactNode
19
+ /** Put the control under the label, for wide controls. */
20
+ stacked?: boolean
21
+ /** A wide control (segmented) that moves under the label on phones. */
22
+ wide?: boolean
23
+ }) {
24
+ return (
25
+ <div
26
+ className={cn(
27
+ "flex flex-wrap gap-x-4 gap-y-2.5 px-4 py-3",
28
+ stacked
29
+ ? "flex-col items-stretch"
30
+ : cn(
31
+ "items-center justify-between",
32
+ wide &&
33
+ "max-sm:flex-col max-sm:items-stretch max-sm:gap-2.5"
34
+ )
35
+ )}
36
+ >
37
+ <div
38
+ className={cn(
39
+ "flex min-w-0 flex-1 flex-col break-normal",
40
+ !stacked && "basis-36",
41
+ wide && "max-sm:basis-auto"
42
+ )}
43
+ >
44
+ <span className="text-subheadline text-label">
45
+ {label}
46
+ </span>
47
+ {description && (
48
+ <span className="text-footnote leading-snug text-label-secondary">
49
+ {description}
50
+ </span>
51
+ )}
52
+ </div>
53
+ {/* Controls give way before the label does; a segmented control that
54
+ no longer fits turns into a pop-up button by itself. */}
55
+ <div
56
+ className={cn(
57
+ "ms-auto flex max-w-full min-w-0 shrink-0 justify-end [&>*]:max-w-full",
58
+ stacked && "w-full",
59
+ wide && "max-sm:w-full"
60
+ )}
61
+ >
62
+ {children}
63
+ </div>
64
+ </div>
65
+ )
66
+ }
@@ -0,0 +1,30 @@
1
+ import { LocaleSwitcher } from "@passui/react/locale-switcher"
2
+ import { useState } from "react"
3
+
4
+ export function LocaleSwitcherNativeNames() {
5
+ const [locale, setLocale] = useState("en")
6
+
7
+ // a transformed frame holds the fixed switcher on this page
8
+ return (
9
+ <div className="relative h-80 w-full max-w-sm transform-gpu overflow-hidden rounded-4xl bg-fill/40">
10
+ <p className="p-6 text-footnote text-label-secondary">
11
+ Current: {locale}
12
+ </p>
13
+ <LocaleSwitcher
14
+ label="Change language"
15
+ locales={[
16
+ "en",
17
+ "fr",
18
+ "de",
19
+ "es",
20
+ "pt",
21
+ "ja",
22
+ "zh",
23
+ ]}
24
+ locale={locale}
25
+ onSelect={setLocale}
26
+ storageKey="docs-locale-switcher-native-names"
27
+ />
28
+ </div>
29
+ )
30
+ }
@@ -0,0 +1,46 @@
1
+ import { LocaleSwitcher } from "@passui/react/locale-switcher"
2
+ import { useState } from "react"
3
+
4
+ const greetings: Record<
5
+ string,
6
+ { title: string; body: string }
7
+ > = {
8
+ en: {
9
+ title: "Welcome back",
10
+ body: "Pick up where you left off.",
11
+ },
12
+ fr: {
13
+ title: "Bon retour",
14
+ body: "Reprenez là où vous vous êtes arrêté.",
15
+ },
16
+ ja: {
17
+ title: "おかえりなさい",
18
+ body: "前回の続きから始めましょう。",
19
+ },
20
+ }
21
+
22
+ export function LocaleSwitcherShowcase() {
23
+ const [locale, setLocale] = useState("en")
24
+ const text = greetings[locale]
25
+
26
+ return (
27
+ <div
28
+ lang={locale}
29
+ className="relative flex h-72 w-full max-w-sm transform-gpu flex-col items-center justify-center gap-2 overflow-hidden rounded-sheet bg-background p-8 text-center shadow-sm ring-1 ring-separator/60"
30
+ >
31
+ <h2 className="text-title2 font-bold text-label">
32
+ {text.title}
33
+ </h2>
34
+ <p className="text-callout text-label-secondary">
35
+ {text.body}
36
+ </p>
37
+ <LocaleSwitcher
38
+ label="Change language"
39
+ locales={["en", "fr", "ja"]}
40
+ locale={locale}
41
+ onSelect={setLocale}
42
+ storageKey="docs-locale-switcher-showcase"
43
+ />
44
+ </div>
45
+ )
46
+ }
@@ -0,0 +1,35 @@
1
+ import { LocaleSwitcher } from "@passui/react/locale-switcher"
2
+ import { useState } from "react"
3
+
4
+ const copy = {
5
+ en: { title: "Sign in", button: "Continue" },
6
+ fr: { title: "Connexion", button: "Continuer" },
7
+ zh: { title: "登录", button: "继续" },
8
+ }
9
+
10
+ type Locale = keyof typeof copy
11
+
12
+ export function LocaleSwitcherTranslatedPage() {
13
+ const [locale, setLocale] = useState<Locale>("fr")
14
+
15
+ return (
16
+ <div
17
+ lang={locale}
18
+ className="relative flex h-64 w-full max-w-sm transform-gpu flex-col items-center justify-center gap-4 overflow-hidden rounded-4xl bg-elevated shadow-sm ring-1 ring-separator/60"
19
+ >
20
+ <h2 className="text-title2 font-bold text-label">
21
+ {copy[locale].title}
22
+ </h2>
23
+ <span className="rounded-full bg-inverse px-5 py-2 text-subheadline font-semibold text-on-inverse">
24
+ {copy[locale].button}
25
+ </span>
26
+ <LocaleSwitcher
27
+ label="Change language"
28
+ locales={["en", "fr", "zh"] as Locale[]}
29
+ locale={locale}
30
+ onSelect={setLocale}
31
+ storageKey="docs-locale-switcher-translated-page"
32
+ />
33
+ </div>
34
+ )
35
+ }
@@ -0,0 +1,20 @@
1
+ import { LocaleSwitcher } from "@passui/react/locale-switcher"
2
+ import { useState } from "react"
3
+
4
+ export function LocaleSwitcherUsage() {
5
+ const [locale, setLocale] = useState("en")
6
+
7
+ // In an app it floats over the whole screen; here a transformed
8
+ // frame holds it, since a fixed element sits inside such a frame.
9
+ return (
10
+ <div className="relative h-48 w-full max-w-sm transform-gpu overflow-hidden rounded-4xl bg-fill/40">
11
+ <LocaleSwitcher
12
+ label="Change language"
13
+ locales={["en", "fr", "zh"]}
14
+ locale={locale}
15
+ onSelect={setLocale}
16
+ storageKey="docs-locale-switcher-usage"
17
+ />
18
+ </div>
19
+ )
20
+ }
@@ -0,0 +1 @@
1
+ export * from "./locale-switcher"
@@ -0,0 +1,91 @@
1
+ import { LocaleSwitcherTranslatedPage } from "./examples/locale-switcher-translated-page"
2
+ import { LocaleSwitcherNativeNames } from "./examples/locale-switcher-native-names"
3
+ import { useState } from "react"
4
+ import { defineDocs } from "../../docs/types"
5
+ import { LocaleSwitcherShowcase } from "./examples/locale-switcher-showcase"
6
+ import { LocaleSwitcherUsage } from "./examples/locale-switcher-usage"
7
+ import { LocaleSwitcher } from "./locale-switcher"
8
+
9
+ // The locales list is an array, so the playground offers a few sets.
10
+ type DemoProps = {
11
+ label: string
12
+ locales: "en-fr-zh" | "en-ja" | "en-de-es-pt"
13
+ }
14
+
15
+ const sets = {
16
+ "en-fr-zh": ["en", "fr", "zh"],
17
+ "en-ja": ["en", "ja"],
18
+ "en-de-es-pt": ["en", "de", "es", "pt"],
19
+ } as const
20
+
21
+ function LocaleSwitcherDemo({ label, locales }: DemoProps) {
22
+ const [locale, setLocale] = useState<string>("en")
23
+ return (
24
+ <div className="flex flex-col items-center gap-2 text-center">
25
+ <p className="text-headline font-semibold text-label">
26
+ Current language: {locale}
27
+ </p>
28
+ <p className="text-footnote text-label-secondary">
29
+ The switcher floats in the bottom corner. Drag
30
+ it to another corner.
31
+ </p>
32
+ <LocaleSwitcher
33
+ key={locales}
34
+ label={label}
35
+ locales={sets[locales]}
36
+ locale={locale}
37
+ onSelect={setLocale}
38
+ storageKey="passui-docs-locale-switcher"
39
+ />
40
+ </div>
41
+ )
42
+ }
43
+
44
+ export const localeSwitcherDocs = defineDocs<DemoProps>()({
45
+ slug: "locale-switcher",
46
+ name: "LocaleSwitcher",
47
+ importPath: "@passui/react/locale-switcher",
48
+ controls: {
49
+ label: {
50
+ kind: "text",
51
+ default: "Change language",
52
+ required: true,
53
+ },
54
+ locales: {
55
+ kind: "select",
56
+ options: {
57
+ "en-fr-zh": true,
58
+ "en-ja": true,
59
+ "en-de-es-pt": true,
60
+ },
61
+ default: "en-fr-zh",
62
+ },
63
+ },
64
+ render: (values) => <LocaleSwitcherDemo {...values} />,
65
+ childrenSnippet: ({ locales }) => ({
66
+ imports: ['import { useState } from "react"'],
67
+ setup: ['const [locale, setLocale] = useState("en")'],
68
+ omit: ["locales"],
69
+ extraProps: { storageKey: "locale-switcher" },
70
+ expressionProps: {
71
+ locales: JSON.stringify(sets[locales]),
72
+ locale: "locale",
73
+ onSelect: "setLocale",
74
+ },
75
+ children: "",
76
+ }),
77
+ usage: LocaleSwitcherUsage,
78
+ showcase: LocaleSwitcherShowcase,
79
+ examples: [
80
+ {
81
+ id: "native-names",
82
+ component: LocaleSwitcherNativeNames,
83
+ playground: { locales: "en-de-es-pt" },
84
+ },
85
+ {
86
+ id: "translated-page",
87
+ component: LocaleSwitcherTranslatedPage,
88
+ },
89
+ ],
90
+ previewHeight: 320,
91
+ })
@@ -0,0 +1,252 @@
1
+ import { getNativeLanguageName } from "../../lib/language"
2
+ import { cn } from "../../lib/class-names"
3
+ import {
4
+ AnimatePresence,
5
+ motion,
6
+ useDragControls,
7
+ useMotionValue,
8
+ } from "motion/react"
9
+ import { useEffect, useRef, useState } from "react"
10
+
11
+ type Side = "left" | "right"
12
+ type Edge = "top" | "bottom"
13
+
14
+ type Anchor = {
15
+ side: Side
16
+ edge: Edge
17
+ x: number
18
+ y: number
19
+ }
20
+
21
+ const DEFAULT_ANCHOR: Anchor = {
22
+ side: "right",
23
+ edge: "bottom",
24
+ x: 20,
25
+ y: 20,
26
+ }
27
+
28
+ function loadStoredAnchor(storageKey: string): Anchor | null {
29
+ try {
30
+ const raw = localStorage.getItem(storageKey)
31
+ if (!raw) return null
32
+ const parsed = JSON.parse(raw) as Partial<Anchor>
33
+ if (
34
+ (parsed.side === "left" ||
35
+ parsed.side === "right") &&
36
+ (parsed.edge === "top" ||
37
+ parsed.edge === "bottom") &&
38
+ typeof parsed.x === "number" &&
39
+ typeof parsed.y === "number"
40
+ ) {
41
+ return {
42
+ side: parsed.side,
43
+ edge: parsed.edge,
44
+ x: parsed.x,
45
+ y: parsed.y,
46
+ }
47
+ }
48
+ } catch {}
49
+ return null
50
+ }
51
+
52
+ function saveStoredAnchor(
53
+ storageKey: string,
54
+ anchor: Anchor
55
+ ) {
56
+ try {
57
+ localStorage.setItem(
58
+ storageKey,
59
+ JSON.stringify(anchor)
60
+ )
61
+ } catch {}
62
+ }
63
+
64
+ export type LocaleSwitcherProps<L extends string> = {
65
+ locales: readonly L[]
66
+ locale: L
67
+ onSelect: (locale: L) => void
68
+ /** localStorage key for where the person dragged it. */
69
+ storageKey: string
70
+ label: string
71
+ }
72
+
73
+ /** A draggable floating language picker that snaps to the nearest corner. */
74
+ export function LocaleSwitcher<L extends string>({
75
+ locales,
76
+ locale,
77
+ onSelect,
78
+ storageKey,
79
+ label,
80
+ }: LocaleSwitcherProps<L>) {
81
+ const containerRef = useRef<HTMLDivElement>(null)
82
+ const dragControls = useDragControls()
83
+ const x = useMotionValue(0)
84
+ const y = useMotionValue(0)
85
+ const [open, setOpen] = useState(false)
86
+ const [anchor, setAnchor] = useState(DEFAULT_ANCHOR)
87
+
88
+ const langs = Array.from(locales, (value) => ({
89
+ value: value,
90
+ label: getNativeLanguageName(value),
91
+ }))
92
+
93
+ useEffect(() => {
94
+ const stored = loadStoredAnchor(storageKey)
95
+ if (stored) setAnchor(stored)
96
+ }, [storageKey])
97
+
98
+ useEffect(() => {
99
+ if (!open) return
100
+ function handlePointerDown(e: PointerEvent) {
101
+ if (
102
+ containerRef.current &&
103
+ !containerRef.current.contains(
104
+ e.target as Node
105
+ )
106
+ ) {
107
+ setOpen(false)
108
+ }
109
+ }
110
+ document.addEventListener(
111
+ "pointerdown",
112
+ handlePointerDown
113
+ )
114
+ return () =>
115
+ document.removeEventListener(
116
+ "pointerdown",
117
+ handlePointerDown
118
+ )
119
+ }, [open])
120
+
121
+ function commitDraggedPosition() {
122
+ const el = containerRef.current
123
+ if (!el) return
124
+ const rect = el.getBoundingClientRect()
125
+ const side: Side =
126
+ rect.left + rect.width / 2 < window.innerWidth / 2
127
+ ? "left"
128
+ : "right"
129
+ const edge: Edge =
130
+ rect.top + rect.height / 2 <
131
+ window.innerHeight / 2
132
+ ? "top"
133
+ : "bottom"
134
+ const next: Anchor = {
135
+ side,
136
+ edge,
137
+ x:
138
+ side === "left"
139
+ ? rect.left
140
+ : window.innerWidth - rect.right,
141
+ y:
142
+ edge === "top"
143
+ ? rect.top
144
+ : window.innerHeight - rect.bottom,
145
+ }
146
+ setAnchor(next)
147
+ saveStoredAnchor(storageKey, next)
148
+ x.set(0)
149
+ y.set(0)
150
+ }
151
+
152
+ const growsUp = anchor.edge === "bottom"
153
+
154
+ return (
155
+ <motion.div
156
+ ref={containerRef}
157
+ drag
158
+ dragControls={dragControls}
159
+ dragListener={false}
160
+ dragMomentum={false}
161
+ dragElastic={0}
162
+ style={{
163
+ position: "fixed",
164
+ x,
165
+ y,
166
+ ...(anchor.side === "left"
167
+ ? { left: anchor.x }
168
+ : { right: anchor.x }),
169
+ ...(anchor.edge === "top"
170
+ ? { top: anchor.y }
171
+ : { bottom: anchor.y }),
172
+ }}
173
+ onDragEnd={commitDraggedPosition}
174
+ onMouseLeave={() => open && setOpen(false)}
175
+ className="z-60"
176
+ >
177
+ <motion.div
178
+ layout
179
+ transition={{
180
+ type: "spring",
181
+ damping: 28,
182
+ stiffness: 380,
183
+ }}
184
+ className={cn(
185
+ "overflow-hidden rounded-2xl border border-border/35 bg-popover/80 shadow-lg",
186
+ "backdrop-blur-xl backdrop-saturate-150",
187
+ "flex",
188
+ growsUp ? "flex-col-reverse" : "flex-col",
189
+ open ? "w-40 gap-px p-1" : "size-10"
190
+ )}
191
+ >
192
+ <button
193
+ type="button"
194
+ aria-label={label}
195
+ aria-expanded={open}
196
+ onPointerDown={(e) =>
197
+ dragControls.start(e)
198
+ }
199
+ onClick={() => setOpen((prev) => !prev)}
200
+ className={cn(
201
+ "flex shrink-0 cursor-grab items-center justify-center",
202
+ "active:cursor-grabbing text-xs font-semibold",
203
+ open
204
+ ? "h-9 w-full rounded-lg hover:bg-accent"
205
+ : "size-10 rounded-2xl"
206
+ )}
207
+ >
208
+ {locale.toUpperCase()}
209
+ </button>
210
+
211
+ <AnimatePresence>
212
+ {open && (
213
+ <motion.div
214
+ initial={{ opacity: 0 }}
215
+ animate={{ opacity: 1 }}
216
+ exit={{ opacity: 0 }}
217
+ transition={{ delay: 0.05 }}
218
+ className="flex flex-col gap-px"
219
+ >
220
+ {langs.map((item) => {
221
+ const active =
222
+ item.value === locale
223
+ return (
224
+ <button
225
+ key={item.value}
226
+ type="button"
227
+ onClick={() => {
228
+ setOpen(false)
229
+ if (!active) {
230
+ onSelect(
231
+ item.value
232
+ )
233
+ }
234
+ }}
235
+ className={cn(
236
+ "rounded-sm px-2 py-1.5 text-left text-sm",
237
+ "hover:bg-accent transition-colors",
238
+ active &&
239
+ "text-primary font-medium"
240
+ )}
241
+ >
242
+ {item.label}
243
+ </button>
244
+ )
245
+ })}
246
+ </motion.div>
247
+ )}
248
+ </AnimatePresence>
249
+ </motion.div>
250
+ </motion.div>
251
+ )
252
+ }
@@ -0,0 +1,44 @@
1
+ import {
2
+ LabeledContent,
3
+ Picker,
4
+ Section,
5
+ } from "@passui/react"
6
+ import { useState } from "react"
7
+
8
+ export function PickerDetails() {
9
+ const [region, setRegion] = useState("eu")
10
+
11
+ return (
12
+ <div className="w-full max-w-sm">
13
+ <Section
14
+ title="Data"
15
+ footer="Where your account's data is stored."
16
+ >
17
+ <LabeledContent label="Region">
18
+ <Picker
19
+ label="Region"
20
+ value={region}
21
+ onChange={setRegion}
22
+ options={[
23
+ {
24
+ value: "eu",
25
+ label: "Europe",
26
+ detail: "Frankfurt",
27
+ },
28
+ {
29
+ value: "us",
30
+ label: "United States",
31
+ detail: "Virginia",
32
+ },
33
+ {
34
+ value: "ap",
35
+ label: "Asia Pacific",
36
+ detail: "Tokyo",
37
+ },
38
+ ]}
39
+ />
40
+ </LabeledContent>
41
+ </Section>
42
+ </div>
43
+ )
44
+ }
@@ -0,0 +1,48 @@
1
+ import { Picker } from "@passui/react"
2
+ import { useState } from "react"
3
+
4
+ const zones = [
5
+ {
6
+ value: "Africa/Nairobi",
7
+ label: "Nairobi",
8
+ detail: "GMT+3",
9
+ },
10
+ {
11
+ value: "Europe/Paris",
12
+ label: "Paris",
13
+ detail: "GMT+2",
14
+ },
15
+ {
16
+ value: "Europe/London",
17
+ label: "London",
18
+ detail: "GMT+1",
19
+ },
20
+ {
21
+ value: "America/New_York",
22
+ label: "New York",
23
+ detail: "GMT-4",
24
+ },
25
+ { value: "Asia/Tokyo", label: "Tokyo", detail: "GMT+9" },
26
+ {
27
+ value: "Australia/Sydney",
28
+ label: "Sydney",
29
+ detail: "GMT+11",
30
+ },
31
+ ]
32
+
33
+ export function PickerSearchable() {
34
+ const [zone, setZone] = useState("Africa/Nairobi")
35
+ return (
36
+ <div className="w-full max-w-xs">
37
+ <Picker
38
+ label="Time zone"
39
+ value={zone}
40
+ onChange={setZone}
41
+ options={zones}
42
+ searchable
43
+ searchPlaceholder="Search cities"
44
+ emptyLabel="No matching city"
45
+ />
46
+ </div>
47
+ )
48
+ }