191 lines
3.7 KiB
Vue
191 lines
3.7 KiB
Vue
<template>
|
|
<button
|
|
:type="type"
|
|
:disabled="disabled"
|
|
:class="[
|
|
'base-button',
|
|
`base-button--${variant}`,
|
|
`base-button--${size}`,
|
|
{ 'base-button--loading': loading }
|
|
]"
|
|
:aria-label="ariaLabel"
|
|
:aria-describedby="ariaDescribedby"
|
|
@click="$emit('click', $event)"
|
|
@keydown="handleKeydown"
|
|
>
|
|
<span v-if="loading" class="base-button__spinner" aria-hidden="true"></span>
|
|
<span :class="{ 'base-button__content--hidden': loading }">
|
|
<slot />
|
|
</span>
|
|
</button>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
interface Props {
|
|
type?: 'button' | 'submit' | 'reset'
|
|
variant?: 'primary' | 'secondary' | 'danger' | 'ghost'
|
|
size?: 'sm' | 'md' | 'lg'
|
|
disabled?: boolean
|
|
loading?: boolean
|
|
ariaLabel?: string
|
|
ariaDescribedby?: string
|
|
}
|
|
|
|
withDefaults(defineProps<Props>(), {
|
|
type: 'button',
|
|
variant: 'primary',
|
|
size: 'md',
|
|
disabled: false,
|
|
loading: false
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
click: [event: MouseEvent]
|
|
}>()
|
|
|
|
const handleKeydown = (event: KeyboardEvent) => {
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault()
|
|
emit('click', event as any)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.base-button {
|
|
position: relative;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 1px solid transparent;
|
|
border-radius: 8px;
|
|
font-family: inherit;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
outline: none;
|
|
text-decoration: none;
|
|
/* iOS-specific optimizations */
|
|
-webkit-appearance: none;
|
|
-webkit-tap-highlight-color: transparent;
|
|
-webkit-touch-callout: none;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
}
|
|
|
|
.base-button:focus-visible {
|
|
outline: 2px solid #646cff;
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.base-button:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* Sizes */
|
|
.base-button--sm {
|
|
padding: 0.5rem 0.75rem;
|
|
font-size: 0.875rem;
|
|
min-height: 2.75rem; /* 44px minimum for iOS touch targets */
|
|
min-width: 2.75rem;
|
|
}
|
|
|
|
.base-button--md {
|
|
padding: 0.75rem 1rem;
|
|
font-size: 1rem;
|
|
min-height: 2.75rem;
|
|
min-width: 2.75rem;
|
|
}
|
|
|
|
.base-button--lg {
|
|
padding: 1rem 1.5rem;
|
|
font-size: 1.125rem;
|
|
min-height: 3rem;
|
|
min-width: 3rem;
|
|
}
|
|
|
|
/* Variants */
|
|
.base-button--primary {
|
|
background-color: #646cff;
|
|
color: white;
|
|
}
|
|
|
|
.base-button--primary:hover:not(:disabled) {
|
|
background-color: #535bf2;
|
|
}
|
|
|
|
.base-button--secondary {
|
|
background-color: #f9f9f9;
|
|
color: #213547;
|
|
border-color: #d1d5db;
|
|
}
|
|
|
|
.base-button--secondary:hover:not(:disabled) {
|
|
background-color: #f3f4f6;
|
|
border-color: #9ca3af;
|
|
}
|
|
|
|
.base-button--danger {
|
|
background-color: #ef4444;
|
|
color: white;
|
|
}
|
|
|
|
.base-button--danger:hover:not(:disabled) {
|
|
background-color: #dc2626;
|
|
}
|
|
|
|
.base-button--ghost {
|
|
background-color: transparent;
|
|
color: #646cff;
|
|
/* Ensure ghost buttons always meet minimum touch targets */
|
|
min-height: 2.75rem;
|
|
min-width: 2.75rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.base-button--ghost:hover:not(:disabled) {
|
|
background-color: rgba(100, 108, 255, 0.1);
|
|
}
|
|
|
|
/* Loading state */
|
|
.base-button--loading {
|
|
cursor: wait;
|
|
}
|
|
|
|
.base-button__spinner {
|
|
position: absolute;
|
|
width: 1rem;
|
|
height: 1rem;
|
|
border: 2px solid transparent;
|
|
border-top: 2px solid currentColor;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
.base-button__content--hidden {
|
|
opacity: 0;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
/* Dark mode */
|
|
@media (prefers-color-scheme: dark) {
|
|
.base-button--secondary {
|
|
background-color: #374151;
|
|
color: rgba(255, 255, 255, 0.87);
|
|
border-color: #4b5563;
|
|
}
|
|
|
|
.base-button--secondary:hover:not(:disabled) {
|
|
background-color: #4b5563;
|
|
border-color: #6b7280;
|
|
}
|
|
}
|
|
</style> |