Update progress documentation and mark Step 3 as completed in project tracker

This commit is contained in:
2026-01-28 22:22:30 +00:00
parent 77a5d09db1
commit 9467d4d81d
2 changed files with 287 additions and 5 deletions

View File

@@ -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>