@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,50 @@
1
+ import { toast } from "@passui/react"
2
+ import { Button } from "@passui/react/button"
3
+ import { TextField } from "@passui/react/text-field"
4
+ import { useState } from "react"
5
+
6
+ export function TextFieldShowcase() {
7
+ const [name, setName] = useState("Aiko Kamado")
8
+ const [role, setRole] = useState("Product designer")
9
+ const [site, setSite] = useState("")
10
+
11
+ return (
12
+ <form
13
+ className="flex w-full max-w-sm flex-col gap-4 rounded-4xl bg-elevated p-6 shadow-sm ring-1 ring-separator/60"
14
+ onSubmit={(event) => {
15
+ event.preventDefault()
16
+ toast.success("Profile updated")
17
+ }}
18
+ >
19
+ <h2 className="text-headline font-semibold text-label">
20
+ Edit profile
21
+ </h2>
22
+ <TextField
23
+ label="Name"
24
+ value={name}
25
+ onChange={(event) =>
26
+ setName(event.target.value)
27
+ }
28
+ />
29
+ <TextField
30
+ label="Role"
31
+ value={role}
32
+ onChange={(event) =>
33
+ setRole(event.target.value)
34
+ }
35
+ />
36
+ <TextField
37
+ label="Website"
38
+ placeholder="example.com"
39
+ description="Shown on your public profile."
40
+ value={site}
41
+ onChange={(event) =>
42
+ setSite(event.target.value)
43
+ }
44
+ />
45
+ <Button type="submit" variant="accent">
46
+ Save profile
47
+ </Button>
48
+ </form>
49
+ )
50
+ }
@@ -0,0 +1,18 @@
1
+ import { TextField } from "@passui/react/text-field"
2
+ import { useState } from "react"
3
+
4
+ export function TextFieldUsage() {
5
+ const [name, setName] = useState("")
6
+ return (
7
+ <div className="w-full max-w-xs">
8
+ <TextField
9
+ label="Name"
10
+ placeholder="Aiko Kamado"
11
+ value={name}
12
+ onChange={(event) =>
13
+ setName(event.target.value)
14
+ }
15
+ />
16
+ </div>
17
+ )
18
+ }
@@ -0,0 +1,27 @@
1
+ import { TextField } from "@passui/react/text-field"
2
+ import { useState } from "react"
3
+
4
+ export function TextFieldValidation() {
5
+ const [email, setEmail] = useState("aiko@")
6
+ const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
7
+
8
+ return (
9
+ <div className="w-full max-w-xs">
10
+ <TextField
11
+ label="Email"
12
+ type="email"
13
+ description="We only use it to sign you in."
14
+ value={email}
15
+ aria-invalid={!valid}
16
+ errors={
17
+ valid
18
+ ? []
19
+ : ["Enter a full email address."]
20
+ }
21
+ onChange={(event) =>
22
+ setEmail(event.target.value)
23
+ }
24
+ />
25
+ </div>
26
+ )
27
+ }
@@ -0,0 +1,118 @@
1
+ import {
2
+ Mail01Icon,
3
+ Tick02Icon,
4
+ } from "@hugeicons/core-free-icons"
5
+ import { HugeiconsIcon } from "@hugeicons/react"
6
+ import { useState } from "react"
7
+ import { defineDocs } from "../../docs/types"
8
+ import { TextFieldIcons } from "./examples/text-field-icons"
9
+ import { TextFieldShowcase } from "./examples/text-field-showcase"
10
+ import { TextFieldUsage } from "./examples/text-field-usage"
11
+ import { TextFieldValidation } from "./examples/text-field-validation"
12
+ import { TextField, type TextFieldProps } from "./text-field"
13
+
14
+ // The icons are nodes, so the playground offers a switch for each and
15
+ // the snippet writes the icon itself.
16
+ type DemoProps = Pick<
17
+ TextFieldProps,
18
+ "label" | "placeholder" | "disabled"
19
+ > & {
20
+ icon?: boolean
21
+ trailingIcon?: boolean
22
+ }
23
+
24
+ function TextFieldDemo({
25
+ icon,
26
+ trailingIcon,
27
+ ...props
28
+ }: DemoProps) {
29
+ const [value, setValue] = useState("")
30
+ return (
31
+ <div className="w-full max-w-xs">
32
+ <TextField
33
+ {...props}
34
+ icon={
35
+ icon && (
36
+ <HugeiconsIcon
37
+ icon={Mail01Icon}
38
+ size={18}
39
+ />
40
+ )
41
+ }
42
+ trailingIcon={
43
+ trailingIcon && (
44
+ <HugeiconsIcon
45
+ icon={Tick02Icon}
46
+ size={18}
47
+ />
48
+ )
49
+ }
50
+ value={value}
51
+ onChange={(event) =>
52
+ setValue(event.target.value)
53
+ }
54
+ />
55
+ </div>
56
+ )
57
+ }
58
+
59
+ export const textFieldDocs = defineDocs<DemoProps>()({
60
+ slug: "text-field",
61
+ name: "TextField",
62
+ importPath: "@passui/react/text-field",
63
+ controls: {
64
+ label: {
65
+ kind: "text",
66
+ default: "Email",
67
+ required: true,
68
+ },
69
+ placeholder: {
70
+ kind: "text",
71
+ default: "you@example.com",
72
+ },
73
+ disabled: { kind: "boolean", default: false },
74
+ icon: { kind: "boolean", default: false },
75
+ trailingIcon: { kind: "boolean", default: false },
76
+ },
77
+ render: (values) => <TextFieldDemo {...values} />,
78
+ childrenSnippet: ({ icon, trailingIcon }) => ({
79
+ imports: [
80
+ 'import { useState } from "react"',
81
+ ...(icon || trailingIcon
82
+ ? [
83
+ `import { ${[icon && "Mail01Icon", trailingIcon && "Tick02Icon"].filter(Boolean).join(", ")} } from "@hugeicons/core-free-icons"`,
84
+ 'import { HugeiconsIcon } from "@hugeicons/react"',
85
+ ]
86
+ : []),
87
+ ],
88
+ setup: ['const [value, setValue] = useState("")'],
89
+ children: "",
90
+ omit: ["icon", "trailingIcon"],
91
+ expressionProps: {
92
+ ...(icon
93
+ ? {
94
+ icon: "<HugeiconsIcon icon={Mail01Icon} size={18} />",
95
+ }
96
+ : {}),
97
+ ...(trailingIcon
98
+ ? {
99
+ trailingIcon:
100
+ "<HugeiconsIcon icon={Tick02Icon} size={18} />",
101
+ }
102
+ : {}),
103
+ value: "value",
104
+ onChange:
105
+ "(event) => setValue(event.target.value)",
106
+ },
107
+ }),
108
+ usage: TextFieldUsage,
109
+ showcase: TextFieldShowcase,
110
+ examples: [
111
+ {
112
+ id: "icons",
113
+ component: TextFieldIcons,
114
+ playground: { icon: true, trailingIcon: true },
115
+ },
116
+ { id: "validation", component: TextFieldValidation },
117
+ ],
118
+ })
@@ -0,0 +1,135 @@
1
+ import {
2
+ type ComponentProps,
3
+ type ReactNode,
4
+ useId,
5
+ useState,
6
+ } from "react"
7
+ import { Button } from "../button/button"
8
+ import { cn } from "../../lib/class-names"
9
+
10
+ export const textControlClasses = cn(
11
+ "w-full min-w-0 rounded-xl border border-separator bg-elevated",
12
+ "px-3 py-2 text-body text-label placeholder:text-label-tertiary",
13
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-text",
14
+ "disabled:cursor-not-allowed disabled:opacity-40 aria-invalid:border-danger"
15
+ )
16
+
17
+ const fieldIconClasses =
18
+ "flex shrink-0 items-center text-label-secondary"
19
+
20
+ export type TextFieldProps = ComponentProps<"input"> & {
21
+ label: string
22
+ description?: ReactNode
23
+ errors?: string[]
24
+ /** A decorative icon before the text, such as a mail glyph. */
25
+ icon?: ReactNode
26
+ /** A decorative icon after the text, such as a unit or a check. */
27
+ trailingIcon?: ReactNode
28
+ /** An interactive control after the text, such as a clear button. Size it to the field: an `xs` or `icon-xs` button, never the default. */
29
+ action?: ReactNode
30
+ revealLabels?: { show: string; hide: string }
31
+ }
32
+
33
+ export function TextField({
34
+ label,
35
+ description,
36
+ errors = [],
37
+ icon,
38
+ trailingIcon,
39
+ action,
40
+ revealLabels,
41
+ type = "text",
42
+ id: suppliedId,
43
+ className,
44
+ ...props
45
+ }: TextFieldProps) {
46
+ const generatedId = useId()
47
+ const id = suppliedId ?? generatedId
48
+ const [revealed, setRevealed] = useState(false)
49
+ const messages = Array.from(new Set(errors))
50
+ const reveal = type === "password" && revealLabels
51
+ return (
52
+ <div className="flex flex-col gap-2">
53
+ <label
54
+ htmlFor={id}
55
+ className="text-subheadline font-semibold"
56
+ >
57
+ {label}
58
+ </label>
59
+ <div className="flex items-center gap-2 rounded-xl border border-separator bg-elevated px-3 focus-within:ring-1 focus-within:ring-accent/40 has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-accent-text/70">
60
+ {icon && (
61
+ <span
62
+ aria-hidden
63
+ className={fieldIconClasses}
64
+ >
65
+ {icon}
66
+ </span>
67
+ )}
68
+ <input
69
+ {...props}
70
+ id={id}
71
+ type={reveal && revealed ? "text" : type}
72
+ aria-invalid={messages.length > 0}
73
+ aria-describedby={
74
+ messages.length || description
75
+ ? `${id}-description`
76
+ : undefined
77
+ }
78
+ className={cn(
79
+ "h-11 w-full min-w-0 bg-transparent text-body text-label outline-none",
80
+ "placeholder:text-label-tertiary disabled:opacity-40",
81
+ className
82
+ )}
83
+ />
84
+ {trailingIcon && (
85
+ <span
86
+ aria-hidden
87
+ className={fieldIconClasses}
88
+ >
89
+ {trailingIcon}
90
+ </span>
91
+ )}
92
+ {reveal && (
93
+ <Button
94
+ variant="ghost"
95
+ size="xs"
96
+ // sized to sit inside the 44px field; on touch a
97
+ // wider invisible area keeps a 44px target
98
+ className="-mr-1.5 shrink-0 whitespace-nowrap [@media(pointer:coarse)]:before:absolute [@media(pointer:coarse)]:before:-inset-1.5 [@media(pointer:coarse)]:before:content-['']"
99
+ aria-controls={id}
100
+ aria-label={
101
+ revealed
102
+ ? revealLabels.hide
103
+ : revealLabels.show
104
+ }
105
+ aria-pressed={revealed}
106
+ onClick={() => setRevealed(!revealed)}
107
+ >
108
+ {revealed
109
+ ? revealLabels.hide
110
+ : revealLabels.show}
111
+ </Button>
112
+ )}
113
+ {action}
114
+ </div>
115
+ {(messages.length > 0 || description) && (
116
+ <div
117
+ id={`${id}-description`}
118
+ role={
119
+ messages.length ? "alert" : undefined
120
+ }
121
+ className={cn(
122
+ "text-footnote",
123
+ messages.length
124
+ ? "text-danger"
125
+ : "text-label-secondary"
126
+ )}
127
+ >
128
+ {messages.length
129
+ ? messages.join(". ")
130
+ : description}
131
+ </div>
132
+ )}
133
+ </div>
134
+ )
135
+ }
@@ -0,0 +1,32 @@
1
+ import { Button } from "@passui/react/button"
2
+ import { toast } from "@passui/react/toast"
3
+ import { useState } from "react"
4
+
5
+ export function ToastAction() {
6
+ const [archived, setArchived] = useState(0)
7
+
8
+ return (
9
+ <div className="flex flex-col items-center gap-3">
10
+ <Button
11
+ variant="secondary"
12
+ onClick={() => {
13
+ setArchived((count) => count + 1)
14
+ toast("Note archived", {
15
+ action: {
16
+ label: "Undo",
17
+ onClick: () =>
18
+ setArchived(
19
+ (count) => count - 1
20
+ ),
21
+ },
22
+ })
23
+ }}
24
+ >
25
+ Archive note
26
+ </Button>
27
+ <p className="text-footnote text-label-secondary">
28
+ Archived: {archived}
29
+ </p>
30
+ </div>
31
+ )
32
+ }
@@ -0,0 +1,45 @@
1
+ import { Button } from "@passui/react/button"
2
+ import { toast } from "@passui/react/toast"
3
+
4
+ function upload(fails: boolean) {
5
+ return new Promise<string>((resolve, reject) =>
6
+ setTimeout(
7
+ () =>
8
+ fails
9
+ ? reject(new Error("offline"))
10
+ : resolve("photo.jpg"),
11
+ 1600
12
+ )
13
+ )
14
+ }
15
+
16
+ export function ToastPromise() {
17
+ return (
18
+ <div className="flex flex-wrap justify-center gap-2">
19
+ <Button
20
+ variant="secondary"
21
+ onClick={() =>
22
+ toast.promise(upload(false), {
23
+ loading: "Uploading",
24
+ success: (name) => `${name} uploaded`,
25
+ error: "Upload failed",
26
+ })
27
+ }
28
+ >
29
+ Upload
30
+ </Button>
31
+ <Button
32
+ variant="secondary"
33
+ onClick={() =>
34
+ toast.promise(upload(true), {
35
+ loading: "Uploading",
36
+ success: "Uploaded",
37
+ error: "Upload failed. Check your connection.",
38
+ })
39
+ }
40
+ >
41
+ Upload while offline
42
+ </Button>
43
+ </div>
44
+ )
45
+ }
@@ -0,0 +1,49 @@
1
+ import { LabeledContent, Section } from "@passui/react"
2
+ import { toast } from "@passui/react/toast"
3
+ import { Toggle } from "@passui/react/toggle"
4
+ import { useState } from "react"
5
+
6
+ export function ToastShowcase() {
7
+ const [backups, setBackups] = useState(true)
8
+ const [alerts, setAlerts] = useState(false)
9
+
10
+ return (
11
+ <div className="w-full max-w-sm">
12
+ <Section
13
+ title="Account"
14
+ footer="Each change saves at once and confirms with a toast."
15
+ >
16
+ <LabeledContent label="Daily backups">
17
+ <Toggle
18
+ label="Daily backups"
19
+ labelHidden
20
+ checked={backups}
21
+ onChange={(checked) => {
22
+ setBackups(checked)
23
+ toast.success(
24
+ checked
25
+ ? "Backups turned on"
26
+ : "Backups turned off"
27
+ )
28
+ }}
29
+ />
30
+ </LabeledContent>
31
+ <LabeledContent label="Sign-in alerts">
32
+ <Toggle
33
+ label="Sign-in alerts"
34
+ labelHidden
35
+ checked={alerts}
36
+ onChange={(checked) => {
37
+ setAlerts(checked)
38
+ toast(
39
+ checked
40
+ ? "We'll email you about new sign-ins"
41
+ : "Sign-in alerts turned off"
42
+ )
43
+ }}
44
+ />
45
+ </LabeledContent>
46
+ </Section>
47
+ </div>
48
+ )
49
+ }
@@ -0,0 +1,13 @@
1
+ import { Button } from "@passui/react/button"
2
+ import { toast } from "@passui/react/toast"
3
+
4
+ export function ToastUsage() {
5
+ // render one <Toaster /> near the root of your app
6
+ return (
7
+ <Button
8
+ onClick={() => toast.success("Changes saved")}
9
+ >
10
+ Save
11
+ </Button>
12
+ )
13
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * App-wide toasts: short messages that confirm, report progress or explain
3
+ * a failure. Call `toast` from anywhere; render one `<Toaster />`.
4
+ */
5
+ export type ToastTone =
6
+ | "neutral"
7
+ | "success"
8
+ | "error"
9
+ | "loading"
10
+
11
+ export interface ToastAction {
12
+ label: string
13
+ onClick: () => void
14
+ }
15
+
16
+ export interface Toast {
17
+ id: number
18
+ tone: ToastTone
19
+ title: string
20
+ description?: string
21
+ action?: ToastAction
22
+ /** Milliseconds before it leaves on its own; `null` stays until dismissed. */
23
+ duration: number | null
24
+ createdAt: number
25
+ }
26
+
27
+ export interface ToastOptions {
28
+ description?: string
29
+ action?: ToastAction
30
+ duration?: number | null
31
+ }
32
+
33
+ const DEFAULT_DURATION = 4200
34
+ const MAX_VISIBLE = 3
35
+
36
+ let toasts: Toast[] = []
37
+ let nextId = 1
38
+ const listeners = new Set<() => void>()
39
+
40
+ const emit = () => {
41
+ for (const listener of listeners) listener()
42
+ }
43
+
44
+ function show(
45
+ tone: ToastTone,
46
+ title: string,
47
+ options: ToastOptions = {}
48
+ ): number {
49
+ const id = nextId++
50
+ const duration =
51
+ options.duration !== undefined
52
+ ? options.duration
53
+ : tone === "loading"
54
+ ? null
55
+ : DEFAULT_DURATION
56
+ toasts = [
57
+ {
58
+ id,
59
+ tone,
60
+ title,
61
+ description: options.description,
62
+ action: options.action,
63
+ duration,
64
+ createdAt: Date.now(),
65
+ },
66
+ ...toasts,
67
+ ].slice(0, MAX_VISIBLE + 2)
68
+ emit()
69
+ return id
70
+ }
71
+
72
+ /** Changes a toast in place; it morphs to its new state. */
73
+ function update(
74
+ id: number,
75
+ tone: ToastTone,
76
+ title: string,
77
+ options: ToastOptions = {}
78
+ ) {
79
+ if (!toasts.some((t) => t.id === id))
80
+ return show(tone, title, options)
81
+ toasts = toasts.map((t) =>
82
+ t.id === id
83
+ ? {
84
+ ...t,
85
+ tone,
86
+ title,
87
+ description: options.description,
88
+ action: options.action,
89
+ duration:
90
+ options.duration !== undefined
91
+ ? options.duration
92
+ : tone === "loading"
93
+ ? null
94
+ : DEFAULT_DURATION,
95
+ createdAt: Date.now(),
96
+ }
97
+ : t
98
+ )
99
+ emit()
100
+ return id
101
+ }
102
+
103
+ function dismiss(id: number) {
104
+ toasts = toasts.filter((t) => t.id !== id)
105
+ emit()
106
+ }
107
+
108
+ interface PromiseMessages<T> {
109
+ loading: string
110
+ success: string | ((value: T) => string)
111
+ error: string | ((error: unknown) => string)
112
+ }
113
+
114
+ export const toast = Object.assign(
115
+ (title: string, options?: ToastOptions) =>
116
+ show("neutral", title, options),
117
+ {
118
+ success: (title: string, options?: ToastOptions) =>
119
+ show("success", title, options),
120
+ error: (title: string, options?: ToastOptions) =>
121
+ show("error", title, options),
122
+ loading: (title: string, options?: ToastOptions) =>
123
+ show("loading", title, options),
124
+ update,
125
+ dismiss,
126
+ /** Shows progress, then morphs into the outcome. Returns the promise. */
127
+ promise<T>(
128
+ promise: Promise<T>,
129
+ messages: PromiseMessages<T>
130
+ ): Promise<T> {
131
+ const id = show("loading", messages.loading)
132
+ promise.then(
133
+ (value) =>
134
+ update(
135
+ id,
136
+ "success",
137
+ typeof messages.success === "function"
138
+ ? messages.success(value)
139
+ : messages.success
140
+ ),
141
+ (error: unknown) =>
142
+ update(
143
+ id,
144
+ "error",
145
+ typeof messages.error === "function"
146
+ ? messages.error(error)
147
+ : messages.error
148
+ )
149
+ )
150
+ return promise
151
+ },
152
+ }
153
+ )
154
+
155
+ export function subscribeToasts(listener: () => void) {
156
+ listeners.add(listener)
157
+ return () => {
158
+ listeners.delete(listener)
159
+ }
160
+ }
161
+
162
+ export function getToasts(): Toast[] {
163
+ return toasts
164
+ }
165
+
166
+ export const toastLimits = { visible: MAX_VISIBLE } as const