@passui/react 0.1.45 → 0.1.46

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 (140) hide show
  1. package/README.md +57 -46
  2. package/dist/types/src/components/accordion/accordion.d.ts +1 -1
  3. package/dist/types/src/components/breadcrumb/breadcrumb.d.ts +1 -1
  4. package/dist/types/src/components/inline-disclosure/inline-disclosure.d.ts +1 -1
  5. package/dist/types/src/components/popover/anchored-surface.d.ts +5 -1
  6. package/dist/types/src/components/popover/index.d.ts +41 -0
  7. package/dist/types/src/components/popover/popover-content.d.ts +32 -0
  8. package/dist/types/src/components/popover/popover-root.d.ts +22 -0
  9. package/dist/types/src/components/popover/popover-trigger.d.ts +23 -0
  10. package/dist/types/src/components/stepper/rolling-digit.d.ts +1 -1
  11. package/dist/types/src/components/table/table.d.ts +1 -1
  12. package/dist/types/src/components/view-controller/view-controller.d.ts +2 -2
  13. package/dist/types/src/index.d.ts +1 -1
  14. package/dist/types/src/machines/popover.machine.d.ts +30 -0
  15. package/package.json +307 -307
  16. package/src/appearance/index.ts +2 -2
  17. package/src/appearance/platform.tsx +76 -60
  18. package/src/appearance/theme.tsx +409 -377
  19. package/src/components/accordion/accordion.tsx +58 -49
  20. package/src/components/ambient/ambient.tsx +25 -25
  21. package/src/components/ambient/index.ts +1 -1
  22. package/src/components/aspect-ratio/aspect-ratio.tsx +21 -16
  23. package/src/components/breadcrumb/breadcrumb.tsx +63 -57
  24. package/src/components/button/button-styles.ts +39 -32
  25. package/src/components/button/button.tsx +91 -84
  26. package/src/components/button/icon-button.tsx +27 -25
  27. package/src/components/button/pending-spinner.tsx +8 -8
  28. package/src/components/button/use-pending-indicator.ts +11 -11
  29. package/src/components/chip/chip.tsx +35 -30
  30. package/src/components/code-block/code-block.tsx +40 -39
  31. package/src/components/combo-box/combo-box.tsx +5 -5
  32. package/src/components/command/command.tsx +124 -94
  33. package/src/components/compact-date-picker/calendar-month.ts +75 -53
  34. package/src/components/compact-date-picker/compact-date-picker.tsx +308 -289
  35. package/src/components/compact-time-picker/compact-time-picker.tsx +288 -281
  36. package/src/components/compact-time-picker/time-value.ts +32 -29
  37. package/src/components/content-unavailable-view/content-unavailable-view.tsx +64 -48
  38. package/src/components/content-view/content-view.tsx +168 -159
  39. package/src/components/content-view/index.ts +1 -1
  40. package/src/components/context-menu/conceal-menu-source.ts +25 -25
  41. package/src/components/context-menu/context-menu-host.tsx +439 -402
  42. package/src/components/context-menu/context-menu-store.ts +125 -123
  43. package/src/components/context-menu/menu-navigation.ts +25 -18
  44. package/src/components/context-menu/menu-position.ts +33 -29
  45. package/src/components/context-menu/menu-reveal.ts +32 -32
  46. package/src/components/context-menu/use-context-menu.ts +63 -56
  47. package/src/components/conversation/conversation.tsx +265 -263
  48. package/src/components/conversation/index.ts +1 -1
  49. package/src/components/date-picker/date-picker.tsx +95 -94
  50. package/src/components/date-picker/date-value.ts +20 -10
  51. package/src/components/dialog/dialog-host.tsx +105 -103
  52. package/src/components/dialog/dialog-store.ts +37 -36
  53. package/src/components/dialog/dialog.tsx +192 -179
  54. package/src/components/dialog/index.ts +1 -1
  55. package/src/components/disclosure-group/disclosure-group.tsx +93 -86
  56. package/src/components/divider/divider.tsx +24 -22
  57. package/src/components/inline-disclosure/inline-disclosure.tsx +40 -36
  58. package/src/components/keyboard-shortcut/keyboard-shortcut.tsx +23 -19
  59. package/src/components/labeled-content/labeled-content.tsx +68 -68
  60. package/src/components/locale-switcher/index.ts +1 -1
  61. package/src/components/locale-switcher/locale-switcher.tsx +279 -251
  62. package/src/components/menu/menu.tsx +75 -58
  63. package/src/components/morph/index.ts +2 -2
  64. package/src/components/morph/morph.tsx +227 -201
  65. package/src/components/morph/use-morph-disclosure.ts +19 -19
  66. package/src/components/one-time-code/one-time-code.tsx +216 -200
  67. package/src/components/picker/picker-trigger.ts +16 -13
  68. package/src/components/picker/picker.tsx +371 -345
  69. package/src/components/popover/anchored-surface.tsx +253 -229
  70. package/src/components/popover/index.ts +50 -0
  71. package/src/components/popover/popover-content.tsx +334 -0
  72. package/src/components/popover/popover-fit.ts +58 -53
  73. package/src/components/popover/popover-root.tsx +97 -0
  74. package/src/components/popover/popover-trigger.tsx +71 -0
  75. package/src/components/profile-image/profile-image.tsx +80 -64
  76. package/src/components/progress-view/progress-view.tsx +61 -59
  77. package/src/components/progress-view/use-liquid-progress.ts +38 -38
  78. package/src/components/radio-group/radio-group.tsx +71 -68
  79. package/src/components/search-field/expanding-search-field.tsx +191 -177
  80. package/src/components/search-field/search-field.tsx +59 -50
  81. package/src/components/search-list/search-list.tsx +246 -236
  82. package/src/components/section/section.tsx +58 -58
  83. package/src/components/secure-field/secure-field.tsx +3 -3
  84. package/src/components/segmented-control/segmented-control.tsx +136 -136
  85. package/src/components/sheet/modal-environment.ts +71 -64
  86. package/src/components/sheet/sheet-portal.tsx +58 -53
  87. package/src/components/sheet/sheet.tsx +146 -141
  88. package/src/components/sheet/use-sheet-focus.ts +97 -82
  89. package/src/components/sidebar/sidebar.tsx +78 -78
  90. package/src/components/skeleton/skeleton.tsx +30 -21
  91. package/src/components/slider/slider.tsx +113 -104
  92. package/src/components/slosh-slider/liquid-step.ts +32 -31
  93. package/src/components/slosh-slider/slosh-slider.tsx +265 -228
  94. package/src/components/stat-tile/index.ts +1 -1
  95. package/src/components/stat-tile/stat-tile.tsx +79 -68
  96. package/src/components/status-indicator/status-indicator.tsx +40 -37
  97. package/src/components/stepper/rolling-digit.tsx +82 -68
  98. package/src/components/stepper/stepper.tsx +122 -113
  99. package/src/components/swatch-picker/swatch-picker.tsx +120 -120
  100. package/src/components/tab-bar/tab-bar.tsx +149 -148
  101. package/src/components/tab-view/tab-view.tsx +140 -137
  102. package/src/components/table/table.tsx +117 -98
  103. package/src/components/text-editor/text-editor.tsx +34 -29
  104. package/src/components/text-field/text-field.tsx +182 -180
  105. package/src/components/toast/toast-store.ts +127 -115
  106. package/src/components/toast/toaster.tsx +361 -347
  107. package/src/components/toggle/toggle.tsx +297 -282
  108. package/src/components/tooltip/tooltip-host.tsx +157 -150
  109. package/src/components/view-controller/index.ts +1 -1
  110. package/src/components/view-controller/view-controller.tsx +64 -53
  111. package/src/components/wake-slider/wake-slider.tsx +220 -205
  112. package/src/hooks/scroll/minimize-decision.ts +11 -7
  113. package/src/hooks/use-anchored-disclosure.ts +19 -19
  114. package/src/hooks/use-anchored-placement.ts +29 -25
  115. package/src/hooks/use-dismiss.ts +33 -29
  116. package/src/hooks/use-floating-dismiss.ts +24 -22
  117. package/src/hooks/use-media-query.ts +67 -60
  118. package/src/hooks/use-minimize-on-scroll.ts +69 -60
  119. package/src/hooks/use-visual-viewport.ts +63 -63
  120. package/src/hooks/viewport/types.ts +16 -16
  121. package/src/hooks/viewport/viewport-metrics.ts +16 -10
  122. package/src/icons/icons.tsx +440 -328
  123. package/src/index.ts +91 -91
  124. package/src/lib/anchored-placement.ts +70 -65
  125. package/src/lib/class-names.ts +14 -13
  126. package/src/lib/clipboard.ts +8 -7
  127. package/src/lib/date.ts +61 -60
  128. package/src/lib/floating-layers.ts +39 -24
  129. package/src/lib/haptics.ts +10 -10
  130. package/src/lib/language.ts +9 -9
  131. package/src/lib/range-value.ts +27 -21
  132. package/src/lib/screen-edges.ts +33 -30
  133. package/src/lib/shapes.ts +38 -34
  134. package/src/machines/popover.machine.ts +51 -0
  135. package/src/motion/transitions.ts +46 -43
  136. package/src/styles/code-block.css +69 -43
  137. package/src/styles/motion-controls.css +393 -191
  138. package/src/tokens.ts +1 -1
  139. package/dist/types/src/components/popover/popover.d.ts +0 -21
  140. package/src/components/popover/popover.tsx +0 -334
