mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +00:00
feat: Update button component (#10362)
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, useSlots } from 'vue';
|
||||
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
@@ -11,49 +11,28 @@ const props = defineProps({
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
default: 'solid',
|
||||
validator: value =>
|
||||
[
|
||||
'default',
|
||||
'destructive',
|
||||
'outline',
|
||||
'secondary',
|
||||
'ghost',
|
||||
'link',
|
||||
].includes(value),
|
||||
['solid', 'outline', 'faded', 'link', 'ghost'].includes(value),
|
||||
},
|
||||
textVariant: {
|
||||
color: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: 'blue',
|
||||
validator: value =>
|
||||
['', 'default', 'success', 'warning', 'danger', 'info'].includes(value),
|
||||
['blue', 'ruby', 'amber', 'slate', 'teal'].includes(value),
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: value => ['default', 'sm', 'lg'].includes(value),
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
validator: value => ['button', 'submit', 'reset'].includes(value),
|
||||
default: 'md',
|
||||
validator: value => ['xs', 'sm', 'md', 'lg'].includes(value),
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
emoji: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
iconPosition: {
|
||||
type: String,
|
||||
default: 'left',
|
||||
validator: value => ['left', 'right'].includes(value),
|
||||
},
|
||||
iconLib: {
|
||||
type: String,
|
||||
default: 'fluent',
|
||||
trailingIcon: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
@@ -61,92 +40,128 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
const slots = useSlots();
|
||||
|
||||
const buttonVariants = {
|
||||
variant: {
|
||||
default:
|
||||
'bg-n-brand text-white dark:text-white hover:bg-woot-600 dark:hover:bg-woot-600',
|
||||
destructive: 'bg-n-ruby-9 text-white dark:text-white hover:bg-n-ruby-10',
|
||||
outline:
|
||||
'border border-n-weak dark:border-n-weak hover:border-n-slate-6 dark:hover:border-n-slate-6',
|
||||
secondary: 'bg-n-solid-3 text-n-slate-12 hover:bg-n-solid-2',
|
||||
ghost: 'text-n-slate-12',
|
||||
link: 'text-n-brand underline-offset-4 hover:underline dark:hover:underline',
|
||||
const STYLE_CONFIG = {
|
||||
colors: {
|
||||
blue: {
|
||||
solid: 'bg-n-brand text-white hover:bg-n-blue-text outline-transparent',
|
||||
faded:
|
||||
'bg-n-brand/10 text-n-slate-12 hover:bg-n-brand/20 outline-transparent',
|
||||
outline: 'text-n-blue-text hover:bg-n-brand/10 outline-n-blue-border',
|
||||
link: 'text-n-brand hover:underline outline-transparent',
|
||||
},
|
||||
ruby: {
|
||||
solid: 'bg-n-ruby-9 text-white hover:bg-n-ruby-10 outline-transparent',
|
||||
faded:
|
||||
'bg-n-ruby-9/10 text-n-slate-12 hover:bg-n-ruby-9/20 outline-transparent',
|
||||
outline: 'text-n-ruby-11 hover:bg-n-ruby-9/10 outline-n-ruby-9',
|
||||
link: 'text-n-ruby-9 hover:underline outline-transparent',
|
||||
},
|
||||
amber: {
|
||||
solid: 'bg-n-amber-9 text-white hover:bg-n-amber-10 outline-transparent',
|
||||
faded:
|
||||
'bg-n-amber-9/10 text-n-slate-12 hover:bg-n-amber-9/20 outline-transparent',
|
||||
outline: 'text-n-amber-11 hover:bg-n-amber-9/10 outline-n-amber-9',
|
||||
link: 'text-n-amber-9 hover:underline outline-transparent',
|
||||
},
|
||||
slate: {
|
||||
solid:
|
||||
'bg-n-solid-3 hover:bg-n-solid-2 text-n-slate-12 outline-n-container',
|
||||
faded:
|
||||
'bg-n-slate-9/10 text-n-slate-12 hover:bg-n-slate-9/20 outline-transparent',
|
||||
outline: 'text-n-slate-11 outline-n-strong hover:bg-n-slate-9/10',
|
||||
link: 'text-n-slate-11 hover:text-n-slate-12 hover:underline outline-transparent',
|
||||
},
|
||||
teal: {
|
||||
solid: 'bg-n-teal-9 text-white hover:bg-n-teal-10 outline-transparent',
|
||||
faded:
|
||||
'bg-n-teal-9/10 text-n-slate-12 hover:bg-n-teal-9/20 outline-transparent',
|
||||
outline: 'text-n-teal-11 hover:bg-n-teal-9/10 outline-n-teal-9',
|
||||
link: 'text-n-teal-9 hover:underline outline-transparent',
|
||||
},
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-8 px-3 py-1',
|
||||
lg: 'h-12 px-5 py-3',
|
||||
sizes: {
|
||||
regular: {
|
||||
xs: 'h-6 px-2',
|
||||
sm: 'h-8 px-3',
|
||||
md: 'h-10 px-4',
|
||||
lg: 'h-12 px-5',
|
||||
},
|
||||
iconOnly: {
|
||||
xs: 'h-6 w-6 p-0',
|
||||
sm: 'h-8 w-8 p-0',
|
||||
md: 'h-10 w-10 p-0',
|
||||
lg: 'h-12 w-12 p-0',
|
||||
},
|
||||
link: {
|
||||
xs: 'p-0',
|
||||
sm: 'p-0',
|
||||
md: 'p-0',
|
||||
lg: 'p-0',
|
||||
},
|
||||
},
|
||||
text: {
|
||||
default:
|
||||
'!text-n-brand dark:!text-n-brand hover:!text-woot-600 dark:hover:!text-woot-600',
|
||||
success:
|
||||
'!text-green-500 dark:!text-green-500 hover:!text-green-600 dark:hover:!text-green-600',
|
||||
warning:
|
||||
'!text-amber-600 dark:!text-amber-600 hover:!text-amber-600 dark:hover:!text-amber-600',
|
||||
danger: '!text-n-ruby-11 hover:!text-n-ruby-10',
|
||||
info: '!text-n-slate-12 hover:!text-n-slate-11',
|
||||
fontSize: {
|
||||
xs: 'text-xs',
|
||||
sm: 'text-sm',
|
||||
md: 'text-sm font-medium',
|
||||
lg: 'text-base',
|
||||
},
|
||||
base: 'inline-flex items-center justify-center min-w-0 gap-2 transition-all duration-200 ease-in-out border-0 rounded-lg outline-1 outline disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50',
|
||||
};
|
||||
|
||||
const buttonClasses = computed(() => {
|
||||
const classes = [
|
||||
buttonVariants.variant[props.variant],
|
||||
buttonVariants.size[props.size],
|
||||
];
|
||||
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,
|
||||
};
|
||||
|
||||
if (props.textVariant && buttonVariants.text[props.textVariant]) {
|
||||
classes.push(buttonVariants.text[props.textVariant]);
|
||||
}
|
||||
return variantMap[props.variant];
|
||||
});
|
||||
|
||||
const isIconOnly = computed(() => !props.label && !slots.default);
|
||||
const isLink = computed(() => props.variant === 'link');
|
||||
|
||||
const buttonClasses = computed(() => {
|
||||
const sizeConfig = isIconOnly.value ? 'iconOnly' : 'regular';
|
||||
const classes = [
|
||||
variantClasses.value,
|
||||
props.variant !== 'link' && STYLE_CONFIG.sizes[sizeConfig][props.size],
|
||||
].filter(Boolean);
|
||||
|
||||
return classes.join(' ');
|
||||
});
|
||||
|
||||
const iconSize = computed(() => {
|
||||
if (props.size === 'sm') return 16;
|
||||
if (props.size === 'lg') return 20;
|
||||
return 18;
|
||||
});
|
||||
const linkButtonClasses = computed(() => {
|
||||
const classes = [
|
||||
variantClasses.value,
|
||||
STYLE_CONFIG.sizes.link[props.size],
|
||||
].filter(Boolean);
|
||||
|
||||
const handleClick = e => {
|
||||
emit('click', e);
|
||||
};
|
||||
return classes.join(' ');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:class="buttonClasses"
|
||||
:type="type"
|
||||
class="inline-flex items-center justify-center min-w-0 gap-2 text-sm font-medium transition-all duration-200 ease-in-out rounded-lg disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50"
|
||||
@click="handleClick"
|
||||
:class="{
|
||||
[STYLE_CONFIG.base]: true,
|
||||
[isLink ? linkButtonClasses : buttonClasses]: true,
|
||||
[STYLE_CONFIG.fontSize[size]]: true,
|
||||
'flex-row-reverse': trailingIcon && !isIconOnly,
|
||||
}"
|
||||
>
|
||||
<FluentIcon
|
||||
v-if="icon && iconPosition === 'left' && !isLoading"
|
||||
:icon="icon"
|
||||
:size="iconSize"
|
||||
:icon-lib="iconLib"
|
||||
class="flex-shrink-0"
|
||||
:class="{
|
||||
'text-n-slate-11 dark:text-n-slate-11': variant === 'secondary',
|
||||
}"
|
||||
/>
|
||||
<slot v-if="(icon || $slots.icon) && !isLoading" name="icon">
|
||||
<Icon :icon="icon" class="flex-shrink-0" />
|
||||
</slot>
|
||||
|
||||
<Spinner v-if="isLoading" class="!w-5 !h-5 flex-shrink-0" />
|
||||
<slot name="leftPrefix" />
|
||||
<span v-if="emoji">{{ emoji }}</span>
|
||||
<span v-if="label" class="min-w-0 truncate">{{ label }}</span>
|
||||
<slot />
|
||||
<slot name="rightPrefix" />
|
||||
<FluentIcon
|
||||
v-if="icon && iconPosition === 'right'"
|
||||
:icon="icon"
|
||||
:size="iconSize"
|
||||
:icon-lib="iconLib"
|
||||
class="flex-shrink-0"
|
||||
:class="{
|
||||
'text-n-slate-11 dark:text-n-slate-11': variant === 'secondary',
|
||||
}"
|
||||
/>
|
||||
|
||||
<slot v-if="label || $slots.default" name="default">
|
||||
<span class="min-w-0 truncate">{{ label }}</span>
|
||||
</slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user