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:
Sivin Varghese
2026-01-29 04:06:04 +05:30
committed by GitHub
parent 0ca98bc84f
commit 2a69b37958
101 changed files with 927 additions and 265 deletions

View File

@@ -131,7 +131,7 @@ export default {
<div <div
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem" v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
id="app" id="app"
class="flex flex-col w-full h-screen min-h-0" class="flex flex-col w-full h-screen min-h-0 bg-n-background"
:dir="isRTL ? 'rtl' : 'ltr'" :dir="isRTL ? 'rtl' : 'ltr'"
> >
<UpdateBanner :latest-chatwoot-version="latestChatwootVersion" /> <UpdateBanner :latest-chatwoot-version="latestChatwootVersion" />

View File

@@ -107,21 +107,39 @@
--violet-11: 101 85 183; --violet-11: 101 85 183;
--violet-12: 47 38 95; --violet-12: 47 38 95;
--background-color: 253 253 253; --background-color: 247 247 247;
--text-blue: 8 109 224; --surface-1: 254 254 254;
--surface-2: 255 255 255;
--surface-active: 255 255 255;
--background-input-box: 0, 0, 0, 0.03;
--text-blue: 1 22 44;
--text-purple: 2 4 49;
--text-amber: 37 24 1;
--border-container: 236 236 236; --border-container: 236 236 236;
--border-strong: 235 235 235; --border-strong: 226 227 231;
--border-weak: 234 234 234; --border-weak: 234 234 234;
--border-blue-strong: 18 61 117;
--solid-1: 255 255 255; --solid-1: 255 255 255;
--solid-2: 255 255 255; --solid-2: 255 255 255;
--solid-3: 255 255 255; --solid-3: 255 255 255;
--solid-active: 255 255 255; --solid-active: 255 255 255;
--solid-amber: 252 232 193; --solid-amber: 255 228 181;
--solid-blue: 218 236 255; --solid-blue: 218 236 255;
--solid-blue-2: 251 253 255;
--solid-iris: 230 231 255; --solid-iris: 230 231 255;
--solid-purple: 230 231 255;
--solid-red: 254 200 201;
--solid-amber-button: 255 221 141;
--card-color: 255 255 255;
--overlay: 0, 0, 0, 0.12;
--overlay-avatar: 255, 255, 255, 0.67;
--button-color: 255 255 255;
--button-hover-color: 255, 255, 255, 0.2;
--label-background: 247 247 247;
--label-border: 0, 0, 0, 0.04;
--alpha-1: 67, 67, 67, 0.06; --alpha-1: 215, 215, 215, 0.22;
--alpha-2: 201, 202, 207, 0.15; --alpha-2: 196, 197, 198, 0.22;
--alpha-3: 255, 255, 255, 0.96; --alpha-3: 255, 255, 255, 0.96;
--black-alpha-1: 0, 0, 0, 0.12; --black-alpha-1: 0, 0, 0, 0.12;
--black-alpha-2: 0, 0, 0, 0.04; --black-alpha-2: 0, 0, 0, 0.04;
@@ -235,25 +253,43 @@
--violet-11: 169 153 236; --violet-11: 169 153 236;
--violet-12: 226 221 254; --violet-12: 226 221 254;
--background-color: 18 18 19; --background-color: 28 29 32;
--border-strong: 52 52 52; --surface-1: 20 21 23;
--border-weak: 38 38 42; --surface-2: 22 23 26;
--surface-active: 53 57 66;
--background-input-box: 255, 255, 255, 0.02;
--text-blue: 213 234 255;
--text-purple: 232 233 254;
--text-amber: 255 247 234;
--border-strong: 46 45 50;
--border-weak: 31 31 37;
--border-blue-strong: 201 226 255;
--solid-1: 23 23 26; --solid-1: 23 23 26;
--solid-2: 29 30 36; --solid-2: 29 30 36;
--solid-3: 44 45 54; --solid-3: 44 45 54;
--solid-active: 53 57 66; --solid-active: 53 57 66;
--solid-amber: 42 37 30; --solid-amber: 56 50 41;
--solid-blue: 16 49 91; --solid-blue: 15 57 102;
--solid-blue-2: 26 29 35;
--solid-iris: 38 42 101; --solid-iris: 38 42 101;
--text-blue: 126 182 255; --solid-purple: 51 51 107;
--solid-red: 90 33 34;
--solid-amber-button: 255 221 141;
--card-color: 28 30 34;
--overlay: 0, 0, 0, 0.4;
--overlay-avatar: 0, 0, 0, 0.05;
--button-color: 42 43 51;
--button-hover-color: 0, 0, 0, 0.15;
--label-background: 36 38 45;
--label-border: 255, 255, 255, 0.03;
--alpha-1: 36, 36, 36, 0.8; --alpha-1: 35, 36, 42, 0.8;
--alpha-2: 139, 147, 182, 0.15; --alpha-2: 147, 153, 176, 0.12;
--alpha-3: 36, 38, 45, 0.9; --alpha-3: 33, 34, 38, 0.95;
--black-alpha-1: 0, 0, 0, 0.3; --black-alpha-1: 0, 0, 0, 0.3;
--black-alpha-2: 0, 0, 0, 0.2; --black-alpha-2: 0, 0, 0, 0.2;
--border-blue: 39, 129, 246, 0.5; --border-blue: 39, 129, 246, 0.5;
--border-container: 236, 236, 236, 0; --border-container: 255, 255, 255, 0;
--white-alpha: 255, 255, 255, 0.1; --white-alpha: 255, 255, 255, 0.1;
} }
} }

View File

@@ -7,7 +7,7 @@
@apply bg-n-slate-3; @apply bg-n-slate-3;
&::after { &::after {
@apply text-n-blue-text; @apply text-n-blue-11;
} }
} }
} }

View File

@@ -119,7 +119,7 @@ onMounted(() => {
) )
" "
:items="filteredTags" :items="filteredTags"
class="[&>button]:!text-n-blue-text [&>div]:min-w-64" class="[&>button]:!text-n-blue-11 [&>div]:min-w-64"
@add="onClickAddTag" @add="onClickAddTag"
/> />
</div> </div>

View File

