diff --git a/app/javascript/shared/components/ChatCard.vue b/app/javascript/shared/components/ChatCard.vue index 14683784f..7e3339101 100644 --- a/app/javascript/shared/components/ChatCard.vue +++ b/app/javascript/shared/components/ChatCard.vue @@ -1,12 +1,11 @@ - + {{ title }} - + {{ description }} diff --git a/app/javascript/shared/components/ChatForm.vue b/app/javascript/shared/components/ChatForm.vue index f246f94a7..80a45e26d 100644 --- a/app/javascript/shared/components/ChatForm.vue +++ b/app/javascript/shared/components/ChatForm.vue @@ -1,10 +1,9 @@ @@ -13,7 +16,7 @@ export default { {{ replyWaitMessage }} diff --git a/app/javascript/widget/components/ChatHeaderExpanded.vue b/app/javascript/widget/components/ChatHeaderExpanded.vue index 864985fde..526baba39 100755 --- a/app/javascript/widget/components/ChatHeaderExpanded.vue +++ b/app/javascript/widget/components/ChatHeaderExpanded.vue @@ -1,13 +1,12 @@ @@ -51,12 +54,12 @@ export default { diff --git a/app/javascript/widget/components/ChatInputWrap.vue b/app/javascript/widget/components/ChatInputWrap.vue index c68ec3fdf..5f09659a3 100755 --- a/app/javascript/widget/components/ChatInputWrap.vue +++ b/app/javascript/widget/components/ChatInputWrap.vue @@ -6,7 +6,7 @@ import ChatSendButton from 'widget/components/ChatSendButton.vue'; import configMixin from '../mixins/configMixin'; import FluentIcon from 'shared/components/FluentIcon/Index.vue'; import ResizableTextArea from 'shared/components/ResizableTextArea.vue'; -import darkModeMixin from 'widget/mixins/darkModeMixin.js'; +import { useDarkMode } from 'widget/composables/useDarkMode'; const EmojiInput = () => import('shared/components/emoji/EmojiInput.vue'); @@ -19,7 +19,7 @@ export default { FluentIcon, ResizableTextArea, }, - mixins: [configMixin, darkModeMixin], + mixins: [configMixin], props: { onSendMessage: { type: Function, @@ -30,7 +30,10 @@ export default { default: () => {}, }, }, - + setup() { + const { getThemeClass } = useDarkMode(); + return { getThemeClass }; + }, data() { return { userInput: '', @@ -51,13 +54,16 @@ export default { return this.userInput.length > 0; }, inputColor() { - return `${this.$dm('bg-white', 'dark:bg-slate-600')} - ${this.$dm('text-black-900', 'dark:text-slate-50')}`; + return `${this.getThemeClass('bg-white', 'dark:bg-slate-600')} + ${this.getThemeClass('text-black-900', 'dark:text-slate-50')}`; }, emojiIconColor() { return this.showEmojiPicker - ? `text-woot-500 ${this.$dm('text-black-900', 'dark:text-slate-100')}` - : `${this.$dm('text-black-900', 'dark:text-slate-100')}`; + ? `text-woot-500 ${this.getThemeClass( + 'text-black-900', + 'dark:text-slate-100' + )}` + : `${this.getThemeClass('text-black-900', 'dark:text-slate-100')}`; }, }, watch: { @@ -128,7 +134,7 @@ export default { [], }, }, + setup() { + const { darkMode } = useDarkMode(); + return { darkMode }; + }, data() { return { previousScrollHeight: 0, diff --git a/app/javascript/widget/components/FileBubble.vue b/app/javascript/widget/components/FileBubble.vue index faba119f0..c4b18805d 100644 --- a/app/javascript/widget/components/FileBubble.vue +++ b/app/javascript/widget/components/FileBubble.vue @@ -1,13 +1,12 @@ @@ -27,7 +25,7 @@ export default { @@ -35,15 +33,16 @@ export default { - {{ - item.title - }} + {{ item.title }} {{ truncateMessage(item.description) }} diff --git a/app/javascript/widget/components/template/EmailInput.vue b/app/javascript/widget/components/template/EmailInput.vue index 2d95569eb..45718a055 100644 --- a/app/javascript/widget/components/template/EmailInput.vue +++ b/app/javascript/widget/components/template/EmailInput.vue @@ -6,14 +6,13 @@ import { getContrastingTextColor } from '@chatwoot/utils'; import FluentIcon from 'shared/components/FluentIcon/Index.vue'; import Spinner from 'shared/components/Spinner.vue'; -import darkModeMixin from 'widget/mixins/darkModeMixin.js'; +import { useDarkMode } from 'widget/composables/useDarkMode'; export default { components: { FluentIcon, Spinner, }, - mixins: [darkModeMixin], props: { messageId: { type: Number, @@ -25,7 +24,8 @@ export default { }, }, setup() { - return { v$: useVuelidate() }; + const { getThemeClass } = useDarkMode(); + return { v$: useVuelidate(), getThemeClass }; }, data() { return { @@ -47,9 +47,9 @@ export default { ); }, inputColor() { - return `${this.$dm('bg-white', 'dark:bg-slate-600')} - ${this.$dm('text-black-900', 'dark:text-slate-50')} - ${this.$dm('border-black-200', 'dark:border-black-500')}`; + return `${this.getThemeClass('bg-white', 'dark:bg-slate-600')} + ${this.getThemeClass('text-black-900', 'dark:text-slate-50')} + ${this.getThemeClass('border-black-200', 'dark:border-black-500')}`; }, inputHasError() { return this.v$.email.$error diff --git a/app/javascript/widget/composables/specs/useDarkMode.spec.js b/app/javascript/widget/composables/specs/useDarkMode.spec.js new file mode 100644 index 000000000..0ed6b43bb --- /dev/null +++ b/app/javascript/widget/composables/specs/useDarkMode.spec.js @@ -0,0 +1,71 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { useDarkMode } from '../useDarkMode'; +import { useMapGetter } from 'dashboard/composables/store'; + +vi.mock('dashboard/composables/store', () => ({ + useMapGetter: vi.fn(), +})); + +describe('useDarkMode', () => { + let mockDarkMode; + + beforeEach(() => { + mockDarkMode = { value: 'light' }; + vi.mocked(useMapGetter).mockReturnValue(mockDarkMode); + }); + + it('returns darkMode, prefersDarkMode, and getThemeClass', () => { + const result = useDarkMode(); + expect(result).toHaveProperty('darkMode'); + expect(result).toHaveProperty('prefersDarkMode'); + expect(result).toHaveProperty('getThemeClass'); + }); + + describe('prefersDarkMode', () => { + it('returns false when darkMode is light', () => { + const { prefersDarkMode } = useDarkMode(); + expect(prefersDarkMode.value).toBe(false); + }); + + it('returns true when darkMode is dark', () => { + mockDarkMode.value = 'dark'; + const { prefersDarkMode } = useDarkMode(); + expect(prefersDarkMode.value).toBe(true); + }); + + it('returns true when darkMode is auto and OS prefers dark mode', () => { + mockDarkMode.value = 'auto'; + vi.spyOn(window, 'matchMedia').mockReturnValue({ matches: true }); + const { prefersDarkMode } = useDarkMode(); + expect(prefersDarkMode.value).toBe(true); + }); + + it('returns false when darkMode is auto and OS prefers light mode', () => { + mockDarkMode.value = 'auto'; + vi.spyOn(window, 'matchMedia').mockReturnValue({ matches: false }); + const { prefersDarkMode } = useDarkMode(); + expect(prefersDarkMode.value).toBe(false); + }); + }); + + describe('getThemeClass', () => { + it('returns light class when darkMode is light', () => { + const { getThemeClass } = useDarkMode(); + expect(getThemeClass('light-class', 'dark-class')).toBe('light-class'); + }); + + it('returns dark class when darkMode is dark', () => { + mockDarkMode.value = 'dark'; + const { getThemeClass } = useDarkMode(); + expect(getThemeClass('light-class', 'dark-class')).toBe('dark-class'); + }); + + it('returns both classes when darkMode is auto', () => { + mockDarkMode.value = 'auto'; + const { getThemeClass } = useDarkMode(); + expect(getThemeClass('light-class', 'dark-class')).toBe( + 'light-class dark-class' + ); + }); + }); +}); diff --git a/app/javascript/widget/composables/useDarkMode.js b/app/javascript/widget/composables/useDarkMode.js new file mode 100644 index 000000000..118d07161 --- /dev/null +++ b/app/javascript/widget/composables/useDarkMode.js @@ -0,0 +1,39 @@ +import { computed } from 'vue'; +import { useMapGetter } from 'dashboard/composables/store'; + +const isDarkModeAuto = mode => mode === 'auto'; +const isDarkMode = mode => mode === 'dark'; + +const getSystemPreference = () => + window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false; + +const calculatePrefersDarkMode = (mode, systemPreference) => + isDarkModeAuto(mode) ? systemPreference : isDarkMode(mode); + +const calculateThemeClass = (mode, light, dark) => { + if (isDarkModeAuto(mode)) return `${light} ${dark}`; + return isDarkMode(mode) ? dark : light; +}; + +/** + * Composable for handling dark mode. + * @returns {Object} An object containing computed properties and methods for dark mode. + */ +export function useDarkMode() { + const darkMode = useMapGetter('appConfig/darkMode'); + + const systemPreference = computed(getSystemPreference); + + const prefersDarkMode = computed(() => + calculatePrefersDarkMode(darkMode.value, systemPreference.value) + ); + + const getThemeClass = (light, dark) => + calculateThemeClass(darkMode.value, light, dark); + + return { + darkMode, + prefersDarkMode, + getThemeClass, + }; +} diff --git a/app/javascript/widget/mixins/darkModeMixin.js b/app/javascript/widget/mixins/darkModeMixin.js deleted file mode 100644 index 62a225521..000000000 --- a/app/javascript/widget/mixins/darkModeMixin.js +++ /dev/null @@ -1,24 +0,0 @@ -import { mapGetters } from 'vuex'; - -export default { - computed: { - ...mapGetters({ darkMode: 'appConfig/darkMode' }), - prefersDarkMode() { - const isOSOnDarkMode = - this.darkMode === 'auto' && - window.matchMedia('(prefers-color-scheme: dark)').matches; - return isOSOnDarkMode || this.darkMode === 'dark'; - }, - }, - methods: { - $dm(light, dark) { - if (this.darkMode === 'light') { - return light; - } - if (this.darkMode === 'dark') { - return dark; - } - return light + ' ' + dark; - }, - }, -}; diff --git a/app/javascript/widget/mixins/specs/darkModeMixin.spec.js b/app/javascript/widget/mixins/specs/darkModeMixin.spec.js deleted file mode 100644 index 852bcc6e6..000000000 --- a/app/javascript/widget/mixins/specs/darkModeMixin.spec.js +++ /dev/null @@ -1,41 +0,0 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; -import darkModeMixin from '../darkModeMixin'; -import Vuex from 'vuex'; -const localVue = createLocalVue(); -localVue.use(Vuex); - -const darkModeValues = ['light', 'auto']; - -describe('darkModeMixin', () => { - let getters; - let store; - beforeEach(() => { - getters = { - 'appConfig/darkMode': () => darkModeValues[0], - }; - store = new Vuex.Store({ getters }); - }); - - it('if light theme', () => { - const Component = { - render() {}, - mixins: [darkModeMixin], - }; - const wrapper = shallowMount(Component, { store, localVue }); - expect(wrapper.vm.$dm('bg-100', 'bg-600')).toBe('bg-100'); - }); - - it('if auto theme', () => { - getters = { - 'appConfig/darkMode': () => darkModeValues[2], - }; - store = new Vuex.Store({ getters }); - - const Component = { - render() {}, - mixins: [darkModeMixin], - }; - const wrapper = shallowMount(Component, { store, localVue }); - expect(wrapper.vm.$dm('bg-100', 'bg-600')).toBe('bg-100 bg-600'); - }); -}); diff --git a/app/javascript/widget/views/Home.vue b/app/javascript/widget/views/Home.vue index 1d17e406c..8aabda108 100755 --- a/app/javascript/widget/views/Home.vue +++ b/app/javascript/widget/views/Home.vue @@ -6,6 +6,8 @@ import ArticleCardSkeletonLoader from 'widget/components/ArticleCardSkeletonLoad import { mapGetters } from 'vuex'; import darkModeMixin from 'widget/mixins/darkModeMixin'; import useReplaceRoute from 'widget/composables/useReplaceRoute'; +import { useDarkMode } from 'widget/composables/useDarkMode'; +import routerMixin from 'widget/mixins/routerMixin'; import configMixin from 'widget/mixins/configMixin'; export default { @@ -15,10 +17,11 @@ export default { TeamAvailability, ArticleCardSkeletonLoader, }, - mixins: [configMixin, darkModeMixin], + mixins: [configMixin], setup() { const replaceRoute = useReplaceRoute(); - return { replaceRoute }; + const { prefersDarkMode } = useDarkMode(); + return { prefersDarkMode, replaceRoute }; }, computed: { ...mapGetters({
+
{{ description }}