From 357a3a50fa8d5e73483b1f52003d9deb72867226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Tue, 1 Aug 2023 10:51:37 -0300 Subject: [PATCH] feat: high-contrast theme (#29805) --- .changeset/smooth-planes-cough.md | 7 + .../GenericUpsellModal/GenericUpsellModal.tsx | 4 +- .../providers/UserProvider/UserProvider.tsx | 12 +- .../sidebar/header/hooks/useAccountItems.tsx | 43 ++-- .../sidebar/header/hooks/useThemeItems.tsx | 33 --- .../sidebar/header/hooks/useUserMenu.tsx | 24 +- .../preferences/PreferencesGlobalSection.tsx | 21 +- apps/meteor/client/views/account/routes.tsx | 8 + .../client/views/account/sidebarItems.tsx | 15 +- .../themes/HighContrastUpsellModal.tsx | 41 ++++ .../client/views/account/themes/ThemePage.tsx | 103 +++++++++ .../client/views/account/themes/themeItems.ts | 24 ++ .../rocketchat-i18n/i18n/en.i18n.json | 11 + .../images/high-contrast-upsell-modal.png | Bin 0 -> 13392 bytes .../server/methods/saveUserPreferences.ts | 3 +- .../ui-theming/src/PaletteStyleTag.tsx | 15 +- .../ui-theming/src/hooks/useThemeMode.ts | 25 ++- ee/packages/ui-theming/src/palette.ts | 10 +- .../ui-theming/src/paletteHighContrast.ts | 210 ++++++++++++++++++ ee/packages/ui-theming/src/types/themes.ts | 3 + .../v1/users/UsersSetPreferenceParamsPOST.ts | 3 +- 21 files changed, 513 insertions(+), 102 deletions(-) create mode 100644 .changeset/smooth-planes-cough.md delete mode 100644 apps/meteor/client/sidebar/header/hooks/useThemeItems.tsx create mode 100644 apps/meteor/client/views/account/themes/HighContrastUpsellModal.tsx create mode 100644 apps/meteor/client/views/account/themes/ThemePage.tsx create mode 100644 apps/meteor/client/views/account/themes/themeItems.ts create mode 100644 apps/meteor/public/images/high-contrast-upsell-modal.png create mode 100644 ee/packages/ui-theming/src/paletteHighContrast.ts create mode 100644 ee/packages/ui-theming/src/types/themes.ts diff --git a/.changeset/smooth-planes-cough.md b/.changeset/smooth-planes-cough.md new file mode 100644 index 00000000000..9ad4239f034 --- /dev/null +++ b/.changeset/smooth-planes-cough.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/ui-theming': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +feat: high-contrast theme diff --git a/apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx b/apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx index 15c47db711b..79215ef7c10 100644 --- a/apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx +++ b/apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx @@ -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) => { diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index d4f982d3350..fa5eca49cae 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -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} />; }; diff --git a/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx b/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx index e4757c2d48b..10966e4b859 100644 --- a/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx +++ b/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx @@ -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] : []), ]; }; diff --git a/apps/meteor/client/sidebar/header/hooks/useThemeItems.tsx b/apps/meteor/client/sidebar/header/hooks/useThemeItems.tsx deleted file mode 100644 index d571acde162..00000000000 --- a/apps/meteor/client/sidebar/header/hooks/useThemeItems.tsx +++ /dev/null @@ -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' />, - }, - ]; -}; diff --git a/apps/meteor/client/sidebar/header/hooks/useUserMenu.tsx b/apps/meteor/client/sidebar/header/hooks/useUserMenu.tsx index 232011dbe31..111065b0ac4 100644 --- a/apps/meteor/client/sidebar/header/hooks/useUserMenu.tsx +++ b/apps/meteor/client/sidebar/header/hooks/useUserMenu.tsx @@ -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], }, ]; }; diff --git a/apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx index edc7e64b9dc..7c2410deb9f 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx @@ -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> ); diff --git a/apps/meteor/client/views/account/routes.tsx b/apps/meteor/client/views/account/routes.tsx index f6a49eb268d..6d17b1e1308 100644 --- a/apps/meteor/client/views/account/routes.tsx +++ b/apps/meteor/client/views/account/routes.tsx @@ -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')), +}); diff --git a/apps/meteor/client/views/account/sidebarItems.tsx b/apps/meteor/client/views/account/sidebarItems.tsx index a2d0720fe6e..1bdb488d419 100644 --- a/apps/meteor/client/views/account/sidebarItems.tsx +++ b/apps/meteor/client/views/account/sidebarItems.tsx @@ -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', diff --git a/apps/meteor/client/views/account/themes/HighContrastUpsellModal.tsx b/apps/meteor/client/views/account/themes/HighContrastUpsellModal.tsx new file mode 100644 index 00000000000..c06501aae28 --- /dev/null +++ b/apps/meteor/client/views/account/themes/HighContrastUpsellModal.tsx @@ -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; diff --git a/apps/meteor/client/views/account/themes/ThemePage.tsx b/apps/meteor/client/views/account/themes/ThemePage.tsx new file mode 100644 index 00000000000..3f3f022be5f --- /dev/null +++ b/apps/meteor/client/views/account/themes/ThemePage.tsx @@ -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; diff --git a/apps/meteor/client/views/account/themes/themeItems.ts b/apps/meteor/client/views/account/themes/themeItems.ts new file mode 100644 index 00000000000..82457ac3938 --- /dev/null +++ b/apps/meteor/client/views/account/themes/themeItems.ts @@ -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', + }, +]; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 0bd39f8e43e..09e3b5ed0bb 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -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", diff --git a/apps/meteor/public/images/high-contrast-upsell-modal.png b/apps/meteor/public/images/high-contrast-upsell-modal.png new file mode 100644 index 0000000000000000000000000000000000000000..b761a1b0b76c81cda36007039682c3a5493b8448 GIT binary patch literal 13392 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 ZrjobB@s5`tfeS!L%JLd=)iU6){{_?5;TZq` literal 0 HcmV?d00001 diff --git a/apps/meteor/server/methods/saveUserPreferences.ts b/apps/meteor/server/methods/saveUserPreferences.ts index 4d82a3f35ba..ad2934f3f99 100644 --- a/apps/meteor/server/methods/saveUserPreferences.ts +++ b/apps/meteor/server/methods/saveUserPreferences.ts @@ -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; }; diff --git a/ee/packages/ui-theming/src/PaletteStyleTag.tsx b/ee/packages/ui-theming/src/PaletteStyleTag.tsx index 84bbee2eb8a..95f0f40c7fd 100644 --- a/ee/packages/ui-theming/src/PaletteStyleTag.tsx +++ b/ee/packages/ui-theming/src/PaletteStyleTag.tsx @@ -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')); }); diff --git a/ee/packages/ui-theming/src/hooks/useThemeMode.ts b/ee/packages/ui-theming/src/hooks/useThemeMode.ts index a467345f810..63cf01bf6eb 100644 --- a/ee/packages/ui-theming/src/hooks/useThemeMode.ts +++ b/ee/packages/ui-theming/src/hooks/useThemeMode.ts @@ -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()]; }; diff --git a/ee/packages/ui-theming/src/palette.ts b/ee/packages/ui-theming/src/palette.ts index 4abcefa6524..4825beec0cf 100644 --- a/ee/packages/ui-theming/src/palette.ts +++ b/ee/packages/ui-theming/src/palette.ts @@ -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' }, ], }, ]; diff --git a/ee/packages/ui-theming/src/paletteHighContrast.ts b/ee/packages/ui-theming/src/paletteHighContrast.ts new file mode 100644 index 00000000000..82ebb68f1e9 --- /dev/null +++ b/ee/packages/ui-theming/src/paletteHighContrast.ts @@ -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>, + ), +}; diff --git a/ee/packages/ui-theming/src/types/themes.ts b/ee/packages/ui-theming/src/types/themes.ts new file mode 100644 index 00000000000..2fe98dfbe2b --- /dev/null +++ b/ee/packages/ui-theming/src/types/themes.ts @@ -0,0 +1,3 @@ +export type ThemePreference = 'light' | 'dark' | 'auto' | 'high-contrast'; + +export type Themes = 'light' | 'dark' | 'high-contrast'; diff --git a/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts b/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts index ce1e33e02dc..c283867345e 100644 --- a/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts +++ b/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts @@ -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; -- GitLab