@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,535 @@
1
+ import * as React from "react"
2
+ import {
3
+ createContext,
4
+ memo,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useState,
10
+ } from "react"
11
+ import type {
12
+ Dispatch,
13
+ PropsWithChildren,
14
+ ReactNode,
15
+ SetStateAction,
16
+ } from "react"
17
+
18
+ type ValueObject = {
19
+ [themeName: string]: string
20
+ }
21
+
22
+ /**
23
+ * Props for the useTheme hook
24
+ * @author la niina
25
+ */
26
+ export type UseThemeProps = {
27
+ /** List of all available theme names */
28
+ themes: Array<string>
29
+ /** Forced theme name for the current page */
30
+ forcedTheme?: string | undefined
31
+ /** Update the theme */
32
+ setTheme: Dispatch<SetStateAction<string>>
33
+ /** Active theme name */
34
+ theme?: string | undefined
35
+ /** If enableSystem is true, returns the System theme preference ("dark" or "light"), regardless what the active theme is */
36
+ systemTheme?: SystemTheme | undefined
37
+ }
38
+
39
+ export type SystemTheme = "dark" | "light"
40
+
41
+ export type Attribute = `data-${string}` | "class"
42
+
43
+ export interface ThemeProviderProps
44
+ extends PropsWithChildren {
45
+ /** List of all available theme names */
46
+ themes?: Array<string> | undefined
47
+ /** Forced theme name for the current page */
48
+ forcedTheme?: string | undefined
49
+ /** Whether to switch between dark and light themes based on prefers-color-scheme */
50
+ enableSystem?: boolean | undefined
51
+ /** Disable all CSS transitions when switching themes */
52
+ disableTransitionOnChange?: boolean | undefined
53
+ /** Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons */
54
+ enableColorScheme?: boolean | undefined
55
+ /** Key used to store theme setting in localStorage */
56
+ storageKey?: string | undefined
57
+ /** Default theme name (for v0.0.12 and lower the default was light). If `enableSystem` is false, the default theme is light */
58
+ defaultTheme?: string | undefined
59
+ /** HTML attribute modified based on the active theme. Accepts `class`, `data-*` (meaning any data attribute, `data-mode`, `data-color`, etc.), or an array which could include both */
60
+ attribute?: Attribute | Array<Attribute> | undefined
61
+ /** Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value */
62
+ value?: ValueObject | undefined
63
+ /** Nonce string to pass to the inline script for CSP headers */
64
+ nonce?: string | undefined
65
+ }
66
+
67
+ const colorSchemes = ["light", "dark"]
68
+ const MEDIA = "(prefers-color-scheme: dark)"
69
+ const isServer = typeof window === "undefined"
70
+ const ThemeContext = createContext<UseThemeProps | undefined>(
71
+ undefined
72
+ )
73
+ const defaultContext: UseThemeProps = {
74
+ setTheme: () => {
75
+ /* no-op */
76
+ },
77
+ themes: [],
78
+ }
79
+
80
+ /**
81
+ * Hook to access the theme context
82
+ * @returns {UseThemeProps} theme context
83
+ * @example
84
+ * const { theme, setTheme } = useTheme()
85
+ */
86
+ export const useTheme = (): UseThemeProps =>
87
+ useContext(ThemeContext) ?? defaultContext
88
+
89
+ /**
90
+ * Theme provider component is used to wrap your app &
91
+ * provide theme context to all components. It can be used to switch
92
+ * between themes.
93
+ * @author la niina
94
+ * @param {ThemeProviderProps} props
95
+ * @returns {ReactNode} children
96
+ * @example
97
+ * <ThemeProvider attribute="class" disableTransitionOnChange>
98
+ * <App />
99
+ * </ThemeProvider>
100
+ *
101
+ * @example
102
+ * <ThemeProvider themes={["light", "queer", "dark", "system"]} defaultTheme="light">
103
+ * <App />
104
+ * </ThemeProvider>
105
+ */
106
+ export const ThemeProvider = (
107
+ props: ThemeProviderProps
108
+ ): ReactNode => {
109
+ const context = useContext(ThemeContext)
110
+ // Ignore nested context providers, just passthrough children
111
+ if (context) {
112
+ return props.children
113
+ }
114
+ return <Theme {...props} />
115
+ }
116
+
117
+ const defaultThemes = ["light", "dark"]
118
+ const Theme = ({
119
+ forcedTheme,
120
+ disableTransitionOnChange = false,
121
+ enableSystem = true,
122
+ enableColorScheme = true,
123
+ storageKey = "theme",
124
+ themes = defaultThemes,
125
+ defaultTheme = enableSystem ? "system" : "light",
126
+ attribute = "data-theme",
127
+ value,
128
+ children,
129
+ nonce,
130
+ }: ThemeProviderProps) => {
131
+ const [theme, setThemeState] = useState(() =>
132
+ getTheme(storageKey, defaultTheme)
133
+ )
134
+
135
+ const applyClassAttribute = useCallback(
136
+ (
137
+ name: string | undefined,
138
+ attrValues: Array<string>
139
+ ) => {
140
+ const d = document.documentElement
141
+ d.classList.remove(...attrValues)
142
+ if (name) {
143
+ d.classList.add(name)
144
+ }
145
+ },
146
+ []
147
+ )
148
+
149
+ const applyDataAttribute = useCallback(
150
+ (attr: string, name: string | undefined) => {
151
+ const d = document.documentElement
152
+ if (name) {
153
+ d.setAttribute(attr, name)
154
+ } else {
155
+ d.removeAttribute(attr)
156
+ }
157
+ },
158
+ []
159
+ )
160
+
161
+ const applyAttributesToDOM = useCallback(
162
+ (resolved: string) => {
163
+ const attributeList = Array.isArray(attribute)
164
+ ? attribute
165
+ : [attribute]
166
+ const attrValues = value
167
+ ? Object.values(value)
168
+ : themes
169
+ const name = value ? value[resolved] : resolved
170
+
171
+ for (const attr of attributeList) {
172
+ if (attr === "class") {
173
+ applyClassAttribute(name, attrValues)
174
+ } else if (attr.startsWith("data-")) {
175
+ applyDataAttribute(attr, name)
176
+ }
177
+ }
178
+ },
179
+ [
180
+ attribute,
181
+ themes,
182
+ value,
183
+ applyClassAttribute,
184
+ applyDataAttribute,
185
+ ]
186
+ )
187
+
188
+ const applyColorScheme = useCallback(
189
+ (resolved: string) => {
190
+ if (!enableColorScheme) {
191
+ return
192
+ }
193
+ const fallback = colorSchemes.includes(
194
+ defaultTheme
195
+ )
196
+ ? defaultTheme
197
+ : null
198
+ const colorScheme = colorSchemes.includes(
199
+ resolved
200
+ )
201
+ ? resolved
202
+ : fallback
203
+ document.documentElement.style.colorScheme =
204
+ colorScheme || ""
205
+ },
206
+ [enableColorScheme, defaultTheme]
207
+ )
208
+
209
+ // apply selected theme function (light, dark, system)
210
+ const applyTheme = useCallback(
211
+ (nextTheme: string | undefined) => {
212
+ if (!nextTheme) {
213
+ return
214
+ }
215
+ const resolved =
216
+ nextTheme === "system" && enableSystem
217
+ ? getSystemTheme()
218
+ : nextTheme
219
+ const enable = disableTransitionOnChange
220
+ ? disableAnimation()
221
+ : null
222
+ applyAttributesToDOM(resolved)
223
+ applyColorScheme(resolved)
224
+ enable?.()
225
+ },
226
+ [
227
+ enableSystem,
228
+ disableTransitionOnChange,
229
+ applyAttributesToDOM,
230
+ applyColorScheme,
231
+ ]
232
+ )
233
+
234
+ // Set theme state and save to local storage
235
+ const setTheme = useCallback(
236
+ (newValue: SetStateAction<string>) => {
237
+ const newTheme =
238
+ typeof newValue === "function"
239
+ ? newValue(theme ?? "")
240
+ : newValue
241
+ setThemeState(newTheme)
242
+
243
+ // Save to storage
244
+ try {
245
+ localStorage.setItem(storageKey, newTheme)
246
+ } catch {
247
+ // localStorage might not be available
248
+ console.error(
249
+ "Failed to save theme to localStorage"
250
+ )
251
+ }
252
+ },
253
+ [theme, storageKey]
254
+ )
255
+
256
+ const handleMediaQuery = useCallback(
257
+ (_event: MediaQueryListEvent | MediaQueryList) => {
258
+ if (
259
+ theme === "system" &&
260
+ enableSystem &&
261
+ !forcedTheme
262
+ ) {
263
+ applyTheme("system")
264
+ }
265
+ },
266
+ [applyTheme, enableSystem, forcedTheme, theme]
267
+ )
268
+
269
+ // Always listen to System preference
270
+ React.useEffect(() => {
271
+ if (isServer) {
272
+ return
273
+ }
274
+
275
+ const media = window.matchMedia(MEDIA)
276
+ // Intentionally use deprecated listener methods to support iOS 13 and older browsers
277
+ media.addEventListener("change", handleMediaQuery)
278
+ handleMediaQuery(media)
279
+ return () =>
280
+ media.removeEventListener(
281
+ "change",
282
+ handleMediaQuery
283
+ )
284
+ }, [handleMediaQuery])
285
+
286
+ // localStorage event handling, allow to sync theme changes between tabs
287
+ React.useEffect(() => {
288
+ if (isServer) {
289
+ return
290
+ }
291
+
292
+ const handleStorage = (e: StorageEvent) => {
293
+ if (e.key !== storageKey) {
294
+ return
295
+ }
296
+ // If default theme set, use it if localstorage === null (happens on local storage manual deletion)
297
+ const newTheme = e.newValue || defaultTheme
298
+ setTheme(newTheme)
299
+ }
300
+
301
+ window.addEventListener("storage", handleStorage)
302
+ return () =>
303
+ window.removeEventListener(
304
+ "storage",
305
+ handleStorage
306
+ )
307
+ }, [defaultTheme, setTheme, storageKey])
308
+
309
+ // Whenever theme or forcedTheme changes, apply it
310
+ useEffect(() => {
311
+ applyTheme(forcedTheme ?? theme)
312
+ }, [applyTheme, forcedTheme, theme])
313
+
314
+ const providerValue = useMemo(
315
+ () => ({
316
+ theme,
317
+ setTheme,
318
+ forcedTheme,
319
+ themes: enableSystem
320
+ ? [...themes, "system"]
321
+ : themes,
322
+ systemTheme: enableSystem
323
+ ? (getSystemTheme() as
324
+ | "dark"
325
+ | "light"
326
+ | undefined)
327
+ : undefined,
328
+ }),
329
+ [theme, forcedTheme, enableSystem, themes, setTheme]
330
+ )
331
+
332
+ return (
333
+ <ThemeContext.Provider value={providerValue}>
334
+ <ThemeScript
335
+ {...{
336
+ forcedTheme,
337
+ storageKey,
338
+ attribute,
339
+ enableSystem,
340
+ enableColorScheme,
341
+ defaultTheme,
342
+ value,
343
+ themes,
344
+ nonce,
345
+ }}
346
+ />
347
+ {children}
348
+ </ThemeContext.Provider>
349
+ )
350
+ }
351
+
352
+ const ThemeScript = memo(
353
+ ({
354
+ forcedTheme,
355
+ storageKey,
356
+ attribute,
357
+ enableSystem,
358
+ enableColorScheme,
359
+ defaultTheme,
360
+ value,
361
+ themes,
362
+ nonce,
363
+ }: Omit<ThemeProviderProps, "children"> & {
364
+ defaultTheme: string
365
+ }) => {
366
+ const scriptArgs = JSON.stringify([
367
+ attribute,
368
+ storageKey,
369
+ defaultTheme,
370
+ forcedTheme,
371
+ themes,
372
+ value,
373
+ enableSystem,
374
+ enableColorScheme,
375
+ ]).slice(1, -1)
376
+
377
+ return (
378
+ <script
379
+ // Run the theme bootstrap with serialized provider configuration before first paint.
380
+ dangerouslySetInnerHTML={{
381
+ __html: `(${script.toString()})(${scriptArgs})`,
382
+ }}
383
+ nonce={nonce}
384
+ suppressHydrationWarning
385
+ />
386
+ )
387
+ }
388
+ )
389
+
390
+ ThemeScript.displayName = "ThemeScript"
391
+
392
+ // Helpers
393
+ const getTheme = (key: string, fallback?: string) => {
394
+ if (isServer) {
395
+ return fallback
396
+ }
397
+ let theme: string | undefined
398
+ try {
399
+ theme = localStorage.getItem(key) || undefined
400
+ } catch {
401
+ // localStorage might not be available
402
+ }
403
+ return theme || fallback
404
+ }
405
+
406
+ const disableAnimation = () => {
407
+ const css = document.createElement("style")
408
+ css.appendChild(
409
+ document.createTextNode(
410
+ "*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}"
411
+ )
412
+ )
413
+ document.head.appendChild(css)
414
+
415
+ return () => {
416
+ // Force restyle
417
+ window.getComputedStyle(document.body)
418
+ // Wait for next tick before removing
419
+ setTimeout(() => {
420
+ document.head.removeChild(css)
421
+ }, 1)
422
+ }
423
+ }
424
+
425
+ const getSystemTheme = (
426
+ e?: MediaQueryList | MediaQueryListEvent
427
+ ) => {
428
+ if (isServer) {
429
+ return "light"
430
+ }
431
+ const event = e ?? window.matchMedia(MEDIA)
432
+ const isDark = event.matches
433
+ const systemTheme = isDark ? "dark" : "light"
434
+ return systemTheme
435
+ }
436
+
437
+ /*
438
+ This file is adapted from next-themes to work with tanstack start.
439
+ next-themes can be found at https://github.com/pacocoursey/next-themes under the MIT license.
440
+ */
441
+
442
+ export const script = (
443
+ attribute: Attribute | Array<Attribute>,
444
+ storageKey: string,
445
+ defaultTheme: string,
446
+ forcedTheme: string | undefined,
447
+ themes: Array<string>,
448
+ value: ValueObject | undefined,
449
+ enableSystem: boolean,
450
+ enableColorScheme: boolean
451
+ ) => {
452
+ const el = document.documentElement
453
+ const systemThemes = ["light", "dark"]
454
+ const attributes = Array.isArray(attribute)
455
+ ? attribute
456
+ : [attribute]
457
+ const attrValues = value ? Object.values(value) : themes
458
+
459
+ function applyClassAttr(name: string | undefined) {
460
+ el.classList.remove(...attrValues)
461
+ if (name) {
462
+ el.classList.add(name)
463
+ }
464
+ }
465
+
466
+ function applyDataAttr(
467
+ attr: string,
468
+ name: string | undefined
469
+ ) {
470
+ if (name) {
471
+ el.setAttribute(attr, name)
472
+ } else {
473
+ el.removeAttribute(attr)
474
+ }
475
+ }
476
+
477
+ function updateDOM(theme: string) {
478
+ const name = value ? value[theme] : theme
479
+
480
+ for (const attr of attributes) {
481
+ if (attr === "class") {
482
+ applyClassAttr(name)
483
+ } else if (attr.startsWith("data-")) {
484
+ applyDataAttr(attr, name)
485
+ }
486
+ }
487
+
488
+ setColorScheme(theme)
489
+ }
490
+
491
+ function setColorScheme(theme: string) {
492
+ if (!enableColorScheme) {
493
+ return
494
+ }
495
+
496
+ const fallback = systemThemes.includes(defaultTheme)
497
+ ? defaultTheme
498
+ : null
499
+ const colorScheme = systemThemes.includes(theme)
500
+ ? theme
501
+ : fallback
502
+ el.style.colorScheme = colorScheme || ""
503
+ }
504
+
505
+ function resolveSystemTheme() {
506
+ return window.matchMedia(
507
+ "(prefers-color-scheme: dark)"
508
+ ).matches
509
+ ? "dark"
510
+ : "light"
511
+ }
512
+
513
+ if (forcedTheme) {
514
+ const resolvedForcedTheme =
515
+ forcedTheme === "system" && enableSystem
516
+ ? resolveSystemTheme()
517
+ : forcedTheme
518
+ updateDOM(resolvedForcedTheme)
519
+ } else {
520
+ try {
521
+ const themeName =
522
+ localStorage.getItem(storageKey) ||
523
+ defaultTheme
524
+ const isSystem =
525
+ enableSystem && themeName === "system"
526
+ const theme = isSystem
527
+ ? resolveSystemTheme()
528
+ : themeName
529
+ updateDOM(theme)
530
+ } catch {
531
+ // localStorage might not be available
532
+ updateDOM(defaultTheme)
533
+ }
534
+ }
535
+ }
@@ -0,0 +1,68 @@
1
+ import { AmbientNowPlaying } from "./examples/ambient-now-playing"
2
+ import { Ambient } from "./ambient"
3
+ import { defineDocs } from "../../docs/types"
4
+ import { AmbientImages } from "./examples/ambient-images"
5
+ import { AmbientShowcase } from "./examples/ambient-showcase"
6
+ import { AmbientUsage } from "./examples/ambient-usage"
7
+
8
+ // src is any image URL; the playground offers the three sample images.
9
+ type DemoProps = {
10
+ image: "aiko" | "kenji" | "mira" | "none"
11
+ }
12
+
13
+ export const ambientDocs = defineDocs<DemoProps>()({
14
+ slug: "ambient",
15
+ name: "Ambient",
16
+ importPath: "@passui/react/ambient",
17
+ controls: {
18
+ image: {
19
+ kind: "select",
20
+ options: {
21
+ aiko: true,
22
+ kenji: true,
23
+ mira: true,
24
+ none: true,
25
+ },
26
+ default: "aiko",
27
+ },
28
+ },
29
+ render: ({ image }) => (
30
+ <Ambient
31
+ key={image}
32
+ src={
33
+ image === "none"
34
+ ? undefined
35
+ : `/avatars/${image}.svg`
36
+ }
37
+ className="flex w-72 flex-col items-center gap-3 py-12 text-center"
38
+ >
39
+ <img
40
+ src={
41
+ image === "none"
42
+ ? "/avatars/aiko.svg"
43
+ : `/avatars/${image}.svg`
44
+ }
45
+ alt=""
46
+ className="size-24 rounded-3xl shadow-sm"
47
+ />
48
+ <p className="text-headline font-semibold text-label">
49
+ Now playing
50
+ </p>
51
+ </Ambient>
52
+ ),
53
+ childrenSnippet: ({ image }) => ({
54
+ imports: [],
55
+ omit: ["image"],
56
+ extraProps:
57
+ image === "none"
58
+ ? undefined
59
+ : { src: `/avatars/${image}.svg` },
60
+ children: `\n <img src="/avatars/${image === "none" ? "aiko" : image}.svg" alt="" />\n`,
61
+ }),
62
+ usage: AmbientUsage,
63
+ showcase: AmbientShowcase,
64
+ examples: [
65
+ { id: "images", component: AmbientImages },
66
+ { id: "now-playing", component: AmbientNowPlaying },
67
+ ],
68
+ })
@@ -0,0 +1,43 @@
1
+ import type { ComponentPropsWithoutRef } from "react"
2
+ import { cn } from "../../lib/class-names"
3
+
4
+ type AmbientProps = ComponentPropsWithoutRef<"section"> & {
5
+ /** The image whose colors glow behind the content. */
6
+ src?: string | null
7
+ }
8
+
9
+ /**
10
+ * Content with its image's colors glowing softly behind it, no card or
11
+ * edge: a blown-up, blurred copy of the image masked into a circle that
12
+ * fades into the page, the way Apple lets a photo light its surroundings.
13
+ */
14
+ export function Ambient({
15
+ src,
16
+ className,
17
+ children,
18
+ ...props
19
+ }: AmbientProps) {
20
+ return (
21
+ <section
22
+ className={cn("relative isolate", className)}
23
+ {...props}
24
+ >
25
+ {src && (
26
+ <img
27
+ aria-hidden
28
+ src={src}
29
+ alt=""
30
+ className={cn(
31
+ "pointer-events-none absolute top-[38%] left-1/2 -z-10 size-[min(32rem,100vw)] -translate-x-1/2 -translate-y-1/2",
32
+ "rounded-full object-cover opacity-20 blur-3xl",
33
+ // on a dark page the same light blooms far wider, so it shrinks and dims
34
+ "dark:size-[min(20rem,80vw)] dark:opacity-15 dark:blur-2xl",
35
+ "mask-[radial-gradient(closest-side,black_35%,transparent)]",
36
+ "animate-in fade-in-0 zoom-in-90 duration-1000"
37
+ )}
38
+ />
39
+ )}
40
+ {children}
41
+ </section>
42
+ )
43
+ }
@@ -0,0 +1,30 @@
1
+ import { Ambient } from "@passui/react/ambient"
2
+
3
+ const people = [
4
+ { name: "Aiko", src: "/avatars/aiko.svg" },
5
+ { name: "Kenji", src: "/avatars/kenji.svg" },
6
+ { name: "Mira", src: "/avatars/mira.svg" },
7
+ ]
8
+
9
+ export function AmbientImages() {
10
+ return (
11
+ <div className="grid w-full max-w-xl grid-cols-3 gap-2">
12
+ {people.map((person) => (
13
+ <Ambient
14
+ key={person.name}
15
+ src={person.src}
16
+ className="flex flex-col items-center gap-2 py-8"
17
+ >
18
+ <img
19
+ src={person.src}
20
+ alt=""
21
+ className="size-14 rounded-full"
22
+ />
23
+ <span className="text-subheadline font-medium text-label">
24
+ {person.name}
25
+ </span>
26
+ </Ambient>
27
+ ))}
28
+ </div>
29
+ )
30
+ }