@@ -1,65 +1,78 @@
1
- import * as React from "react"
2
- import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from "react"
3
- import type { Dispatch, PropsWithChildren, ReactNode, SetStateAction } from "react"
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";
4
17
 
5
18
  type ValueObject = {
6
- [themeName: string]: string
7
- }
19
+ [themeName: string]: string;
20
+ };
8
21
 
9
22
  /**
10
23
  * Props for the useTheme hook
11
24
  * @author la niina
12
25
  */
13
26
  export type UseThemeProps = {
14
- /** List of all available theme names */
15
- themes: Array<string>
16
- /** Forced theme name for the current page */
17
- forcedTheme?: string | undefined
18
- /** Update the theme */
19
- setTheme: Dispatch<SetStateAction<string>>
20
- /** Active theme name */
21
- theme?: string | undefined
22
- /** If enableSystem is true, returns the System theme preference ("dark" or "light"), regardless what the active theme is */
23
- systemTheme?: SystemTheme | undefined
24
- }
25
-
26
- export type SystemTheme = "dark" | "light"
27
-
28
- export type Attribute = `data-${string}` | "class"
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";
29
42
 
30
43
  export interface ThemeProviderProps extends PropsWithChildren {
31
- /** List of all available theme names */
32
- themes?: Array<string> | undefined
33
- /** Forced theme name for the current page */
34
- forcedTheme?: string | undefined
35
- /** Whether to switch between dark and light themes based on prefers-color-scheme */
36
- enableSystem?: boolean | undefined
37
- /** Disable all CSS transitions when switching themes */
38
- disableTransitionOnChange?: boolean | undefined
39
- /** Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons */
40
- enableColorScheme?: boolean | undefined
41
- /** Key used to store theme setting in localStorage */
42
- storageKey?: string | undefined
43
- /** Default theme name (for v0.0.12 and lower the default was light). If `enableSystem` is false, the default theme is light */
44
- defaultTheme?: string | undefined
45
- /** 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 */
46
- attribute?: Attribute | Array<Attribute> | undefined
47
- /** Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value */
48
- value?: ValueObject | undefined
49
- /** Nonce string to pass to the inline script for CSP headers */
50
- nonce?: string | undefined
44
+ /** List of all available theme names */
45
+ themes?: Array<string> | undefined;
46
+ /** Forced theme name for the current page */
47
+ forcedTheme?: string | undefined;
48
+ /** Whether to switch between dark and light themes based on prefers-color-scheme */
49
+ enableSystem?: boolean | undefined;
50
+ /** Disable all CSS transitions when switching themes */
51
+ disableTransitionOnChange?: boolean | undefined;
52
+ /** Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons */
53
+ enableColorScheme?: boolean | undefined;
54
+ /** Key used to store theme setting in localStorage */
55
+ storageKey?: string | undefined;
56
+ /** Default theme name (for v0.0.12 and lower the default was light). If `enableSystem` is false, the default theme is light */
57
+ defaultTheme?: string | undefined;
58
+ /** 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 */
59
+ attribute?: Attribute | Array<Attribute> | undefined;
60
+ /** Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value */
61
+ value?: ValueObject | undefined;
62
+ /** Nonce string to pass to the inline script for CSP headers */
63
+ nonce?: string | undefined;
51
64
  }
