diff --git a/packages/core/src/api/apis/definitions/AppThemeApi.ts b/packages/core/src/api/apis/definitions/AppThemeApi.ts index f866e38738..801210de0a 100644 --- a/packages/core/src/api/apis/definitions/AppThemeApi.ts +++ b/packages/core/src/api/apis/definitions/AppThemeApi.ts @@ -16,6 +16,7 @@ import { ApiRef } from '../ApiRef'; import { BackstageTheme } from '@backstage/theme'; +import { Observable } from '../../types'; /** * Describes a theme provided by the app. @@ -53,14 +54,21 @@ export type AppThemeApi = { getThemeOptions(): AppTheme[]; /** - * Get the current theme. Returns undefined if the default theme is used. + * Observe the currently selected theme. A value of undefined means no specific theme has been selected. */ - getTheme(): AppTheme | undefined; + activeThemeId$(): Observable; + + /** + * Get the current theme ID. Returns undefined if no specific theme is selected. + */ + getActiveThemeId(): string | undefined; /** * Set a specific theme to use in the app, overriding the default theme selection. + * + * Clear the selection by passing in undefined. */ - setTheme(theme?: AppTheme): void; + setActiveThemeId(themeId?: string): void; }; export const appThemeApiRef = new ApiRef({ diff --git a/packages/core/src/api/app/AppThemeProvider.tsx b/packages/core/src/api/app/AppThemeProvider.tsx new file mode 100644 index 0000000000..487a79889f --- /dev/null +++ b/packages/core/src/api/app/AppThemeProvider.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC } from 'react'; +import { ThemeProvider, CssBaseline } from '@material-ui/core'; +import { useApi, appThemeApiRef, AppTheme } from '../apis'; +import { useObservable } from 'react-use'; + +// This tries to find the most accurate match, but also falls back to less +// accurate results in order to avoid errors. +function resolveTheme(themeId: string | undefined, themes: AppTheme[]) { + if (themeId !== undefined) { + const selectedTheme = themes.find((theme) => theme.id === themeId); + if (selectedTheme) { + return selectedTheme; + } + } + + const preferDark = window?.matchMedia('(prefers-color-scheme: dark)') + ?.matches; + + if (preferDark) { + const darkTheme = themes.find((theme) => theme.variant === 'dark'); + if (darkTheme) { + return darkTheme; + } + } + + const lightTheme = themes.find((theme) => theme.variant === 'light'); + if (lightTheme) { + return lightTheme; + } + + return themes[0]; +} + +export const AppThemeProvider: FC<{}> = ({ children }) => { + const appThemeApi = useApi(appThemeApiRef); + const themeId = useObservable( + appThemeApi.activeThemeId$(), + appThemeApi.getActiveThemeId(), + ); + + const theme = resolveTheme(themeId, appThemeApi.getInstalledThemes()); + if (!theme) { + throw new Error('App has no themes'); + } + + return ( + + {children} + + ); +};