315 lines
7.0 KiB
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> |