52
65
 
53
- const colorSchemes = ["light", "dark"]
54
- const MEDIA = "(prefers-color-scheme: dark)"
55
- const isServer = typeof window === "undefined"
56
- const ThemeContext = createContext<UseThemeProps | undefined>(undefined)
66
+ const colorSchemes = ["light", "dark"];
67
+ const MEDIA = "(prefers-color-scheme: dark)";
68
+ const isServer = typeof window === "undefined";
69
+ const ThemeContext = createContext<UseThemeProps | undefined>(undefined);
57
70
  const defaultContext: UseThemeProps = {
58
- setTheme: () => {
59
- /* no-op */
60
- },
61
- themes: [],
62
- }
71
+ setTheme: () => {
72
+ /* no-op */
73
+ },
74
+ themes: [],
75
+ };
63
76
 
64
77
  /**
65
78
  * Hook to access the theme context
@@ -67,7 +80,8 @@ const defaultContext: UseThemeProps = {
67
80
  * @example
68
81
  * const { theme, setTheme } = useTheme()
69
82
  */
70
- export const useTheme = (): UseThemeProps => useContext(ThemeContext) ?? defaultContext
83
+ export const useTheme = (): UseThemeProps =>
84
+ useContext(ThemeContext) ?? defaultContext;
71
85
 
