chore: Update theme colors and add new Inter variable fonts (#13347)
# Pull Request Template ## Description This PR includes the following updates: 1. Updated the design system color tokens by introducing new tokens for surfaces, overlays, buttons, labels, and cards, along with refinements to existing shades. 2. Refreshed both light and dark themes with adjusted background, border, and solid colors. 3. Replaced static Inter font files with the Inter variable font (including italic), supporting weights from 100–900. 4. Added custom font weights (420, 440, 460, 520) along with custom typography classes to enable more fine-grained and consistent typography control. ## Type of change - [x] New feature (non-breaking change which adds functionality) ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] 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: Pranav <pranav@chatwoot.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { h, ref, computed, onMounted } from 'vue';
|
||||
import { provideSidebarContext } from './provider';
|
||||
import { provideSidebarContext, useSidebarResize } from './provider';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useKbd } from 'dashboard/composables/utils/useKbd';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
@@ -8,6 +8,7 @@ import { useStore } from 'vuex';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import { useWindowSize, useEventListener } from '@vueuse/core';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
|
||||
@@ -15,7 +16,9 @@ import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import SidebarGroup from './SidebarGroup.vue';
|
||||
import SidebarProfileMenu from './SidebarProfileMenu.vue';
|
||||
import SidebarChangelogCard from './SidebarChangelogCard.vue';
|
||||
import SidebarChangelogButton from './SidebarChangelogButton.vue';
|
||||
import ChannelLeaf from './ChannelLeaf.vue';
|
||||
import ChannelIcon from 'next/icon/ChannelIcon.vue';
|
||||
import SidebarAccountSwitcher from './SidebarAccountSwitcher.vue';
|
||||
import Logo from 'next/icon/Logo.vue';
|
||||
import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue';
|
||||
@@ -42,6 +45,10 @@ const { t } = useI18n();
|
||||
const isACustomBrandedInstance = useMapGetter(
|
||||
'globalConfig/isACustomBrandedInstance'
|
||||
);
|
||||
const isRTL = useMapGetter('accounts/isRTL');
|
||||
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
const isMobile = computed(() => windowWidth.value < 768);
|
||||
|
||||
const toggleShortcutModalFn = show => {
|
||||
if (show) {
|
||||
@@ -58,11 +65,85 @@ const expandedItem = ref(null);
|
||||
const setExpandedItem = name => {
|
||||
expandedItem.value = expandedItem.value === name ? null : name;
|
||||
};
|
||||
|
||||
const {
|
||||
sidebarWidth,
|
||||
isCollapsed,
|
||||
setSidebarWidth,
|
||||
saveWidth,
|
||||
snapToCollapsed,
|
||||
snapToExpanded,
|
||||
COLLAPSED_THRESHOLD,
|
||||
} = useSidebarResize();
|
||||
|
||||
// On mobile, sidebar is always expanded (flyout mode)
|
||||
const isEffectivelyCollapsed = computed(
|
||||
() => !isMobile.value && isCollapsed.value
|
||||
);
|
||||
|
||||
// Resize handle logic
|
||||
const isResizing = ref(false);
|
||||
const startX = ref(0);
|
||||
const startWidth = ref(0);
|
||||
|
||||
provideSidebarContext({
|
||||
expandedItem,
|
||||
setExpandedItem,
|
||||
isCollapsed: isEffectivelyCollapsed,
|
||||
sidebarWidth,
|
||||
isResizing,
|
||||
});
|
||||
|
||||
// Get clientX from mouse or touch event
|
||||
const getClientX = event =>
|
||||
event.touches ? event.touches[0].clientX : event.clientX;
|
||||
|
||||
const onResizeStart = event => {
|
||||
isResizing.value = true;
|
||||
startX.value = getClientX(event);
|
||||
startWidth.value = sidebarWidth.value;
|
||||
Object.assign(document.body.style, {
|
||||
cursor: 'col-resize',
|
||||
userSelect: 'none',
|
||||
});
|
||||
// Prevent default to avoid scrolling on touch
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const onResizeMove = event => {
|
||||
if (!isResizing.value) return;
|
||||
|
||||
const delta = isRTL.value
|
||||
? startX.value - getClientX(event)
|
||||
: getClientX(event) - startX.value;
|
||||
setSidebarWidth(startWidth.value + delta);
|
||||
};
|
||||
|
||||
const onResizeEnd = () => {
|
||||
if (!isResizing.value) return;
|
||||
|
||||
isResizing.value = false;
|
||||
Object.assign(document.body.style, { cursor: '', userSelect: '' });
|
||||
|
||||
// Snap to collapsed state if below threshold
|
||||
if (sidebarWidth.value < COLLAPSED_THRESHOLD) {
|
||||
snapToCollapsed();
|
||||
} else {
|
||||
saveWidth();
|
||||
}
|
||||
};
|
||||
|
||||
const onResizeHandleDoubleClick = () => {
|
||||
if (isCollapsed.value) snapToExpanded();
|
||||
else snapToCollapsed();
|
||||
};
|
||||
|
||||
// Support both mouse and touch events
|
||||
useEventListener(document, 'mousemove', onResizeMove);
|
||||
useEventListener(document, 'mouseup', onResizeEnd);
|
||||
useEventListener(document, 'touchmove', onResizeMove, { passive: false });
|
||||
useEventListener(document, 'touchend', onResizeEnd);
|
||||
|
||||
const inboxes = useMapGetter('inboxes/getInboxes');
|
||||
const labels = useMapGetter('labels/getLabelsOnSidebar');
|
||||
const teams = useMapGetter('teams/getMyTeams');
|
||||
@@ -192,6 +273,7 @@ const menuItems = computed(() => {
|
||||
children: sortedInboxes.value.map(inbox => ({
|
||||
name: `${inbox.name}-${inbox.id}`,
|
||||
label: inbox.name,
|
||||
icon: h(ChannelIcon, { inbox, class: 'size-[12px]' }),
|
||||
to: accountScopedRoute('inbox_dashboard', { inbox_id: inbox.id }),
|
||||
component: leafProps =>
|
||||
h(ChannelLeaf, {
|
||||
@@ -210,7 +292,7 @@ const menuItems = computed(() => {
|
||||
name: `${label.title}-${label.id}`,
|
||||
label: label.title,
|
||||
icon: h('span', {
|
||||
class: `size-[12px] ring-1 ring-n-alpha-1 dark:ring-white/20 ring-inset rounded-sm`,
|
||||
class: `size-[8px] rounded-sm`,
|
||||
style: { backgroundColor: label.color },
|
||||
}),
|
||||
to: accountScopedRoute('label_conversations', {
|
||||
@@ -338,7 +420,7 @@ const menuItems = computed(() => {
|
||||
name: `${label.title}-${label.id}`,
|
||||
label: label.title,
|
||||
icon: h('span', {
|
||||
class: `size-[12px] ring-1 ring-n-alpha-1 dark:ring-white/20 ring-inset rounded-sm`,
|
||||
class: `size-[8px] rounded-sm`,
|
||||
style: { backgroundColor: label.color },
|
||||
}),
|
||||
to: accountScopedRoute(
|
||||
@@ -604,32 +686,56 @@ const menuItems = computed(() => {
|
||||
closeMobileSidebar,
|
||||
{ ignore: ['#mobile-sidebar-launcher'] },
|
||||
]"
|
||||
class="bg-n-solid-2 rtl:border-l ltr:border-r border-n-weak flex flex-col text-sm pb-1 fixed top-0 ltr:left-0 rtl:right-0 h-full z-40 transition-transform duration-200 ease-in-out md:static w-[200px] basis-[200px] md:flex-shrink-0 md:ltr:translate-x-0 md:rtl:-translate-x-0"
|
||||
class="bg-n-background flex flex-col text-sm pb-0.5 fixed top-0 ltr:left-0 rtl:right-0 h-full z-40 w-[200px] md:w-auto md:relative md:flex-shrink-0 md:ltr:translate-x-0 md:rtl:translate-x-0 ltr:border-r rtl:border-l border-n-weak"
|
||||
:class="[
|
||||
{
|
||||
'shadow-lg md:shadow-none': isMobileSidebarOpen,
|
||||
'ltr:-translate-x-full rtl:translate-x-full': !isMobileSidebarOpen,
|
||||
'transition-transform duration-200 ease-out md:transition-[width]':
|
||||
!isResizing,
|
||||
},
|
||||
]"
|
||||
:style="isMobile ? undefined : { width: `${sidebarWidth}px` }"
|
||||
>
|
||||
<section class="grid gap-2 mt-2 mb-4">
|
||||
<div class="flex gap-2 items-center px-2 min-w-0">
|
||||
<div class="grid flex-shrink-0 place-content-center size-6">
|
||||
<Logo class="size-4" />
|
||||
</div>
|
||||
<div class="flex-shrink-0 w-px h-3 bg-n-strong" />
|
||||
<SidebarAccountSwitcher
|
||||
class="flex-grow -mx-1 min-w-0"
|
||||
@show-create-account-modal="emit('showCreateAccountModal')"
|
||||
/>
|
||||
<section
|
||||
class="grid"
|
||||
:class="isEffectivelyCollapsed ? 'mt-3 mb-6 gap-4' : 'mt-1 mb-4 gap-2'"
|
||||
>
|
||||
<div
|
||||
class="flex gap-2 items-center min-w-0"
|
||||
:class="{
|
||||
'justify-center px-1': isEffectivelyCollapsed,
|
||||
'px-2': !isEffectivelyCollapsed,
|
||||
}"
|
||||
>
|
||||
<template v-if="isEffectivelyCollapsed">
|
||||
<SidebarAccountSwitcher
|
||||
is-collapsed
|
||||
@show-create-account-modal="emit('showCreateAccountModal')"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="grid flex-shrink-0 place-content-center size-6">
|
||||
<Logo class="size-4" />
|
||||
</div>
|
||||
<div class="flex-shrink-0 w-px h-3 bg-n-strong" />
|
||||
<SidebarAccountSwitcher
|
||||
class="flex-grow -mx-1 min-w-0"
|
||||
@show-create-account-modal="emit('showCreateAccountModal')"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex gap-2 px-2">
|
||||
<div
|
||||
class="flex gap-2"
|
||||
:class="isEffectivelyCollapsed ? 'flex-col items-center' : 'px-2'"
|
||||
>
|
||||
<RouterLink
|
||||
v-if="!isEffectivelyCollapsed"
|
||||
:to="{ name: 'search' }"
|
||||
class="flex gap-2 items-center px-2 py-1 w-full h-7 rounded-lg outline outline-1 outline-n-weak bg-n-solid-3 dark:bg-n-black/30"
|
||||
class="flex gap-2 items-center px-2 py-1 w-full h-7 rounded-lg outline outline-1 outline-n-weak bg-n-button-color transition-all duration-100 ease-out"
|
||||
>
|
||||
<span class="flex-shrink-0 i-lucide-search size-4 text-n-slate-11" />
|
||||
<span class="flex-grow text-left">
|
||||
<span class="flex-shrink-0 i-lucide-search size-4 text-n-slate-10" />
|
||||
<span class="flex-grow text-start text-n-slate-10">
|
||||
{{ t('COMBOBOX.SEARCH_PLACEHOLDER') }}
|
||||
</span>
|
||||
<span
|
||||
@@ -638,21 +744,41 @@ const menuItems = computed(() => {
|
||||
{{ searchShortcut }}
|
||||
</span>
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
v-else
|
||||
:to="{ name: 'search' }"
|
||||
class="flex items-center justify-center size-8 rounded-lg outline outline-1 outline-n-weak bg-n-button-color transition-all duration-100 ease-out hover:bg-n-alpha-2 dark:hover:bg-n-slate-9/30"
|
||||
:title="t('COMBOBOX.SEARCH_PLACEHOLDER')"
|
||||
>
|
||||
<span class="i-lucide-search size-4 text-n-slate-11" />
|
||||
</RouterLink>
|
||||
<ComposeConversation align-position="right" @close="onComposeClose">
|
||||
<template #trigger="{ toggle }">
|
||||
<template #trigger="{ toggle, isOpen }">
|
||||
<Button
|
||||
icon="i-lucide-pen-line"
|
||||
color="slate"
|
||||
size="sm"
|
||||
class="!h-7 !bg-n-solid-3 dark:!bg-n-black/30 !outline-n-weak !text-n-slate-11"
|
||||
class="dark:hover:!bg-n-slate-9/30"
|
||||
:class="[
|
||||
isEffectivelyCollapsed
|
||||
? '!size-8 !outline-n-weak !text-n-slate-11'
|
||||
: '!h-7 !outline-n-weak !text-n-slate-11',
|
||||
{ '!bg-n-alpha-2 dark:!bg-n-slate-9/30': isOpen },
|
||||
]"
|
||||
@click="onComposeOpen(toggle)"
|
||||
/>
|
||||
</template>
|
||||
</ComposeConversation>
|
||||
</div>
|
||||
</section>
|
||||
<nav class="grid overflow-y-scroll flex-grow gap-2 px-2 pb-5 no-scrollbar">
|
||||
<ul class="flex flex-col gap-1.5 m-0 list-none">
|
||||
<nav
|
||||
class="grid overflow-y-scroll flex-grow gap-2 pb-5 no-scrollbar min-w-0"
|
||||
:class="isEffectivelyCollapsed ? 'px-1' : 'px-2'"
|
||||
>
|
||||
<ul
|
||||
class="flex flex-col gap-1 m-0 list-none min-w-0"
|
||||
:class="{ 'items-center': isEffectivelyCollapsed }"
|
||||
>
|
||||
<SidebarGroup
|
||||
v-for="item in menuItems"
|
||||
:key="item.name"
|
||||
@@ -664,18 +790,43 @@ const menuItems = computed(() => {
|
||||
class="flex relative flex-col flex-shrink-0 gap-1 justify-between items-center"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-x-0 -top-[31px] h-8 bg-gradient-to-t from-n-solid-2 to-transparent"
|
||||
class="pointer-events-none absolute inset-x-0 -top-[1.938rem] h-8 bg-gradient-to-t from-n-background to-transparent"
|
||||
/>
|
||||
<SidebarChangelogCard
|
||||
v-if="isOnChatwootCloud && !isACustomBrandedInstance"
|
||||
v-if="
|
||||
isOnChatwootCloud &&
|
||||
!isACustomBrandedInstance &&
|
||||
!isEffectivelyCollapsed
|
||||
"
|
||||
/>
|
||||
<SidebarChangelogButton
|
||||
v-if="
|
||||
isOnChatwootCloud &&
|
||||
!isACustomBrandedInstance &&
|
||||
isEffectivelyCollapsed
|
||||
"
|
||||
/>
|
||||
<div
|
||||
class="p-1 flex-shrink-0 flex w-full justify-between z-10 gap-2 items-center border-t border-n-weak shadow-[0px_-2px_4px_0px_rgba(27,28,29,0.02)]"
|
||||
class="p-1 flex-shrink-0 flex w-full z-50 gap-2 items-center border-t border-n-weak shadow-[0px_-2px_4px_0px_rgba(27,28,29,0.02)]"
|
||||
:class="isEffectivelyCollapsed ? 'justify-center' : 'justify-between'"
|
||||
>
|
||||
<SidebarProfileMenu
|
||||
:is-collapsed="isEffectivelyCollapsed"
|
||||
@open-key-shortcut-modal="emit('openKeyShortcutModal')"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Resize Handle (desktop only) -->
|
||||
<div
|
||||
class="hidden md:block absolute top-0 h-full w-1 cursor-col-resize z-40 ltr:right-0 rtl:left-0 group"
|
||||
@mousedown="onResizeStart"
|
||||
@touchstart="onResizeStart"
|
||||
@dblclick="onResizeHandleDoubleClick"
|
||||
>
|
||||
<div
|
||||
class="absolute top-0 h-full w-px ltr:right-0 rtl:left-0 bg-transparent group-hover:bg-n-brand transition-colors"
|
||||
:class="{ 'bg-n-brand': isResizing }"
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user