@@ -20,7 +20,7 @@ const handleButtonClick = () => {
</script> </script>
<template> <template>
<section class="flex flex-col w-full h-full overflow-hidden bg-n-background"> <section class="flex flex-col w-full h-full overflow-hidden bg-n-surface-1">
<header class="sticky top-0 z-10 px-6 lg:px-0"> <header class="sticky top-0 z-10 px-6 lg:px-0">
<div class="w-full max-w-[60rem] mx-auto"> <div class="w-full max-w-[60rem] mx-auto">
<div class="flex items-center justify-between w-full h-20 gap-2"> <div class="flex items-center justify-between w-full h-20 gap-2">

View File

@@ -306,7 +306,7 @@ defineExpose({ prepareCampaignDetails, isSubmitDisabled });
variant="faded" variant="faded"
color="slate" color="slate"
:label="t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.BUTTONS.CANCEL')" :label="t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.BUTTONS.CANCEL')"
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3" class="w-full bg-n-alpha-2 text-n-blue-11 hover:bg-n-alpha-3"
@click="handleCancel" @click="handleCancel"
/> />
<Button <Button

View File

@@ -174,7 +174,7 @@ const handleSubmit = async () => {
color="slate" color="slate"
type="button" type="button"
:label="t('CAMPAIGN.SMS.CREATE.FORM.BUTTONS.CANCEL')" :label="t('CAMPAIGN.SMS.CREATE.FORM.BUTTONS.CANCEL')"
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3" class="w-full bg-n-alpha-2 text-n-blue-11 hover:bg-n-alpha-3"
@click="handleCancel" @click="handleCancel"
/> />
<Button <Button

View File

@@ -251,7 +251,7 @@ watch(
color="slate" color="slate"
type="button" type="button"
:label="t('CAMPAIGN.WHATSAPP.CREATE.FORM.BUTTONS.CANCEL')" :label="t('CAMPAIGN.WHATSAPP.CREATE.FORM.BUTTONS.CANCEL')"
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3" class="w-full bg-n-alpha-2 text-n-blue-11 hover:bg-n-alpha-3"
@click="handleCancel" @click="handleCancel"
/> />
<Button <Button

View File

@@ -21,7 +21,7 @@ const updateCurrentPage = page => {
<template> <template>
<section <section
class="flex w-full h-full gap-4 overflow-hidden justify-evenly bg-n-background" class="flex w-full h-full gap-4 overflow-hidden justify-evenly bg-n-surface-1"
> >
<div class="flex flex-col w-full h-full transition-all duration-300"> <div class="flex flex-col w-full h-full transition-all duration-300">
<CompanyHeader <CompanyHeader

View File

@@ -73,7 +73,7 @@ const closeMobileSidebar = () => {
<template> <template>
<section <section
class="flex w-full h-full overflow-hidden justify-evenly bg-n-background" class="flex w-full h-full overflow-hidden justify-evenly bg-n-surface-1"
> >
<div <div
class="flex flex-col w-full h-full transition-all duration-300 ltr:2xl:ml-56 rtl:2xl:mr-56" class="flex flex-col w-full h-full transition-all duration-300 ltr:2xl:ml-56 rtl:2xl:mr-56"

View File

@@ -73,7 +73,7 @@ defineExpose({ dialogRef });
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
download="import-contacts-sample.csv" download="import-contacts-sample.csv"
class="text-n-blue-text" class="text-n-blue-11"
> >
{{ {{
t('CONTACTS_LAYOUT.HEADER.ACTIONS.IMPORT_CONTACT.DOWNLOAD_LABEL') t('CONTACTS_LAYOUT.HEADER.ACTIONS.IMPORT_CONTACT.DOWNLOAD_LABEL')

View File

@@ -78,7 +78,7 @@ const showPagination = computed(() => {
<template> <template>
<section <section
class="flex w-full h-full gap-4 overflow-hidden justify-evenly bg-n-background" class="flex w-full h-full gap-4 overflow-hidden justify-evenly bg-n-surface-1"
> >
<div class="flex flex-col w-full h-full transition-all duration-300"> <div class="flex flex-col w-full h-full transition-all duration-300">
<ContactListHeaderWrapper <ContactListHeaderWrapper

View File

@@ -130,7 +130,7 @@ const onMergeContacts = async () => {
variant="faded" variant="faded"
color="slate" color="slate"
:label="t('CONTACTS_LAYOUT.SIDEBAR.MERGE.BUTTONS.CANCEL')" :label="t('CONTACTS_LAYOUT.SIDEBAR.MERGE.BUTTONS.CANCEL')"
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3" class="w-full bg-n-alpha-2 text-n-blue-11 hover:bg-n-alpha-3"
@click="resetState" @click="resetState"
/> />
<Button <Button

View File

@@ -128,7 +128,7 @@ const handleInputUpdate = async () => {
'cursor-pointer text-n-slate-11 hover:text-n-slate-12 py-2 select-none font-medium': 'cursor-pointer text-n-slate-11 hover:text-n-slate-12 py-2 select-none font-medium':
!isEditingView, !isEditingView,
'text-n-slate-12 truncate': isEditingView && !isAttributeTypeLink, 'text-n-slate-12 truncate': isEditingView && !isAttributeTypeLink,
'truncate hover:text-n-brand text-n-blue-text': 'truncate hover:text-n-brand text-n-blue-11':
isEditingView && isAttributeTypeLink, isEditingView && isAttributeTypeLink,
}" }"
@click="toggleEditValue(!isEditingView)" @click="toggleEditValue(!isEditingView)"

View File

@@ -37,7 +37,7 @@ defineProps({
<div <div
class="flex flex-col items-center justify-end w-full h-full pb-20" class="flex flex-col items-center justify-end w-full h-full pb-20"
:class="{ :class="{
'absolute inset-x-0 bottom-0 bg-gradient-to-t from-n-background from-25% dark:from-n-background to-transparent': 'absolute inset-x-0 bottom-0 bg-gradient-to-t from-n-surface-1 from-25% to-transparent':
showBackdrop, showBackdrop,
}" }"
> >
@@ -48,14 +48,12 @@ defineProps({
}" }"
> >
<div class="flex flex-col items-center justify-center gap-3"> <div class="flex flex-col items-center justify-center gap-3">
<h2 <h2 class="text-3xl font-medium text-center text-n-slate-12">
class="text-3xl font-medium text-center text-n-slate-12 font-interDisplay"
>
{{ title }} {{ title }}
</h2> </h2>
<p <p
v-if="subtitle" v-if="subtitle"
class="max-w-xl text-base text-center text-n-slate-11 font-interDisplay tracking-[0.3px]" class="max-w-xl text-base text-center text-n-slate-11 tracking-[0.3px]"
> >
{{ subtitle }} {{ subtitle }}
</p> </p>

View File

@@ -126,7 +126,7 @@ const handleClick = id => {
<CardLayout> <CardLayout>
<div class="flex justify-between w-full gap-1"> <div class="flex justify-between w-full gap-1">
<span <span
class="text-base cursor-pointer hover:underline underline-offset-2 hover:text-n-blue-text text-n-slate-12 line-clamp-1" class="text-base cursor-pointer hover:underline underline-offset-2 hover:text-n-blue-11 text-n-slate-12 line-clamp-1"
@click="handleClick(id)" @click="handleClick(id)"
> >
{{ title }} {{ title }}

View File

@@ -83,7 +83,7 @@ const handleAction = ({ action, value }) => {
<div class="flex justify-between w-full gap-2"> <div class="flex justify-between w-full gap-2">
<div class="flex items-center justify-start w-full min-w-0 gap-2"> <div class="flex items-center justify-start w-full min-w-0 gap-2">
<span <span
class="text-base truncate cursor-pointer hover:underline underline-offset-2 hover:text-n-blue-text text-n-slate-12" class="text-base truncate cursor-pointer hover:underline underline-offset-2 hover:text-n-blue-11 text-n-slate-12"
@click="handleClick(slug)" @click="handleClick(slug)"
> >
{{ categoryTitleWithIcon }} {{ categoryTitleWithIcon }}

View File

@@ -58,7 +58,7 @@ const togglePortalSwitcher = () => {
</script> </script>
<template> <template>
<section class="flex flex-col w-full h-full overflow-hidden bg-n-background"> <section class="flex flex-col w-full h-full overflow-hidden bg-n-surface-1">
<header class="sticky top-0 z-10 px-6 pb-3 lg:px-0"> <header class="sticky top-0 z-10 px-6 pb-3 lg:px-0">
<div class="w-full max-w-[60rem] mx-auto lg:px-6"> <div class="w-full max-w-[60rem] mx-auto lg:px-6">
<div <div

View File

@@ -60,7 +60,7 @@ const handleAction = ({ action, value }) => {
</span> </span>
<span <span
v-if="isDefault" v-if="isDefault"
class="bg-n-alpha-2 h-6 inline-flex items-center justify-center rounded-md text-xs border-px border-transparent text-n-blue-text px-2 py-0.5" class="bg-n-alpha-2 h-6 inline-flex items-center justify-center rounded-md text-xs border-px border-transparent text-n-blue-11 px-2 py-0.5"
> >
{{ $t('HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DEFAULT') }} {{ $t('HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DEFAULT') }}
</span> </span>

View File

@@ -246,7 +246,7 @@ defineExpose({ state, isSubmitDisabled });
variant="faded" variant="faded"
color="slate" color="slate"
:label="t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_DIALOG.BUTTONS.CANCEL')" :label="t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_DIALOG.BUTTONS.CANCEL')"
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3" class="w-full bg-n-alpha-2 text-n-blue-11 hover:bg-n-alpha-3"
@click="handleCancel" @click="handleCancel"
/> />
<Button <Button

View File

@@ -100,7 +100,7 @@ const formattedMessage = computed(() => {
const notificationDetails = computed(() => { const notificationDetails = computed(() => {
const type = props.inboxItem?.notificationType?.toUpperCase() || ''; const type = props.inboxItem?.notificationType?.toUpperCase() || '';
const [icon = '', color = 'text-n-blue-text'] = const [icon = '', color = 'text-n-blue-11'] =
NOTIFICATION_TYPES_MAPPING[type] || []; NOTIFICATION_TYPES_MAPPING[type] || [];
return { text: type ? t(`INBOX.TYPES_NEXT.${type}`) : '', icon, color }; return { text: type ? t(`INBOX.TYPES_NEXT.${type}`) : '', icon, color };
}); });
@@ -181,11 +181,11 @@ onBeforeMount(contextMenuActions.close);
: 'i-lucide-alarm-clock-off' : 'i-lucide-alarm-clock-off'
" "
class="flex-shrink-0 size-4" class="flex-shrink-0 size-4"
:class="!isUnread ? 'text-n-slate-11' : 'text-n-blue-text'" :class="!isUnread ? 'text-n-slate-11' : 'text-n-blue-11'"
/> />
<span <span
class="text-xs font-medium truncate" class="text-xs font-medium truncate"
:class="!isUnread ? 'text-n-slate-11' : 'text-n-blue-text'" :class="!isUnread ? 'text-n-slate-11' : 'text-n-blue-11'"
> >
{{ snoozedText }} {{ snoozedText }}
</span> </span>

View File

@@ -103,11 +103,11 @@ const STYLE_CONFIG = {
solid: solid:
'bg-n-brand text-white hover:enabled:brightness-110 focus-visible:brightness-110 outline-transparent', 'bg-n-brand text-white hover:enabled:brightness-110 focus-visible:brightness-110 outline-transparent',
faded: faded:
'bg-n-brand/10 text-n-blue-text hover:enabled:bg-n-brand/20 focus-visible:bg-n-brand/20 outline-transparent', 'bg-n-brand/10 text-n-blue-11 hover:enabled:bg-n-brand/20 focus-visible:bg-n-brand/20 outline-transparent',
outline: 'text-n-blue-text outline-n-brand', outline: 'text-n-blue-11 outline-n-brand',
ghost: ghost:
'text-n-blue-text hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent', 'text-n-blue-11 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
link: 'text-n-blue-text hover:enabled:underline focus-visible:underline outline-transparent', link: 'text-n-blue-11 hover:enabled:underline focus-visible:underline outline-transparent',
}, },
ruby: { ruby: {
solid: solid:
@@ -133,7 +133,7 @@ const STYLE_CONFIG = {
}, },
slate: { slate: {
solid: solid:
'bg-n-solid-3 dark:hover:enabled:bg-n-solid-2 dark:focus-visible:bg-n-solid-2 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 text-n-slate-12 outline-n-container', 'bg-n-button-color dark:hover:enabled:bg-n-solid-2 dark:focus-visible:bg-n-solid-2 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 text-n-slate-12 outline-n-container',
faded: faded:
'bg-n-slate-9/10 text-n-slate-12 hover:enabled:bg-n-slate-9/20 focus-visible:bg-n-slate-9/20 outline-transparent', 'bg-n-slate-9/10 text-n-slate-12 hover:enabled:bg-n-slate-9/20 focus-visible:bg-n-slate-9/20 outline-transparent',
outline: outline:

View File

@@ -115,7 +115,7 @@ const handleCreateAssistant = () => {
</script> </script>
<template> <template>
<section class="flex flex-col w-full h-full overflow-hidden bg-n-background"> <section class="flex flex-col w-full h-full overflow-hidden bg-n-surface-1">
<header class="sticky top-0 z-10 px-6"> <header class="sticky top-0 z-10 px-6">
<div class="w-full max-w-[60rem] mx-auto"> <div class="w-full max-w-[60rem] mx-auto">
<div <div

View File

@@ -141,7 +141,7 @@ const onClickCancel = () => {
variant="faded" variant="faded"
color="slate" color="slate"
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.CANCEL')" :label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.CANCEL')"
class="w-full bg-n-alpha-2 !text-n-blue-text hover:bg-n-alpha-3" class="w-full bg-n-alpha-2 !text-n-blue-11 hover:bg-n-alpha-3"
@click="onClickCancel" @click="onClickCancel"
/> />
<Button <Button

View File

@@ -169,7 +169,7 @@ watch(
variant="faded" variant="faded"
color="slate" color="slate"
:label="t('CAPTAIN.FORM.CANCEL')" :label="t('CAPTAIN.FORM.CANCEL')"
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3" class="w-full bg-n-alpha-2 text-n-blue-11 hover:bg-n-alpha-3"
@click="handleCancel" @click="handleCancel"
/> />
<Button <Button

View File

@@ -254,7 +254,7 @@ const handleSubmit = async () => {
variant="faded" variant="faded"
color="slate" color="slate"
:label="t('CAPTAIN.FORM.CANCEL')" :label="t('CAPTAIN.FORM.CANCEL')"
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3" class="w-full bg-n-alpha-2 text-n-blue-11 hover:bg-n-alpha-3"
@click="handleCancel" @click="handleCancel"
/> />
<Button <Button

View File

@@ -220,7 +220,7 @@ const handleSubmit = async () => {
variant="faded" variant="faded"
color="slate" color="slate"
:label="t('CAPTAIN.FORM.CANCEL')" :label="t('CAPTAIN.FORM.CANCEL')"
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3" class="w-full bg-n-alpha-2 text-n-blue-11 hover:bg-n-alpha-3"
@click="handleCancel" @click="handleCancel"
/> />
<Button <Button

View File

@@ -100,7 +100,7 @@ const handleSubmit = async () => {
variant="faded" variant="faded"
color="slate" color="slate"
:label="t('CAPTAIN.FORM.CANCEL')" :label="t('CAPTAIN.FORM.CANCEL')"
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3" class="w-full bg-n-alpha-2 text-n-blue-11 hover:bg-n-alpha-3"
@click="handleCancel" @click="handleCancel"
/> />
<Button <Button

View File

@@ -116,7 +116,7 @@ watch(
variant="faded" variant="faded"
color="slate" color="slate"
:label="t('CAPTAIN.FORM.CANCEL')" :label="t('CAPTAIN.FORM.CANCEL')"
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3" class="w-full bg-n-alpha-2 text-n-blue-11 hover:bg-n-alpha-3"
@click="handleCancel" @click="handleCancel"
/> />
<Button <Button

View File

@@ -34,7 +34,7 @@ const handleImgClick = () => {
<template> <template>
<div <div
data-testid="changelog-card" data-testid="changelog-card"
class="flex flex-col justify-between p-3 w-full rounded-lg border shadow-sm transition-all duration-200 border-n-weak bg-n-background text-n-slate-12" class="flex flex-col justify-between p-3 w-full rounded-lg border shadow-sm transition-all duration-200 border-n-weak bg-n-card text-n-slate-12"
:class="{ :class="{
'animate-fade-out pointer-events-none': isDismissing, 'animate-fade-out pointer-events-none': isDismissing,
'hover:shadow': isActive, 'hover:shadow': isActive,

View File

@@ -77,12 +77,12 @@ const queryOperatorOptions = computed(() => {
{ {
label: t(`FILTER.QUERY_DROPDOWN_LABELS.AND`), label: t(`FILTER.QUERY_DROPDOWN_LABELS.AND`),
value: 'and', value: 'and',
icon: h('span', { class: 'i-lucide-ampersands !text-n-blue-text' }), icon: h('span', { class: 'i-lucide-ampersands !text-n-blue-11' }),
}, },
{ {
label: t(`FILTER.QUERY_DROPDOWN_LABELS.OR`), label: t(`FILTER.QUERY_DROPDOWN_LABELS.OR`),
value: 'or', value: 'or',
icon: h('span', { class: 'i-woot-logic-or !text-n-blue-text' }), icon: h('span', { class: 'i-woot-logic-or !text-n-blue-11' }),
}, },
]; ];
}); });

View File

@@ -82,7 +82,7 @@ export function useOperators() {
hasInput: !NO_INPUT_OPTS.includes(value), hasInput: !NO_INPUT_OPTS.includes(value),
inputOverride: OPS_INPUT_OVERRIDE[value] || null, inputOverride: OPS_INPUT_OVERRIDE[value] || null,
icon: h('span', { icon: h('span', {
class: `${filterOperatorIcon[value]} !text-n-blue-text`, class: `${filterOperatorIcon[value]} !text-n-blue-11`,
}), }),
}; };
return acc; return acc;

View File

@@ -163,7 +163,7 @@ const getInReplyToMessage = parentMessage => {
</script> </script>
<template> <template>
<ul class="px-4 bg-n-background"> <ul class="px-4 bg-n-surface-1">
<slot name="beforeAll" /> <slot name="beforeAll" />
<template v-for="(message, index) in allMessages" :key="message.id"> <template v-for="(message, index) in allMessages" :key="message.id">
<slot <slot

View File

@@ -22,7 +22,7 @@ defineProps({
/> />
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<Button :label="buttonText" slate class="!text-n-blue-text w-full" /> <Button :label="buttonText" slate class="!text-n-blue-11 w-full" />
</div> </div>
</div> </div>
</template> </template>

View File

@@ -18,12 +18,8 @@ defineProps({
/> />
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<Button label="Call us" slate class="!text-n-blue-text w-full" /> <Button label="Call us" slate class="!text-n-blue-11 w-full" />
<Button <Button label="Visit our website" slate class="!text-n-blue-11 w-full" />
label="Visit our website"
slate
class="!text-n-blue-text w-full"
/>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -71,7 +71,7 @@ const pageInfo = computed(() => {
<template> <template>
<div <div
class="flex justify-between h-12 w-full max-w-[calc(60rem-3px)] outline outline-n-container outline-1 -outline-offset-1 mx-auto bg-n-solid-2 rounded-xl py-2 ltr:pl-4 rtl:pr-4 ltr:pr-3 rtl:pl-3 items-center before:absolute before:inset-x-0 before:-top-4 before:bg-gradient-to-t before:from-n-background before:from-10% before:dark:from-0% before:to-transparent before:h-4 before:pointer-events-none" class="flex justify-between h-12 w-full max-w-[calc(60rem-3px)] outline outline-n-container outline-1 -outline-offset-1 mx-auto bg-n-solid-2 rounded-xl py-2 ltr:pl-4 rtl:pr-4 ltr:pr-3 rtl:pl-3 items-center before:absolute before:inset-x-0 before:-top-4 before:bg-gradient-to-t before:from-n-surface-1 before:from-10% before:dark:from-0% before:to-transparent before:h-4 before:pointer-events-none"
> >
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="min-w-0 text-sm font-normal line-clamp-1 text-n-slate-11"> <span class="min-w-0 text-sm font-normal line-clamp-1 text-n-slate-11">

View File

@@ -8,6 +8,7 @@ const props = defineProps({
type: String, type: String,
required: true, required: true,
}, },
// eslint-disable-next-line vue/no-unused-properties
active: { active: {
type: Boolean, type: Boolean,
default: false, default: false,
@@ -24,10 +25,7 @@ const reauthorizationRequired = computed(() => {
</script> </script>
<template> <template>
<span <span class="size-5 grid place-content-center rounded-full bg-n-alpha-2">
class="size-5 grid place-content-center rounded-full bg-n-alpha-2"
:class="{ 'bg-n-solid-blue': active }"
>
<ChannelIcon :inbox="inbox" class="size-3" /> <ChannelIcon :inbox="inbox" class="size-3" />
</span> </span>
<div class="flex-1 truncate min-w-0">{{ label }}</div> <div class="flex-1 truncate min-w-0">{{ label }}</div>

View File

@@ -39,7 +39,7 @@ const toggleSidebar = () => {
<div <div
v-if="!isConversationRoute" v-if="!isConversationRoute"
id="mobile-sidebar-launcher" id="mobile-sidebar-launcher"
class="fixed bottom-4 ltr:left-4 rtl:right-4 z-40 transition-transform duration-200 ease-in-out block md:hidden" class="fixed bottom-4 ltr:left-4 rtl:right-4 z-40 transition-transform duration-200 ease-out block md:hidden"
:class="[ :class="[
{ {
'ltr:translate-x-48 rtl:-translate-x-48': isMobileSidebarOpen, 'ltr:translate-x-48 rtl:-translate-x-48': isMobileSidebarOpen,

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import { h, ref, computed, onMounted } from 'vue'; import { h, ref, computed, onMounted } from 'vue';
import { provideSidebarContext } from './provider'; import { provideSidebarContext, useSidebarResize } from './provider';
import { useAccount } from 'dashboard/composables/useAccount'; import { useAccount } from 'dashboard/composables/useAccount';
import { useKbd } from 'dashboard/composables/utils/useKbd'; import { useKbd } from 'dashboard/composables/utils/useKbd';
import { useMapGetter } from 'dashboard/composables/store'; import { useMapGetter } from 'dashboard/composables/store';
@@ -8,6 +8,7 @@ import { useStore } from 'vuex';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts'; import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
import { vOnClickOutside } from '@vueuse/components'; import { vOnClickOutside } from '@vueuse/components';
import { useWindowSize, useEventListener } from '@vueuse/core';
import { emitter } from 'shared/helpers/mitt'; import { emitter } from 'shared/helpers/mitt';
import { BUS_EVENTS } from 'shared/constants/busEvents'; 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 SidebarGroup from './SidebarGroup.vue';
import SidebarProfileMenu from './SidebarProfileMenu.vue'; import SidebarProfileMenu from './SidebarProfileMenu.vue';
import SidebarChangelogCard from './SidebarChangelogCard.vue'; import SidebarChangelogCard from './SidebarChangelogCard.vue';
import SidebarChangelogButton from './SidebarChangelogButton.vue';
import ChannelLeaf from './ChannelLeaf.vue'; import ChannelLeaf from './ChannelLeaf.vue';
import ChannelIcon from 'next/icon/ChannelIcon.vue';
import SidebarAccountSwitcher from './SidebarAccountSwitcher.vue'; import SidebarAccountSwitcher from './SidebarAccountSwitcher.vue';
import Logo from 'next/icon/Logo.vue'; import Logo from 'next/icon/Logo.vue';
import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue'; import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue';
@@ -42,6 +45,10 @@ const { t } = useI18n();
const isACustomBrandedInstance = useMapGetter( const isACustomBrandedInstance = useMapGetter(
'globalConfig/isACustomBrandedInstance' 'globalConfig/isACustomBrandedInstance'
); );
const isRTL = useMapGetter('accounts/isRTL');
const { width: windowWidth } = useWindowSize();
const isMobile = computed(() => windowWidth.value < 768);
const toggleShortcutModalFn = show => { const toggleShortcutModalFn = show => {
if (show) { if (show) {
@@ -58,11 +65,85 @@ const expandedItem = ref(null);
const setExpandedItem = name => { const setExpandedItem = name => {
expandedItem.value = expandedItem.value === name ? null : 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({ provideSidebarContext({
expandedItem, expandedItem,
setExpandedItem, 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 inboxes = useMapGetter('inboxes/getInboxes');
const labels = useMapGetter('labels/getLabelsOnSidebar'); const labels = useMapGetter('labels/getLabelsOnSidebar');
const teams = useMapGetter('teams/getMyTeams'); const teams = useMapGetter('teams/getMyTeams');
@@ -192,6 +273,7 @@ const menuItems = computed(() => {
children: sortedInboxes.value.map(inbox => ({ children: sortedInboxes.value.map(inbox => ({
name: `${inbox.name}-${inbox.id}`, name: `${inbox.name}-${inbox.id}`,
label: inbox.name, label: inbox.name,
icon: h(ChannelIcon, { inbox, class: 'size-[12px]' }),
to: accountScopedRoute('inbox_dashboard', { inbox_id: inbox.id }), to: accountScopedRoute('inbox_dashboard', { inbox_id: inbox.id }),
component: leafProps => component: leafProps =>
h(ChannelLeaf, { h(ChannelLeaf, {
@@ -210,7 +292,7 @@ const menuItems = computed(() => {
name: `${label.title}-${label.id}`, name: `${label.title}-${label.id}`,
label: label.title, label: label.title,
icon: h('span', { 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 }, style: { backgroundColor: label.color },
}), }),
to: accountScopedRoute('label_conversations', { to: accountScopedRoute('label_conversations', {
@@ -338,7 +420,7 @@ const menuItems = computed(() => {
name: `${label.title}-${label.id}`, name: `${label.title}-${label.id}`,
label: label.title, label: label.title,
icon: h('span', { 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 }, style: { backgroundColor: label.color },
}), }),
to: accountScopedRoute( to: accountScopedRoute(
@@ -604,16 +686,35 @@ const menuItems = computed(() => {
closeMobileSidebar, closeMobileSidebar,
{ ignore: ['#mobile-sidebar-launcher'] }, { 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="[ :class="[
{ {
'shadow-lg md:shadow-none': isMobileSidebarOpen, 'shadow-lg md:shadow-none': isMobileSidebarOpen,
'ltr:-translate-x-full rtl:translate-x-full': !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"> <section
<div class="flex gap-2 items-center px-2 min-w-0"> 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"> <div class="grid flex-shrink-0 place-content-center size-6">
<Logo class="size-4" /> <Logo class="size-4" />
</div> </div>
@@ -622,14 +723,19 @@ const menuItems = computed(() => {
class="flex-grow -mx-1 min-w-0" class="flex-grow -mx-1 min-w-0"
@show-create-account-modal="emit('showCreateAccountModal')" @show-create-account-modal="emit('showCreateAccountModal')"
/> />
</template>
</div> </div>
<div class="flex gap-2 px-2"> <div
<RouterLink class="flex gap-2"
:to="{ name: 'search' }" :class="isEffectivelyCollapsed ? 'flex-col items-center' : 'px-2'"
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"
> >
<span class="flex-shrink-0 i-lucide-search size-4 text-n-slate-11" /> <RouterLink
<span class="flex-grow text-left"> 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-button-color transition-all duration-100 ease-out"
>
<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') }} {{ t('COMBOBOX.SEARCH_PLACEHOLDER') }}
</span> </span>
<span <span
@@ -638,21 +744,41 @@ const menuItems = computed(() => {
{{ searchShortcut }} {{ searchShortcut }}
</span> </span>
</RouterLink> </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"> <ComposeConversation align-position="right" @close="onComposeClose">
<template #trigger="{ toggle }"> <template #trigger="{ toggle, isOpen }">
<Button <Button
icon="i-lucide-pen-line" icon="i-lucide-pen-line"
color="slate" color="slate"
size="sm" 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)" @click="onComposeOpen(toggle)"
/> />
</template> </template>
</ComposeConversation> </ComposeConversation>
</div> </div>
</section> </section>
<nav class="grid overflow-y-scroll flex-grow gap-2 px-2 pb-5 no-scrollbar"> <nav
<ul class="flex flex-col gap-1.5 m-0 list-none"> 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 <SidebarGroup
v-for="item in menuItems" v-for="item in menuItems"
:key="item.name" :key="item.name"
@@ -664,18 +790,43 @@ const menuItems = computed(() => {
class="flex relative flex-col flex-shrink-0 gap-1 justify-between items-center" class="flex relative flex-col flex-shrink-0 gap-1 justify-between items-center"
> >
<div <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 <SidebarChangelogCard
v-if="isOnChatwootCloud && !isACustomBrandedInstance" v-if="
isOnChatwootCloud &&
!isACustomBrandedInstance &&
!isEffectivelyCollapsed
"
/>
<SidebarChangelogButton
v-if="
isOnChatwootCloud &&
!isACustomBrandedInstance &&
isEffectivelyCollapsed
"
/> />
<div <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 <SidebarProfileMenu
:is-collapsed="isEffectivelyCollapsed"
@open-key-shortcut-modal="emit('openKeyShortcutModal')" @open-key-shortcut-modal="emit('openKeyShortcutModal')"
/> />
</div> </div>
</section> </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> </aside>
</template> </template>

View File

@@ -5,6 +5,7 @@ import { useMapGetter } from 'dashboard/composables/store';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import ButtonNext from 'next/button/Button.vue'; import ButtonNext from 'next/button/Button.vue';
import Icon from 'next/icon/Icon.vue'; import Icon from 'next/icon/Icon.vue';
import Logo from 'next/icon/Logo.vue';
import { import {
DropdownContainer, DropdownContainer,
@@ -13,6 +14,13 @@ import {
DropdownItem, DropdownItem,
} from 'next/dropdown-menu/base'; } from 'next/dropdown-menu/base';
defineProps({
isCollapsed: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['showCreateAccountModal']); const emit = defineEmits(['showCreateAccountModal']);
const { t } = useI18n(); const { t } = useI18n();
@@ -45,7 +53,19 @@ const emitNewAccount = () => {
<template> <template>
<DropdownContainer> <DropdownContainer>
<template #trigger="{ toggle, isOpen }"> <template #trigger="{ toggle, isOpen }">
<!-- Collapsed view: Logo trigger -->
<button <button
v-if="isCollapsed"
class="grid flex-shrink-0 place-content-center p-2 rounded-lg cursor-pointer hover:bg-n-alpha-1"
:class="{ 'bg-n-alpha-1': isOpen }"
:title="currentAccount.name"
@click="toggle"
>
<Logo class="size-7" />
</button>
<!-- Expanded view: Account name trigger -->
<button
v-else
id="sidebar-account-switcher" id="sidebar-account-switcher"
:data-account-id="accountId" :data-account-id="accountId"
aria-haspopup="listbox" aria-haspopup="listbox"
@@ -73,7 +93,10 @@ const emitNewAccount = () => {
/> />
</button> </button>
</template> </template>
<DropdownBody v-if="showAccountSwitcher" class="min-w-80 z-50"> <DropdownBody
v-if="showAccountSwitcher || isCollapsed"
class="min-w-80 z-50"
>
<DropdownSection :title="t('SIDEBAR_ITEMS.SWITCH_ACCOUNT')"> <DropdownSection :title="t('SIDEBAR_ITEMS.SWITCH_ACCOUNT')">
<DropdownItem <DropdownItem
v-for="account in sortedCurrentUserAccounts" v-for="account in sortedCurrentUserAccounts"

View File

@@ -0,0 +1,46 @@
<script setup>
import { computed, useTemplateRef } from 'vue';
import { useToggle } from '@vueuse/core';
import { vOnClickOutside } from '@vueuse/components';
import Button from 'dashboard/components-next/button/Button.vue';
import SidebarChangelogCard from './SidebarChangelogCard.vue';
const [isOpen, toggleOpen] = useToggle(false);
const changelogCard = useTemplateRef('changelogCard');
const isLoading = computed(() => changelogCard.value?.isLoading || false);
const hasArticles = computed(
() => changelogCard.value?.unDismissedPosts?.length > 0
);
const shouldShowButton = computed(() => !isLoading.value && hasArticles.value);
const closePopover = () => {
if (isOpen.value) {
toggleOpen(false);
}
};
</script>
<template>
<div v-on-click-outside="closePopover" class="relative mb-2">
<Button
v-if="shouldShowButton"
icon="i-lucide-sparkles"
ghost
slate
:class="{ '!bg-n-alpha-2 dark:!bg-n-slate-9/30': isOpen }"
@click="toggleOpen()"
/>
<!-- Always render card so it can fetch data, control visibility with v-show -->
<div
v-show="isOpen && hasArticles"
class="absolute ltr:left-full rtl:right-full bottom-0 ltr:ml-4 rtl:mr-4 z-40 bg-transparent w-52"
>
<SidebarChangelogCard
ref="changelogCard"
class="[&>div]:!pb-0 [&>div]:!px-0 rounded-lg"
/>
</div>
</div>
</template>

View File

@@ -4,6 +4,10 @@ import GroupedStackedChangelogCard from 'dashboard/components-next/changelog-car
import { useUISettings } from 'dashboard/composables/useUISettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
import changelogAPI from 'dashboard/api/changelog'; import changelogAPI from 'dashboard/api/changelog';
defineOptions({
inheritAttrs: false,
});
const MAX_DISMISSED_SLUGS = 5; const MAX_DISMISSED_SLUGS = 5;
const { uiSettings, updateUISettings } = useUISettings(); const { uiSettings, updateUISettings } = useUISettings();
@@ -90,6 +94,11 @@ const handleImgClick = ({ index }) => {
handleReadMore(); handleReadMore();
}; };
defineExpose({
isLoading,
unDismissedPosts,
});
onMounted(() => { onMounted(() => {
fetchChangelog(); fetchChangelog();
}); });
@@ -98,6 +107,7 @@ onMounted(() => {
<template> <template>
<GroupedStackedChangelogCard <GroupedStackedChangelogCard
v-if="unDismissedPosts.length > 0" v-if="unDismissedPosts.length > 0"
v-bind="$attrs"
:posts="unDismissedPosts" :posts="unDismissedPosts"
:current-index="currentIndex" :current-index="currentIndex"
:dismissing-slugs="dismissingCards" :dismissing-slugs="dismissingCards"

View File

@@ -0,0 +1,198 @@
<script setup>
import { computed, ref, onMounted, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import { useSidebarContext } from './provider';
import { useMapGetter } from 'dashboard/composables/store';
import Icon from 'next/icon/Icon.vue';
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
const props = defineProps({
label: { type: String, required: true },
children: { type: Array, default: () => [] },
activeChild: { type: Object, default: undefined },
triggerRect: { type: Object, default: () => ({ top: 0, left: 0 }) },
});
const emit = defineEmits(['close', 'mouseenter', 'mouseleave']);
const router = useRouter();
const { isAllowed, sidebarWidth } = useSidebarContext();
const expandedSubGroup = ref(null);
const popoverRef = ref(null);
const topPosition = ref(0);
const isRTL = useMapGetter('accounts/isRTL');
const skipTransition = ref(true);
const toggleSubGroup = name => {
expandedSubGroup.value = expandedSubGroup.value === name ? null : name;
};
const navigateAndClose = to => {
router.push(to);
emit('close');
};
const isActive = child => props.activeChild?.name === child.name;
const getAccessibleSubChildren = children =>
children.filter(c => isAllowed(c.to));
const renderIcon = icon => ({
component: typeof icon === 'object' ? icon : Icon,
props: typeof icon === 'string' ? { icon } : null,
});
const transition = computed(() =>
skipTransition.value
? {}
: {
enterActiveClass: 'transition-all duration-200 ease-out',
enterFromClass: 'opacity-0 -translate-y-2 max-h-0',
enterToClass: 'opacity-100 translate-y-0 max-h-96',
leaveActiveClass: 'transition-all duration-150 ease-in',
leaveFromClass: 'opacity-100 translate-y-0 max-h-96',
leaveToClass: 'opacity-0 -translate-y-2 max-h-0',
}
);
const accessibleChildren = computed(() => {
return props.children.filter(child => {
if (child.children) {
return child.children.some(subChild => isAllowed(subChild.to));
}
return child.to && isAllowed(child.to);
});
});
onMounted(async () => {
await nextTick();
// Auto-expand subgroup if active child is inside it
if (props.activeChild) {
const parentGroup = props.children.find(child =>
child.children?.some(subChild => subChild.name === props.activeChild.name)
);
if (parentGroup) {
expandedSubGroup.value = parentGroup.name;
// Wait for the subgroup expansion to render before measuring height
await nextTick();
}
}
if (!props.triggerRect) return;
const viewportHeight = window.innerHeight;
const popoverHeight = popoverRef.value?.offsetHeight || 300;
const { top: triggerTop } = props.triggerRect;
// Adjust position if popover would overflow viewport
topPosition.value =
triggerTop + popoverHeight > viewportHeight - 20
? Math.max(20, viewportHeight - popoverHeight - 20)
: triggerTop;
await nextTick();
skipTransition.value = false;
});
</script>
<template>
<TeleportWithDirection>
<div
ref="popoverRef"
class="fixed z-[100] min-w-[200px] max-w-[280px]"
:style="{
[isRTL ? 'right' : 'left']: `${sidebarWidth + 8}px`,
top: `${topPosition}px`,
}"
@mouseenter="emit('mouseenter')"
@mouseleave="emit('mouseleave')"
>
<div
class="bg-n-alpha-3 backdrop-blur-[100px] outline outline-1 -outline-offset-1 w-56 outline-n-weak rounded-xl shadow-lg py-2 px-2"
>
<div
class="px-2 py-1.5 text-xs font-medium text-n-slate-11 uppercase tracking-wider border-b border-n-weak mb-1"
>
{{ label }}
</div>
<ul
class="m-0 p-0 list-none max-h-[400px] overflow-y-auto no-scrollbar"
>
<template v-for="child in accessibleChildren" :key="child.name">
<!-- SubGroup with children -->
<li v-if="child.children" class="py-0.5">
<button
class="flex items-center gap-2 px-2 py-1.5 w-full rounded-lg text-n-slate-11 hover:bg-n-alpha-2 transition-colors duration-150 ease-out text-left rtl:text-right"
@click="toggleSubGroup(child.name)"
>
<Icon
v-if="child.icon"
:icon="child.icon"
class="size-4 flex-shrink-0"
/>
<span class="flex-1 truncate text-sm">{{ child.label }}</span>
<span
class="size-3 transition-transform i-lucide-chevron-down"
:class="{
'rotate-180': expandedSubGroup === child.name,
}"
/>
</button>
<Transition v-bind="transition">
<ul
v-if="expandedSubGroup === child.name"
class="m-0 p-0 list-none ltr:pl-4 rtl:pr-4 mt-1 overflow-hidden"
>
<li
v-for="subChild in getAccessibleSubChildren(child.children)"
:key="subChild.name"
class="py-0.5"
>
<button
class="flex items-center gap-2 px-2 py-1.5 w-full rounded-lg text-sm text-left rtl:text-right transition-colors duration-150 ease-out"
:class="{
'text-n-slate-12 bg-n-alpha-2': isActive(subChild),
'text-n-slate-11 hover:bg-n-alpha-2':
!isActive(subChild),
}"
@click="navigateAndClose(subChild.to)"
>
<component
:is="renderIcon(subChild.icon).component"
v-if="subChild.icon"
v-bind="renderIcon(subChild.icon).props"
class="size-4 flex-shrink-0"
/>
<span class="flex-1 truncate">{{ subChild.label }}</span>
</button>
</li>
</ul>
</Transition>
</li>
<!-- Direct child item -->
<li v-else class="py-0.5">
<button
class="flex items-center gap-2 px-2 py-1.5 w-full rounded-lg text-sm text-left rtl:text-right transition-colors duration-150 ease-out"
:class="{
'text-n-slate-12 bg-n-alpha-2': isActive(child),
'text-n-slate-11 hover:bg-n-alpha-2': !isActive(child),
}"
@click="navigateAndClose(child.to)"
>
<component
:is="renderIcon(child.icon).component"
v-if="child.icon"
v-bind="renderIcon(child.icon).props"
class="size-4 flex-shrink-0"
/>
<span class="flex-1 truncate">{{ child.label }}</span>
</button>
</li>
</template>
</ul>
</div>
</div>
</TeleportWithDirection>
</template>

View File

@@ -1,12 +1,14 @@
<script setup> <script setup>
import { computed, onMounted, watch, nextTick } from 'vue'; import { computed, onMounted, onUnmounted, watch, nextTick, ref } from 'vue';
import { useSidebarContext } from './provider'; import { useSidebarContext, usePopoverState } from './provider';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import Policy from 'dashboard/components/policy.vue'; import Policy from 'dashboard/components/policy.vue';
import Icon from 'next/icon/Icon.vue';
import SidebarGroupHeader from './SidebarGroupHeader.vue'; import SidebarGroupHeader from './SidebarGroupHeader.vue';
import SidebarGroupLeaf from './SidebarGroupLeaf.vue'; import SidebarGroupLeaf from './SidebarGroupLeaf.vue';
import SidebarSubGroup from './SidebarSubGroup.vue'; import SidebarSubGroup from './SidebarSubGroup.vue';
import SidebarGroupEmptyLeaf from './SidebarGroupEmptyLeaf.vue'; import SidebarGroupEmptyLeaf from './SidebarGroupEmptyLeaf.vue';
import SidebarCollapsedPopover from './SidebarCollapsedPopover.vue';
const props = defineProps({ const props = defineProps({
name: { type: String, required: true }, name: { type: String, required: true },
@@ -25,8 +27,18 @@ const {
resolvePermissions, resolvePermissions,
resolveFeatureFlag, resolveFeatureFlag,
isAllowed, isAllowed,
isCollapsed,
isResizing,
} = useSidebarContext(); } = useSidebarContext();
const {
activePopover,
setActivePopover,
closeActivePopover,
scheduleClose,
cancelClose,
} = usePopoverState();
const navigableChildren = computed(() => { const navigableChildren = computed(() => {
return props.children?.flatMap(child => child.children || child) || []; return props.children?.flatMap(child => child.children || child) || [];
}); });
@@ -39,6 +51,54 @@ const hasChildren = computed(
() => Array.isArray(props.children) && props.children.length > 0 () => Array.isArray(props.children) && props.children.length > 0
); );
// Use shared popover state - only one popover can be open at a time
const isPopoverOpen = computed(() => activePopover.value === props.name);
const triggerRef = ref(null);
const triggerRect = ref({ top: 0, left: 0, bottom: 0, right: 0 });
const openPopover = () => {
if (triggerRef.value) {
const rect = triggerRef.value.getBoundingClientRect();
triggerRect.value = {
top: rect.top,
left: rect.left,
bottom: rect.bottom,
right: rect.right,
};
}
setActivePopover(props.name);
};
const closePopover = () => {
if (activePopover.value === props.name) {
closeActivePopover();
}
};
const handleMouseEnter = () => {
if (!hasChildren.value || isResizing.value) return;
cancelClose();
openPopover();
};
const handleMouseLeave = () => {
if (!hasChildren.value) return;
scheduleClose(200);
};
const handlePopoverMouseEnter = () => {
cancelClose();
};
const handlePopoverMouseLeave = () => {
scheduleClose(100);
};
// Close popover when mouse leaves the window
const handleWindowBlur = () => {
closeActivePopover();
};
const accessibleItems = computed(() => { const accessibleItems = computed(() => {
if (!hasChildren.value) return []; if (!hasChildren.value) return [];
return props.children.filter(child => { return props.children.filter(child => {
@@ -107,6 +167,13 @@ const hasActiveChild = computed(() => {
return activeChild.value !== undefined; return activeChild.value !== undefined;
}); });
const handleCollapsedClick = () => {
if (hasChildren.value && hasAccessibleChildren.value) {
const firstItem = accessibleItems.value[0];
router.push(firstItem.to);
}
};
const toggleTrigger = () => { const toggleTrigger = () => {
if ( if (
hasAccessibleChildren.value && hasAccessibleChildren.value &&
@@ -125,6 +192,13 @@ onMounted(async () => {
if (hasActiveChild.value) { if (hasActiveChild.value) {
setExpandedItem(props.name); setExpandedItem(props.name);
} }
window.addEventListener('blur', handleWindowBlur);
document.addEventListener('mouseleave', handleWindowBlur);
});
onUnmounted(() => {
window.removeEventListener('blur', handleWindowBlur);
document.removeEventListener('mouseleave', handleWindowBlur);
}); });
watch( watch(
@@ -145,8 +219,44 @@ watch(
:permissions="resolvePermissions(to)" :permissions="resolvePermissions(to)"
:feature-flag="resolveFeatureFlag(to)" :feature-flag="resolveFeatureFlag(to)"
as="li" as="li"
class="grid gap-1 text-sm cursor-pointer select-none" class="grid gap-1 text-sm cursor-pointer select-none min-w-0"
> >
<!-- Collapsed State -->
<template v-if="isCollapsed">
<div
class="relative"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<component
:is="to && !hasChildren ? 'router-link' : 'button'"
ref="triggerRef"
:to="to && !hasChildren ? to : undefined"
type="button"
class="flex items-center justify-center size-10 rounded-lg"
:class="{
'text-n-slate-12 bg-n-alpha-2': isActive || hasActiveChild,
'text-n-slate-11 hover:bg-n-alpha-2': !isActive && !hasActiveChild,
}"
:title="label"
@click="hasChildren ? handleCollapsedClick() : undefined"
>
<Icon v-if="icon" :icon="icon" class="size-4" />
</component>
<SidebarCollapsedPopover
v-if="hasChildren && isPopoverOpen"
:label="label"
:children="children"
:active-child="activeChild"
:trigger-rect="triggerRect"
@close="closePopover"
@mouseenter="handlePopoverMouseEnter"
@mouseleave="handlePopoverMouseLeave"
/>
</div>
</template>
<!-- Expanded State -->
<template v-else>
<SidebarGroupHeader <SidebarGroupHeader
:icon :icon
:name :name
@@ -162,7 +272,7 @@ watch(
<ul <ul
v-if="hasChildren" v-if="hasChildren"
v-show="isExpanded || hasActiveChild" v-show="isExpanded || hasActiveChild"
class="grid m-0 list-none sidebar-group-children" class="grid m-0 list-none sidebar-group-children min-w-0"
> >
<template v-for="child in children" :key="child.name"> <template v-for="child in children" :key="child.name">
<SidebarSubGroup <SidebarSubGroup
@@ -184,6 +294,7 @@ watch(
<ul v-else-if="isExpandable && isExpanded"> <ul v-else-if="isExpandable && isExpanded">
<SidebarGroupEmptyLeaf /> <SidebarGroupEmptyLeaf />
</ul> </ul>
</template>
</Policy> </Policy>
</template> </template>

View File

@@ -26,13 +26,13 @@ const count = computed(() =>
<template> <template>
<component <component
:is="to ? 'router-link' : 'div'" :is="to ? 'router-link' : 'div'"
class="flex items-center gap-2 px-2 py-1.5 rounded-lg h-8 min-w-0" class="flex items-center gap-2 px-1.5 py-1 rounded-lg h-8 min-w-0"
role="button" role="button"
draggable="false" draggable="false"
:to="to" :to="to"
:title="label" :title="label"
:class="{ :class="{
'text-n-blue-text bg-n-alpha-2 font-medium': isActive && !hasActiveChild, 'text-n-slate-12 bg-n-alpha-2 font-medium': isActive && !hasActiveChild,
'text-n-slate-12 font-medium': hasActiveChild, 'text-n-slate-12 font-medium': hasActiveChild,
'text-n-slate-11 hover:bg-n-alpha-2': !isActive && !hasActiveChild, 'text-n-slate-11 hover:bg-n-alpha-2': !isActive && !hasActiveChild,
}" }"
@@ -45,15 +45,21 @@ const count = computed(() =>
class="size-2 -top-px ltr:-right-px rtl:-left-px bg-n-brand absolute rounded-full border border-n-solid-2" class="size-2 -top-px ltr:-right-px rtl:-left-px bg-n-brand absolute rounded-full border border-n-solid-2"
/> />
</div> </div>
<div class="flex items-center gap-1.5 flex-grow min-w-0"> <div class="flex items-center gap-1.5 flex-grow min-w-0 flex-1">
<span class="text-sm font-medium leading-5 truncate"> <span
class="truncate"
:class="{
'text-body-main': !isActive,
'font-medium text-sm': isActive || hasActiveChild,
}"
>
{{ label }} {{ label }}
</span> </span>
<span <span
v-if="dynamicCount && !expandable" v-if="dynamicCount && !expandable"
class="rounded-md capitalize text-xs leading-5 font-medium text-center outline outline-1 px-1 flex-shrink-0" class="rounded-md capitalize text-xs leading-5 font-medium text-center outline outline-1 px-1 flex-shrink-0"
:class="{ :class="{
'text-n-blue-text outline-n-slate-6': isActive, 'text-n-slate-12 outline-n-slate-6': isActive,
'text-n-slate-11 outline-n-strong': !isActive, 'text-n-slate-11 outline-n-strong': !isActive,
}" }"
> >

View File

@@ -25,15 +25,15 @@ const shouldRenderComponent = computed(() => {
:permissions="resolvePermissions(to)" :permissions="resolvePermissions(to)"
:feature-flag="resolveFeatureFlag(to)" :feature-flag="resolveFeatureFlag(to)"
as="li" as="li"
class="py-0.5 ltr:pl-3 rtl:pr-3 rtl:mr-3 ltr:ml-3 relative text-n-slate-11 child-item before:bg-n-slate-4 after:bg-transparent after:border-n-slate-4 before:left-0 rtl:before:right-0" class="py-0.5 ltr:pl-2 rtl:pr-2 rtl:mr-3 ltr:ml-3 relative text-n-slate-11 child-item before:bg-n-slate-4 after:bg-transparent after:border-n-slate-4 before:left-0 rtl:before:right-0 min-w-0"
> >
<component <component
:is="to ? 'router-link' : 'div'" :is="to ? 'router-link' : 'div'"
:to="to" :to="to"
:title="label" :title="label"
class="flex h-8 items-center gap-2 px-2 py-1 rounded-lg max-w-[9.438rem] hover:bg-gradient-to-r from-transparent via-n-slate-3/70 to-n-slate-3/70 group" class="flex h-8 items-center gap-2 px-2 py-1 rounded-lg hover:bg-gradient-to-r from-transparent via-n-slate-3/70 to-n-slate-3/70 group min-w-0"
:class="{ :class="{
'text-n-blue-text bg-n-alpha-2 active': active, 'text-n-slate-12 bg-n-alpha-2 active': active,
}" }"
> >
<component <component

View File

@@ -15,6 +15,10 @@ import {
} from 'next/dropdown-menu/base'; } from 'next/dropdown-menu/base';
import CustomBrandPolicyWrapper from '../../components/CustomBrandPolicyWrapper.vue'; import CustomBrandPolicyWrapper from '../../components/CustomBrandPolicyWrapper.vue';
defineProps({
isCollapsed: { type: Boolean, default: false },
});
const emit = defineEmits(['close', 'openKeyShortcutModal']); const emit = defineEmits(['close', 'openKeyShortcutModal']);
defineOptions({ defineOptions({
@@ -120,11 +124,19 @@ const allowedMenuItems = computed(() => {
</script> </script>
<template> <template>
<DropdownContainer class="relative w-full min-w-0" @close="emit('close')"> <DropdownContainer
class="relative min-w-0"
:class="isCollapsed ? 'w-auto' : 'w-full'"
@close="emit('close')"
>
<template #trigger="{ toggle, isOpen }"> <template #trigger="{ toggle, isOpen }">
<button <button
class="flex gap-2 items-center p-1 w-full text-left rounded-lg cursor-pointer hover:bg-n-alpha-1" class="flex gap-2 items-center p-1 text-left rounded-lg cursor-pointer hover:bg-n-alpha-1"
:class="{ 'bg-n-alpha-1': isOpen }" :class="[
{ 'bg-n-alpha-1': isOpen },
isCollapsed ? 'justify-center' : 'w-full',
]"
:title="isCollapsed ? currentUser.available_name : undefined"
@click="toggle" @click="toggle"
> >
<Avatar <Avatar
@@ -135,7 +147,7 @@ const allowedMenuItems = computed(() => {
class="flex-shrink-0" class="flex-shrink-0"
rounded-full rounded-full
/> />
<div class="min-w-0"> <div v-if="!isCollapsed" class="min-w-0">
<div class="text-sm font-medium leading-4 truncate text-n-slate-12"> <div class="text-sm font-medium leading-4 truncate text-n-slate-12">
{{ currentUser.available_name }} {{ currentUser.available_name }}
</div> </div>

View File

@@ -48,11 +48,15 @@ useEventListener(scrollableContainer, 'scroll', () => {
:icon :icon
class="my-1" class="my-1"
/> />
<ul v-if="children.length" class="m-0 list-none reset-base relative group"> <ul
v-if="children.length"
class="m-0 list-none reset-base relative group min-w-0"
>
<!-- Each element has h-8, which is 32px, we will show 7 items with one hidden at the end, <!-- Each element has h-8, which is 32px, we will show 7 items with one hidden at the end,
which is 14rem. Then we add 16px so that we have some text visible from the next item --> which is 14rem. Then we add 16px so that we have some text visible from the next item -->
<div <div
ref="scrollableContainer" ref="scrollableContainer"
class="min-w-0"
:class="{ :class="{
'max-h-[calc(14rem+16px)] overflow-y-scroll no-scrollbar': isScrollable, 'max-h-[calc(14rem+16px)] overflow-y-scroll no-scrollbar': isScrollable,
}" }"
@@ -68,7 +72,7 @@ useEventListener(scrollableContainer, 'scroll', () => {
<div <div
v-if="isScrollable && isExpanded" v-if="isScrollable && isExpanded"
v-show="!scrollEnd" v-show="!scrollEnd"
class="absolute bg-gradient-to-t from-n-solid-2 w-full h-12 to-transparent -bottom-1 pointer-events-none flex items-end justify-end px-2 animate-fade-in-up" class="absolute bg-gradient-to-t from-n-background w-full h-12 to-transparent -bottom-1 pointer-events-none flex items-end justify-end px-2 animate-fade-in-up"
> >
<svg <svg
width="16" width="16"

View File

@@ -1,9 +1,87 @@
import { inject, provide } from 'vue'; import { inject, provide, ref, computed } from 'vue';
import { usePolicy } from 'dashboard/composables/usePolicy'; import { usePolicy } from 'dashboard/composables/usePolicy';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useUISettings } from 'dashboard/composables/useUISettings';
const SidebarControl = Symbol('SidebarControl'); const SidebarControl = Symbol('SidebarControl');
const DEFAULT_WIDTH = 200;
const MIN_WIDTH = 56;
const COLLAPSED_THRESHOLD = 160;
const MAX_WIDTH = 320;
// Shared state for active popover (only one can be open at a time)
const activePopover = ref(null);
let globalCloseTimeout = null;
export function useSidebarResize() {
const { uiSettings, updateUISettings } = useUISettings();
const sidebarWidth = ref(uiSettings.value.sidebar_width || DEFAULT_WIDTH);
const isCollapsed = computed(() => sidebarWidth.value < COLLAPSED_THRESHOLD);
const setSidebarWidth = width => {
sidebarWidth.value = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, width));
};
const saveWidth = () => {
updateUISettings({ sidebar_width: sidebarWidth.value });
};
const snapToCollapsed = () => {
sidebarWidth.value = MIN_WIDTH;
updateUISettings({ sidebar_width: MIN_WIDTH });
};
const snapToExpanded = () => {
sidebarWidth.value = DEFAULT_WIDTH;
updateUISettings({ sidebar_width: DEFAULT_WIDTH });
};
return {
sidebarWidth,
isCollapsed,
setSidebarWidth,
saveWidth,
snapToCollapsed,
snapToExpanded,
MIN_WIDTH,
MAX_WIDTH,
COLLAPSED_THRESHOLD,
DEFAULT_WIDTH,
};
}
export function usePopoverState() {
const setActivePopover = name => {
clearTimeout(globalCloseTimeout);
activePopover.value = name;
};
const closeActivePopover = () => {
activePopover.value = null;
};
const scheduleClose = (delay = 150) => {
clearTimeout(globalCloseTimeout);
globalCloseTimeout = setTimeout(() => {
closeActivePopover();
}, delay);
};
const cancelClose = () => {
clearTimeout(globalCloseTimeout);
};
return {
activePopover,
setActivePopover,
closeActivePopover,
scheduleClose,
cancelClose,
};
}
export function useSidebarContext() { export function useSidebarContext() {
const context = inject(SidebarControl, null); const context = inject(SidebarControl, null);
if (context === null) { if (context === null) {
@@ -11,7 +89,6 @@ export function useSidebarContext() {
} }
const router = useRouter(); const router = useRouter();
const { shouldShow } = usePolicy(); const { shouldShow } = usePolicy();
const resolvePath = to => { const resolvePath = to => {

View File

@@ -27,7 +27,7 @@ const updateValue = () => {
> >
<span class="sr-only">{{ t('SWITCH.TOGGLE') }}</span> <span class="sr-only">{{ t('SWITCH.TOGGLE') }}</span>
<span <span
class="absolute top-0.5 left-0.5 h-3 w-3 transform rounded-full shadow-sm transition-transform duration-200 ease-in-out" class="absolute top-0.5 left-0.5 h-3 w-3 transform rounded-full shadow-sm transition-transform duration-200 ease-out"
:class=" :class="
modelValue modelValue
? 'translate-x-3 bg-n-background' ? 'translate-x-3 bg-n-background'

View File

@@ -84,7 +84,7 @@ const showDivider = index => {
class="relative z-10 px-4 truncate py-1.5 text-sm border-0 outline-1 outline-transparent rounded-lg transition-all duration-200 ease-out hover:text-n-brand active:scale-[1.02]" class="relative z-10 px-4 truncate py-1.5 text-sm border-0 outline-1 outline-transparent rounded-lg transition-all duration-200 ease-out hover:text-n-brand active:scale-[1.02]"
:class="[ :class="[
activeTab === index activeTab === index
? 'text-n-blue-text scale-100' ? 'text-n-blue-11 scale-100'
: 'text-n-slate-10 scale-[0.98]', : 'text-n-slate-10 scale-[0.98]',
]" ]"
@click="selectTab(index)" @click="selectTab(index)"

View File

@@ -47,7 +47,7 @@ const onToggle = () => {
</div> </div>
<div class="flex flex-row"> <div class="flex flex-row">
<slot name="button" /> <slot name="button" />
<div class="flex justify-end w-3 text-n-blue-text cursor-pointer"> <div class="flex justify-end w-3 text-n-blue-11 cursor-pointer">
<fluent-icon v-if="isOpen" size="24" icon="subtract" type="solid" /> <fluent-icon v-if="isOpen" size="24" icon="subtract" type="solid" />
<fluent-icon v-else size="24" icon="add" type="solid" /> <fluent-icon v-else size="24" icon="add" type="solid" />
</div> </div>
@@ -55,7 +55,7 @@ const onToggle = () => {
</button> </button>
<div <div
v-if="isOpen" v-if="isOpen"
class="bg-n-background outline outline-1 outline-n-weak -mt-[-1px] border-t-0 rounded-br-lg rounded-bl-lg" class="outline outline-1 outline-n-weak -mt-[-1px] border-t-0 rounded-br-lg rounded-bl-lg"
:class="compact ? 'p-0' : 'px-2 py-4'" :class="compact ? 'p-0' : 'px-2 py-4'"
> >
<slot /> <slot />

View File

@@ -902,7 +902,7 @@ watch(conversationFilters, (newVal, oldVal) => {
<template> <template>
<div <div
class="flex flex-col flex-shrink-0 bg-n-solid-1 conversations-list-wrap" class="flex flex-col flex-shrink-0 conversations-list-wrap bg-n-surface-1"
:class="[ :class="[
{ hidden: !showConversationList }, { hidden: !showConversationList },
isOnExpandedLayout ? 'basis-full' : 'w-[340px] 2xl:w-[412px]', isOnExpandedLayout ? 'basis-full' : 'w-[340px] 2xl:w-[412px]',

View File

@@ -57,7 +57,7 @@ const toggleConversationLayout = () => {
<template> <template>
<div <div
class="flex items-center justify-between gap-2 px-3 h-12" class="flex items-center justify-between gap-2 px-3 h-[3.25rem]"
:class="{ :class="{
'border-b border-n-strong': hasAppliedFiltersOrActiveFolders, 'border-b border-n-strong': hasAppliedFiltersOrActiveFolders,
}" }"

View File

@@ -133,7 +133,7 @@ onMounted(() => {
<div <div
v-if="shouldShowCopilotPanel" v-if="shouldShowCopilotPanel"
v-on-click-outside="() => closeCopilotPanel()" v-on-click-outside="() => closeCopilotPanel()"
class="bg-n-background h-full overflow-hidden flex-col fixed top-0 ltr:right-0 rtl:left-0 z-40 w-full max-w-sm transition-transform duration-300 ease-in-out md:static md:w-[320px] md:min-w-[320px] ltr:border-l rtl:border-r border-n-weak 2xl:min-w-[360px] 2xl:w-[360px] shadow-lg md:shadow-none" class="bg-n-surface-2 h-full overflow-hidden flex-col fixed top-0 ltr:right-0 rtl:left-0 z-40 w-full max-w-sm transition-transform duration-300 ease-in-out md:static md:w-[320px] md:min-w-[320px] ltr:border-l rtl:border-r border-n-weak 2xl:min-w-[360px] 2xl:w-[360px] shadow-lg md:shadow-none"
:class="[ :class="[
{ {
'md:flex': shouldShowCopilotPanel, 'md:flex': shouldShowCopilotPanel,

View File

@@ -116,7 +116,7 @@ const dayClasses = day => ({
(isInRange(day) || isHoveringInRange(day)) && (isInRange(day) || isHoveringInRange(day)) &&
!isSelectedStartOrEndDate(day) && !isSelectedStartOrEndDate(day) &&
isInCurrentMonth(day), isInCurrentMonth(day),
'outline outline-1 outline-n-blue-8 -outline-offset-1 !text-n-blue-text': 'outline outline-1 outline-n-blue-8 -outline-offset-1 !text-n-blue-11':
isToday(props.currentDate, day) && !isSelectedStartOrEndDate(day), isToday(props.currentDate, day) && !isSelectedStartOrEndDate(day),
}); });
</script> </script>

View File

@@ -47,23 +47,23 @@ const onTabClick = event => {
class="flex-shrink-0 my-0 mx-2 ltr:first:ml-0 rtl:first:mr-0 ltr:last:mr-0 rtl:last:ml-0 hover:text-n-slate-12" class="flex-shrink-0 my-0 mx-2 ltr:first:ml-0 rtl:first:mr-0 ltr:last:mr-0 rtl:last:ml-0 hover:text-n-slate-12"
> >
<a <a
class="flex items-center flex-row border-b select-none cursor-pointer text-sm relative top-[1px] transition-[border-color] duration-[150ms] ease-[cubic-bezier(0.37,0,0.63,1)]" class="flex items-center flex-row select-none cursor-pointer relative after:absolute after:bottom-px after:left-0 after:right-0 after:h-[2px] after:rounded-full after:transition-all after:duration-200 text-button"
:class="[ :class="[
active active
? 'border-b border-n-brand text-n-blue-text' ? 'text-n-blue-11 after:bg-n-brand after:opacity-100'
: 'border-transparent text-n-slate-11', : 'text-n-slate-11 after:bg-transparent after:opacity-0',
isCompact ? 'py-2 text-sm' : 'text-base py-3', isCompact ? 'py-2.5' : '!text-base py-3',
]" ]"
@click="onTabClick" @click="onTabClick"
> >
{{ name }} {{ name }}
<div <div
v-if="showBadge" v-if="showBadge"
class="rounded-md h-5 flex items-center justify-center text-xxs font-semibold my-0 mx-1 px-1 py-0 min-w-[20px]" class="rounded-full h-5 flex items-center justify-center text-xs font-medium my-0 ltr:ml-1 rtl:mr-1 px-1.5 py-0 min-w-[20px]"
:class="[ :class="[
active active
? 'bg-n-brand/10 dark:bg-n-brand/20 text-n-blue-text' ? 'bg-n-blue-3 text-n-blue-11'
: 'bg-n-alpha-black2 dark:bg-n-solid-3 text-n-slate-11', : 'bg-n-alpha-1 text-n-slate-10',
]" ]"
> >
<span> <span>

View File

@@ -48,7 +48,7 @@ useKeyboardEvents(keyboardEvents);
<template> <template>
<woot-tabs <woot-tabs
:index="activeTabIndex" :index="activeTabIndex"
class="w-full px-3 -mt-1 py-0 [&_ul]:p-0" class="w-full px-3 -mt-1 py-0 [&_ul]:p-0 h-10"
@change="onTabChange" @change="onTabChange"
> >
<woot-tabs-item <woot-tabs-item

View File

@@ -91,7 +91,7 @@ export default {
<template> <template>
<div <div
class="conversation-details-wrap flex flex-col min-w-0 w-full bg-n-background relative" class="conversation-details-wrap flex flex-col min-w-0 w-full bg-n-surface-1 relative"
:class="{ :class="{
'border-l rtl:border-l-0 rtl:border-r border-n-weak': !isOnExpandedLayout, 'border-l rtl:border-l-0 rtl:border-r border-n-weak': !isOnExpandedLayout,
}" }"
@@ -104,7 +104,7 @@ export default {
<woot-tabs <woot-tabs
v-if="dashboardApps.length && currentChat.id" v-if="dashboardApps.length && currentChat.id"
:index="activeIndex" :index="activeIndex"
class="-mt-px border-t border-t-n-background" class="h-10"
@change="onDashboardAppTabChange" @change="onDashboardAppTabChange"
> >
<woot-tabs-item <woot-tabs-item
@@ -114,7 +114,6 @@ export default {
:name="tab.name" :name="tab.name"
:show-badge="false" :show-badge="false"
is-compact is-compact
class="[&_a]:pt-1"
/> />
</woot-tabs> </woot-tabs>
<div v-show="!activeIndex" class="flex h-full min-h-0 m-0"> <div v-show="!activeIndex" class="flex h-full min-h-0 m-0">

View File

@@ -236,9 +236,8 @@ const deleteConversation = () => {
<div <div
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full py-0 border-t-0 border-b-0 border-l-0 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3 group" class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full py-0 border-t-0 border-b-0 border-l-0 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3 group"
:class="{ :class="{
'active animate-card-select bg-n-alpha-1 dark:bg-n-alpha-3 border-n-weak': 'active animate-card-select bg-n-background border-n-weak': isActiveChat,
isActiveChat, 'bg-n-slate-2': selected,
'bg-n-slate-2 dark:bg-n-slate-3': selected,
'px-0': compact, 'px-0': compact,
'px-3': !compact, 'px-3': !compact,
}" }"

View File

@@ -95,7 +95,7 @@ const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id);
<template> <template>
<div <div
ref="conversationHeader" ref="conversationHeader"
class="flex flex-col gap-3 items-center justify-between flex-1 w-full min-w-0 xl:flex-row px-3 py-2 border-b bg-n-background border-n-weak h-24 xl:h-12" class="flex flex-col gap-3 items-center justify-between flex-1 w-full min-w-0 xl:flex-row px-3 pt-3 pb-2 h-24 xl:h-12"
> >
<div <div
class="flex items-center justify-start w-full xl:w-auto max-w-full min-w-0 xl:flex-1" class="flex items-center justify-start w-full xl:w-auto max-w-full min-w-0 xl:flex-1"

View File

@@ -42,7 +42,7 @@ const closeContactPanel = () => {
<template> <template>
<div <div
v-on-click-outside="() => closeContactPanel()" v-on-click-outside="() => closeContactPanel()"
class="bg-n-background h-full overflow-hidden flex flex-col fixed top-0 z-40 w-full max-w-sm transition-transform duration-300 ease-in-out ltr:right-0 rtl:left-0 md:static md:w-[320px] md:min-w-[320px] ltr:border-l rtl:border-r border-n-weak 2xl:min-w-[360px] 2xl:w-[360px] shadow-lg md:shadow-none" class="bg-n-surface-2 h-full overflow-hidden flex flex-col fixed top-0 z-40 w-full max-w-sm transition-transform duration-300 ease-in-out ltr:right-0 rtl:left-0 md:static md:w-[320px] md:min-w-[320px] ltr:border-l rtl:border-r border-n-weak 2xl:min-w-[360px] 2xl:w-[360px] shadow-lg md:shadow-none"
:class="[ :class="[
{ {
'md:flex': activeTab === 0, 'md:flex': activeTab === 0,

View File

@@ -58,7 +58,7 @@ export default {
) { ) {
return 'h-full overflow-auto w-full'; return 'h-full overflow-auto w-full';
} }
return 'flex-1 min-w-0 px-0 flex flex-col items-center justify-center h-full'; return 'flex-1 min-w-0 px-0 flex flex-col items-center justify-center h-full bg-n-surface-1';
}, },
}, },
}; };

View File

@@ -502,7 +502,7 @@ export default {
class="flex relative flex-col" class="flex relative flex-col"
:class="{ :class="{
'modal-mask': isPopOutReplyBox, 'modal-mask': isPopOutReplyBox,
'bg-n-background': !isPopOutReplyBox, 'bg-n-surface-1': !isPopOutReplyBox,
}" }"
> >
<div <div

View File

@@ -29,7 +29,7 @@ defineProps({
<template> <template>
<div <div
class="h-full w-full bg-n-background border border-n-weak rounded-lg p-4 flex flex-col" class="h-full w-full bg-n-surface-2 border border-n-weak rounded-lg p-4 flex flex-col"
> >
<div class="flex-1 flex items-center justify-center"> <div class="flex-1 flex items-center justify-center">
<img :src="imageSrc" :alt="imageAlt" class="h-36 w-auto mx-auto" /> <img :src="imageSrc" :alt="imageAlt" class="h-36 w-auto mx-auto" />

View File

@@ -60,7 +60,7 @@ const onClickTabChange = index => {
<div class="flex flex-col h-auto overflow-auto"> <div class="flex flex-col h-auto overflow-auto">
<div class="flex flex-col px-8 pb-4 mt-1"> <div class="flex flex-col px-8 pb-4 mt-1">
<woot-tabs <woot-tabs
class="ltr:[&>ul]:pl-0 rtl:[&>ul]:pr-0" class="ltr:[&>ul]:pl-0 rtl:[&>ul]:pr-0 h-10"
:index="selectedTabIndex" :index="selectedTabIndex"
@change="onClickTabChange" @change="onClickTabChange"
> >

View File

@@ -94,7 +94,7 @@ onUnmounted(() => {
class="icon" class="icon"
aria-hidden="true" aria-hidden="true"
:class="{ :class="{
'text-n-blue-text': isInputFocused, 'text-n-blue-11': isInputFocused,
'text-n-slate-10': !isInputFocused, 'text-n-slate-10': !isInputFocused,
}" }"
/> />

View File

@@ -358,7 +358,7 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<div class="flex flex-col w-full h-full bg-n-background"> <div class="flex flex-col w-full h-full bg-n-surface-1">
<div class="flex w-full p-4"> <div class="flex w-full p-4">
<NextButton <NextButton
:label="t('GENERAL_SETTINGS.BACK')" :label="t('GENERAL_SETTINGS.BACK')"

View File

@@ -140,7 +140,9 @@ export default {
@close-mobile-sidebar="closeMobileSidebar" @close-mobile-sidebar="closeMobileSidebar"
/> />
<main class="flex flex-1 h-full w-full min-h-0 px-0 overflow-hidden"> <main
class="flex flex-1 h-full w-full min-h-0 px-0 overflow-hidden bg-n-surface-1"
>
<UpgradePage <UpgradePage
v-show="showUpgradePage" v-show="showUpgradePage"
ref="upgradePageRef" ref="upgradePageRef"

View File

@@ -16,7 +16,7 @@ onMounted(() => {
<template> <template>
<div <div
class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-background px-6" class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-surface-1 px-6"
> >
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive v-if="keepAlive"> <keep-alive v-if="keepAlive">

View File

@@ -82,7 +82,7 @@ onMounted(() => performRouting());
<template> <template>
<div <div
class="flex items-center justify-center w-full bg-n-background text-n-slate-11" class="flex items-center justify-center w-full bg-n-surface-1 text-n-slate-11"
> >
<Spinner /> <Spinner />
</div> </div>

View File

@@ -23,7 +23,7 @@ watch(
<template> <template>
<div class="flex w-full h-full min-h-0"> <div class="flex w-full h-full min-h-0">
<section class="flex flex-1 h-full px-0 overflow-hidden bg-n-background"> <section class="flex flex-1 h-full px-0 overflow-hidden bg-n-surface-1">
<router-view /> <router-view />
</section> </section>
</div> </div>

View File

@@ -127,7 +127,7 @@ onMounted(() => {
<template> <template>
<div <div
class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-background" class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-surface-1"
> >
<ContactsDetailsLayout <ContactsDetailsLayout
:button-label="$t('CONTACTS_LAYOUT.HEADER.SEND_MESSAGE')" :button-label="$t('CONTACTS_LAYOUT.HEADER.SEND_MESSAGE')"

View File

@@ -435,7 +435,7 @@ onMounted(async () => {
<template> <template>
<div <div
class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-background" class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-surface-1"
> >
<ContactsListLayout <ContactsListLayout
:search-value="searchValue" :search-value="searchValue"

View File

@@ -207,7 +207,7 @@ export default {
</script> </script>
<template> <template>
<div class="bg-n-background"> <div>
<div class="multiselect-wrap--small"> <div class="multiselect-wrap--small">
<ContactDetailsItem <ContactDetailsItem
compact compact

View File

@@ -155,7 +155,7 @@ export default {
</script> </script>
<template> <template>
<div class="relative bg-n-background"> <div class="relative">
<div class="flex justify-between"> <div class="flex justify-between">
<div class="flex justify-between w-full mb-1"> <div class="flex justify-between w-full mb-1">
<div> <div>

View File

@@ -251,8 +251,8 @@ onMounted(() => {
}); });
const evenClass = [ const evenClass = [
'[&>*:nth-child(odd)]:!bg-n-background [&>*:nth-child(even)]:!bg-n-slate-2', '[&>*:nth-child(odd)]:!bg-n-surface-1 [&>*:nth-child(even)]:!bg-n-slate-1',
'dark:[&>*:nth-child(odd)]:!bg-n-background dark:[&>*:nth-child(even)]:!bg-n-solid-1', 'dark:[&>*:nth-child(odd)]:!bg-n-surface-2 dark:[&>*:nth-child(even)]:!bg-n-surface-1',
]; ];
</script> </script>

View File

@@ -68,7 +68,7 @@ export default {
<template> <template>
<div <div
class="flex flex-col gap-12 sm:gap-16 items-center justify-center py-0 px-4 w-full min-h-screen max-w-full overflow-auto bg-n-background" class="flex flex-col gap-12 sm:gap-16 items-center justify-center py-0 px-4 w-full min-h-screen max-w-full overflow-auto bg-n-surface-1"
> >
<div class="flex flex-col justify-start sm:justify-center gap-6"> <div class="flex flex-col justify-start sm:justify-center gap-6">
<div class="flex flex-col gap-1.5 items-start sm:items-center"> <div class="flex flex-col gap-1.5 items-start sm:items-center">

View File

@@ -67,7 +67,7 @@ watch(
<div class="flex w-full h-full min-h-0"> <div class="flex w-full h-full min-h-0">
<section <section
v-if="isHelpCenterEnabled" v-if="isHelpCenterEnabled"
class="flex flex-1 h-full px-0 overflow-hidden bg-n-background" class="flex flex-1 h-full px-0 overflow-hidden bg-n-surface-1"
> >
<router-view /> <router-view />
</section> </section>

View File

@@ -69,7 +69,7 @@ onMounted(() => performRouting());
<template> <template>
<div <div
class="flex items-center justify-center w-full bg-n-background text-n-slate-11" class="flex items-center justify-center w-full bg-n-surface-1 text-n-slate-11"
> >
<Spinner /> <Spinner />
</div> </div>

View File

@@ -28,7 +28,7 @@ export default {
<template> <template>
<div <div
class="items-center justify-center hidden w-full h-full text-center bg-n-background lg:flex" class="items-center justify-center hidden w-full h-full text-center bg-n-surface-1 lg:flex"
> >
<div v-if="uiFlags.isFetching" class="flex justify-center my-4"> <div v-if="uiFlags.isFetching" class="flex justify-center my-4">
<Spinner class="text-n-brand" /> <Spinner class="text-n-brand" />

View File

@@ -222,7 +222,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<section class="flex w-full h-full bg-n-solid-1"> <section class="flex w-full h-full bg-n-surface-1">
<div <div
class="flex flex-col h-full w-full lg:min-w-[340px] lg:max-w-[340px] ltr:border-r rtl:border-l border-n-weak" class="flex flex-col h-full w-full lg:min-w-[340px] lg:max-w-[340px] ltr:border-r rtl:border-l border-n-weak"
:class="!currentConversationId ? 'flex' : 'hidden xl:flex'" :class="!currentConversationId ? 'flex' : 'hidden xl:flex'"
@@ -244,7 +244,7 @@ onMounted(() => {
class="inbox-card rounded-none hover:rounded-lg hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3" class="inbox-card rounded-none hover:rounded-lg hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3"
:class=" :class="
currentConversationId === notificationItem.primaryActor?.id currentConversationId === notificationItem.primaryActor?.id
? 'bg-n-alpha-1 dark:bg-n-alpha-3 rounded-lg active' ? 'bg-n-alpha-1 dark:bg-n-alpha-3 !rounded-lg active'
: '' : ''
" "
@mark-notification-as-read="markNotificationAsRead" @mark-notification-as-read="markNotificationAsRead"

View File

@@ -152,8 +152,7 @@ export default {
<span <span
class="text-xs font-medium hover:text-n-brand truncate min-w-0 dark:hover:text-n-brand" class="text-xs font-medium hover:text-n-brand truncate min-w-0 dark:hover:text-n-brand"
:class="{ :class="{
'text-n-blue-text dark:text-n-blue-text': 'text-n-blue-11 dark:text-n-blue-11': activeSort === option.key,
activeSort === option.key,
'text-n-slate-11': activeSort !== option.key, 'text-n-slate-11': activeSort !== option.key,
}" }"
> >
@@ -161,7 +160,7 @@ export default {
</span> </span>
<span <span
v-if="activeSort === option.key" v-if="activeSort === option.key"
class="i-lucide-check size-2.5 flex-shrink-0 text-n-blue-text" class="i-lucide-check size-2.5 flex-shrink-0 text-n-blue-11"
/> />
</div> </div>
</div> </div>

View File

@@ -109,7 +109,7 @@ export default {
<template> <template>
<div <div
class="flex items-center justify-between w-full gap-2 border-b px-3 h-12 rtl:border-r border-n-weak flex-shrink-0" class="flex items-center justify-between w-full gap-2 border-b px-3 h-12 rtl:border-r border-n-weak flex-shrink-0 bg-n-surface-1"
> >
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<BackButton <BackButton

View File

@@ -10,7 +10,7 @@ defineProps({
<template> <template>
<div <div
role="button" role="button"
class="flex items-center w-full h-8 px-2 py-1 rounded-md cursor-pointer hover:text-n-blue-text min-w-0" class="flex items-center w-full h-8 px-2 py-1 rounded-md cursor-pointer hover:text-n-blue-11 min-w-0"
> >
<span class="text-xs font-medium truncate text-n-slate-12"> <span class="text-xs font-medium truncate text-n-slate-12">
{{ label }} {{ label }}

View File

@@ -1,14 +1,14 @@
export const NOTIFICATION_TYPES_MAPPING = { export const NOTIFICATION_TYPES_MAPPING = {
CONVERSATION_MENTION: ['i-lucide-at-sign', 'text-n-blue-text'], CONVERSATION_MENTION: ['i-lucide-at-sign', 'text-n-blue-11'],
CONVERSATION_ASSIGNMENT: ['i-lucide-chevrons-right', 'text-n-blue-text'], CONVERSATION_ASSIGNMENT: ['i-lucide-chevrons-right', 'text-n-blue-11'],
CONVERSATION_CREATION: ['i-lucide-mail-plus', 'text-n-blue-text'], CONVERSATION_CREATION: ['i-lucide-mail-plus', 'text-n-blue-11'],
PARTICIPATING_CONVERSATION_NEW_MESSAGE: [ PARTICIPATING_CONVERSATION_NEW_MESSAGE: [
'i-lucide-message-square-plus', 'i-lucide-message-square-plus',
'text-n-blue-text', 'text-n-blue-11',
], ],
ASSIGNED_CONVERSATION_NEW_MESSAGE: [ ASSIGNED_CONVERSATION_NEW_MESSAGE: [
'i-lucide-message-square-plus', 'i-lucide-message-square-plus',
'text-n-blue-text', 'text-n-blue-11',
], ],
SLA_MISSED_FIRST_RESPONSE: ['i-lucide-heart-crack', 'text-n-ruby-11'], SLA_MISSED_FIRST_RESPONSE: ['i-lucide-heart-crack', 'text-n-ruby-11'],
SLA_MISSED_NEXT_RESPONSE: ['i-lucide-heart-crack', 'text-n-ruby-11'], SLA_MISSED_NEXT_RESPONSE: ['i-lucide-heart-crack', 'text-n-ruby-11'],

View File

@@ -41,7 +41,7 @@ export default {
<template> <template>
<div <div
class="flex justify-between items-center h-20 min-h-[3.5rem] px-4 py-2 bg-n-background" class="flex justify-between items-center h-20 min-h-[3.5rem] px-4 py-2 bg-n-surface-1"
> >
<h1 class="flex items-center mb-0 text-2xl text-n-slate-12"> <h1 class="flex items-center mb-0 text-2xl text-n-slate-12">
<BackButton <BackButton

View File

@@ -9,7 +9,7 @@ defineProps({
<template> <template>
<div <div
class="flex flex-col w-full h-full m-0 p-6 sm:py-8 lg:px-16 overflow-auto bg-n-background font-inter" class="flex flex-col w-full h-full m-0 p-6 sm:py-8 lg:px-16 overflow-auto bg-n-surface-1 font-inter"
> >
<div class="flex items-start w-full max-w-6xl mx-auto"> <div class="flex items-start w-full max-w-6xl mx-auto">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">

View File

@@ -19,7 +19,7 @@ const showSettingsHeader = computed(
</script> </script>
<template> <template>
<div class="flex flex-1 flex-col m-0 bg-n-background overflow-auto"> <div class="flex flex-1 flex-col m-0 bg-n-surface-1 overflow-auto">
<div <div
class="mx-auto w-full flex flex-col flex-1" class="mx-auto w-full flex flex-col flex-1"
:class="{ 'max-w-6xl': !fullWidth }" :class="{ 'max-w-6xl': !fullWidth }"

View File

@@ -85,12 +85,12 @@ const openInNewTab = url => {
:href="helpURL" :href="helpURL"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="items-center hidden gap-1 text-sm font-medium sm:inline-flex w-fit text-n-blue-text hover:underline" class="items-center hidden gap-1 text-sm font-medium sm:inline-flex w-fit text-n-blue-11 hover:underline"
> >
{{ linkText }} {{ linkText }}
<Icon <Icon
icon="i-lucide-chevron-right" icon="i-lucide-chevron-right"
class="flex-shrink-0 text-n-blue-text size-4" class="flex-shrink-0 text-n-blue-11 size-4"
/> />
</a> </a>
</CustomBrandPolicyWrapper> </CustomBrandPolicyWrapper>

View File

@@ -509,7 +509,7 @@ export default {
:header-title="inboxName" :header-title="inboxName"
> >
<woot-tabs <woot-tabs
class="[&_ul]:p-0" class="[&_ul]:p-0 top-px relative"
:index="selectedTabIndex" :index="selectedTabIndex"
:border="false" :border="false"
@change="onTabChange" @change="onTabChange"

View File

@@ -210,7 +210,7 @@ export default {
<div> <div>
<span <span
v-if="isDayEnabled && !hasError" v-if="isDayEnabled && !hasError"
class="label bg-n-brand/10 dark:bg-n-brand/30 text-n-blue-text text-xs inline-block px-2 py-1 rounded-lg cursor-default whitespace-nowrap" class="label bg-n-brand/10 dark:bg-n-brand/30 text-n-blue-11 text-xs inline-block px-2 py-1 rounded-lg cursor-default whitespace-nowrap"
> >
{{ totalHours }} {{ totalHours }}
</span> </span>

View File

@@ -48,7 +48,7 @@ export default {
<div class="macro__node"> <div class="macro__node">
<div> <div>
<span <span
class="bg-n-solid-blue text-n-blue-text py-1 px-1.5 leading-none text-sm rounded-md" class="bg-n-solid-blue text-n-blue-11 py-1 px-1.5 leading-none text-sm rounded-md"
> >
{{ $t('MACROS.EDITOR.START_FLOW') }} {{ $t('MACROS.EDITOR.START_FLOW') }}
</span> </span>
@@ -102,7 +102,7 @@ export default {
<div class="macro__node"> <div class="macro__node">
<div> <div>
<span <span
class="bg-n-solid-blue text-n-blue-text py-1 px-1.5 leading-none text-sm rounded-md" class="bg-n-solid-blue text-n-blue-11 py-1 px-1.5 leading-none text-sm rounded-md"
> >
{{ $t('MACROS.EDITOR.END_FLOW') }} {{ $t('MACROS.EDITOR.END_FLOW') }}
</span> </span>

View File

@@ -9,7 +9,7 @@ defineProps({
<template> <template>
<div <div
class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-background" class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-surface-1"
> >
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive v-if="keepAlive"> <keep-alive v-if="keepAlive">

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="reports--wrapper overflow-auto bg-n-background w-full px-6"> <div class="reports--wrapper overflow-auto bg-n-surface-1 w-full px-6">
<div class="max-w-[60rem] mx-auto pb-12"> <div class="max-w-[60rem] mx-auto pb-12">
<router-view /> <router-view />
</div> </div>

View File

@@ -1,55 +1,17 @@
// Inter Variable Font - Supports any weight from 100-900
@font-face { @font-face {
font-family: 'Inter'; font-family: 'Inter';
font-style: normal; font-style: normal;
font-weight: 100; font-weight: 100 900;
font-display: swap; font-display: swap;
src: url('shared/assets/fonts/Inter/Inter-Thin.woff2') format('woff2'); src: url('shared/assets/fonts/Inter/InterVariable.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('shared/assets/fonts/Inter/Inter-Light.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('shared/assets/fonts/Inter/Inter-Regular.woff2') format('woff2');
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: 'Inter';
font-style: italic; font-style: italic;
font-weight: 400; font-weight: 100 900;
font-display: swap; font-display: swap;
src: url('shared/assets/fonts/Inter/Inter-Italic.woff2') format('woff2'); src: url('shared/assets/fonts/Inter/InterVariable-Italic.woff2')
} format('woff2');
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('shared/assets/fonts/Inter/Inter-Medium.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('shared/assets/fonts/Inter/Inter-SemiBold.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('shared/assets/fonts/Inter/Inter-Bold.woff2') format('woff2');
} }

View File

@@ -263,7 +263,7 @@ export default {
<div <div
v-if="shouldShowHeaderMessage" v-if="shouldShowHeaderMessage"
v-dompurify-html="formatMessage(headerMessage, false)" v-dompurify-html="formatMessage(headerMessage, false)"
class="mb-4 text-base leading-5 text-n-slate-12 [&>p>.link]:text-n-blue-text [&>p>.link]:hover:underline" class="mb-4 text-base leading-5 text-n-slate-12 [&>p>.link]:text-n-blue-11 [&>p>.link]:hover:underline"
/> />
<!-- Why do the v-bind shenanigan? Because Formkit API is really bad. <!-- Why do the v-bind shenanigan? Because Formkit API is really bad.
If we just pass the options as is even with null or undefined or false, If we just pass the options as is even with null or undefined or false,

View File

@@ -43,6 +43,12 @@ const tailwindConfig = {
inter: ['Inter', ...defaultSansFonts], inter: ['Inter', ...defaultSansFonts],
interDisplay: ['InterDisplay', ...defaultSansFonts], interDisplay: ['InterDisplay', ...defaultSansFonts],
}, },
fontWeight: {
420: '420',
440: '440',
460: '460',
520: '520',
},
typography: { typography: {
bubble: { bubble: {
css: { css: {
@@ -187,6 +193,7 @@ const tailwindConfig = {
lg: '1024px', lg: '1024px',
xl: '1280px', xl: '1280px',
'2xl': '1536px', '2xl': '1536px',
'3xl': '1900px',
}, },
fontSize: { fontSize: {
...defaultTheme.fontSize, ...defaultTheme.fontSize,

Some files were not shown because too many files have changed in this diff Show More