72
86
  /**
73
87
  * Theme provider component is used to wrap your app &
@@ -87,272 +101,286 @@ export const useTheme = (): UseThemeProps => useContext(ThemeContext) ?? default
87
101
  * </ThemeProvider>
88
102
  */
89
103
  export const ThemeProvider = (props: ThemeProviderProps): ReactNode => {
90
- const context = useContext(ThemeContext)
91
- // Ignore nested context providers, just passthrough children
92
- if (context) {
93
- return props.children
94
- }
95
- return <Theme {...props} />
96
- }
97
-
98
- const defaultThemes = ["light", "dark"]
104
+ const context = useContext(ThemeContext);
105
+ // Ignore nested context providers, just passthrough children
106
+ if (context) {
107
+ return props.children;
108
+ }
109
+ return <Theme {...props} />;
110
+ };
111
+
112
+ const defaultThemes = ["light", "dark"];
99
113
  const Theme = ({
100
- forcedTheme,
101
- disableTransitionOnChange = false,
102
- enableSystem = true,
103
- enableColorScheme = true,
104
- storageKey = "theme",
105
- themes = defaultThemes,
106
- defaultTheme = enableSystem ? "system" : "light",
107
- attribute = "data-theme",
108
- value,
109
- children,
110
- nonce,
114
+ forcedTheme,
115
+ disableTransitionOnChange = false,
116
+ enableSystem = true,
117
+ enableColorScheme = true,
118
+ storageKey = "theme",
119
+ themes = defaultThemes,
120
+ defaultTheme = enableSystem ? "system" : "light",
121
+ attribute = "data-theme",
122
+ value,
123
+ children,
124
+ nonce,
111
125
  }: ThemeProviderProps) => {
112
- const [theme, setThemeState] = useState(() => getTheme(storageKey, defaultTheme))
113
-
114
- const applyClassAttribute = useCallback(
115
- (name: string | undefined, attrValues: Array<string>) => {
116
- const d = document.documentElement
117
- d.classList.remove(...attrValues)
118
- if (name) {
119
- d.classList.add(name)
120
- }
121
- },
122
- []
123
- )
124
-
125
- const applyDataAttribute = useCallback((attr: string, name: string | undefined) => {
126
- const d = document.documentElement
127
- if (name) {
128
- d.setAttribute(attr, name)
129
- } else {
130
- d.removeAttribute(attr)
131
- }
132
- }, [])
133
-
134
- const applyAttributesToDOM = useCallback(
135
- (resolved: string) => {
136
- const attributeList = Array.isArray(attribute) ? attribute : [attribute]
137
- const attrValues = value ? Object.values(value) : themes
138
- const name = value ? value[resolved] : resolved
139
-
140
- for (const attr of attributeList) {
141
- if (attr === "class") {
142
- applyClassAttribute(name, attrValues)
143
- } else if (attr.startsWith("data-")) {
144
- applyDataAttribute(attr, name)
145
- }
146
- }
147
- },
148
- [attribute, themes, value, applyClassAttribute, applyDataAttribute]
149
- )
150
-
151
- const applyColorScheme = useCallback(
152
- (resolved: string) => {
153
- if (!enableColorScheme) {
154
- return
155
- }
156
- const fallback = colorSchemes.includes(defaultTheme) ? defaultTheme : null
157
- const colorScheme = colorSchemes.includes(resolved) ? resolved : fallback
158
- document.documentElement.style.colorScheme = colorScheme || ""
159
- },
160
- [enableColorScheme, defaultTheme]
161
- )
162
-
163
- // apply selected theme function (light, dark, system)
164
- const applyTheme = useCallback(
165
- (nextTheme: string | undefined) => {
166
- if (!nextTheme) {
167
- return
168
- }
169
- const resolved = nextTheme === "system" && enableSystem ? getSystemTheme() : nextTheme
170
- const enable = disableTransitionOnChange ? disableAnimation() : null
171
- applyAttributesToDOM(resolved)
172
- applyColorScheme(resolved)
173
- enable?.()
174
- },
175
- [enableSystem, disableTransitionOnChange, applyAttributesToDOM, applyColorScheme]
176
- )
177
-
178
- // Set theme state and save to local storage
179
- const setTheme = useCallback(
180
- (newValue: SetStateAction<string>) => {
181
- const newTheme = typeof newValue === "function" ? newValue(theme ?? "") : newValue
182
- setThemeState(newTheme)
183
-
184
- // Save to storage
185
- try {
186
- localStorage.setItem(storageKey, newTheme)
187
- } catch {
188
- // localStorage might not be available
189
- console.error("Failed to save theme to localStorage")
190
- }
191
- },
192
- [theme, storageKey]
193
- )
194
-
195
- const handleMediaQuery = useCallback(
196
- (_event: MediaQueryListEvent | MediaQueryList) => {
197
- if (theme === "system" && enableSystem && !forcedTheme) {
198
- applyTheme("system")
199
- }
200
- },
201
- [applyTheme, enableSystem, forcedTheme, theme]
202
- )
203
-
204
- // Always listen to System preference
205
- React.useEffect(() => {
206
- if (isServer) {
207
- return
208
- }
209
-
210
- const media = window.matchMedia(MEDIA)
211
- // Intentionally use deprecated listener methods to support iOS 13 and older browsers
212
- media.addEventListener("change", handleMediaQuery)
213
- handleMediaQuery(media)
214
- return () => media.removeEventListener("change", handleMediaQuery)
215
- }, [handleMediaQuery])
216
-
217
- // localStorage event handling, allow to sync theme changes between tabs
218
- React.useEffect(() => {
219
- if (isServer) {
220
- return
221
- }
222
-
223
- const handleStorage = (e: StorageEvent) => {
224
- if (e.key !== storageKey) {
225
- return
226
- }
227
- // If default theme set, use it if localstorage === null (happens on local storage manual deletion)
228
- const newTheme = e.newValue || defaultTheme
229
- setTheme(newTheme)
230
- }
231
-
232
- window.addEventListener("storage", handleStorage)
233
- return () => window.removeEventListener("storage", handleStorage)
234
- }, [defaultTheme, setTheme, storageKey])
235
-
236
- // Whenever theme or forcedTheme changes, apply it
237
- useEffect(() => {
238
- applyTheme(forcedTheme ?? theme)
239
- }, [applyTheme, forcedTheme, theme])
240
-
241
- const providerValue = useMemo(
242
- () => ({
243
- theme,
244
- setTheme,
245
- forcedTheme,
246
- themes: enableSystem ? [...themes, "system"] : themes,
247
- systemTheme: enableSystem
248
- ? (getSystemTheme() as "dark" | "light" | undefined)
249
- : undefined,
250
- }),
251
- [theme, forcedTheme, enableSystem, themes, setTheme]
252
- )
253
-
254
- return (
255
- <ThemeContext.Provider value={providerValue}>
256
- <ThemeScript
257
- {...{
258
- forcedTheme,
259
- storageKey,
260
- attribute,
261
- enableSystem,
262
- enableColorScheme,
263
- defaultTheme,
264
- value,
265
- themes,
266
- nonce,
267
- }}
268
- />
269
- {children}
270
- </ThemeContext.Provider>
271
- )
272
- }
126
+ const [theme, setThemeState] = useState(() =>
127
+ getTheme(storageKey, defaultTheme),
128
+ );
129
+
130
+ const applyClassAttribute = useCallback(
131
+ (name: string | undefined, attrValues: Array<string>) => {
132
+ const d = document.documentElement;
133
+ d.classList.remove(...attrValues);
134
+ if (name) {
135
+ d.classList.add(name);
136
+ }
137
+ },
138
+ [],
139
+ );
140
+
141
+ const applyDataAttribute = useCallback(
142
+ (attr: string, name: string | undefined) => {
143
+ const d = document.documentElement;
144
+ if (name) {
145
+ d.setAttribute(attr, name);
146
+ } else {
147
+ d.removeAttribute(attr);
148
+ }
149
+ },
150
+ [],
151
+ );
152
+
153
+ const applyAttributesToDOM = useCallback(
154
+ (resolved: string) => {
155
+ const attributeList = Array.isArray(attribute) ? attribute : [attribute];
156
+ const attrValues = value ? Object.values(value) : themes;
157
+ const name = value ? value[resolved] : resolved;
158
+
159
+ for (const attr of attributeList) {
160
+ if (attr === "class") {
161
+ applyClassAttribute(name, attrValues);
162
+ } else if (attr.startsWith("data-")) {
163
+ applyDataAttribute(attr, name);
164
+ }
165
+ }
166
+ },
167
+ [attribute, themes, value, applyClassAttribute, applyDataAttribute],
168
+ );
169
+
170
+ const applyColorScheme = useCallback(
171
+ (resolved: string) => {
172
+ if (!enableColorScheme) {
173
+ return;
174
+ }
175
+ const fallback = colorSchemes.includes(defaultTheme)
176
+ ? defaultTheme
177
+ : null;
178
+ const colorScheme = colorSchemes.includes(resolved) ? resolved : fallback;
179
+ document.documentElement.style.colorScheme = colorScheme || "";
180
+ },
181
+ [enableColorScheme, defaultTheme],
182
+ );
183
+
184
+ // apply selected theme function (light, dark, system)
185
+ const applyTheme = useCallback(
186
+ (nextTheme: string | undefined) => {
187
+ if (!nextTheme) {
188
+ return;
189
+ }
190
+ const resolved =
191
+ nextTheme === "system" && enableSystem ? getSystemTheme() : nextTheme;
192
+ const enable = disableTransitionOnChange ? disableAnimation() : null;
193
+ applyAttributesToDOM(resolved);
194
+ applyColorScheme(resolved);
195
+ enable?.();
196
+ },
197
+ [
198
+ enableSystem,
199
+ disableTransitionOnChange,
200
+ applyAttributesToDOM,
201
+ applyColorScheme,
202
+ ],
203
+ );
204
+
205
+ // Set theme state and save to local storage
206
+ const setTheme = useCallback(
207
+ (newValue: SetStateAction<string>) => {
208
+ const newTheme =
209
+ typeof newValue === "function" ? newValue(theme ?? "") : newValue;
210
+ setThemeState(newTheme);
211
+
212
+ // Save to storage
213
+ try {
214
+ localStorage.setItem(storageKey, newTheme);
215
+ } catch {
216
+ // localStorage might not be available
217
+ console.error("Failed to save theme to localStorage");
218
+ }
219
+ },
220
+ [theme, storageKey],
221
+ );
222
+
223
+ const handleMediaQuery = useCallback(
224
+ (_event: MediaQueryListEvent | MediaQueryList) => {
225
+ if (theme === "system" && enableSystem && !forcedTheme) {
226
+ applyTheme("system");
227
+ }
228
+ },
229
+ [applyTheme, enableSystem, forcedTheme, theme],
230
+ );
231
+
232
+ // Always listen to System preference
233
+ React.useEffect(() => {
234
+ if (isServer) {
235
+ return;
236
+ }
237
+
238
+ const media = window.matchMedia(MEDIA);
239
+ // Intentionally use deprecated listener methods to support iOS 13 and older browsers
240
+ media.addEventListener("change", handleMediaQuery);
241
+ handleMediaQuery(media);
242
+ return () => media.removeEventListener("change", handleMediaQuery);
243
+ }, [handleMediaQuery]);
244
+
245
+ // localStorage event handling, allow to sync theme changes between tabs
246
+ React.useEffect(() => {
247
+ if (isServer) {
248
+ return;
249
+ }
250
+
251
+ const handleStorage = (e: StorageEvent) => {
252
+ if (e.key !== storageKey) {
253
+ return;
254
+ }
255
+ // If default theme set, use it if localstorage === null (happens on local storage manual deletion)
256
+ const newTheme = e.newValue || defaultTheme;
257
+ setTheme(newTheme);
258
+ };
259
+
260
+ window.addEventListener("storage", handleStorage);
261
+ return () => window.removeEventListener("storage", handleStorage);
262
+ }, [defaultTheme, setTheme, storageKey]);
263
+
264
+ // Whenever theme or forcedTheme changes, apply it
265
+ useEffect(() => {
266
+ applyTheme(forcedTheme ?? theme);
267
+ }, [applyTheme, forcedTheme, theme]);
268
+
269
+ const providerValue = useMemo(
270
+ () => ({
271
+ theme,
272
+ setTheme,
273
+ forcedTheme,
274
+ themes: enableSystem ? [...themes, "system"] : themes,
275
+ systemTheme: enableSystem
276
+ ? (getSystemTheme() as "dark" | "light" | undefined)
277
+ : undefined,
278
+ }),
279
+ [theme, forcedTheme, enableSystem, themes, setTheme],
280
+ );
281
+
282
+ return (
283
+ <ThemeContext.Provider value={providerValue}>
284
+ <ThemeScript
285
+ {...{
286
+ forcedTheme,
287
+ storageKey,
288
+ attribute,
289
+ enableSystem,
290
+ enableColorScheme,
291
+ defaultTheme,
292
+ value,
293
+ themes,
294
+ nonce,
295
+ }}
296
+ />
297
+ {children}
298
+ </ThemeContext.Provider>
299
+ );
300
+ };
273
301
 
