feat: Updates on new components (#10444)

This commit is contained in:
Sivin Varghese
2024-11-20 20:21:35 +05:30
committed by GitHub
parent 76a4140224
commit b0d6089bb6
33 changed files with 684 additions and 703 deletions

View File

@@ -82,9 +82,8 @@ const inboxIcon = computed(() => {
</script> </script>
<template> <template>
<CardLayout class="flex flex-row justify-between flex-1 gap-8" layout="row"> <CardLayout layout="row">
<template #header> <div class="flex flex-col items-start justify-between flex-1 min-w-0 gap-2">
<div class="flex flex-col items-start gap-2">
<div class="flex justify-between gap-3 w-fit"> <div class="flex justify-between gap-3 w-fit">
<span <span
class="text-base font-medium capitalize text-n-slate-12 line-clamp-1" class="text-base font-medium capitalize text-n-slate-12 line-clamp-1"
@@ -117,8 +116,6 @@ const inboxIcon = computed(() => {
/> />
</div> </div>
</div> </div>
</template>
<template #footer>
<div class="flex items-center justify-end w-20 gap-2"> <div class="flex items-center justify-end w-20 gap-2">
<Button <Button
v-if="isLiveChatType" v-if="isLiveChatType"
@@ -136,6 +133,5 @@ const inboxIcon = computed(() => {
@click="emit('delete')" @click="emit('delete')"
/> />
</div> </div>
</template>
</CardLayout> </CardLayout>
</template> </template>

View File

@@ -1,7 +1,8 @@
<script setup> <script setup>
import Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue';
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue'; import Icon from 'dashboard/components-next/icon/Icon.vue';
const props = defineProps({ const props = defineProps({
@@ -33,10 +34,11 @@ const senderThumbnailSrc = computed(() => props.sender?.thumbnail);
{{ t('CAMPAIGN.LIVE_CHAT.CARD.CAMPAIGN_DETAILS.SENT_BY') }} {{ t('CAMPAIGN.LIVE_CHAT.CARD.CAMPAIGN_DETAILS.SENT_BY') }}
</span> </span>
<div class="flex items-center gap-1.5 flex-shrink-0"> <div class="flex items-center gap-1.5 flex-shrink-0">
<Thumbnail <Avatar
:author="sender || { name: senderName }"
:name="senderName" :name="senderName"
:src="senderThumbnailSrc" :src="senderThumbnailSrc"
:size="16"
rounded-full
/> />
<span class="text-sm font-medium text-n-slate-12"> <span class="text-sm font-medium text-n-slate-12">
{{ senderName }} {{ senderName }}

View File

@@ -27,7 +27,6 @@ defineProps({
:message="campaign.message" :message="campaign.message"
:is-enabled="campaign.enabled" :is-enabled="campaign.enabled"
:status="campaign.campaign_status" :status="campaign.campaign_status"
:trigger-rules="campaign.trigger_rules"
:sender="campaign.sender" :sender="campaign.sender"
:inbox="campaign.inbox" :inbox="campaign.inbox"
:scheduled-at="campaign.scheduled_at" :scheduled-at="campaign.scheduled_at"

View File

@@ -27,7 +27,6 @@ defineProps({
:message="campaign.message" :message="campaign.message"
:is-enabled="campaign.enabled" :is-enabled="campaign.enabled"
:status="campaign.campaign_status" :status="campaign.campaign_status"
:trigger-rules="campaign.trigger_rules"
:sender="campaign.sender" :sender="campaign.sender"
:inbox="campaign.inbox" :inbox="campaign.inbox"
:scheduled-at="campaign.scheduled_at" :scheduled-at="campaign.scheduled_at"

View File

@@ -27,7 +27,6 @@ const handleDelete = campaign => emit('delete', campaign);
:message="campaign.message" :message="campaign.message"
:is-enabled="campaign.enabled" :is-enabled="campaign.enabled"
:status="campaign.campaign_status" :status="campaign.campaign_status"
:trigger-rules="campaign.trigger_rules"
:sender="campaign.sender" :sender="campaign.sender"
:inbox="campaign.inbox" :inbox="campaign.inbox"
:scheduled-at="campaign.scheduled_at" :scheduled-at="campaign.scheduled_at"

View File

@@ -63,7 +63,6 @@ defineExpose({ dialogRef });
overflow-y-auto overflow-y-auto
@confirm="handleSubmit" @confirm="handleSubmit"
> >
<template #form>
<LiveChatCampaignForm <LiveChatCampaignForm
ref="liveChatCampaignFormRef" ref="liveChatCampaignFormRef"
mode="edit" mode="edit"
@@ -71,6 +70,5 @@ defineExpose({ dialogRef });
:show-action-buttons="false" :show-action-buttons="false"
@submit="handleSubmit" @submit="handleSubmit"
/> />
</template>
</Dialog> </Dialog>
</template> </template>

View File

@@ -1,11 +1,13 @@
<script setup> <script setup>
const props = defineProps({ defineProps({
layout: { layout: {
type: String, type: String,
default: 'col', default: 'col',
}, },
}); });
const emit = defineEmits(['click']); const emit = defineEmits(['click']);
const handleClick = () => { const handleClick = () => {
emit('click'); emit('click');
}; };
@@ -13,11 +15,18 @@ const handleClick = () => {
<template> <template>
<div <div
class="relative flex w-full gap-3 px-6 py-5 shadow outline-1 outline outline-n-container group/cardLayout rounded-2xl bg-n-solid-2" class="flex flex-col w-full shadow outline-1 outline outline-n-container group/cardLayout rounded-2xl bg-n-solid-2"
:class="props.layout === 'col' ? 'flex-col' : 'flex-row'" >
<div
class="flex w-full gap-3 px-6 py-5"
:class="
layout === 'col' ? 'flex-col' : 'flex-row justify-between items-center'
"
@click="handleClick" @click="handleClick"
> >
<slot name="header" /> <slot />
<slot name="footer" /> </div>
<slot name="after" />
</div> </div>
</template> </template>

View File

@@ -13,7 +13,7 @@ import Icon from 'dashboard/components-next/icon/Icon.vue';
import CardLayout from 'dashboard/components-next/CardLayout.vue'; import CardLayout from 'dashboard/components-next/CardLayout.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue'; import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
import Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
const props = defineProps({ const props = defineProps({
id: { id: {
@@ -101,7 +101,7 @@ const categoryName = computed(() => {
}); });
const authorName = computed(() => { const authorName = computed(() => {
return props.author?.name || props.author?.availableName || '-'; return props.author?.name || props.author?.availableName || '';
}); });
const authorThumbnailSrc = computed(() => { const authorThumbnailSrc = computed(() => {
@@ -124,8 +124,7 @@ const handleClick = id => {
<template> <template>
<CardLayout> <CardLayout>
<template #header> <div class="flex justify-between w-full gap-1">
<div class="flex justify-between 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-text text-n-slate-12 line-clamp-1"
@click="handleClick(id)" @click="handleClick(id)"
@@ -159,18 +158,17 @@ const handleClick = id => {
</div> </div>
</div> </div>
</div> </div>
</template> <div class="flex items-center justify-between w-full gap-4">
<template #footer>
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Thumbnail <Avatar
:author="author"
:name="authorName" :name="authorName"
:src="authorThumbnailSrc" :src="authorThumbnailSrc"
:size="16"
rounded-full
/> />
<span class="text-sm text-n-slate-11"> <span class="text-sm truncate text-n-slate-11">
{{ authorName }} {{ authorName || '-' }}
</span> </span>
</div> </div>
<span class="block text-sm whitespace-nowrap text-n-slate-11"> <span class="block text-sm whitespace-nowrap text-n-slate-11">
@@ -193,6 +191,5 @@ const handleClick = id => {
{{ lastUpdatedAt }} {{ lastUpdatedAt }}
</span> </span>
</div> </div>
</template>
</CardLayout> </CardLayout>
</template> </template>

View File

@@ -79,8 +79,7 @@ const handleAction = ({ action, value }) => {
<template> <template>
<CardLayout> <CardLayout>
<template #header> <div class="flex w-full gap-2">
<div class="flex gap-2">
<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
@@ -120,8 +119,6 @@ const handleAction = ({ action, value }) => {
</div> </div>
</div> </div>
</div> </div>
</template>
<template #footer>
<span <span
class="text-sm line-clamp-3" class="text-sm line-clamp-3"
:class=" :class="
@@ -132,6 +129,5 @@ const handleAction = ({ action, value }) => {
> >
{{ description }} {{ description }}
</span> </span>
</template>
</CardLayout> </CardLayout>
</template> </template>

View File

@@ -53,7 +53,6 @@ const handleAction = ({ action, value }) => {
<template> <template>
<CardLayout> <CardLayout>
<template #header>
<div class="flex justify-between gap-2"> <div class="flex justify-between gap-2">
<div class="flex items-center justify-start gap-2"> <div class="flex items-center justify-start gap-2">
<span <span
@@ -113,6 +112,5 @@ const handleAction = ({ action, value }) => {
</div> </div>
</div> </div>
</div> </div>
</template>
</CardLayout> </CardLayout>
</template> </template>

View File

@@ -6,7 +6,7 @@ import { OnClickOutside } from '@vueuse/components';
import { useMapGetter } from 'dashboard/composables/store'; import { useMapGetter } from 'dashboard/composables/store';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
import Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue'; import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
import ArticleEditorProperties from 'dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorProperties.vue'; import ArticleEditorProperties from 'dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorProperties.vue';
@@ -50,7 +50,7 @@ const author = computed(() => {
}); });
const authorName = computed( const authorName = computed(
() => author.value?.name || author.value?.available_name || '-' () => author.value?.name || author.value?.available_name || ''
); );
const authorThumbnailSrc = computed(() => author.value?.thumbnail); const authorThumbnailSrc = computed(() => author.value?.thumbnail);
@@ -186,17 +186,14 @@ onMounted(() => {
text-variant="info" text-variant="info"
@click="openAgentsList = !openAgentsList" @click="openAgentsList = !openAgentsList"
> >
<Thumbnail <Avatar
:author="author"
:name="authorName" :name="authorName"
:size="20"
:src="authorThumbnailSrc" :src="authorThumbnailSrc"
:size="20"
rounded-full
/> />
<span <span class="text-sm text-n-slate-12 hover:text-n-slate-11">
v-if="author" {{ authorName || '-' }}
class="text-sm text-n-slate-12 hover:text-n-slate-11"
>
{{ author.available_name }}
</span> </span>
</Button> </Button>
<DropdownMenu <DropdownMenu

View File

@@ -97,7 +97,6 @@ defineExpose({ dialogRef });
:disable-confirm-button="isUpdatingCategory || isInvalidForm" :disable-confirm-button="isUpdatingCategory || isInvalidForm"
@confirm="onUpdateCategory" @confirm="onUpdateCategory"
> >
<template #form>
<CategoryForm <CategoryForm
ref="categoryFormRef" ref="categoryFormRef"
mode="edit" mode="edit"
@@ -107,6 +106,5 @@ defineExpose({ dialogRef });
:active-locale-name="activeLocaleName" :active-locale-name="activeLocaleName"
:show-action-buttons="false" :show-action-buttons="false"
/> />
</template>
</Dialog> </Dialog>
</template> </template>

View File

@@ -85,7 +85,6 @@ defineExpose({ dialogRef });
:description="t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.DESCRIPTION')" :description="t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.DESCRIPTION')"
@confirm="onCreate" @confirm="onCreate"
> >
<template #form>
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<ComboBox <ComboBox
v-model="selectedLocale" v-model="selectedLocale"
@@ -96,6 +95,5 @@ defineExpose({ dialogRef });
class="[&>div>button]:!border-n-slate-5 [&>div>button]:dark:!border-n-slate-5" class="[&>div>button]:!border-n-slate-5 [&>div>button]:dark:!border-n-slate-5"
/> />
</div> </div>
</template>
</Dialog> </Dialog>
</template> </template>

View File

@@ -55,7 +55,6 @@ defineExpose({ dialogRef });
" "
@confirm="handleDialogConfirm" @confirm="handleDialogConfirm"
> >
<template #form>
<Input <Input
v-model="formState.customDomain" v-model="formState.customDomain"
:label=" :label="
@@ -69,6 +68,5 @@ defineExpose({ dialogRef });
) )
" "
/> />
</template>
</Dialog> </Dialog>
</template> </template>

View File

@@ -59,7 +59,7 @@ defineExpose({ dialogRef });
}} }}
</p> </p>
</template> </template>
<template #form>
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<span <span
class="h-10 px-3 py-2.5 text-sm select-none bg-transparent border rounded-lg text-n-slate-11 border-n-strong" class="h-10 px-3 py-2.5 text-sm select-none bg-transparent border rounded-lg text-n-slate-11 border-n-strong"
@@ -74,6 +74,5 @@ defineExpose({ dialogRef });
}} }}
</p> </p>
</div> </div>
</template>
</Dialog> </Dialog>
</template> </template>

View File

@@ -12,7 +12,7 @@ import { shouldBeUrl } from 'shared/helpers/Validators';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
import Input from 'dashboard/components-next/input/Input.vue'; import Input from 'dashboard/components-next/input/Input.vue';
import EditableAvatar from 'dashboard/components-next/avatar/EditableAvatar.vue'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue'; import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
import ColorPicker from 'dashboard/components-next/colorpicker/ColorPicker.vue'; import ColorPicker from 'dashboard/components-next/colorpicker/ColorPicker.vue';
@@ -187,10 +187,12 @@ const handleAvatarDelete = () => {
<label class="mb-0.5 text-sm font-medium text-gray-900 dark:text-gray-50"> <label class="mb-0.5 text-sm font-medium text-gray-900 dark:text-gray-50">
{{ t('HELP_CENTER.PORTAL_SETTINGS.FORM.AVATAR.LABEL') }} {{ t('HELP_CENTER.PORTAL_SETTINGS.FORM.AVATAR.LABEL') }}
</label> </label>
<EditableAvatar <Avatar
label="Avatar"
:src="state.logoUrl" :src="state.logoUrl"
:name="state.name" :name="state.name"
:size="72"
allow-upload
icon-name="i-lucide-building-2"
@upload="handleAvatarUpload" @upload="handleAvatarUpload"
@delete="handleAvatarDelete" @delete="handleAvatarDelete"
/> />

View File

@@ -120,7 +120,6 @@ defineExpose({ dialogRef });
:is-loading="isCreatingPortal" :is-loading="isCreatingPortal"
@confirm="handleDialogConfirm" @confirm="handleDialogConfirm"
> >
<template #form>
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<Input <Input
id="portal-name" id="portal-name"
@@ -143,6 +142,5 @@ defineExpose({ dialogRef });
:message="slugError || buildPortalURL(state.slug)" :message="slugError || buildPortalURL(state.slug)"
/> />
</div> </div>
</template>
</Dialog> </Dialog>
</template> </template>

View File

@@ -6,7 +6,7 @@ import { useMapGetter, useStore } from 'dashboard/composables/store.js';
import { buildPortalURL } from 'dashboard/helper/portalHelper'; import { buildPortalURL } from 'dashboard/helper/portalHelper';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
import Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
const emit = defineEmits(['close', 'createPortal']); const emit = defineEmits(['close', 'createPortal']);
@@ -148,14 +148,13 @@ const redirectToPortalHomePage = () => {
<span class="text-sm font-medium truncate text-n-slate-12"> <span class="text-sm font-medium truncate text-n-slate-12">
{{ portal.name || '' }} {{ portal.name || '' }}
</span> </span>
<Thumbnail <Avatar
v-if="portal" v-if="portal"
:author="portal"
:name="portal.name" :name="portal.name"
:size="20"
:src="getPortalThumbnailSrc(portal)" :src="getPortalThumbnailSrc(portal)"
:show-author-name="false" :size="20"
icon-name="i-lucide-building-2" icon-name="i-lucide-building-2"
rounded-full
/> />
</Button> </Button>
</div> </div>

View File

@@ -5,37 +5,30 @@ import Avatar from './Avatar.vue';
<template> <template>
<Story title="Components/Avatar" :layout="{ type: 'grid', width: '400' }"> <Story title="Components/Avatar" :layout="{ type: 'grid', width: '400' }">
<Variant title="Default"> <Variant title="Default">
<div class="p-4 bg-white dark:bg-slate-900"> <div class="flex p-4 space-x-4 bg-white dark:bg-slate-900">
<Avatar <Avatar
name=""
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Amaya" src="https://api.dicebear.com/9.x/thumbs/svg?seed=Amaya"
class="bg-ruby-300 dark:bg-ruby-900"
/> />
<Avatar name="Amaya" src="" />
<Avatar name="" src="" />
</div> </div>
</Variant> </Variant>
<Variant title="Default with upload"> <Variant title="Different Shapes">
<div class="p-4 bg-white dark:bg-slate-900"> <div class="gap-4 p-4 bg-white dark:bg-slate-900">
<Avatar
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Amaya"
class="bg-ruby-300 dark:bg-ruby-900"
allow-upload
/>
</div>
</Variant>
<Variant title="Invalid or empty SRC">
<div class="p-4 space-x-4 bg-white dark:bg-slate-900">
<Avatar src="https://example.com/ruby.png" name="Ruby" allow-upload />
<Avatar name="Bruce Wayne" allow-upload />
</div>
</Variant>
<Variant title="Rounded Full">
<div class="p-4 space-x-4 bg-white dark:bg-slate-900">
<Avatar <Avatar
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Amaya" src="https://api.dicebear.com/9.x/thumbs/svg?seed=Amaya"
name=""
allow-upload allow-upload
rounded-full rounded-full
:size="48"
/>
<Avatar
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Amaya"
name=""
allow-upload
:size="48"
/> />
</div> </div>
</Variant> </Variant>
@@ -43,24 +36,78 @@ import Avatar from './Avatar.vue';
<Variant title="Different Sizes"> <Variant title="Different Sizes">
<div class="flex flex-wrap gap-4 p-4 bg-white dark:bg-slate-900"> <div class="flex flex-wrap gap-4 p-4 bg-white dark:bg-slate-900">
<Avatar <Avatar
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Felix" src="https://api.dicebear.com/9.x/avataaars/svg?seed=Felix"
:size="48" :size="48"
class="bg-green-300 dark:bg-green-900" name=""
allow-upload allow-upload
/> />
<Avatar <Avatar
:size="72" :size="72"
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Jade" src="https://api.dicebear.com/9.x/avataaars/svg?seed=Jade"
class="bg-indigo-300 dark:bg-indigo-900" name=""
allow-upload allow-upload
/> />
<Avatar <Avatar
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Emery" src="https://api.dicebear.com/9.x/avataaars/svg?seed=Emery"
name=""
:size="96" :size="96"
class="bg-woot-300 dark:bg-woot-900"
allow-upload allow-upload
/> />
</div> </div>
</Variant> </Variant>
<Variant title="With Status">
<div class="flex flex-wrap gap-4 p-4 bg-white dark:bg-slate-900">
<Avatar
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Felix"
status="online"
name="Felix Online"
/>
<Avatar
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Jade"
status="busy"
name="Jade Busy"
/>
<Avatar
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Emery"
status="offline"
name="Emery Offline"
/>
</div>
</Variant>
<Variant title="With Custom Icon">
<div class="flex flex-wrap gap-4 p-4 bg-white dark:bg-slate-900">
<Avatar name="Custom Icon" icon-name="i-lucide-user" :size="48" />
<Avatar
name="Custom Industry"
icon-name="i-lucide-building-2"
:size="48"
/>
</div>
</Variant>
<Variant title="Upload States">
<div class="flex flex-wrap gap-4 p-4 bg-white dark:bg-slate-900">
<!-- Empty state with upload -->
<Avatar name="Upload New" allow-upload :size="48" />
<!-- With image and upload -->
<Avatar
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Upload"
name="Replace Image"
allow-upload
:size="48"
/>
</div>
</Variant>
<Variant title="Name Initials">
<div class="flex flex-wrap gap-4 p-4 bg-white dark:bg-slate-900">
<Avatar name="Catherine" :size="48" />
<Avatar name="John Doe" :size="48" />
<Avatar name="Rose Doe John" :size="48" />
</div>
</Variant>
</Story> </Story>
</template> </template>

View File

@@ -39,11 +39,12 @@ const props = defineProps({
}, },
}); });
const emit = defineEmits(['upload']); const emit = defineEmits(['upload', 'delete']);
const { t } = useI18n(); const { t } = useI18n();
const isImageValid = ref(true); const isImageValid = ref(true);
const fileInput = ref(null);
const AVATAR_COLORS = { const AVATAR_COLORS = {
dark: [ dark: [
@@ -131,13 +132,39 @@ const iconStyles = computed(() => ({
})); }));
const initialsStyles = computed(() => ({ const initialsStyles = computed(() => ({
fontSize: `${props.size / 1.8}px`, fontSize: `${props.size / 2}px`,
})); }));
const invalidateCurrentImage = () => { const invalidateCurrentImage = () => {
isImageValid.value = false; isImageValid.value = false;
}; };
const handleUploadAvatar = () => {
fileInput.value.click();
};
const handleImageUpload = event => {
const [file] = event.target.files;
if (file) {
emit('upload', {
file,
url: file ? URL.createObjectURL(file) : null,
});
}
};
const handleDeleteAvatar = () => {
if (fileInput.value) {
fileInput.value.value = null;
}
emit('delete');
};
const handleDismiss = event => {
event.stopPropagation();
handleDeleteAvatar();
};
watch( watch(
() => props.src, () => props.src,
() => { () => {
@@ -147,7 +174,7 @@ watch(
</script> </script>
<template> <template>
<span class="relative inline-flex" :style="containerStyles"> <span class="relative inline-flex group/avatar" :style="containerStyles">
<!-- Status Badge --> <!-- Status Badge -->
<slot name="badge" :size="size"> <slot name="badge" :size="size">
<div <div
@@ -158,6 +185,15 @@ watch(
/> />
</slot> </slot>
<!-- Delete Avatar Button -->
<div
v-if="src && allowUpload"
class="absolute z-20 flex items-center justify-center invisible w-6 h-6 transition-all duration-300 ease-in-out opacity-0 cursor-pointer outline outline-1 outline-n-container -top-2 -right-2 rounded-xl bg-n-solid-3 group-hover/avatar:visible group-hover/avatar:opacity-100"
@click="handleDismiss"
>
<Icon icon="i-lucide-x" class="text-n-slate-11 size-4" />
</div>
<!-- Avatar Container --> <!-- Avatar Container -->
<span <span
role="img" role="img"
@@ -202,16 +238,23 @@ watch(
/> />
</template> </template>
<!-- Upload Overlay --> <!-- Upload Overlay and Input -->
<div <div
v-if="allowUpload" v-if="allowUpload"
role="button" 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 flex items-center justify-center invisible w-full h-full transition-all duration-500 ease-in-out opacity-0 rounded-xl dark:bg-slate-900/50 bg-slate-900/20 group-hover/avatar:visible group-hover/avatar:opacity-100" @click="handleUploadAvatar"
@click="emit('upload')"
> >
<Icon <Icon
icon="i-lucide-upload" icon="i-lucide-upload"
class="text-white dark:text-white size-4" class="text-white"
:style="{ width: `${size / 2}px`, height: `${size / 2}px` }"
/>
<input
ref="fileInput"
type="file"
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
class="hidden"
@change="handleImageUpload"
/> />
</div> </div>
</span> </span>

View File

@@ -1,36 +0,0 @@
<script setup>
import EditableAvatar from './EditableAvatar.vue';
</script>
<template>
<Story title="Components/Avatar" :layout="{ type: 'grid', width: '400' }">
<Variant title="Default">
<div class="p-4 bg-white dark:bg-slate-900">
<EditableAvatar
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Amaya"
class="bg-ruby-300 dark:bg-ruby-900"
/>
</div>
</Variant>
<Variant title="Different Sizes">
<div class="flex flex-wrap gap-4 p-4 bg-white dark:bg-slate-900">
<EditableAvatar
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Felix"
:size="48"
class="bg-green-300 dark:bg-green-900"
/>
<EditableAvatar
:size="72"
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Jade"
class="bg-indigo-300 dark:bg-indigo-900"
/>
<EditableAvatar
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Emery"
:size="96"
class="bg-woot-300 dark:bg-woot-900"
/>
</div>
</Variant>
</Story>
</template>

View File

@@ -1,105 +0,0 @@
<script setup>
import { computed, ref } from 'vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
const props = defineProps({
src: {
type: String,
default: '',
},
size: {
type: Number,
default: 72,
},
name: {
type: String,
default: '',
},
});
const emit = defineEmits(['upload', 'delete']);
const avatarSize = computed(() => `${props.size}px`);
const iconSize = computed(() => `${props.size / 2}px`);
const fileInput = ref(null);
const imgError = ref(false);
const shouldShowImage = computed(() => props.src && !imgError.value);
const handleUploadAvatar = () => {
fileInput.value.click();
};
const handleImageUpload = event => {
const [file] = event.target.files;
if (file) {
emit('upload', {
file,
url: file ? URL.createObjectURL(file) : null,
});
}
};
const handleDeleteAvatar = () => {
if (fileInput.value) {
fileInput.value.value = null;
}
emit('delete');
};
const handleDismiss = event => {
event.stopPropagation();
handleDeleteAvatar();
};
</script>
<template>
<div
class="relative flex flex-col items-center gap-2 select-none rounded-xl outline outline-1 outline-n-container group/avatar"
:style="{ width: avatarSize, height: avatarSize }"
>
<img
v-if="shouldShowImage"
:src="src"
:alt="name || 'avatar'"
class="object-cover w-full h-full shadow-sm rounded-xl"
@error="imgError = true"
/>
<div
v-else
class="flex items-center justify-center w-full h-full rounded-xl bg-n-alpha-2"
>
<Icon
icon="i-lucide-building-2"
class="text-n-brand/50"
:style="{ width: `${iconSize}`, height: `${iconSize}` }"
/>
</div>
<div
v-if="src"
class="absolute z-20 outline outline-1 outline-n-container flex items-center cursor-pointer justify-center w-6 h-6 transition-all invisible opacity-0 duration-500 ease-in-out -top-2.5 -right-2.5 rounded-xl bg-n-solid-3 group-hover/avatar:visible group-hover/avatar:opacity-100"
@click="handleDismiss"
>
<Icon icon="i-lucide-x" class="text-n-slate-11 size-4" />
</div>
<div
class="absolute inset-0 z-10 flex items-center justify-center invisible w-full h-full transition-all duration-500 ease-in-out opacity-0 rounded-xl bg-n-alpha-black1 group-hover/avatar:visible group-hover/avatar:opacity-100"
@click="handleUploadAvatar"
>
<Icon
icon="i-lucide-upload"
class="text-white"
:style="{ width: `${iconSize}`, height: `${iconSize}` }"
/>
<input
ref="fileInput"
type="file"
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
class="hidden"
@change="handleImageUpload"
/>
</div>
</div>
</template>

View File

@@ -6,7 +6,7 @@ import Icon from 'dashboard/components-next/icon/Icon.vue';
const props = defineProps({ const props = defineProps({
label: { label: {
type: String, type: [String, Number],
default: '', default: '',
}, },
variant: { variant: {
@@ -47,9 +47,9 @@ const STYLE_CONFIG = {
blue: { blue: {
solid: 'bg-n-brand text-white hover:brightness-110 outline-transparent', solid: 'bg-n-brand text-white hover:brightness-110 outline-transparent',
faded: faded:
'bg-n-brand/10 text-n-slate-12 hover:bg-n-brand/20 outline-transparent', 'bg-n-brand/10 text-n-blue-text hover:bg-n-brand/20 outline-transparent',
outline: 'text-n-blue-text outline-n-blue-border', outline: 'text-n-blue-text outline-n-blue-border',
link: 'text-n-brand hover:underline outline-transparent', link: 'text-n-blue-text hover:underline outline-transparent',
}, },
ruby: { ruby: {
solid: 'bg-n-ruby-9 text-white hover:bg-n-ruby-10 outline-transparent', solid: 'bg-n-ruby-9 text-white hover:bg-n-ruby-10 outline-transparent',
@@ -161,7 +161,7 @@ const linkButtonClasses = computed(() => {
<Spinner v-if="isLoading" class="!w-5 !h-5 flex-shrink-0" /> <Spinner v-if="isLoading" class="!w-5 !h-5 flex-shrink-0" />
<slot v-if="label || $slots.default" name="default"> <slot v-if="label || $slots.default" name="default">
<span class="min-w-0 truncate">{{ label }}</span> <span v-if="label" class="min-w-0 truncate">{{ label }}</span>
</slot> </slot>
</button> </button>
</template> </template>

View File

@@ -43,7 +43,7 @@ const props = defineProps({
}, },
}); });
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue', 'search']);
const { t } = useI18n(); const { t } = useI18n();
@@ -118,13 +118,13 @@ watch(
<ComboBoxDropdown <ComboBoxDropdown
ref="dropdownRef" ref="dropdownRef"
v-model:search-value="search"
:open="open" :open="open"
:options="filteredOptions" :options="filteredOptions"
:search-value="search"
:search-placeholder="searchPlaceholder" :search-placeholder="searchPlaceholder"
:empty-state="emptyState" :empty-state="emptyState"
:selected-values="selectedValue" :selected-values="selectedValue"
@update:search-value="search = $event" @search="emit('search', $event)"
@select="selectOption" @select="selectOption"
/> />

View File

@@ -11,10 +11,6 @@ const props = defineProps({
type: Array, type: Array,
required: true, required: true,
}, },
searchValue: {
type: String,
required: true,
},
searchPlaceholder: { searchPlaceholder: {
type: String, type: String,
default: '', default: '',
@@ -33,10 +29,15 @@ const props = defineProps({
}, },
}); });
const emit = defineEmits(['update:searchValue', 'select']); const emit = defineEmits(['update:searchValue', 'select', 'search']);
const { t } = useI18n(); const { t } = useI18n();
const searchValue = defineModel('searchValue', {
type: String,
default: '',
});
const searchInput = ref(null); const searchInput = ref(null);
const isSelected = option => { const isSelected = option => {
@@ -46,6 +47,11 @@ const isSelected = option => {
return option.value === props.selectedValues; return option.value === props.selectedValues;
}; };
const onInputSearch = event => {
searchValue.value = event.target.value;
emit('search', event.target.value);
};
defineExpose({ defineExpose({
focus: () => searchInput.value?.focus(), focus: () => searchInput.value?.focus(),
}); });
@@ -64,7 +70,7 @@ defineExpose({
type="search" type="search"
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')" :placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
class="w-full py-2 pl-10 pr-2 text-sm border-none rounded-t-md bg-n-solid-1 text-slate-900 dark:text-slate-50" class="w-full py-2 pl-10 pr-2 text-sm border-none rounded-t-md bg-n-solid-1 text-slate-900 dark:text-slate-50"
@input="emit('update:searchValue', $event.target.value)" @input="onInputSearch"
/> />
</div> </div>
<ul <ul

View File

@@ -7,6 +7,7 @@ import Input from 'dashboard/components-next/input/Input.vue';
const alertDialog = ref(null); const alertDialog = ref(null);
const editDialog = ref(null); const editDialog = ref(null);
const confirmDialog = ref(null); const confirmDialog = ref(null);
const confirmDialogWithCustomFooter = ref(null);
const openAlertDialog = () => { const openAlertDialog = () => {
alertDialog.value.open(); alertDialog.value.open();
@@ -17,6 +18,9 @@ const openEditDialog = () => {
const openConfirmDialog = () => { const openConfirmDialog = () => {
confirmDialog.value.open(); confirmDialog.value.open();
}; };
const openConfirmDialogWithCustomFooter = () => {
confirmDialogWithCustomFooter.value.open();
};
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const onConfirm = dialog => {}; const onConfirm = dialog => {};
@@ -44,7 +48,6 @@ const onConfirm = dialog => {};
confirm-button-label="Save" confirm-button-label="Save"
@confirm="onConfirm()" @confirm="onConfirm()"
> >
<template #form>
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<Input <Input
id="portal-name" id="portal-name"
@@ -61,7 +64,6 @@ const onConfirm = dialog => {};
message="app.chatwoot.com/hc/my-portal/en-US/categories/my-slug" message="app.chatwoot.com/hc/my-portal/en-US/categories/my-slug"
/> />
</div> </div>
</template>
</Dialog> </Dialog>
</Variant> </Variant>
@@ -77,5 +79,21 @@ const onConfirm = dialog => {};
@confirm="onConfirm()" @confirm="onConfirm()"
/> />
</Variant> </Variant>
<Variant title="With custom footer">
<Button
label="Open Confirm Dialog with custom footer"
@click="openConfirmDialogWithCustomFooter"
/>
<Dialog
ref="confirmDialogWithCustomFooter"
title="Confirm Action"
description="Are you sure you want to perform this action?"
>
<template #footer>
<Button label="Custom Button" @click="onConfirm()" />
</template>
</Dialog>
</Variant>
</Story> </Story>
</template> </template>

View File

@@ -1,12 +1,12 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref, computed } from 'vue';
import { OnClickOutside } from '@vueuse/components'; import { OnClickOutside } from '@vueuse/components';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useMapGetter } from 'dashboard/composables/store.js'; import { useMapGetter } from 'dashboard/composables/store.js';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
defineProps({ const props = defineProps({
type: { type: {
type: String, type: String,
default: 'edit', default: 'edit',
@@ -14,7 +14,7 @@ defineProps({
}, },
title: { title: {
type: String, type: String,
required: true, default: '',
}, },
description: { description: {
type: String, type: String,
@@ -48,6 +48,11 @@ defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
width: {
type: String,
default: 'lg',
validator: value => ['3xl', '2xl', 'xl', 'lg', 'md', 'sm'].includes(value),
},
}); });
const emit = defineEmits(['confirm', 'close']); const emit = defineEmits(['confirm', 'close']);
@@ -59,6 +64,19 @@ const isRTL = useMapGetter('accounts/isRTL');
const dialogRef = ref(null); const dialogRef = ref(null);
const dialogContentRef = ref(null); const dialogContentRef = ref(null);
const maxWidthClass = computed(() => {
const classesMap = {
'3xl': 'max-w-3xl',
'2xl': 'max-w-2xl',
xl: 'max-w-xl',
lg: 'max-w-lg',
md: 'max-w-md',
sm: 'max-w-sm',
};
return classesMap[props.width] ?? 'max-w-md';
});
const open = () => { const open = () => {
dialogRef.value?.showModal(); dialogRef.value?.showModal();
}; };
@@ -77,8 +95,11 @@ defineExpose({ open, close });
<Teleport to="body"> <Teleport to="body">
<dialog <dialog
ref="dialogRef" ref="dialogRef"
class="w-full max-w-lg transition-all duration-300 ease-in-out shadow-xl rounded-xl" class="w-full transition-all duration-300 ease-in-out shadow-xl rounded-xl"
:class="overflowYAuto ? 'overflow-y-auto' : 'overflow-visible'" :class="[
maxWidthClass,
overflowYAuto ? 'overflow-y-auto' : 'overflow-visible',
]"
:dir="isRTL ? 'rtl' : 'ltr'" :dir="isRTL ? 'rtl' : 'ltr'"
@close="close" @close="close"
> >
@@ -88,7 +109,7 @@ defineExpose({ open, close });
class="flex flex-col w-full h-auto gap-6 p-6 overflow-visible text-left align-middle transition-all duration-300 ease-in-out transform bg-n-alpha-3 backdrop-blur-[100px] shadow-xl rounded-xl" class="flex flex-col w-full h-auto gap-6 p-6 overflow-visible text-left align-middle transition-all duration-300 ease-in-out transform bg-n-alpha-3 backdrop-blur-[100px] shadow-xl rounded-xl"
@click.stop @click.stop
> >
<div class="flex flex-col gap-2"> <div v-if="title || description" class="flex flex-col gap-2">
<h3 class="text-base font-medium leading-6 text-n-slate-12"> <h3 class="text-base font-medium leading-6 text-n-slate-12">
{{ title }} {{ title }}
</h3> </h3>
@@ -98,9 +119,9 @@ defineExpose({ open, close });
</p> </p>
</slot> </slot>
</div> </div>
<slot name="form"> <slot />
<!-- Form content will be injected here --> <!-- Dialog content will be injected here -->
</slot> <slot name="footer">
<div class="flex items-center justify-between w-full gap-3"> <div class="flex items-center justify-between w-full gap-3">
<Button <Button
v-if="showCancelButton" v-if="showCancelButton"
@@ -120,6 +141,7 @@ defineExpose({ open, close });
@click="confirm" @click="confirm"
/> />
</div> </div>
</slot>
</div> </div>
</OnClickOutside> </OnClickOutside>
</dialog> </dialog>

View File

@@ -51,5 +51,19 @@ const handleAction = () => {
/> />
</div> </div>
</Variant> </Variant>
<Variant title="With search">
<div class="p-4 bg-white h-72 dark:bg-slate-900">
<DropdownMenu
:menu-items="[
{ label: 'Custom 1', action: 'custom1', icon: 'file-upload' },
{ label: 'Custom 2', action: 'custom2', icon: 'document' },
{ label: 'Danger', action: 'delete', icon: 'delete' },
]"
show-search
@action="handleAction"
/>
</div>
</Variant>
</Story> </Story>
</template> </template>

View File

@@ -1,10 +1,11 @@
<script setup> <script setup>
import { defineProps, defineEmits } from 'vue'; import { defineProps, ref, defineEmits, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import Icon from 'dashboard/components-next/icon/Icon.vue'; import Icon from 'dashboard/components-next/icon/Icon.vue';
import Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
defineProps({ const props = defineProps({
menuItems: { menuItems: {
type: Array, type: Array,
required: true, required: true,
@@ -16,21 +17,61 @@ defineProps({
type: Number, type: Number,
default: 20, default: 20,
}, },
showSearch: {
type: Boolean,
default: false,
},
searchPlaceholder: {
type: String,
default: '',
},
}); });
const emit = defineEmits(['action']); const emit = defineEmits(['action']);
const handleAction = (action, value) => { const { t } = useI18n();
emit('action', { action, value });
const searchInput = ref(null);
const searchQuery = ref('');
const filteredMenuItems = computed(() => {
if (!searchQuery.value) return props.menuItems;
return props.menuItems.filter(item =>
item.label.toLowerCase().includes(searchQuery.value.toLowerCase())
);
});
const handleAction = item => {
const { action, value, ...rest } = item;
emit('action', { action, value, ...rest });
}; };
onMounted(() => {
if (searchInput.value && props.showSearch) {
searchInput.value.focus();
}
});
</script> </script>
<template> <template>
<div <div
class="bg-n-alpha-3 backdrop-blur-[100px] border-0 outline outline-1 outline-n-container absolute rounded-xl z-50 py-2 px-2 gap-2 flex flex-col min-w-[136px] shadow-lg" class="bg-n-alpha-3 backdrop-blur-[100px] border-0 outline outline-1 outline-n-container absolute rounded-xl z-50 py-2 px-2 gap-2 flex flex-col min-w-[136px] shadow-lg"
> >
<div v-if="showSearch" class="relative">
<span class="absolute i-lucide-search size-3.5 top-2 left-3" />
<input
ref="searchInput"
v-model="searchQuery"
type="search"
:placeholder="
searchPlaceholder || t('DROPDOWN_MENU.SEARCH_PLACEHOLDER')
"
class="w-full h-8 py-2 pl-10 pr-2 text-sm border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
/>
</div>
<button <button
v-for="item in menuItems" v-for="item in filteredMenuItems"
:key="item.action" :key="item.action"
class="inline-flex items-center justify-start w-full h-8 min-w-0 gap-2 px-2 py-1.5 transition-all duration-200 ease-in-out border-0 rounded-lg z-60 hover:bg-n-alpha-1 dark:hover:bg-n-alpha-2 disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50" class="inline-flex items-center justify-start w-full h-8 min-w-0 gap-2 px-2 py-1.5 transition-all duration-200 ease-in-out border-0 rounded-lg z-60 hover:bg-n-alpha-1 dark:hover:bg-n-alpha-2 disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50"
:class="{ :class="{
@@ -39,20 +80,29 @@ const handleAction = (action, value) => {
'text-n-slate-12': item.action !== 'delete', 'text-n-slate-12': item.action !== 'delete',
}" }"
:disabled="item.disabled" :disabled="item.disabled"
@click="handleAction(item.action, item.value)" @click="handleAction(item)"
@keydown.enter="handleAction(item)"
> >
<Thumbnail <slot name="thumbnail" :item="item">
<Avatar
v-if="item.thumbnail" v-if="item.thumbnail"
:author="item.thumbnail"
:name="item.thumbnail.name" :name="item.thumbnail.name"
:size="thumbnailSize"
:src="item.thumbnail.src" :src="item.thumbnail.src"
:size="thumbnailSize"
rounded-full
/> />
</slot>
<Icon v-if="item.icon" :icon="item.icon" class="flex-shrink-0" /> <Icon v-if="item.icon" :icon="item.icon" class="flex-shrink-0" />
<span v-if="item.emoji" class="flex-shrink-0">{{ item.emoji }}</span> <span v-if="item.emoji" class="flex-shrink-0">{{ item.emoji }}</span>
<span v-if="item.label" class="min-w-0 text-sm truncate">{{ <span v-if="item.label" class="min-w-0 text-sm truncate">{{
item.label item.label
}}</span> }}</span>
</button> </button>
<div
v-if="filteredMenuItems.length === 0"
class="text-sm text-n-slate-11 px-2 py-1.5"
>
{{ t('DROPDOWN_MENU.EMPTY_STATE') }}
</div>
</div> </div>
</template> </template>

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed, ref, onMounted, nextTick } from 'vue';
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: [String, Number], type: [String, Number],
@@ -42,9 +42,22 @@ const props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
autofocus: {
type: Boolean,
default: false,
},
}); });
const emit = defineEmits(['update:modelValue', 'blur', 'input']); const emit = defineEmits([
'update:modelValue',
'blur',
'input',
'focus',
'enter',
]);
const isFocused = ref(false);
const inputRef = ref(null);
const messageClass = computed(() => { const messageClass = computed(() => {
switch (props.messageType) { switch (props.messageType) {
@@ -62,7 +75,7 @@ const inputBorderClass = computed(() => {
case 'error': case 'error':
return 'border-n-ruby-8 dark:border-n-ruby-8 hover:border-n-ruby-9 dark:hover:border-n-ruby-9 disabled:border-n-ruby-8 dark:disabled:border-n-ruby-8'; return 'border-n-ruby-8 dark:border-n-ruby-8 hover:border-n-ruby-9 dark:hover:border-n-ruby-9 disabled:border-n-ruby-8 dark:disabled:border-n-ruby-8';
default: default:
return 'border-n-weak dark:border-n-weak hover:border-n-slate-6 dark:hover:border-n-slate-6 disabled:border-n-weak dark:disabled:border-n-weak'; return 'border-n-weak dark:border-n-weak hover:border-n-slate-6 dark:hover:border-n-slate-6 disabled:border-n-weak dark:disabled:border-n-weak focus:border-n-brand dark:focus:border-n-brand';
} }
}); });
@@ -70,6 +83,28 @@ const handleInput = event => {
emit('update:modelValue', event.target.value); emit('update:modelValue', event.target.value);
emit('input', event); emit('input', event);
}; };
const handleFocus = event => {
emit('focus', event);
isFocused.value = true;
};
const handleBlur = event => {
emit('blur', event);
isFocused.value = false;
};
const handleEnter = event => {
emit('enter', event);
};
onMounted(() => {
if (props.autofocus) {
nextTick(() => {
inputRef.value?.focus();
});
}
});
</script> </script>
<template> <template>
@@ -85,15 +120,25 @@ const handleInput = event => {
<slot name="prefix" /> <slot name="prefix" />
<input <input
:id="id" :id="id"
ref="inputRef"
:value="modelValue" :value="modelValue"
:class="[customInputClass, inputBorderClass]" :class="[
customInputClass,
inputBorderClass,
{
error: messageType === 'error',
focus: isFocused,
},
]"
:type="type" :type="type"
:placeholder="placeholder" :placeholder="placeholder"
:disabled="disabled" :disabled="disabled"
:min="['date', 'datetime-local', 'time'].includes(type) ? min : undefined" :min="['date', 'datetime-local', 'time'].includes(type) ? min : undefined"
class="block w-full reset-base text-sm h-10 !px-3 !py-2.5 !mb-0 border rounded-lg focus:border-n-brand dark:focus:border-n-brand bg-n-alpha-black2 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-n-slate-11 dark:placeholder:text-n-slate-11 disabled:cursor-not-allowed disabled:opacity-50 text-n-slate-12 transition-all duration-500 ease-in-out" class="block w-full reset-base text-sm h-10 !px-3 !py-2.5 !mb-0 border rounded-lg bg-n-alpha-black2 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10 disabled:cursor-not-allowed disabled:opacity-50 text-n-slate-12 transition-all duration-500 ease-in-out"
@input="handleInput" @input="handleInput"
@blur="emit('blur')" @focus="handleFocus"
@blur="handleBlur"
@keyup.enter="handleEnter"
/> />
<p <p
v-if="message" v-if="message"

View File

@@ -46,6 +46,13 @@ const handleClickOutside = () => {
} }
}; };
const handleKeydown = event => {
if (event.key === ',') {
event.preventDefault();
addTag();
}
};
watch( watch(
() => props.modelValue, () => props.modelValue,
newValue => { newValue => {
@@ -81,6 +88,7 @@ watch(
:placeholder="placeholder" :placeholder="placeholder"
custom-input-class="flex-grow" custom-input-class="flex-grow"
@enter-press="addTag" @enter-press="addTag"
@keydown="handleKeydown"
/> />
</div> </div>
</OnClickOutside> </OnClickOutside>

View File

@@ -1,117 +0,0 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { removeEmoji } from 'shared/helpers/emoji';
const props = defineProps({
author: {
type: Object,
default: null,
},
name: {
type: String,
default: '',
},
src: {
type: String,
default: '',
},
size: {
type: Number,
default: 16,
},
showAuthorName: {
type: Boolean,
default: true,
},
iconName: {
type: String,
default: '',
},
});
const { t } = useI18n();
const hasImageLoaded = ref(false);
const imgError = ref(false);
const authorInitial = computed(() => {
if (!props.name) return '';
const name = removeEmoji(props.name);
const words = name.split(/\s+/);
if (words.length === 1) {
return name.substring(0, 2).toUpperCase();
}
return words
.slice(0, 2)
.map(word => word[0])
.join('')
.toUpperCase();
});
const fontSize = computed(() => {
return props.size / 2;
});
const iconSize = computed(() => {
return Math.round(props.size / 1.8);
});
const shouldShowImage = computed(() => {
return props.src && !imgError.value;
});
const onImgError = () => {
imgError.value = true;
};
const onImgLoad = () => {
hasImageLoaded.value = true;
};
</script>
<template>
<div
class="flex items-center justify-center rounded-full bg-n-slate-3 dark:bg-n-slate-4"
:style="{ width: `${size}px`, height: `${size}px` }"
>
<div v-if="author" class="flex items-center justify-center">
<img
v-if="shouldShowImage"
:src="src"
:alt="name"
class="w-full h-full rounded-full"
@load="onImgLoad"
@error="onImgError"
/>
<template v-else>
<span
v-if="showAuthorName"
class="flex items-center justify-center font-medium text-n-slate-11"
:style="{ fontSize: `${fontSize}px` }"
>
{{ authorInitial }}
</span>
<div
v-else
class="flex items-center justify-center w-full h-full rounded-xl"
>
<span
v-if="iconName"
:class="`${iconName} text-n-brand/70`"
:style="{ width: `${iconSize}px`, height: `${iconSize}px` }"
/>
</div>
</template>
</div>
<div
v-else
v-tooltip.top-start="t('THUMBNAIL.AUTHOR.NOT_AVAILABLE')"
class="flex items-center justify-center w-4 h-4 rounded-full bg-n-slate-3 dark:bg-n-slate-4"
>
<span class="i-lucide-user size-2.5 text-n-brand" />
</div>
</div>
</template>

View File

@@ -8,6 +8,10 @@
"EMPTY_STATE": "No results found.", "EMPTY_STATE": "No results found.",
"SEARCH_PLACEHOLDER": "Search..." "SEARCH_PLACEHOLDER": "Search..."
}, },
"DROPDOWN_MENU": {
"SEARCH_PLACEHOLDER": "Search...",
"EMPTY_STATE": "No results found."
},
"DIALOG": { "DIALOG": {
"BUTTONS": { "BUTTONS": {
"CANCEL": "Cancel", "CANCEL": "Cancel",