mirror of
https://github.com/logto-io/logto.git
synced 2025-03-03 22:15:32 -05:00
refactor(console): refactor theme-related types (#3419)
This commit is contained in:
parent
dd4ae5b18c
commit
658d905fe0
48 changed files with 270 additions and 206 deletions
|
@ -1,3 +1,4 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -5,7 +6,6 @@ import ErrorDark from '@/assets/images/error-dark.svg';
|
|||
import Error from '@/assets/images/error.svg';
|
||||
import KeyboardArrowDown from '@/assets/images/keyboard-arrow-down.svg';
|
||||
import KeyboardArrowUp from '@/assets/images/keyboard-arrow-up.svg';
|
||||
import { Theme } from '@/types/theme';
|
||||
import { onKeyDownHandler } from '@/utils/a11y';
|
||||
import { getThemeFromLocalStorage } from '@/utils/theme';
|
||||
|
||||
|
@ -26,7 +26,7 @@ const AppError = ({ title, errorCode, errorMessage, callStack, children }: Props
|
|||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{theme === Theme.LightMode ? <Error /> : <ErrorDark />}
|
||||
{theme === Theme.Light ? <Error /> : <ErrorDark />}
|
||||
<label>{title ?? t('errors.something_went_wrong')}</label>
|
||||
<div className={styles.summary}>
|
||||
<span>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
|
||||
import IllustrationDark from '@/assets/images/loading-illustration-dark.svg';
|
||||
import Illustration from '@/assets/images/loading-illustration.svg';
|
||||
import { Daisy as Spinner } from '@/components/Spinner';
|
||||
import { Theme } from '@/types/theme';
|
||||
import { getThemeFromLocalStorage } from '@/utils/theme';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -14,7 +15,7 @@ export const AppLoadingOffline = () => {
|
|||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{theme === Theme.LightMode ? <Illustration /> : <IllustrationDark />}
|
||||
{theme === Theme.Light ? <Illustration /> : <IllustrationDark />}
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
import { useContext } from 'react';
|
||||
import { Theme } from '@logto/schemas';
|
||||
|
||||
import IllustrationDark from '@/assets/images/loading-illustration-dark.svg';
|
||||
import Illustration from '@/assets/images/loading-illustration.svg';
|
||||
import { Daisy as Spinner } from '@/components/Spinner';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import { Theme } from '@/types/theme';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const AppLoading = () => {
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{theme === Theme.LightMode ? <Illustration /> : <IllustrationDark />}
|
||||
{theme === Theme.Light ? <Illustration /> : <IllustrationDark />}
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type { ApplicationType } from '@logto/schemas';
|
||||
import { useContext } from 'react';
|
||||
import { Theme } from '@logto/schemas';
|
||||
|
||||
import { darkModeApplicationIconMap, lightModeApplicationIconMap } from '@/consts';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import { Theme } from '@/types/theme';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
type Props = {
|
||||
type: ApplicationType;
|
||||
|
@ -11,8 +10,8 @@ type Props = {
|
|||
};
|
||||
|
||||
const ApplicationIcon = ({ type, className }: Props) => {
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const isLightMode = theme === Theme.LightMode;
|
||||
const theme = useTheme();
|
||||
const isLightMode = theme === Theme.Light;
|
||||
const Icon = isLightMode ? lightModeApplicationIconMap[type] : darkModeApplicationIconMap[type];
|
||||
|
||||
return <Icon className={className} />;
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type { ConnectorResponse } from '@logto/schemas';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import { Theme } from '@/types/theme';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
import ImageWithErrorFallback from '../ImageWithErrorFallback';
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -15,14 +14,14 @@ type Props = {
|
|||
};
|
||||
|
||||
const ConnectorLogo = ({ className, data, size = 'medium' }: Props) => {
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<ImageWithErrorFallback
|
||||
containerClassName={classNames(styles.container, styles[size])}
|
||||
className={classNames(styles.logo, styles[size], className)}
|
||||
alt="logo"
|
||||
src={theme === Theme.DarkMode && data.logoDark ? data.logoDark : data.logo}
|
||||
src={theme === Theme.Dark && data.logoDark ? data.logoDark : data.logo}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import EmptyDark from '@/assets/images/table-empty-dark.svg';
|
||||
import Empty from '@/assets/images/table-empty.svg';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import { Theme } from '@/types/theme';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -16,8 +15,8 @@ export type Props = {
|
|||
|
||||
const EmptyDataPlaceholder = ({ title, size = 'medium' }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const EmptyImage = theme === Theme.LightMode ? Empty : EmptyDark;
|
||||
const theme = useTheme();
|
||||
const EmptyImage = theme === Theme.Light ? Empty : EmptyDark;
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.empty, styles[size])}>
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import type { ImgHTMLAttributes } from 'react';
|
||||
import { useContext, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import FallbackImageDark from '@/assets/images/broken-image-dark.svg';
|
||||
import FallbackImageLight from '@/assets/images/broken-image-light.svg';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import { Theme } from '@/types/theme';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
type Props = { containerClassName?: string } & ImgHTMLAttributes<HTMLImageElement>;
|
||||
|
||||
const ImageWithErrorFallback = ({ src, alt, className, containerClassName, ...props }: Props) => {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const Fallback = theme === Theme.LightMode ? FallbackImageLight : FallbackImageDark;
|
||||
const theme = useTheme();
|
||||
const Fallback = theme === Theme.Light ? FallbackImageLight : FallbackImageDark;
|
||||
|
||||
const errorHandler = () => {
|
||||
setHasError(true);
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import RequestErrorDarkImage from '@/assets/images/request-error-dark.svg';
|
||||
import RequestErrorImage from '@/assets/images/request-error.svg';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import type { RequestError } from '@/hooks/use-api';
|
||||
import { Theme } from '@/types/theme';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
import Button from '../Button';
|
||||
import Card from '../Card';
|
||||
|
@ -20,10 +19,10 @@ type Props = {
|
|||
|
||||
const RequestDataError = ({ error, onRetry, className }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const theme = useTheme();
|
||||
const errorMessage = error.body?.message ?? error.message;
|
||||
const isNotFoundError = error.status === 404;
|
||||
const ErrorImage = theme === Theme.LightMode ? RequestErrorImage : RequestErrorDarkImage;
|
||||
const ErrorImage = theme === Theme.Light ? RequestErrorImage : RequestErrorDarkImage;
|
||||
|
||||
return (
|
||||
<Card className={classNames(styles.error, className)}>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import Moon from '@/assets/images/moon.svg';
|
||||
import Sun from '@/assets/images/sun.svg';
|
||||
import { Theme } from '@/types/theme';
|
||||
|
||||
import type { Props as ButtonProps } from '../Button';
|
||||
import Button from '../Button';
|
||||
import type { Props as ButtonProps } from '../../../Button';
|
||||
import Button from '../../../Button';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
|
@ -16,14 +16,14 @@ type Props = {
|
|||
size?: ButtonProps['size'];
|
||||
};
|
||||
|
||||
const ToggleThemeButton = ({
|
||||
const ToggleUiThemeButton = ({
|
||||
value,
|
||||
onToggle,
|
||||
className,
|
||||
iconClassName,
|
||||
size = 'medium',
|
||||
}: Props) => {
|
||||
const ThemeIcon = value === Theme.LightMode ? Sun : Moon;
|
||||
const ThemeIcon = value === Theme.Light ? Sun : Moon;
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, styles[size])}>
|
||||
|
@ -32,11 +32,11 @@ const ToggleThemeButton = ({
|
|||
className={classNames(styles.button, className)}
|
||||
icon={<ThemeIcon className={classNames(styles.icon, iconClassName)} />}
|
||||
onClick={() => {
|
||||
onToggle(value === Theme.LightMode ? Theme.DarkMode : Theme.LightMode);
|
||||
onToggle(value === Theme.Light ? Theme.Dark : Theme.Light);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleThemeButton;
|
||||
export default ToggleUiThemeButton;
|
|
@ -1,6 +1,6 @@
|
|||
import type { LanguageTag } from '@logto/language-kit';
|
||||
import type { ConnectorMetadata, ConnectorResponse, SignInExperience } from '@logto/schemas';
|
||||
import { ConnectorType } from '@logto/schemas';
|
||||
import { Theme, ConnectorType } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import { format } from 'date-fns';
|
||||
|
@ -12,11 +12,12 @@ import PhoneInfo from '@/assets/images/phone-info.svg';
|
|||
import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider';
|
||||
import type { RequestError } from '@/hooks/use-api';
|
||||
import useUiLanguages from '@/hooks/use-ui-languages';
|
||||
import { Theme } from '@/types/theme';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
import { PreviewPlatform } from './types';
|
||||
|
||||
export { default as ToggleUiThemeButton } from './components/ToggleUiThemeButton';
|
||||
|
||||
type Props = {
|
||||
platform: PreviewPlatform;
|
||||
mode: Theme;
|
||||
|
@ -114,7 +115,7 @@ const SignInExperiencePreview = ({ platform, mode, language = 'en', signInExperi
|
|||
style={conditional(
|
||||
platform === PreviewPlatform.DesktopWeb && {
|
||||
// Set background color to match iframe's background color on both dark and light mode.
|
||||
backgroundColor: mode === Theme.DarkMode ? '#000' : '#e5e1ec',
|
||||
backgroundColor: mode === Theme.Dark ? '#000' : '#e5e1ec',
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { useContext } from 'react';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import RequestErrorDarkImage from '@/assets/images/request-error-dark.svg';
|
||||
import RequestErrorImage from '@/assets/images/request-error.svg';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import { Theme } from '@/types/theme';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
import Button from '../Button';
|
||||
import * as styles from './TableError.module.scss';
|
||||
|
@ -18,13 +17,13 @@ type Props = {
|
|||
|
||||
const TableError = ({ title, content, onRetry, columns }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={columns}>
|
||||
<div className={styles.tableError}>
|
||||
{theme === Theme.LightMode ? <RequestErrorImage /> : <RequestErrorDarkImage />}
|
||||
{theme === Theme.Light ? <RequestErrorImage /> : <RequestErrorDarkImage />}
|
||||
<div className={styles.title}>{title ?? t('errors.something_went_wrong')}</div>
|
||||
<div className={styles.content}>{content ?? t('errors.unknown_server_error')}</div>
|
||||
{onRetry && <Button title="general.retry" onClick={onRetry} />}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import type { AdminConsoleKey } from '@logto/phrases';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import { Theme } from '@/types/theme';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
import TextLink from '../TextLink';
|
||||
import * as styles from './TablePlaceholder.module.scss';
|
||||
|
@ -27,11 +26,11 @@ const TablePlaceholder = ({
|
|||
action,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div className={styles.placeholder}>
|
||||
<div className={styles.image}>{theme === Theme.LightMode ? image : imageDark}</div>
|
||||
<div className={styles.image}>{theme === Theme.Light ? image : imageDark}</div>
|
||||
<div className={styles.title}>{t(title)}</div>
|
||||
<div className={styles.description}>
|
||||
{t(description)}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import type { Nullable } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import DarkAvatar from '@/assets/images/default-avatar-dark.svg';
|
||||
import LightAvatar from '@/assets/images/default-avatar-light.svg';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import { Theme } from '@/types/theme';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
import ImageWithErrorFallback from '../ImageWithErrorFallback';
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -17,8 +16,8 @@ type Props = {
|
|||
};
|
||||
|
||||
const UserAvatar = ({ className, url, size = 'medium' }: Props) => {
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const DefaultAvatar = theme === Theme.LightMode ? LightAvatar : DarkAvatar;
|
||||
const theme = useTheme();
|
||||
const DefaultAvatar = theme === Theme.Light ? LightAvatar : DarkAvatar;
|
||||
const avatarClassName = classNames(styles.avatar, styles[size], className);
|
||||
|
||||
if (url) {
|
||||
|
|
|
@ -6,7 +6,7 @@ export * from './tenants';
|
|||
export * from './page-tabs';
|
||||
export * from './external-links';
|
||||
|
||||
export const themeStorageKey = 'logto:admin_console:theme';
|
||||
export const appearanceModeStorageKey = 'logto:admin_console:appearance_mode';
|
||||
export const profileSocialLinkingKeyPrefix = 'logto:admin_console:linking_social_connector';
|
||||
export const requestTimeout = 20_000;
|
||||
export const defaultPageSize = 20;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { builtInLanguageOptions as consoleBuiltInLanguageOptions } from '@logto/phrases';
|
||||
import { useLogto } from '@logto/react';
|
||||
import { AppearanceMode } from '@logto/schemas';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -19,6 +19,7 @@ import UserInfoCard from '@/components/UserInfoCard';
|
|||
import { getSignOutRedirectPathname } from '@/consts';
|
||||
import useCurrentUser from '@/hooks/use-current-user';
|
||||
import useUserPreferences from '@/hooks/use-user-preferences';
|
||||
import { DynamicAppearanceMode } from '@/types/appearance-mode';
|
||||
import { onKeyDownHandler } from '@/utils/a11y';
|
||||
|
||||
import SubMenu from '../SubMenu';
|
||||
|
@ -98,15 +99,15 @@ const UserInfo = () => {
|
|||
title="menu.appearance.label"
|
||||
options={[
|
||||
{
|
||||
value: AppearanceMode.SyncWithSystem,
|
||||
value: DynamicAppearanceMode.System,
|
||||
title: t('menu.appearance.system'),
|
||||
},
|
||||
{
|
||||
value: AppearanceMode.LightMode,
|
||||
value: Theme.Light,
|
||||
title: t('menu.appearance.light'),
|
||||
},
|
||||
{
|
||||
value: AppearanceMode.DarkMode,
|
||||
value: Theme.Dark,
|
||||
title: t('menu.appearance.dark'),
|
||||
},
|
||||
]}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { AdminConsoleKey } from '@logto/phrases';
|
||||
import { useContext } from 'react';
|
||||
import { Theme } from '@logto/schemas';
|
||||
|
||||
import DiscordDark from '@/assets/images/discord-dark.svg';
|
||||
import Discord from '@/assets/images/discord.svg';
|
||||
|
@ -8,8 +8,7 @@ import Email from '@/assets/images/email.svg';
|
|||
import GithubDark from '@/assets/images/github-dark.svg';
|
||||
import Github from '@/assets/images/github.svg';
|
||||
import { contactEmailLink, discordLink, githubIssuesLink } from '@/consts';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import { Theme } from '@/types/theme';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
type ContactItem = {
|
||||
icon: SvgComponent;
|
||||
|
@ -20,8 +19,8 @@ type ContactItem = {
|
|||
};
|
||||
|
||||
export const useContacts = (): ContactItem[] => {
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const isLightMode = theme === Theme.LightMode;
|
||||
const theme = useTheme();
|
||||
const isLightMode = theme === Theme.Light;
|
||||
|
||||
return [
|
||||
{
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { AppearanceMode } from '@logto/schemas';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import { conditionalString } from '@silverhand/essentials';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect, useMemo, useState, createContext } from 'react';
|
||||
|
||||
import useUserPreferences from '@/hooks/use-user-preferences';
|
||||
import { Theme } from '@/types/theme';
|
||||
import { DynamicAppearanceMode } from '@/types/appearance-mode';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -17,16 +17,18 @@ type AppTheme = {
|
|||
theme: Theme;
|
||||
};
|
||||
|
||||
const defaultTheme: Theme = Theme.Light;
|
||||
|
||||
const darkThemeWatchMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const getThemeBySystemConfiguration = (): Theme =>
|
||||
darkThemeWatchMedia.matches ? Theme.DarkMode : Theme.LightMode;
|
||||
darkThemeWatchMedia.matches ? Theme.Dark : Theme.Light;
|
||||
|
||||
export const AppThemeContext = createContext<AppTheme>({
|
||||
theme: Theme.LightMode,
|
||||
theme: defaultTheme,
|
||||
});
|
||||
|
||||
export const AppThemeProvider = ({ fixedTheme, children }: Props) => {
|
||||
const [theme, setTheme] = useState<Theme>(Theme.LightMode);
|
||||
const [theme, setTheme] = useState<Theme>(defaultTheme);
|
||||
|
||||
const {
|
||||
data: { appearanceMode },
|
||||
|
@ -39,8 +41,8 @@ export const AppThemeProvider = ({ fixedTheme, children }: Props) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (appearanceMode !== AppearanceMode.SyncWithSystem) {
|
||||
setTheme(appearanceMode === AppearanceMode.LightMode ? Theme.LightMode : Theme.DarkMode);
|
||||
if (appearanceMode !== DynamicAppearanceMode.System) {
|
||||
setTheme(appearanceMode);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
11
packages/console/src/hooks/use-theme.ts
Normal file
11
packages/console/src/hooks/use-theme.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { useContext } from 'react';
|
||||
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
|
||||
const useTheme = () => {
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
|
||||
return theme;
|
||||
};
|
||||
|
||||
export default useTheme;
|
|
@ -1,10 +1,10 @@
|
|||
import { builtInLanguages as builtInConsoleLanguages } from '@logto/phrases';
|
||||
import { AppearanceMode } from '@logto/schemas';
|
||||
import type { Nullable, Optional } from '@silverhand/essentials';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { themeStorageKey } from '@/consts';
|
||||
import { appearanceModeStorageKey } from '@/consts';
|
||||
import { appearanceModeGuard } from '@/types/appearance-mode';
|
||||
import { getAppearanceModeFromLocalStorage } from '@/utils/theme';
|
||||
|
||||
import useMeCustomData from './use-me-custom-data';
|
||||
|
||||
|
@ -12,7 +12,7 @@ const adminConsolePreferencesKey = 'adminConsolePreferences';
|
|||
|
||||
const userPreferencesGuard = z.object({
|
||||
language: z.enum(builtInConsoleLanguages).optional(),
|
||||
appearanceMode: z.nativeEnum(AppearanceMode),
|
||||
appearanceMode: appearanceModeGuard,
|
||||
experienceNoticeConfirmed: z.boolean().optional(),
|
||||
getStartedHidden: z.boolean().optional(),
|
||||
connectorSieNoticeConfirmed: z.boolean().optional(),
|
||||
|
@ -20,11 +20,6 @@ const userPreferencesGuard = z.object({
|
|||
|
||||
export type UserPreferences = z.infer<typeof userPreferencesGuard>;
|
||||
|
||||
const getEnumFromArray = <T extends string>(
|
||||
array: T[],
|
||||
value: Nullable<Optional<string>>
|
||||
): Optional<T> => array.find((element) => element === value);
|
||||
|
||||
const useUserPreferences = () => {
|
||||
const { data, error, isLoading, isLoaded, update: updateMeCustomData } = useMeCustomData();
|
||||
|
||||
|
@ -34,11 +29,7 @@ const useUserPreferences = () => {
|
|||
return parsed.success
|
||||
? parsed.data[adminConsolePreferencesKey]
|
||||
: {
|
||||
appearanceMode:
|
||||
getEnumFromArray(
|
||||
Object.values(AppearanceMode),
|
||||
localStorage.getItem(themeStorageKey)
|
||||
) ?? AppearanceMode.SyncWithSystem,
|
||||
appearanceMode: getAppearanceModeFromLocalStorage(),
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
|
@ -52,7 +43,7 @@ const useUserPreferences = () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(themeStorageKey, userPreferences.appearanceMode);
|
||||
localStorage.setItem(appearanceModeStorageKey, userPreferences.appearanceMode);
|
||||
}, [userPreferences.appearanceMode]);
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
|
@ -8,7 +9,6 @@ import AppBoundary from '@/containers/AppBoundary';
|
|||
import { AppThemeProvider } from '@/contexts/AppThemeProvider';
|
||||
import useSwrOptions from '@/hooks/use-swr-options';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
import { Theme } from '@/types/theme';
|
||||
|
||||
import * as styles from './App.module.scss';
|
||||
import AppContent from './containers/AppContent';
|
||||
|
@ -38,7 +38,7 @@ const App = () => {
|
|||
<BrowserRouter basename={getBasename()}>
|
||||
<div className={styles.app}>
|
||||
<SWRConfig value={swrOptions}>
|
||||
<AppThemeProvider fixedTheme={Theme.LightMode}>
|
||||
<AppThemeProvider fixedTheme={Theme.Light}>
|
||||
<AppBoundary>
|
||||
<Toast />
|
||||
<Routes>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { SignInExperience } from '@logto/schemas';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
|
@ -6,7 +7,6 @@ import { useState } from 'react';
|
|||
import LivePreviewButton from '@/components/LivePreviewButton';
|
||||
import SignInExperiencePreview from '@/components/SignInExperiencePreview';
|
||||
import { PreviewPlatform } from '@/components/SignInExperiencePreview/types';
|
||||
import { Theme } from '@/types/theme';
|
||||
|
||||
import PlatformTabs from '../PlatformTabs';
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -32,7 +32,7 @@ const Preview = ({ signInExperience, isLivePreviewDisabled = false, className }:
|
|||
</div>
|
||||
<SignInExperiencePreview
|
||||
platform={currentTab}
|
||||
mode={Theme.LightMode}
|
||||
mode={Theme.Light}
|
||||
signInExperience={signInExperience}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { Resource } from '@logto/schemas';
|
||||
import { defaultManagementApi } from '@logto/schemas';
|
||||
import { defaultManagementApi, Theme } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Outlet, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
|
@ -21,11 +21,10 @@ import RequestDataError from '@/components/RequestDataError';
|
|||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||
import TextLink from '@/components/TextLink';
|
||||
import { ApiResourceDetailsTabs } from '@/consts/page-tabs';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import type { RequestError } from '@/hooks/use-api';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
import * as detailsStyles from '@/scss/details.module.scss';
|
||||
import { Theme } from '@/types/theme';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
import { ApiResourceDetailsOutletContext } from './types';
|
||||
|
@ -37,8 +36,8 @@ const ApiResourceDetails = () => {
|
|||
const navigate = useNavigate();
|
||||
const { data, error, mutate } = useSWR<Resource, RequestError>(id && `api/resources/${id}`);
|
||||
const isLoading = !data && !error;
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const Icon = theme === Theme.LightMode ? ApiResource : ApiResourceDark;
|
||||
const theme = useTheme();
|
||||
const Icon = theme === Theme.Light ? ApiResource : ApiResourceDark;
|
||||
|
||||
const isOnPermissionPage = pathname.endsWith(ApiResourceDetailsTabs.Permissions);
|
||||
const isLogtoManagementApiResource = data?.indicator === defaultManagementApi.resource.indicator;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Resource } from '@logto/schemas';
|
||||
import { useContext } from 'react';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Modal from 'react-modal';
|
||||
|
@ -18,12 +18,11 @@ import Pagination from '@/components/Pagination';
|
|||
import Table from '@/components/Table';
|
||||
import { defaultPageSize } from '@/consts';
|
||||
import { ApiResourceDetailsTabs } from '@/consts/page-tabs';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import type { RequestError } from '@/hooks/use-api';
|
||||
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import * as resourcesStyles from '@/scss/resources.module.scss';
|
||||
import { Theme } from '@/types/theme';
|
||||
import { buildUrl } from '@/utils/url';
|
||||
|
||||
import CreateForm from './components/CreateForm';
|
||||
|
@ -53,10 +52,10 @@ const ApiResources = () => {
|
|||
|
||||
const isLoading = !data && !error;
|
||||
const navigate = useNavigate();
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const theme = useTheme();
|
||||
const [apiResources, totalCount] = data ?? [];
|
||||
|
||||
const ResourceIcon = theme === Theme.LightMode ? ApiResource : ApiResourceDark;
|
||||
const ResourceIcon = theme === Theme.Light ? ApiResource : ApiResourceDark;
|
||||
|
||||
return (
|
||||
<div className={resourcesStyles.container}>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useContext, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import CongratsDark from '@/assets/images/congrats-dark.svg';
|
||||
|
@ -11,9 +12,8 @@ import Card from '@/components/Card';
|
|||
import RadioGroup, { Radio } from '@/components/RadioGroup';
|
||||
import Select from '@/components/Select';
|
||||
import Spacer from '@/components/Spacer';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
import type { SupportedSdk } from '@/types/applications';
|
||||
import { Theme } from '@/types/theme';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -36,8 +36,8 @@ const SdkSelector = ({
|
|||
}: Props) => {
|
||||
const [isFolded, setIsFolded] = useState(isCompact);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const isLightMode = theme === Theme.LightMode;
|
||||
const theme = useTheme();
|
||||
const isLightMode = theme === Theme.Light;
|
||||
const CongratsIcon = isLightMode ? Congrats : CongratsDark;
|
||||
const TadaIcon = isLightMode ? Tada : TadaDark;
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useContext, useRef, useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import TadaDark from '@/assets/images/tada-dark.svg';
|
||||
import Tada from '@/assets/images/tada.svg';
|
||||
import Dropdown, { DropdownItem } from '@/components/Dropdown';
|
||||
import Index from '@/components/Index';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
import useUserPreferences from '@/hooks/use-user-preferences';
|
||||
import { Theme } from '@/types/theme';
|
||||
import { onKeyDownHandler } from '@/utils/a11y';
|
||||
|
||||
import useGetStartedMetadata from '../../hook';
|
||||
|
@ -19,8 +19,8 @@ const GetStartedProgress = () => {
|
|||
const {
|
||||
data: { getStartedHidden },
|
||||
} = useUserPreferences();
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const Icon = theme === Theme.LightMode ? Tada : TadaDark;
|
||||
const theme = useTheme();
|
||||
const Icon = theme === Theme.Light ? Tada : TadaDark;
|
||||
const anchorRef = useRef<HTMLDivElement>(null);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const { data, completedCount, totalCount } = useGetStartedMetadata();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { AdminConsoleKey } from '@logto/phrases';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
|
@ -20,9 +21,8 @@ import { discordLink, githubLink } from '@/consts';
|
|||
import { isCloud } from '@/consts/cloud';
|
||||
import { ConnectorsTabs } from '@/consts/page-tabs';
|
||||
import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import useConfigs from '@/hooks/use-configs';
|
||||
import { Theme } from '@/types/theme';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
type GetStartedMetadata = {
|
||||
id: string;
|
||||
|
@ -38,8 +38,8 @@ type GetStartedMetadata = {
|
|||
const useGetStartedMetadata = () => {
|
||||
const { configs, updateConfigs } = useConfigs();
|
||||
const { userEndpoint } = useContext(AppEndpointsContext);
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const isLightMode = theme === Theme.LightMode;
|
||||
const theme = useTheme();
|
||||
const isLightMode = theme === Theme.Light;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const data = useMemo(() => {
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
import { useContext } from 'react';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import NotFoundDarkImage from '@/assets/images/not-found-dark.svg';
|
||||
import NotFoundImage from '@/assets/images/not-found.svg';
|
||||
import Card from '@/components/Card';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import { Theme } from '@/types/theme';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const NotFound = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Card className={styles.content}>
|
||||
{theme === Theme.LightMode ? <NotFoundImage /> : <NotFoundDarkImage />}
|
||||
{theme === Theme.Light ? <NotFoundImage /> : <NotFoundDarkImage />}
|
||||
<div className={styles.message}>{t('errors.page_not_found')}</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { buildIdGenerator } from '@logto/core-kit';
|
||||
import type { ConnectorResponse, UserInfo } from '@logto/schemas';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import type { Optional } from '@silverhand/essentials';
|
||||
import { appendPath, conditional } from '@silverhand/essentials';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { is } from 'superstruct';
|
||||
|
@ -13,12 +14,11 @@ import ImageWithErrorFallback from '@/components/ImageWithErrorFallback';
|
|||
import UnnamedTrans from '@/components/UnnamedTrans';
|
||||
import UserInfoCard from '@/components/UserInfoCard';
|
||||
import { adminTenantEndpoint, getBasename, meApi, profileSocialLinkingKeyPrefix } from '@/consts';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import { useStaticApi } from '@/hooks/use-api';
|
||||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
import type { SocialUserInfo } from '@/types/profile';
|
||||
import { socialUserInfoGuard } from '@/types/profile';
|
||||
import { Theme } from '@/types/theme';
|
||||
|
||||
import { popupWindow } from '../../utils';
|
||||
import type { Row } from '../CardContent';
|
||||
|
@ -35,7 +35,7 @@ type Props = {
|
|||
const LinkAccountSection = ({ user, connectors, onUpdate }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const navigate = useNavigate();
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const theme = useTheme();
|
||||
const { show: showConfirm } = useConfirmModal();
|
||||
const api = useStaticApi({ prefixUrl: adminTenantEndpoint, resourceIndicator: meApi.indicator });
|
||||
|
||||
|
@ -61,7 +61,7 @@ const LinkAccountSection = ({ user, connectors, onUpdate }: Props) => {
|
|||
}
|
||||
|
||||
return connectors.map(({ id, name, logo, logoDark, target }) => {
|
||||
const logoSrc = theme === Theme.DarkMode && logoDark ? logoDark : logo;
|
||||
const logoSrc = theme === Theme.Dark && logoDark ? logoDark : logo;
|
||||
const relatedUserDetails = user.identities?.[target]?.details;
|
||||
const hasLinked = is(relatedUserDetails, socialUserInfoGuard);
|
||||
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import type { LanguageTag } from '@logto/language-kit';
|
||||
import { languages as uiLanguageNameMapping } from '@logto/language-kit';
|
||||
import type { SignInExperience } from '@logto/schemas';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import LivePreviewButton from '@/components/LivePreviewButton';
|
||||
import Select from '@/components/Select';
|
||||
import SignInExperiencePreview from '@/components/SignInExperiencePreview';
|
||||
import SignInExperiencePreview, { ToggleUiThemeButton } from '@/components/SignInExperiencePreview';
|
||||
import { PreviewPlatform } from '@/components/SignInExperiencePreview/types';
|
||||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||
import ToggleThemeButton from '@/components/ToggleThemeButton';
|
||||
import useUiLanguages from '@/hooks/use-ui-languages';
|
||||
import { Theme } from '@/types/theme';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -31,13 +30,13 @@ const Preview = ({
|
|||
}: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const [language, setLanguage] = useState<LanguageTag>('en');
|
||||
const [mode, setMode] = useState<Theme>(Theme.LightMode);
|
||||
const [mode, setMode] = useState<Theme>(Theme.Light);
|
||||
const [platform, setPlatform] = useState<PreviewPlatform>(PreviewPlatform.DesktopWeb);
|
||||
const { languages } = useUiLanguages();
|
||||
|
||||
useEffect(() => {
|
||||
if (!signInExperience?.color.isDarkModeEnabled) {
|
||||
setMode(Theme.LightMode);
|
||||
setMode(Theme.Light);
|
||||
}
|
||||
}, [mode, signInExperience]);
|
||||
|
||||
|
@ -71,7 +70,7 @@ const Preview = ({
|
|||
<div className={styles.title}>{t('sign_in_exp.preview.title')}</div>
|
||||
<div className={styles.selects}>
|
||||
{signInExperience?.color.isDarkModeEnabled && (
|
||||
<ToggleThemeButton value={mode} size="small" onToggle={setMode} />
|
||||
<ToggleUiThemeButton value={mode} size="small" onToggle={setMode} />
|
||||
)}
|
||||
<Select
|
||||
className={styles.language}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { useContext, useState } from 'react';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import WelcomeImageDark from '@/assets/images/sign-in-experience-welcome-dark.svg';
|
||||
import WelcomeImage from '@/assets/images/sign-in-experience-welcome.svg';
|
||||
import Button from '@/components/Button';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import { Theme } from '@/types/theme';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
import GuideModal from './GuideModal';
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -17,16 +17,13 @@ type Props = {
|
|||
const Welcome = ({ mutate }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const theme = useTheme();
|
||||
const WelcomeIcon = theme === Theme.Light ? WelcomeImage : WelcomeImageDark;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
{theme === Theme.LightMode ? (
|
||||
<WelcomeImage className={styles.icon} />
|
||||
) : (
|
||||
<WelcomeImageDark className={styles.icon} />
|
||||
)}
|
||||
<WelcomeIcon className={styles.icon} />
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.title}>{t('sign_in_exp.welcome.title')}</div>
|
||||
<div className={styles.description}>{t('sign_in_exp.welcome.description')}</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { LogtoClientError, useLogto } from '@logto/react';
|
||||
import classNames from 'classnames';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useHref } from 'react-router-dom';
|
||||
|
||||
|
@ -8,7 +8,7 @@ import Logo from '@/assets/images/logo.svg';
|
|||
import AppError from '@/components/AppError';
|
||||
import Button from '@/components/Button';
|
||||
import SessionExpired from '@/components/SessionExpired';
|
||||
import { AppThemeContext } from '@/contexts/AppThemeProvider';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -17,7 +17,7 @@ const Welcome = () => {
|
|||
const navigate = useNavigate();
|
||||
const { isAuthenticated, error, signIn } = useLogto();
|
||||
const href = useHref('/callback');
|
||||
const { theme } = useContext(AppThemeContext);
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
// If Authenticated, navigate to the Admin Console root page. directly
|
||||
|
|
10
packages/console/src/types/appearance-mode.ts
Normal file
10
packages/console/src/types/appearance-mode.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import { z } from 'zod';
|
||||
|
||||
export enum DynamicAppearanceMode {
|
||||
System = 'system',
|
||||
}
|
||||
|
||||
export const appearanceModeGuard = z.nativeEnum(Theme).or(z.nativeEnum(DynamicAppearanceMode));
|
||||
|
||||
export type AppearanceMode = z.infer<typeof appearanceModeGuard>;
|
|
@ -1,4 +0,0 @@
|
|||
export enum Theme {
|
||||
LightMode = 'light',
|
||||
DarkMode = 'dark',
|
||||
}
|
|
@ -1,23 +1,23 @@
|
|||
import { AppearanceMode } from '@logto/schemas';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import { trySafe } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { themeStorageKey } from '@/consts';
|
||||
import { Theme } from '@/types/theme';
|
||||
import { appearanceModeStorageKey } from '@/consts';
|
||||
import { appearanceModeGuard, DynamicAppearanceMode } from '@/types/appearance-mode';
|
||||
import type { AppearanceMode } from '@/types/appearance-mode';
|
||||
|
||||
export const getTheme = (appearanceMode: AppearanceMode): Theme => {
|
||||
if (appearanceMode !== AppearanceMode.SyncWithSystem) {
|
||||
return appearanceMode === AppearanceMode.LightMode ? Theme.LightMode : Theme.DarkMode;
|
||||
if (appearanceMode !== DynamicAppearanceMode.System) {
|
||||
return appearanceMode;
|
||||
}
|
||||
|
||||
const darkThemeWatchMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const theme = darkThemeWatchMedia.matches ? Theme.DarkMode : Theme.LightMode;
|
||||
const theme = darkThemeWatchMedia.matches ? Theme.Dark : Theme.Light;
|
||||
|
||||
return theme;
|
||||
};
|
||||
|
||||
export const getThemeFromLocalStorage = () =>
|
||||
getTheme(
|
||||
trySafe(() => z.nativeEnum(AppearanceMode).parse(localStorage.getItem(themeStorageKey))) ??
|
||||
AppearanceMode.SyncWithSystem
|
||||
);
|
||||
export const getThemeFromLocalStorage = () => getTheme(getAppearanceModeFromLocalStorage());
|
||||
|
||||
export const getAppearanceModeFromLocalStorage = (): AppearanceMode =>
|
||||
trySafe(() => appearanceModeGuard.parse(localStorage.getItem(appearanceModeStorageKey))) ??
|
||||
DynamicAppearanceMode.System;
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
import type { DatabaseTransactionConnection } from 'slonik';
|
||||
import { sql } from 'slonik';
|
||||
|
||||
import type { AlterationScript } from '../lib/types/alteration.js';
|
||||
|
||||
const adminConsoleConfigKey = 'adminConsole';
|
||||
const defaultAppearanceMode = 'system';
|
||||
const defaultLanguage = 'en';
|
||||
|
||||
type OldAdminConsoleData = {
|
||||
language: string;
|
||||
appearanceMode: string;
|
||||
} & Record<string, unknown>;
|
||||
|
||||
type OldLogtoAdminConsoleConfig = {
|
||||
tenantId: string;
|
||||
value: OldAdminConsoleData;
|
||||
};
|
||||
|
||||
type NewAdminConsoleData = Omit<OldAdminConsoleData, 'language' | 'appearanceMode'>;
|
||||
|
||||
type NewLogtoAdminConsoleConfig = {
|
||||
tenantId: string;
|
||||
value: NewAdminConsoleData;
|
||||
};
|
||||
|
||||
const alterAdminConsoleData = async (
|
||||
logtoConfig: OldLogtoAdminConsoleConfig,
|
||||
pool: DatabaseTransactionConnection
|
||||
) => {
|
||||
const { tenantId, value: oldAdminConsoleData } = logtoConfig;
|
||||
|
||||
const {
|
||||
language, // Extract to remove from config
|
||||
appearanceMode, // Extract to remove from config
|
||||
...others
|
||||
} = oldAdminConsoleData;
|
||||
|
||||
const newAdminConsoleData: NewAdminConsoleData = {
|
||||
...others,
|
||||
};
|
||||
|
||||
await pool.query(
|
||||
sql`update logto_configs set value = ${JSON.stringify(
|
||||
newAdminConsoleData
|
||||
)} where tenant_id = ${tenantId} and key = ${adminConsoleConfigKey}`
|
||||
);
|
||||
};
|
||||
|
||||
const rollbackAdminConsoleData = async (
|
||||
logtoConfig: NewLogtoAdminConsoleConfig,
|
||||
pool: DatabaseTransactionConnection
|
||||
) => {
|
||||
const { tenantId, value: newAdminConsoleData } = logtoConfig;
|
||||
|
||||
const oldAdminConsoleData: OldAdminConsoleData = {
|
||||
...newAdminConsoleData,
|
||||
language: defaultLanguage,
|
||||
appearanceMode: defaultAppearanceMode,
|
||||
};
|
||||
|
||||
await pool.query(
|
||||
sql`update logto_configs set value = ${JSON.stringify(
|
||||
oldAdminConsoleData
|
||||
)} where tenant_id = ${tenantId} and key = ${adminConsoleConfigKey}`
|
||||
);
|
||||
};
|
||||
|
||||
const alteration: AlterationScript = {
|
||||
up: async (pool) => {
|
||||
const rows = await pool.many<OldLogtoAdminConsoleConfig>(
|
||||
sql`select * from logto_configs where key = ${adminConsoleConfigKey}`
|
||||
);
|
||||
await Promise.all(rows.map(async (row) => alterAdminConsoleData(row, pool)));
|
||||
},
|
||||
down: async (pool) => {
|
||||
const rows = await pool.many<NewLogtoAdminConsoleConfig>(
|
||||
sql`select * from logto_configs where key = ${adminConsoleConfigKey}`
|
||||
);
|
||||
await Promise.all(rows.map(async (row) => rollbackAdminConsoleData(row, pool)));
|
||||
},
|
||||
};
|
||||
|
||||
export default alteration;
|
|
@ -147,18 +147,6 @@ export const customContentGuard = z.record(z.string());
|
|||
|
||||
export type CustomContent = z.infer<typeof customContentGuard>;
|
||||
|
||||
/* === Logto Configs === */
|
||||
|
||||
/**
|
||||
* Settings
|
||||
*/
|
||||
|
||||
export enum AppearanceMode {
|
||||
SyncWithSystem = 'system',
|
||||
LightMode = 'light',
|
||||
DarkMode = 'dark',
|
||||
}
|
||||
|
||||
/* === Phrases === */
|
||||
|
||||
export type Translation = {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { CreateLogtoConfig } from '../db-entries/index.js';
|
||||
import { AppearanceMode } from '../foundations/index.js';
|
||||
import type { AdminConsoleData } from '../types/index.js';
|
||||
import { LogtoTenantConfigKey } from '../types/index.js';
|
||||
|
||||
|
@ -14,8 +13,6 @@ export const createDefaultAdminConsoleConfig = (
|
|||
tenantId: forTenantId,
|
||||
key: LogtoTenantConfigKey.AdminConsole,
|
||||
value: {
|
||||
language: 'en',
|
||||
appearanceMode: AppearanceMode.SyncWithSystem,
|
||||
livePreviewChecked: false,
|
||||
applicationCreated: false,
|
||||
signInExperienceCustomized: false,
|
||||
|
|
|
@ -15,3 +15,4 @@ export * from './tenant.js';
|
|||
export * from './user-assets.js';
|
||||
export * from './hook.js';
|
||||
export * from './service-log.js';
|
||||
export * from './theme.js';
|
||||
|
|
4
packages/schemas/src/types/theme.ts
Normal file
4
packages/schemas/src/types/theme.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export enum Theme {
|
||||
Light = 'light',
|
||||
Dark = 'dark',
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useContext } from 'react';
|
||||
|
||||
|
@ -20,6 +21,7 @@ type Props = {
|
|||
|
||||
const LogtoSignature = ({ className }: Props) => {
|
||||
const { theme } = useContext(PageContext);
|
||||
const LogtoLogo = theme === Theme.Light ? LogtoLogoLight : LogtoLogtoDark;
|
||||
|
||||
return (
|
||||
<a
|
||||
|
@ -31,11 +33,7 @@ const LogtoSignature = ({ className }: Props) => {
|
|||
>
|
||||
<span className={styles.text}>Powered by</span>
|
||||
<LogtoLogoShadow className={styles.staticIcon} />
|
||||
{theme === 'light' ? (
|
||||
<LogtoLogoLight className={styles.highlightIcon} />
|
||||
) : (
|
||||
<LogtoLogtoDark className={styles.highlightIcon} />
|
||||
)}
|
||||
<LogtoLogo className={styles.highlightIcon} />
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useContext } from 'react';
|
||||
|
||||
|
@ -21,7 +22,7 @@ const SocialLanding = ({ className, connectorId, isLoading = false }: Props) =>
|
|||
<div className={styles.connector}>
|
||||
{connector ? (
|
||||
<img
|
||||
src={theme === 'dark' ? connector.logoDark ?? connector.logo : connector.logo}
|
||||
src={theme === Theme.Dark ? connector.logoDark ?? connector.logo : connector.logo}
|
||||
alt="logo"
|
||||
/>
|
||||
) : (
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { absoluteDarken, absoluteLighten } from '@logto/core-kit';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import color from 'color';
|
||||
import { useEffect, useContext } from 'react';
|
||||
|
||||
|
@ -34,7 +35,7 @@ const useColorTheme = () => {
|
|||
|
||||
const lightPrimary = color(primaryColor);
|
||||
|
||||
if (theme === 'light') {
|
||||
if (theme === Theme.Light) {
|
||||
const lightColorLibrary = generateLightColorLibrary(lightPrimary);
|
||||
|
||||
for (const [key, value] of Object.entries(lightColorLibrary)) {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import { noop } from '@silverhand/essentials';
|
||||
import { useState, useMemo, createContext } from 'react';
|
||||
import { isMobile } from 'react-device-detect';
|
||||
|
||||
import type { SignInExperienceResponse, Platform, Theme } from '@/types';
|
||||
import type { SignInExperienceResponse, Platform } from '@/types';
|
||||
import { parseQueryParameters } from '@/utils';
|
||||
|
||||
export type Context = {
|
||||
|
@ -23,7 +24,7 @@ export type Context = {
|
|||
|
||||
export const PageContext = createContext<Context>({
|
||||
toast: '',
|
||||
theme: 'light',
|
||||
theme: Theme.Light,
|
||||
loading: false,
|
||||
platform: isMobile ? 'mobile' : 'web',
|
||||
termsAgreement: false,
|
||||
|
@ -40,7 +41,7 @@ export const PageContext = createContext<Context>({
|
|||
const usePageContext = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [toast, setToast] = useState('');
|
||||
const [theme, setTheme] = useState<Theme>('light');
|
||||
const [theme, setTheme] = useState<Theme>(Theme.Light);
|
||||
const [platform, setPlatform] = useState<Platform>(isMobile ? 'mobile' : 'web');
|
||||
const [experienceSettings, setExperienceSettings] = useState<SignInExperienceResponse>();
|
||||
const [termsAgreement, setTermsAgreement] = useState(false);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import { useEffect, useContext } from 'react';
|
||||
|
||||
import type { Theme } from '@/types';
|
||||
|
||||
import { PageContext } from './use-page-context';
|
||||
|
||||
const darkThemeWatchMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const getThemeBySystemConfiguration = (): Theme => (darkThemeWatchMedia.matches ? 'dark' : 'light');
|
||||
const getThemeBySystemConfiguration = (): Theme =>
|
||||
darkThemeWatchMedia.matches ? Theme.Dark : Theme.Light;
|
||||
|
||||
export default function useTheme(): Theme {
|
||||
const { isPreview, experienceSettings, theme, setTheme } = useContext(PageContext);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import { useContext } from 'react';
|
||||
import type { TFuncKey } from 'react-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -30,7 +31,7 @@ const ErrorPage = ({ title = 'description.not_found', message, rawMessage, isRoo
|
|||
<StaticPageLayout>
|
||||
{!isRootPath && <NavBar />}
|
||||
<div className={styles.container}>
|
||||
{theme === 'light' ? <EmptyState /> : <EmptyStateDark />}
|
||||
{theme === Theme.Light ? <EmptyState /> : <EmptyStateDark />}
|
||||
<div className={styles.title}>{t(title)}</div>
|
||||
{errorMessage && <div className={styles.message}>{String(errorMessage)}</div>}
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
import type {
|
||||
SignInExperience,
|
||||
ConnectorMetadata,
|
||||
AppearanceMode,
|
||||
SignInIdentifier,
|
||||
} from '@logto/schemas';
|
||||
import type { SignInExperience, ConnectorMetadata, SignInIdentifier, Theme } from '@logto/schemas';
|
||||
|
||||
export enum UserFlow {
|
||||
signIn = 'sign-in',
|
||||
|
@ -20,9 +15,6 @@ export enum SearchParameters {
|
|||
|
||||
export type Platform = 'web' | 'mobile';
|
||||
|
||||
// TODO: @simeng, @sijie, @charles should we combine this with admin console?
|
||||
export type Theme = 'dark' | 'light';
|
||||
|
||||
export type VerificationCodeIdentifier = SignInIdentifier.Email | SignInIdentifier.Phone;
|
||||
|
||||
// Omit socialSignInConnectorTargets since it is being translated into socialConnectors
|
||||
|
@ -42,7 +34,7 @@ export enum ConfirmModalMessage {
|
|||
export type PreviewConfig = {
|
||||
signInExperience: SignInExperienceResponse;
|
||||
language: string;
|
||||
mode: AppearanceMode.LightMode | AppearanceMode.DarkMode;
|
||||
mode: Theme;
|
||||
platform: Platform;
|
||||
isNative: boolean;
|
||||
};
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import type { Branding } from '@logto/schemas';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import type { Nullable } from '@silverhand/essentials';
|
||||
|
||||
import type { Theme } from '@/types';
|
||||
|
||||
export type GetLogoUrl = {
|
||||
theme: Theme;
|
||||
logoUrl: string;
|
||||
|
@ -11,7 +10,7 @@ export type GetLogoUrl = {
|
|||
};
|
||||
|
||||
export const getLogoUrl = ({ theme, logoUrl, darkLogoUrl, isApple }: GetLogoUrl) => {
|
||||
if (theme === (isApple ? 'light' : 'dark')) {
|
||||
if (theme === (isApple ? Theme.Light : Theme.Dark)) {
|
||||
return darkLogoUrl ?? logoUrl;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue