Files
9cb6c501b5 fix: html/body background not applied in appearance mode (#13989)
# Pull Request Template

## Description

This PR fixes the white background bleed visible in the widget, article
viewer, and Help Center when dark mode is active.
This change was previously merged but later reverted due to a background
being added for the unread bubble.

Reverted PR: https://github.com/chatwoot/chatwoot/pull/13955,
https://github.com/chatwoot/chatwoot/pull/13981

**What was happening**

While scrolling, the `<body>` element retained a white background in
dark mode. This occurred because dark mode classes were only applied to
inner container elements, not the root.

**What changed**

* **Widget:** Updated the `useDarkMode` composable to sync the `dark`
class to `<html>` using `watchEffect`, allowing `<body>` to inherit dark
theme variables. Also added background styles to `html`, `body`, and
`#app` in `woot.scss`.
* **Help center portal:** Moved `bg-white dark:bg-slate-900` from
`<main>` to `<body>` in the portal layout so the entire page background
responds correctly to dark mode, including within the widget iframe.
* **ArticleViewer:** Replaced hardcoded `bg-white` with `bg-n-solid-1`
to ensure better theming.


Fixes
https://linear.app/chatwoot/issue/CW-6704/widget-body-colour-not-implemented

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Screencasts

### Before

**Widget**


https://github.com/user-attachments/assets/e0224ad1-81a6-440a-a824-e115fb806728

**Help center**


https://github.com/user-attachments/assets/40a8ded5-5360-474d-9ec5-fd23e037c845



### After

**Widget**


https://github.com/user-attachments/assets/dd37cc68-99fc-4d60-b2ae-cf41f9d4d38c

<img width="347" height="265" alt="image"
src="https://github.com/user-attachments/assets/89460b36-c8ed-4579-9737-c258c6fe5da5"
/>


**Help center**


https://github.com/user-attachments/assets/bc998c4e-ef77-46fa-ac7f-4ea16d912ce3




## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-07-16 12:21:01 +05:30

391 lines
13 KiB
Vue
Executable File

<script>
import { mapGetters, mapActions } from 'vuex';
import { setHeader } from 'widget/helpers/axios';
import addHours from 'date-fns/addHours';
import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
import configMixin from './mixins/configMixin';
import { getLocale } from './helpers/urlParamsHelper';
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import { isEmptyObject } from 'widget/helpers/utils';
import Spinner from 'shared/components/Spinner.vue';
import {
getExtraSpaceToScroll,
loadedEventConfig,
} from './helpers/IframeEventHelper';
import {
ON_AGENT_MESSAGE_RECEIVED,
ON_CAMPAIGN_MESSAGE_CLICK,
ON_UNREAD_MESSAGE_CLICK,
} from './constants/widgetBusEvents';
import { useDarkMode } from 'widget/composables/useDarkMode';
import { useRouter } from 'vue-router';
import { useAvailability } from 'widget/composables/useAvailability';
import { SDK_SET_BUBBLE_VISIBILITY } from '../shared/constants/sharedFrameEvents';
import { emitter } from 'shared/helpers/mitt';
export default {
name: 'App',
components: {
Spinner,
},
mixins: [configMixin],
setup() {
const { prefersDarkMode } = useDarkMode();
const router = useRouter();
const { isInWorkingHours } = useAvailability();
return { prefersDarkMode, router, isInWorkingHours };
},
data() {
return {
isMobile: false,
campaignsSnoozedTill: undefined,
};
},
computed: {
...mapGetters({
activeCampaign: 'campaign/getActiveCampaign',
conversationSize: 'conversation/getConversationSize',
hideMessageBubble: 'appConfig/getHideMessageBubble',
isFetchingList: 'conversation/getIsFetchingList',
isRightAligned: 'appConfig/isRightAligned',
isWidgetOpen: 'appConfig/getIsWidgetOpen',
messageCount: 'conversation/getMessageCount',
unreadMessageCount: 'conversation/getUnreadMessageCount',
isWidgetStyleFlat: 'appConfig/isWidgetStyleFlat',
showUnreadMessagesDialog: 'appConfig/getShowUnreadMessagesDialog',
}),
isIFrame() {
return IFrameHelper.isIFrame();
},
isRNWebView() {
return RNHelper.isRNWebView();
},
isRTL() {
return this.$root.$i18n.locale
? getLanguageDirection(this.$root.$i18n.locale)
: false;
},
isUnreadOrCampaignView() {
return ['unread-messages', 'campaigns'].includes(this.$route.name);
},
},
watch: {
activeCampaign() {
this.setCampaignView();
},
isRTL: {
immediate: true,
handler(value) {
document.documentElement.dir = value ? 'rtl' : 'ltr';
},
},
},
mounted() {
const { websiteToken, locale, widgetColor } = window.chatwootWebChannel;
this.setLocale(locale);
this.setWidgetColor(widgetColor);
this.setWidgetColorVariable(widgetColor);
setHeader(window.authToken);
if (this.isIFrame) {
this.registerListeners();
this.sendLoadedEvent();
} else {
this.fetchOldConversations();
this.fetchAvailableAgents(websiteToken);
this.setLocale(getLocale(window.location.search));
}
if (this.isRNWebView) {
this.registerListeners();
this.sendRNWebViewLoadedEvent();
}
this.$store.dispatch('conversationAttributes/getAttributes');
this.registerUnreadEvents();
this.registerCampaignEvents();
},
methods: {
...mapActions('appConfig', [
'setAppConfig',
'setReferrerHost',
'setWidgetColor',
'setBubbleVisibility',
'setColorScheme',
]),
...mapActions('conversation', ['fetchOldConversations']),
...mapActions('campaign', [
'initCampaigns',
'executeCampaign',
'resetCampaign',
]),
...mapActions('agent', ['fetchAvailableAgents']),
setWidgetColorVariable(widgetColor) {
if (widgetColor) {
document.documentElement.style.setProperty(
'--widget-color',
widgetColor
);
}
},
scrollConversationToBottom() {
const container = this.$el.querySelector('.conversation-wrap');
container.scrollTop = container.scrollHeight;
},
setBubbleLabel() {
IFrameHelper.sendMessage({
event: 'setBubbleLabel',
label: this.$t('BUBBLE.LABEL'),
});
},
setIframeHeight(isFixedHeight) {
this.$nextTick(() => {
const extraHeight = getExtraSpaceToScroll();
IFrameHelper.sendMessage({
event: 'updateIframeHeight',
isFixedHeight,
extraHeight,
});
});
},
setLocale(localeWithVariation) {
if (!localeWithVariation) return;
const { enabledLanguages } = window.chatwootWebChannel;
const localeWithoutVariation = localeWithVariation.split('_')[0];
const hasLocaleWithoutVariation = enabledLanguages.some(
lang => lang.iso_639_1_code === localeWithoutVariation
);
const hasLocaleWithVariation = enabledLanguages.some(
lang => lang.iso_639_1_code === localeWithVariation
);
if (hasLocaleWithVariation) {
this.$root.$i18n.locale = localeWithVariation;
} else if (hasLocaleWithoutVariation) {
this.$root.$i18n.locale = localeWithoutVariation;
}
},
registerUnreadEvents() {
emitter.on(ON_AGENT_MESSAGE_RECEIVED, () => {
const { name: routeName } = this.$route;
if ((this.isWidgetOpen || !this.isIFrame) && routeName === 'messages') {
this.$store.dispatch('conversation/setUserLastSeen');
}
this.setUnreadView();
});
emitter.on(ON_UNREAD_MESSAGE_CLICK, () => {
this.router
.replace({ name: 'messages' })
.then(() => this.unsetUnreadView());
});
},
registerCampaignEvents() {
emitter.on(ON_CAMPAIGN_MESSAGE_CLICK, () => {
if (this.shouldShowPreChatForm) {
this.router.replace({ name: 'prechat-form' });
} else {
this.router.replace({ name: 'messages' });
emitter.emit('execute-campaign', {
campaignId: this.activeCampaign.id,
});
}
this.unsetUnreadView();
});
emitter.on('execute-campaign', campaignDetails => {
const { customAttributes, campaignId } = campaignDetails;
const { websiteToken } = window.chatwootWebChannel;
this.executeCampaign({ campaignId, websiteToken, customAttributes });
this.router.replace({ name: 'messages' });
});
emitter.on('snooze-campaigns', () => {
const expireBy = addHours(new Date(), 1);
this.campaignsSnoozedTill = Number(expireBy);
});
},
setCampaignView() {
const { messageCount, activeCampaign } = this;
const shouldSnoozeCampaign =
this.campaignsSnoozedTill && this.campaignsSnoozedTill > Date.now();
const isCampaignReadyToExecute =
!isEmptyObject(activeCampaign) &&
!messageCount &&
!shouldSnoozeCampaign;
if (this.isIFrame && isCampaignReadyToExecute) {
this.router.replace({ name: 'campaigns' }).then(() => {
this.setIframeHeight(true);
IFrameHelper.sendMessage({ event: 'setUnreadMode' });
});
}
},
setUnreadView() {
const { unreadMessageCount } = this;
if (!this.showUnreadMessagesDialog) {
this.handleUnreadNotificationDot();
} else if (
this.isIFrame &&
unreadMessageCount > 0 &&
!this.isWidgetOpen
) {
this.router.replace({ name: 'unread-messages' }).then(() => {
this.setIframeHeight(true);
IFrameHelper.sendMessage({ event: 'setUnreadMode' });
});
this.handleUnreadNotificationDot();
}
},
unsetUnreadView() {
if (this.isIFrame) {
IFrameHelper.sendMessage({ event: 'resetUnreadMode' });
this.setIframeHeight(false);
this.handleUnreadNotificationDot();
}
},
handleUnreadNotificationDot() {
const { unreadMessageCount } = this;
if (this.isIFrame) {
IFrameHelper.sendMessage({
event: 'handleNotificationDot',
unreadMessageCount,
});
}
},
createWidgetEvents(message) {
const { eventName } = message;
const isWidgetTriggerEvent = eventName === 'webwidget.triggered';
if (
isWidgetTriggerEvent &&
['unread-messages', 'campaigns'].includes(this.$route.name)
) {
return;
}
this.$store.dispatch('events/create', { name: eventName });
},
registerListeners() {
const { websiteToken } = window.chatwootWebChannel;
window.addEventListener('message', e => {
if (!IFrameHelper.isAValidEvent(e)) {
return;
}
const message = IFrameHelper.getMessage(e);
if (message.event === 'config-set') {
this.setLocale(message.locale);
this.setBubbleLabel();
this.fetchOldConversations().then(() => this.setUnreadView());
this.fetchAvailableAgents(websiteToken);
this.setAppConfig(message);
this.$store.dispatch('contacts/get');
this.setCampaignReadData(message.campaignsSnoozedTill);
} else if (message.event === 'widget-visible') {
this.scrollConversationToBottom();
} else if (message.event === 'change-url') {
const { referrerURL, referrerHost } = message;
this.initCampaigns({
currentURL: referrerURL,
websiteToken,
isInBusinessHours: this.isInWorkingHours,
});
window.referrerURL = referrerURL;
this.setReferrerHost(referrerHost);
} else if (message.event === 'toggle-close-button') {
this.isMobile = message.isMobile;
} else if (message.event === 'push-event') {
this.createWidgetEvents(message);
} else if (message.event === 'set-label') {
this.$store.dispatch('conversationLabels/create', message.label);
} else if (message.event === 'remove-label') {
this.$store.dispatch('conversationLabels/destroy', message.label);
} else if (message.event === 'set-user') {
this.$store.dispatch('contacts/setUser', message);
} else if (message.event === 'set-custom-attributes') {
this.$store.dispatch(
'contacts/setCustomAttributes',
message.customAttributes
);
} else if (message.event === 'delete-custom-attribute') {
this.$store.dispatch(
'contacts/deleteCustomAttribute',
message.customAttribute
);
} else if (message.event === 'set-conversation-custom-attributes') {
this.$store.dispatch(
'conversation/setCustomAttributes',
message.customAttributes
);
} else if (message.event === 'delete-conversation-custom-attribute') {
this.$store.dispatch(
'conversation/deleteCustomAttribute',
message.customAttribute
);
} else if (message.event === 'set-locale') {
this.setLocale(message.locale);
this.setBubbleLabel();
} else if (message.event === 'set-color-scheme') {
this.setColorScheme(message.darkMode);
} else if (message.event === 'toggle-open') {
this.$store.dispatch('appConfig/toggleWidgetOpen', message.isOpen);
const shouldShowMessageView =
['home'].includes(this.$route.name) &&
message.isOpen &&
this.messageCount;
const shouldShowHomeView =
!message.isOpen &&
['unread-messages', 'campaigns'].includes(this.$route.name);
if (shouldShowMessageView) {
this.router.replace({ name: 'messages' });
}
if (shouldShowHomeView) {
this.$store.dispatch('conversation/setUserLastSeen');
this.unsetUnreadView();
this.router.replace({ name: 'home' });
}
if (!message.isOpen) {
this.resetCampaign();
}
} else if (message.event === SDK_SET_BUBBLE_VISIBILITY) {
this.setBubbleVisibility(message.hideMessageBubble);
}
});
},
sendLoadedEvent() {
IFrameHelper.sendMessage(loadedEventConfig());
},
sendRNWebViewLoadedEvent() {
RNHelper.sendMessage(loadedEventConfig());
},
setCampaignReadData(snoozedTill) {
if (snoozedTill) {
this.campaignsSnoozedTill = Number(snoozedTill);
}
},
},
};
</script>
<template>
<div
v-if="!conversationSize && isFetchingList"
class="flex items-center justify-center flex-1 h-full bg-n-background"
:class="{ dark: prefersDarkMode }"
>
<Spinner size="" />
</div>
<div
v-else
class="flex flex-col justify-end h-full"
:class="{
'is-mobile': isMobile,
'is-widget-right': isRightAligned,
'is-bubble-hidden': hideMessageBubble,
'is-flat-design': isWidgetStyleFlat,
'bg-n-slate-2 dark:bg-n-solid-1': !isUnreadOrCampaignView,
dark: prefersDarkMode,
}"
>
<router-view />
</div>
</template>
<style lang="scss">
@import 'widget/assets/scss/woot.scss';
</style>