Merge branch 'develop' into feat/contacts-redesign
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
|
||||
defineProps({
|
||||
labelMenuItems: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['updateLabel']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const showDropdown = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative">
|
||||
<button
|
||||
class="flex items-center gap-1 px-2 py-1 rounded-md outline-dashed h-[26px] outline-1 outline-n-slate-6 hover:bg-n-alpha-2"
|
||||
:class="{ 'bg-n-alpha-2': showDropdown }"
|
||||
@click="showDropdown = !showDropdown"
|
||||
>
|
||||
<span class="i-lucide-plus" />
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ t('LABEL.TAG_BUTTON') }}
|
||||
</span>
|
||||
</button>
|
||||
<DropdownMenu
|
||||
v-if="showDropdown"
|
||||
v-on-clickaway="() => (showDropdown = false)"
|
||||
:menu-items="labelMenuItems"
|
||||
show-search
|
||||
class="z-[100] w-48 mt-2 overflow-y-auto ltr:left-0 rtl:right-0 top-full max-h-52"
|
||||
@action="emit('updateLabel', $event)"
|
||||
>
|
||||
<template #thumbnail="{ item }">
|
||||
<div
|
||||
class="rounded-sm size-2"
|
||||
:style="{ backgroundColor: item.thumbnail.color }"
|
||||
/>
|
||||
</template>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
label: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-n-alpha-2 rounded-md flex items-center h-7 w-fit py-1 ltr:pl-1 rtl:pr-1 ltr:pr-1.5 rtl:pl-1.5"
|
||||
>
|
||||
<div
|
||||
class="w-2 h-2 m-1 rounded-sm"
|
||||
:style="{ backgroundColor: label.color }"
|
||||
/>
|
||||
<span class="text-sm text-n-slate-12">
|
||||
{{ label.title }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
import AddLabel from '../AddLabel.vue';
|
||||
import { labelMenuItems } from './fixtures';
|
||||
|
||||
function onUpdateLabel(label) {
|
||||
console.log('Label updated:', label);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/Label/Add Label">
|
||||
<Variant title="Default (button with label menu items with active state)">
|
||||
<div class="h-[300px] p-4">
|
||||
<AddLabel
|
||||
:label-menu-items="labelMenuItems"
|
||||
@update-label="onUpdateLabel"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Empty List (button with empty label menu)">
|
||||
<div class="h-[300px] p-4">
|
||||
<AddLabel :label-menu-items="[]" @update-label="onUpdateLabel" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import Label from '../LabelItem.vue';
|
||||
import { label } from './fixtures';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/Label/Label item">
|
||||
<Variant title="Default">
|
||||
<Label :label="label" />
|
||||
</Variant>
|
||||
|
||||
<Variant title="Custom Label">
|
||||
<Label
|
||||
:label="{
|
||||
title: 'Custom Label',
|
||||
color: '#FF5733',
|
||||
}"
|
||||
/>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,62 @@
|
||||
export const label = {
|
||||
id: 1,
|
||||
title: 'delivery',
|
||||
color: '#A2FDD5',
|
||||
};
|
||||
|
||||
export const labelMenuItems = [
|
||||
{
|
||||
label: 'delivery',
|
||||
value: 3,
|
||||
thumbnail: {
|
||||
color: '#A2FDD5',
|
||||
},
|
||||
isSelected: true,
|
||||
action: 'addLabel',
|
||||
},
|
||||
{
|
||||
label: 'lead',
|
||||
value: 6,
|
||||
thumbnail: {
|
||||
color: '#F161C8',
|
||||
},
|
||||
isSelected: false,
|
||||
action: 'addLabel',
|
||||
},
|
||||
{
|
||||
label: 'ops-handover',
|
||||
value: 4,
|
||||
thumbnail: {
|
||||
color: '#A53326',
|
||||
},
|
||||
isSelected: false,
|
||||
action: 'addLabel',
|
||||
},
|
||||
{
|
||||
label: 'billing',
|
||||
value: 1,
|
||||
thumbnail: {
|
||||
color: '#28AD21',
|
||||
},
|
||||
isSelected: false,
|
||||
action: 'addLabel',
|
||||
},
|
||||
{
|
||||
label: 'premium-customer',
|
||||
value: 5,
|
||||
thumbnail: {
|
||||
color: '#6FD4EF',
|
||||
},
|
||||
isSelected: false,
|
||||
action: 'addLabel',
|
||||
},
|
||||
{
|
||||
label: 'software',
|
||||
value: 2,
|
||||
thumbnail: {
|
||||
color: '#8F6EF2',
|
||||
},
|
||||
isSelected: false,
|
||||
action: 'addLabel',
|
||||
},
|
||||
];
|
||||
@@ -1,46 +1,85 @@
|
||||
<script setup>
|
||||
import { computed, useSlots } from 'vue';
|
||||
import { computed, useSlots, useAttrs } from 'vue';
|
||||
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import {
|
||||
VARIANT_OPTIONS,
|
||||
COLOR_OPTIONS,
|
||||
SIZE_OPTIONS,
|
||||
EXCLUDED_ATTRS,
|
||||
} from './constants.js';
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
label: { type: [String, Number], default: '' },
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'solid',
|
||||
validator: value =>
|
||||
['solid', 'outline', 'faded', 'link', 'ghost'].includes(value),
|
||||
default: null,
|
||||
validator: value => VARIANT_OPTIONS.includes(value) || value === null,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'blue',
|
||||
validator: value =>
|
||||
['blue', 'ruby', 'amber', 'slate', 'teal'].includes(value),
|
||||
default: null,
|
||||
validator: value => COLOR_OPTIONS.includes(value) || value === null,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator: value => ['xs', 'sm', 'md', 'lg'].includes(value),
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
trailingIcon: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
default: null,
|
||||
validator: value => SIZE_OPTIONS.includes(value) || value === null,
|
||||
},
|
||||
icon: { type: [String, Object, Function], default: '' },
|
||||
trailingIcon: { type: Boolean, default: false },
|
||||
isLoading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
const attrs = useAttrs();
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const filteredAttrs = computed(() => {
|
||||
const standardAttrs = {};
|
||||
|
||||
Object.entries(attrs)
|
||||
.filter(([key]) => !EXCLUDED_ATTRS.includes(key))
|
||||
.forEach(([key, value]) => {
|
||||
standardAttrs[key] = value;
|
||||
});
|
||||
|
||||
return standardAttrs;
|
||||
});
|
||||
|
||||
const computedVariant = computed(() => {
|
||||
if (props.variant) return props.variant;
|
||||
// The useAttrs method returns attributes values an empty string (not boolean value as in props).
|
||||
if (attrs.solid || attrs.solid === '') return 'solid';
|
||||
if (attrs.outline || attrs.outline === '') return 'outline';
|
||||
if (attrs.faded || attrs.faded === '') return 'faded';
|
||||
if (attrs.link || attrs.link === '') return 'link';
|
||||
if (attrs.ghost || attrs.ghost === '') return 'ghost';
|
||||
return 'solid'; // Default variant
|
||||
});
|
||||
|
||||
const computedColor = computed(() => {
|
||||
if (props.color) return props.color;
|
||||
if (attrs.blue || attrs.blue === '') return 'blue';
|
||||
if (attrs.ruby || attrs.ruby === '') return 'ruby';
|
||||
if (attrs.amber || attrs.amber === '') return 'amber';
|
||||
if (attrs.slate || attrs.slate === '') return 'slate';
|
||||
if (attrs.teal || attrs.teal === '') return 'teal';
|
||||
return 'blue'; // Default color
|
||||
});
|
||||
|
||||
const computedSize = computed(() => {
|
||||
if (props.size) return props.size;
|
||||
if (attrs.xs || attrs.xs === '') return 'xs';
|
||||
if (attrs.sm || attrs.sm === '') return 'sm';
|
||||
if (attrs.md || attrs.md === '') return 'md';
|
||||
if (attrs.lg || attrs.lg === '') return 'lg';
|
||||
return 'md';
|
||||
});
|
||||
|
||||
const STYLE_CONFIG = {
|
||||
colors: {
|
||||
@@ -113,23 +152,24 @@ const STYLE_CONFIG = {
|
||||
const variantClasses = computed(() => {
|
||||
const variantMap = {
|
||||
ghost: 'text-n-slate-12 hover:bg-n-alpha-2 outline-transparent',
|
||||
link: `${STYLE_CONFIG.colors[props.color].link} p-0 font-medium underline-offset-4`,
|
||||
outline: STYLE_CONFIG.colors[props.color].outline,
|
||||
faded: STYLE_CONFIG.colors[props.color].faded,
|
||||
solid: STYLE_CONFIG.colors[props.color].solid,
|
||||
link: `${STYLE_CONFIG.colors[computedColor.value].link} p-0 font-medium underline-offset-4`,
|
||||
outline: STYLE_CONFIG.colors[computedColor.value].outline,
|
||||
faded: STYLE_CONFIG.colors[computedColor.value].faded,
|
||||
solid: STYLE_CONFIG.colors[computedColor.value].solid,
|
||||
};
|
||||
|
||||
return variantMap[props.variant];
|
||||
return variantMap[computedVariant.value];
|
||||
});
|
||||
|
||||
const isIconOnly = computed(() => !props.label && !slots.default);
|
||||
const isLink = computed(() => props.variant === 'link');
|
||||
const isLink = computed(() => computedVariant.value === 'link');
|
||||
|
||||
const buttonClasses = computed(() => {
|
||||
const sizeConfig = isIconOnly.value ? 'iconOnly' : 'regular';
|
||||
const classes = [
|
||||
variantClasses.value,
|
||||
props.variant !== 'link' && STYLE_CONFIG.sizes[sizeConfig][props.size],
|
||||
computedVariant.value !== 'link' &&
|
||||
STYLE_CONFIG.sizes[sizeConfig][computedSize.value],
|
||||
].filter(Boolean);
|
||||
|
||||
return classes.join(' ');
|
||||
@@ -138,7 +178,7 @@ const buttonClasses = computed(() => {
|
||||
const linkButtonClasses = computed(() => {
|
||||
const classes = [
|
||||
variantClasses.value,
|
||||
STYLE_CONFIG.sizes.link[props.size],
|
||||
STYLE_CONFIG.sizes.link[computedSize.value],
|
||||
].filter(Boolean);
|
||||
|
||||
return classes.join(' ');
|
||||
@@ -147,10 +187,11 @@ const linkButtonClasses = computed(() => {
|
||||
|
||||
<template>
|
||||
<button
|
||||
v-bind="filteredAttrs"
|
||||
:class="{
|
||||
[STYLE_CONFIG.base]: true,
|
||||
[isLink ? linkButtonClasses : buttonClasses]: true,
|
||||
[STYLE_CONFIG.fontSize[size]]: true,
|
||||
[STYLE_CONFIG.fontSize[computedSize]]: true,
|
||||
'flex-row-reverse': trailingIcon && !isIconOnly,
|
||||
}"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export const VARIANT_OPTIONS = ['solid', 'outline', 'faded', 'link', 'ghost'];
|
||||
export const COLOR_OPTIONS = ['blue', 'ruby', 'amber', 'slate', 'teal'];
|
||||
export const SIZE_OPTIONS = ['xs', 'sm', 'md', 'lg'];
|
||||
|
||||
export const EXCLUDED_ATTRS = [
|
||||
'variant',
|
||||
'color',
|
||||
'size',
|
||||
'icon',
|
||||
'trailingIcon',
|
||||
'isLoading',
|
||||
...VARIANT_OPTIONS,
|
||||
...COLOR_OPTIONS,
|
||||
...SIZE_OPTIONS,
|
||||
];
|
||||
Reference in New Issue
Block a user