274
302
  const ThemeScript = memo(
275
- ({
276
- forcedTheme,
277
- storageKey,
278
- attribute,
279
- enableSystem,
280
- enableColorScheme,
281
- defaultTheme,
282
- value,
283
- themes,
284
- nonce,
285
- }: Omit<ThemeProviderProps, "children"> & {
286
- defaultTheme: string
287
- }) => {
288
- const scriptArgs = JSON.stringify([
289
- attribute,
290
- storageKey,
291
- defaultTheme,
292
- forcedTheme,
293
- themes,
294
- value,
295
- enableSystem,
296
- enableColorScheme,
297
- ]).slice(1, -1)
298
-
299
- return (
300
- <script
301
- // Run the theme bootstrap with serialized provider configuration before first paint.
302
- dangerouslySetInnerHTML={{
303
- __html: `(${script.toString()})(${scriptArgs})`,
304
- }}
305
- nonce={nonce}
306
- suppressHydrationWarning
307
- />
308
- )
309
- }
310
- )
311
-
312
- ThemeScript.displayName = "ThemeScript"
303
+ ({
304
+ forcedTheme,
305
+ storageKey,
306
+ attribute,
307
+ enableSystem,
308
+ enableColorScheme,
309
+ defaultTheme,
310
+ value,
311
+ themes,
312
+ nonce,
313
+ }: Omit<ThemeProviderProps, "children"> & {
314
+ defaultTheme: string;
315
+ }) => {
316
+ const scriptArgs = JSON.stringify([
317
+ attribute,
318
+ storageKey,
319
+ defaultTheme,
320
+ forcedTheme,
321
+ themes,
322
+ value,
323
+ enableSystem,
324
+ enableColorScheme,
325
+ ]).slice(1, -1);
326
+
327
+ return (
328
+ <script
329
+ // Run the theme bootstrap with serialized provider configuration before first paint.
330
+ dangerouslySetInnerHTML={{
331
+ __html: `(${script.toString()})(${scriptArgs})`,
332
+ }}
333
+ nonce={nonce}
334
+ suppressHydrationWarning
335
+ />
336
+ );
337
+ },
338
+ );
339
+
340
+ ThemeScript.displayName = "ThemeScript";
313
341
 
