✅ Update progress documentation and mark Step 3 as completed in project tracker
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user