@thunderid/react 1.9.0 → 1.9.1
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.
- package/dist/cjs/contexts/Theme/ThemeProvider.cjs +36 -6
- package/dist/cjs/contexts/Theme/ThemeProvider.cjs.map +1 -1
- package/dist/cjs/contexts/Theme/ThemeRootContext.cjs +12 -0
- package/dist/cjs/contexts/Theme/ThemeRootContext.cjs.map +1 -0
- package/dist/contexts/Theme/ThemeProvider.d.ts +2 -1
- package/dist/contexts/Theme/ThemeProvider.d.ts.map +1 -1
- package/dist/contexts/Theme/ThemeProvider.js +37 -7
- package/dist/contexts/Theme/ThemeProvider.js.map +1 -1
- package/dist/contexts/Theme/ThemeRootContext.d.ts +11 -0
- package/dist/contexts/Theme/ThemeRootContext.d.ts.map +1 -0
- package/dist/contexts/Theme/ThemeRootContext.js +10 -0
- package/dist/contexts/Theme/ThemeRootContext.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const require_rolldown_runtime = require('../../_virtual/rolldown_runtime.cjs');
|
|
2
2
|
const require_FlowMetaContext = require('../FlowMeta/FlowMetaContext.cjs');
|
|
3
3
|
const require_ThemeContext = require('./ThemeContext.cjs');
|
|
4
|
+
const require_ThemeRootContext = require('./ThemeRootContext.cjs');
|
|
4
5
|
const require_applyThemeToDOM = require('../../utils/applyThemeToDOM.cjs');
|
|
5
6
|
const require_buildThemeConfigFromFlowMeta = require('../../utils/buildThemeConfigFromFlowMeta.cjs');
|
|
6
7
|
const require_normalizeThemeConfig = require('../../utils/normalizeThemeConfig.cjs');
|
|
@@ -53,6 +54,11 @@ const ThemeProvider = ({ children, designTheme = void 0, mode, theme: themeOverr
|
|
|
53
54
|
return flowMetaTheme?.defaultColorScheme ?? "light";
|
|
54
55
|
};
|
|
55
56
|
const [colorScheme, setColorScheme] = (0, react.useState)(getInitialColorScheme);
|
|
57
|
+
const [seededMode, setSeededMode] = (0, react.useState)(mode);
|
|
58
|
+
if (mode !== seededMode) {
|
|
59
|
+
setSeededMode(mode);
|
|
60
|
+
if (mode !== void 0) setColorScheme(getInitialColorScheme());
|
|
61
|
+
}
|
|
56
62
|
(0, react.useEffect)(() => {
|
|
57
63
|
if (mode === void 0 && flowMetaTheme?.defaultColorScheme) setColorScheme(flowMetaTheme.defaultColorScheme);
|
|
58
64
|
}, [mode, flowMetaTheme?.defaultColorScheme]);
|
|
@@ -103,12 +109,33 @@ const ThemeProvider = ({ children, designTheme = void 0, mode, theme: themeOverr
|
|
|
103
109
|
]);
|
|
104
110
|
const theme = (0, react.useMemo)(() => (0, __thunderid_browser.createTheme)(finalThemeConfig, colorScheme === "dark"), [finalThemeConfig, colorScheme]);
|
|
105
111
|
const direction = flowMetaTheme?.direction ?? "ltr";
|
|
112
|
+
const parentRoot = (0, react.useContext)(require_ThemeRootContext.default);
|
|
113
|
+
const claims = (0, react.useRef)(0);
|
|
114
|
+
const applied = (0, react.useRef)(null);
|
|
115
|
+
const applyToRoot = (0, react.useCallback)(() => {
|
|
116
|
+
if (!applied.current || typeof document === "undefined") return;
|
|
117
|
+
require_applyThemeToDOM.default(applied.current.theme);
|
|
118
|
+
document.documentElement.dir = applied.current.direction;
|
|
119
|
+
}, []);
|
|
120
|
+
const root = (0, react.useMemo)(() => ({ claim: () => {
|
|
121
|
+
claims.current += 1;
|
|
122
|
+
return () => {
|
|
123
|
+
claims.current -= 1;
|
|
124
|
+
if (claims.current === 0) applyToRoot();
|
|
125
|
+
};
|
|
126
|
+
} }), [applyToRoot]);
|
|
127
|
+
(0, react.useEffect)(() => parentRoot?.claim(), [parentRoot]);
|
|
106
128
|
(0, react.useEffect)(() => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
applied.current = {
|
|
130
|
+
direction,
|
|
131
|
+
theme
|
|
132
|
+
};
|
|
133
|
+
if (claims.current === 0) applyToRoot();
|
|
134
|
+
}, [
|
|
135
|
+
applyToRoot,
|
|
136
|
+
direction,
|
|
137
|
+
theme
|
|
138
|
+
]);
|
|
112
139
|
const value = {
|
|
113
140
|
colorScheme,
|
|
114
141
|
direction,
|
|
@@ -117,7 +144,10 @@ const ThemeProvider = ({ children, designTheme = void 0, mode, theme: themeOverr
|
|
|
117
144
|
};
|
|
118
145
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_ThemeContext.default.Provider, {
|
|
119
146
|
value,
|
|
120
|
-
children
|
|
147
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_ThemeRootContext.default.Provider, {
|
|
148
|
+
value: root,
|
|
149
|
+
children
|
|
150
|
+
})
|
|
121
151
|
});
|
|
122
152
|
};
|
|
123
153
|
var ThemeProvider_default = ThemeProvider;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemeProvider.cjs","names":["ThemeProvider: FC<PropsWithChildren<ThemeProviderProps>>","themeOverride: RecursivePartial<ThemeConfig> | undefined","normalizeThemeConfig","flowMetaContext: FlowMetaContextValue | null","FlowMetaContext","flowMetaTheme: FlowMetaTheme | null","toggleTheme: () => void","finalThemeConfig: RecursivePartial<ThemeConfig> | undefined","metaConfig: RecursivePartial<ThemeConfig>","buildThemeConfigFromFlowMeta","theme: Theme","direction: 'ltr' | 'rtl'","value: any","ThemeContext"],"sources":["../../../../src/contexts/Theme/ThemeProvider.tsx"],"sourcesContent":["// Copyright 2026 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {\n createClassObserver,\n createMediaQueryListener,\n createTheme,\n detectThemeMode,\n FlowMetaTheme,\n RecursivePartial,\n Theme,\n ThemeConfig,\n ThemeMode,\n} from '@thunderid/browser';\nimport {FC, PropsWithChildren, ReactElement, useCallback, useContext, useEffect, useMemo, useState} from 'react';\nimport ThemeContext from './ThemeContext';\nimport applyThemeToDOM from '../../utils/applyThemeToDOM';\nimport buildThemeConfigFromFlowMeta from '../../utils/buildThemeConfigFromFlowMeta';\nimport normalizeThemeConfig from '../../utils/normalizeThemeConfig';\nimport FlowMetaContext, {FlowMetaContextValue} from '../FlowMeta/FlowMetaContext';\n\nexport interface ThemeProviderProps {\n /**\n * A Design theme to use instead of the one the server returns in flow meta, for example the theme an\n * app configuration names. `theme` overrides are still applied on top.\n */\n designTheme?: FlowMetaTheme | null;\n /**\n * Initial color scheme override. Overrides the server default when provided.\n */\n mode?: ThemeMode;\n\n /**\n * Optional theme overrides merged on top of the server-side flow meta theme.\n * User-supplied values take highest precedence.\n */\n theme?: RecursivePartial<ThemeConfig>;\n}\n\n/**\n * ThemeProvider is the v2 drop-in replacement for `ThemeProvider`.\n *\n * It reads the design theme from the nearest `FlowMetaContext` (provided by\n * `FlowMetaProvider`) and publishes a resolved `Theme` object through the\n * **same** `ThemeContext` that `useTheme` consumes. This means all existing\n * components that call `useTheme` continue to work without any changes.\n *\n * The `defaultColorScheme` field returned by the server is used to seed the\n * active color scheme; the user can still toggle it locally via the\n * `toggleTheme` value exposed in the context.\n *\n * @example\n * ```tsx\n * <FlowMetaProvider config={{ baseUrl, type: FlowMetaType.App, id: appId }}>\n * <ThemeProvider>\n * <App /> {/* useTheme() works here as usual *\\/}\n * </ThemeProvider>\n * </FlowMetaProvider>\n * ```\n *\n * @example\n * With user theme overrides (user values win over server values):\n * ```tsx\n * <ThemeProvider theme={{ colors: { primary: { main: '#ff0000' } } }}>\n * <App />\n * </ThemeProvider>\n * ```\n */\nconst ThemeProvider: FC<PropsWithChildren<ThemeProviderProps>> = ({\n children,\n designTheme = undefined,\n mode,\n theme: themeOverrideProp,\n}: PropsWithChildren<ThemeProviderProps>): ReactElement => {\n const themeOverride: RecursivePartial<ThemeConfig> | undefined = normalizeThemeConfig(themeOverrideProp);\n const flowMetaContext: FlowMetaContextValue | null = useContext(FlowMetaContext);\n\n const flowMetaTheme: FlowMetaTheme | null = designTheme ?? flowMetaContext?.meta?.design?.theme ?? null;\n const isLoading: boolean = flowMetaContext?.isLoading ?? false;\n const error: Error | null = flowMetaContext?.error ?? null;\n\n // Seed the color scheme: a fixed mode wins outright, 'system'/'class' detect it from the\n // environment via the same shared utilities Vue's ThemeProvider uses, and no mode falls back\n // to the server's default (unchanged pre-`mode` behavior).\n const getInitialColorScheme = (): 'light' | 'dark' => {\n if (mode === 'light' || mode === 'dark') return mode;\n if (mode === 'system' || mode === 'class') return detectThemeMode(mode);\n return flowMetaTheme?.defaultColorScheme ?? 'light';\n };\n\n const [colorScheme, setColorScheme] = useState<'light' | 'dark'>(getInitialColorScheme);\n\n // When mode is unset, sync the color scheme with the server default once flow meta loads.\n useEffect(() => {\n if (mode === undefined && flowMetaTheme?.defaultColorScheme) {\n setColorScheme(flowMetaTheme.defaultColorScheme);\n }\n }, [mode, flowMetaTheme?.defaultColorScheme]);\n\n // Set up automatic detection listeners for 'system'/'class' modes.\n useEffect(() => {\n if (mode !== 'system' && mode !== 'class') return undefined;\n\n const handleThemeChange = (isDark: boolean): void => {\n setColorScheme(isDark ? 'dark' : 'light');\n };\n\n if (mode === 'class') {\n if (typeof document === 'undefined') return undefined;\n const observer = createClassObserver(document.documentElement, handleThemeChange);\n return () => {\n observer.disconnect();\n };\n }\n\n const mediaQuery = createMediaQueryListener(handleThemeChange);\n return () => {\n mediaQuery?.disconnect();\n };\n }, [mode]);\n\n const toggleTheme: () => void = useCallback(() => {\n setColorScheme((prev: 'light' | 'dark') => (prev === 'light' ? 'dark' : 'light'));\n }, []);\n\n // Build the resolved ThemeConfig: flow meta base → user overrides on top.\n const finalThemeConfig: RecursivePartial<ThemeConfig> | undefined = useMemo(() => {\n if (!flowMetaTheme) {\n return themeOverride;\n }\n\n const metaConfig: RecursivePartial<ThemeConfig> = buildThemeConfigFromFlowMeta(flowMetaTheme, colorScheme);\n\n if (!themeOverride) {\n return metaConfig;\n }\n\n return {\n ...metaConfig,\n ...themeOverride,\n borderRadius: {\n ...metaConfig.borderRadius,\n ...themeOverride.borderRadius,\n },\n colors: {\n ...metaConfig.colors,\n ...themeOverride.colors,\n },\n ...(metaConfig.typography || themeOverride.typography\n ? {\n typography: {\n ...(metaConfig as any).typography,\n ...themeOverride.typography,\n },\n }\n : {}),\n };\n }, [flowMetaTheme, colorScheme, themeOverride]);\n\n const theme: Theme = useMemo(\n () => createTheme(finalThemeConfig, colorScheme === 'dark'),\n [finalThemeConfig, colorScheme],\n );\n\n const direction: 'ltr' | 'rtl' = flowMetaTheme?.direction ?? 'ltr';\n\n // Apply CSS variables to the document root.\n useEffect(() => {\n applyThemeToDOM(theme);\n }, [theme]);\n\n // Apply text direction to the document root.\n useEffect(() => {\n if (typeof document !== 'undefined') {\n document.documentElement.dir = direction;\n }\n }, [direction]);\n\n const value: any = {\n colorScheme,\n direction,\n theme,\n toggleTheme,\n };\n\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;\n};\n\nexport default ThemeProvider;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,MAAMA,iBAA4D,EAChE,UACA,cAAc,QACd,MACA,OAAO,wBACkD;CACzD,MAAMC,gBAA2DC,qCAAqB,kBAAkB;CACxG,MAAMC,wCAA0DC,gCAAgB;CAEhF,MAAMC,gBAAsC,eAAe,iBAAiB,MAAM,QAAQ,SAAS;AACxE,kBAAiB;AAChB,kBAAiB;CAK7C,MAAM,8BAAgD;AACpD,MAAI,SAAS,WAAW,SAAS,OAAQ,QAAO;AAChD,MAAI,SAAS,YAAY,SAAS,QAAS,iDAAuB,KAAK;AACvE,SAAO,eAAe,sBAAsB;;CAG9C,MAAM,CAAC,aAAa,sCAA6C,sBAAsB;AAGvF,4BAAgB;AACd,MAAI,SAAS,UAAa,eAAe,mBACvC,gBAAe,cAAc,mBAAmB;IAEjD,CAAC,MAAM,eAAe,mBAAmB,CAAC;AAG7C,4BAAgB;AACd,MAAI,SAAS,YAAY,SAAS,QAAS,QAAO;EAElD,MAAM,qBAAqB,WAA0B;AACnD,kBAAe,SAAS,SAAS,QAAQ;;AAG3C,MAAI,SAAS,SAAS;AACpB,OAAI,OAAO,aAAa,YAAa,QAAO;GAC5C,MAAM,wDAA+B,SAAS,iBAAiB,kBAAkB;AACjF,gBAAa;AACX,aAAS,YAAY;;;EAIzB,MAAM,+DAAsC,kBAAkB;AAC9D,eAAa;AACX,eAAY,YAAY;;IAEzB,CAAC,KAAK,CAAC;CAEV,MAAMC,2CAA4C;AAChD,kBAAgB,SAA4B,SAAS,UAAU,SAAS,QAAS;IAChF,EAAE,CAAC;CAGN,MAAMC,4CAA4E;AAChF,MAAI,CAAC,cACH,QAAO;EAGT,MAAMC,aAA4CC,6CAA6B,eAAe,YAAY;AAE1G,MAAI,CAAC,cACH,QAAO;AAGT,SAAO;GACL,GAAG;GACH,GAAG;GACH,cAAc;IACZ,GAAG,WAAW;IACd,GAAG,cAAc;IAClB;GACD,QAAQ;IACN,GAAG,WAAW;IACd,GAAG,cAAc;IAClB;GACD,GAAI,WAAW,cAAc,cAAc,aACvC,EACE,YAAY;IACV,GAAI,WAAmB;IACvB,GAAG,cAAc;IAClB,EACF,GACD,EAAE;GACP;IACA;EAAC;EAAe;EAAa;EAAc,CAAC;CAE/C,MAAMC,sEACc,kBAAkB,gBAAgB,OAAO,EAC3D,CAAC,kBAAkB,YAAY,CAChC;CAED,MAAMC,YAA2B,eAAe,aAAa;AAG7D,4BAAgB;AACd,kCAAgB,MAAM;IACrB,CAAC,MAAM,CAAC;AAGX,4BAAgB;AACd,MAAI,OAAO,aAAa,YACtB,UAAS,gBAAgB,MAAM;IAEhC,CAAC,UAAU,CAAC;CAEf,MAAMC,QAAa;EACjB;EACA;EACA;EACA;EACD;AAED,QAAO,2CAACC,6BAAa;EAAgB;EAAQ;GAAiC;;AAGhF,4BAAe"}
|
|
1
|
+
{"version":3,"file":"ThemeProvider.cjs","names":["ThemeProvider: FC<PropsWithChildren<ThemeProviderProps>>","themeOverride: RecursivePartial<ThemeConfig> | undefined","normalizeThemeConfig","flowMetaContext: FlowMetaContextValue | null","FlowMetaContext","flowMetaTheme: FlowMetaTheme | null","toggleTheme: () => void","finalThemeConfig: RecursivePartial<ThemeConfig> | undefined","metaConfig: RecursivePartial<ThemeConfig>","buildThemeConfigFromFlowMeta","theme: Theme","direction: 'ltr' | 'rtl'","parentRoot: ThemeRootContextValue | null","ThemeRootContext","claims: MutableRefObject<number>","applied: MutableRefObject<{direction: 'ltr' | 'rtl'; theme: Theme} | null>","applyToRoot: () => void","root: ThemeRootContextValue","value: any","ThemeContext"],"sources":["../../../../src/contexts/Theme/ThemeProvider.tsx"],"sourcesContent":["// Copyright 2026 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {\n createClassObserver,\n createMediaQueryListener,\n createTheme,\n detectThemeMode,\n FlowMetaTheme,\n RecursivePartial,\n Theme,\n ThemeConfig,\n ThemeMode,\n} from '@thunderid/browser';\nimport {\n FC,\n MutableRefObject,\n PropsWithChildren,\n ReactElement,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport ThemeContext from './ThemeContext';\nimport ThemeRootContext, {ThemeRootContextValue} from './ThemeRootContext';\nimport applyThemeToDOM from '../../utils/applyThemeToDOM';\nimport buildThemeConfigFromFlowMeta from '../../utils/buildThemeConfigFromFlowMeta';\nimport normalizeThemeConfig from '../../utils/normalizeThemeConfig';\nimport FlowMetaContext, {FlowMetaContextValue} from '../FlowMeta/FlowMetaContext';\n\nexport interface ThemeProviderProps {\n /**\n * A Design theme to use instead of the one the server returns in flow meta, for example the theme an\n * app configuration names. `theme` overrides are still applied on top.\n */\n designTheme?: FlowMetaTheme | null;\n /**\n * Color scheme override. Overrides the server default when provided, and the color scheme follows it\n * when it changes.\n */\n mode?: ThemeMode;\n\n /**\n * Optional theme overrides merged on top of the server-side flow meta theme.\n * User-supplied values take highest precedence.\n */\n theme?: RecursivePartial<ThemeConfig>;\n}\n\n/**\n * ThemeProvider is the v2 drop-in replacement for `ThemeProvider`.\n *\n * It reads the design theme from the nearest `FlowMetaContext` (provided by\n * `FlowMetaProvider`) and publishes a resolved `Theme` object through the\n * **same** `ThemeContext` that `useTheme` consumes. This means all existing\n * components that call `useTheme` continue to work without any changes.\n *\n * The `defaultColorScheme` field returned by the server is used to seed the\n * active color scheme; the user can still toggle it locally via the\n * `toggleTheme` value exposed in the context.\n *\n * @example\n * ```tsx\n * <FlowMetaProvider config={{ baseUrl, type: FlowMetaType.App, id: appId }}>\n * <ThemeProvider>\n * <App /> {/* useTheme() works here as usual *\\/}\n * </ThemeProvider>\n * </FlowMetaProvider>\n * ```\n *\n * @example\n * With user theme overrides (user values win over server values):\n * ```tsx\n * <ThemeProvider theme={{ colors: { primary: { main: '#ff0000' } } }}>\n * <App />\n * </ThemeProvider>\n * ```\n */\nconst ThemeProvider: FC<PropsWithChildren<ThemeProviderProps>> = ({\n children,\n designTheme = undefined,\n mode,\n theme: themeOverrideProp,\n}: PropsWithChildren<ThemeProviderProps>): ReactElement => {\n const themeOverride: RecursivePartial<ThemeConfig> | undefined = normalizeThemeConfig(themeOverrideProp);\n const flowMetaContext: FlowMetaContextValue | null = useContext(FlowMetaContext);\n\n const flowMetaTheme: FlowMetaTheme | null = designTheme ?? flowMetaContext?.meta?.design?.theme ?? null;\n const isLoading: boolean = flowMetaContext?.isLoading ?? false;\n const error: Error | null = flowMetaContext?.error ?? null;\n\n // Seed the color scheme: a fixed mode wins outright, 'system'/'class' detect it from the\n // environment via the same shared utilities Vue's ThemeProvider uses, and no mode falls back\n // to the server's default (unchanged pre-`mode` behavior).\n const getInitialColorScheme = (): 'light' | 'dark' => {\n if (mode === 'light' || mode === 'dark') return mode;\n if (mode === 'system' || mode === 'class') return detectThemeMode(mode);\n return flowMetaTheme?.defaultColorScheme ?? 'light';\n };\n\n const [colorScheme, setColorScheme] = useState<'light' | 'dark'>(getInitialColorScheme);\n\n // Follow a change of mode after mount, such as a host app's own color scheme toggle.\n const [seededMode, setSeededMode] = useState<ThemeMode | undefined>(mode);\n\n if (mode !== seededMode) {\n setSeededMode(mode);\n if (mode !== undefined) {\n setColorScheme(getInitialColorScheme());\n }\n }\n\n // When mode is unset, sync the color scheme with the server default once flow meta loads.\n useEffect(() => {\n if (mode === undefined && flowMetaTheme?.defaultColorScheme) {\n setColorScheme(flowMetaTheme.defaultColorScheme);\n }\n }, [mode, flowMetaTheme?.defaultColorScheme]);\n\n // Set up automatic detection listeners for 'system'/'class' modes.\n useEffect(() => {\n if (mode !== 'system' && mode !== 'class') return undefined;\n\n const handleThemeChange = (isDark: boolean): void => {\n setColorScheme(isDark ? 'dark' : 'light');\n };\n\n if (mode === 'class') {\n if (typeof document === 'undefined') return undefined;\n const observer = createClassObserver(document.documentElement, handleThemeChange);\n return () => {\n observer.disconnect();\n };\n }\n\n const mediaQuery = createMediaQueryListener(handleThemeChange);\n return () => {\n mediaQuery?.disconnect();\n };\n }, [mode]);\n\n const toggleTheme: () => void = useCallback(() => {\n setColorScheme((prev: 'light' | 'dark') => (prev === 'light' ? 'dark' : 'light'));\n }, []);\n\n // Build the resolved ThemeConfig: flow meta base → user overrides on top.\n const finalThemeConfig: RecursivePartial<ThemeConfig> | undefined = useMemo(() => {\n if (!flowMetaTheme) {\n return themeOverride;\n }\n\n const metaConfig: RecursivePartial<ThemeConfig> = buildThemeConfigFromFlowMeta(flowMetaTheme, colorScheme);\n\n if (!themeOverride) {\n return metaConfig;\n }\n\n return {\n ...metaConfig,\n ...themeOverride,\n borderRadius: {\n ...metaConfig.borderRadius,\n ...themeOverride.borderRadius,\n },\n colors: {\n ...metaConfig.colors,\n ...themeOverride.colors,\n },\n ...(metaConfig.typography || themeOverride.typography\n ? {\n typography: {\n ...(metaConfig as any).typography,\n ...themeOverride.typography,\n },\n }\n : {}),\n };\n }, [flowMetaTheme, colorScheme, themeOverride]);\n\n const theme: Theme = useMemo(\n () => createTheme(finalThemeConfig, colorScheme === 'dark'),\n [finalThemeConfig, colorScheme],\n );\n\n const direction: 'ltr' | 'rtl' = flowMetaTheme?.direction ?? 'ltr';\n\n // The innermost ThemeProvider owns the document root: an outer one stops writing its CSS variables\n // and text direction while a nested one is mounted, and writes them again once it unmounts.\n const parentRoot: ThemeRootContextValue | null = useContext(ThemeRootContext);\n const claims: MutableRefObject<number> = useRef<number>(0);\n const applied: MutableRefObject<{direction: 'ltr' | 'rtl'; theme: Theme} | null> = useRef(null);\n\n const applyToRoot: () => void = useCallback(() => {\n if (!applied.current || typeof document === 'undefined') return;\n applyThemeToDOM(applied.current.theme);\n document.documentElement.dir = applied.current.direction;\n }, []);\n\n const root: ThemeRootContextValue = useMemo(\n () => ({\n claim: (): (() => void) => {\n claims.current += 1;\n return (): void => {\n claims.current -= 1;\n if (claims.current === 0) applyToRoot();\n };\n },\n }),\n [applyToRoot],\n );\n\n useEffect(() => parentRoot?.claim(), [parentRoot]);\n\n useEffect(() => {\n applied.current = {direction, theme};\n if (claims.current === 0) applyToRoot();\n }, [applyToRoot, direction, theme]);\n\n const value: any = {\n colorScheme,\n direction,\n theme,\n toggleTheme,\n };\n\n return (\n <ThemeContext.Provider value={value}>\n <ThemeRootContext.Provider value={root}>{children}</ThemeRootContext.Provider>\n </ThemeContext.Provider>\n );\n};\n\nexport default ThemeProvider;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('../../_virtual/rolldown_runtime.cjs');
|
|
2
|
+
let react = require("react");
|
|
3
|
+
react = require_rolldown_runtime.__toESM(react);
|
|
4
|
+
|
|
5
|
+
//#region src/contexts/Theme/ThemeRootContext.ts
|
|
6
|
+
const ThemeRootContext = (0, react.createContext)(null);
|
|
7
|
+
ThemeRootContext.displayName = "ThemeRootContext";
|
|
8
|
+
var ThemeRootContext_default = ThemeRootContext;
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
exports.default = ThemeRootContext_default;
|
|
12
|
+
//# sourceMappingURL=ThemeRootContext.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThemeRootContext.cjs","names":["ThemeRootContext: Context<ThemeRootContextValue | null>"],"sources":["../../../../src/contexts/Theme/ThemeRootContext.ts"],"sourcesContent":["// Copyright 2026 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {Context, createContext} from 'react';\n\nexport interface ThemeRootContextValue {\n /**\n * Hands the document root to a nested `ThemeProvider` until the returned release is called, so the\n * innermost provider's CSS variables and text direction are the ones on the page.\n */\n claim: () => () => void;\n}\n\nconst ThemeRootContext: Context<ThemeRootContextValue | null> = createContext<ThemeRootContextValue | null>(null);\n\nThemeRootContext.displayName = 'ThemeRootContext';\n\nexport default ThemeRootContext;\n"],"mappings":";;;;;AAaA,MAAMA,4CAAsG,KAAK;AAEjH,iBAAiB,cAAc;AAE/B,+BAAe"}
|
|
@@ -7,7 +7,8 @@ export interface ThemeProviderProps {
|
|
|
7
7
|
*/
|
|
8
8
|
designTheme?: FlowMetaTheme | null;
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Color scheme override. Overrides the server default when provided, and the color scheme follows it
|
|
11
|
+
* when it changes.
|
|
11
12
|
*/
|
|
12
13
|
mode?: ThemeMode;
|
|
13
14
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemeProvider.d.ts","sourceRoot":"","sources":["../../../src/contexts/Theme/ThemeProvider.tsx"],"names":[],"mappings":"AAGA,OAAO,EAKL,aAAa,EACb,gBAAgB,EAEhB,WAAW,EACX,SAAS,EACV,MAAM,oBAAoB,CAAC;AAC5B,OAAO,
|
|
1
|
+
{"version":3,"file":"ThemeProvider.d.ts","sourceRoot":"","sources":["../../../src/contexts/Theme/ThemeProvider.tsx"],"names":[],"mappings":"AAGA,OAAO,EAKL,aAAa,EACb,gBAAgB,EAEhB,WAAW,EACX,SAAS,EACV,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,EAAE,EAEF,iBAAiB,EAQlB,MAAM,OAAO,CAAC;AAQf,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,WAAW,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACnC;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IAEjB;;;OAGG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,QAAA,MAAM,aAAa,EAAE,EAAE,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,CAwJ5D,CAAC;AAEF,eAAe,aAAa,CAAC"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import FlowMetaContext_default from "../FlowMeta/FlowMetaContext.js";
|
|
2
2
|
import ThemeContext_default from "./ThemeContext.js";
|
|
3
|
+
import ThemeRootContext_default from "./ThemeRootContext.js";
|
|
3
4
|
import applyThemeToDOM_default from "../../utils/applyThemeToDOM.js";
|
|
4
5
|
import buildThemeConfigFromFlowMeta_default from "../../utils/buildThemeConfigFromFlowMeta.js";
|
|
5
6
|
import normalizeThemeConfig_default from "../../utils/normalizeThemeConfig.js";
|
|
6
7
|
import { createClassObserver, createMediaQueryListener, createTheme, detectThemeMode } from "@thunderid/browser";
|
|
7
|
-
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
8
|
+
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
8
9
|
import { jsx } from "react/jsx-runtime";
|
|
9
10
|
|
|
10
11
|
//#region src/contexts/Theme/ThemeProvider.tsx
|
|
@@ -49,6 +50,11 @@ const ThemeProvider = ({ children, designTheme = void 0, mode, theme: themeOverr
|
|
|
49
50
|
return flowMetaTheme?.defaultColorScheme ?? "light";
|
|
50
51
|
};
|
|
51
52
|
const [colorScheme, setColorScheme] = useState(getInitialColorScheme);
|
|
53
|
+
const [seededMode, setSeededMode] = useState(mode);
|
|
54
|
+
if (mode !== seededMode) {
|
|
55
|
+
setSeededMode(mode);
|
|
56
|
+
if (mode !== void 0) setColorScheme(getInitialColorScheme());
|
|
57
|
+
}
|
|
52
58
|
useEffect(() => {
|
|
53
59
|
if (mode === void 0 && flowMetaTheme?.defaultColorScheme) setColorScheme(flowMetaTheme.defaultColorScheme);
|
|
54
60
|
}, [mode, flowMetaTheme?.defaultColorScheme]);
|
|
@@ -99,12 +105,33 @@ const ThemeProvider = ({ children, designTheme = void 0, mode, theme: themeOverr
|
|
|
99
105
|
]);
|
|
100
106
|
const theme = useMemo(() => createTheme(finalThemeConfig, colorScheme === "dark"), [finalThemeConfig, colorScheme]);
|
|
101
107
|
const direction = flowMetaTheme?.direction ?? "ltr";
|
|
108
|
+
const parentRoot = useContext(ThemeRootContext_default);
|
|
109
|
+
const claims = useRef(0);
|
|
110
|
+
const applied = useRef(null);
|
|
111
|
+
const applyToRoot = useCallback(() => {
|
|
112
|
+
if (!applied.current || typeof document === "undefined") return;
|
|
113
|
+
applyThemeToDOM_default(applied.current.theme);
|
|
114
|
+
document.documentElement.dir = applied.current.direction;
|
|
115
|
+
}, []);
|
|
116
|
+
const root = useMemo(() => ({ claim: () => {
|
|
117
|
+
claims.current += 1;
|
|
118
|
+
return () => {
|
|
119
|
+
claims.current -= 1;
|
|
120
|
+
if (claims.current === 0) applyToRoot();
|
|
121
|
+
};
|
|
122
|
+
} }), [applyToRoot]);
|
|
123
|
+
useEffect(() => parentRoot?.claim(), [parentRoot]);
|
|
102
124
|
useEffect(() => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
125
|
+
applied.current = {
|
|
126
|
+
direction,
|
|
127
|
+
theme
|
|
128
|
+
};
|
|
129
|
+
if (claims.current === 0) applyToRoot();
|
|
130
|
+
}, [
|
|
131
|
+
applyToRoot,
|
|
132
|
+
direction,
|
|
133
|
+
theme
|
|
134
|
+
]);
|
|
108
135
|
const value = {
|
|
109
136
|
colorScheme,
|
|
110
137
|
direction,
|
|
@@ -113,7 +140,10 @@ const ThemeProvider = ({ children, designTheme = void 0, mode, theme: themeOverr
|
|
|
113
140
|
};
|
|
114
141
|
return /* @__PURE__ */ jsx(ThemeContext_default.Provider, {
|
|
115
142
|
value,
|
|
116
|
-
children
|
|
143
|
+
children: /* @__PURE__ */ jsx(ThemeRootContext_default.Provider, {
|
|
144
|
+
value: root,
|
|
145
|
+
children
|
|
146
|
+
})
|
|
117
147
|
});
|
|
118
148
|
};
|
|
119
149
|
var ThemeProvider_default = ThemeProvider;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemeProvider.js","names":["ThemeProvider: FC<PropsWithChildren<ThemeProviderProps>>","themeOverride: RecursivePartial<ThemeConfig> | undefined","normalizeThemeConfig","flowMetaContext: FlowMetaContextValue | null","FlowMetaContext","flowMetaTheme: FlowMetaTheme | null","toggleTheme: () => void","finalThemeConfig: RecursivePartial<ThemeConfig> | undefined","metaConfig: RecursivePartial<ThemeConfig>","buildThemeConfigFromFlowMeta","theme: Theme","direction: 'ltr' | 'rtl'","value: any","ThemeContext"],"sources":["../../../src/contexts/Theme/ThemeProvider.tsx"],"sourcesContent":["// Copyright 2026 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {\n createClassObserver,\n createMediaQueryListener,\n createTheme,\n detectThemeMode,\n FlowMetaTheme,\n RecursivePartial,\n Theme,\n ThemeConfig,\n ThemeMode,\n} from '@thunderid/browser';\nimport {FC, PropsWithChildren, ReactElement, useCallback, useContext, useEffect, useMemo, useState} from 'react';\nimport ThemeContext from './ThemeContext';\nimport applyThemeToDOM from '../../utils/applyThemeToDOM';\nimport buildThemeConfigFromFlowMeta from '../../utils/buildThemeConfigFromFlowMeta';\nimport normalizeThemeConfig from '../../utils/normalizeThemeConfig';\nimport FlowMetaContext, {FlowMetaContextValue} from '../FlowMeta/FlowMetaContext';\n\nexport interface ThemeProviderProps {\n /**\n * A Design theme to use instead of the one the server returns in flow meta, for example the theme an\n * app configuration names. `theme` overrides are still applied on top.\n */\n designTheme?: FlowMetaTheme | null;\n /**\n * Initial color scheme override. Overrides the server default when provided.\n */\n mode?: ThemeMode;\n\n /**\n * Optional theme overrides merged on top of the server-side flow meta theme.\n * User-supplied values take highest precedence.\n */\n theme?: RecursivePartial<ThemeConfig>;\n}\n\n/**\n * ThemeProvider is the v2 drop-in replacement for `ThemeProvider`.\n *\n * It reads the design theme from the nearest `FlowMetaContext` (provided by\n * `FlowMetaProvider`) and publishes a resolved `Theme` object through the\n * **same** `ThemeContext` that `useTheme` consumes. This means all existing\n * components that call `useTheme` continue to work without any changes.\n *\n * The `defaultColorScheme` field returned by the server is used to seed the\n * active color scheme; the user can still toggle it locally via the\n * `toggleTheme` value exposed in the context.\n *\n * @example\n * ```tsx\n * <FlowMetaProvider config={{ baseUrl, type: FlowMetaType.App, id: appId }}>\n * <ThemeProvider>\n * <App /> {/* useTheme() works here as usual *\\/}\n * </ThemeProvider>\n * </FlowMetaProvider>\n * ```\n *\n * @example\n * With user theme overrides (user values win over server values):\n * ```tsx\n * <ThemeProvider theme={{ colors: { primary: { main: '#ff0000' } } }}>\n * <App />\n * </ThemeProvider>\n * ```\n */\nconst ThemeProvider: FC<PropsWithChildren<ThemeProviderProps>> = ({\n children,\n designTheme = undefined,\n mode,\n theme: themeOverrideProp,\n}: PropsWithChildren<ThemeProviderProps>): ReactElement => {\n const themeOverride: RecursivePartial<ThemeConfig> | undefined = normalizeThemeConfig(themeOverrideProp);\n const flowMetaContext: FlowMetaContextValue | null = useContext(FlowMetaContext);\n\n const flowMetaTheme: FlowMetaTheme | null = designTheme ?? flowMetaContext?.meta?.design?.theme ?? null;\n const isLoading: boolean = flowMetaContext?.isLoading ?? false;\n const error: Error | null = flowMetaContext?.error ?? null;\n\n // Seed the color scheme: a fixed mode wins outright, 'system'/'class' detect it from the\n // environment via the same shared utilities Vue's ThemeProvider uses, and no mode falls back\n // to the server's default (unchanged pre-`mode` behavior).\n const getInitialColorScheme = (): 'light' | 'dark' => {\n if (mode === 'light' || mode === 'dark') return mode;\n if (mode === 'system' || mode === 'class') return detectThemeMode(mode);\n return flowMetaTheme?.defaultColorScheme ?? 'light';\n };\n\n const [colorScheme, setColorScheme] = useState<'light' | 'dark'>(getInitialColorScheme);\n\n // When mode is unset, sync the color scheme with the server default once flow meta loads.\n useEffect(() => {\n if (mode === undefined && flowMetaTheme?.defaultColorScheme) {\n setColorScheme(flowMetaTheme.defaultColorScheme);\n }\n }, [mode, flowMetaTheme?.defaultColorScheme]);\n\n // Set up automatic detection listeners for 'system'/'class' modes.\n useEffect(() => {\n if (mode !== 'system' && mode !== 'class') return undefined;\n\n const handleThemeChange = (isDark: boolean): void => {\n setColorScheme(isDark ? 'dark' : 'light');\n };\n\n if (mode === 'class') {\n if (typeof document === 'undefined') return undefined;\n const observer = createClassObserver(document.documentElement, handleThemeChange);\n return () => {\n observer.disconnect();\n };\n }\n\n const mediaQuery = createMediaQueryListener(handleThemeChange);\n return () => {\n mediaQuery?.disconnect();\n };\n }, [mode]);\n\n const toggleTheme: () => void = useCallback(() => {\n setColorScheme((prev: 'light' | 'dark') => (prev === 'light' ? 'dark' : 'light'));\n }, []);\n\n // Build the resolved ThemeConfig: flow meta base → user overrides on top.\n const finalThemeConfig: RecursivePartial<ThemeConfig> | undefined = useMemo(() => {\n if (!flowMetaTheme) {\n return themeOverride;\n }\n\n const metaConfig: RecursivePartial<ThemeConfig> = buildThemeConfigFromFlowMeta(flowMetaTheme, colorScheme);\n\n if (!themeOverride) {\n return metaConfig;\n }\n\n return {\n ...metaConfig,\n ...themeOverride,\n borderRadius: {\n ...metaConfig.borderRadius,\n ...themeOverride.borderRadius,\n },\n colors: {\n ...metaConfig.colors,\n ...themeOverride.colors,\n },\n ...(metaConfig.typography || themeOverride.typography\n ? {\n typography: {\n ...(metaConfig as any).typography,\n ...themeOverride.typography,\n },\n }\n : {}),\n };\n }, [flowMetaTheme, colorScheme, themeOverride]);\n\n const theme: Theme = useMemo(\n () => createTheme(finalThemeConfig, colorScheme === 'dark'),\n [finalThemeConfig, colorScheme],\n );\n\n const direction: 'ltr' | 'rtl' = flowMetaTheme?.direction ?? 'ltr';\n\n // Apply CSS variables to the document root.\n useEffect(() => {\n applyThemeToDOM(theme);\n }, [theme]);\n\n // Apply text direction to the document root.\n useEffect(() => {\n if (typeof document !== 'undefined') {\n document.documentElement.dir = direction;\n }\n }, [direction]);\n\n const value: any = {\n colorScheme,\n direction,\n theme,\n toggleTheme,\n };\n\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;\n};\n\nexport default ThemeProvider;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,MAAMA,iBAA4D,EAChE,UACA,cAAc,QACd,MACA,OAAO,wBACkD;CACzD,MAAMC,gBAA2DC,6BAAqB,kBAAkB;CACxG,MAAMC,kBAA+C,WAAWC,wBAAgB;CAEhF,MAAMC,gBAAsC,eAAe,iBAAiB,MAAM,QAAQ,SAAS;AACxE,kBAAiB;AAChB,kBAAiB;CAK7C,MAAM,8BAAgD;AACpD,MAAI,SAAS,WAAW,SAAS,OAAQ,QAAO;AAChD,MAAI,SAAS,YAAY,SAAS,QAAS,QAAO,gBAAgB,KAAK;AACvE,SAAO,eAAe,sBAAsB;;CAG9C,MAAM,CAAC,aAAa,kBAAkB,SAA2B,sBAAsB;AAGvF,iBAAgB;AACd,MAAI,SAAS,UAAa,eAAe,mBACvC,gBAAe,cAAc,mBAAmB;IAEjD,CAAC,MAAM,eAAe,mBAAmB,CAAC;AAG7C,iBAAgB;AACd,MAAI,SAAS,YAAY,SAAS,QAAS,QAAO;EAElD,MAAM,qBAAqB,WAA0B;AACnD,kBAAe,SAAS,SAAS,QAAQ;;AAG3C,MAAI,SAAS,SAAS;AACpB,OAAI,OAAO,aAAa,YAAa,QAAO;GAC5C,MAAM,WAAW,oBAAoB,SAAS,iBAAiB,kBAAkB;AACjF,gBAAa;AACX,aAAS,YAAY;;;EAIzB,MAAM,aAAa,yBAAyB,kBAAkB;AAC9D,eAAa;AACX,eAAY,YAAY;;IAEzB,CAAC,KAAK,CAAC;CAEV,MAAMC,cAA0B,kBAAkB;AAChD,kBAAgB,SAA4B,SAAS,UAAU,SAAS,QAAS;IAChF,EAAE,CAAC;CAGN,MAAMC,mBAA8D,cAAc;AAChF,MAAI,CAAC,cACH,QAAO;EAGT,MAAMC,aAA4CC,qCAA6B,eAAe,YAAY;AAE1G,MAAI,CAAC,cACH,QAAO;AAGT,SAAO;GACL,GAAG;GACH,GAAG;GACH,cAAc;IACZ,GAAG,WAAW;IACd,GAAG,cAAc;IAClB;GACD,QAAQ;IACN,GAAG,WAAW;IACd,GAAG,cAAc;IAClB;GACD,GAAI,WAAW,cAAc,cAAc,aACvC,EACE,YAAY;IACV,GAAI,WAAmB;IACvB,GAAG,cAAc;IAClB,EACF,GACD,EAAE;GACP;IACA;EAAC;EAAe;EAAa;EAAc,CAAC;CAE/C,MAAMC,QAAe,cACb,YAAY,kBAAkB,gBAAgB,OAAO,EAC3D,CAAC,kBAAkB,YAAY,CAChC;CAED,MAAMC,YAA2B,eAAe,aAAa;AAG7D,iBAAgB;AACd,0BAAgB,MAAM;IACrB,CAAC,MAAM,CAAC;AAGX,iBAAgB;AACd,MAAI,OAAO,aAAa,YACtB,UAAS,gBAAgB,MAAM;IAEhC,CAAC,UAAU,CAAC;CAEf,MAAMC,QAAa;EACjB;EACA;EACA;EACA;EACD;AAED,QAAO,oBAACC,qBAAa;EAAgB;EAAQ;GAAiC;;AAGhF,4BAAe"}
|
|
1
|
+
{"version":3,"file":"ThemeProvider.js","names":["ThemeProvider: FC<PropsWithChildren<ThemeProviderProps>>","themeOverride: RecursivePartial<ThemeConfig> | undefined","normalizeThemeConfig","flowMetaContext: FlowMetaContextValue | null","FlowMetaContext","flowMetaTheme: FlowMetaTheme | null","toggleTheme: () => void","finalThemeConfig: RecursivePartial<ThemeConfig> | undefined","metaConfig: RecursivePartial<ThemeConfig>","buildThemeConfigFromFlowMeta","theme: Theme","direction: 'ltr' | 'rtl'","parentRoot: ThemeRootContextValue | null","ThemeRootContext","claims: MutableRefObject<number>","applied: MutableRefObject<{direction: 'ltr' | 'rtl'; theme: Theme} | null>","applyToRoot: () => void","root: ThemeRootContextValue","value: any","ThemeContext"],"sources":["../../../src/contexts/Theme/ThemeProvider.tsx"],"sourcesContent":["// Copyright 2026 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {\n createClassObserver,\n createMediaQueryListener,\n createTheme,\n detectThemeMode,\n FlowMetaTheme,\n RecursivePartial,\n Theme,\n ThemeConfig,\n ThemeMode,\n} from '@thunderid/browser';\nimport {\n FC,\n MutableRefObject,\n PropsWithChildren,\n ReactElement,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport ThemeContext from './ThemeContext';\nimport ThemeRootContext, {ThemeRootContextValue} from './ThemeRootContext';\nimport applyThemeToDOM from '../../utils/applyThemeToDOM';\nimport buildThemeConfigFromFlowMeta from '../../utils/buildThemeConfigFromFlowMeta';\nimport normalizeThemeConfig from '../../utils/normalizeThemeConfig';\nimport FlowMetaContext, {FlowMetaContextValue} from '../FlowMeta/FlowMetaContext';\n\nexport interface ThemeProviderProps {\n /**\n * A Design theme to use instead of the one the server returns in flow meta, for example the theme an\n * app configuration names. `theme` overrides are still applied on top.\n */\n designTheme?: FlowMetaTheme | null;\n /**\n * Color scheme override. Overrides the server default when provided, and the color scheme follows it\n * when it changes.\n */\n mode?: ThemeMode;\n\n /**\n * Optional theme overrides merged on top of the server-side flow meta theme.\n * User-supplied values take highest precedence.\n */\n theme?: RecursivePartial<ThemeConfig>;\n}\n\n/**\n * ThemeProvider is the v2 drop-in replacement for `ThemeProvider`.\n *\n * It reads the design theme from the nearest `FlowMetaContext` (provided by\n * `FlowMetaProvider`) and publishes a resolved `Theme` object through the\n * **same** `ThemeContext` that `useTheme` consumes. This means all existing\n * components that call `useTheme` continue to work without any changes.\n *\n * The `defaultColorScheme` field returned by the server is used to seed the\n * active color scheme; the user can still toggle it locally via the\n * `toggleTheme` value exposed in the context.\n *\n * @example\n * ```tsx\n * <FlowMetaProvider config={{ baseUrl, type: FlowMetaType.App, id: appId }}>\n * <ThemeProvider>\n * <App /> {/* useTheme() works here as usual *\\/}\n * </ThemeProvider>\n * </FlowMetaProvider>\n * ```\n *\n * @example\n * With user theme overrides (user values win over server values):\n * ```tsx\n * <ThemeProvider theme={{ colors: { primary: { main: '#ff0000' } } }}>\n * <App />\n * </ThemeProvider>\n * ```\n */\nconst ThemeProvider: FC<PropsWithChildren<ThemeProviderProps>> = ({\n children,\n designTheme = undefined,\n mode,\n theme: themeOverrideProp,\n}: PropsWithChildren<ThemeProviderProps>): ReactElement => {\n const themeOverride: RecursivePartial<ThemeConfig> | undefined = normalizeThemeConfig(themeOverrideProp);\n const flowMetaContext: FlowMetaContextValue | null = useContext(FlowMetaContext);\n\n const flowMetaTheme: FlowMetaTheme | null = designTheme ?? flowMetaContext?.meta?.design?.theme ?? null;\n const isLoading: boolean = flowMetaContext?.isLoading ?? false;\n const error: Error | null = flowMetaContext?.error ?? null;\n\n // Seed the color scheme: a fixed mode wins outright, 'system'/'class' detect it from the\n // environment via the same shared utilities Vue's ThemeProvider uses, and no mode falls back\n // to the server's default (unchanged pre-`mode` behavior).\n const getInitialColorScheme = (): 'light' | 'dark' => {\n if (mode === 'light' || mode === 'dark') return mode;\n if (mode === 'system' || mode === 'class') return detectThemeMode(mode);\n return flowMetaTheme?.defaultColorScheme ?? 'light';\n };\n\n const [colorScheme, setColorScheme] = useState<'light' | 'dark'>(getInitialColorScheme);\n\n // Follow a change of mode after mount, such as a host app's own color scheme toggle.\n const [seededMode, setSeededMode] = useState<ThemeMode | undefined>(mode);\n\n if (mode !== seededMode) {\n setSeededMode(mode);\n if (mode !== undefined) {\n setColorScheme(getInitialColorScheme());\n }\n }\n\n // When mode is unset, sync the color scheme with the server default once flow meta loads.\n useEffect(() => {\n if (mode === undefined && flowMetaTheme?.defaultColorScheme) {\n setColorScheme(flowMetaTheme.defaultColorScheme);\n }\n }, [mode, flowMetaTheme?.defaultColorScheme]);\n\n // Set up automatic detection listeners for 'system'/'class' modes.\n useEffect(() => {\n if (mode !== 'system' && mode !== 'class') return undefined;\n\n const handleThemeChange = (isDark: boolean): void => {\n setColorScheme(isDark ? 'dark' : 'light');\n };\n\n if (mode === 'class') {\n if (typeof document === 'undefined') return undefined;\n const observer = createClassObserver(document.documentElement, handleThemeChange);\n return () => {\n observer.disconnect();\n };\n }\n\n const mediaQuery = createMediaQueryListener(handleThemeChange);\n return () => {\n mediaQuery?.disconnect();\n };\n }, [mode]);\n\n const toggleTheme: () => void = useCallback(() => {\n setColorScheme((prev: 'light' | 'dark') => (prev === 'light' ? 'dark' : 'light'));\n }, []);\n\n // Build the resolved ThemeConfig: flow meta base → user overrides on top.\n const finalThemeConfig: RecursivePartial<ThemeConfig> | undefined = useMemo(() => {\n if (!flowMetaTheme) {\n return themeOverride;\n }\n\n const metaConfig: RecursivePartial<ThemeConfig> = buildThemeConfigFromFlowMeta(flowMetaTheme, colorScheme);\n\n if (!themeOverride) {\n return metaConfig;\n }\n\n return {\n ...metaConfig,\n ...themeOverride,\n borderRadius: {\n ...metaConfig.borderRadius,\n ...themeOverride.borderRadius,\n },\n colors: {\n ...metaConfig.colors,\n ...themeOverride.colors,\n },\n ...(metaConfig.typography || themeOverride.typography\n ? {\n typography: {\n ...(metaConfig as any).typography,\n ...themeOverride.typography,\n },\n }\n : {}),\n };\n }, [flowMetaTheme, colorScheme, themeOverride]);\n\n const theme: Theme = useMemo(\n () => createTheme(finalThemeConfig, colorScheme === 'dark'),\n [finalThemeConfig, colorScheme],\n );\n\n const direction: 'ltr' | 'rtl' = flowMetaTheme?.direction ?? 'ltr';\n\n // The innermost ThemeProvider owns the document root: an outer one stops writing its CSS variables\n // and text direction while a nested one is mounted, and writes them again once it unmounts.\n const parentRoot: ThemeRootContextValue | null = useContext(ThemeRootContext);\n const claims: MutableRefObject<number> = useRef<number>(0);\n const applied: MutableRefObject<{direction: 'ltr' | 'rtl'; theme: Theme} | null> = useRef(null);\n\n const applyToRoot: () => void = useCallback(() => {\n if (!applied.current || typeof document === 'undefined') return;\n applyThemeToDOM(applied.current.theme);\n document.documentElement.dir = applied.current.direction;\n }, []);\n\n const root: ThemeRootContextValue = useMemo(\n () => ({\n claim: (): (() => void) => {\n claims.current += 1;\n return (): void => {\n claims.current -= 1;\n if (claims.current === 0) applyToRoot();\n };\n },\n }),\n [applyToRoot],\n );\n\n useEffect(() => parentRoot?.claim(), [parentRoot]);\n\n useEffect(() => {\n applied.current = {direction, theme};\n if (claims.current === 0) applyToRoot();\n }, [applyToRoot, direction, theme]);\n\n const value: any = {\n colorScheme,\n direction,\n theme,\n toggleTheme,\n };\n\n return (\n <ThemeContext.Provider value={value}>\n <ThemeRootContext.Provider value={root}>{children}</ThemeRootContext.Provider>\n </ThemeContext.Provider>\n );\n};\n\nexport default ThemeProvider;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Context } from 'react';
|
|
2
|
+
export interface ThemeRootContextValue {
|
|
3
|
+
/**
|
|
4
|
+
* Hands the document root to a nested `ThemeProvider` until the returned release is called, so the
|
|
5
|
+
* innermost provider's CSS variables and text direction are the ones on the page.
|
|
6
|
+
*/
|
|
7
|
+
claim: () => () => void;
|
|
8
|
+
}
|
|
9
|
+
declare const ThemeRootContext: Context<ThemeRootContextValue | null>;
|
|
10
|
+
export default ThemeRootContext;
|
|
11
|
+
//# sourceMappingURL=ThemeRootContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThemeRootContext.d.ts","sourceRoot":"","sources":["../../../src/contexts/Theme/ThemeRootContext.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,OAAO,EAAgB,MAAM,OAAO,CAAC;AAE7C,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,KAAK,EAAE,MAAM,MAAM,IAAI,CAAC;CACzB;AAED,QAAA,MAAM,gBAAgB,EAAE,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAqD,CAAC;AAIlH,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/contexts/Theme/ThemeRootContext.ts
|
|
4
|
+
const ThemeRootContext = createContext(null);
|
|
5
|
+
ThemeRootContext.displayName = "ThemeRootContext";
|
|
6
|
+
var ThemeRootContext_default = ThemeRootContext;
|
|
7
|
+
|
|
8
|
+
//#endregion
|
|
9
|
+
export { ThemeRootContext_default as default };
|
|
10
|
+
//# sourceMappingURL=ThemeRootContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThemeRootContext.js","names":["ThemeRootContext: Context<ThemeRootContextValue | null>"],"sources":["../../../src/contexts/Theme/ThemeRootContext.ts"],"sourcesContent":["// Copyright 2026 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {Context, createContext} from 'react';\n\nexport interface ThemeRootContextValue {\n /**\n * Hands the document root to a nested `ThemeProvider` until the returned release is called, so the\n * innermost provider's CSS variables and text direction are the ones on the page.\n */\n claim: () => () => void;\n}\n\nconst ThemeRootContext: Context<ThemeRootContextValue | null> = createContext<ThemeRootContextValue | null>(null);\n\nThemeRootContext.displayName = 'ThemeRootContext';\n\nexport default ThemeRootContext;\n"],"mappings":";;;AAaA,MAAMA,mBAA0D,cAA4C,KAAK;AAEjH,iBAAiB,cAAc;AAE/B,+BAAe"}
|