feat: high-contrast theme (#29805)

pull/29955/head
Júlia Jaeger Foresti 2 years ago committed by GitHub
parent 6d453f71ac
commit 357a3a50fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      .changeset/smooth-planes-cough.md
  2. 4
      apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx
  3. 12
      apps/meteor/client/providers/UserProvider/UserProvider.tsx
  4. 43
      apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx
  5. 33
      apps/meteor/client/sidebar/header/hooks/useThemeItems.tsx
  6. 24
      apps/meteor/client/sidebar/header/hooks/useUserMenu.tsx
  7. 21
      apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx
  8. 8
      apps/meteor/client/views/account/routes.tsx
  9. 15
      apps/meteor/client/views/account/sidebarItems.tsx
  10. 41
      apps/meteor/client/views/account/themes/HighContrastUpsellModal.tsx
  11. 103
      apps/meteor/client/views/account/themes/ThemePage.tsx
  12. 24
      apps/meteor/client/views/account/themes/themeItems.ts
  13. 11
      apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
  14. BIN
      apps/meteor/public/images/high-contrast-upsell-modal.png
  15. 3
      apps/meteor/server/methods/saveUserPreferences.ts
  16. 15
      ee/packages/ui-theming/src/PaletteStyleTag.tsx
  17. 25
      ee/packages/ui-theming/src/hooks/useThemeMode.ts
  18. 10
      ee/packages/ui-theming/src/palette.ts
  19. 210
      ee/packages/ui-theming/src/paletteHighContrast.ts
  20. 3
      ee/packages/ui-theming/src/types/themes.ts
  21. 3
      packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts

@ -0,0 +1,7 @@
---
'@rocket.chat/ui-theming': minor
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---
feat: high-contrast theme

@ -15,7 +15,7 @@ type GenericUpsellModalProps = {
icon?: IconName;
img: ComponentProps<typeof Modal.HeroImage>['src'];
onCancel?: () => void;
onClose?: () => void;
onClose: () => void;
onConfirm?: () => void;
annotation?: ReactNode;
} & ComponentProps<typeof Modal>;
@ -30,8 +30,8 @@ const GenericUpsellModal = ({
icon,
description,
onCancel,
onClose,
onConfirm,
onClose = onCancel,
annotation,
...props
}: GenericUpsellModalProps) => {

@ -1,6 +1,6 @@
import type { IRoom, ISubscription, IUser } from '@rocket.chat/core-typings';
import { useLocalStorage } from '@rocket.chat/fuselage-hooks';
import { UserContext, useSetting } from '@rocket.chat/ui-contexts';
import { UserContext, useEndpoint, useSetting } from '@rocket.chat/ui-contexts';
import type { LoginService, SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { Meteor } from 'meteor/meteor';
import type { ContextType, ReactElement, ReactNode } from 'react';
@ -10,6 +10,7 @@ import { Subscriptions, ChatRoom } from '../../../app/models/client';
import { getUserPreference } from '../../../app/utils/client';
import { sdk } from '../../../app/utils/client/lib/SDKClient';
import { afterLogoutCleanUpCallback } from '../../../lib/callbacks/afterLogoutCleanUpCallback';
import { useIsEnterprise } from '../../hooks/useIsEnterprise';
import { useReactiveValue } from '../../hooks/useReactiveValue';
import { createReactiveSubscriptionFactory } from '../../lib/createReactiveSubscriptionFactory';
import { useEmailVerificationWarning } from './hooks/useEmailVerificationWarning';
@ -66,6 +67,9 @@ const UserProvider = ({ children }: UserProviderProps): ReactElement => {
const user = useReactiveValue(getUser);
const [language, setLanguage] = useLocalStorage('userLanguage', user?.language ?? 'en');
const { data: license } = useIsEnterprise();
const setUserPreferences = useEndpoint('POST', '/v1/users.setPreferences');
const loginMethod: LoginMethods = (isLdapEnabled && 'loginWithLDAP') || (isCrowdEnabled && 'loginWithCrowd') || 'loginWithPassword';
useLDAPAndCrowdCollisionWarning();
@ -166,6 +170,12 @@ const UserProvider = ({ children }: UserProviderProps): ReactElement => {
}
}, [user?.language, language, setLanguage]);
useEffect(() => {
if (!license?.isEnterprise && user?.settings?.preferences?.themeAppearence === 'high-contrast') {
setUserPreferences({ data: { themeAppearence: 'light' } });
}
}, [license?.isEnterprise, setUserPreferences, user?.settings?.preferences?.themeAppearence]);
return <UserContext.Provider children={children} value={contextValue} />;
};

@ -1,29 +1,28 @@
import { Badge } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { defaultFeaturesPreview, useFeaturePreviewList } from '@rocket.chat/ui-client';
import { useLogout, useRoute, useTranslation } from '@rocket.chat/ui-contexts';
import { useRouter, useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';
import type { GenericMenuItemProps } from '../../../components/GenericMenu/GenericMenuItem';
export const useAccountItems = (): GenericMenuItemProps[] => {
const t = useTranslation();
const accountRoute = useRoute('account-index');
const featurePreviewRoute = useRoute('feature-preview');
const { unseenFeatures, featurePreviewEnabled } = useFeaturePreviewList();
const router = useRouter();
const logout = useLogout();
const { unseenFeatures, featurePreviewEnabled } = useFeaturePreviewList();
const handleMyAccount = useMutableCallback(() => {
accountRoute.push({});
router.navigate('/account');
});
const handleFeaturePreview = useMutableCallback(() => {
featurePreviewRoute.push();
const handleThemes = useMutableCallback(() => {
router.navigate('/account/theme');
});
const handleLogout = useMutableCallback(() => {
logout();
const handlePreferences = useMutableCallback(() => {
router.navigate('/account/preferences');
});
const handleFeaturePreview = useMutableCallback(() => {
router.navigate('/account/feature-preview');
});
const featurePreviewItem = {
@ -42,17 +41,23 @@ export const useAccountItems = (): GenericMenuItemProps[] => {
return [
{
id: 'my-account',
id: 'profile',
icon: 'user',
content: t('My_Account'),
content: t('Profile'),
onClick: handleMyAccount,
},
...(featurePreviewEnabled && defaultFeaturesPreview.length > 0 ? [featurePreviewItem] : []),
{
id: 'logout',
icon: 'sign-out',
content: t('Logout'),
onClick: handleLogout,
id: 'theme',
icon: 'palette',
content: t('Theme'),
onClick: handleThemes,
},
{
id: 'preferences',
icon: 'customize',
content: t('Preferences'),
onClick: handlePreferences,
},
...(featurePreviewEnabled && defaultFeaturesPreview.length > 0 ? [featurePreviewItem] : []),
];
};

@ -1,33 +0,0 @@
import { RadioButton } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import { useThemeMode } from '@rocket.chat/ui-theming/src/hooks/useThemeMode';
import React from 'react';
import type { GenericMenuItemProps } from '../../../components/GenericMenu/GenericMenuItem';
export const useThemeItems = (): GenericMenuItemProps[] => {
const t = useTranslation();
const [selectedTheme, setTheme] = useThemeMode();
return [
{
id: 'light',
icon: 'sun',
content: t('Theme_light'),
addon: <RadioButton checked={selectedTheme === 'light'} onChange={setTheme('light')} m='x4' />,
},
{
id: 'dark',
icon: 'moon',
content: t('Theme_dark'),
addon: <RadioButton checked={selectedTheme === 'dark'} onChange={setTheme('dark')} m='x4' />,
},
{
id: 'auto',
icon: 'desktop',
content: t('Theme_match_system'),
addon: <RadioButton checked={selectedTheme === 'auto'} onChange={setTheme('auto')} m='x4' />,
},
];
};

@ -1,19 +1,31 @@
import type { IUser } from '@rocket.chat/core-typings';
import { useTranslation } from '@rocket.chat/ui-contexts';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useLogout, useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';
import type { GenericMenuItemProps } from '../../../components/GenericMenu/GenericMenuItem';
import UserMenuHeader from '../UserMenuHeader';
import { useAccountItems } from './useAccountItems';
import { useStatusItems } from './useStatusItems';
import { useThemeItems } from './useThemeItems';
export const useUserMenu = (user: IUser) => {
const t = useTranslation();
const statusItems = useStatusItems(user);
const themeItems = useThemeItems();
const accountItems = useAccountItems();
const logout = useLogout();
const handleLogout = useMutableCallback(() => {
logout();
});
const logoutItem: GenericMenuItemProps = {
id: 'logout',
icon: 'sign-out',
content: t('Logout'),
onClick: handleLogout,
};
return [
{
title: <UserMenuHeader user={user} />,
@ -24,11 +36,11 @@ export const useUserMenu = (user: IUser) => {
items: statusItems,
},
{
title: t('Theme'),
items: themeItems,
title: t('Account'),
items: accountItems,
},
{
items: accountItems,
items: [logoutItem],
},
];
};

@ -1,5 +1,5 @@
import type { SelectOption } from '@rocket.chat/fuselage';
import { Select, Accordion, Field, FieldGroup, MultiSelect } from '@rocket.chat/fuselage';
import { Accordion, Field, FieldGroup, MultiSelect } from '@rocket.chat/fuselage';
import { useUserPreference, useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { useMemo } from 'react';
@ -11,7 +11,6 @@ const PreferencesGlobalSection = ({ onChange, commitRef, ...props }: FormSection
const t = useTranslation();
const userDontAskAgainList = useUserPreference<{ action: string; label: string }[]>('dontAskAgainList');
const themePreference = useUserPreference<'light' | 'dark' | 'auto'>('themeAppearence');
const options = useMemo(
() => (userDontAskAgainList || []).map(({ action, label }) => [action, label]) as SelectOption[],
@ -23,26 +22,18 @@ const PreferencesGlobalSection = ({ onChange, commitRef, ...props }: FormSection
const { values, handlers, commit } = useForm(
{
dontAskAgainList: selectedOptions,
themeAppearence: themePreference,
},
onChange,
);
const { dontAskAgainList, themeAppearence } = values as {
const { dontAskAgainList } = values as {
dontAskAgainList: string[];
themeAppearence: string;
};
const { handleDontAskAgainList, handleThemeAppearence } = handlers;
const { handleDontAskAgainList } = handlers;
commitRef.current.global = commit;
const themeOptions: SelectOption[] = [
['auto', t('Theme_match_system')],
['light', t('Theme_light')],
['dark', t('Theme_dark')],
];
return (
<Accordion.Item title={t('Global')} {...props}>
<FieldGroup>
@ -57,12 +48,6 @@ const PreferencesGlobalSection = ({ onChange, commitRef, ...props }: FormSection
/>
</Field.Row>
</Field>
<Field>
<Field.Label>{t('Theme_Appearence')}</Field.Label>
<Field.Row>
<Select value={themeAppearence} onChange={handleThemeAppearence} options={themeOptions} />
</Field.Row>
</Field>
</FieldGroup>
</Accordion.Item>
);

@ -36,6 +36,10 @@ declare module '@rocket.chat/ui-contexts' {
pathname: '/account/feature-preview';
pattern: '/account/feature-preview';
};
'theme': {
pathname: '/account/theme';
pattern: '/account/theme';
};
}
}
@ -79,3 +83,7 @@ registerAccountRoute('/feature-preview', {
name: 'feature-preview',
component: lazy(() => import('./featurePreview/AccountFeaturePreviewPage')),
});
registerAccountRoute('/theme', {
name: 'theme',
component: lazy(() => import('./themes/ThemePage')),
});

@ -12,17 +12,22 @@ export const {
getSidebarItems: getAccountSidebarItems,
subscribeToSidebarItems: subscribeToAccountSidebarItems,
} = createSidebarItems([
{
href: '/account/preferences',
i18nLabel: 'Preferences',
icon: 'customize',
},
{
href: '/account/profile',
i18nLabel: 'Profile',
icon: 'user',
permissionGranted: (): boolean => settings.get('Accounts_AllowUserProfileChange'),
},
{
href: '/account/theme',
i18nLabel: 'Theme',
icon: 'palette',
},
{
href: '/account/preferences',
i18nLabel: 'Preferences',
icon: 'customize',
},
{
href: '/account/security',
i18nLabel: 'Security',

@ -0,0 +1,41 @@
import { useRole, useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';
import GenericUpsellModal from '../../../components/GenericUpsellModal';
import { useUpsellActions } from '../../../components/GenericUpsellModal/hooks';
const HighContrastUpsellModal = ({ onClose }: { onClose: () => void }) => {
const t = useTranslation();
const isAdmin = useRole('admin');
const { handleGoFullyFeatured, handleTalkToSales } = useUpsellActions();
if (!isAdmin) {
return (
<GenericUpsellModal
title={t('High_contrast_upsell_title')}
img='images/high-contrast-upsell-modal.png'
subtitle={t('High_contrast_upsell_subtitle')}
description={t('High_contrast_upsell_description')}
onClose={onClose}
onCancel={onClose}
cancelText={t('Close')}
annotation={t('High_contrast_upsell_annotation')}
/>
);
}
return (
<GenericUpsellModal
title={t('High_contrast_upsell_title')}
img='images/high-contrast-upsell-modal.png'
subtitle={t('High_contrast_upsell_subtitle')}
description={t('High_contrast_upsell_description')}
onClose={onClose}
onCancel={handleTalkToSales}
onConfirm={handleGoFullyFeatured}
cancelText={t('Talk_to_sales')}
confirmText={t('Start_free_trial')}
/>
);
};
export default HighContrastUpsellModal;

@ -0,0 +1,103 @@
import { Accordion, Box, Button, ButtonGroup, Field, RadioButton, Tag } from '@rocket.chat/fuselage';
import { ExternalLink } from '@rocket.chat/ui-client';
import { useEndpoint, useSetModal, useToastMessageDispatch, useTranslation, useUserPreference } from '@rocket.chat/ui-contexts';
import type { ThemePreference } from '@rocket.chat/ui-theming/src/types/themes';
import React from 'react';
import { useForm } from 'react-hook-form';
import Page from '../../../components/Page';
import { useIsEnterprise } from '../../../hooks/useIsEnterprise';
import HighContrastUpsellModal from './HighContrastUpsellModal';
import { themeItems as themes } from './themeItems';
const ThemePage = () => {
const t = useTranslation();
const setModal = useSetModal();
const dispatchToastMessage = useToastMessageDispatch();
const { data: license } = useIsEnterprise();
const themePreference = useUserPreference<ThemePreference>('themeAppearence') || 'auto';
const setUserPreferences = useEndpoint('POST', '/v1/users.setPreferences');
const {
formState: { isDirty },
handleSubmit,
register,
reset,
} = useForm({
defaultValues: { themeAppearence: themePreference },
});
const handleSave = async ({ themeAppearence }: { themeAppearence: ThemePreference }) => {
try {
await setUserPreferences({ data: { themeAppearence } });
dispatchToastMessage({ type: 'success', message: t('Preferences_saved') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
reset({ themeAppearence });
}
};
return (
<Page>
<Page.Header title={t('Theme')}>
<ButtonGroup>
<Button primary disabled={!isDirty} onClick={handleSubmit(handleSave)}>
{t('Save_changes')}
</Button>
</ButtonGroup>
</Page.Header>
<Page.ScrollableContentWithShadow>
<Box maxWidth='x600' w='full' alignSelf='center' mb='x40' mi='x36'>
<Box fontScale='p1' mbe='x24'>
<Box pb='x16'>{t('Choose_theme_description')}</Box>
</Box>
<Accordion>
<Accordion.Item defaultExpanded={true} title={t('Theme')}>
{themes.map(({ id, title, description, ...item }, index) => {
const externalLink = 'externalLink' in item && item.externalLink;
const communityDisabled = 'isEEOnly' in item && item.isEEOnly && !license?.isEnterprise;
return (
<Field key={id} pbe={themes.length - 1 ? undefined : 'x28'} pbs={index === 0 ? undefined : 'x28'}>
<Box display='flex' flexDirection='row' justifyContent='spaceBetween' flexGrow={1}>
<Field.Label display='flex' alignItems='center' htmlFor={id}>
{t.has(title) ? t(title) : title}
{communityDisabled && (
<Box is='span' mis='x8'>
<Tag variant='featured'>{t('Enterprise')}</Tag>
</Box>
)}
</Field.Label>
<Field.Row>
{communityDisabled ? (
<RadioButton
onClick={() => setModal(<HighContrastUpsellModal onClose={() => setModal(null)} />)}
checked={false}
/>
) : (
<RadioButton id={id} {...register('themeAppearence')} value={id} />
)}
</Field.Row>
</Box>
<Field.Hint mbs='x12' style={{ whiteSpace: 'break-spaces' }}>
{t.has(description) ? t(description) : description}
{externalLink && communityDisabled && (
<Box mbs='x12'>
<ExternalLink to={externalLink}>{t('Talk_to_an_expert')}</ExternalLink>
</Box>
)}
</Field.Hint>
</Field>
);
})}
</Accordion.Item>
</Accordion>
</Box>
</Page.ScrollableContentWithShadow>
</Page>
);
};
export default ThemePage;

@ -0,0 +1,24 @@
export const themeItems = [
{
id: 'light',
title: 'Theme_light',
description: 'Theme_light_description',
},
{
id: 'dark',
title: 'Theme_dark',
description: 'Theme_dark_description',
},
{
id: 'auto',
title: 'Theme_match_system',
description: 'Theme_match_system_description',
},
{
isEEOnly: true,
id: 'high-contrast',
title: 'Theme_high_contrast',
externalLink: 'https://www.rocket.chat/sales-contact',
description: 'Theme_high_contrast_description',
},
];

@ -4892,6 +4892,8 @@
"The_user_will_be_removed_from_s": "The user will be removed from %s",
"The_user_wont_be_able_to_type_in_s": "The user won't be able to type in %s",
"Theme": "Theme",
"Themes": "Themes",
"Choose_theme_description": "Choose the interface appearance that best suits your needs.",
"theme-color-attention-color": "Attention Color",
"theme-color-component-color": "Component Color",
"theme-color-content-background-color": "Content Background Color",
@ -5800,7 +5802,9 @@
"Something_Went_Wrong": "Something went wrong",
"Toolbox_room_actions": "Primary Room actions",
"Theme_light": "Light",
"Theme_light_description": "More accessible for individuals with visual impairments and a good choice for well-lit environments.",
"Theme_dark": "Dark",
"Theme_dark_description": "Reduce eye strain and fatigue in low-light conditions by minimizing the amount of light emitted by the screen.",
"Enable_of_limit_apps_currently_enabled": "**{{enabled}} of {{limit}} {{context}} apps currently enabled.** \n \nWorkspaces on Community Edition can have up to {{limit}} {{context}} apps enabled. \n \n**{{appName}} will be disabled by default.** Disable another {{context}} app or upgrade to Enterprise to enable this app.",
"Enable_of_limit_apps_currently_enabled_exceeded": "**{{enabled}} of {{limit}} {{context}} apps currently enabled.** \n \nCommunity edition app limit has been exceeded. \n \nWorkspaces on Community Edition can have up to {{limit}} {{context}} apps enabled. \n \n**{{appName}} will be disabled by default.** You will need to disable at least {{exceed}} other {{context}} apps or upgrade to Enterprise to enable this app.",
"Workspaces_on_Community_edition_install_app": "Workspaces on Community Edition can have up to {{limit}} {{context}} apps enabled. Upgrade to Enterprise to enable unlimited apps.",
@ -5814,6 +5818,13 @@
"Disable_at_least_more_apps": "You will need to disable at least {{numberOfExceededApps}} other apps or upgrade to Enterprise to enable this app.",
"Community_Private_apps_limit_exceeded": "Community edition app limit has been exceeded.",
"Theme_match_system": "Match system",
"Theme_match_system_description": "Automatically match the theme to your system preferences. This option is only available if your browser supports the prefers-color-scheme media query.",
"Theme_high_contrast": "High contrast",
"Theme_high_contrast_description": "Maximum tonal differentiation with bold colors and sharp contrasts provide enhanced accessibility.",
"High_contrast_upsell_title": "Enable high contrast theme",
"High_contrast_upsell_subtitle": "Enhance your team’s reading experience",
"High_contrast_upsell_description": "Especially designed for individuals with visual impairments or conditions such as color vision deficiency, low vision, or sensitivity to low contrast. \nThis theme increases contrast between text and background elements, making content more distinguishable and easier to read.",
"High_contrast_upsell_annotation": "Talk to your workspace admin about enabling the high contrast theme for everyone.",
"Join_your_team": "Join your team",
"Create_a_password": "Create a password",
"Create_an_account": "Create an account",

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@ -1,5 +1,6 @@
import { Subscriptions, Users } from '@rocket.chat/models';
import type { ServerMethods } from '@rocket.chat/ui-contexts';
import type { ThemePreference } from '@rocket.chat/ui-theming/src/types/themes';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
@ -34,7 +35,7 @@ type UserPreferences = {
sidebarGroupByType: boolean;
muteFocusedConversations: boolean;
dontAskAgainList: { action: string; label: string }[];
themeAppearence: 'auto' | 'light' | 'dark';
themeAppearence: ThemePreference;
receiveLoginDetectionEmail: boolean;
notifyCalendarEvents: boolean;
};

@ -7,14 +7,21 @@ import { useCreateStyleContainer } from './hooks/useCreateStyleContainer';
import { useThemeMode } from './hooks/useThemeMode';
import { defaultPalette } from './palette';
import { darkPalette } from './paletteDark';
import { paletteHighContrast } from './paletteHighContrast';
export const PaletteStyleTag = memo(function PaletteStyleTag() {
const [, , theme] = useThemeMode();
const palette =
theme === 'dark'
? convertToCss(filterOnlyChangedColors(defaultPalette, darkPalette), '.rcx-content--main')
: convertToCss(filterOnlyChangedColors(defaultPalette, {}), '.rcx-content--main');
const getPalette = () => {
if (theme === 'dark') {
return darkPalette;
}
if (theme === 'high-contrast') {
return paletteHighContrast;
}
return {};
};
const palette = convertToCss(filterOnlyChangedColors(defaultPalette, getPalette()), '.rcx-content--main');
return createPortal(palette, useCreateStyleContainer('main-palette'));
});

@ -1,29 +1,38 @@
import { useDarkMode } from '@rocket.chat/fuselage-hooks';
import { useEndpoint, useUserPreference } from '@rocket.chat/ui-contexts';
import type { ThemePreference as ThemeMode, Themes } from '@rocket.chat/ui-theming/src/types/themes';
import { useCallback, useState } from 'react';
type ThemeMode = 'light' | 'dark' | 'auto';
/**
* Returns the current option set by the user, the theme mode resolved given the user configuration and OS (if applies) and a function to set it.
* @param defaultThemeMode The default theme mode to use if the user has not set any.
* @returns [currentThemeMode, setThemeMode, resolvedThemeMode]
*/
export const useThemeMode = (): [ThemeMode, (value: ThemeMode) => () => void, 'light' | 'dark'] => {
const theme = useUserPreference<ThemeMode>('themeAppearence') || 'auto';
export const useThemeMode = (): [ThemeMode, (value: ThemeMode) => () => void, Themes] => {
const themeMode = useUserPreference<ThemeMode>('themeAppearence') || 'auto';
const saveUserPreferences = useEndpoint('POST', '/v1/users.setPreferences');
const [updaters] = useState(
(): Record<ThemeMode, () => void> => ({
light: () => saveUserPreferences({ data: { themeAppearence: 'light' } }),
dark: () => saveUserPreferences({ data: { themeAppearence: 'dark' } }),
auto: () => saveUserPreferences({ data: { themeAppearence: 'auto' } }),
'light': () => saveUserPreferences({ data: { themeAppearence: 'light' } }),
'dark': () => saveUserPreferences({ data: { themeAppearence: 'dark' } }),
'auto': () => saveUserPreferences({ data: { themeAppearence: 'auto' } }),
'high-contrast': () => saveUserPreferences({ data: { themeAppearence: 'high-contrast' } }),
}),
);
const setTheme = useCallback((value: ThemeMode): (() => void) => updaters[value], [updaters]);
return [theme, setTheme, useDarkMode(theme === 'auto' ? undefined : theme === 'dark') ? 'dark' : 'light'];
const useTheme = () => {
if (useDarkMode(themeMode === 'auto' ? undefined : themeMode === 'dark')) {
return 'dark';
}
if (themeMode === 'high-contrast') {
return 'high-contrast';
}
return 'light';
};
return [themeMode, setTheme, useTheme()];
};

@ -24,7 +24,7 @@ export const palette = [
{ name: 'surface-neutral', token: 'N400', color: '#E4E7EA' },
{ name: 'surface-disabled', token: 'N100', color: '#F7F8FA' },
{ name: 'surface-hover', token: 'N200', color: '#F2F3F5' },
{ name: 'surface-selected', token: 'N400', color: '#E4E7EA' },
{ name: 'surface-selected', token: '', color: '#D7DBE0' },
{ name: 'surface-dark', token: 'N900', color: '#1F2329' },
{ name: 'surface-featured', token: '', color: '#5F1477' },
{ name: 'surface-featured-hover', token: '', color: '#4A105D' },
@ -176,17 +176,19 @@ export const palette = [
description: 'Font',
list: [
{ name: 'button-font-on-primary', token: 'white', color: '#FFFFFF' },
{ name: 'button-font-on-primary-disabled', token: 'white', color: '#FFFFFF' },
{ name: 'button-font-on-secondary', token: 'N900', color: '#1F2329' },
{ name: 'button-font-on-secondary-disabled', token: 'N600', color: '#CBCED1' },
{ name: 'button-font-on-secondary-danger', token: 'D900', color: '#BB0B21' },
{ name: 'button-font-on-danger', token: 'white', color: '#FFFFFF' },
{ name: 'button-font-on-primary-disabled', token: 'white', color: '#FFFFFF' },
{ name: 'button-font-on-secondary-disabled', token: 'N600', color: '#9EA2A8' },
{
name: 'button-font-on-secondary-danger-disabled',
token: 'D300',
color: '#F98F9D',
},
{ name: 'button-font-on-danger', token: 'white', color: '#FFFFFF' },
{ name: 'button-font-on-danger-disabled', token: 'white', color: '#FFFFFF' },
{ name: 'button-font-on-success', token: '', color: '#EBECEF' },
{ name: 'button-font-on-success-disabled', token: 'white', color: '#FFFFFF' },
],
},
];

@ -0,0 +1,210 @@
export const palette = [
{
category: 'Stroke',
description: "Use as component's outline, stroke, dividers",
list: [
{ name: 'stroke-extra-light', token: 'N250', color: '#EBECEF' },
{ name: 'stroke-light', token: 'N500', color: '#CBCED1' },
{ name: 'stroke-medium', token: 'N600', color: '#9EA2A8' },
{ name: 'stroke-dark', token: 'N700', color: '#6C727A' },
{ name: 'stroke-extra-dark', token: 'N800', color: '#2F343D' },
{ name: 'stroke-extra-light-highlight', token: 'P200', color: '#D1EBFE' },
{ name: 'stroke-highlight', token: 'P500', color: '#156FF5' },
{ name: 'stroke-extra-light-error', token: 'D200', color: '#FFC1C9' },
{ name: 'stroke-error', token: 'D500', color: '#EC0D2A' },
],
},
{
category: 'Surface',
description: 'Use as a container on top of the background',
list: [
{ name: 'surface-light', token: 'white', color: '#FFFFFF' },
{ name: 'surface-tint', token: 'N100', color: '#F7F8FA' },
{ name: 'surface-room', token: 'white', color: '#FFFFFF' },
{ name: 'surface-neutral', token: 'N400', color: '#E4E7EA' },
{ name: 'surface-disabled', token: 'N100', color: '#F7F8FA' },
{ name: 'surface-hover', token: 'N200', color: '#F2F3F5' },
{ name: 'surface-selected', token: 'N400', color: '#D7DBE0' },
{ name: 'surface-dark', token: 'N900', color: '#1F2329' },
{ name: 'surface-featured', token: '', color: '#5F1477' },
{ name: 'surface-featured-hover', token: '', color: '#4A105D' },
{ name: 'surface-overlay', token: '', color: 'rgba(47, 52, 61, 0.5)' },
],
},
{
category: 'Shadow',
description: 'Use as a shadow color',
list: [
{ name: 'shadow-highlight', token: '', color: '#D1EBFE' },
{ name: 'shadow-danger', token: '', color: '#FFE9EC' },
],
},
{
category: 'Font',
description: 'These should be applied according to surfaces',
list: [
{ name: 'font-white', token: 'white', color: '#FFFFFF' },
{ name: 'font-disabled', token: '', color: '#F7F8FA' },
{ name: 'font-annotation', token: '', color: '#7020C0' },
{ name: 'font-hint', token: '', color: '#3C3F44' },
{ name: 'font-secondary-info', token: '', color: '#3C3F44' },
{ name: 'font-default', token: '', color: '#24272E' },
{ name: 'font-titles-labels', token: '', color: '#1F2329' },
{ name: 'font-info', token: '', color: '#084BB0' },
{ name: 'font-danger', token: '', color: '#A90A1E' },
{ name: 'font-pure-black', token: '', color: '#2F343D' },
{ name: 'font-pure-white', token: '', color: '#FFFFFF' },
],
},
{
category: 'Status',
description: 'Status Background',
list: [
{ name: 'status-background-info', token: 'P200', color: '#D1EBFE' },
{ name: 'status-background-success', token: 'S500', color: '#C0F6E4' },
{ name: 'status-background-danger', token: 'D200', color: '#FFC1C9' },
{ name: 'status-background-warning', token: 'W200', color: '#FFECAD' },
{ name: 'status-background-warning-2', token: 'W100', color: '#FFF8E0' },
{ name: 'status-background-service-1', token: 'S1-200', color: '#FAD1B0' },
{ name: 'status-background-service-2', token: 'S2-200', color: '#EDD0F7' },
{ name: 'status-background-service-3', token: 'S2-700', color: '#5F1477' },
],
},
{
description: 'Status Font',
list: [
{ name: 'status-font-on-info', token: '', color: '#053070' },
{ name: 'status-font-on-success', token: '', color: '#0D5940' },
{ name: 'status-font-on-danger', token: 'D800', color: '#9B1325' },
{ name: 'status-font-on-warning', token: 'W900', color: '#B88D00' },
{ name: 'status-font-on-warning-2', token: 'N800', color: '#2F343D' },
{ name: 'status-font-on-service-1', token: 'S1-800', color: '#974809' },
{ name: 'status-font-on-service-2 ', token: 'S2-600', color: '#7F1B9F' },
{ name: 'status-font-on-service-3 ', token: 'white', color: '#FFFFFF' },
],
},
{
category: 'Badge',
description: 'Badge Background',
list: [
{ name: 'badge-background-level-0', token: '', color: '#F4F5F6' },
{ name: 'badge-background-level-1', token: 'N700', color: '#52565B' },
{ name: 'badge-background-level-2', token: '', color: '#064FBC' },
{ name: 'badge-background-level-3', token: '', color: '#874108' },
{ name: 'badge-background-level-4', token: '', color: '#AE091F' },
],
},
{
category: 'Status Bullet',
description: 'Used to show user status',
list: [
{ name: 'status-bullet-online', token: '', color: '#158D65' },
{ name: 'status-bullet-away', token: '', color: '#AC892F' },
{ name: 'status-bullet-busy', token: '', color: '#DA1F37' },
{ name: 'status-bullet-disabled', token: '', color: '#F38C39' },
{ name: 'status-bullet-offline', token: '', color: '#AC892F' },
{ name: 'status-bullet-loading', token: '', color: '#9ea2a8' },
],
},
{
category: 'Elevation',
description: 'Elevation border and shadow levels',
list: [
{ name: 'shadow-elevation-border', token: '', color: '#EBECEF' },
{ name: 'shadow-elevation-1', token: '', color: 'rgba(47, 52, 61, 0.1)' },
{ name: 'shadow-elevation-2x', token: '', color: 'rgba(47, 52, 61, 0.08)' },
{ name: 'shadow-elevation-2y', token: '', color: 'rgba(47, 52, 61, 0.12)' },
],
},
{
category: 'Button',
description: 'Primary Background',
list: [
{ name: 'button-background-primary-default', token: '', color: '#084FBA' },
{ name: 'button-background-primary-hover', token: '', color: '#063D8E' },
{ name: 'button-background-primary-press', token: '', color: '#09305D' },
{ name: 'button-background-primary-focus', token: '', color: '#084FBA' },
{ name: 'button-background-primary-keyfocus', token: '', color: '#084FBA' },
{ name: 'button-background-primary-disabled', token: '', color: '#8CCDFD' },
],
},
{
description: 'Secondary Background',
list: [
{ name: 'button-background-secondary-default', token: 'N400', color: '#E4E7EA' },
{ name: 'button-background-secondary-hover', token: 'N500', color: '#CBCED1' },
{ name: 'button-background-secondary-press', token: '', color: '#C4C6CA' },
{ name: 'button-background-secondary-focus', token: 'N400', color: '#E4E7EA' },
{ name: 'button-background-secondary-keyfocus', token: 'N400', color: '#E4E7EA' },
{ name: 'button-background-secondary-disabled', token: 'N300', color: '#EEEFF1' },
],
},
{
description: 'Secondary Danger Background',
list: [
{ name: 'button-background-secondary-danger-default', token: '', color: '#F4F5F6' },
{ name: 'button-background-secondary-danger-hover', token: '', color: '#E4E6E7' },
{ name: 'button-background-secondary-danger-press', token: '', color: '#C9CBCF' },
{ name: 'button-background-secondary-danger-focus', token: 'N400', color: '#E4E7EA' },
{ name: 'button-background-secondary-danger-keyfocus', token: 'N400', color: '#E4E7EA' },
{ name: 'button-background-secondary-danger-disabled', token: '', color: '#FAFAFA' },
],
},
{
description: 'Danger Background',
list: [
{ name: 'button-background-danger-default', token: '', color: '#B30A20' },
{ name: 'button-background-danger-hover', token: '', color: '#901323' },
{ name: 'button-background-danger-press', token: '', color: '#7A101D' },
{ name: 'button-background-danger-focus', token: '', color: '#B30920' },
{ name: 'button-background-danger-keyfocus', token: '', color: '#B30A20' },
{ name: 'button-background-danger-disabled', token: 'D200', color: '#FFC1C9' },
],
},
{
description: 'Success Background',
list: [
{ name: 'button-background-success-default', token: '', color: '#158D65' },
{ name: 'button-background-success-hover', token: 'S900', color: '#106D4F' },
{ name: 'button-background-success-press', token: 'S1000', color: '#0D5940' },
{ name: 'button-background-success-focus', token: '', color: '#158D65' },
{ name: 'button-background-success-keyfocus', token: '', color: '#158D65' },
{ name: 'button-background-success-disabled', token: 'S200', color: '#C0F6E4' },
],
},
{
description: 'Font',
list: [
{ name: 'button-font-on-primary', token: 'white', color: '#FFFFFF' },
{ name: 'button-font-on-primary-disabled', token: '', color: '#09305D' },
{ name: 'button-font-on-secondary', token: '', color: '#14161A' },
{ name: 'button-font-on-secondary-disabled', token: '', color: '#4D5257' },
{ name: 'button-font-on-danger', token: 'white', color: '#FFFFFF' },
{ name: 'button-font-on-danger-disabled', token: '', color: '#7A101D' },
{ name: 'button-font-on-secondary-danger', token: '', color: '#6E0210' },
{
name: 'button-font-on-secondary-danger-disabled',
token: '',
color: '#AE091F',
},
{ name: 'button-font-on-success', token: '', color: '#EBECEF' },
{ name: 'button-font-on-success-disabled', token: 'white', color: '#FFFFFF' },
],
},
];
export const paletteHighContrast = {
...palette.reduce(
(rec, group) => ({
...rec,
...group.list.reduce(
(rec, item) => ({
...rec,
[item.name]: item.color,
}),
{} as Record<string, string>,
),
}),
{} as Record<string, string>,
),
};

@ -0,0 +1,3 @@
export type ThemePreference = 'light' | 'dark' | 'auto' | 'high-contrast';
export type Themes = 'light' | 'dark' | 'high-contrast';

@ -1,3 +1,4 @@
import type { ThemePreference } from '@rocket.chat/ui-theming/src/types/themes';
import Ajv from 'ajv';
const ajv = new Ajv({
@ -40,7 +41,7 @@ export type UsersSetPreferencesParamsPOST = {
muteFocusedConversations?: boolean;
dontAskAgainList?: Array<{ action: string; label: string }>;
featuresPreview?: { name: string; value: boolean }[];
themeAppearence?: 'auto' | 'light' | 'dark';
themeAppearence?: ThemePreference;
receiveLoginDetectionEmail?: boolean;
notifyCalendarEvents?: boolean;
idleTimeLimit?: number;

Loading…
Cancel
Save