All files / components/base BaseButton.vue

100% Statements 57/57
100% Branches 0/0
100% Functions 0/0
100% Lines 57/57

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98  1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x                                                   1x               1x   1x 1x 1x 1x 1x 1x   1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x   1x 1x 1x   1x 1x    
<template>
  <button
    :type="type"
    :class="buttonClasses"
    :disabled="disabled || loading"
    @click="emit('click', $event)"
  >
    <span v-if="loading" class="mr-2 animate-spin">
      <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24">
        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
      </svg>
    </span>
    <slot />
  </button>
</template>
 
<script setup lang="ts">
/**
 * BaseButton
 * @description Reusable button component with variants and loading state
 */
 
interface Props {
  /** Button variant style */
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger'
  /** Button size */
  size?: 'sm' | 'md' | 'lg'
  /** HTML button type */
  type?: 'button' | 'submit' | 'reset'
  /** Disabled state */
  disabled?: boolean
  /** Loading state with spinner */
  loading?: boolean
}
 
interface Emits {
  (e: 'click', event: MouseEvent): void
}
 
const props = withDefaults(defineProps<Props>(), {
  variant: 'primary',
  size: 'md',
  type: 'button',
  disabled: false,
  loading: false
})
 
const emit = defineEmits<Emits>()
 
const buttonClasses = computed(() => {
  const base = [
    'inline-flex items-center justify-center font-medium rounded-lg',
    'transition-all duration-200',
    'focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-opacity-50'
  ]
 
  const sizes = {
    sm: 'px-3 py-2 text-sm min-h-[44px]',
    md: 'px-4 py-2.5 text-base min-h-[44px]',
    lg: 'px-6 py-3 text-lg min-h-[48px]'
  }
 
  const variants = {
    primary: [
      'bg-primary-600 text-white',
      'hover:bg-primary-700',
      'focus-visible:ring-primary-500',
      'active:bg-primary-800'
    ],
    secondary: [
      'bg-secondary-200 text-secondary-800',
      'hover:bg-secondary-300',
      'focus-visible:ring-secondary-500',
      'active:bg-secondary-400'
    ],
    ghost: [
      'bg-transparent text-secondary-700',
      'hover:bg-secondary-100',
      'focus-visible:ring-secondary-500',
      'active:bg-secondary-200'
    ],
    danger: [
      'bg-error-600 text-white',
      'hover:bg-error-700',
      'focus-visible:ring-error-500',
      'active:bg-error-800'
    ]
  }
 
  const disabled = (props.disabled || props.loading)
    ? 'opacity-50 cursor-not-allowed'
    : 'cursor-pointer'
 
  return [...base, sizes[props.size], ...variants[props.variant], disabled]
})
</script>