314
342
  // Helpers
315
343
  const getTheme = (key: string, fallback?: string) => {
316
- if (isServer) {
317
- return fallback
318
- }
319
- let theme: string | undefined
320
- try {
321
- theme = localStorage.getItem(key) || undefined
322
- } catch {
323
- // localStorage might not be available
324
- }
325
- return theme || fallback
326
- }
344
+ if (isServer) {
345
+ return fallback;
346
+ }
347
+ let theme: string | undefined;
348
+ try {
349
+ theme = localStorage.getItem(key) || undefined;
350
+ } catch {
351
+ // localStorage might not be available
352
+ }
353
+ return theme || fallback;
354
+ };
327
355
 
328
356
  const disableAnimation = () => {
329
- const css = document.createElement("style")
330
- css.appendChild(
331
- document.createTextNode(
332
- "*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}"
333
- )
334
- )
335
- document.head.appendChild(css)
336
-
337
- return () => {
338
- // Force restyle
339
- window.getComputedStyle(document.body)
340
- // Wait for next tick before removing
341
- setTimeout(() => {
342
- document.head.removeChild(css)
343
- }, 1)
344
- }
345
- }
357
+ const css = document.createElement("style");
358
+ css.appendChild(
359
+ document.createTextNode(
360
+ "*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}",
361
+ ),
362
+ );
363
+ document.head.appendChild(css);
364
+
365
+ return () => {
366
+ // Force restyle
367
+ window.getComputedStyle(document.body);
368
+ // Wait for next tick before removing
369
+ setTimeout(() => {
370
+ document.head.removeChild(css);
371
+ }, 1);
372
+ };
373
+ };
346
374
 
