monacousa-portal/design-mockups/components/core/ProfessionalButton.vue

315 lines
7.0 KiB
Vue

<template>
<button
:class="[
'professional-button',
variantClasses,
sizeClasses,
{
'professional-button--loading': loading,
'professional-button--disabled': disabled || loading,
'professional-button--block': block,
'professional-button--icon-only': !$slots.default && icon
}
]"
:disabled="disabled || loading"
@click="handleClick"
>
<span v-if="loading" class="professional-button__spinner">
<svg class="animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span>
<span v-if="icon && !loading" class="professional-button__icon">
<component :is="icon" />
</span>
<span v-if="$slots.default" class="professional-button__content">
<slot></slot>
</span>
<span v-if="endIcon && !loading" class="professional-button__end-icon">
<component :is="endIcon" />
</span>
</button>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import type { Component } from 'vue';
interface Props {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
disabled?: boolean;
block?: boolean;
icon?: Component;
endIcon?: Component;
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'md',
loading: false,
disabled: false,
block: false
});
const emit = defineEmits<{
click: [event: MouseEvent];
}>();
const variantClasses = computed(() => {
const variants = {
primary: 'professional-button--primary',
secondary: 'professional-button--secondary',
outline: 'professional-button--outline',
ghost: 'professional-button--ghost',
danger: 'professional-button--danger'
};
return variants[props.variant];
});
const sizeClasses = computed(() => {
const sizes = {
sm: 'professional-button--sm',
md: 'professional-button--md',
lg: 'professional-button--lg'
};
return sizes[props.size];
});
const handleClick = (event: MouseEvent) => {
if (!props.disabled && !props.loading) {
emit('click', event);
}
};
</script>
<style lang="scss" scoped>
@import '../../styles/neumorphic-system.scss';
.professional-button {
display: inline-flex;
align-items: center;
justify-center: center;
gap: $space-2;
border: none;
border-radius: $radius-lg;
font-family: $font-body;
font-weight: $font-medium;
cursor: pointer;
transition: all $transition-base $ease-in-out-soft;
position: relative;
overflow: hidden;
// Size variants
&--sm {
padding: $space-2 $space-4;
font-size: $text-sm;
min-height: $button-height-sm;
&.professional-button--icon-only {
width: $button-height-sm;
padding: $space-2;
}
}
&--md {
padding: $space-3 $space-5;
font-size: $text-base;
min-height: $button-height-md;
&.professional-button--icon-only {
width: $button-height-md;
padding: $space-3;
}
}
&--lg {
padding: $space-4 $space-6;
font-size: $text-lg;
min-height: $button-height-lg;
&.professional-button--icon-only {
width: $button-height-lg;
padding: $space-4;
}
}
// Variant styles
&--primary {
background: linear-gradient(135deg, $primary-500, $primary-600);
color: white;
box-shadow: $shadow-soft-sm;
&:hover:not(.professional-button--disabled) {
background: linear-gradient(135deg, $primary-600, $primary-700);
box-shadow: $shadow-soft-md;
transform: translateY(-1px);
}
&:active:not(.professional-button--disabled) {
box-shadow: $shadow-inset-sm;
transform: translateY(0);
}
}
&--secondary {
background: linear-gradient(145deg, #ffffff, #f0f0f0);
color: $neutral-800;
box-shadow: $shadow-soft-sm;
&:hover:not(.professional-button--disabled) {
box-shadow: $shadow-soft-md;
transform: translateY(-1px);
}
&:active:not(.professional-button--disabled) {
box-shadow: $shadow-inset-sm;
transform: translateY(0);
}
}
&--outline {
background: transparent;
color: $primary-600;
border: 2px solid $primary-200;
box-shadow: none;
&:hover:not(.professional-button--disabled) {
background: rgba($primary-500, 0.05);
border-color: $primary-300;
}
&:active:not(.professional-button--disabled) {
background: rgba($primary-500, 0.1);
}
}
&--ghost {
background: transparent;
color: $neutral-700;
box-shadow: none;
&:hover:not(.professional-button--disabled) {
background: rgba($neutral-500, 0.08);
}
&:active:not(.professional-button--disabled) {
background: rgba($neutral-500, 0.12);
}
}
&--danger {
background: linear-gradient(135deg, $error-500, #dc2626);
color: white;
box-shadow: $shadow-soft-sm;
&:hover:not(.professional-button--disabled) {
background: linear-gradient(135deg, #dc2626, #b91c1c);
box-shadow: $shadow-soft-md;
transform: translateY(-1px);
}
&:active:not(.professional-button--disabled) {
box-shadow: $shadow-inset-sm;
transform: translateY(0);
}
}
// States
&--block {
width: 100%;
}
&--disabled {
opacity: 0.5;
cursor: not-allowed;
}
&--loading {
color: transparent;
}
// Icons
&__spinner {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 1.25rem;
height: 1.25rem;
color: inherit;
svg {
width: 100%;
height: 100%;
}
}
&__icon,
&__end-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.25em;
height: 1.25em;
flex-shrink: 0;
}
&__content {
flex: 1;
text-align: center;
}
// Focus state
&:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba($primary-500, 0.2);
}
// Dark mode support
@include dark-mode {
&--secondary {
background: linear-gradient(145deg, $neutral-700, $neutral-800);
color: $neutral-100;
&:hover:not(.professional-button--disabled) {
background: linear-gradient(145deg, $neutral-600, $neutral-700);
}
}
&--ghost {
color: $neutral-300;
&:hover:not(.professional-button--disabled) {
background: rgba($neutral-400, 0.1);
}
}
&--outline {
color: $primary-400;
border-color: $primary-800;
&:hover:not(.professional-button--disabled) {
background: rgba($primary-400, 0.1);
border-color: $primary-700;
}
}
}
}
// Animation
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style>