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",