diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 890673ca4..05888c5c7 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -3,7 +3,7 @@ v-if="!authUIFlags.isFetching" id="app" class="app-wrapper app-root" - :class="{ 'app-rtl--wrapper': isRTLView, dark: theme === 'dark' }" + :class="{ 'app-rtl--wrapper': isRTLView }" :dir="isRTLView ? 'rtl' : 'ltr'" > @@ -35,12 +35,11 @@ import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue'; import vueActionCable from './helper/actionCable'; import WootSnackbarBox from './components/SnackbarContainer'; import rtlMixin from 'shared/mixins/rtlMixin'; -import { LocalStorage } from 'shared/helpers/localStorage'; +import { setColorTheme } from './helper/themeHelper'; import { registerSubscription, verifyServiceWorkerExistence, } from './helper/pushHelper'; -import { LOCAL_STORAGE_KEYS } from './constants/localStorage'; export default { name: 'App', @@ -61,7 +60,6 @@ export default { return { showAddAccountModal: false, latestChatwootVersion: null, - theme: 'light', }; }, @@ -99,29 +97,11 @@ export default { }, methods: { initializeColorTheme() { - this.setColorTheme( - window.matchMedia('(prefers-color-scheme: dark)').matches - ); - }, - setColorTheme(isOSOnDarkMode) { - const selectedColorScheme = - LocalStorage.get(LOCAL_STORAGE_KEYS.COLOR_SCHEME) || 'light'; - if ( - (selectedColorScheme === 'auto' && isOSOnDarkMode) || - selectedColorScheme === 'dark' - ) { - this.theme = 'dark'; - document.body.classList.add('dark'); - document.documentElement.setAttribute('style', 'color-scheme: dark;'); - } else { - this.theme = 'light '; - document.body.classList.remove('dark'); - document.documentElement.setAttribute('style', 'color-scheme: light;'); - } + setColorTheme(window.matchMedia('(prefers-color-scheme: dark)').matches); }, listenToThemeChanges() { const mql = window.matchMedia('(prefers-color-scheme: dark)'); - mql.onchange = e => this.setColorTheme(e.matches); + mql.onchange = e => setColorTheme(e.matches); }, setLocale(locale) { this.$root.$i18n.locale = locale; diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/OptionsMenu.vue b/app/javascript/dashboard/components/layout/sidebarComponents/OptionsMenu.vue index 832f9246f..65ad23a88 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/OptionsMenu.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/OptionsMenu.vue @@ -60,6 +60,17 @@ + + + {{ $t('SIDEBAR_ITEMS.APPEARANCE') }} + + diff --git a/app/javascript/dashboard/helper/specs/themeHelper.spec.js b/app/javascript/dashboard/helper/specs/themeHelper.spec.js new file mode 100644 index 000000000..745d71560 --- /dev/null +++ b/app/javascript/dashboard/helper/specs/themeHelper.spec.js @@ -0,0 +1,76 @@ +import { setColorTheme } from 'dashboard/helper/themeHelper.js'; +import { LocalStorage } from 'shared/helpers/localStorage'; + +jest.mock('shared/helpers/localStorage'); + +describe('setColorTheme', () => { + it('should set body class to dark if selectedColorScheme is dark', () => { + LocalStorage.get.mockReturnValue('dark'); + setColorTheme(true); + expect(document.body.classList.contains('dark')).toBe(true); + }); + + it('should set body class to dark if selectedColorScheme is auto and isOSOnDarkMode is true', () => { + LocalStorage.get.mockReturnValue('auto'); + setColorTheme(true); + expect(document.body.classList.contains('dark')).toBe(true); + }); + + it('should not set body class to dark if selectedColorScheme is auto and isOSOnDarkMode is false', () => { + LocalStorage.get.mockReturnValue('auto'); + setColorTheme(false); + expect(document.body.classList.contains('dark')).toBe(false); + }); + + it('should not set body class to dark if selectedColorScheme is light', () => { + LocalStorage.get.mockReturnValue('light'); + setColorTheme(true); + expect(document.body.classList.contains('dark')).toBe(false); + }); + + it('should not set body class to dark if selectedColorScheme is undefined', () => { + LocalStorage.get.mockReturnValue(undefined); + setColorTheme(true); + expect(document.body.classList.contains('dark')).toBe(true); + }); + + it('should set documentElement style to dark if selectedColorScheme is dark', () => { + LocalStorage.get.mockReturnValue('dark'); + setColorTheme(true); + expect(document.documentElement.getAttribute('style')).toBe( + 'color-scheme: dark;' + ); + }); + + it('should set documentElement style to dark if selectedColorScheme is auto and isOSOnDarkMode is true', () => { + LocalStorage.get.mockReturnValue('auto'); + setColorTheme(true); + expect(document.documentElement.getAttribute('style')).toBe( + 'color-scheme: dark;' + ); + }); + + it('should set documentElement style to light if selectedColorScheme is auto and isOSOnDarkMode is false', () => { + LocalStorage.get.mockReturnValue('auto'); + setColorTheme(false); + expect(document.documentElement.getAttribute('style')).toBe( + 'color-scheme: light;' + ); + }); + + it('should set documentElement style to light if selectedColorScheme is light', () => { + LocalStorage.get.mockReturnValue('light'); + setColorTheme(true); + expect(document.documentElement.getAttribute('style')).toBe( + 'color-scheme: light;' + ); + }); + + it('should set documentElement style to light if selectedColorScheme is undefined', () => { + LocalStorage.get.mockReturnValue(undefined); + setColorTheme(true); + expect(document.documentElement.getAttribute('style')).toBe( + 'color-scheme: dark;' + ); + }); +}); diff --git a/app/javascript/dashboard/helper/themeHelper.js b/app/javascript/dashboard/helper/themeHelper.js new file mode 100644 index 000000000..b19b0541a --- /dev/null +++ b/app/javascript/dashboard/helper/themeHelper.js @@ -0,0 +1,17 @@ +import { LocalStorage } from 'shared/helpers/localStorage'; +import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; + +export const setColorTheme = isOSOnDarkMode => { + const selectedColorScheme = + LocalStorage.get(LOCAL_STORAGE_KEYS.COLOR_SCHEME) || 'auto'; + if ( + (selectedColorScheme === 'auto' && isOSOnDarkMode) || + selectedColorScheme === 'dark' + ) { + document.body.classList.add('dark'); + document.documentElement.setAttribute('style', 'color-scheme: dark;'); + } else { + document.body.classList.remove('dark'); + document.documentElement.setAttribute('style', 'color-scheme: light;'); + } +}; diff --git a/app/javascript/dashboard/i18n/locale/en/generalSettings.json b/app/javascript/dashboard/i18n/locale/en/generalSettings.json index aeafa9d54..4d9df5772 100644 --- a/app/javascript/dashboard/i18n/locale/en/generalSettings.json +++ b/app/javascript/dashboard/i18n/locale/en/generalSettings.json @@ -111,7 +111,8 @@ "ADD_LABEL": "Add label to the conversation", "REMOVE_LABEL": "Remove label from the conversation", "SETTINGS": "Settings", - "AI_ASSIST": "AI Assist" + "AI_ASSIST": "AI Assist", + "APPEARANCE": "Appearance" }, "COMMANDS": { "GO_TO_CONVERSATION_DASHBOARD": "Go to Conversation Dashboard", @@ -148,7 +149,11 @@ "UNTIL_TOMORROW": "Until tomorrow", "UNTIL_NEXT_MONTH": "Until next month", "AN_HOUR_FROM_NOW": "Until an hour from now", - "CUSTOM": "Custom..." + "CUSTOM": "Custom...", + "CHANGE_APPEARANCE": "Change Appearance", + "LIGHT_MODE": "Light", + "DARK_MODE": "Dark", + "SYSTEM_MODE": "System" } }, "DASHBOARD_APPS": { diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index de972eb78..158207bc7 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -145,6 +145,7 @@ "SELECTOR_SUBTITLE": "Select an account from the following list", "PROFILE_SETTINGS": "Profile Settings", "KEYBOARD_SHORTCUTS": "Keyboard Shortcuts", + "APPEARANCE": "Change Appearance", "SUPER_ADMIN_CONSOLE": "Super Admin Console", "LOGOUT": "Logout" }, diff --git a/app/javascript/dashboard/routes/dashboard/commands/CommandBarIcons.js b/app/javascript/dashboard/routes/dashboard/commands/CommandBarIcons.js index 97e6f33bc..d3f10b342 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/CommandBarIcons.js +++ b/app/javascript/dashboard/routes/dashboard/commands/CommandBarIcons.js @@ -66,3 +66,8 @@ export const ICON_AI_SPELLING = ``; export const ICON_AI_SHORTEN = ``; export const ICON_AI_GRAMMAR = ``; + +export const ICON_APPEARANCE = ``; +export const ICON_LIGHT_MODE = ``; +export const ICON_DARK_MODE = ``; +export const ICON_SYSTEM_MODE = ``; diff --git a/app/javascript/dashboard/routes/dashboard/commands/appearanceHotKeys.js b/app/javascript/dashboard/routes/dashboard/commands/appearanceHotKeys.js new file mode 100644 index 000000000..0eb8d075f --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/commands/appearanceHotKeys.js @@ -0,0 +1,64 @@ +import { + ICON_APPEARANCE, + ICON_LIGHT_MODE, + ICON_DARK_MODE, + ICON_SYSTEM_MODE, +} from './CommandBarIcons'; +import { LocalStorage } from 'shared/helpers/localStorage'; +import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; +import { setColorTheme } from 'dashboard/helper/themeHelper.js'; + +export default { + computed: { + themeOptions() { + return [ + { + key: 'light', + label: this.$t('COMMAND_BAR.COMMANDS.LIGHT_MODE'), + icon: ICON_LIGHT_MODE, + }, + { + key: 'dark', + label: this.$t('COMMAND_BAR.COMMANDS.DARK_MODE'), + icon: ICON_DARK_MODE, + }, + + { + key: 'auto', + label: this.$t('COMMAND_BAR.COMMANDS.SYSTEM_MODE'), + icon: ICON_SYSTEM_MODE, + }, + ]; + }, + goToAppearanceHotKeys() { + const options = this.themeOptions.map(theme => ({ + id: theme.key, + title: theme.label, + parent: 'appearance_settings', + section: this.$t('COMMAND_BAR.SECTIONS.APPEARANCE'), + icon: theme.icon, + handler: () => { + this.setAppearance(theme.key); + }, + })); + return [ + { + id: 'appearance_settings', + title: this.$t('COMMAND_BAR.COMMANDS.CHANGE_APPEARANCE'), + section: this.$t('COMMAND_BAR.SECTIONS.APPEARANCE'), + icon: ICON_APPEARANCE, + children: options.map(option => option.id), + }, + ...options, + ]; + }, + }, + methods: { + setAppearance(theme) { + LocalStorage.set(LOCAL_STORAGE_KEYS.COLOR_SCHEME, theme); + const isOSOnDarkMode = window.matchMedia('(prefers-color-scheme: dark)') + .matches; + setColorTheme(isOSOnDarkMode); + }, + }, +}; diff --git a/app/javascript/dashboard/routes/dashboard/commands/commandbar.vue b/app/javascript/dashboard/routes/dashboard/commands/commandbar.vue index 6b7c2f039..8c9ecf787 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/commandbar.vue +++ b/app/javascript/dashboard/routes/dashboard/commands/commandbar.vue @@ -13,6 +13,7 @@ import 'ninja-keys'; import conversationHotKeysMixin from './conversationHotKeys'; import goToCommandHotKeys from './goToCommandHotKeys'; +import appearanceHotKeys from './appearanceHotKeys'; import agentMixin from 'dashboard/mixins/agentMixin'; import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin'; import conversationTeamMixin from 'dashboard/mixins/conversation/teamMixin'; @@ -26,6 +27,7 @@ export default { conversationHotKeysMixin, conversationLabelMixin, conversationTeamMixin, + appearanceHotKeys, goToCommandHotKeys, ], @@ -42,7 +44,11 @@ export default { return this.$route.name; }, hotKeys() { - return [...this.conversationHotKeys, ...this.goToCommandHotKeys]; + return [ + ...this.conversationHotKeys, + ...this.goToCommandHotKeys, + ...this.goToAppearanceHotKeys, + ]; }, }, watch: { @@ -76,8 +82,12 @@ ninja-keys { --ninja-accent-color: var(--w-500); --ninja-font-family: 'PlusJakarta'; z-index: 9999; +} - @media (prefers-color-scheme: dark) { +// Wrapped with body.dark to avoid overriding the default theme +// If OS is in dark theme and app is in light mode, It will prevent showing dark theme in command bar +body.dark { + ninja-keys { --ninja-overflow-background: rgba(26, 29, 30, 0.5); --ninja-modal-background: #151718; --ninja-secondary-background-color: #26292b; diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json index aba364d14..8dd57513d 100644 --- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json +++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json @@ -72,6 +72,7 @@ "cloud-backup-outline": "M6.087 7.75a5.752 5.752 0 0 1 11.326 0h.087a4 4 0 0 1 3.962 4.552 6.534 6.534 0 0 0-1.597-1.364A2.501 2.501 0 0 0 17.5 9.25h-.756a.75.75 0 0 1-.75-.713 4.25 4.25 0 0 0-8.489 0 .75.75 0 0 1-.749.713H6a2.5 2.5 0 0 0 0 5h4.4a6.458 6.458 0 0 0-.357 1.5H6a4 4 0 0 1 0-8h.087ZM22 16.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0Zm-6-1.793V19.5a.5.5 0 0 0 1 0v-4.793l1.646 1.647a.5.5 0 0 0 .708-.708l-2.5-2.5a.5.5 0 0 0-.708 0l-2.5 2.5a.5.5 0 0 0 .708.708L16 14.707Z", "cloud-outline": "M6.087 9.75a5.752 5.752 0 0 1 11.326 0h.087a4 4 0 0 1 0 8H6a4 4 0 0 1 0-8h.087ZM11.75 6.5a4.25 4.25 0 0 0-4.245 4.037.75.75 0 0 1-.749.713H6a2.5 2.5 0 0 0 0 5h11.5a2.5 2.5 0 0 0 0-5h-.756a.75.75 0 0 1-.75-.713A4.25 4.25 0 0 0 11.75 6.5Z", "code-outline": "m8.066 18.943 6.5-14.5a.75.75 0 0 1 1.404.518l-.036.096-6.5 14.5a.75.75 0 0 1-1.404-.518l.036-.096 6.5-14.5-6.5 14.5ZM2.22 11.47l4.25-4.25a.75.75 0 0 1 1.133.976l-.073.085L3.81 12l3.72 3.719a.75.75 0 0 1-.976 1.133l-.084-.073-4.25-4.25a.75.75 0 0 1-.073-.976l.073-.084 4.25-4.25-4.25 4.25Zm14.25-4.25a.75.75 0 0 1 .976-.073l.084.073 4.25 4.25a.75.75 0 0 1 .073.976l-.073.085-4.25 4.25a.75.75 0 0 1-1.133-.977l.073-.084L20.19 12l-3.72-3.72a.75.75 0 0 1 0-1.06Z", + "appearance-outline": "M3.839 5.858c2.94-3.916 9.03-5.055 13.364-2.36c4.28 2.66 5.854 7.777 4.1 12.577c-1.655 4.533-6.016 6.328-9.159 4.048c-1.177-.854-1.634-1.925-1.854-3.664l-.106-.987l-.045-.398c-.123-.934-.311-1.352-.705-1.572c-.535-.298-.892-.305-1.595-.033l-.351.146l-.179.078c-1.014.44-1.688.595-2.541.416l-.2-.047l-.164-.047c-2.789-.864-3.202-4.647-.565-8.157Zm.984 6.716l.123.037l.134.03c.439.087.814.015 1.437-.242l.602-.257c1.202-.493 1.985-.54 3.046.05c.917.512 1.275 1.298 1.457 2.66l.053.459l.055.532l.047.422c.172 1.361.485 2.09 1.248 2.644c2.275 1.65 5.534.309 6.87-3.349c1.516-4.152.174-8.514-3.484-10.789c-3.675-2.284-8.899-1.306-11.373 1.987c-2.075 2.763-1.82 5.28-.215 5.816Zm11.225-1.994a1.25 1.25 0 1 1 2.414-.647a1.25 1.25 0 0 1-2.414.647Zm.494 3.488a1.25 1.25 0 1 1 2.415-.647a1.25 1.25 0 0 1-2.415.647ZM14.07 7.577a1.25 1.25 0 1 1 2.415-.647a1.25 1.25 0 0 1-2.415.647Zm-.028 8.998a1.25 1.25 0 1 1 2.414-.647a1.25 1.25 0 0 1-2.414.647Zm-3.497-9.97a1.25 1.25 0 1 1 2.415-.646a1.25 1.25 0 0 1-2.415.646Z", "comment-add-outline": "M12.022 3a6.473 6.473 0 0 0-.709 1.5H5.25A1.75 1.75 0 0 0 3.5 6.25v8.5c0 .966.784 1.75 1.75 1.75h2.249v3.75l5.015-3.75h6.236a1.75 1.75 0 0 0 1.75-1.75l.001-2.483a6.517 6.517 0 0 0 1.5-1.077L22 14.75A3.25 3.25 0 0 1 18.75 18h-5.738L8 21.75a1.25 1.25 0 0 1-1.999-1V18h-.75A3.25 3.25 0 0 1 2 14.75v-8.5A3.25 3.25 0 0 1 5.25 3h6.772ZM17.5 1a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11Zm0 1.5-.09.008a.5.5 0 0 0-.402.402L17 3l-.001 3H14l-.09.008a.5.5 0 0 0-.402.402l-.008.09.008.09a.5.5 0 0 0 .402.402L14 7h2.999L17 10l.008.09a.5.5 0 0 0 .402.402l.09.008.09-.008a.5.5 0 0 0 .402-.402L18 10l-.001-3H21l.09-.008a.5.5 0 0 0 .402-.402l.008-.09-.008-.09a.5.5 0 0 0-.402-.402L21 6h-3.001L18 3l-.008-.09a.5.5 0 0 0-.402-.402L17.5 2.5Z", "contact-card-group-outline": "M18.75 4A3.25 3.25 0 0 1 22 7.25v9.505a3.25 3.25 0 0 1-3.25 3.25H5.25A3.25 3.25 0 0 1 2 16.755V7.25a3.25 3.25 0 0 1 3.066-3.245L5.25 4h13.5Zm0 1.5H5.25l-.144.006A1.75 1.75 0 0 0 3.5 7.25v9.505c0 .966.784 1.75 1.75 1.75h13.5a1.75 1.75 0 0 0 1.75-1.75V7.25a1.75 1.75 0 0 0-1.75-1.75Zm-9.497 7a.75.75 0 0 1 .75.75v.582c0 1.272-.969 1.918-2.502 1.918S5 15.104 5 13.831v-.581a.75.75 0 0 1 .75-.75h3.503Zm1.58-.001 1.417.001a.75.75 0 0 1 .75.75v.333c0 .963-.765 1.417-1.875 1.417-.116 0-.229-.005-.337-.015a2.85 2.85 0 0 0 .206-.9l.009-.253v-.582c0-.269-.061-.524-.17-.751Zm4.417.001h3a.75.75 0 0 1 .102 1.493L18.25 14h-3a.75.75 0 0 1-.102-1.493l.102-.007h3-3Zm-7.75-4a1.5 1.5 0 1 1 0 3.001 1.5 1.5 0 0 1 0-3.001Zm3.87.502a1.248 1.248 0 1 1 0 2.496 1.248 1.248 0 0 1 0-2.496Zm3.88.498h3a.75.75 0 0 1 .102 1.493L18.25 11h-3a.75.75 0 0 1-.102-1.493l.102-.007h3-3Z", "contact-card-outline": "M19.75 4A2.25 2.25 0 0 1 22 6.25v11.505a2.25 2.25 0 0 1-2.25 2.25H4.25A2.25 2.25 0 0 1 2 17.755V6.25A2.25 2.25 0 0 1 4.25 4h15.5Zm0 1.5H4.25a.75.75 0 0 0-.75.75v11.505c0 .414.336.75.75.75h15.5a.75.75 0 0 0 .75-.75V6.25a.75.75 0 0 0-.75-.75Zm-10 7a.75.75 0 0 1 .75.75v.493l-.008.108c-.163 1.113-1.094 1.65-2.492 1.65s-2.33-.537-2.492-1.65l-.008-.11v-.491a.75.75 0 0 1 .75-.75h3.5Zm3.502.496h4.498a.75.75 0 0 1 .102 1.493l-.102.007h-4.498a.75.75 0 0 1-.102-1.493l.102-.007h4.498-4.498ZM8 8.502a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm5.252.998h4.498a.75.75 0 0 1 .102 1.493L17.75 11h-4.498a.75.75 0 0 1-.102-1.493l.102-.007h4.498-4.498Z",