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,60 +82,56 @@ const inboxIcon = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardLayout class="flex flex-row justify-between flex-1 gap-8" layout="row">
|
||||
<template #header>
|
||||
<div class="flex flex-col items-start gap-2">
|
||||
<div class="flex justify-between gap-3 w-fit">
|
||||
<span
|
||||
class="text-base font-medium capitalize text-n-slate-12 line-clamp-1"
|
||||
>
|
||||
{{ title }}
|
||||
</span>
|
||||
<span
|
||||
class="text-xs font-medium inline-flex items-center h-6 px-2 py-0.5 rounded-md bg-n-alpha-2"
|
||||
:class="statusTextColor"
|
||||
>
|
||||
{{ campaignStatus }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-dompurify-html="formatMessage(message)"
|
||||
class="text-sm text-n-slate-11 line-clamp-1 [&>p]:mb-0 h-6"
|
||||
/>
|
||||
<div class="flex items-center w-full h-6 gap-2 overflow-hidden">
|
||||
<LiveChatCampaignDetails
|
||||
v-if="isLiveChatType"
|
||||
:sender="sender"
|
||||
:inbox-name="inboxName"
|
||||
:inbox-icon="inboxIcon"
|
||||
/>
|
||||
<SMSCampaignDetails
|
||||
v-else
|
||||
:inbox-name="inboxName"
|
||||
:inbox-icon="inboxIcon"
|
||||
:scheduled-at="scheduledAt"
|
||||
/>
|
||||
</div>
|
||||
<CardLayout layout="row">
|
||||
<div class="flex flex-col items-start justify-between flex-1 min-w-0 gap-2">
|
||||
<div class="flex justify-between gap-3 w-fit">
|
||||
<span
|
||||
class="text-base font-medium capitalize text-n-slate-12 line-clamp-1"
|
||||
>
|
||||
{{ title }}
|
||||
</span>
|
||||
<span
|
||||
class="text-xs font-medium inline-flex items-center h-6 px-2 py-0.5 rounded-md bg-n-alpha-2"
|
||||
:class="statusTextColor"
|
||||
>
|
||||
{{ campaignStatus }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-end w-20 gap-2">
|
||||
<Button
|
||||
<div
|
||||
v-dompurify-html="formatMessage(message)"
|
||||
class="text-sm text-n-slate-11 line-clamp-1 [&>p]:mb-0 h-6"
|
||||
/>
|
||||
<div class="flex items-center w-full h-6 gap-2 overflow-hidden">
|
||||
<LiveChatCampaignDetails
|
||||
v-if="isLiveChatType"
|
||||
variant="faded"
|
||||
size="sm"
|
||||
color="slate"
|
||||
icon="i-lucide-sliders-vertical"
|
||||
@click="emit('edit')"
|
||||
:sender="sender"
|
||||
:inbox-name="inboxName"
|
||||
:inbox-icon="inboxIcon"
|
||||
/>
|
||||
<Button
|
||||
variant="faded"
|
||||
color="ruby"
|
||||
size="sm"
|
||||
icon="i-lucide-trash"
|
||||
@click="emit('delete')"
|
||||
<SMSCampaignDetails
|
||||
v-else
|
||||
:inbox-name="inboxName"
|
||||
:inbox-icon="inboxIcon"
|
||||
:scheduled-at="scheduledAt"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex items-center justify-end w-20 gap-2">
|
||||
<Button
|
||||
v-if="isLiveChatType"
|
||||
variant="faded"
|
||||
size="sm"
|
||||
color="slate"
|
||||
icon="i-lucide-sliders-vertical"
|
||||
@click="emit('edit')"
|
||||
/>
|
||||
<Button
|
||||
variant="faded"
|
||||
color="ruby"
|
||||
size="sm"
|
||||
icon="i-lucide-trash"
|
||||
@click="emit('delete')"
|
||||
/>
|
||||
</div>
|
||||
</CardLayout>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup>
|
||||
import Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -33,10 +34,11 @@ const senderThumbnailSrc = computed(() => props.sender?.thumbnail);
|
||||
{{ t('CAMPAIGN.LIVE_CHAT.CARD.CAMPAIGN_DETAILS.SENT_BY') }}
|
||||
</span>
|
||||
<div class="flex items-center gap-1.5 flex-shrink-0">
|
||||
<Thumbnail
|
||||
:author="sender || { name: senderName }"
|
||||
<Avatar
|
||||
:name="senderName"
|
||||
:src="senderThumbnailSrc"
|
||||
:size="16"
|
||||
rounded-full
|
||||
/>
|
||||
<span class="text-sm font-medium text-n-slate-12">
|
||||
{{ senderName }}
|
||||
|
||||
@@ -27,7 +27,6 @@ defineProps({
|
||||
:message="campaign.message"
|
||||
:is-enabled="campaign.enabled"
|
||||
:status="campaign.campaign_status"
|
||||
:trigger-rules="campaign.trigger_rules"
|
||||
:sender="campaign.sender"
|
||||
:inbox="campaign.inbox"
|
||||
:scheduled-at="campaign.scheduled_at"
|
||||
|
||||
@@ -27,7 +27,6 @@ defineProps({
|
||||
:message="campaign.message"
|
||||
:is-enabled="campaign.enabled"
|
||||
:status="campaign.campaign_status"
|
||||
:trigger-rules="campaign.trigger_rules"
|
||||
:sender="campaign.sender"
|
||||
:inbox="campaign.inbox"
|
||||
:scheduled-at="campaign.scheduled_at"
|
||||
|
||||
@@ -27,7 +27,6 @@ const handleDelete = campaign => emit('delete', campaign);
|
||||
:message="campaign.message"
|
||||
:is-enabled="campaign.enabled"
|
||||
:status="campaign.campaign_status"
|
||||
:trigger-rules="campaign.trigger_rules"
|
||||
:sender="campaign.sender"
|
||||
:inbox="campaign.inbox"
|
||||
:scheduled-at="campaign.scheduled_at"
|
||||
|
||||
@@ -63,14 +63,12 @@ defineExpose({ dialogRef });
|
||||
overflow-y-auto
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<template #form>
|
||||
<LiveChatCampaignForm
|
||||
ref="liveChatCampaignFormRef"
|
||||
mode="edit"
|
||||
:selected-campaign="selectedCampaign"
|
||||
:show-action-buttons="false"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
<LiveChatCampaignForm
|
||||
ref="liveChatCampaignFormRef"
|
||||
mode="edit"
|
||||
:selected-campaign="selectedCampaign"
|
||||
:show-action-buttons="false"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
layout: {
|
||||
type: String,
|
||||
default: 'col',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
const handleClick = () => {
|
||||
emit('click');
|
||||
};
|
||||
@@ -13,11 +15,18 @@ const handleClick = () => {
|
||||
|
||||
<template>
|
||||
<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="props.layout === 'col' ? 'flex-col' : 'flex-row'"
|
||||
@click="handleClick"
|
||||
class="flex flex-col w-full shadow outline-1 outline outline-n-container group/cardLayout rounded-2xl bg-n-solid-2"
|
||||
>
|
||||
<slot name="header" />
|
||||
<slot name="footer" />
|
||||
<div
|
||||
class="flex w-full gap-3 px-6 py-5"
|
||||
:class="
|
||||
layout === 'col' ? 'flex-col' : 'flex-row justify-between items-center'
|
||||
"
|
||||
@click="handleClick"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<slot name="after" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -13,7 +13,7 @@ import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.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({
|
||||
id: {
|
||||
@@ -101,7 +101,7 @@ const categoryName = computed(() => {
|
||||
});
|
||||
|
||||
const authorName = computed(() => {
|
||||
return props.author?.name || props.author?.availableName || '-';
|
||||
return props.author?.name || props.author?.availableName || '';
|
||||
});
|
||||
|
||||
const authorThumbnailSrc = computed(() => {
|
||||
@@ -124,75 +124,72 @@ const handleClick = id => {
|
||||
|
||||
<template>
|
||||
<CardLayout>
|
||||
<template #header>
|
||||
<div class="flex justify-between gap-1">
|
||||
<div class="flex justify-between w-full gap-1">
|
||||
<span
|
||||
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)"
|
||||
>
|
||||
{{ title }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
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)"
|
||||
class="text-xs font-medium inline-flex items-center h-6 px-2 py-0.5 rounded-md bg-n-alpha-2"
|
||||
:class="statusTextColor"
|
||||
>
|
||||
{{ title }}
|
||||
{{ statusText }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="text-xs font-medium inline-flex items-center h-6 px-2 py-0.5 rounded-md bg-n-alpha-2"
|
||||
:class="statusTextColor"
|
||||
>
|
||||
{{ statusText }}
|
||||
</span>
|
||||
<div
|
||||
v-on-clickaway="() => toggleDropdown(false)"
|
||||
class="relative flex items-center group"
|
||||
>
|
||||
<Button
|
||||
icon="i-lucide-ellipsis-vertical"
|
||||
color="slate"
|
||||
size="xs"
|
||||
class="rounded-md group-hover:bg-n-alpha-2"
|
||||
@click="toggleDropdown()"
|
||||
/>
|
||||
<DropdownMenu
|
||||
v-if="showActionsDropdown"
|
||||
:menu-items="articleMenuItems"
|
||||
class="mt-1 ltr:right-0 rtl:left-0 xl:ltr:left-0 xl:rtl:right-0 top-full"
|
||||
@action="handleArticleAction($event)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-on-clickaway="() => toggleDropdown(false)"
|
||||
class="relative flex items-center group"
|
||||
>
|
||||
<Button
|
||||
icon="i-lucide-ellipsis-vertical"
|
||||
color="slate"
|
||||
size="xs"
|
||||
class="rounded-md group-hover:bg-n-alpha-2"
|
||||
@click="toggleDropdown()"
|
||||
/>
|
||||
<DropdownMenu
|
||||
v-if="showActionsDropdown"
|
||||
:menu-items="articleMenuItems"
|
||||
class="mt-1 ltr:right-0 rtl:left-0 xl:ltr:left-0 xl:rtl:right-0 top-full"
|
||||
@action="handleArticleAction($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-1">
|
||||
<Thumbnail
|
||||
:author="author"
|
||||
:name="authorName"
|
||||
:src="authorThumbnailSrc"
|
||||
/>
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ authorName }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="block text-sm whitespace-nowrap text-n-slate-11">
|
||||
{{ categoryName }}
|
||||
</div>
|
||||
<div class="flex items-center justify-between w-full gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-1">
|
||||
<Avatar
|
||||
:name="authorName"
|
||||
:src="authorThumbnailSrc"
|
||||
:size="16"
|
||||
rounded-full
|
||||
/>
|
||||
<span class="text-sm truncate text-n-slate-11">
|
||||
{{ authorName || '-' }}
|
||||
</span>
|
||||
<div
|
||||
class="inline-flex items-center gap-1 text-n-slate-11 whitespace-nowrap"
|
||||
>
|
||||
<Icon icon="i-lucide-eye" class="size-4" />
|
||||
<span class="text-sm">
|
||||
{{
|
||||
t('HELP_CENTER.ARTICLES_PAGE.ARTICLE_CARD.CARD.VIEWS', {
|
||||
count: views,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-sm text-n-slate-11 line-clamp-1">
|
||||
{{ lastUpdatedAt }}
|
||||
<span class="block text-sm whitespace-nowrap text-n-slate-11">
|
||||
{{ categoryName }}
|
||||
</span>
|
||||
<div
|
||||
class="inline-flex items-center gap-1 text-n-slate-11 whitespace-nowrap"
|
||||
>
|
||||
<Icon icon="i-lucide-eye" class="size-4" />
|
||||
<span class="text-sm">
|
||||
{{
|
||||
t('HELP_CENTER.ARTICLES_PAGE.ARTICLE_CARD.CARD.VIEWS', {
|
||||
count: views,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<span class="text-sm text-n-slate-11 line-clamp-1">
|
||||
{{ lastUpdatedAt }}
|
||||
</span>
|
||||
</div>
|
||||
</CardLayout>
|
||||
</template>
|
||||
|
||||
@@ -79,59 +79,55 @@ const handleAction = ({ action, value }) => {
|
||||
|
||||
<template>
|
||||
<CardLayout>
|
||||
<template #header>
|
||||
<div class="flex 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">
|
||||
<span
|
||||
class="text-base truncate cursor-pointer hover:underline underline-offset-2 hover:text-n-blue-text text-n-slate-12"
|
||||
@click="handleClick(slug)"
|
||||
>
|
||||
{{ categoryTitleWithIcon }}
|
||||
</span>
|
||||
<span
|
||||
class="inline-flex items-center justify-center h-6 px-2 py-1 text-xs text-center border rounded-lg bg-n-slate-1 whitespace-nowrap shrink-0 text-n-slate-11 border-n-slate-4"
|
||||
>
|
||||
{{
|
||||
t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_CARD.ARTICLES_COUNT', {
|
||||
count: articlesCount,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-on-clickaway="() => toggleDropdown(false)"
|
||||
class="relative group"
|
||||
<div class="flex 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">
|
||||
<span
|
||||
class="text-base truncate cursor-pointer hover:underline underline-offset-2 hover:text-n-blue-text text-n-slate-12"
|
||||
@click="handleClick(slug)"
|
||||
>
|
||||
<Button
|
||||
icon="i-lucide-ellipsis-vertical"
|
||||
color="slate"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
class="rounded-md group-hover:bg-n-alpha-2"
|
||||
@click="toggleDropdown()"
|
||||
/>
|
||||
<DropdownMenu
|
||||
v-if="showActionsDropdown"
|
||||
:menu-items="categoryMenuItems"
|
||||
class="mt-1 ltr:right-0 rtl:left-0 xl:ltr:left-0 xl:rtl:right-0 top-full z-60"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</div>
|
||||
{{ categoryTitleWithIcon }}
|
||||
</span>
|
||||
<span
|
||||
class="inline-flex items-center justify-center h-6 px-2 py-1 text-xs text-center border rounded-lg bg-n-slate-1 whitespace-nowrap shrink-0 text-n-slate-11 border-n-slate-4"
|
||||
>
|
||||
{{
|
||||
t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_CARD.ARTICLES_COUNT', {
|
||||
count: articlesCount,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-on-clickaway="() => toggleDropdown(false)"
|
||||
class="relative group"
|
||||
>
|
||||
<Button
|
||||
icon="i-lucide-ellipsis-vertical"
|
||||
color="slate"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
class="rounded-md group-hover:bg-n-alpha-2"
|
||||
@click="toggleDropdown()"
|
||||
/>
|
||||
<DropdownMenu
|
||||
v-if="showActionsDropdown"
|
||||
:menu-items="categoryMenuItems"
|
||||
class="mt-1 ltr:right-0 rtl:left-0 xl:ltr:left-0 xl:rtl:right-0 top-full z-60"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span
|
||||
class="text-sm line-clamp-3"
|
||||
:class="
|
||||
hasDescription
|
||||
? 'text-slate-500 dark:text-slate-400'
|
||||
: 'text-slate-400 dark:text-slate-700'
|
||||
"
|
||||
>
|
||||
{{ description }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<span
|
||||
class="text-sm line-clamp-3"
|
||||
:class="
|
||||
hasDescription
|
||||
? 'text-slate-500 dark:text-slate-400'
|
||||
: 'text-slate-400 dark:text-slate-700'
|
||||
"
|
||||
>
|
||||
{{ description }}
|
||||
</span>
|
||||
</CardLayout>
|
||||
</template>
|
||||
|
||||
@@ -53,66 +53,64 @@ const handleAction = ({ action, value }) => {
|
||||
|
||||
<template>
|
||||
<CardLayout>
|
||||
<template #header>
|
||||
<div class="flex justify-between gap-2">
|
||||
<div class="flex items-center justify-start gap-2">
|
||||
<div class="flex justify-between gap-2">
|
||||
<div class="flex items-center justify-start gap-2">
|
||||
<span
|
||||
class="text-sm font-medium text-slate-900 dark:text-slate-50 line-clamp-1"
|
||||
>
|
||||
{{ locale }} ({{ localeCode }})
|
||||
</span>
|
||||
<span
|
||||
v-if="isDefault"
|
||||
class="bg-n-alpha-2 h-6 inline-flex items-center justify-center rounded-md text-xs border-px border-transparent text-n-blue-text px-2 py-0.5"
|
||||
>
|
||||
{{ $t('HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DEFAULT') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<span
|
||||
class="text-sm font-medium text-slate-900 dark:text-slate-50 line-clamp-1"
|
||||
class="text-sm text-slate-500 dark:text-slate-400 whitespace-nowrap"
|
||||
>
|
||||
{{ locale }} ({{ localeCode }})
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.ARTICLES_COUNT',
|
||||
articleCount
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<div class="w-px h-3 bg-slate-75 dark:bg-slate-800" />
|
||||
<span
|
||||
v-if="isDefault"
|
||||
class="bg-n-alpha-2 h-6 inline-flex items-center justify-center rounded-md text-xs border-px border-transparent text-n-blue-text px-2 py-0.5"
|
||||
class="text-sm text-slate-500 dark:text-slate-400 whitespace-nowrap"
|
||||
>
|
||||
{{ $t('HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DEFAULT') }}
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.CATEGORIES_COUNT',
|
||||
categoryCount
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<span
|
||||
class="text-sm text-slate-500 dark:text-slate-400 whitespace-nowrap"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.ARTICLES_COUNT',
|
||||
articleCount
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<div class="w-px h-3 bg-slate-75 dark:bg-slate-800" />
|
||||
<span
|
||||
class="text-sm text-slate-500 dark:text-slate-400 whitespace-nowrap"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.CATEGORIES_COUNT',
|
||||
categoryCount
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-on-clickaway="() => toggleDropdown(false)"
|
||||
class="relative group"
|
||||
>
|
||||
<Button
|
||||
icon="i-lucide-ellipsis-vertical"
|
||||
color="slate"
|
||||
size="xs"
|
||||
class="rounded-md group-hover:bg-n-alpha-2"
|
||||
@click="toggleDropdown()"
|
||||
/>
|
||||
<div
|
||||
v-on-clickaway="() => toggleDropdown(false)"
|
||||
class="relative group"
|
||||
>
|
||||
<Button
|
||||
icon="i-lucide-ellipsis-vertical"
|
||||
color="slate"
|
||||
size="xs"
|
||||
class="rounded-md group-hover:bg-n-alpha-2"
|
||||
@click="toggleDropdown()"
|
||||
/>
|
||||
|
||||
<DropdownMenu
|
||||
v-if="showDropdownMenu"
|
||||
:menu-items="localeMenuItems"
|
||||
class="ltr:right-0 rtl:left-0 mt-1 xl:ltr:left-0 xl:rtl:right-0 top-full z-60 min-w-[150px]"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenu
|
||||
v-if="showDropdownMenu"
|
||||
:menu-items="localeMenuItems"
|
||||
class="ltr:right-0 rtl:left-0 mt-1 xl:ltr:left-0 xl:rtl:right-0 top-full z-60 min-w-[150px]"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</CardLayout>
|
||||
</template>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { OnClickOutside } from '@vueuse/components';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
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 ArticleEditorProperties from 'dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorProperties.vue';
|
||||
|
||||
@@ -50,7 +50,7 @@ const author = computed(() => {
|
||||
});
|
||||
|
||||
const authorName = computed(
|
||||
() => author.value?.name || author.value?.available_name || '-'
|
||||
() => author.value?.name || author.value?.available_name || ''
|
||||
);
|
||||
const authorThumbnailSrc = computed(() => author.value?.thumbnail);
|
||||
|
||||
@@ -186,17 +186,14 @@ onMounted(() => {
|
||||
text-variant="info"
|
||||
@click="openAgentsList = !openAgentsList"
|
||||
>
|
||||
<Thumbnail
|
||||
:author="author"
|
||||
<Avatar
|
||||
:name="authorName"
|
||||
:size="20"
|
||||
:src="authorThumbnailSrc"
|
||||
:size="20"
|
||||
rounded-full
|
||||
/>
|
||||
<span
|
||||
v-if="author"
|
||||
class="text-sm text-n-slate-12 hover:text-n-slate-11"
|
||||
>
|
||||
{{ author.available_name }}
|
||||
<span class="text-sm text-n-slate-12 hover:text-n-slate-11">
|
||||
{{ authorName || '-' }}
|
||||
</span>
|
||||
</Button>
|
||||
<DropdownMenu
|
||||
|
||||
@@ -97,16 +97,14 @@ defineExpose({ dialogRef });
|
||||
:disable-confirm-button="isUpdatingCategory || isInvalidForm"
|
||||
@confirm="onUpdateCategory"
|
||||
>
|
||||
<template #form>
|
||||
<CategoryForm
|
||||
ref="categoryFormRef"
|
||||
mode="edit"
|
||||
:selected-category="selectedCategory"
|
||||
:active-locale-code="activeLocaleCode"
|
||||
:portal-name="route.params.portalSlug"
|
||||
:active-locale-name="activeLocaleName"
|
||||
:show-action-buttons="false"
|
||||
/>
|
||||
</template>
|
||||
<CategoryForm
|
||||
ref="categoryFormRef"
|
||||
mode="edit"
|
||||
:selected-category="selectedCategory"
|
||||
:active-locale-code="activeLocaleCode"
|
||||
:portal-name="route.params.portalSlug"
|
||||
:active-locale-name="activeLocaleName"
|
||||
:show-action-buttons="false"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -85,17 +85,15 @@ defineExpose({ dialogRef });
|
||||
:description="t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.DESCRIPTION')"
|
||||
@confirm="onCreate"
|
||||
>
|
||||
<template #form>
|
||||
<div class="flex flex-col gap-6">
|
||||
<ComboBox
|
||||
v-model="selectedLocale"
|
||||
:options="locales"
|
||||
:placeholder="
|
||||
t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.COMBOBOX.PLACEHOLDER')
|
||||
"
|
||||
class="[&>div>button]:!border-n-slate-5 [&>div>button]:dark:!border-n-slate-5"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<ComboBox
|
||||
v-model="selectedLocale"
|
||||
:options="locales"
|
||||
:placeholder="
|
||||
t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.COMBOBOX.PLACEHOLDER')
|
||||
"
|
||||
class="[&>div>button]:!border-n-slate-5 [&>div>button]:dark:!border-n-slate-5"
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -55,20 +55,18 @@ defineExpose({ dialogRef });
|
||||
"
|
||||
@confirm="handleDialogConfirm"
|
||||
>
|
||||
<template #form>
|
||||
<Input
|
||||
v-model="formState.customDomain"
|
||||
:label="
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DIALOG.LABEL'
|
||||
)
|
||||
"
|
||||
:placeholder="
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DIALOG.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<Input
|
||||
v-model="formState.customDomain"
|
||||
:label="
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DIALOG.LABEL'
|
||||
)
|
||||
"
|
||||
:placeholder="
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DIALOG.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -59,21 +59,20 @@ defineExpose({ dialogRef });
|
||||
}}
|
||||
</p>
|
||||
</template>
|
||||
<template #form>
|
||||
<div class="flex flex-col gap-6">
|
||||
<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"
|
||||
>
|
||||
{{ subdomainCNAME }}
|
||||
</span>
|
||||
<p class="text-sm text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.HELP_TEXT'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<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"
|
||||
>
|
||||
{{ subdomainCNAME }}
|
||||
</span>
|
||||
<p class="text-sm text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.HELP_TEXT'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { shouldBeUrl } from 'shared/helpers/Validators';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.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 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">
|
||||
{{ t('HELP_CENTER.PORTAL_SETTINGS.FORM.AVATAR.LABEL') }}
|
||||
</label>
|
||||
<EditableAvatar
|
||||
label="Avatar"
|
||||
<Avatar
|
||||
:src="state.logoUrl"
|
||||
:name="state.name"
|
||||
:size="72"
|
||||
allow-upload
|
||||
icon-name="i-lucide-building-2"
|
||||
@upload="handleAvatarUpload"
|
||||
@delete="handleAvatarDelete"
|
||||
/>
|
||||
|
||||
@@ -120,29 +120,27 @@ defineExpose({ dialogRef });
|
||||
:is-loading="isCreatingPortal"
|
||||
@confirm="handleDialogConfirm"
|
||||
>
|
||||
<template #form>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Input
|
||||
id="portal-name"
|
||||
v-model="state.name"
|
||||
type="text"
|
||||
:placeholder="t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.PLACEHOLDER')"
|
||||
:label="t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.LABEL')"
|
||||
:message-type="nameError ? 'error' : 'info'"
|
||||
:message="
|
||||
nameError || t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.MESSAGE')
|
||||
"
|
||||
/>
|
||||
<Input
|
||||
id="portal-slug"
|
||||
v-model="state.slug"
|
||||
type="text"
|
||||
:placeholder="t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.PLACEHOLDER')"
|
||||
:label="t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.LABEL')"
|
||||
:message-type="slugError ? 'error' : 'info'"
|
||||
:message="slugError || buildPortalURL(state.slug)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Input
|
||||
id="portal-name"
|
||||
v-model="state.name"
|
||||
type="text"
|
||||
:placeholder="t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.PLACEHOLDER')"
|
||||
:label="t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.LABEL')"
|
||||
:message-type="nameError ? 'error' : 'info'"
|
||||
:message="
|
||||
nameError || t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.MESSAGE')
|
||||
"
|
||||
/>
|
||||
<Input
|
||||
id="portal-slug"
|
||||
v-model="state.slug"
|
||||
type="text"
|
||||
:placeholder="t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.PLACEHOLDER')"
|
||||
:label="t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.LABEL')"
|
||||
:message-type="slugError ? 'error' : 'info'"
|
||||
:message="slugError || buildPortalURL(state.slug)"
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||
import { buildPortalURL } from 'dashboard/helper/portalHelper';
|
||||
|
||||
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']);
|
||||
|
||||
@@ -148,14 +148,13 @@ const redirectToPortalHomePage = () => {
|
||||
<span class="text-sm font-medium truncate text-n-slate-12">
|
||||
{{ portal.name || '' }}
|
||||
</span>
|
||||
<Thumbnail
|
||||
<Avatar
|
||||
v-if="portal"
|
||||
:author="portal"
|
||||
:name="portal.name"
|
||||
:size="20"
|
||||
:src="getPortalThumbnailSrc(portal)"
|
||||
:show-author-name="false"
|
||||
:size="20"
|
||||
icon-name="i-lucide-building-2"
|
||||
rounded-full
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -5,37 +5,30 @@ import Avatar from './Avatar.vue';
|
||||
<template>
|
||||
<Story title="Components/Avatar" :layout="{ type: 'grid', width: '400' }">
|
||||
<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
|
||||
name=""
|
||||
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>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Default with upload">
|
||||
<div class="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">
|
||||
<Variant title="Different Shapes">
|
||||
<div class="gap-4 p-4 bg-white dark:bg-slate-900">
|
||||
<Avatar
|
||||
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Amaya"
|
||||
name=""
|
||||
allow-upload
|
||||
rounded-full
|
||||
:size="48"
|
||||
/>
|
||||
<Avatar
|
||||
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Amaya"
|
||||
name=""
|
||||
allow-upload
|
||||
:size="48"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
@@ -43,24 +36,78 @@ import Avatar from './Avatar.vue';
|
||||
<Variant title="Different Sizes">
|
||||
<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"
|
||||
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Felix"
|
||||
:size="48"
|
||||
class="bg-green-300 dark:bg-green-900"
|
||||
name=""
|
||||
allow-upload
|
||||
/>
|
||||
<Avatar
|
||||
:size="72"
|
||||
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Jade"
|
||||
class="bg-indigo-300 dark:bg-indigo-900"
|
||||
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Jade"
|
||||
name=""
|
||||
allow-upload
|
||||
/>
|
||||
<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"
|
||||
class="bg-woot-300 dark:bg-woot-900"
|
||||
allow-upload
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -39,11 +39,12 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['upload']);
|
||||
const emit = defineEmits(['upload', 'delete']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const isImageValid = ref(true);
|
||||
const fileInput = ref(null);
|
||||
|
||||
const AVATAR_COLORS = {
|
||||
dark: [
|
||||
@@ -131,13 +132,39 @@ const iconStyles = computed(() => ({
|
||||
}));
|
||||
|
||||
const initialsStyles = computed(() => ({
|
||||
fontSize: `${props.size / 1.8}px`,
|
||||
fontSize: `${props.size / 2}px`,
|
||||
}));
|
||||
|
||||
const invalidateCurrentImage = () => {
|
||||
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(
|
||||
() => props.src,
|
||||
() => {
|
||||
@@ -147,7 +174,7 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="relative inline-flex" :style="containerStyles">
|
||||
<span class="relative inline-flex group/avatar" :style="containerStyles">
|
||||
<!-- Status Badge -->
|
||||
<slot name="badge" :size="size">
|
||||
<div
|
||||
@@ -158,6 +185,15 @@ watch(
|
||||
/>
|
||||
</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 -->
|
||||
<span
|
||||
role="img"
|
||||
@@ -202,16 +238,23 @@ watch(
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Upload Overlay -->
|
||||
<!-- Upload Overlay and Input -->
|
||||
<div
|
||||
v-if="allowUpload"
|
||||
role="button"
|
||||
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="emit('upload')"
|
||||
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"
|
||||
@click="handleUploadAvatar"
|
||||
>
|
||||
<Icon
|
||||
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>
|
||||
</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({
|
||||
label: {
|
||||
type: String,
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
variant: {
|
||||
@@ -47,9 +47,9 @@ const STYLE_CONFIG = {
|
||||
blue: {
|
||||
solid: 'bg-n-brand text-white hover:brightness-110 outline-transparent',
|
||||
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',
|
||||
link: 'text-n-brand hover:underline outline-transparent',
|
||||
link: 'text-n-blue-text hover:underline outline-transparent',
|
||||
},
|
||||
ruby: {
|
||||
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" />
|
||||
|
||||
<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>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -43,7 +43,7 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const emit = defineEmits(['update:modelValue', 'search']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -118,13 +118,13 @@ watch(
|
||||
|
||||
<ComboBoxDropdown
|
||||
ref="dropdownRef"
|
||||
v-model:search-value="search"
|
||||
:open="open"
|
||||
:options="filteredOptions"
|
||||
:search-value="search"
|
||||
:search-placeholder="searchPlaceholder"
|
||||
:empty-state="emptyState"
|
||||
:selected-values="selectedValue"
|
||||
@update:search-value="search = $event"
|
||||
@search="emit('search', $event)"
|
||||
@select="selectOption"
|
||||
/>
|
||||
|
||||
|
||||
@@ -11,10 +11,6 @@ const props = defineProps({
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
searchValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
searchPlaceholder: {
|
||||
type: String,
|
||||
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 searchValue = defineModel('searchValue', {
|
||||
type: String,
|
||||
default: '',
|
||||
});
|
||||
|
||||
const searchInput = ref(null);
|
||||
|
||||
const isSelected = option => {
|
||||
@@ -46,6 +47,11 @@ const isSelected = option => {
|
||||
return option.value === props.selectedValues;
|
||||
};
|
||||
|
||||
const onInputSearch = event => {
|
||||
searchValue.value = event.target.value;
|
||||
emit('search', event.target.value);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
focus: () => searchInput.value?.focus(),
|
||||
});
|
||||
@@ -64,7 +70,7 @@ defineExpose({
|
||||
type="search"
|
||||
: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"
|
||||
@input="emit('update:searchValue', $event.target.value)"
|
||||
@input="onInputSearch"
|
||||
/>
|
||||
</div>
|
||||
<ul
|
||||
|
||||
@@ -7,6 +7,7 @@ import Input from 'dashboard/components-next/input/Input.vue';
|
||||
const alertDialog = ref(null);
|
||||
const editDialog = ref(null);
|
||||
const confirmDialog = ref(null);
|
||||
const confirmDialogWithCustomFooter = ref(null);
|
||||
|
||||
const openAlertDialog = () => {
|
||||
alertDialog.value.open();
|
||||
@@ -17,6 +18,9 @@ const openEditDialog = () => {
|
||||
const openConfirmDialog = () => {
|
||||
confirmDialog.value.open();
|
||||
};
|
||||
const openConfirmDialogWithCustomFooter = () => {
|
||||
confirmDialogWithCustomFooter.value.open();
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const onConfirm = dialog => {};
|
||||
@@ -44,24 +48,22 @@ const onConfirm = dialog => {};
|
||||
confirm-button-label="Save"
|
||||
@confirm="onConfirm()"
|
||||
>
|
||||
<template #form>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Input
|
||||
id="portal-name"
|
||||
type="text"
|
||||
placeholder="User Guide | Chatwoot"
|
||||
label="Name"
|
||||
message="This will be the name of your public facing portal"
|
||||
/>
|
||||
<Input
|
||||
id="portal-slug"
|
||||
type="text"
|
||||
placeholder="user-guide"
|
||||
label="Slug"
|
||||
message="app.chatwoot.com/hc/my-portal/en-US/categories/my-slug"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Input
|
||||
id="portal-name"
|
||||
type="text"
|
||||
placeholder="User Guide | Chatwoot"
|
||||
label="Name"
|
||||
message="This will be the name of your public facing portal"
|
||||
/>
|
||||
<Input
|
||||
id="portal-slug"
|
||||
type="text"
|
||||
placeholder="user-guide"
|
||||
label="Slug"
|
||||
message="app.chatwoot.com/hc/my-portal/en-US/categories/my-slug"
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Variant>
|
||||
|
||||
@@ -77,5 +79,21 @@ const onConfirm = dialog => {};
|
||||
@confirm="onConfirm()"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { OnClickOutside } from '@vueuse/components';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'edit',
|
||||
@@ -14,7 +14,7 @@ defineProps({
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
@@ -48,6 +48,11 @@ defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: 'lg',
|
||||
validator: value => ['3xl', '2xl', 'xl', 'lg', 'md', 'sm'].includes(value),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['confirm', 'close']);
|
||||
@@ -59,6 +64,19 @@ const isRTL = useMapGetter('accounts/isRTL');
|
||||
const dialogRef = 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 = () => {
|
||||
dialogRef.value?.showModal();
|
||||
};
|
||||
@@ -77,8 +95,11 @@ defineExpose({ open, close });
|
||||
<Teleport to="body">
|
||||
<dialog
|
||||
ref="dialogRef"
|
||||
class="w-full max-w-lg transition-all duration-300 ease-in-out shadow-xl rounded-xl"
|
||||
:class="overflowYAuto ? 'overflow-y-auto' : 'overflow-visible'"
|
||||
class="w-full transition-all duration-300 ease-in-out shadow-xl rounded-xl"
|
||||
:class="[
|
||||
maxWidthClass,
|
||||
overflowYAuto ? 'overflow-y-auto' : 'overflow-visible',
|
||||
]"
|
||||
:dir="isRTL ? 'rtl' : 'ltr'"
|
||||
@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"
|
||||
@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">
|
||||
{{ title }}
|
||||
</h3>
|
||||
@@ -98,28 +119,29 @@ defineExpose({ open, close });
|
||||
</p>
|
||||
</slot>
|
||||
</div>
|
||||
<slot name="form">
|
||||
<!-- Form content will be injected here -->
|
||||
<slot />
|
||||
<!-- Dialog content will be injected here -->
|
||||
<slot name="footer">
|
||||
<div class="flex items-center justify-between w-full gap-3">
|
||||
<Button
|
||||
v-if="showCancelButton"
|
||||
variant="faded"
|
||||
color="slate"
|
||||
:label="cancelButtonLabel || t('DIALOG.BUTTONS.CANCEL')"
|
||||
class="w-full"
|
||||
@click="close"
|
||||
/>
|
||||
<Button
|
||||
v-if="showConfirmButton"
|
||||
:color="type === 'edit' ? 'blue' : 'ruby'"
|
||||
:label="confirmButtonLabel || t('DIALOG.BUTTONS.CONFIRM')"
|
||||
class="w-full"
|
||||
:is-loading="isLoading"
|
||||
:disabled="disableConfirmButton || isLoading"
|
||||
@click="confirm"
|
||||
/>
|
||||
</div>
|
||||
</slot>
|
||||
<div class="flex items-center justify-between w-full gap-3">
|
||||
<Button
|
||||
v-if="showCancelButton"
|
||||
variant="faded"
|
||||
color="slate"
|
||||
:label="cancelButtonLabel || t('DIALOG.BUTTONS.CANCEL')"
|
||||
class="w-full"
|
||||
@click="close"
|
||||
/>
|
||||
<Button
|
||||
v-if="showConfirmButton"
|
||||
:color="type === 'edit' ? 'blue' : 'ruby'"
|
||||
:label="confirmButtonLabel || t('DIALOG.BUTTONS.CONFIRM')"
|
||||
class="w-full"
|
||||
:is-loading="isLoading"
|
||||
:disabled="disableConfirmButton || isLoading"
|
||||
@click="confirm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</OnClickOutside>
|
||||
</dialog>
|
||||
|
||||
@@ -51,5 +51,19 @@ const handleAction = () => {
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<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 Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
menuItems: {
|
||||
type: Array,
|
||||
required: true,
|
||||
@@ -16,21 +17,61 @@ defineProps({
|
||||
type: Number,
|
||||
default: 20,
|
||||
},
|
||||
showSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
searchPlaceholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['action']);
|
||||
|
||||
const handleAction = (action, value) => {
|
||||
emit('action', { action, value });
|
||||
const { t } = useI18n();
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
>
|
||||
<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
|
||||
v-for="item in menuItems"
|
||||
v-for="item in filteredMenuItems"
|
||||
: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="{
|
||||
@@ -39,20 +80,29 @@ const handleAction = (action, value) => {
|
||||
'text-n-slate-12': item.action !== 'delete',
|
||||
}"
|
||||
:disabled="item.disabled"
|
||||
@click="handleAction(item.action, item.value)"
|
||||
@click="handleAction(item)"
|
||||
@keydown.enter="handleAction(item)"
|
||||
>
|
||||
<Thumbnail
|
||||
v-if="item.thumbnail"
|
||||
:author="item.thumbnail"
|
||||
:name="item.thumbnail.name"
|
||||
:size="thumbnailSize"
|
||||
:src="item.thumbnail.src"
|
||||
/>
|
||||
<slot name="thumbnail" :item="item">
|
||||
<Avatar
|
||||
v-if="item.thumbnail"
|
||||
:name="item.thumbnail.name"
|
||||
:src="item.thumbnail.src"
|
||||
:size="thumbnailSize"
|
||||
rounded-full
|
||||
/>
|
||||
</slot>
|
||||
<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.label" class="min-w-0 text-sm truncate">{{
|
||||
item.label
|
||||
}}</span>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref, onMounted, nextTick } from 'vue';
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
@@ -42,9 +42,22 @@ const props = defineProps({
|
||||
type: String,
|
||||
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(() => {
|
||||
switch (props.messageType) {
|
||||
@@ -62,7 +75,7 @@ const inputBorderClass = computed(() => {
|
||||
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';
|
||||
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('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>
|
||||
|
||||
<template>
|
||||
@@ -85,15 +120,25 @@ const handleInput = event => {
|
||||
<slot name="prefix" />
|
||||
<input
|
||||
:id="id"
|
||||
ref="inputRef"
|
||||
:value="modelValue"
|
||||
:class="[customInputClass, inputBorderClass]"
|
||||
:class="[
|
||||
customInputClass,
|
||||
inputBorderClass,
|
||||
{
|
||||
error: messageType === 'error',
|
||||
focus: isFocused,
|
||||
},
|
||||
]"
|
||||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
: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"
|
||||
@blur="emit('blur')"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@keyup.enter="handleEnter"
|
||||
/>
|
||||
<p
|
||||
v-if="message"
|
||||
|
||||
@@ -46,6 +46,13 @@ const handleClickOutside = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeydown = event => {
|
||||
if (event.key === ',') {
|
||||
event.preventDefault();
|
||||
addTag();
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
newValue => {
|
||||
@@ -81,6 +88,7 @@ watch(
|
||||
:placeholder="placeholder"
|
||||
custom-input-class="flex-grow"
|
||||
@enter-press="addTag"
|
||||
@keydown="handleKeydown"
|
||||
/>
|
||||
</div>
|
||||
</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.",
|
||||
"SEARCH_PLACEHOLDER": "Search..."
|
||||
},
|
||||
"DROPDOWN_MENU": {
|
||||
"SEARCH_PLACEHOLDER": "Search...",
|
||||
"EMPTY_STATE": "No results found."
|
||||
},
|
||||
"DIALOG": {
|
||||
"BUTTONS": {
|
||||
"CANCEL": "Cancel",
|
||||
|
||||
Reference in New Issue
Block a user