feat: Update contact list page (#13020)

This commit is contained in:
Sivin Varghese
2025-12-10 19:47:20 +05:30
committed by GitHub
parent ee82a63b35
commit 8d191a70b1
44 changed files with 1266 additions and 379 deletions
@@ -0,0 +1,67 @@
<script setup>
import { reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import Editor from 'dashboard/components-next/Editor/Editor.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
const props = defineProps({
contactId: {
type: Number,
default: null,
},
});
const { t } = useI18n();
const store = useStore();
const state = reactive({
message: '',
});
const onAdd = async content => {
if (!content) return;
try {
await store.dispatch('contactNotes/create', {
content,
contactId: props.contactId,
});
state.message = '';
useAlert(t('CONTACTS_LAYOUT.CARD.ADD_NOTE.API.SUCCESS_MESSAGE'));
} catch (error) {
useAlert(t('CONTACTS_LAYOUT.CARD.ADD_NOTE.API.ERROR_MESSAGE'));
}
};
const keyboardEvents = {
'$mod+Enter': {
action: () => onAdd(state.message),
allowOnFocusedInput: true,
},
};
useKeyboardEvents(keyboardEvents);
</script>
<template>
<div class="flex flex-col items-start gap-4 py-3">
<span
class="py-1 text-sm font-medium text-n-slate-12 z-10 h-6 bg-n-surface-1 inline-flex items-center gap-2"
>
<Icon
icon="i-lucide-file-plus-corner"
class="size-4 text-n-slate-11 hidden lg:block"
/>
{{ t('CONTACTS_LAYOUT.CARD.ADD_NOTE.TITLE') }}
</span>
<div class="flex flex-col gap-6 lg:px-6 max-w-lg w-full">
<Editor
v-model="state.message"
:placeholder="t('CONTACTS_LAYOUT.CARD.ADD_NOTE.PLACEHOLDER')"
class="[&>div]:!border-transparent [&>div]:!px-3 [&>div]:!pb-4 [&_.ProseMirror-woot-style]:min-h-6 [&_.ProseMirror-menubar]:!relative ltr:[&_.ProseMirror-menubar]:!-left-[3px] ltr:[&_.ProseMirror-menubar]:!right-[unset] rtl:[&_.ProseMirror-menubar]:!-right-[3px] rtl:[&_.ProseMirror-menubar]:!left-[unset] [&_.ProseMirror-menubar]:!w-[unset] [&_.ProseMirror-menubar]:!top-[unset] [&_.ProseMirror-menubar-spacer]:!hidden"
/>
</div>
</div>
</template>
@@ -0,0 +1,319 @@
<script setup>
import { computed, reactive, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { required, email as emailValidator } from '@vuelidate/validators';
import { useVuelidate } from '@vuelidate/core';
import { splitName } from '@chatwoot/utils';
import Input from 'dashboard/components-next/input/Input.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import PhoneNumberInput from 'dashboard/components-next/phonenumberinput/PhoneNumberInput.vue';
import CountryDropdown from 'dashboard/components-next/Countries/CountryDropdown.vue';
import CompaniesDropdown from 'dashboard/components-next/Companies/CompaniesDropdown.vue';
import AddContactNote from './AddContactNote.vue';
import ContactLabels from 'dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue';
const props = defineProps({
contactData: {
type: Object,
default: null,
},
});
const emit = defineEmits(['update']);
const { t } = useI18n();
const SOCIAL_CONFIG = {
LINKEDIN: 'i-woot-linkedin',
FACEBOOK: 'i-woot-facebook',
INSTAGRAM: 'i-woot-instagram',
TWITTER: 'i-woot-x',
GITHUB: 'i-woot-github',
};
const formState = reactive({
firstName: '',
lastName: '',
email: '',
phoneNumber: '',
city: '',
countryCode: '',
bio: '',
companyName: '',
companyId: null,
socialProfiles: {
facebook: '',
github: '',
instagram: '',
linkedin: '',
twitter: '',
},
});
const validationRules = {
firstName: { required },
email: { email: emailValidator },
};
const v$ = useVuelidate(validationRules, formState);
const isFormInvalid = computed(() => v$.value.$invalid);
const prepareStateBasedOnProps = () => {
const {
name = '',
email: emailAddress = '',
phoneNumber: phone = '',
additionalAttributes = {},
} = props.contactData || {};
const { firstName: fName, lastName: lName } = splitName(name || '');
const {
description = '',
countryCode: country = '',
city: cityName = '',
socialProfiles: profiles = {},
companyName: company = '',
} = additionalAttributes || {};
formState.firstName = fName;
formState.lastName = lName;
formState.email = emailAddress;
formState.phoneNumber = phone || '';
formState.city = cityName;
formState.countryCode = country;
formState.bio = description;
formState.companyName = company;
formState.socialProfiles = { ...formState.socialProfiles, ...profiles };
};
const socialProfilesForm = computed(() =>
Object.entries(SOCIAL_CONFIG).map(([key, icon]) => ({
key,
placeholder: t(`CONTACTS_LAYOUT.CARD.SOCIAL_MEDIA.FORM.${key}.PLACEHOLDER`),
icon,
}))
);
const handleCompanyChange = company => {
formState.companyName = company?.name || '';
};
const handleUpdate = async () => {
const isFormValid = await v$.value.$validate();
if (!isFormValid) return;
const contactData = {
name: `${formState.firstName} ${formState.lastName}`.trim(),
email: formState.email,
phoneNumber: formState.phoneNumber,
additionalAttributes: {
description: formState.bio,
city: formState.city,
countryCode: formState.countryCode,
companyName: formState.companyName,
socialProfiles: formState.socialProfiles,
},
};
emit('update', contactData);
};
const resetValidation = () => {
v$.value.$reset();
};
const resetForm = () => {
Object.assign(formState, {
firstName: '',
lastName: '',
email: '',
phoneNumber: '',
city: '',
countryCode: '',
bio: '',
companyName: '',
companyId: null,
socialProfiles: {
facebook: '',
github: '',
instagram: '',
linkedin: '',
twitter: '',
},
});
};
watch(
() => props.contactData?.id,
id => {
if (id) prepareStateBasedOnProps();
},
{ immediate: true }
);
defineExpose({
resetValidation,
isFormInvalid,
resetForm,
handleUpdate,
});
</script>
<template>
<div class="flex flex-col">
<AddContactNote :contact-id="contactData?.id" />
<div class="flex flex-col items-start gap-4 pt-2 pb-3">
<span
class="py-1 text-sm font-medium text-n-slate-12 z-10 h-6 bg-n-surface-1 inline-flex items-center gap-2"
>
<Icon
icon="i-lucide-settings-2"
class="size-4 text-n-slate-11 hidden lg:block"
/>
{{ t('CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.TITLE') }}
</span>
<div
class="lg:grid lg:grid-cols-[1fr_auto_1fr] flex flex-col items-start lg:items-center w-full gap-3 lg:px-6"
>
<div class="grid grid-cols-2 gap-2 min-w-0 w-full">
<Input
v-model="formState.firstName"
:placeholder="
t(
'CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.FORM.FIRST_NAME.PLACEHOLDER'
)
"
:message-type="v$.firstName.$error ? 'error' : 'info'"
class="h-8 min-w-0"
@input="v$.firstName.$touch()"
@blur="v$.firstName.$touch()"
/>
<Input
v-model="formState.lastName"
:placeholder="
t(
'CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.FORM.LAST_NAME.PLACEHOLDER'
)
"
class="h-8 min-w-0"
/>
</div>
<div class="w-px h-3 bg-n-strong hidden lg:block" />
<div class="grid grid-cols-3 gap-3 min-w-0 w-full">
<CompaniesDropdown
v-model="formState.companyId"
:placeholder="
t(
'CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.FORM.COMPANY_NAME.PLACEHOLDER'
)
"
:selected-company-name="formState.companyName"
class="min-w-0"
@change="handleCompanyChange"
/>
<Input
v-model="formState.city"
:placeholder="
t('CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.FORM.CITY.PLACEHOLDER')
"
class="h-8 min-w-0"
/>
<CountryDropdown
v-model="formState.countryCode"
:placeholder="
t(
'CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.FORM.COUNTRY.PLACEHOLDER'
)
"
class="min-w-0"
/>
</div>
</div>
<div
class="lg:grid lg:grid-cols-[1fr_auto_1fr] flex flex-col lg:items-center w-full gap-3 lg:px-6"
>
<div class="grid grid-cols-2 gap-2 min-w-0 w-full">
<Input
v-model="formState.email"
type="email"
:placeholder="
t(
'CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.FORM.EMAIL_ADDRESS.PLACEHOLDER'
)
"
:message-type="v$.email.$error ? 'error' : 'info'"
class="h-8 min-w-0 [&>input]:ltr:!pl-8 [&>input]:rtl:!pr-8"
@input="v$.email.$touch()"
@blur="v$.email.$touch()"
>
<template #prefix>
<Icon
icon="i-woot-mail"
class="absolute -translate-y-1/2 text-n-slate-11 size-4 top-1/2 ltr:left-2.5 rtl:right-2.5"
/>
</template>
</Input>
<PhoneNumberInput
v-model="formState.phoneNumber"
:placeholder="
t(
'CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.FORM.PHONE_NUMBER.PLACEHOLDER'
)
"
class="min-w-0"
/>
</div>
<div class="w-px h-3 bg-n-strong hidden lg:block" />
<Input
v-model="formState.bio"
:placeholder="
t('CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.FORM.BIO.PLACEHOLDER')
"
class="h-8 min-w-0"
/>
</div>
<div v-if="contactData?.id" class="lg:px-6">
<ContactLabels :contact-id="contactData.id" />
</div>
</div>
<div class="flex flex-col items-start gap-4 py-2">
<span
class="py-1 text-sm font-medium text-n-slate-12 z-10 h-6 bg-n-surface-1 inline-flex items-center gap-2"
>
<Icon
icon="i-lucide-globe"
class="size-4 text-n-slate-11 hidden lg:block"
/>
{{ t('CONTACTS_LAYOUT.CARD.SOCIAL_MEDIA.TITLE') }}
</span>
<div class="flex flex-wrap gap-2 lg:px-6">
<div
v-for="item in socialProfilesForm"
:key="item.key"
class="flex items-center h-8 gap-2 px-2 rounded-lg bg-n-alpha-2 outline-1 outline outline-n-weak -outline-offset-1 dark:bg-n-solid-2 hover:outline-n-slate-6 transition-all duration-150"
>
<Icon
:icon="item.icon"
class="flex-shrink-0 text-n-slate-11 size-5"
/>
<input
v-model="formState.socialProfiles[item.key.toLowerCase()]"
class="w-auto min-w-[100px] text-sm bg-transparent outline-none reset-base text-n-slate-12 dark:text-n-slate-12 placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10"
:placeholder="item.placeholder"
:size="item.placeholder.length"
/>
</div>
</div>
</div>
</div>
</template>
@@ -1,68 +0,0 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useToggle } from '@vueuse/core';
import Button from 'dashboard/components-next/button/Button.vue';
import ConfirmContactDeleteDialog from 'dashboard/components-next/Contacts/ContactsForm/ConfirmContactDeleteDialog.vue';
import Policy from 'dashboard/components/policy.vue';
defineProps({
selectedContact: {
type: Object,
required: true,
},
});
const { t } = useI18n();
const [showDeleteSection, toggleDeleteSection] = useToggle();
const confirmDeleteContactDialogRef = ref(null);
const openConfirmDeleteContactDialog = () => {
confirmDeleteContactDialogRef.value?.dialogRef.open();
};
</script>
<template>
<Policy :permissions="['administrator']">
<div class="flex flex-col items-start border-t border-n-strong px-6 py-5">
<Button
:label="t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT')"
sm
link
slate
class="hover:!no-underline text-n-slate-12"
icon="i-lucide-chevron-down"
trailing-icon
@click="toggleDeleteSection()"
/>
<div
class="transition-all duration-300 ease-in-out grid w-full overflow-hidden"
:class="
showDeleteSection
? 'grid-rows-[1fr] opacity-100 mt-2'
: 'grid-rows-[0fr] opacity-0 mt-0'
"
>
<div class="overflow-hidden min-h-0">
<span class="inline-flex text-n-slate-11 text-sm items-center gap-1">
{{ t('CONTACTS_LAYOUT.CARD.DELETE_CONTACT.MESSAGE') }}
<Button
:label="t('CONTACTS_LAYOUT.CARD.DELETE_CONTACT.BUTTON')"
sm
ruby
link
@click="openConfirmDeleteContactDialog()"
/>
</span>
</div>
</div>
</div>
<ConfirmContactDeleteDialog
ref="confirmDeleteContactDialogRef"
:selected-contact="selectedContact"
/>
</Policy>
</template>
@@ -2,19 +2,21 @@
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import CardLayout from 'dashboard/components-next/CardLayout.vue';
import ContactsForm from 'dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue';
import ContactCardForm from 'dashboard/components-next/Contacts/ContactsCard/ContactCardForm.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import Flag from 'dashboard/components-next/flag/Flag.vue';
import ContactDeleteSection from 'dashboard/components-next/Contacts/ContactsCard/ContactDeleteSection.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import ContactLabels from 'dashboard/components-next/Conversation/ConversationCard/CardLabels.vue';
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
import Policy from 'dashboard/components/policy.vue';
import countries from 'shared/constants/countries';
const props = defineProps({
id: { type: Number, required: true },
name: { type: String, default: '' },
email: { type: String, default: '' },
labels: { type: Array, default: () => [] },
additionalAttributes: { type: Object, default: () => ({}) },
phoneNumber: { type: String, default: '' },
thumbnail: { type: String, default: '' },
@@ -31,11 +33,13 @@ const emit = defineEmits([
'showContact',
'select',
'avatarHover',
'sendMessage',
'deleteContact',
]);
const { t } = useI18n();
const contactsFormRef = ref(null);
const contactCardFormRef = ref(null);
const getInitialContactData = () => ({
id: props.id,
@@ -47,7 +51,7 @@ const getInitialContactData = () => ({
const contactData = ref(getInitialContactData());
const isFormInvalid = computed(() => contactsFormRef.value?.isFormInvalid);
const isFormInvalid = computed(() => contactCardFormRef.value?.isFormInvalid);
const countriesMap = computed(() => {
return countries.reduce((acc, country) => {
@@ -75,6 +79,11 @@ const countryDetails = computed(() => {
};
});
const companyName = computed(() => {
const attributes = props.additionalAttributes || {};
return attributes.companyName || '';
});
const formattedLocation = computed(() => {
if (!countryDetails.value) return '';
@@ -85,10 +94,11 @@ const formattedLocation = computed(() => {
const handleFormUpdate = updatedData => {
Object.assign(contactData.value, updatedData);
emit('updateContact', contactData.value);
};
const handleUpdateContact = () => {
emit('updateContact', contactData.value);
contactCardFormRef.value?.handleUpdate();
};
const onClickExpand = () => {
@@ -98,6 +108,12 @@ const onClickExpand = () => {
const onClickViewDetails = () => emit('showContact', props.id);
const onClickOpenSendMessage = () => emit('sendMessage', props.id);
const onClickDeleteContact = () => {
emit('deleteContact', props.id);
};
const toggleSelect = checked => {
emit('select', checked);
};
@@ -109,30 +125,44 @@ const handleAvatarHover = isHovered => {
<template>
<div class="relative">
<CardLayout
:key="id"
layout="row"
:class="{
'outline-n-weak !bg-n-slate-3 dark:!bg-n-solid-3': isSelected,
}"
<div
class="flex flex-col gap-2 pt-3 pb-2 lg:pb-3 lg:grid lg:gap-4 lg:items-center lg:rounded-lg lg:transition-all lg:duration-200 lg:grid-cols-[minmax(42%,1fr)_minmax(0,1fr)_minmax(0,1fr)]"
:class="{ 'border-b border-n-weak lg:border-none': isExpanded }"
>
<div class="flex items-center justify-start flex-1 gap-4">
<div
class="flex items-center gap-3 lg:gap-2"
:class="{ 'lg:col-span-3': isExpanded }"
>
<div
class="relative"
class="relative hidden lg:block size-5 rounded-md hover:bg-n-alpha-2 flex-shrink-0"
>
<Button
icon="i-lucide-chevron-down"
link
slate
sm
no-animation
class="flex-shrink-0 !size-8 absolute -inset-1.5"
:class="{ 'rotate-180': isExpanded }"
@click="onClickExpand"
/>
</div>
<div
class="flex-shrink-0 flex items-center"
@mouseenter="handleAvatarHover(true)"
@mouseleave="handleAvatarHover(false)"
>
<Avatar
:name="name"
:src="thumbnail"
:size="48"
:size="24"
:status="availabilityStatus"
hide-offline-status
rounded-full
>
<template v-if="selectable" #overlay="{ size }">
<label
class="flex items-center justify-center rounded-full cursor-pointer absolute inset-0 z-10 backdrop-blur-[2px] border border-n-weak"
class="flex items-center justify-center rounded-md cursor-pointer absolute inset-0 z-10 backdrop-blur-[2px]"
:style="{ width: `${size}px`, height: `${size}px` }"
@click.stop
>
@@ -144,101 +174,215 @@ const handleAvatarHover = isHovered => {
</template>
</Avatar>
</div>
<div class="flex flex-col gap-0.5 flex-1">
<div class="flex flex-wrap items-center gap-x-4 gap-y-1">
<span class="text-base font-medium truncate text-n-slate-12">
{{ name }}
</span>
<span class="inline-flex items-center gap-1">
<span
v-if="additionalAttributes?.companyName"
class="i-ph-building-light size-4 text-n-slate-10 mb-0.5"
/>
<span
v-if="additionalAttributes?.companyName"
class="text-sm truncate text-n-slate-11"
>
{{ additionalAttributes.companyName }}
</span>
</span>
</div>
<h4
class="text-sm my-0 capitalize hover:cursor-pointer truncate text-n-slate-12 font-medium lg:max-w-40"
@click="onClickViewDetails"
>
{{ name }}
</h4>
<div
v-if="isExpanded"
class="hidden lg:flex items-center gap-2 flex-shrink-0"
>
<div
class="flex flex-wrap items-center justify-start gap-x-3 gap-y-1"
class="w-px h-3 bg-n-strong rounded-md ltr:ml-1 rtl:mr-1 flex-shrink-0"
/>
<Button
:label="t('CONTACTS_LAYOUT.CARD.ACTIONS.SEND_MESSAGE')"
icon="i-lucide-message-circle"
link
sm
class="hover:!no-underline"
@click="onClickOpenSendMessage"
/>
<div
class="w-px h-3 bg-n-strong rounded-md ltr:ml-1 rtl:mr-1 flex-shrink-0"
/>
<Button
:label="t('CONTACTS_LAYOUT.CARD.ACTIONS.VIEW_DETAILS')"
icon="i-lucide-info"
link
sm
class="hover:!no-underline"
@click="onClickViewDetails"
/>
<Policy
:permissions="['administrator']"
class="flex items-center gap-2"
>
<div v-if="email" class="truncate max-w-72" :title="email">
<span class="text-sm text-n-slate-11">
{{ email }}
</span>
</div>
<div v-if="email" class="w-px h-3 truncate bg-n-slate-6" />
<span v-if="phoneNumber" class="text-sm truncate text-n-slate-11">
{{ phoneNumber }}
</span>
<div v-if="phoneNumber" class="w-px h-3 truncate bg-n-slate-6" />
<span
v-if="countryDetails"
class="inline-flex items-center gap-2 text-sm truncate text-n-slate-11"
>
<Flag :country="countryDetails.countryCode" class="size-3.5" />
{{ formattedLocation }}
</span>
<div v-if="countryDetails" class="w-px h-3 truncate bg-n-slate-6" />
<div
class="w-px h-3 bg-n-strong rounded-md ltr:ml-1 rtl:mr-1 flex-shrink-0"
/>
<Button
:label="t('CONTACTS_LAYOUT.CARD.VIEW_DETAILS')"
variant="link"
size="xs"
@click="onClickViewDetails"
:label="t('CONTACTS_LAYOUT.CARD.ACTIONS.DELETE_CONTACT')"
icon="i-lucide-trash-2"
link
sm
ruby
class="hover:!no-underline"
@click="onClickDeleteContact"
/>
</Policy>
</div>
<div
v-if="companyName"
class="text-xs my-0 capitalize h-6 px-1 inline-flex items-center gap-1 rounded-md text-n-slate-12 font-440 max-w-40 min-w-0 outline outline-1 outline-n-weak"
:class="{ 'lg:hidden': isExpanded }"
>
<Icon
icon="i-lucide-briefcase-business"
class="size-3.5 flex-shrink-0 text-n-slate-11"
/>
<span class="truncate">{{ companyName }}</span>
</div>
<div
v-if="countryDetails"
class="w-px h-3 bg-n-strong rounded-lg flex-shrink-0"
:class="{ 'lg:hidden': isExpanded }"
/>
<span
v-if="countryDetails"
class="inline-flex items-center gap-2 text-sm min-w-0 text-n-slate-11"
:class="{ 'lg:hidden': isExpanded }"
>
<Flag
:country="countryDetails.countryCode"
class="size-3.5 flex-shrink-0"
/>
<span class="truncate">{{ formattedLocation }}</span>
</span>
<div
class="relative lg:hidden size-5 rounded-md hover:bg-n-alpha-2 ltr:ml-auto rtl:mr-auto flex-shrink-0"
>
<Button
icon="i-lucide-chevron-down"
link
slate
sm
no-animation
class="flex-shrink-0 !size-8 absolute -inset-1.5"
:class="{ 'rotate-180': isExpanded }"
@click="onClickExpand"
/>
</div>
</div>
<div
class="flex items-center gap-2 lg:gap-2 lg:min-w-0"
:class="{ 'lg:hidden': isExpanded }"
>
<div
v-if="email"
v-tooltip.top="{
content: email,
delay: { show: 500, hide: 0 },
}"
class="flex items-center gap-1 truncate lg:max-w-72"
>
<Icon
icon="i-woot-mail"
class="size-4 flex-shrink-0 text-n-slate-11"
/>
<span class="text-sm text-n-slate-12 font-420 truncate">
{{ email }}
</span>
</div>
<div
v-if="email && phoneNumber"
class="w-px h-3 bg-n-strong rounded-lg flex-shrink-0"
/>
<div
v-if="phoneNumber"
v-tooltip.top="{
content: phoneNumber,
delay: { show: 500, hide: 0 },
}"
class="flex items-center gap-1 truncate lg:max-w-72"
>
<Icon
icon="i-lucide-phone"
class="size-3 flex-shrink-0 text-n-slate-11"
/>
<span class="text-sm text-n-slate-12 font-420 truncate">
{{ phoneNumber }}
</span>
</div>
</div>
<div class="flex flex-col gap-2 lg:hidden">
<div v-if="labels.length > 0" class="min-h-[1rem]">
<ContactLabels :labels="labels" disable-toggle class="my-0" />
</div>
<Button
:label="t('CONTACTS_LAYOUT.CARD.VIEW_DETAILS')"
link
xs
class="w-fit hover:!no-underline"
slate
@click="onClickViewDetails"
/>
</div>
<div
class="hidden lg:flex items-center justify-end gap-3 flex-shrink-0"
:class="{ 'lg:hidden': isExpanded }"
>
<ContactLabels
:labels="labels"
disable-toggle
class="my-0 flex-1 justify-end"
/>
<Button
:label="t('CONTACTS_LAYOUT.CARD.VIEW_DETAILS')"
link
xs
slate
class="flex-shrink-0 hover:!no-underline"
@click="onClickViewDetails"
/>
</div>
</div>
<div
class="grid [transition:grid-template-rows_300ms_ease-out]"
:class="[
isExpanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
isExpanded ? '' : 'overflow-hidden',
]"
>
<div class="min-h-0">
<div
class="relative flex flex-col lg:pt-3 pb-3 overflow-visible transition-opacity duration-[600ms] ease-out"
:class="isExpanded ? 'opacity-100 delay-200' : 'opacity-0'"
>
<ContactCardForm
ref="contactCardFormRef"
:contact-data="contactData"
class="lg:after:content-[''] lg:after:absolute lg:after:ltr:left-2 lg:after:rtl:right-2 lg:after:top-0 lg:after:w-px lg:after:bg-n-strong lg:after:bottom-11"
@update="handleFormUpdate"
/>
<div
class="relative lg:ltr:pl-6 lg:rtl:pr-6 mt-6 mb-4 lg:before:block lg:before:content-[''] lg:before:absolute lg:ltr:before:left-2 lg:rtl:before:right-2 lg:before:top-1/2 lg:before:w-2 lg:before:h-px lg:before:bg-n-strong"
>
<Button
:label="t('CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.UPDATE_BUTTON')"
size="sm"
:is-loading="isUpdating"
:disabled="isUpdating || isFormInvalid"
@click="handleUpdateContact"
/>
</div>
</div>
</div>
<Button
icon="i-lucide-chevron-down"
variant="ghost"
color="slate"
size="xs"
:class="{ 'rotate-180': isExpanded }"
@click="onClickExpand"
/>
<template #after>
<div
class="transition-all duration-500 ease-in-out grid overflow-hidden"
:class="
isExpanded
? 'grid-rows-[1fr] opacity-100'
: 'grid-rows-[0fr] opacity-0'
"
>
<div class="overflow-hidden">
<div class="flex flex-col gap-6 p-6 border-t border-n-strong">
<ContactsForm
ref="contactsFormRef"
:contact-data="contactData"
@update="handleFormUpdate"
/>
<div>
<Button
:label="
t('CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.UPDATE_BUTTON')
"
size="sm"
:is-loading="isUpdating"
:disabled="isUpdating || isFormInvalid"
@click="handleUpdateContact"
/>
</div>
</div>
<ContactDeleteSection
:selected-contact="{
id: props.id,
name: props.name,
}"
/>
</div>
</div>
</template>
</CardLayout>
</div>
</div>
</template>