@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,37 @@
1
+ import { useContextMenu } from "@passui/react"
2
+ import { useState } from "react"
3
+
4
+ const orders = {
5
+ recent: "Recently edited",
6
+ title: "Title",
7
+ created: "Date created",
8
+ }
9
+
10
+ type Order = keyof typeof orders
11
+
12
+ export function ContextMenuChecked() {
13
+ const [order, setOrder] = useState<Order>("recent")
14
+ const menu = useContextMenu(() => [
15
+ { heading: "Sort by" },
16
+ ...(Object.keys(orders) as Order[]).map((key) => ({
17
+ label: orders[key],
18
+ checked: key === order,
19
+ onSelect: () => setOrder(key),
20
+ })),
21
+ ])
22
+
23
+ return (
24
+ <div
25
+ {...menu}
26
+ className="flex w-64 select-none flex-col items-center gap-1 rounded-4xl bg-elevated p-6 text-center shadow-sm ring-1 ring-separator/60"
27
+ >
28
+ <span className="text-subheadline font-medium text-label">
29
+ Notes
30
+ </span>
31
+ <span className="text-footnote text-label-secondary">
32
+ Sorted by {orders[order].toLowerCase()}.
33
+ Right-click to change.
34
+ </span>
35
+ </div>
36
+ )
37
+ }
@@ -0,0 +1,85 @@
1
+ import { toast, useContextMenu } from "@passui/react"
2
+ import { useState } from "react"
3
+
4
+ type Note = { id: string; title: string; pinned: boolean }
5
+
6
+ function NoteRow({
7
+ note,
8
+ onPin,
9
+ onDelete,
10
+ }: {
11
+ note: Note
12
+ onPin: () => void
13
+ onDelete: () => void
14
+ }) {
15
+ const menu = useContextMenu(() => [
16
+ { heading: note.title },
17
+ {
18
+ label: note.pinned ? "Unpin" : "Pin",
19
+ onSelect: onPin,
20
+ },
21
+ {
22
+ label: "Duplicate",
23
+ onSelect: () => toast(`Duplicated ${note.title}`),
24
+ },
25
+ "divider",
26
+ {
27
+ label: "Delete",
28
+ destructive: true,
29
+ onSelect: onDelete,
30
+ },
31
+ ])
32
+
33
+ return (
34
+ <li
35
+ {...menu}
36
+ className="flex select-none items-center justify-between rounded-2xl px-3 py-2.5 text-subheadline text-label hover:bg-fill/60"
37
+ >
38
+ {note.title}
39
+ {note.pinned && (
40
+ <span className="text-caption text-accent-text">
41
+ Pinned
42
+ </span>
43
+ )}
44
+ </li>
45
+ )
46
+ }
47
+
48
+ export function ContextMenuList() {
49
+ const [notes, setNotes] = useState<Note[]>([
50
+ { id: "1", title: "Trip to Kyoto", pinned: true },
51
+ { id: "2", title: "Groceries", pinned: false },
52
+ { id: "3", title: "Book club picks", pinned: false },
53
+ ])
54
+
55
+ return (
56
+ <ul className="m-0 flex w-full max-w-xs list-none flex-col p-0">
57
+ {notes.map((note) => (
58
+ <NoteRow
59
+ key={note.id}
60
+ note={note}
61
+ onPin={() =>
62
+ setNotes((current) =>
63
+ current.map((item) =>
64
+ item.id === note.id
65
+ ? {
66
+ ...item,
67
+ pinned:
68
+ !item.pinned,
69
+ }
70
+ : item
71
+ )
72
+ )
73
+ }
74
+ onDelete={() =>
75
+ setNotes((current) =>
76
+ current.filter(
77
+ (item) => item.id !== note.id
78
+ )
79
+ )
80
+ }
81
+ />
82
+ ))}
83
+ </ul>
84
+ )
85
+ }
@@ -0,0 +1,56 @@
1
+ import { toast, useContextMenu } from "@passui/react"
2
+
3
+ const photos = [
4
+ { id: "aiko", src: "/avatars/aiko.svg" },
5
+ { id: "kenji", src: "/avatars/kenji.svg" },
6
+ { id: "mira", src: "/avatars/mira.svg" },
7
+ ]
8
+
9
+ function Photo({ id, src }: { id: string; src: string }) {
10
+ const menu = useContextMenu(() => [
11
+ {
12
+ label: "Copy",
13
+ onSelect: () => toast("Photo copied"),
14
+ },
15
+ {
16
+ label: "Share",
17
+ onSelect: () => toast("Share sheet"),
18
+ },
19
+ {
20
+ label: "Add to favourites",
21
+ onSelect: () =>
22
+ toast.success("Added to favourites"),
23
+ },
24
+ "divider",
25
+ {
26
+ label: "Delete",
27
+ destructive: true,
28
+ onSelect: () => toast("Photo deleted"),
29
+ },
30
+ ])
31
+
32
+ return (
33
+ <img
34
+ {...menu}
35
+ src={src}
36
+ alt={`${id} portrait`}
37
+ className="aspect-square w-full select-none rounded-3xl object-cover"
38
+ />
39
+ )
40
+ }
41
+
42
+ export function ContextMenuShowcase() {
43
+ return (
44
+ <div className="flex w-full max-w-sm flex-col gap-3">
45
+ <div className="grid grid-cols-3 gap-2">
46
+ {photos.map((photo) => (
47
+ <Photo key={photo.id} {...photo} />
48
+ ))}
49
+ </div>
50
+ <p className="text-center text-footnote text-label-secondary">
51
+ Right-click a photo, or press and hold on a
52
+ touch screen.
53
+ </p>
54
+ </div>
55
+ )
56
+ }
@@ -0,0 +1,63 @@
1
+ import { useRef } from "react"
2
+ import { IconButton } from "../../button/icon-button"
3
+ import {
4
+ MoreIcon,
5
+ ShareIcon,
6
+ StarIcon,
7
+ } from "../../../icons/icons"
8
+ import { openMenu } from "../context-menu-store"
9
+ import { toast } from "../../toast/toast-store"
10
+
11
+ export function ContextMenuToolbar() {
12
+ const toolbar = useRef<HTMLDivElement>(null)
13
+ return (
14
+ <div
15
+ ref={toolbar}
16
+ className="flex items-center gap-1"
17
+ >
18
+ <IconButton
19
+ label="Favourite"
20
+ variant="ghost"
21
+ size="icon-xs"
22
+ >
23
+ <StarIcon size={18} />
24
+ </IconButton>
25
+ <IconButton
26
+ label="Share"
27
+ variant="ghost"
28
+ size="icon-xs"
29
+ >
30
+ <ShareIcon size={18} />
31
+ </IconButton>
32
+ <IconButton
33
+ label="More"
34
+ variant="ghost"
35
+ size="icon-xs"
36
+ onClick={(event) =>
37
+ openMenu(
38
+ event.currentTarget,
39
+ [
40
+ {
41
+ label: "Duplicate",
42
+ onSelect: () =>
43
+ toast("Duplicated"),
44
+ },
45
+ {
46
+ label: "Move",
47
+ onSelect: () =>
48
+ toast("Choose a folder"),
49
+ },
50
+ ],
51
+ {
52
+ edge: "trailing",
53
+ toolbar:
54
+ toolbar.current ?? undefined,
55
+ }
56
+ )
57
+ }
58
+ >
59
+ <MoreIcon size={18} />
60
+ </IconButton>
61
+ </div>
62
+ )
63
+ }
@@ -0,0 +1,24 @@
1
+ import { toast, useContextMenu } from "@passui/react"
2
+
3
+ export function ContextMenuUsage() {
4
+ // render one <ContextMenuHost /> near the root of your app
5
+ const menu = useContextMenu(() => [
6
+ {
7
+ label: "Copy link",
8
+ onSelect: () => toast("Link copied"),
9
+ },
10
+ {
11
+ label: "Open in new tab",
12
+ onSelect: () => toast("Opened"),
13
+ },
14
+ ])
15
+
16
+ return (
17
+ <div
18
+ {...menu}
19
+ className="select-none rounded-3xl bg-fill/60 px-6 py-4 text-callout text-label"
20
+ >
21
+ Right-click this card
22
+ </div>
23
+ )
24
+ }
@@ -0,0 +1,55 @@
1
+ import type { OpenMenu } from "./context-menu-store"
2
+ import type { MenuRect } from "./menu-reveal"
3
+
4
+ type MenuAnchor = Pick<
5
+ OpenMenu,
6
+ "x" | "y" | "alignX" | "alignY"
7
+ >
8
+ type MenuSize = Pick<MenuRect, "width" | "height">
9
+ type MenuBounds = {
10
+ left: number
11
+ top: number
12
+ right: number
13
+ bottom: number
14
+ }
15
+
16
+ /** Fit within the current visible viewport, including a keyboard or Safari pan. */
17
+ export function menuPosition(
18
+ menu: MenuAnchor,
19
+ size: MenuSize,
20
+ bounds: MenuBounds
21
+ ) {
22
+ const { width, height } = size
23
+ const { left, top, right, bottom } = bounds
24
+ const fitsAfterX = menu.x + width <= right
25
+ const fitsBeforeX = menu.x - width >= left
26
+ const endX =
27
+ menu.alignX === "end"
28
+ ? fitsBeforeX || !fitsAfterX
29
+ : !fitsAfterX
30
+ const fitsBelow = menu.y + height <= bottom
31
+ const fitsAbove = menu.y - height >= top
32
+ const endY =
33
+ menu.alignY === "bottom"
34
+ ? fitsAbove || !fitsBelow
35
+ : !fitsBelow
36
+ const clamp = (value: number, min: number, max: number) =>
37
+ Math.min(Math.max(value, min), Math.max(min, max))
38
+ return {
39
+ left: clamp(
40
+ endX ? menu.x - width : menu.x,
41
+ left,
42
+ right - width
43
+ ),
44
+ top: clamp(
45
+ endY ? menu.y - height : menu.y,
46
+ top,
47
+ bottom - height
48
+ ),
49
+ originX: endX ? 1 : 0,
50
+ originY: endY ? 1 : 0,
51
+ maxHeight: Math.max(0, bottom - top),
52
+ width,
53
+ height: Math.min(height, Math.max(0, bottom - top)),
54
+ }
55
+ }
@@ -0,0 +1,75 @@
1
+ export type MenuRect = {
2
+ left: number
3
+ top: number
4
+ width: number
5
+ height: number
6
+ }
7
+
8
+ /** The closed surface occupies its trigger, even when the final menu flips at an edge. */
9
+ export function menuReveal(
10
+ source: MenuRect | undefined,
11
+ target: MenuRect
12
+ ) {
13
+ if (
14
+ !source ||
15
+ source.width <= 0 ||
16
+ source.height <= 0 ||
17
+ target.width <= 0 ||
18
+ target.height <= 0
19
+ )
20
+ return {
21
+ x: 0,
22
+ y: 0,
23
+ scaleX: 0.96,
24
+ scaleY: 0.96,
25
+ opacity: 0,
26
+ }
27
+ return {
28
+ x:
29
+ source.left +
30
+ source.width / 2 -
31
+ target.left -
32
+ target.width / 2,
33
+ y:
34
+ source.top +
35
+ source.height / 2 -
36
+ target.top -
37
+ target.height / 2,
38
+ scaleX: source.width / target.width,
39
+ scaleY: source.height / target.height,
40
+ opacity: 1,
41
+ }
42
+ }
43
+
44
+ /** Stretch across the action group before opening vertically; reverse on dismissal. */
45
+ export function toolbarMenuReveal(
46
+ source: MenuRect,
47
+ toolbar: MenuRect,
48
+ target: MenuRect
49
+ ) {
50
+ const start = menuReveal(source, target)
51
+ const stretched = menuReveal(
52
+ {
53
+ ...toolbar,
54
+ top: source.top,
55
+ height: source.height,
56
+ },
57
+ target
58
+ )
59
+ return {
60
+ enter: {
61
+ x: [start.x, stretched.x, 0],
62
+ y: [start.y, stretched.y, 0],
63
+ scaleX: [start.scaleX, stretched.scaleX, 1],
64
+ scaleY: [start.scaleY, stretched.scaleY, 1],
65
+ opacity: 1,
66
+ },
67
+ exit: {
68
+ x: [0, stretched.x, start.x],
69
+ y: [0, stretched.y, start.y],
70
+ scaleX: [1, stretched.scaleX, start.scaleX],
71
+ scaleY: [1, stretched.scaleY, start.scaleY],
72
+ opacity: 1,
73
+ },
74
+ }
75
+ }
@@ -0,0 +1,90 @@
1
+ import {
2
+ type MouseEvent,
3
+ type PointerEvent,
4
+ useRef,
5
+ } from "react"
6
+ import { haptic } from "../../lib/haptics"
7
+ import {
8
+ type ContextMenuItem,
9
+ openContextMenu,
10
+ } from "./context-menu-store"
11
+
12
+ const LONG_PRESS_MS = 480
13
+ const MOVE_TOLERANCE = 10
14
+
15
+ /**
16
+ * Right-click on desktop, long-press on touch screens: both open the same
17
+ * menu of actions. Spread the returned handlers onto the element.
18
+ */
19
+ export function useContextMenu(
20
+ items: () => ContextMenuItem[]
21
+ ) {
22
+ const press = useRef<{
23
+ x: number
24
+ y: number
25
+ timer: ReturnType<typeof setTimeout>
26
+ } | null>(null)
27
+ const suppressClick = useRef(false)
28
+
29
+ const cancel = () => {
30
+ if (press.current) clearTimeout(press.current.timer)
31
+ press.current = null
32
+ }
33
+
34
+ return {
35
+ onContextMenu(event: MouseEvent) {
36
+ event.preventDefault()
37
+ cancel()
38
+ openContextMenu(
39
+ event.clientX,
40
+ event.clientY,
41
+ items(),
42
+ false,
43
+ event.currentTarget
44
+ )
45
+ },
46
+ onPointerDown(event: PointerEvent) {
47
+ if (event.pointerType !== "touch") return
48
+ const { clientX: x, clientY: y } = event
49
+ const anchor = event.currentTarget
50
+ cancel()
51
+ press.current = {
52
+ x,
53
+ y,
54
+ timer: setTimeout(() => {
55
+ press.current = null
56
+ suppressClick.current = true
57
+ haptic("medium")
58
+ openContextMenu(
59
+ x,
60
+ y,
61
+ items(),
62
+ true,
63
+ anchor
64
+ )
65
+ }, LONG_PRESS_MS),
66
+ }
67
+ },
68
+ onPointerMove(event: PointerEvent) {
69
+ const start = press.current
70
+ if (
71
+ start &&
72
+ Math.hypot(
73
+ event.clientX - start.x,
74
+ event.clientY - start.y
75
+ ) > MOVE_TOLERANCE
76
+ ) {
77
+ cancel()
78
+ }
79
+ },
80
+ onPointerUp: cancel,
81
+ onPointerCancel: cancel,
82
+ // The tap that ends a long-press shouldn't also open the row.
83
+ onClickCapture(event: MouseEvent) {
84
+ if (!suppressClick.current) return
85
+ suppressClick.current = false
86
+ event.preventDefault()
87
+ event.stopPropagation()
88
+ },
89
+ }
90
+ }
@@ -0,0 +1,88 @@
1
+ import { ConversationDays } from "./examples/conversation-days"
2
+ import { CustomerSupportIcon } from "@hugeicons/core-free-icons"
3
+ import { HugeiconsIcon } from "@hugeicons/react"
4
+ import { defineDocs } from "../../docs/types"
5
+ import { Conversation } from "./conversation"
6
+ import { ConversationComposer } from "./examples/conversation-composer"
7
+ import { ConversationShowcase } from "./examples/conversation-showcase"
8
+ import { ConversationUsage } from "./examples/conversation-usage"
9
+
10
+ // Conversation is two parts; the playground shows Messages, with the
11
+ // other side's name and avatar switched on and off.
12
+ type DemoProps = {
13
+ author: string
14
+ avatar: boolean
15
+ }
16
+
17
+ const minutesAgo = (minutes: number) =>
18
+ new Date(Date.now() - minutes * 60_000).toISOString()
19
+
20
+ export const conversationDocs = defineDocs<DemoProps>()({
21
+ slug: "conversation",
22
+ name: "Conversation",
23
+ importPath: "@passui/react/conversation",
24
+ controls: {
25
+ author: { kind: "text", default: "Support" },
26
+ avatar: { kind: "boolean", default: true },
27
+ },
28
+ render: ({ author, avatar }) => {
29
+ const them = {
30
+ mine: false,
31
+ author: author || undefined,
32
+ avatar: avatar ? (
33
+ <span className="flex size-7 items-center justify-center rounded-full bg-accent-soft text-accent-text">
34
+ <HugeiconsIcon
35
+ icon={CustomerSupportIcon}
36
+ size={16}
37
+ />
38
+ </span>
39
+ ) : undefined,
40
+ }
41
+ return (
42
+ <div className="w-full max-w-sm">
43
+ <Conversation.Messages
44
+ messages={[
45
+ {
46
+ id: "1",
47
+ mine: true,
48
+ body: "I can't sign in on my new phone.",
49
+ createdAt: minutesAgo(9),
50
+ },
51
+ {
52
+ id: "2",
53
+ ...them,
54
+ body: "Sorry about that! Did you set up a passkey on your old one?",
55
+ createdAt: minutesAgo(7),
56
+ },
57
+ {
58
+ id: "3",
59
+ ...them,
60
+ body: "If so, you can use it from your new phone by scanning the code.",
61
+ createdAt: minutesAgo(7),
62
+ },
63
+ ]}
64
+ />
65
+ </div>
66
+ )
67
+ },
68
+ childrenSnippet: ({ author, avatar }) => ({
69
+ imports: avatar
70
+ ? [
71
+ 'import { CustomerSupportIcon } from "@hugeicons/core-free-icons"',
72
+ 'import { HugeiconsIcon } from "@hugeicons/react"',
73
+ ]
74
+ : [],
75
+ element: "Conversation.Messages",
76
+ omit: ["author", "avatar"],
77
+ expressionProps: {
78
+ messages: `[\n { id: "1", mine: true, body: "Hi!", createdAt: "2026-10-14T09:00:00Z" },\n { id: "2", mine: false, body: "Hello, how can we help?"${author ? `, author: ${JSON.stringify(author)}` : ""}${avatar ? ", avatar: <HugeiconsIcon icon={CustomerSupportIcon} size={16} />" : ""}, createdAt: "2026-10-14T09:01:00Z" },\n ]`,
79
+ },
80
+ children: "",
81
+ }),
82
+ usage: ConversationUsage,
83
+ showcase: ConversationShowcase,
84
+ examples: [
85
+ { id: "composer", component: ConversationComposer },
86
+ { id: "days", component: ConversationDays },
87
+ ],
88
+ })