@passui/react 0.0.0-stage → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -2
  3. package/package.json +96 -5
  4. package/src/appearance/index.ts +2 -0
  5. package/src/appearance/platform.tsx +123 -0
  6. package/src/appearance/theme.tsx +535 -0
  7. package/src/components/ambient/ambient.docs.tsx +68 -0
  8. package/src/components/ambient/ambient.tsx +43 -0
  9. package/src/components/ambient/examples/ambient-images.tsx +30 -0
  10. package/src/components/ambient/examples/ambient-now-playing.tsx +55 -0
  11. package/src/components/ambient/examples/ambient-showcase.tsx +33 -0
  12. package/src/components/ambient/examples/ambient-usage.tsx +19 -0
  13. package/src/components/ambient/index.ts +1 -0
  14. package/src/components/button/button-styles.ts +60 -0
  15. package/src/components/button/button.docs.tsx +116 -0
  16. package/src/components/button/button.tsx +91 -0
  17. package/src/components/button/examples/button-disabled.tsx +19 -0
  18. package/src/components/button/examples/button-icon.tsx +39 -0
  19. package/src/components/button/examples/button-link.tsx +12 -0
  20. package/src/components/button/examples/button-pending.tsx +27 -0
  21. package/src/components/button/examples/button-showcase.tsx +115 -0
  22. package/src/components/button/examples/button-sizes.tsx +12 -0
  23. package/src/components/button/examples/button-usage.tsx +12 -0
  24. package/src/components/button/examples/button-variants.tsx +15 -0
  25. package/src/components/button/examples/button-with-icon.tsx +32 -0
  26. package/src/components/button/examples/icon-button-player.tsx +38 -0
  27. package/src/components/button/examples/icon-button-showcase.tsx +83 -0
  28. package/src/components/button/examples/icon-button-usage.tsx +11 -0
  29. package/src/components/button/examples/icon-button-variants.tsx +39 -0
  30. package/src/components/button/icon-button.docs.tsx +68 -0
  31. package/src/components/button/icon-button.tsx +73 -0
  32. package/src/components/button/pending-spinner.tsx +14 -0
  33. package/src/components/button/use-pending-indicator.ts +27 -0
  34. package/src/components/chip/chip.docs.tsx +37 -0
  35. package/src/components/chip/chip.tsx +35 -0
  36. package/src/components/chip/examples/chip-in-rows.tsx +19 -0
  37. package/src/components/chip/examples/chip-showcase.tsx +51 -0
  38. package/src/components/chip/examples/chip-tones.tsx +13 -0
  39. package/src/components/chip/examples/chip-usage.tsx +5 -0
  40. package/src/components/compact-date-picker/calendar-month.ts +131 -0
  41. package/src/components/compact-date-picker/compact-date-picker.docs.tsx +81 -0
  42. package/src/components/compact-date-picker/compact-date-picker.tsx +438 -0
  43. package/src/components/compact-date-picker/examples/compact-date-picker-showcase.tsx +35 -0
  44. package/src/components/compact-date-picker/examples/compact-date-picker-usage.tsx +22 -0
  45. package/src/components/compact-time-picker/compact-time-picker.docs.tsx +80 -0
  46. package/src/components/compact-time-picker/compact-time-picker.tsx +432 -0
  47. package/src/components/compact-time-picker/examples/compact-time-picker-showcase.tsx +31 -0
  48. package/src/components/compact-time-picker/examples/compact-time-picker-usage.tsx +21 -0
  49. package/src/components/compact-time-picker/time-value.ts +67 -0
  50. package/src/components/content-view/content-view.docs.tsx +61 -0
  51. package/src/components/content-view/content-view.tsx +197 -0
  52. package/src/components/content-view/examples/content-view-header-action.tsx +33 -0
  53. package/src/components/content-view/examples/content-view-showcase.tsx +65 -0
  54. package/src/components/content-view/examples/content-view-stats.tsx +35 -0
  55. package/src/components/content-view/examples/content-view-usage.tsx +14 -0
  56. package/src/components/content-view/index.ts +1 -0
  57. package/src/components/context-menu/conceal-menu-source.ts +33 -0
  58. package/src/components/context-menu/context-menu-host.tsx +445 -0
  59. package/src/components/context-menu/context-menu-store.ts +165 -0
  60. package/src/components/context-menu/context-menu.docs.tsx +92 -0
  61. package/src/components/context-menu/examples/context-menu-checked.tsx +37 -0
  62. package/src/components/context-menu/examples/context-menu-list.tsx +85 -0
  63. package/src/components/context-menu/examples/context-menu-showcase.tsx +56 -0
  64. package/src/components/context-menu/examples/context-menu-toolbar.tsx +63 -0
  65. package/src/components/context-menu/examples/context-menu-usage.tsx +24 -0
  66. package/src/components/context-menu/menu-position.ts +55 -0
  67. package/src/components/context-menu/menu-reveal.ts +75 -0
  68. package/src/components/context-menu/use-context-menu.ts +90 -0
  69. package/src/components/conversation/conversation.docs.tsx +88 -0
  70. package/src/components/conversation/conversation.tsx +279 -0
  71. package/src/components/conversation/examples/conversation-composer.tsx +36 -0
  72. package/src/components/conversation/examples/conversation-days.tsx +48 -0
  73. package/src/components/conversation/examples/conversation-showcase.tsx +71 -0
  74. package/src/components/conversation/examples/conversation-usage.tsx +28 -0
  75. package/src/components/conversation/index.ts +1 -0
  76. package/src/components/date-picker/date-picker.docs.tsx +88 -0
  77. package/src/components/date-picker/date-picker.tsx +103 -0
  78. package/src/components/date-picker/date-value.ts +37 -0
  79. package/src/components/date-picker/examples/date-picker-locales.tsx +41 -0
  80. package/src/components/date-picker/examples/date-picker-past-only.tsx +29 -0
  81. package/src/components/date-picker/examples/date-picker-showcase.tsx +49 -0
  82. package/src/components/date-picker/examples/date-picker-usage.tsx +22 -0
  83. package/src/components/dialog/dialog-host.tsx +156 -0
  84. package/src/components/dialog/dialog-store.ts +56 -0
  85. package/src/components/dialog/dialog.docs.tsx +114 -0
  86. package/src/components/dialog/dialog.tsx +200 -0
  87. package/src/components/dialog/examples/dialog-confirm.tsx +24 -0
  88. package/src/components/dialog/examples/dialog-form.tsx +46 -0
  89. package/src/components/dialog/examples/dialog-inline.tsx +48 -0
  90. package/src/components/dialog/examples/dialog-showcase.tsx +53 -0
  91. package/src/components/dialog/examples/dialog-usage.tsx +31 -0
  92. package/src/components/dialog/index.ts +1 -0
  93. package/src/components/labeled-content/examples/labeled-content-descriptions.tsx +37 -0
  94. package/src/components/labeled-content/examples/labeled-content-showcase.tsx +54 -0
  95. package/src/components/labeled-content/examples/labeled-content-usage.tsx +22 -0
  96. package/src/components/labeled-content/examples/labeled-content-values.tsx +26 -0
  97. package/src/components/labeled-content/examples/labeled-content-wide.tsx +46 -0
  98. package/src/components/labeled-content/labeled-content.docs.tsx +69 -0
  99. package/src/components/labeled-content/labeled-content.tsx +66 -0
  100. package/src/components/locale-switcher/examples/locale-switcher-native-names.tsx +30 -0
  101. package/src/components/locale-switcher/examples/locale-switcher-showcase.tsx +46 -0
  102. package/src/components/locale-switcher/examples/locale-switcher-translated-page.tsx +35 -0
  103. package/src/components/locale-switcher/examples/locale-switcher-usage.tsx +20 -0
  104. package/src/components/locale-switcher/index.ts +1 -0
  105. package/src/components/locale-switcher/locale-switcher.docs.tsx +91 -0
  106. package/src/components/locale-switcher/locale-switcher.tsx +252 -0
  107. package/src/components/picker/examples/picker-details.tsx +44 -0
  108. package/src/components/picker/examples/picker-searchable.tsx +48 -0
  109. package/src/components/picker/examples/picker-showcase.tsx +66 -0
  110. package/src/components/picker/examples/picker-usage.tsx +20 -0
  111. package/src/components/picker/picker.docs.tsx +68 -0
  112. package/src/components/picker/picker.tsx +493 -0
  113. package/src/components/popover/examples/popover-help.tsx +57 -0
  114. package/src/components/popover/examples/popover-menu.tsx +51 -0
  115. package/src/components/popover/examples/popover-showcase.tsx +61 -0
  116. package/src/components/popover/examples/popover-usage.tsx +31 -0
  117. package/src/components/popover/popover.docs.tsx +84 -0
  118. package/src/components/popover/popover.tsx +138 -0
  119. package/src/components/profile-image/examples/profile-image-fallbacks.tsx +29 -0
  120. package/src/components/profile-image/examples/profile-image-group.tsx +34 -0
  121. package/src/components/profile-image/examples/profile-image-showcase.tsx +30 -0
  122. package/src/components/profile-image/examples/profile-image-usage.tsx +14 -0
  123. package/src/components/profile-image/profile-image.docs.tsx +81 -0
  124. package/src/components/profile-image/profile-image.tsx +80 -0
  125. package/src/components/search-field/examples/search-field-filter.tsx +53 -0
  126. package/src/components/search-field/examples/search-field-showcase.tsx +56 -0
  127. package/src/components/search-field/examples/search-field-toolbar.tsx +27 -0
  128. package/src/components/search-field/examples/search-field-usage.tsx +18 -0
  129. package/src/components/search-field/search-field.docs.tsx +58 -0
  130. package/src/components/search-field/search-field.tsx +29 -0
  131. package/src/components/search-list/examples/search-list-dropdown.tsx +53 -0
  132. package/src/components/search-list/examples/search-list-groups.tsx +64 -0
  133. package/src/components/search-list/examples/search-list-showcase.tsx +67 -0
  134. package/src/components/search-list/examples/search-list-usage.tsx +27 -0
  135. package/src/components/search-list/search-list.docs.tsx +87 -0
  136. package/src/components/search-list/search-list.tsx +265 -0
  137. package/src/components/section/examples/section-actions.tsx +35 -0
  138. package/src/components/section/examples/section-groups.tsx +41 -0
  139. package/src/components/section/examples/section-showcase.tsx +63 -0
  140. package/src/components/section/examples/section-usage.tsx +19 -0
  141. package/src/components/section/section.docs.tsx +51 -0
  142. package/src/components/section/section.tsx +41 -0
  143. package/src/components/secure-field/examples/secure-field-new-password.tsx +56 -0
  144. package/src/components/secure-field/examples/secure-field-reveal.tsx +20 -0
  145. package/src/components/secure-field/examples/secure-field-showcase.tsx +62 -0
  146. package/src/components/secure-field/examples/secure-field-usage.tsx +19 -0
  147. package/src/components/secure-field/secure-field.docs.tsx +72 -0
  148. package/src/components/secure-field/secure-field.tsx +10 -0
  149. package/src/components/segmented-control/examples/segmented-control-overflow.tsx +28 -0
  150. package/src/components/segmented-control/examples/segmented-control-showcase.tsx +59 -0
  151. package/src/components/segmented-control/examples/segmented-control-usage.tsx +20 -0
  152. package/src/components/segmented-control/examples/segmented-control-views.tsx +32 -0
  153. package/src/components/segmented-control/segmented-control.docs.tsx +61 -0
  154. package/src/components/segmented-control/segmented-control.tsx +122 -0
  155. package/src/components/sheet/examples/sheet-choices.tsx +58 -0
  156. package/src/components/sheet/examples/sheet-share.tsx +51 -0
  157. package/src/components/sheet/examples/sheet-showcase.tsx +74 -0
  158. package/src/components/sheet/examples/sheet-usage.tsx +32 -0
  159. package/src/components/sheet/modal-environment.ts +113 -0
  160. package/src/components/sheet/sheet-portal.tsx +89 -0
  161. package/src/components/sheet/sheet.docs.tsx +75 -0
  162. package/src/components/sheet/sheet.tsx +112 -0
  163. package/src/components/sheet/use-sheet-focus.ts +130 -0
  164. package/src/components/sidebar/examples/sidebar-groups.tsx +133 -0
  165. package/src/components/sidebar/examples/sidebar-showcase.tsx +123 -0
  166. package/src/components/sidebar/examples/sidebar-unread.tsx +66 -0
  167. package/src/components/sidebar/examples/sidebar-usage.tsx +52 -0
  168. package/src/components/sidebar/sidebar.docs.tsx +135 -0
  169. package/src/components/sidebar/sidebar.tsx +69 -0
  170. package/src/components/stat-tile/examples/stat-tile-filters.tsx +32 -0
  171. package/src/components/stat-tile/examples/stat-tile-grid.tsx +21 -0
  172. package/src/components/stat-tile/examples/stat-tile-showcase.tsx +52 -0
  173. package/src/components/stat-tile/examples/stat-tile-usage.tsx +13 -0
  174. package/src/components/stat-tile/index.ts +1 -0
  175. package/src/components/stat-tile/stat-tile.docs.tsx +60 -0
  176. package/src/components/stat-tile/stat-tile.tsx +68 -0
  177. package/src/components/status-indicator/examples/status-indicator-live.tsx +34 -0
  178. package/src/components/status-indicator/examples/status-indicator-showcase.tsx +39 -0
  179. package/src/components/status-indicator/examples/status-indicator-states.tsx +17 -0
  180. package/src/components/status-indicator/examples/status-indicator-usage.tsx +9 -0
  181. package/src/components/status-indicator/status-indicator.docs.tsx +43 -0
  182. package/src/components/status-indicator/status-indicator.tsx +35 -0
  183. package/src/components/swatch-picker/examples/swatch-picker-accents.tsx +42 -0
  184. package/src/components/swatch-picker/examples/swatch-picker-labels.tsx +45 -0
  185. package/src/components/swatch-picker/examples/swatch-picker-showcase.tsx +70 -0
  186. package/src/components/swatch-picker/examples/swatch-picker-usage.tsx +39 -0
  187. package/src/components/swatch-picker/swatch-picker.docs.tsx +60 -0
  188. package/src/components/swatch-picker/swatch-picker.tsx +96 -0
  189. package/src/components/tab-bar/examples/tab-bar-five-tabs.tsx +66 -0
  190. package/src/components/tab-bar/examples/tab-bar-minimize.tsx +71 -0
  191. package/src/components/tab-bar/examples/tab-bar-showcase.tsx +82 -0
  192. package/src/components/tab-bar/examples/tab-bar-usage.tsx +54 -0
  193. package/src/components/tab-bar/tab-bar.docs.tsx +136 -0
  194. package/src/components/tab-bar/tab-bar.tsx +148 -0
  195. package/src/components/tab-view/examples/tab-view-keyboard.tsx +39 -0
  196. package/src/components/tab-view/examples/tab-view-settings.tsx +56 -0
  197. package/src/components/tab-view/examples/tab-view-showcase.tsx +53 -0
  198. package/src/components/tab-view/examples/tab-view-usage.tsx +23 -0
  199. package/src/components/tab-view/tab-view.docs.tsx +77 -0
  200. package/src/components/tab-view/tab-view.tsx +130 -0
  201. package/src/components/table/examples/table-empty.tsx +53 -0
  202. package/src/components/table/examples/table-invoices.tsx +102 -0
  203. package/src/components/table/examples/table-selection.tsx +150 -0
  204. package/src/components/table/examples/table-showcase.tsx +115 -0
  205. package/src/components/table/examples/table-usage.tsx +67 -0
  206. package/src/components/table/examples/table-wide.tsx +75 -0
  207. package/src/components/table/table.docs.tsx +131 -0
  208. package/src/components/table/table.tsx +149 -0
  209. package/src/components/text-editor/examples/text-editor-comment.tsx +48 -0
  210. package/src/components/text-editor/examples/text-editor-count.tsx +38 -0
  211. package/src/components/text-editor/examples/text-editor-showcase.tsx +58 -0
  212. package/src/components/text-editor/examples/text-editor-usage.tsx +25 -0
  213. package/src/components/text-editor/text-editor.docs.tsx +61 -0
  214. package/src/components/text-editor/text-editor.tsx +19 -0
  215. package/src/components/text-field/examples/text-field-icons.tsx +72 -0
  216. package/src/components/text-field/examples/text-field-showcase.tsx +50 -0
  217. package/src/components/text-field/examples/text-field-usage.tsx +18 -0
  218. package/src/components/text-field/examples/text-field-validation.tsx +27 -0
  219. package/src/components/text-field/text-field.docs.tsx +118 -0
  220. package/src/components/text-field/text-field.tsx +135 -0
  221. package/src/components/toast/examples/toast-action.tsx +32 -0
  222. package/src/components/toast/examples/toast-promise.tsx +45 -0
  223. package/src/components/toast/examples/toast-showcase.tsx +49 -0
  224. package/src/components/toast/examples/toast-usage.tsx +13 -0
  225. package/src/components/toast/toast-store.ts +166 -0
  226. package/src/components/toast/toast.docs.tsx +79 -0
  227. package/src/components/toast/toaster.tsx +374 -0
  228. package/src/components/toggle/examples/toggle-checkboxes.tsx +43 -0
  229. package/src/components/toggle/examples/toggle-settings.tsx +31 -0
  230. package/src/components/toggle/examples/toggle-showcase.tsx +59 -0
  231. package/src/components/toggle/examples/toggle-usage.tsx +13 -0
  232. package/src/components/toggle/toggle.docs.tsx +69 -0
  233. package/src/components/toggle/toggle.tsx +85 -0
  234. package/src/components/tooltip/examples/tooltip-icon-buttons.tsx +27 -0
  235. package/src/components/tooltip/examples/tooltip-shortcuts.tsx +38 -0
  236. package/src/components/tooltip/examples/tooltip-showcase.tsx +44 -0
  237. package/src/components/tooltip/examples/tooltip-usage.tsx +13 -0
  238. package/src/components/tooltip/tooltip-host.tsx +156 -0
  239. package/src/components/tooltip/tooltip.docs.tsx +56 -0
  240. package/src/components/view-controller/examples/view-controller-action.tsx +27 -0
  241. package/src/components/view-controller/examples/view-controller-settings.tsx +31 -0
  242. package/src/components/view-controller/examples/view-controller-showcase.tsx +45 -0
  243. package/src/components/view-controller/examples/view-controller-usage.tsx +19 -0
  244. package/src/components/view-controller/index.ts +1 -0
  245. package/src/components/view-controller/view-controller.docs.tsx +71 -0
  246. package/src/components/view-controller/view-controller.tsx +81 -0
  247. package/src/docs/index.ts +3 -0
  248. package/src/docs/registry.ts +113 -0
  249. package/src/docs/snippet.ts +17 -0
  250. package/src/docs/types.ts +180 -0
  251. package/src/hooks/scroll/minimize-decision.ts +13 -0
  252. package/src/hooks/use-anchored-placement.ts +43 -0
  253. package/src/hooks/use-dismiss.ts +58 -0
  254. package/src/hooks/use-media-query.ts +106 -0
  255. package/src/hooks/use-minimize-on-scroll.ts +92 -0
  256. package/src/hooks/use-visual-viewport.ts +76 -0
  257. package/src/hooks/viewport/types.ts +19 -0
  258. package/src/hooks/viewport/viewport-metrics.ts +25 -0
  259. package/src/icons/icons.tsx +298 -0
  260. package/src/index.ts +78 -0
  261. package/src/lib/anchored-placement.ts +92 -0
  262. package/src/lib/class-names.ts +21 -0
  263. package/src/lib/date.ts +96 -0
  264. package/src/lib/floating-layers.ts +28 -0
  265. package/src/lib/haptics.ts +25 -0
  266. package/src/lib/language.ts +21 -0
  267. package/src/lib/screen-edges.ts +45 -0
  268. package/src/lib/shapes.ts +70 -0
  269. package/src/motion/transitions.ts +30 -0
  270. package/src/styles/base.css +1 -0
  271. package/src/styles/styles.css +3 -0
  272. package/src/styles/theme.css +1 -0
  273. package/src/styles/typeset.css +1 -0
  274. package/src/styles/utilities.css +1 -0
  275. package/src/tokens.ts +1 -0
