Initial vue frontend

This commit is contained in:
2025-08-12 01:05:59 +02:00
parent 64e50027ca
commit 58e0c10b4e
70 changed files with 16958 additions and 0 deletions

View File

@@ -0,0 +1,173 @@
<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;
}
.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;
}
.base-button--md {
padding: 0.75rem 1rem;
font-size: 1rem;
}
.base-button--lg {
padding: 1rem 1.5rem;
font-size: 1.125rem;
}
/* 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;
}
.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>