feat: Update conversation side panel layout and styling (#12983)
This commit is contained in:
+20
-22
@@ -48,10 +48,26 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2 border-b border-n-strong group/note">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-1.5 min-w-0">
|
||||
<div class="flex flex-col gap-1.5 border-b border-n-strong group/note">
|
||||
<div class="flex items-center justify-between w-full gap-1.5 min-w-0 h-9">
|
||||
<div class="min-w-0 truncate text-n-slate-11 text-xs font-440">
|
||||
{{ dynamicTime(note.createdAt) }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
v-if="allowDelete"
|
||||
ghost
|
||||
ruby
|
||||
size="xs"
|
||||
icon="i-lucide-trash"
|
||||
class="opacity-0 group-hover/note:opacity-100"
|
||||
@click="handleDelete"
|
||||
/>
|
||||
<Avatar
|
||||
v-tooltip.left="{
|
||||
content: writtenBy,
|
||||
delay: { show: 500, hide: 0 },
|
||||
}"
|
||||
:name="note?.user?.name || 'Bot'"
|
||||
:src="
|
||||
note?.user?.name
|
||||
@@ -61,30 +77,12 @@ onMounted(() => {
|
||||
:size="16"
|
||||
rounded-full
|
||||
/>
|
||||
<div class="min-w-0 truncate">
|
||||
<span class="inline-flex items-center gap-1 text-sm text-n-slate-11">
|
||||
<span class="font-medium text-n-slate-12">{{ writtenBy }}</span>
|
||||
{{ t('CONTACTS_LAYOUT.SIDEBAR.NOTES.WROTE') }}
|
||||
<span class="font-medium text-n-slate-12">
|
||||
{{ dynamicTime(note.createdAt) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
v-if="allowDelete"
|
||||
variant="faded"
|
||||
color="ruby"
|
||||
size="xs"
|
||||
icon="i-lucide-trash"
|
||||
class="opacity-0 group-hover/note:opacity-100"
|
||||
@click="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
ref="noteContentRef"
|
||||
v-dompurify-html="formatMessage(note.content || '')"
|
||||
class="mb-0 prose-sm prose-p:text-sm prose-p:leading-relaxed prose-p:mb-1 prose-p:mt-0 prose-ul:mb-1 prose-ul:mt-0 text-n-slate-12"
|
||||
class="mb-0 font-420 prose-sm prose-p:text-sm prose-p:leading-relaxed prose-p:mb-1 prose-p:mt-0 prose-ul:mb-1 prose-ul:mt-0 text-n-slate-12"
|
||||
:class="{
|
||||
'line-clamp-4': collapsible && !isExpanded && needsCollapse,
|
||||
}"
|
||||
|
||||
@@ -101,6 +101,7 @@ const onPickInbox = async inbox => {
|
||||
:label="label"
|
||||
:icon="icon"
|
||||
:size="size"
|
||||
class="!px-2"
|
||||
@click="onClick"
|
||||
/>
|
||||
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ const onSelectConversation = checked => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full cursor-pointer group transition-all duration-200"
|
||||
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full cursor-pointer group transition-all duration-150"
|
||||
:class="{
|
||||
'active animate-card-select bg-n-alpha-1 dark:bg-n-alpha-3': isActiveChat,
|
||||
'selected bg-n-slate-2 dark:bg-n-slate-3': selected,
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ const selectedModel = computed({
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative cursor-pointer group transition-all duration-200 grid gap-4 items-center px-2 h-12"
|
||||
class="relative cursor-pointer group transition-all duration-150 grid gap-4 items-center px-2 h-12"
|
||||
:class="{
|
||||
'active animate-card-select bg-n-alpha-1 dark:bg-n-alpha-3': isActiveChat,
|
||||
'selected bg-n-slate-2 dark:bg-n-slate-3': selected,
|
||||
|
||||
@@ -20,10 +20,10 @@ const showCopilotTab = computed(() =>
|
||||
|
||||
const { uiSettings } = useUISettings();
|
||||
const isContactSidebarOpen = computed(
|
||||
() => uiSettings.value.is_contact_sidebar_open
|
||||
() => uiSettings.value.is_contact_sidebar_open || false
|
||||
);
|
||||
const isCopilotPanelOpen = computed(
|
||||
() => uiSettings.value.is_copilot_panel_open
|
||||
() => uiSettings.value.is_copilot_panel_open || false
|
||||
);
|
||||
|
||||
const toggleConversationSidebarToggle = () => {
|
||||
@@ -35,7 +35,7 @@ const toggleConversationSidebarToggle = () => {
|
||||
|
||||
const handleConversationSidebarToggle = () => {
|
||||
updateUISettings({
|
||||
is_contact_sidebar_open: true,
|
||||
is_contact_sidebar_open: !isContactSidebarOpen.value,
|
||||
is_copilot_panel_open: false,
|
||||
});
|
||||
};
|
||||
@@ -43,7 +43,7 @@ const handleConversationSidebarToggle = () => {
|
||||
const handleCopilotSidebarToggle = () => {
|
||||
updateUISettings({
|
||||
is_contact_sidebar_open: false,
|
||||
is_copilot_panel_open: true,
|
||||
is_copilot_panel_open: !isCopilotPanelOpen.value,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ defineExpose({
|
||||
<SLAPopoverCard
|
||||
v-if="showSlaPopoverCard"
|
||||
:sla-missed-events="slaEvents"
|
||||
class="start-0 xl:start-auto xl:end-0 top-7 hidden group-hover:flex"
|
||||
class="start-0 md:start-auto md:end-0 top-7 hidden group-hover:flex"
|
||||
/>
|
||||
</div>
|
||||
<template v-else />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Switch from 'dashboard/components-next/switch/Switch.vue';
|
||||
@@ -19,28 +19,35 @@ const emit = defineEmits(['update', 'delete']);
|
||||
|
||||
const attributeValue = ref(Boolean(props.attribute.value));
|
||||
|
||||
const isAttributePresent = computed(() => {
|
||||
return (
|
||||
props.attribute.value !== undefined &&
|
||||
props.attribute.value !== null &&
|
||||
props.attribute.value !== ''
|
||||
);
|
||||
});
|
||||
|
||||
const handleChange = value => {
|
||||
emit('update', value);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
attributeValue.value = false;
|
||||
emit('delete');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center w-full gap-2"
|
||||
:class="{
|
||||
'justify-start': isEditingView,
|
||||
'justify-end': !isEditingView,
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center w-full gap-2 justify-end">
|
||||
<Switch v-model="attributeValue" @change="handleChange" />
|
||||
<Button
|
||||
v-if="isEditingView"
|
||||
variant="faded"
|
||||
color="ruby"
|
||||
v-if="isEditingView && isAttributePresent"
|
||||
ghost
|
||||
ruby
|
||||
icon="i-lucide-trash"
|
||||
size="xs"
|
||||
class="flex-shrink-0 opacity-0 group-hover/attribute:opacity-100 hover:no-underline"
|
||||
@click="emit('delete')"
|
||||
xs
|
||||
class="flex-shrink-0 !size-5 !rounded"
|
||||
@click="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -75,20 +75,15 @@ const handleInputUpdate = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center w-full min-w-0 gap-2"
|
||||
:class="{
|
||||
'justify-start': isEditingView,
|
||||
'justify-end': !isEditingView,
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center w-full min-w-0 gap-1.5 justify-end">
|
||||
<span
|
||||
v-if="!isEditingValue"
|
||||
class="min-w-0 text-sm"
|
||||
:class="{
|
||||
'cursor-pointer text-n-slate-11 hover:text-n-slate-12 py-2 select-none font-medium':
|
||||
!isEditingView,
|
||||
'text-n-slate-12 truncate': isEditingView,
|
||||
'text-n-slate-12 truncate': isEditingView && attribute.value,
|
||||
'text-n-slate-10 truncate': isEditingView && !attribute.value,
|
||||
}"
|
||||
@click="toggleEditValue(!isEditingView)"
|
||||
>
|
||||
@@ -100,19 +95,20 @@ const handleInputUpdate = async () => {
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<Button
|
||||
variant="faded"
|
||||
color="slate"
|
||||
icon="i-lucide-pencil"
|
||||
size="xs"
|
||||
class="flex-shrink-0 opacity-0 group-hover/attribute:opacity-100 hover:no-underline"
|
||||
ghost
|
||||
slate
|
||||
icon="i-lucide-pen-line"
|
||||
xs
|
||||
class="flex-shrink-0 !size-5 !rounded"
|
||||
@click="toggleEditValue(true)"
|
||||
/>
|
||||
<Button
|
||||
variant="faded"
|
||||
color="ruby"
|
||||
v-if="attribute.value"
|
||||
ghost
|
||||
ruby
|
||||
icon="i-lucide-trash"
|
||||
size="xs"
|
||||
class="flex-shrink-0 opacity-0 group-hover/attribute:opacity-100 hover:no-underline"
|
||||
xs
|
||||
class="flex-shrink-0 !size-5 !rounded"
|
||||
@click="emit('delete')"
|
||||
/>
|
||||
</div>
|
||||
@@ -125,7 +121,7 @@ const handleInputUpdate = async () => {
|
||||
<Input
|
||||
v-model="defaultDateValue"
|
||||
type="date"
|
||||
class="w-full [&>p]:absolute [&>p]:mt-0.5 [&>p]:top-8 ltr:[&>p]:left-0 rtl:[&>p]:right-0"
|
||||
class="w-full [&>p]:absolute [&>p]:mt-1 [&>p]:top-6 ltr:[&>p]:left-0 rtl:[&>p]:right-0"
|
||||
:message="
|
||||
hasError
|
||||
? t('CONTACTS_LAYOUT.SIDEBAR.ATTRIBUTES.VALIDATIONS.INVALID_DATE')
|
||||
@@ -133,14 +129,15 @@ const handleInputUpdate = async () => {
|
||||
"
|
||||
:message-type="hasError ? 'error' : 'info'"
|
||||
autofocus
|
||||
custom-input-class="h-8 ltr:rounded-r-none rtl:rounded-l-none"
|
||||
@keyup.enter="handleInputUpdate"
|
||||
custom-input-class="h-7 py-1 ltr:rounded-r-none rtl:rounded-l-none"
|
||||
@enter="handleInputUpdate"
|
||||
/>
|
||||
<Button
|
||||
icon="i-lucide-check"
|
||||
:color="hasError ? 'ruby' : 'blue'"
|
||||
size="sm"
|
||||
class="flex-shrink-0 ltr:rounded-l-none rtl:rounded-r-none"
|
||||
type="button"
|
||||
class="flex-shrink-0 ltr:rounded-l-none rtl:rounded-r-none !h-7"
|
||||
@click="handleInputUpdate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
attribute: {
|
||||
@@ -40,61 +41,55 @@ const handleAttributeAction = async action => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center w-full min-w-0 gap-2"
|
||||
:class="{
|
||||
'justify-start': isEditingView,
|
||||
'justify-end': !isEditingView,
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center w-full min-w-0 gap-1.5 justify-end">
|
||||
<div
|
||||
v-on-clickaway="() => toggleAttributeListDropdown(false)"
|
||||
class="relative flex items-center"
|
||||
class="relative flex items-center min-w-0 flex-1 justify-end gap-2 cursor-pointer"
|
||||
@click="toggleAttributeListDropdown()"
|
||||
>
|
||||
<span
|
||||
class="min-w-0 text-sm"
|
||||
v-tooltip.top="{
|
||||
content: attribute.value,
|
||||
delay: { show: 1000, hide: 0 },
|
||||
}"
|
||||
class="min-w-0 text-sm truncate font-420"
|
||||
:class="{
|
||||
'cursor-pointer text-n-slate-11 hover:text-n-slate-12 py-2 select-none font-medium':
|
||||
'text-n-slate-11 hover:text-n-slate-12 py-2 select-none font-medium':
|
||||
!isEditingView,
|
||||
'text-n-slate-12 truncate flex-1': isEditingView,
|
||||
'text-n-slate-12': isEditingView && attribute.value,
|
||||
'text-n-slate-10': isEditingView && !attribute.value,
|
||||
}"
|
||||
@click="toggleAttributeListDropdown(!props.isEditingView)"
|
||||
>
|
||||
{{
|
||||
attribute.value ||
|
||||
t('CONTACTS_LAYOUT.SIDEBAR.ATTRIBUTES.TRIGGER.SELECT')
|
||||
}}
|
||||
</span>
|
||||
<Icon
|
||||
:icon="
|
||||
showAttributeListDropdown
|
||||
? 'i-lucide-chevron-up'
|
||||
: 'i-lucide-chevron-down'
|
||||
"
|
||||
class="text-n-slate-11 flex-shrink-0 size-4"
|
||||
/>
|
||||
<DropdownMenu
|
||||
v-if="showAttributeListDropdown"
|
||||
:menu-items="attributeListMenuItems"
|
||||
show-search
|
||||
class="w-48 mt-2 top-full"
|
||||
:class="{
|
||||
'ltr:right-0 rtl:left-0': !isEditingView,
|
||||
'ltr:left-0 rtl:right-0': isEditingView,
|
||||
}"
|
||||
class="w-48 mt-2 top-full ltr:right-0 rtl:left-0"
|
||||
@action="handleAttributeAction($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isEditingView" class="flex items-center gap-1">
|
||||
<Button
|
||||
variant="faded"
|
||||
color="slate"
|
||||
icon="i-lucide-pencil"
|
||||
size="xs"
|
||||
class="flex-shrink-0 opacity-0 group-hover/attribute:opacity-100 hover:no-underline"
|
||||
@click="toggleAttributeListDropdown()"
|
||||
/>
|
||||
<Button
|
||||
variant="faded"
|
||||
color="ruby"
|
||||
icon="i-lucide-trash"
|
||||
size="xs"
|
||||
class="flex-shrink-0 opacity-0 group-hover/attribute:opacity-100 hover:no-underline"
|
||||
@click="emit('delete')"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
v-if="isEditingView && attribute.value"
|
||||
ghost
|
||||
ruby
|
||||
icon="i-lucide-trash"
|
||||
xs
|
||||
class="flex-shrink-0 !size-5 !rounded"
|
||||
@click="emit('delete')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -114,24 +114,24 @@ const handleInputUpdate = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center w-full min-w-0 gap-2"
|
||||
:class="{
|
||||
'justify-start': isEditingView,
|
||||
'justify-end': !isEditingView,
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center w-full min-w-0 gap-1.5 justify-end">
|
||||
<span
|
||||
v-if="!isEditingValue"
|
||||
class="min-w-0 text-sm"
|
||||
:class="{
|
||||
'cursor-pointer text-n-slate-11 hover:text-n-slate-12 py-2 select-none font-medium':
|
||||
!isEditingView,
|
||||
'text-n-slate-12 truncate': isEditingView && !isAttributeTypeLink,
|
||||
'truncate hover:text-n-brand text-n-blue-text':
|
||||
isEditingView && isAttributeTypeLink,
|
||||
v-tooltip.top="{
|
||||
content: !isAttributeTypeLink ? attribute.value : '',
|
||||
delay: { show: 1000, hide: 0 },
|
||||
}"
|
||||
@click="toggleEditValue(!isEditingView)"
|
||||
class="min-w-0 text-sm cursor-pointer font-420"
|
||||
:class="{
|
||||
'text-n-slate-11 hover:text-n-slate-12 py-2 select-none font-medium':
|
||||
!isEditingView,
|
||||
'text-n-slate-12 truncate':
|
||||
isEditingView && !isAttributeTypeLink && attribute.value,
|
||||
'text-n-slate-10 truncate': isEditingView && !attribute.value,
|
||||
'truncate hover:text-n-brand text-n-blue-10':
|
||||
isEditingView && isAttributeTypeLink && attribute.value,
|
||||
}"
|
||||
@click="toggleEditValue(true)"
|
||||
>
|
||||
<a
|
||||
v-if="isAttributeTypeLink && attribute.value && isEditingView"
|
||||
@@ -156,19 +156,20 @@ const handleInputUpdate = async () => {
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<Button
|
||||
variant="faded"
|
||||
color="slate"
|
||||
icon="i-lucide-pencil"
|
||||
size="xs"
|
||||
class="flex-shrink-0 opacity-0 group-hover/attribute:opacity-100 hover:no-underline"
|
||||
ghost
|
||||
slate
|
||||
icon="i-lucide-pen-line"
|
||||
xs
|
||||
class="flex-shrink-0 !size-5 !rounded"
|
||||
@click="toggleEditValue(true)"
|
||||
/>
|
||||
<Button
|
||||
variant="faded"
|
||||
color="ruby"
|
||||
v-if="attribute.value"
|
||||
ghost
|
||||
ruby
|
||||
icon="i-lucide-trash"
|
||||
size="xs"
|
||||
class="flex-shrink-0 opacity-0 group-hover/attribute:opacity-100 hover:no-underline"
|
||||
xs
|
||||
class="flex-shrink-0 !size-5 !rounded"
|
||||
@click="emit('delete')"
|
||||
/>
|
||||
</div>
|
||||
@@ -182,18 +183,19 @@ const handleInputUpdate = async () => {
|
||||
v-model="editedValue"
|
||||
:placeholder="t('CONTACTS_LAYOUT.SIDEBAR.ATTRIBUTES.TRIGGER.INPUT')"
|
||||
:type="getInputType"
|
||||
class="w-full [&>p]:absolute [&>p]:mt-0.5 [&>p]:top-8 ltr:[&>p]:left-0 rtl:[&>p]:right-0"
|
||||
class="w-full [&>p]:absolute [&>p]:mt-1 [&>p]:top-6 ltr:[&>p]:left-0 rtl:[&>p]:right-0"
|
||||
autofocus
|
||||
:message="attributeErrorMessage"
|
||||
:message-type="hasError ? 'error' : 'info'"
|
||||
custom-input-class="h-8 ltr:rounded-r-none rtl:rounded-l-none"
|
||||
@keyup.enter="handleInputUpdate"
|
||||
custom-input-class="h-7 py-1 ltr:rounded-r-none rtl:rounded-l-none"
|
||||
@enter="handleInputUpdate"
|
||||
/>
|
||||
<Button
|
||||
icon="i-lucide-check"
|
||||
:color="hasError ? 'ruby' : 'blue'"
|
||||
size="sm"
|
||||
class="flex-shrink-0 ltr:rounded-l-none rtl:rounded-r-none"
|
||||
type="button"
|
||||
class="flex-shrink-0 ltr:rounded-l-none rtl:rounded-r-none !h-7"
|
||||
@click="handleInputUpdate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -20,11 +20,11 @@ const handleButtonClick = button => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-2 border-b border-n-weak h-12"
|
||||
class="flex flex-col items-start gap-3 justify-between ltr:pl-4 ltr:pr-2 rtl:pl-2 rtl:pr-4 my-2 py-2"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2 flex-1">
|
||||
<span class="font-medium text-sm text-n-slate-12">{{ title }}</span>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center justify-between gap-2 flex-1 w-full">
|
||||
<span class="font-medium text-base text-n-slate-12">{{ title }}</span>
|
||||
<div class="flex items-center h-6">
|
||||
<Button
|
||||
v-for="button in buttons"
|
||||
:key="button.key"
|
||||
@@ -43,5 +43,6 @@ const handleButtonClick = button => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -121,8 +121,7 @@ const borderRadiusClass = computed(() => {
|
||||
if (props.size <= 16) return 'rounded'; // 4px
|
||||
if (props.size <= 24) return 'rounded-md'; // 6px
|
||||
if (props.size <= 32) return 'rounded-lg'; // 8px
|
||||
if (props.size <= 48) return 'rounded-xl'; // 12px
|
||||
return 'rounded-2xl'; // 16px
|
||||
return 'rounded-lg'; // 16px
|
||||
});
|
||||
|
||||
const avatarStyles = computed(() => ({
|
||||
@@ -280,7 +279,8 @@ watch(
|
||||
:handle-image-upload="handleImageUpload"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 z-10 flex items-center justify-center invisible w-full h-full transition-all duration-300 ease-in-out opacity-0 rounded-xl bg-n-alpha-black1 group-hover/avatar:visible group-hover/avatar:opacity-100"
|
||||
class="absolute inset-0 z-10 flex items-center justify-center invisible w-full h-full transition-all duration-300 ease-in-out opacity-0 bg-n-alpha-black1 group-hover/avatar:visible group-hover/avatar:opacity-100"
|
||||
:class="borderRadiusClass"
|
||||
@click="handleUploadAvatar"
|
||||
>
|
||||
<Icon
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
|
||||
const props = defineProps({
|
||||
usersList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 24,
|
||||
},
|
||||
showMoreCount: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
moreCountText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
gap: {
|
||||
type: String,
|
||||
default: 'normal',
|
||||
validator(value) {
|
||||
// The value must match one of these strings
|
||||
return ['normal', 'tight'].includes(value);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const gapClass = computed(() => {
|
||||
if (props.gap === 'tight') {
|
||||
return 'ltr:[&:not(:first-child)]:-ml-2 rtl:[&:not(:first-child)]:-mr-2';
|
||||
}
|
||||
return 'ltr:[&:not(:first-child)]:-ml-1 rtl:[&:not(:first-child)]:-mr-1';
|
||||
});
|
||||
|
||||
const morePlaceholderClass = computed(() => {
|
||||
if (props.gap === 'tight') {
|
||||
return 'ltr:-ml-2 rtl:-mr-2';
|
||||
}
|
||||
return 'ltr:-ml-1 rtl:-mr-1';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex">
|
||||
<Avatar
|
||||
v-for="user in usersList"
|
||||
:key="user.id"
|
||||
v-tooltip="user.name"
|
||||
:title="user.name"
|
||||
:src="user.thumbnail"
|
||||
:name="user.name"
|
||||
:size="size"
|
||||
class="[&>span]:outline [&>span]:outline-1 [&>span]:outline-n-background [&>span]:shadow"
|
||||
:class="gapClass"
|
||||
rounded-full
|
||||
/>
|
||||
<span
|
||||
v-if="showMoreCount"
|
||||
class="text-n-slate-11 bg-n-button-color outline outline-1 outline-n-weak text-xs font-420 rounded-full px-2 inline-flex items-center shadow relative"
|
||||
:class="morePlaceholderClass"
|
||||
>
|
||||
{{ moreCountText }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -255,7 +255,7 @@ const animationClasses = computed(() => {
|
||||
<Spinner v-if="isLoading" class="!w-5 !h-5 flex-shrink-0" />
|
||||
|
||||
<slot v-if="label || $slots.default" name="default">
|
||||
<span v-if="label" class="min-w-0 truncate">{{ label }}</span>
|
||||
<span v-if="label" class="min-w-0 font-420 truncate">{{ label }}</span>
|
||||
</slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -11,7 +11,13 @@ const props = defineProps({
|
||||
type: Array,
|
||||
default: () => [],
|
||||
validator: value => {
|
||||
return value.every(item => item.action && item.value && item.label);
|
||||
return value.every(
|
||||
item =>
|
||||
item.action &&
|
||||
item.value !== undefined &&
|
||||
item.value !== null &&
|
||||
item.label
|
||||
);
|
||||
},
|
||||
},
|
||||
menuSections: {
|
||||
@@ -22,6 +28,10 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 20,
|
||||
},
|
||||
roundedThumbnail: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -48,7 +58,7 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['action', 'search']);
|
||||
const emit = defineEmits(['action', 'search', 'empty']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -100,9 +110,13 @@ const filteredMenuSections = computed(() => {
|
||||
});
|
||||
|
||||
const handleSearchInput = event => {
|
||||
if (props.disableLocalFiltering) {
|
||||
emit('search', event.target.value);
|
||||
}
|
||||
emit('search', event.target.value);
|
||||
|
||||
const isEmpty = hasSections.value
|
||||
? filteredMenuSections.value.length === 0
|
||||
: filteredMenuItems.value.length === 0;
|
||||
|
||||
if (isEmpty) emit('empty');
|
||||
};
|
||||
|
||||
const handleAction = item => {
|
||||
@@ -194,22 +208,26 @@ onMounted(() => {
|
||||
:name="item.thumbnail.name"
|
||||
:src="item.thumbnail.src"
|
||||
:size="thumbnailSize"
|
||||
rounded-full
|
||||
:rounded-full="roundedThumbnail"
|
||||
/>
|
||||
</slot>
|
||||
<slot name="icon" :item="item">
|
||||
<Icon
|
||||
v-if="item.icon"
|
||||
:icon="item.icon"
|
||||
class="flex-shrink-0 size-3.5"
|
||||
/>
|
||||
</slot>
|
||||
<Icon
|
||||
v-if="item.icon"
|
||||
:icon="item.icon"
|
||||
class="flex-shrink-0 size-3.5"
|
||||
/>
|
||||
<span v-if="item.emoji" class="flex-shrink-0">{{ item.emoji }}</span>
|
||||
<span
|
||||
v-if="item.label"
|
||||
class="min-w-0 text-sm truncate"
|
||||
:class="labelClass"
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<slot name="label" :item="item">
|
||||
<span
|
||||
v-if="item.label"
|
||||
class="min-w-0 text-sm font-420 truncate"
|
||||
:class="labelClass"
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</slot>
|
||||
<slot name="trailing-icon" :item="item" />
|
||||
</button>
|
||||
<div
|
||||
@@ -241,22 +259,26 @@ onMounted(() => {
|
||||
:name="item.thumbnail.name"
|
||||
:src="item.thumbnail.src"
|
||||
:size="thumbnailSize"
|
||||
rounded-full
|
||||
:rounded-full="roundedThumbnail"
|
||||
/>
|
||||
</slot>
|
||||
<slot name="icon" :item="item">
|
||||
<Icon
|
||||
v-if="item.icon"
|
||||
:icon="item.icon"
|
||||
class="flex-shrink-0 size-3.5"
|
||||
/>
|
||||
</slot>
|
||||
<Icon
|
||||
v-if="item.icon"
|
||||
:icon="item.icon"
|
||||
class="flex-shrink-0 size-3.5"
|
||||
/>
|
||||
<span v-if="item.emoji" class="flex-shrink-0">{{ item.emoji }}</span>
|
||||
<span
|
||||
v-if="item.label"
|
||||
class="min-w-0 text-sm truncate"
|
||||
:class="labelClass"
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<slot name="label" :item="item">
|
||||
<span
|
||||
v-if="item.label"
|
||||
class="min-w-0 text-sm font-420 truncate"
|
||||
:class="labelClass"
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</slot>
|
||||
<slot name="trailing-icon" :item="item" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user