@@ -0,0 +1,92 @@
1
+ import { screenEdges } from "./screen-edges"
2
+
3
+ export interface AnchoredPlacement {
4
+ left: number
5
+ width: number
6
+ top?: number
7
+ bottom?: number
8
+ /** Opens upward, so motion should grow from the bottom edge. */
9
+ above: boolean
10
+ /**
11
+ * Set when the chosen side has less room than the panel needs: the
12
+ * panel scrolls inside this height instead of crossing the edge.
13
+ */
14
+ maxHeight?: number
15
+ }
16
+
17
+ /** The measurements placement decides on, so the math stays testable. */
18
+ export interface PlacementFrame {
19
+ anchor: { top: number; bottom: number; left: number }
20
+ viewport: { width: number; height: number }
21
+ edges: {
22
+ top: number
23
+ right: number
24
+ bottom: number
25
+ left: number
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Places a panel of `width` under its anchor, or above it when there's
31
+ * less than `needed` height below and more room above, kept inside the
32
+ * screen's safe edges.
33
+ */
34
+ export function placeInViewport(
35
+ { anchor, viewport, edges }: PlacementFrame,
36
+ width: number,
37
+ needed: number,
38
+ gap = 6
39
+ ): AnchoredPlacement {
40
+ const fitted = Math.min(
41
+ width,
42
+ viewport.width - edges.left - edges.right
43
+ )
44
+ const left = Math.min(
45
+ Math.max(edges.left, anchor.left),
46
+ viewport.width - fitted - edges.right
47
+ )
48
+ const roomBelow =
49
+ viewport.height - anchor.bottom - gap - edges.bottom
50
+ const roomAbove = anchor.top - gap - edges.top
51
+ const above = roomBelow < needed && roomAbove > roomBelow
52
+ const room = above ? roomAbove : roomBelow
53
+ const placement: AnchoredPlacement = {
54
+ left,
55
+ width: fitted,
56
+ above,
57
+ ...(above
58
+ ? { bottom: viewport.height - anchor.top + gap }
59
+ : { top: anchor.bottom + gap }),
60
+ }
61
+ if (room < needed) placement.maxHeight = room
62
+ return placement
63
+ }
64
+
65
+ /** `placeInViewport` from live DOM measurements. */
66
+ export function placeAnchored(
67
+ anchor: HTMLElement,
68
+ width: number,
69
+ needed: number,
70
+ gap = 6
71
+ ): AnchoredPlacement {
72
+ const rect = anchor.getBoundingClientRect()
73
+ const viewport = window.visualViewport
74
+ return placeInViewport(
75
+ {
76
+ anchor: {
77
+ top: rect.top,
78
+ bottom: rect.bottom,
79
+ left: rect.left,
80
+ },
81
+ viewport: {
82
+ width: viewport?.width ?? window.innerWidth,
83
+ height:
84
+ viewport?.height ?? window.innerHeight,
85
+ },
86
+ edges: screenEdges(),
87
+ },
88
+ width,
89
+ needed,
90
+ gap
91
+ )
92
+ }
@@ -0,0 +1,21 @@
1
+ import { clsx } from "clsx"
2
+ import type { ClassValue } from "clsx"
3
+ import { extendTailwindMerge } from "tailwind-merge"
4
+ import { textStyles } from "@passui/tokens/typography"
5
+
6
+ const textStyleClasses = Object.keys(textStyles).map(
7
+ (name) =>
8
+ `text-${name.replace(/[A-Z]/g, (character) => `-${character.toLowerCase()}`)}`
9
+ )
10
+
11
+ const twMerge = extendTailwindMerge({
12
+ extend: {
13
+ classGroups: {
14
+ "font-size": textStyleClasses,
15
+ },
16
+ },
17
+ })
18
+
19
+ export function cn(...inputs: ClassValue[]) {
20
+ return twMerge(clsx(inputs))
21
+ }
@@ -0,0 +1,96 @@
1
+ // Dates through the platform's own Intl, no date library in the bundle.
2
+ // en-US on purpose: the server renders first, so a viewer-specific locale
3
+ // would make server and browser disagree on hydration.
4
+ const LOCALE = "en-US"
5
+
6
+ const PRESETS = {
7
+ /** Aug 30, 2026 */
8
+ PP: { dateStyle: "medium" },
9
+ /** August 30, 2026 */
10
+ PPP: { dateStyle: "long" },
11
+ /** Aug 30, 2026, 2:15 PM */
12
+ PPp: { dateStyle: "medium", timeStyle: "short" },
13
+ /** August 30, 2026 at 2:15 PM */
14
+ PPPp: { dateStyle: "long", timeStyle: "short" },
15
+ /** Aug 30 */
16
+ "MMM d": { month: "short", day: "numeric" },
17
+ /** Aug 30, 14:15 */
18
+ "MMM d, HH:mm": {
19
+ month: "short",
20
+ day: "numeric",
21
+ hour: "2-digit",
22
+ minute: "2-digit",
23
+ hourCycle: "h23",
24
+ },
25
+ /** Aug 30, 14:15:09 */
26
+ "MMM d, HH:mm:ss": {
27
+ month: "short",
28
+ day: "numeric",
29
+ hour: "2-digit",
30
+ minute: "2-digit",
31
+ second: "2-digit",
32
+ hourCycle: "h23",
33
+ },
34
+ } satisfies Record<string, Intl.DateTimeFormatOptions>
35
+
36
+ export type DatePreset = keyof typeof PRESETS
37
+
38
+ const formatters = new Map<string, Intl.DateTimeFormat>()
39
+
40
+ function formatter(
41
+ key: string,
42
+ options: Intl.DateTimeFormatOptions
43
+ ) {
44
+ let cached = formatters.get(key)
45
+ if (!cached) {
46
+ cached = new Intl.DateTimeFormat(LOCALE, options)
47
+ formatters.set(key, cached)
48
+ }
49
+ return cached
50
+ }
51
+
52
+ /** A stored UTC timestamp shown as its UTC wall-clock time, whatever the viewer's timezone. */
53
+ export function formatUtc(
54
+ date: Date | string | number,
55
+ preset: DatePreset
56
+ ) {
57
+ return formatter(`utc:${preset}`, {
58
+ ...PRESETS[preset],
59
+ timeZone: "UTC",
60
+ }).format(new Date(date))
61
+ }
62
+
63
+ /** The viewer's local time of day, like 2:15 PM. */
64
+ export function formatTime(date: Date | string | number) {
65
+ return formatter("local:time", {
66
+ timeStyle: "short",
67
+ }).format(new Date(date))
68
+ }
69
+
70
+ const UNITS: [Intl.RelativeTimeFormatUnit, number][] = [
71
+ ["year", 31_536_000],
72
+ ["month", 2_592_000],
73
+ ["week", 604_800],
74
+ ["day", 86_400],
75
+ ["hour", 3_600],
76
+ ["minute", 60],
77
+ ]
78
+
79
+ let relative: Intl.RelativeTimeFormat | undefined
80
+
81
+ /** "5 minutes ago", "in 30 days": the largest whole unit that fits. */
82
+ export function fromNow(date: Date | string | number) {
83
+ relative ??= new Intl.RelativeTimeFormat(LOCALE, {
84
+ numeric: "auto",
85
+ })
86
+ const seconds =
87
+ (new Date(date).getTime() - Date.now()) / 1000
88
+ for (const [unit, size] of UNITS) {
89
+ if (Math.abs(seconds) >= size)
90
+ return relative.format(
91
+ Math.round(seconds / size),
92
+ unit
93
+ )
94
+ }
95
+ return relative.format(0, "second")
96
+ }
@@ -0,0 +1,28 @@
1
+ export const FLOATING_LAYER = "data-floating-layer"
2
+
3
+ const anchors = new WeakMap<Element, Element>()
4
+
5
+ /** Portals retain the ownership of the control that opened them. */
6
+ export function registerFloatingLayer(
7
+ layer: Element,
8
+ anchor: Element
9
+ ): () => void {
10
+ anchors.set(layer, anchor)
11
+ return () => anchors.delete(layer)
12
+ }
13
+
14
+ export function ownsFloatingLayer(
15
+ container: Element,
16
+ target: Element | null
17
+ ): boolean {
18
+ let layer = target?.closest(`[${FLOATING_LAYER}]`)
19
+ const visited = new Set<Element>()
20
+ while (layer && !visited.has(layer)) {
21
+ visited.add(layer)
22
+ const anchor = anchors.get(layer)
23
+ if (!anchor) return false
24
+ if (container.contains(anchor)) return true
25
+ layer = anchor.closest(`[${FLOATING_LAYER}]`)
26
+ }
27
+ return false
28
+ }
@@ -0,0 +1,25 @@
1
+ /** The feedback kinds the systems share, from a selection tick to an error. */
2
+ export type HapticKind =
3
+ | "selection"
4
+ | "light"
5
+ | "medium"
6
+ | "heavy"
7
+ | "success"
8
+ | "warning"
9
+ | "error"
10
+
11
+ let player: (kind: HapticKind) => void = () => {}
12
+
13
+ /**
14
+ * Components only ask for feedback; the app decides how it plays on this
15
+ * device (system haptics, a vibration, or nothing) and whether it's on.
16
+ */
17
+ export function setHapticPlayer(
18
+ next: (kind: HapticKind) => void
19
+ ): void {
20
+ player = next
21
+ }
22
+
23
+ export function haptic(kind: HapticKind): void {
24
+ player(kind)
25
+ }
@@ -0,0 +1,21 @@
1
+ /** A language's name in itself, capitalized: "Français", "中文". */
2
+ export function getNativeLanguageName(tag: string) {
3
+ const name = new Intl.DisplayNames([tag], {
4
+ type: "language",
5
+ }).of(tag)
6
+ return name
7
+ ? name.charAt(0).toUpperCase() + name.slice(1)
8
+ : tag
9
+ }
10
+
11
+ /** A language's name as read in `displayLocale`: "French" in English. */
12
+ export function getLocalizedLanguageName(
13
+ tag: string,
14
+ displayLocale: string
15
+ ) {
16
+ return (
17
+ new Intl.DisplayNames([displayLocale], {
18
+ type: "language",
19
+ }).of(tag) ?? tag
20
+ )
21
+ }
@@ -0,0 +1,45 @@
1
+ export interface ScreenEdges {
2
+ top: number
3
+ right: number
4
+ bottom: number
5
+ left: number
6
+ }
7
+
8
+ let probe: HTMLDivElement | undefined
9
+
10
+ /**
11
+ * How far menus and popovers keep from each side of the screen: clear of
12
+ * the status bar, gesture bar and notch, and on touch screens a little
13
+ * further from the glass, as the systems' own menus are.
14
+ */
15
+ export function screenEdges(): ScreenEdges {
16
+ const margin =
17
+ typeof window !== "undefined" &&
18
+ window.matchMedia("(pointer: coarse)").matches
19
+ ? 12
20
+ : 8
21
+ if (typeof document === "undefined") {
22
+ return {
23
+ top: margin,
24
+ right: margin,
25
+ bottom: margin,
26
+ left: margin,
27
+ }
28
+ }
29
+ if (!probe) {
30
+ probe = document.createElement("div")
31
+ probe.setAttribute("aria-hidden", "true")
32
+ probe.style.cssText =
33
+ "position:fixed;inset:0;visibility:hidden;pointer-events:none;" +
34
+ "padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)"
35
+ }
36
+ if (!probe.isConnected) document.body.appendChild(probe)
37
+ const style = getComputedStyle(probe)
38
+ return {
39
+ top: Number.parseFloat(style.paddingTop) + margin,
40
+ right: Number.parseFloat(style.paddingRight) + margin,
41
+ bottom:
42
+ Number.parseFloat(style.paddingBottom) + margin,
43
+ left: Number.parseFloat(style.paddingLeft) + margin,
44
+ }
45
+ }
@@ -0,0 +1,70 @@
1
+ /** Material 3 expressive outlines: a circle and the scalloped "cookie", "clover", and "sunny" shapes. */
2
+ export const shapes = [
3
+ "circle",
4
+ "cookie4",
5
+ "cookie6",
6
+ "cookie9",
7
+ "clover",
8
+ "sunny",
9
+ /** Reserved for the Pherus mark itself, so it never matches a project's icon. */
10
+ "cookie12",
11
+ ] as const
12
+
13
+ export type Shape = (typeof shapes)[number]
14
+
15
+ /** Lobe count and wave depth (as a fraction of the radius) for each wavy shape. */
16
+ const waves: Record<
17
+ Exclude<Shape, "circle">,
18
+ { lobes: number; depth: number }
19
+ > = {
20
+ cookie4: { lobes: 4, depth: 0.08 },
21
+ cookie6: { lobes: 6, depth: 0.07 },
22
+ cookie9: { lobes: 9, depth: 0.055 },
23
+ clover: { lobes: 4, depth: 0.16 },
24
+ sunny: { lobes: 8, depth: 0.045 },
25
+ cookie12: { lobes: 12, depth: 0.04 },
26
+ }
27
+
28
+ const STEPS = 144
29
+
30
+ /**
31
+ * An SVG path for a Material 3 expressive shape centred on (cx, cy): a circle
32
+ * whose radius rides a cosine wave, which gives the soft scalloped "cookie",
33
+ * "clover", and "sunny" outlines. The wave is normalised so every shape fills
34
+ * the same `r`, keeping icons optically the same size.
35
+ */
36
+ export function shapePath(
37
+ shape: Shape,
38
+ cx: number,
39
+ cy: number,
40
+ r: number
41
+ ) {
42
+ if (shape === "circle") {
43
+ return `M ${cx - r} ${cy} a ${r} ${r} 0 1 0 ${r * 2} 0 a ${r} ${r} 0 1 0 ${-r * 2} 0 Z`
44
+ }
45
+
46
+ const { lobes, depth } = waves[shape]
47
+ const base = r / (1 + depth)
48
+ const points: string[] = []
49
+ for (let i = 0; i < STEPS; i++) {
50
+ const theta = (i / STEPS) * Math.PI * 2 - Math.PI / 2
51
+ const radius =
52
+ base *
53
+ (1 +
54
+ depth *
55
+ Math.cos(lobes * (theta + Math.PI / 2)))
56
+ const x = cx + radius * Math.cos(theta)
57
+ const y = cy + radius * Math.sin(theta)
58
+ points.push(
59
+ `${i === 0 ? "M" : "L"} ${x.toFixed(2)} ${y.toFixed(2)}`
60
+ )
61
+ }
62
+ return `${points.join(" ")} Z`
63
+ }
64
+
65
+ /**
66
+ * The app wide `dualTone` rule dims every svg path for two tone icons; a
67
+ * drawn shape or illustration opts out with this so its colors stay true.
68
+ */
69
+ export const plainSvg =
70
+ "cursor-[inherit] [&_path]:opacity-100 [&_path]:transform-none"
@@ -0,0 +1,30 @@
1
+ import { springs } from "@passui/tokens/motion"
2
+ import type { Transition } from "motion/react"
3
+
4
+ /** Design-token springs as motion/react transitions. */
5
+ export const spring = {
6
+ smooth: { type: "spring", ...springs.smooth },
7
+ snappy: { type: "spring", ...springs.snappy },
8
+ bouncy: { type: "spring", ...springs.bouncy },
9
+ } as const satisfies Record<keyof typeof springs, Transition>
10
+
11
+ /** Menus, popovers and floating toolbars: grow from their anchor. */
12
+ export const popoverMotion = {
13
+ initial: { opacity: 0, scale: 0.94, y: -6 },
14
+ animate: { opacity: 1, scale: 1, y: 0 },
15
+ exit: {
16
+ opacity: 0,
17
+ scale: 0.97,
18
+ y: -4,
19
+ transition: { duration: 0.12 },
20
+ },
21
+ transition: spring.snappy,
22
+ } as const
23
+
24
+ /** Floating bars that rise from the bottom edge. */
25
+ export const riseMotion = {
26
+ initial: { opacity: 0, y: 32 },
27
+ animate: { opacity: 1, y: 0 },
28
+ exit: { opacity: 0, y: 24 },
29
+ transition: spring.smooth,
30
+ } as const
@@ -0,0 +1 @@
1
+ @import "@passui/tokens/base.css";
@@ -0,0 +1,3 @@
1
+ @import "@passui/tokens/styles.css";
2
+
3
+ @source "../";
@@ -0,0 +1 @@
1
+ @import "@passui/tokens/theme.css";
@@ -0,0 +1 @@
1
+ @import "@passui/tokens/typeset.css";
@@ -0,0 +1 @@
1
+ @import "@passui/tokens/utilities.css";
package/src/tokens.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "@passui/tokens"