347
375
  const getSystemTheme = (e?: MediaQueryList | MediaQueryListEvent) => {
348
- if (isServer) {
349
- return "light"
350
- }
351
- const event = e ?? window.matchMedia(MEDIA)
352
- const isDark = event.matches
353
- const systemTheme = isDark ? "dark" : "light"
354
- return systemTheme
355
- }
376
+ if (isServer) {
377
+ return "light";
378
+ }
379
+ const event = e ?? window.matchMedia(MEDIA);
380
+ const isDark = event.matches;
381
+ const systemTheme = isDark ? "dark" : "light";
382
+ return systemTheme;
383
+ };
356
384
 
357
385
  /*
358
386
  This file is adapted from next-themes to work with tanstack start.
@@ -360,76 +388,80 @@ const getSystemTheme = (e?: MediaQueryList | MediaQueryListEvent) => {
360
388
  */
361
389
 
362
390
  export const script = (
363
- attribute: Attribute | Array<Attribute>,
364
- storageKey: string,
365
- defaultTheme: string,
366
- forcedTheme: string | undefined,
367
- themes: Array<string>,
368
- value: ValueObject | undefined,
369
- enableSystem: boolean,
370
- enableColorScheme: boolean
391
+ attribute: Attribute | Array<Attribute>,
392
+ storageKey: string,
393
+ defaultTheme: string,
394
+ forcedTheme: string | undefined,
395
+ themes: Array<string>,
396
+ value: ValueObject | undefined,
397
+ enableSystem: boolean,
398
+ enableColorScheme: boolean,
371
399
  ) => {
372
- const el = document.documentElement
373
- const systemThemes = ["light", "dark"]
374
- const attributes = Array.isArray(attribute) ? attribute : [attribute]
375
- const attrValues = value ? Object.values(value) : themes
376
-
377
- function applyClassAttr(name: string | undefined) {
378
- el.classList.remove(...attrValues)
379
- if (name) {
380
- el.classList.add(name)
381
- }
382
- }
383
-
384
- function applyDataAttr(attr: string, name: string | undefined) {
385
- if (name) {
386
- el.setAttribute(attr, name)
387
- } else {
388
- el.removeAttribute(attr)
389
- }
390
- }
391
-
392
- function updateDOM(theme: string) {
393
- const name = value ? value[theme] : theme
394
-
395
- for (const attr of attributes) {
396
- if (attr === "class") {
397
- applyClassAttr(name)
398
- } else if (attr.startsWith("data-")) {
399
- applyDataAttr(attr, name)
400
- }
401
- }
402
-
403
- setColorScheme(theme)
404
- }
405
-
406
- function setColorScheme(theme: string) {
407
- if (!enableColorScheme) {
408
- return
409
- }
410
-
411
- const fallback = systemThemes.includes(defaultTheme) ? defaultTheme : null
412
- const colorScheme = systemThemes.includes(theme) ? theme : fallback
413
- el.style.colorScheme = colorScheme || ""
414
- }
415
-
416
- function resolveSystemTheme() {
417
- return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
418
- }
419
-
420
- if (forcedTheme) {
421
- const resolvedForcedTheme =
422
- forcedTheme === "system" && enableSystem ? resolveSystemTheme() : forcedTheme
423
- updateDOM(resolvedForcedTheme)
424
- } else {
425
- try {
426
- const themeName = localStorage.getItem(storageKey) || defaultTheme
427
- const isSystem = enableSystem && themeName === "system"
428
- const theme = isSystem ? resolveSystemTheme() : themeName
429
- updateDOM(theme)
430
- } catch {
431
- // localStorage might not be available
432
- updateDOM(defaultTheme)
433
- }
434
- }
435
- }
400
+ const el = document.documentElement;
401
+ const systemThemes = ["light", "dark"];
402
+ const attributes = Array.isArray(attribute) ? attribute : [attribute];
403
+ const attrValues = value ? Object.values(value) : themes;
404
+
405
+ function applyClassAttr(name: string | undefined) {
406
+ el.classList.remove(...attrValues);
407
+ if (name) {
408
+ el.classList.add(name);
409
+ }
410
+ }
411
+
412
+ function applyDataAttr(attr: string, name: string | undefined) {
413
+ if (name) {
414
+ el.setAttribute(attr, name);
415
+ } else {
416
+ el.removeAttribute(attr);
417
+ }
418
+ }
419
+
420
+ function updateDOM(theme: string) {
421
+ const name = value ? value[theme] : theme;
422
+
423
+ for (const attr of attributes) {
424
+ if (attr === "class") {
425
+ applyClassAttr(name);
426
+ } else if (attr.startsWith("data-")) {
427
+ applyDataAttr(attr, name);
428
+ }
429
+ }
430
+
431
+ setColorScheme(theme);
432
+ }
433
+
434
+ function setColorScheme(theme: string) {
435
+ if (!enableColorScheme) {
436
+ return;
437
+ }
438
+
439
+ const fallback = systemThemes.includes(defaultTheme) ? defaultTheme : null;
440
+ const colorScheme = systemThemes.includes(theme) ? theme : fallback;
441
+ el.style.colorScheme = colorScheme || "";
442
+ }
443
+
444
+ function resolveSystemTheme() {
445
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
446
+ ? "dark"
447
+ : "light";
448
+ }
449
+
450
+ if (forcedTheme) {
451
+ const resolvedForcedTheme =
452
+ forcedTheme === "system" && enableSystem
453
+ ? resolveSystemTheme()
454
+ : forcedTheme;
455
+ updateDOM(resolvedForcedTheme);
456
+ } else {
457
+ try {
458
+ const themeName = localStorage.getItem(storageKey) || defaultTheme;
459
+ const isSystem = enableSystem && themeName === "system";
460
+ const theme = isSystem ? resolveSystemTheme() : themeName;
461
+ updateDOM(theme);
462
+ } catch {
463
+ // localStorage might not be available
464
+ updateDOM(defaultTheme);
465
+ }
466
+ }
467
+ };