@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,71 @@
1
+ import {
2
+ Home01Icon,
3
+ LibraryIcon,
4
+ UserIcon,
5
+ } from "@hugeicons/core-free-icons"
6
+ import { HugeiconsIcon } from "@hugeicons/react"
7
+ import { TabBar } from "@passui/react"
8
+ import { useState } from "react"
9
+
10
+ const tabs = [
11
+ {
12
+ id: "home",
13
+ label: "Home",
14
+ icon: <HugeiconsIcon icon={Home01Icon} size={22} />,
15
+ },
16
+ {
17
+ id: "library",
18
+ label: "Library",
19
+ icon: <HugeiconsIcon icon={LibraryIcon} size={22} />,
20
+ },
21
+ {
22
+ id: "profile",
23
+ label: "Profile",
24
+ icon: <HugeiconsIcon icon={UserIcon} size={22} />,
25
+ },
26
+ ]
27
+
28
+ const books = Array.from(
29
+ { length: 24 },
30
+ (_, index) => `Chapter ${index + 1}`
31
+ )
32
+
33
+ export function TabBarMinimize() {
34
+ const [active, setActive] = useState("library")
35
+
36
+ return (
37
+ <div className="relative h-96 w-full max-w-xs overflow-hidden rounded-5xl bg-background ring-1 ring-separator/60">
38
+ <ul className="m-0 h-full list-none overflow-y-auto p-4 pb-24">
39
+ {books.map((book) => (
40
+ <li
41
+ key={book}
42
+ className="border-separator/60 border-b py-3 text-subheadline text-label"
43
+ >
44
+ {book}
45
+ </li>
46
+ ))}
47
+ </ul>
48
+ <TabBar
49
+ label="Main"
50
+ tabs={tabs}
51
+ active={active}
52
+ search={{
53
+ label: "Search",
54
+ onPress: () => {},
55
+ }}
56
+ className="absolute inset-x-3 bottom-3"
57
+ renderLink={(tab, props) => (
58
+ <a
59
+ key={tab.id}
60
+ href={`#${tab.id}`}
61
+ {...props}
62
+ onClick={(event) => {
63
+ event.preventDefault()
64
+ setActive(tab.id)
65
+ }}
66
+ />
67
+ )}
68
+ />
69
+ </div>
70
+ )
71
+ }
@@ -0,0 +1,82 @@
1
+ import {
2
+ FavouriteIcon,
3
+ Home01Icon,
4
+ LibraryIcon,
5
+ UserIcon,
6
+ } from "@hugeicons/core-free-icons"
7
+ import { HugeiconsIcon } from "@hugeicons/react"
8
+ import { TabBar } from "@passui/react"
9
+ import { useState } from "react"
10
+
11
+ const tabs = [
12
+ {
13
+ id: "home",
14
+ label: "Home",
15
+ icon: <HugeiconsIcon icon={Home01Icon} size={22} />,
16
+ },
17
+ {
18
+ id: "library",
19
+ label: "Library",
20
+ icon: <HugeiconsIcon icon={LibraryIcon} size={22} />,
21
+ },
22
+ {
23
+ id: "saved",
24
+ label: "Saved",
25
+ icon: (
26
+ <HugeiconsIcon icon={FavouriteIcon} size={22} />
27
+ ),
28
+ },
29
+ {
30
+ id: "profile",
31
+ label: "Profile",
32
+ icon: <HugeiconsIcon icon={UserIcon} size={22} />,
33
+ },
34
+ ]
35
+
36
+ const titles: Record<string, string> = {
37
+ home: "Good morning, Aiko",
38
+ library: "Your library",
39
+ saved: "Saved for later",
40
+ profile: "Your profile",
41
+ }
42
+
43
+ export function TabBarShowcase() {
44
+ const [active, setActive] = useState("home")
45
+
46
+ return (
47
+ <div className="relative h-112 w-full max-w-xs overflow-hidden rounded-sheet bg-background shadow-sm ring-1 ring-separator/60">
48
+ <div className="flex flex-col gap-4 p-6 pt-10">
49
+ <h2 className="text-title2 font-bold text-label">
50
+ {titles[active]}
51
+ </h2>
52
+ {[0, 1, 2].map((row) => (
53
+ <div
54
+ key={row}
55
+ className="h-20 rounded-3xl bg-fill/60"
56
+ />
57
+ ))}
58
+ </div>
59
+ <TabBar
60
+ label="Main"
61
+ tabs={tabs}
62
+ active={active}
63
+ search={{
64
+ label: "Search",
65
+ onPress: () => {},
66
+ }}
67
+ className="absolute inset-x-3 bottom-3"
68
+ renderLink={(tab, props) => (
69
+ <a
70
+ key={tab.id}
71
+ href={`#${tab.id}`}
72
+ {...props}
73
+ onClick={(event) => {
74
+ event.preventDefault()
75
+ setActive(tab.id)
76
+ }}
77
+ />
78
+ )}
79
+ />
80
+ </div>
81
+ )
82
+ }
@@ -0,0 +1,54 @@
1
+ import {
2
+ Home01Icon,
3
+ LibraryIcon,
4
+ UserIcon,
5
+ } from "@hugeicons/core-free-icons"
6
+ import { HugeiconsIcon } from "@hugeicons/react"
7
+ import { TabBar } from "@passui/react"
8
+ import { useState } from "react"
9
+
10
+ const tabs = [
11
+ {
12
+ id: "home",
13
+ label: "Home",
14
+ icon: <HugeiconsIcon icon={Home01Icon} size={22} />,
15
+ },
16
+ {
17
+ id: "library",
18
+ label: "Library",
19
+ icon: <HugeiconsIcon icon={LibraryIcon} size={22} />,
20
+ },
21
+ {
22
+ id: "profile",
23
+ label: "Profile",
24
+ icon: <HugeiconsIcon icon={UserIcon} size={22} />,
25
+ },
26
+ ]
27
+
28
+ export function TabBarUsage() {
29
+ const [active, setActive] = useState("home")
30
+
31
+ // In an app the bar is fixed to the bottom of the screen; here it
32
+ // sits in a frame so the example stays on the page.
33
+ return (
34
+ <div className="relative h-32 w-full max-w-sm overflow-hidden rounded-5xl bg-fill/40">
35
+ <TabBar
36
+ label="Main"
37
+ tabs={tabs}
38
+ active={active}
39
+ className="absolute inset-x-3 bottom-3"
40
+ renderLink={(tab, props) => (
41
+ <a
42
+ key={tab.id}
43
+ href={`#${tab.id}`}
44
+ {...props}
45
+ onClick={(event) => {
46
+ event.preventDefault()
47
+ setActive(tab.id)
48
+ }}
49
+ />
50
+ )}
51
+ />
52
+ </div>
53
+ )
54
+ }
@@ -0,0 +1,136 @@
1
+ import { TabBarFiveTabs } from "./examples/tab-bar-five-tabs"
2
+ import {
3
+ Home01Icon,
4
+ LibraryIcon,
5
+ UserIcon,
6
+ } from "@hugeicons/core-free-icons"
7
+ import { HugeiconsIcon } from "@hugeicons/react"
8
+ import { useState } from "react"
9
+ import { defineDocs } from "../../docs/types"
10
+ import { TabBarMinimize } from "./examples/tab-bar-minimize"
11
+ import { TabBarShowcase } from "./examples/tab-bar-showcase"
12
+ import { TabBarUsage } from "./examples/tab-bar-usage"
13
+ import { TabBar } from "./tab-bar"
14
+
15
+ // The tabs and the search button are objects, so the playground picks
16
+ // the active tab and switches search on; the snippet writes the rest.
17
+ type DemoProps = {
18
+ label: string
19
+ active: "home" | "library" | "profile"
20
+ search: boolean
21
+ }
22
+
23
+ const tabs = [
24
+ {
25
+ id: "home",
26
+ label: "Home",
27
+ icon: <HugeiconsIcon icon={Home01Icon} size={22} />,
28
+ },
29
+ {
30
+ id: "library",
31
+ label: "Library",
32
+ icon: <HugeiconsIcon icon={LibraryIcon} size={22} />,
33
+ },
34
+ {
35
+ id: "profile",
36
+ label: "Profile",
37
+ icon: <HugeiconsIcon icon={UserIcon} size={22} />,
38
+ },
39
+ ] as const
40
+
41
+ function TabBarDemo({ label, active, search }: DemoProps) {
42
+ const [current, setCurrent] =
43
+ useState<DemoProps["active"]>(active)
44
+ return (
45
+ <div className="relative h-40 w-full max-w-sm overflow-hidden rounded-5xl bg-fill/40">
46
+ <TabBar
47
+ label={label}
48
+ tabs={tabs}
49
+ active={current}
50
+ search={
51
+ search
52
+ ? {
53
+ label: "Search",
54
+ onPress: () => {},
55
+ }
56
+ : undefined
57
+ }
58
+ className="absolute inset-x-3 bottom-3"
59
+ renderLink={(tab, props) => (
60
+ <a
61
+ key={tab.id}
62
+ href={`#${tab.id}`}
63
+ {...props}
64
+ onClick={(event) => {
65
+ event.preventDefault()
66
+ setCurrent(tab.id)
67
+ }}
68
+ />
69
+ )}
70
+ />
71
+ </div>
72
+ )
73
+ }
74
+
75
+ export const tabBarDocs = defineDocs<DemoProps>()({
76
+ slug: "tab-bar",
77
+ name: "TabBar",
78
+ importPath: "@passui/react",
79
+ controls: {
80
+ label: {
81
+ kind: "text",
82
+ default: "Main",
83
+ required: true,
84
+ },
85
+ active: {
86
+ kind: "select",
87
+ options: {
88
+ home: true,
89
+ library: true,
90
+ profile: true,
91
+ },
92
+ default: "home",
93
+ },
94
+ search: { kind: "boolean", default: true },
95
+ },
96
+ render: (values) => (
97
+ <TabBarDemo key={values.active} {...values} />
98
+ ),
99
+ childrenSnippet: ({ search }) => ({
100
+ imports: [
101
+ 'import { Home01Icon, LibraryIcon, UserIcon } from "@hugeicons/core-free-icons"',
102
+ 'import { HugeiconsIcon } from "@hugeicons/react"',
103
+ `import { Link, useLocation${search ? ", useNavigate" : ""} } from "@tanstack/react-router"`,
104
+ ],
105
+ setup: [
106
+ "const tabs = [",
107
+ ' { id: "/", label: "Home", icon: <HugeiconsIcon icon={Home01Icon} size={22} /> },',
108
+ ' { id: "/library", label: "Library", icon: <HugeiconsIcon icon={LibraryIcon} size={22} /> },',
109
+ ' { id: "/profile", label: "Profile", icon: <HugeiconsIcon icon={UserIcon} size={22} /> },',
110
+ "]",
111
+ "const { pathname } = useLocation()",
112
+ ...(search
113
+ ? ["const navigate = useNavigate()"]
114
+ : []),
115
+ ],
116
+ children: "",
117
+ omit: ["active", "search"],
118
+ expressionProps: {
119
+ tabs: "tabs",
120
+ active: "pathname",
121
+ ...(search
122
+ ? {
123
+ search: '{ label: "Search", onPress: () => navigate({ to: "/search" }) }',
124
+ }
125
+ : {}),
126
+ renderLink:
127
+ "(tab, props) => <Link key={tab.id} to={tab.id} {...props} />",
128
+ },
129
+ }),
130
+ usage: TabBarUsage,
131
+ showcase: TabBarShowcase,
132
+ examples: [
133
+ { id: "minimize", component: TabBarMinimize },
134
+ { id: "five-tabs", component: TabBarFiveTabs },
135
+ ],
136
+ })
@@ -0,0 +1,148 @@
1
+ import { motion } from "motion/react"
2
+ import { type ReactNode, useId } from "react"
3
+ import { useMinimizeOnScroll } from "../../hooks/use-minimize-on-scroll"
4
+ import { SearchIcon } from "../../icons/icons"
5
+ import { cn } from "../../lib/class-names"
6
+ import { spring } from "../../motion/transitions"
7
+
8
+ export interface TabBarTab<Id extends string = string> {
9
+ id: Id
10
+ label: string
11
+ icon: ReactNode
12
+ }
13
+
14
+ /** What a tab's link receives; spread it onto the router's own link. */
15
+ export interface TabBarLinkProps {
16
+ className: string
17
+ "aria-current": "page" | undefined
18
+ onClick: () => void
19
+ children: ReactNode
20
+ }
21
+
22
+ /**
23
+ * The app's main places a thumb's reach away, drawn as iOS 26 draws them: a
24
+ * floating glass capsule of tabs, with Search as its own round button at the
25
+ * trailing end. Scrolling down through content shrinks the capsule to the
26
+ * current tab; scrolling back up, or a tap, restores it. Android shares the
27
+ * glass, but its bar stays full size, as Android's own bars do.
28
+ *
29
+ * Links stay the app's own (`renderLink`), so the bar works with any router.
30
+ */
31
+ export function TabBar<Tab extends TabBarTab>({
32
+ tabs,
33
+ active,
34
+ renderLink,
35
+ search,
36
+ label,
37
+ className,
38
+ minimizeBehavior = "platform",
39
+ }: {
40
+ minimizeBehavior?: "platform" | "scroll" | "never"
41
+ tabs: readonly Tab[]
42
+ /** Null in places the bar has no tab for. */
43
+ active: Tab["id"] | null
44
+ renderLink: (
45
+ tab: Tab,
46
+ props: TabBarLinkProps
47
+ ) => ReactNode
48
+ search?: { label: string; onPress: () => void }
49
+ /** Names the navigation for screen readers. */
50
+ label: string
51
+ className?: string
52
+ }) {
53
+ // Scopes the sliding selection to this bar, so two bars on one
54
+ // screen (a docs page, a split view) never trade their highlight.
55
+ const selectionId = `${useId()}-tab-selection`
56
+ const [scrolledAway, setScrolledAway] =
57
+ useMinimizeOnScroll()
58
+ // Platform behavior keeps Android expanded; apps can opt into scroll minimization.
59
+ const minimized =
60
+ scrolledAway &&
61
+ active !== null &&
62
+ minimizeBehavior !== "never" &&
63
+ (minimizeBehavior === "scroll" || !onAndroid())
64
+ return (
65
+ <nav
66
+ aria-label={label}
67
+ className={cn(
68
+ "fixed inset-x-4 bottom-[max(14px,env(safe-area-inset-bottom))] z-30 flex items-center gap-3",
69
+ className
70
+ )}
71
+ >
72
+ <motion.div
73
+ layout
74
+ transition={spring.smooth}
75
+ // A tap on the shrunken capsule brings every tab back.
76
+ onClickCapture={(event) => {
77
+ if (!minimized) return
78
+ event.preventDefault()
79
+ event.stopPropagation()
80
+ setScrolledAway(false)
81
+ }}
82
+ className={cn(
83
+ "glass flex min-w-0 items-center rounded-full p-0.5",
84
+ minimized ? "grow-0" : "grow"
85
+ )}
86
+ >
87
+ {tabs.map((tab) => {
88
+ const current = tab.id === active
89
+ if (minimized && !current) return null
90
+ return renderLink(tab, {
91
+ "aria-current": current
92
+ ? "page"
93
+ : undefined,
94
+ // Switching tabs is silent, as on Apple and Android: no haptic.
95
+ onClick: () => {},
96
+ className: cn(
97
+ "relative isolate flex h-11 min-w-0 flex-col items-center justify-center gap-0.5 rounded-full font-semibold text-[10px] tracking-[0.01em] no-underline transition-[color,transform] duration-fast active:scale-[0.94]",
98
+ minimized
99
+ ? "w-11"
100
+ : "flex-1 px-1",
101
+ current
102
+ ? "text-accent-text"
103
+ : "text-label-secondary"
104
+ ),
105
+ children: (
106
+ <>
107
+ {current && (
108
+ <motion.span
109
+ layoutId={selectionId}
110
+ className="glass-lens absolute inset-0 -z-10 rounded-full"
111
+ transition={
112
+ spring.snappy
113
+ }
114
+ />
115
+ )}
116
+ {tab.icon}
117
+ {!minimized && (
118
+ <span className="max-w-full truncate">
119
+ {tab.label}
120
+ </span>
121
+ )}
122
+ </>
123
+ ),
124
+ })
125
+ })}
126
+ </motion.div>
127
+ {search && (
128
+ <button
129
+ type="button"
130
+ aria-label={search.label}
131
+ onClick={search.onPress}
132
+ className={cn(
133
+ "glass ms-auto flex size-12 shrink-0 items-center justify-center rounded-full text-label transition-transform duration-fast active:scale-[0.92]"
134
+ )}
135
+ >
136
+ <SearchIcon size={19} strokeWidth={2} />
137
+ </button>
138
+ )}
139
+ </nav>
140
+ )
141
+ }
142
+
143
+ function onAndroid() {
144
+ return (
145
+ typeof document !== "undefined" &&
146
+ document.documentElement.dataset.os === "android"
147
+ )
148
+ }
@@ -0,0 +1,39 @@
1
+ import { TabView } from "@passui/react/tab-view"
2
+
3
+ const days = [
4
+ { value: "mon", label: "Mon" },
5
+ { value: "tue", label: "Tue" },
6
+ { value: "wed", label: "Wed" },
7
+ { value: "thu", label: "Thu" },
8
+ { value: "fri", label: "Fri" },
9
+ ] as const
10
+
11
+ const plans = {
12
+ mon: "Design review at 10:00",
13
+ tue: "Focus time, no meetings",
14
+ wed: "Team lunch at 12:30",
15
+ thu: "Ship the release",
16
+ fri: "Retro and planning",
17
+ }
18
+
19
+ export function TabViewKeyboard() {
20
+ return (
21
+ <div className="flex w-full max-w-sm flex-col gap-3">
22
+ <p className="text-center text-footnote text-label-secondary">
23
+ Tab into the row, then use the arrow keys,
24
+ Home and End.
25
+ </p>
26
+ <TabView
27
+ label="Weekday"
28
+ defaultValue="wed"
29
+ options={days}
30
+ >
31
+ {(day) => (
32
+ <p className="rounded-3xl bg-fill/60 p-4 text-center text-callout text-label">
33
+ {plans[day]}
34
+ </p>
35
+ )}
36
+ </TabView>
37
+ </div>
38
+ )
39
+ }
@@ -0,0 +1,56 @@
1
+ import { LabeledContent, Section } from "@passui/react"
2
+ import { TabView } from "@passui/react/tab-view"
3
+ import { Toggle } from "@passui/react/toggle"
4
+ import { useState } from "react"
5
+
6
+ export function TabViewSettings() {
7
+ const [sounds, setSounds] = useState(true)
8
+ const [badges, setBadges] = useState(false)
9
+
10
+ return (
11
+ <div className="w-full max-w-sm rounded-4xl bg-elevated p-5 shadow-sm ring-1 ring-separator/60">
12
+ <TabView
13
+ label="Settings"
14
+ defaultValue="notifications"
15
+ options={[
16
+ {
17
+ value: "notifications",
18
+ label: "Notifications",
19
+ },
20
+ { value: "privacy", label: "Privacy" },
21
+ ]}
22
+ >
23
+ {(tab) =>
24
+ tab === "notifications" ? (
25
+ <Section>
26
+ <LabeledContent label="Sounds">
27
+ <Toggle
28
+ label="Sounds"
29
+ labelHidden
30
+ checked={sounds}
31
+ onChange={setSounds}
32
+ />
33
+ </LabeledContent>
34
+ <LabeledContent label="Badges">
35
+ <Toggle
36
+ label="Badges"
37
+ labelHidden
38
+ checked={badges}
39
+ onChange={setBadges}
40
+ />
41
+ </LabeledContent>
42
+ </Section>
43
+ ) : (
44
+ <Section footer="Only people you share with can see your profile.">
45
+ <LabeledContent label="Profile">
46
+ <span className="text-subheadline text-label-secondary">
47
+ Private
48
+ </span>
49
+ </LabeledContent>
50
+ </Section>
51
+ )
52
+ }
53
+ </TabView>
54
+ </div>
55
+ )
56
+ }
@@ -0,0 +1,53 @@
1
+ import { LabeledContent, Section } from "@passui/react"
2
+ import { StatTile } from "@passui/react/stat-tile"
3
+ import { TabView } from "@passui/react/tab-view"
4
+
5
+ export function TabViewShowcase() {
6
+ return (
7
+ <div className="w-full max-w-md rounded-4xl bg-elevated p-6 shadow-sm ring-1 ring-separator/60">
8
+ <TabView
9
+ label="Pass console"
10
+ defaultValue="overview"
11
+ options={[
12
+ { value: "overview", label: "Overview" },
13
+ { value: "members", label: "Members" },
14
+ { value: "settings", label: "Settings" },
15
+ ]}
16
+ >
17
+ {(tab) =>
18
+ tab === "overview" ? (
19
+ <div className="grid grid-cols-2 gap-3">
20
+ <StatTile
21
+ label="Sign ins today"
22
+ value={128}
23
+ />
24
+ <StatTile
25
+ label="Failed sign ins"
26
+ value={3}
27
+ tone="critical"
28
+ />
29
+ </div>
30
+ ) : tab === "members" ? (
31
+ <Section title="Team">
32
+ <LabeledContent label="Aiko Kamado">
33
+ Owner
34
+ </LabeledContent>
35
+ <LabeledContent label="Kenji Mori">
36
+ Admin
37
+ </LabeledContent>
38
+ </Section>
39
+ ) : (
40
+ <Section title="Project">
41
+ <LabeledContent label="Name">
42
+ Pass
43
+ </LabeledContent>
44
+ <LabeledContent label="Region">
45
+ Europe
46
+ </LabeledContent>
47
+ </Section>
48
+ )
49
+ }
50
+ </TabView>
51
+ </div>
52
+ )
53
+ }
@@ -0,0 +1,23 @@
1
+ import { TabView } from "@passui/react/tab-view"
2
+
3
+ export function TabViewUsage() {
4
+ return (
5
+ <TabView
6
+ label="Account"
7
+ defaultValue="profile"
8
+ options={[
9
+ { value: "profile", label: "Profile" },
10
+ { value: "security", label: "Security" },
11
+ ]}
12
+ className="max-w-sm"
13
+ >
14
+ {(value) => (
15
+ <p className="text-center text-callout text-label-secondary">
16
+ {value === "profile"
17
+ ? "Your name and photo."
18
+ : "Your password and passkeys."}
19
+ </p>
20
+ )}
21
+ </TabView>
22
+ )
23
+ }