Files
ppeasy_vg12/resources/js/components/ui/Button.vue
T
2026-10-09 08:21:06 +02:00

95 lines
3.6 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue';
type Severity = 'primary' | 'secondary' | 'danger' | 'success' | 'info';
const props = withDefaults(
defineProps<{
/** Text content. The default slot wins when both are given. */
label?: string;
/** Icon class, e.g. `fas fa-plus`. Rendered before the label. */
icon?: string;
severity?: Severity;
size?: 'small' | 'normal' | 'large';
/** Shows a spinner and blocks interaction. */
loading?: boolean;
/** Pill shape. Used for the icon-only action buttons in list views. */
rounded?: boolean;
/** Borderless, transparent until hovered. */
text?: boolean;
outlined?: boolean;
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
}>(),
{
label: undefined,
icon: undefined,
severity: 'primary',
size: 'normal',
type: 'button',
},
);
/**
* Every variant resolves to plain Tailwind utilities so an application built on
* Vanguard can restyle a button by editing this file, with no theme layer or
* specificity to work around.
*/
const solid: Record<Severity, string> = {
primary: 'bg-primary-600 text-white hover:bg-primary-700 focus-visible:outline-primary-600',
secondary: 'bg-slate-200 text-slate-700 hover:bg-slate-300 focus-visible:outline-slate-400',
danger: 'bg-red-600 text-white hover:bg-red-700 focus-visible:outline-red-600',
success: 'bg-green-600 text-white hover:bg-green-700 focus-visible:outline-green-600',
info: 'bg-sky-600 text-white hover:bg-sky-700 focus-visible:outline-sky-600',
};
const subtle: Record<Severity, string> = {
primary: 'text-primary-600 hover:bg-primary-50 focus-visible:outline-primary-600',
secondary: 'text-slate-400 hover:bg-slate-100 hover:text-slate-600 focus-visible:outline-slate-400',
danger: 'text-red-600 hover:bg-red-50 focus-visible:outline-red-600',
success: 'text-green-600 hover:bg-green-50 focus-visible:outline-green-600',
info: 'text-sky-600 hover:bg-sky-50 focus-visible:outline-sky-600',
};
const outline: Record<Severity, string> = {
primary: 'border border-primary-600 text-primary-600 hover:bg-primary-50',
secondary: 'border border-slate-300 text-slate-700 hover:bg-slate-100',
danger: 'border border-red-600 text-red-600 hover:bg-red-50',
success: 'border border-green-600 text-green-600 hover:bg-green-50',
info: 'border border-sky-600 text-sky-600 hover:bg-sky-50',
};
const sizes = {
small: 'px-2.5 py-1 text-xs',
normal: 'px-4 py-2',
large: 'px-5 py-2.5 text-base',
};
/** Icon-only buttons are squared off so the pill variant renders as a circle. */
const iconOnly = computed(() => Boolean(props.icon) && !props.label);
const iconSizes = {
small: 'h-7 w-7',
normal: 'h-9 w-9',
large: 'h-11 w-11',
};
const classes = computed(() => [
'inline-flex cursor-pointer items-center justify-center gap-2 font-medium transition-colors',
'focus-visible:outline-2 focus-visible:outline-offset-2',
'disabled:pointer-events-none disabled:opacity-60',
props.rounded ? 'rounded-full' : 'rounded-md',
iconOnly.value ? iconSizes[props.size] : sizes[props.size],
props.text ? subtle[props.severity] : props.outlined ? outline[props.severity] : solid[props.severity],
]);
</script>
<template>
<button :type="type" :class="classes" :disabled="disabled || loading">
<i v-if="loading" class="fas fa-circle-notch animate-spin" aria-hidden="true" />
<i v-else-if="icon" :class="icon" aria-hidden="true" />
<slot>{{ label }}</slot>
</button>
</template>