@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,113 @@
1
+ import {
2
+ FLOATING_LAYER,
3
+ ownsFloatingLayer,
4
+ } from "../../lib/floating-layers"
5
+
6
+ const surfaces: HTMLElement[] = []
7
+ const isolated = new Map<HTMLElement, boolean>()
8
+ let overflow = ""
9
+ let overflowPriority = ""
10
+
11
+ function restoreBackground() {
12
+ for (const [element, inert] of isolated)
13
+ element.inert = inert
14
+ isolated.clear()
15
+ }
16
+
17
+ function isolateBackground() {
18
+ restoreBackground()
19
+ const surface = surfaces.at(-1)
20
+ if (!surface) return
21
+ const roots = [surface.parentElement ?? surface]
22
+ for (const layer of document.querySelectorAll<HTMLElement>(
23
+ `[${FLOATING_LAYER}]`
24
+ )) {
25
+ if (ownsFloatingLayer(surface, layer))
26
+ roots.push(layer)
27
+ }
28
+ const visit = (parent: HTMLElement) => {
29
+ for (const child of parent.children) {
30
+ if (!(child instanceof HTMLElement)) continue
31
+ if (roots.includes(child)) continue
32
+ if (roots.some((root) => child.contains(root))) {
33
+ visit(child)
34
+ } else if (
35
+ !child.matches("script, style, link")
36
+ ) {
37
+ isolated.set(child, child.inert)
38
+ child.inert = true
39
+ }
40
+ }
41
+ }
42
+ visit(document.body)
43
+ }
44
+
45
+ /** Keeps nested sheets and their portaled controls above an inactive background. */
46
+ export function enterModalEnvironment(
47
+ surface: HTMLElement
48
+ ): () => void {
49
+ if (!surfaces.length) {
50
+ overflow =
51
+ document.body.style.getPropertyValue("overflow")
52
+ overflowPriority =
53
+ document.body.style.getPropertyPriority(
54
+ "overflow"
55
+ )
56
+ document.body.style.setProperty("overflow", "hidden")
57
+ }
58
+ surfaces.push(surface)
59
+ isolateBackground()
60
+ const preventBackgroundScroll = (
61
+ event: WheelEvent | TouchEvent
62
+ ) => {
63
+ if (surfaces.at(-1) !== surface) return
64
+ const target = event.target
65
+ if (
66
+ target instanceof Element &&
67
+ (surface.contains(target) ||
68
+ ownsFloatingLayer(surface, target))
69
+ )
70
+ return
71
+ event.preventDefault()
72
+ }
73
+ document.addEventListener(
74
+ "wheel",
75
+ preventBackgroundScroll,
76
+ { passive: false }
77
+ )
78
+ document.addEventListener(
79
+ "touchmove",
80
+ preventBackgroundScroll,
81
+ { passive: false }
82
+ )
83
+ const observer = new MutationObserver(() => {
84
+ if (surfaces.at(-1) === surface) isolateBackground()
85
+ })
86
+ observer.observe(document.body, {
87
+ childList: true,
88
+ subtree: true,
89
+ })
90
+ return () => {
91
+ observer.disconnect()
92
+ document.removeEventListener(
93
+ "wheel",
94
+ preventBackgroundScroll
95
+ )
96
+ document.removeEventListener(
97
+ "touchmove",
98
+ preventBackgroundScroll
99
+ )
100
+ surfaces.splice(surfaces.indexOf(surface), 1)
101
+ isolateBackground()
102
+ if (!surfaces.length) {
103
+ if (overflow)
104
+ document.body.style.setProperty(
105
+ "overflow",
106
+ overflow,
107
+ overflowPriority
108
+ )
109
+ else
110
+ document.body.style.removeProperty("overflow")
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,89 @@
1
+ import {
2
+ type ReactNode,
3
+ useLayoutEffect,
4
+ useRef,
5
+ useState,
6
+ } from "react"
7
+ import { createPortal } from "react-dom"
8
+
9
+ /** Escape transformed page layers while retaining the source's scoped appearance. */
10
+ export function SheetPortal({
11
+ children,
12
+ }: {
13
+ children: ReactNode
14
+ }) {
15
+ const source = useRef<HTMLSpanElement>(null)
16
+ const [host, setHost] = useState<HTMLDivElement | null>(
17
+ null
18
+ )
19
+ useLayoutEffect(() => {
20
+ const marker = source.current
21
+ if (!marker) return
22
+ const document = marker.ownerDocument
23
+ const layer = document.createElement("div")
24
+ layer.dataset.passuiOverlay = ""
25
+ layer.style.display = "contents"
26
+ const attributes = [
27
+ "data-theme",
28
+ "data-accent",
29
+ "data-os",
30
+ "dir",
31
+ ]
32
+ let properties = new Set<string>()
33
+ const inherit = () => {
34
+ for (const name of attributes) {
35
+ const value = marker
36
+ .closest(`[${name}]`)
37
+ ?.getAttribute(name)
38
+ if (value === null || value === undefined)
39
+ layer.removeAttribute(name)
40
+ else layer.setAttribute(name, value)
41
+ }
42
+ const computed = getComputedStyle(marker)
43
+ const next = new Set<string>()
44
+ for (const name of computed) {
45
+ if (!name.startsWith("--")) continue
46
+ next.add(name)
47
+ layer.style.setProperty(
48
+ name,
49
+ computed.getPropertyValue(name)
50
+ )
51
+ }
52
+ for (const name of properties)
53
+ if (!next.has(name))
54
+ layer.style.removeProperty(name)
55
+ properties = next
56
+ layer.style.colorScheme = computed.colorScheme
57
+ layer.style.fontFamily = computed.fontFamily
58
+ layer.style.direction = computed.direction
59
+ }
60
+ inherit()
61
+ document.body.appendChild(layer)
62
+ setHost(layer)
63
+ const observer = new MutationObserver(inherit)
64
+ for (
65
+ let ancestor = marker.parentElement;
66
+ ancestor;
67
+ ancestor = ancestor.parentElement
68
+ ) {
69
+ observer.observe(ancestor, {
70
+ attributes: true,
71
+ attributeFilter: [
72
+ ...attributes,
73
+ "style",
74
+ "class",
75
+ ],
76
+ })
77
+ }
78
+ return () => {
79
+ observer.disconnect()
80
+ layer.remove()
81
+ }
82
+ }, [])
83
+ return (
84
+ <>
85
+ <span ref={source} hidden aria-hidden="true" />
86
+ {host && createPortal(children, host)}
87
+ </>
88
+ )
89
+ }
@@ -0,0 +1,75 @@
1
+ import { SheetChoices } from "./examples/sheet-choices"
2
+ import { useState } from "react"
3
+ import { Button } from "../button/button"
4
+ import { defineDocs } from "../../docs/types"
5
+ import { SheetShare } from "./examples/sheet-share"
6
+ import { SheetShowcase } from "./examples/sheet-showcase"
7
+ import { SheetUsage } from "./examples/sheet-usage"
8
+ import { Sheet } from "./sheet"
9
+
10
+ type DemoProps = {
11
+ label: string
12
+ }
13
+
14
+ function SheetDemo({ label }: DemoProps) {
15
+ const [open, setOpen] = useState(false)
16
+ return (
17
+ <>
18
+ <Button
19
+ variant="secondary"
20
+ onClick={() => setOpen(true)}
21
+ >
22
+ Open sheet
23
+ </Button>
24
+ <Sheet
25
+ open={open}
26
+ onClose={() => setOpen(false)}
27
+ label={label}
28
+ >
29
+ <div className="flex flex-col gap-3 p-6">
30
+ <h2 className="text-title3 font-semibold text-label">
31
+ {label}
32
+ </h2>
33
+ <p className="text-callout text-label-secondary">
34
+ Escape, a tap outside, or the button
35
+ closes it.
36
+ </p>
37
+ <Button onClick={() => setOpen(false)}>
38
+ Done
39
+ </Button>
40
+ </div>
41
+ </Sheet>
42
+ </>
43
+ )
44
+ }
45
+
46
+ export const sheetDocs = defineDocs<DemoProps>()({
47
+ slug: "sheet",
48
+ name: "Sheet",
49
+ importPath: "@passui/react/sheet",
50
+ controls: {
51
+ label: {
52
+ kind: "text",
53
+ default: "Details",
54
+ required: true,
55
+ },
56
+ },
57
+ render: (values) => <SheetDemo {...values} />,
58
+ childrenSnippet: () => ({
59
+ imports: ['import { useState } from "react"'],
60
+ setup: ["const [open, setOpen] = useState(false)"],
61
+ expressionProps: {
62
+ open: "open",
63
+ onClose: "() => setOpen(false)",
64
+ },
65
+ children:
66
+ '\n <div className="p-6">The sheet\'s content</div>\n',
67
+ }),
68
+ usage: SheetUsage,
69
+ showcase: SheetShowcase,
70
+ examples: [
71
+ { id: "share", component: SheetShare },
72
+ { id: "choices", component: SheetChoices },
73
+ ],
74
+ previewHeight: 420,
75
+ })
@@ -0,0 +1,112 @@
1
+ import {
2
+ AnimatePresence,
3
+ motion,
4
+ useReducedMotion,
5
+ } from "motion/react"
6
+ import { type ReactNode, useRef } from "react"
7
+ import { useVisualViewport } from "../../hooks/use-visual-viewport"
8
+ import { cn } from "../../lib/class-names"
9
+ import { spring } from "../../motion/transitions"
10
+ import { SheetPortal } from "./sheet-portal"
11
+ import { useSheetFocus } from "./use-sheet-focus"
12
+
13
+ interface SheetProps {
14
+ open: boolean
15
+ onClose: () => void
16
+ /** Read out by screen readers as the sheet's name. */
17
+ label: string
18
+ labelledBy?: string
19
+ className?: string
20
+ children: ReactNode
21
+ }
22
+
23
+ /**
24
+ * A modal card: rises from the bottom edge on phones and floats in the
25
+ * middle on larger screens. Escape or a tap outside closes it.
26
+ */
27
+ export function Sheet({ open, ...props }: SheetProps) {
28
+ return (
29
+ <AnimatePresence>
30
+ {open && (
31
+ <SheetPortal>
32
+ <OpenSheet {...props} />
33
+ </SheetPortal>
34
+ )}
35
+ </AnimatePresence>
36
+ )
37
+ }
38
+
39
+ function OpenSheet({
40
+ onClose,
41
+ label,
42
+ labelledBy,
43
+ className,
44
+ children,
45
+ }: Omit<SheetProps, "open">) {
46
+ const panel = useRef<HTMLDivElement>(null)
47
+ const reducedMotion = useReducedMotion()
48
+ const viewport = useVisualViewport()
49
+ useSheetFocus(panel, onClose)
50
+
51
+ return (
52
+ <div
53
+ className={cn(
54
+ "fixed inset-0 z-[65] flex items-end justify-center",
55
+ "sm:items-center sm:p-6"
56
+ )}
57
+ style={
58
+ viewport
59
+ ? {
60
+ top: viewport.top,
61
+ left: viewport.left,
62
+ width: viewport.width,
63
+ height: viewport.height,
64
+ bottom: "auto",
65
+ right: "auto",
66
+ }
67
+ : undefined
68
+ }
69
+ >
70
+ <motion.div
71
+ aria-hidden="true"
72
+ className="absolute inset-0 bg-black/30 backdrop-blur-md"
73
+ initial={{ opacity: 0 }}
74
+ animate={{ opacity: 1 }}
75
+ exit={{ opacity: 0 }}
76
+ onClick={onClose}
77
+ />
78
+ <motion.div
79
+ ref={panel}
80
+ role="dialog"
81
+ aria-modal="true"
82
+ aria-label={labelledBy ? undefined : label}
83
+ aria-labelledby={labelledBy}
84
+ tabIndex={-1}
85
+ className={cn(
86
+ "glass-menu relative flex w-full flex-col overflow-hidden",
87
+ "max-h-[92%] max-w-[560px]",
88
+ "rounded-t-sheet sm:rounded-sheet",
89
+ className
90
+ )}
91
+ initial={
92
+ reducedMotion
93
+ ? { opacity: 0 }
94
+ : { y: 40, opacity: 0, scale: 0.98 }
95
+ }
96
+ animate={{ y: 0, opacity: 1, scale: 1 }}
97
+ exit={
98
+ reducedMotion
99
+ ? { opacity: 0 }
100
+ : { y: 30, opacity: 0 }
101
+ }
102
+ transition={
103
+ reducedMotion
104
+ ? { duration: 0 }
105
+ : spring.smooth
106
+ }
107
+ >
108
+ {children}
109
+ </motion.div>
110
+ </div>
111
+ )
112
+ }
@@ -0,0 +1,130 @@
1
+ import {
2
+ type RefObject,
3
+ useEffect,
4
+ useLayoutEffect,
5
+ useRef,
6
+ useState,
7
+ } from "react"
8
+ import { ownsFloatingLayer } from "../../lib/floating-layers"
9
+ import { enterModalEnvironment } from "./modal-environment"
10
+
11
+ const sheets: HTMLElement[] = []
12
+ const focusableSelector = [
13
+ "a[href]",
14
+ "button",
15
+ "input:not([type=hidden])",
16
+ "select",
17
+ "textarea",
18
+ "[tabindex]",
19
+ "[contenteditable=true]",
20
+ ].join(",")
21
+
22
+ function focusableElements(
23
+ panel: HTMLElement
24
+ ): HTMLElement[] {
25
+ return Array.from(
26
+ panel.querySelectorAll<HTMLElement>(focusableSelector)
27
+ ).filter(
28
+ (element) =>
29
+ element.tabIndex >= 0 &&
30
+ !element.matches(":disabled") &&
31
+ !element.closest("[inert]") &&
32
+ element.getClientRects().length > 0 &&
33
+ getComputedStyle(element).visibility !== "hidden"
34
+ )
35
+ }
36
+
37
+ /** Keeps keyboard focus in the foremost sheet, including its floating pickers. */
38
+ export function useSheetFocus(
39
+ panel: RefObject<HTMLDivElement | null>,
40
+ onClose: () => void
41
+ ) {
42
+ // Capture before children mount, since a child may use autoFocus.
43
+ const [opener] = useState(() =>
44
+ typeof document === "undefined"
45
+ ? null
46
+ : document.activeElement
47
+ )
48
+ const close = useRef(onClose)
49
+ useEffect(() => {
50
+ close.current = onClose
51
+ }, [onClose])
52
+
53
+ useLayoutEffect(() => {
54
+ const surface = panel.current
55
+ if (!surface) return
56
+ sheets.push(surface)
57
+ const foremost = () => sheets.at(-1) === surface
58
+ const focusFirst = () =>
59
+ (focusableElements(surface)[0] ?? surface).focus()
60
+ const inFloatingLayer = (element: Element | null) =>
61
+ ownsFloatingLayer(surface, element)
62
+ const belongsToSheet = (element: Element | null) =>
63
+ surface.contains(element) ||
64
+ inFloatingLayer(element)
65
+
66
+ if (!surface.contains(document.activeElement))
67
+ surface.focus({ preventScroll: true })
68
+ const leaveModalEnvironment =
69
+ enterModalEnvironment(surface)
70
+
71
+ const onFocus = (event: FocusEvent) => {
72
+ if (
73
+ foremost() &&
74
+ !belongsToSheet(
75
+ event.target as Element | null
76
+ )
77
+ )
78
+ focusFirst()
79
+ }
80
+ const onKey = (event: KeyboardEvent) => {
81
+ if (!foremost() || event.defaultPrevented) return
82
+ if (event.key === "Escape") {
83
+ // Floating controls handle their own Escape before the sheet.
84
+ if (inFloatingLayer(document.activeElement))
85
+ return
86
+ event.preventDefault()
87
+ event.stopPropagation()
88
+ close.current()
89
+ }
90
+ if (event.key !== "Tab") return
91
+ const controls = focusableElements(surface)
92
+ const first = controls[0] ?? surface
93
+ const last = controls.at(-1) ?? surface
94
+ const active = document.activeElement
95
+ if (
96
+ event.shiftKey &&
97
+ (active === surface ||
98
+ active === first ||
99
+ !surface.contains(active))
100
+ ) {
101
+ event.preventDefault()
102
+ last.focus()
103
+ } else if (
104
+ !event.shiftKey &&
105
+ (active === last ||
106
+ !surface.contains(active) ||
107
+ active === surface)
108
+ ) {
109
+ event.preventDefault()
110
+ first.focus()
111
+ }
112
+ }
113
+
114
+ document.addEventListener("focusin", onFocus)
115
+ window.addEventListener("keydown", onKey)
116
+ return () => {
117
+ const wasForemost = foremost()
118
+ sheets.splice(sheets.indexOf(surface), 1)
119
+ document.removeEventListener("focusin", onFocus)
120
+ window.removeEventListener("keydown", onKey)
121
+ leaveModalEnvironment()
122
+ if (
123
+ wasForemost &&
124
+ opener instanceof HTMLElement &&
125
+ opener.isConnected
126
+ )
127
+ opener.focus()
128
+ }
129
+ }, [panel, opener])
130
+ }
@@ -0,0 +1,133 @@
1
+ import {
2
+ Archive01Icon,
3
+ Calendar03Icon,
4
+ Delete02Icon,
5
+ Folder01Icon,
6
+ InboxIcon,
7
+ Note01Icon,
8
+ } from "@hugeicons/core-free-icons"
9
+ import {
10
+ HugeiconsIcon,
11
+ type IconSvgElement,
12
+ } from "@hugeicons/react"
13
+ import {
14
+ SidebarItemContent,
15
+ SidebarSection,
16
+ sidebarItemClass,
17
+ } from "@passui/react/sidebar"
18
+ import { useState } from "react"
19
+
20
+ type Row = {
21
+ id: string
22
+ label: string
23
+ icon: IconSvgElement
24
+ count?: number
25
+ }
26
+
27
+ const sections: { title: string; items: Row[] }[] = [
28
+ {
29
+ title: "Library",
30
+ items: [
31
+ {
32
+ id: "notes",
33
+ label: "All notes",
34
+ icon: Note01Icon,
35
+ count: 248,
36
+ },
37
+ {
38
+ id: "today",
39
+ label: "Today",
40
+ icon: Calendar03Icon,
41
+ count: 4,
42
+ },
43
+ {
44
+ id: "inbox",
45
+ label: "Inbox",
46
+ icon: InboxIcon,
47
+ count: 9,
48
+ },
49
+ ],
50
+ },
51
+ {
52
+ title: "Folders",
53
+ items: [
54
+ {
55
+ id: "work",
56
+ label: "Work",
57
+ icon: Folder01Icon,
58
+ count: 61,
59
+ },
60
+ {
61
+ id: "travel",
62
+ label: "Travel",
63
+ icon: Folder01Icon,
64
+ count: 17,
65
+ },
66
+ ],
67
+ },
68
+ {
69
+ title: "More",
70
+ items: [
71
+ {
72
+ id: "archive",
73
+ label: "Archive",
74
+ icon: Archive01Icon,
75
+ },
76
+ {
77
+ id: "trash",
78
+ label: "Recently deleted",
79
+ icon: Delete02Icon,
80
+ },
81
+ ],
82
+ },
83
+ ]
84
+
85
+ export function SidebarGroups() {
86
+ const [active, setActive] = useState("today")
87
+
88
+ return (
89
+ <nav
90
+ aria-label="Notes"
91
+ className="flex w-60 flex-col gap-5 rounded-4xl bg-sidebar p-3"
92
+ >
93
+ {sections.map((section) => (
94
+ <SidebarSection
95
+ key={section.title}
96
+ title={section.title}
97
+ >
98
+ {section.items.map((item) => (
99
+ <a
100
+ key={item.id}
101
+ href={`#${item.id}`}
102
+ aria-current={
103
+ item.id === active
104
+ ? "page"
105
+ : undefined
106
+ }
107
+ className={sidebarItemClass(
108
+ item.id === active
109
+ )}
110
+ onClick={(event) => {
111
+ event.preventDefault()
112
+ setActive(item.id)
113
+ }}
114
+ >
115
+ <SidebarItemContent
116
+ active={item.id === active}
117
+ count={item.count}
118
+ icon={
119
+ <HugeiconsIcon
120
+ icon={item.icon}
121
+ size={18}
122
+ />
123
+ }
124
+ >
125
+ {item.label}
126
+ </SidebarItemContent>
127
+ </a>
128
+ ))}
129
+ </SidebarSection>
130
+ ))}
131
+ </nav>
132
+ )
133
+ }