mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +00:00
feat: Updates on new components (#10444)
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user