From 0739e9b5484481715a438cca8b4fdf9f7678fbd1 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 6 May 2026 16:05:48 +0530 Subject: [PATCH] feat: add support for theme override --- .../organization/preferences/index.tsx | 5 +- .../react/components/organization/profile.tsx | 61 +++++++++++----- .../react/components/organization/routes.tsx | 8 ++- .../preferences/preferences-view.tsx | 53 +++++++++++--- .../views/preferences/preferences-page.tsx | 69 ++++++++++++------- 5 files changed, 144 insertions(+), 52 deletions(-) diff --git a/web/sdk/react/components/organization/preferences/index.tsx b/web/sdk/react/components/organization/preferences/index.tsx index fe555ff6c..207758bc8 100644 --- a/web/sdk/react/components/organization/preferences/index.tsx +++ b/web/sdk/react/components/organization/preferences/index.tsx @@ -1,7 +1,10 @@ 'use client'; +import { useRouteContext } from '@tanstack/react-router'; import { PreferencesPage } from '~/react/views/preferences'; +import { RouterContext } from '../routes'; export default function UserPreferences() { - return ; + const { theme, onThemeChange } = useRouteContext({ from: '__root__' }) as RouterContext; + return ; } diff --git a/web/sdk/react/components/organization/profile.tsx b/web/sdk/react/components/organization/profile.tsx index 42b1fb892..e1ad8ee32 100644 --- a/web/sdk/react/components/organization/profile.tsx +++ b/web/sdk/react/components/organization/profile.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { RouterProvider, createMemoryHistory, @@ -30,7 +31,9 @@ export const OrganizationProfile = ({ showPreferences = false, hideToast = false, customScreens = [], - onLogout = () => {} + onLogout = () => { }, + theme, + onThemeChange, }: OrganizationProfileProps) => { const memoryHistory = createMemoryHistory({ initialEntries: [defaultRoute] @@ -40,21 +43,47 @@ export const OrganizationProfile = ({ const routeTree = getRootTree({ customScreens }); - const memoryRouter = createRouter({ - routeTree, - history: memoryHistory, - context: { - organizationId, - showBilling, - showTokens, - showAPIKeys, - hideToast, - showPreferences, - customRoutes, - onLogout - } - }); - return ; + const memoryRouter = useMemo( + () => + createRouter({ + routeTree, + history: memoryHistory, + context: { + organizationId, + showBilling, + showTokens, + showAPIKeys, + hideToast, + showPreferences, + customRoutes, + onLogout, + theme, + onThemeChange + } + }), + // Router is created once; dynamic context flows through RouterProvider's + // context prop so updates don't recreate the router and reset navigation. + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + return ( + + ); }; declare module '@tanstack/react-router' { diff --git a/web/sdk/react/components/organization/routes.tsx b/web/sdk/react/components/organization/routes.tsx index 0e80fbee2..417f19bf2 100644 --- a/web/sdk/react/components/organization/routes.tsx +++ b/web/sdk/react/components/organization/routes.tsx @@ -38,6 +38,8 @@ export interface CustomScreen { component: RouteComponent; } +export type Theme = 'light' | 'dark' | 'system'; + export interface OrganizationProfileProps { organizationId: string; defaultRoute?: string; @@ -48,6 +50,8 @@ export interface OrganizationProfileProps { hideToast?: boolean; customScreens?: CustomScreen[]; onLogout?: () => void; + theme?: Theme; + onThemeChange?: (theme: Theme) => void; } export interface CustomRoutes { @@ -55,7 +59,7 @@ export interface CustomRoutes { User: Pick[]; } -type RouterContext = Pick< +export type RouterContext = Pick< OrganizationProfileProps, | 'organizationId' | 'showBilling' @@ -63,6 +67,8 @@ type RouterContext = Pick< | 'showAPIKeys' | 'hideToast' | 'showPreferences' + | 'theme' + | 'onThemeChange' > & { customRoutes: CustomRoutes; onLogout?: () => void }; export function getCustomRoutes(customScreens: CustomScreen[] = []) { diff --git a/web/sdk/react/views-new/preferences/preferences-view.tsx b/web/sdk/react/views-new/preferences/preferences-view.tsx index b6169b183..faecfe549 100644 --- a/web/sdk/react/views-new/preferences/preferences-view.tsx +++ b/web/sdk/react/views-new/preferences/preferences-view.tsx @@ -10,13 +10,48 @@ import { useTheme } from '@raystack/apsara-v1'; import styles from './preferences-view.module.css'; import { ReactNode } from 'react'; +const THEME_OPTIONS = { + light: { + label: 'Light', + icon: + }, + dark: { + label: 'Dark', + icon: + }, + system: { + label: 'System', + icon: + } +} +type Theme = keyof typeof THEME_OPTIONS; + interface PreferencesViewProps { children?: ReactNode; + /** + * The theme to use for Theme Select. + * If not provided, the theme will be fetched from ThemeProvider. + */ + theme?: Theme; + /** + * The callback to call when the theme is changed. + * If not provided, the theme will be set in the ThemeProvider. + */ + onThemeChange?: (theme: Theme) => void; } -export function PreferencesView({ children }: PreferencesViewProps) { +export function PreferencesView({ children, theme: providedTheme, onThemeChange }: PreferencesViewProps) { const { theme, setTheme } = useTheme(); const { preferences, isLoading, isFetching, updatePreferences } = usePreferences({}); + const computedTheme = providedTheme ?? theme; + + const handleThemeChange = (theme: string) => { + if (onThemeChange) { + onThemeChange(theme as Theme); + } else { + setTheme(theme); + } + } const newsletterValue = preferences?.[PREFERENCE_OPTIONS.NEWSLETTER]?.value ?? 'false'; @@ -32,20 +67,16 @@ export function PreferencesView({ children }: PreferencesViewProps) { title="Theme" description="Customise your interface color scheme." > - - }> - Light - - }> - Dark - - }> - System - + {Object.entries(THEME_OPTIONS).map(([value, { label, icon }]) => ( + + {label} + + ))} diff --git a/web/sdk/react/views/preferences/preferences-page.tsx b/web/sdk/react/views/preferences/preferences-page.tsx index 41c657437..568125b12 100644 --- a/web/sdk/react/views/preferences/preferences-page.tsx +++ b/web/sdk/react/views/preferences/preferences-page.tsx @@ -72,12 +72,35 @@ const newsletterOptions = [ value: 'false' } ]; +type Theme = 'light' | 'dark' | 'system'; -export default function PreferencesPage() { +interface PreferencesPageProps { + /** + * The theme to use for Theme Select. + * If not provided, the theme will be fetched from ThemeProvider. + */ + theme?: Theme; + /** + * The callback to call when the theme is changed. + * If not provided, the theme will be set in the ThemeProvider. + */ + onThemeChange?: (theme: Theme) => void; +} + +export default function PreferencesPage({ theme: providedTheme, onThemeChange }: PreferencesPageProps) { const { theme, setTheme } = useTheme(); const { preferences, isLoading, isFetching, updatePreferences } = usePreferences({}); + const computedTheme = providedTheme ?? theme; + const handleThemeChange = (theme: string) => { + if (onThemeChange) { + onThemeChange(theme as Theme); + } else { + setTheme(theme); + } + } + const newsletterValue = preferences?.[PREFERENCE_OPTIONS.NEWSLETTER]?.value ?? 'false'; @@ -91,28 +114,28 @@ export default function PreferencesPage() { /> - setTheme(value)} - /> - - { - updatePreferences([{ name: PREFERENCE_OPTIONS.NEWSLETTER, value }]); - }} - /> - + + + { + updatePreferences([{ name: PREFERENCE_OPTIONS.NEWSLETTER, value }]); + }} + /> +