Skip to content

Commit 82da253

Browse files
committed
Support themes
1 parent f4e27de commit 82da253

File tree

8 files changed

+86
-78
lines changed

8 files changed

+86
-78
lines changed

src/components/Core/DarkModeSystemSwitcher.tsx

Lines changed: 0 additions & 30 deletions
This file was deleted.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { resolveTheme } from './ThemeManager.tsx';
3+
4+
describe('ThemeManager', () => {
5+
describe('resolveTheme()', () => {
6+
it('returns theme coming from URL when it is truthy', () => {
7+
expect(resolveTheme('sap_fiori_3', true)).toBe('sap_fiori_3');
8+
expect(resolveTheme('custom_theme', false)).toBe('custom_theme');
9+
});
10+
11+
it('falls back to dark default when URL theme is falsy and user prefers dark mode', () => {
12+
expect(resolveTheme(null, true)).toBe('sap_horizon_dark');
13+
expect(resolveTheme('', true)).toBe('sap_horizon_dark');
14+
});
15+
16+
it('falls back to light default when URL theme is falsy and user prefers light mode', () => {
17+
expect(resolveTheme(null, false)).toBe('sap_horizon');
18+
expect(resolveTheme('', false)).toBe('sap_horizon');
19+
});
20+
});
21+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useEffect } from 'react';
2+
import { setTheme } from '@ui5/webcomponents-base/dist/config/Theme.js';
3+
import { useIsDarkModePreferred } from '../../hooks/useIsDarkModePreferred.ts';
4+
5+
const DEFAULT_THEME_LIGHT = 'sap_horizon';
6+
const DEFAULT_THEME_DARK = 'sap_horizon_dark';
7+
8+
export function resolveTheme(themeFromUrl: string | null, isDarkModePreferred: boolean): string {
9+
if (themeFromUrl) {
10+
return themeFromUrl;
11+
}
12+
return isDarkModePreferred ? DEFAULT_THEME_DARK : DEFAULT_THEME_LIGHT;
13+
}
14+
15+
export function ThemeManager() {
16+
const isDarkModePreferred = useIsDarkModePreferred();
17+
const themeFromUrl = new URL(window.location.href).searchParams.get('sap-theme');
18+
19+
useEffect(() => {
20+
const resolvedTheme = resolveTheme(themeFromUrl, isDarkModePreferred);
21+
void setTheme(resolvedTheme);
22+
}, [isDarkModePreferred, themeFromUrl]);
23+
24+
return null;
25+
}

src/components/Yaml/YamlViewer.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
11
import { FC } from 'react';
22
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
3-
import {
4-
materialLight,
5-
materialDark,
6-
} from 'react-syntax-highlighter/dist/esm/styles/prism';
3+
import { materialLight, materialDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
74

85
import { Button, FlexBox } from '@ui5/webcomponents-react';
96
import styles from './YamlViewer.module.css';
107
import { useToast } from '../../context/ToastContext.tsx';
118
import { useTranslation } from 'react-i18next';
12-
import { useThemeMode } from '../../lib/useThemeMode.ts';
9+
import { useIsDarkModePreferred } from '../../hooks/useIsDarkModePreferred.ts';
1310
type YamlViewerProps = { yamlString: string; filename: string };
1411
const YamlViewer: FC<YamlViewerProps> = ({ yamlString, filename }) => {
1512
const toast = useToast();
1613
const { t } = useTranslation();
17-
const { isDarkMode } = useThemeMode();
14+
const isDarkModePreferred = useIsDarkModePreferred();
1815
const copyToClipboard = () => {
1916
navigator.clipboard.writeText(yamlString);
2017
toast.show(t('yaml.copiedToClipboard'));
@@ -33,13 +30,7 @@ const YamlViewer: FC<YamlViewerProps> = ({ yamlString, filename }) => {
3330

3431
return (
3532
<div className={styles.container}>
36-
<FlexBox
37-
className={styles.buttons}
38-
direction="Row"
39-
justifyContent="End"
40-
alignItems="Baseline"
41-
gap={16}
42-
>
33+
<FlexBox className={styles.buttons} direction="Row" justifyContent="End" alignItems="Baseline" gap={16}>
4334
<Button icon="copy" onClick={copyToClipboard}>
4435
{t('buttons.copy')}
4536
</Button>
@@ -49,7 +40,7 @@ const YamlViewer: FC<YamlViewerProps> = ({ yamlString, filename }) => {
4940
</FlexBox>
5041
<SyntaxHighlighter
5142
language="yaml"
52-
style={isDarkMode ? materialDark : materialLight}
43+
style={isDarkModePreferred ? materialDark : materialLight}
5344
showLineNumbers
5445
lineNumberStyle={{
5546
paddingRight: '20px',
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { useSyncExternalStore } from 'react';
2+
3+
function makeMediaQueryStore(mediaQuery: string) {
4+
function getSnapshot() {
5+
return window.matchMedia(mediaQuery).matches;
6+
}
7+
8+
function subscribe(callback: () => void) {
9+
const mediaQueryList = window.matchMedia(mediaQuery);
10+
mediaQueryList.addEventListener('change', callback);
11+
12+
return () => {
13+
mediaQueryList.removeEventListener('change', callback);
14+
};
15+
}
16+
17+
return function useMediaQuery() {
18+
return useSyncExternalStore(subscribe, getSnapshot);
19+
};
20+
}
21+
22+
export const useIsDarkModePreferred = makeMediaQueryStore('(prefers-color-scheme: dark)');

src/lib/useThemeMode.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/main.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ToastProvider } from './context/ToastContext.tsx';
77
import { CopyButtonProvider } from './context/CopyButtonContext.tsx';
88
import { FrontendConfigProvider } from './context/FrontendConfigContext.tsx';
99
import '@ui5/webcomponents-react/dist/Assets'; //used for loading themes
10-
import { DarkModeSystemSwitcher } from './components/Core/DarkModeSystemSwitcher.tsx';
10+
import { ThemeManager } from './components/ThemeManager/ThemeManager.tsx';
1111
import '.././i18n.ts';
1212
import './utils/i18n/timeAgo';
1313
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
@@ -49,7 +49,7 @@ export function createApp() {
4949
<ApolloClientProvider>
5050
<App />
5151
</ApolloClientProvider>
52-
<DarkModeSystemSwitcher />
52+
<ThemeManager />
5353
</SWRConfig>
5454
</CopyButtonProvider>
5555
</ToastProvider>

src/spaces/onboarding/auth/AuthContextOnboarding.tsx

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ interface AuthContextOnboardingType {
1212
logout: () => Promise<void>;
1313
}
1414

15-
const AuthContextOnboarding = createContext<AuthContextOnboardingType | null>(
16-
null,
17-
);
15+
const AuthContextOnboarding = createContext<AuthContextOnboardingType | null>(null);
1816

1917
export function AuthProviderOnboarding({ children }: { children: ReactNode }) {
2018
const [isAuthenticated, setIsAuthenticated] = useState(false);
@@ -40,22 +38,16 @@ export function AuthProviderOnboarding({ children }: { children: ReactNode }) {
4038
} catch (_) {
4139
/* safe to ignore */
4240
}
43-
throw new Error(
44-
errorBody?.message ||
45-
`Authentication check failed with status: ${response.status}`,
46-
);
41+
throw new Error(errorBody?.message || `Authentication check failed with status: ${response.status}`);
4742
}
4843

4944
const body = await response.json();
5045
const validationResult = MeResponseSchema.safeParse(body);
5146
if (!validationResult.success) {
52-
throw new Error(
53-
`Auth API response validation failed: ${validationResult.error.flatten()}`,
54-
);
47+
throw new Error(`Auth API response validation failed: ${validationResult.error.flatten()}`);
5548
}
5649

57-
const { isAuthenticated: apiIsAuthenticated, user: apiUser } =
58-
validationResult.data;
50+
const { isAuthenticated: apiIsAuthenticated, user: apiUser } = validationResult.data;
5951
setUser(apiUser);
6052
setIsAuthenticated(apiIsAuthenticated);
6153

@@ -75,10 +67,10 @@ export function AuthProviderOnboarding({ children }: { children: ReactNode }) {
7567

7668
const login = () => {
7769
sessionStorage.setItem(AUTH_FLOW_SESSION_KEY, 'onboarding');
78-
79-
window.location.replace(
80-
`/api/auth/onboarding/login?redirectTo=${encodeURIComponent(window.location.hash)}`,
81-
);
70+
// The query parameters and hash fragments need to be preserved, e.g. /?sap-theme=sap_horizon#/mcp/projects
71+
const { search, hash } = window.location;
72+
const redirectTo = (search ? `/${search}` : '') + hash;
73+
window.location.replace(`/api/auth/onboarding/login?redirectTo=${encodeURIComponent(redirectTo)}`);
8274
};
8375

8476
const logout = async () => {
@@ -94,9 +86,7 @@ export function AuthProviderOnboarding({ children }: { children: ReactNode }) {
9486
} catch (_) {
9587
/* safe to ignore */
9688
}
97-
throw new Error(
98-
errorBody?.message || `Logout failed with status: ${response.status}`,
99-
);
89+
throw new Error(errorBody?.message || `Logout failed with status: ${response.status}`);
10090
}
10191

10292
await refreshAuthStatus();
@@ -106,9 +96,7 @@ export function AuthProviderOnboarding({ children }: { children: ReactNode }) {
10696
};
10797

10898
return (
109-
<AuthContextOnboarding
110-
value={{ isLoading, isAuthenticated, user, error, login, logout }}
111-
>
99+
<AuthContextOnboarding value={{ isLoading, isAuthenticated, user, error, login, logout }}>
112100
{children}
113101
</AuthContextOnboarding>
114102
);
@@ -117,9 +105,7 @@ export function AuthProviderOnboarding({ children }: { children: ReactNode }) {
117105
export const useAuthOnboarding = () => {
118106
const context = use(AuthContextOnboarding);
119107
if (!context) {
120-
throw new Error(
121-
'useAuthOnboarding must be used within an AuthProviderOnboarding.',
122-
);
108+
throw new Error('useAuthOnboarding must be used within an AuthProviderOnboarding.');
123109
}
124110
return context;
125111
};

0 commit comments

Comments
 (0)