291 lines
5.1 KiB
Vue
291 lines
5.1 KiB
Vue
<template>
|
|
<button
|
|
:type="type"
|
|
:disabled="disabled || loading"
|
|
:class="buttonClasses"
|
|
@click="handleClick"
|
|
>
|
|
<span v-if="loading" class="spinner" aria-hidden="true"></span>
|
|
<span
|
|
v-if="icon && iconPosition === 'left' && !loading"
|
|
class="icon icon-left"
|
|
>
|
|
{{ icon }}
|
|
</span>
|
|
<span v-if="$slots.default" :class="{ 'sr-only': loading }">
|
|
<slot></slot>
|
|
</span>
|
|
<span
|
|
v-if="icon && iconPosition === 'right' && !loading"
|
|
class="icon icon-right"
|
|
>
|
|
{{ icon }}
|
|
</span>
|
|
</button>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue';
|
|
|
|
const props = defineProps({
|
|
/**
|
|
* Button variant style
|
|
* @type {'primary' | 'secondary' | 'danger' | 'ghost' | 'icon-only'}
|
|
*/
|
|
variant: {
|
|
type: String,
|
|
default: 'primary',
|
|
validator: value =>
|
|
['primary', 'secondary', 'danger', 'ghost', 'icon-only'].includes(value)
|
|
},
|
|
|
|
/**
|
|
* Button size
|
|
* @type {'small' | 'medium' | 'large'}
|
|
*/
|
|
size: {
|
|
type: String,
|
|
default: 'medium',
|
|
validator: value => ['small', 'medium', 'large'].includes(value)
|
|
},
|
|
|
|
/**
|
|
* Whether button is in loading state
|
|
*/
|
|
loading: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
/**
|
|
* Whether button is disabled
|
|
*/
|
|
disabled: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
/**
|
|
* Icon character or emoji to display
|
|
*/
|
|
icon: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
|
|
/**
|
|
* Icon position relative to text
|
|
* @type {'left' | 'right'}
|
|
*/
|
|
iconPosition: {
|
|
type: String,
|
|
default: 'left',
|
|
validator: value => ['left', 'right'].includes(value)
|
|
},
|
|
|
|
/**
|
|
* Whether button should take full width of container
|
|
*/
|
|
fullWidth: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
/**
|
|
* Button type attribute
|
|
* @type {'button' | 'submit' | 'reset'}
|
|
*/
|
|
type: {
|
|
type: String,
|
|
default: 'button',
|
|
validator: value => ['button', 'submit', 'reset'].includes(value)
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['click']);
|
|
|
|
const buttonClasses = computed(() => [
|
|
'base-button',
|
|
`base-button--${props.variant}`,
|
|
`base-button--${props.size}`,
|
|
{
|
|
'base-button--loading': props.loading,
|
|
'base-button--disabled': props.disabled,
|
|
'base-button--full-width': props.fullWidth,
|
|
'base-button--icon-only':
|
|
props.variant === 'icon-only' || (!props.$slots.default && props.icon)
|
|
}
|
|
]);
|
|
|
|
const handleClick = event => {
|
|
if (!props.disabled && !props.loading) {
|
|
emit('click', event);
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.base-button {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
font-family: inherit;
|
|
font-weight: 500;
|
|
line-height: 1.5;
|
|
border: none;
|
|
border-radius: 0.375rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
position: relative;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.base-button:focus-visible {
|
|
outline: 2px solid #3b82f6;
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* Sizes */
|
|
.base-button--small {
|
|
padding: 0.375rem 0.75rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.base-button--medium {
|
|
padding: 0.5rem 1rem;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.base-button--large {
|
|
padding: 0.75rem 1.5rem;
|
|
font-size: 1.125rem;
|
|
}
|
|
|
|
/* Variants */
|
|
.base-button--primary {
|
|
background-color: #3b82f6;
|
|
color: white;
|
|
}
|
|
|
|
.base-button--primary:hover:not(:disabled) {
|
|
background-color: #2563eb;
|
|
}
|
|
|
|
.base-button--primary:active:not(:disabled) {
|
|
background-color: #1d4ed8;
|
|
}
|
|
|
|
.base-button--secondary {
|
|
background-color: #e5e7eb;
|
|
color: #1f2937;
|
|
}
|
|
|
|
.base-button--secondary:hover:not(:disabled) {
|
|
background-color: #d1d5db;
|
|
}
|
|
|
|
.base-button--secondary:active:not(:disabled) {
|
|
background-color: #9ca3af;
|
|
}
|
|
|
|
.base-button--danger {
|
|
background-color: #ef4444;
|
|
color: white;
|
|
}
|
|
|
|
.base-button--danger:hover:not(:disabled) {
|
|
background-color: #dc2626;
|
|
}
|
|
|
|
.base-button--danger:active:not(:disabled) {
|
|
background-color: #b91c1c;
|
|
}
|
|
|
|
.base-button--ghost {
|
|
background-color: transparent;
|
|
color: #3b82f6;
|
|
border: 1px solid transparent;
|
|
}
|
|
|
|
.base-button--ghost:hover:not(:disabled) {
|
|
background-color: #eff6ff;
|
|
border-color: #3b82f6;
|
|
}
|
|
|
|
.base-button--ghost:active:not(:disabled) {
|
|
background-color: #dbeafe;
|
|
}
|
|
|
|
.base-button--icon-only {
|
|
padding: 0.5rem;
|
|
background-color: transparent;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.base-button--icon-only:hover:not(:disabled) {
|
|
background-color: #f3f4f6;
|
|
color: #1f2937;
|
|
}
|
|
|
|
.base-button--icon-only:active:not(:disabled) {
|
|
background-color: #e5e7eb;
|
|
}
|
|
|
|
/* States */
|
|
.base-button:disabled,
|
|
.base-button--disabled,
|
|
.base-button--loading {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.base-button--full-width {
|
|
width: 100%;
|
|
}
|
|
|
|
/* Loading spinner */
|
|
.spinner {
|
|
display: inline-block;
|
|
width: 1em;
|
|
height: 1em;
|
|
border: 2px solid currentColor;
|
|
border-right-color: transparent;
|
|
border-radius: 50%;
|
|
animation: spin 0.6s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
/* Icon spacing */
|
|
.icon {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.icon-left {
|
|
margin-right: -0.25rem;
|
|
}
|
|
|
|
.icon-right {
|
|
margin-left: -0.25rem;
|
|
}
|
|
|
|
/* Screen reader only */
|
|
.sr-only {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
padding: 0;
|
|
margin: -1px;
|
|
overflow: hidden;
|
|
clip: rect(0, 0, 0, 0);
|
|
white-space: nowrap;
|
|
border-width: 0;
|
|
}
|
|
</style>
|