Tabs.vue 2.73 KB
<template>
  <div :class="className">
    <!-- Tab headers -->
    <div :class="getContainerClasses()">
      <button
        v-for="(tab, index) in tabs"
        :key="index"
        :class="getTabHeaderClasses(index)"
        @click="handleTabChange(index)"
        role="tab"
        :aria-selected="currentTab === index"
      >
        {{ tab.label }}
      </button>
    </div>

    <!-- Tab content -->
    <div class="pt-4">
      <component :is="tabs[currentTab]?.component" />
    </div>
  </div>
</template>

<script>
// Define valid values for variant and size props
const validVariants = ['underline', 'pills', 'bordered'];
const validSizes = ['sm', 'md', 'lg'];
</script>

<script setup>
import { ref, computed } from 'vue';

const props = defineProps({
  tabs: {
    type: Array,
    required: true
  },
  activeTab: {
    type: Number,
    default: 0
  },
  className: {
    type: String,
    default: ''
  },
  variant: {
    type: String,
    default: 'underline',
    validator: (value) => validVariants.includes(value)
  },
  size: {
    type: String,
    default: 'md',
    validator: (value) => validSizes.includes(value)
  }
});

const emit = defineEmits(['change']);

const currentTab = ref(props.activeTab);

const handleTabChange = (index) => {
  currentTab.value = index;
  emit('change', index);
};

const getSizeClasses = () => {
  switch (props.size) {
    case 'sm': return 'text-sm py-1 px-2';
    case 'lg': return 'text-lg py-3 px-5';
    case 'md':
    default: return 'text-base py-2 px-4';
  }
};

const getTabHeaderClasses = (index) => {
  const isActive = index === currentTab.value;
  const sizeClasses = getSizeClasses();

  const baseClasses = 'font-medium transition-all duration-200 focus:outline-none';

  switch (props.variant) {
    case 'pills':
      return `${baseClasses} ${sizeClasses} rounded-md ${
        isActive
          ? 'bg-green-500 text-white'
          : 'text-gray-700 hover:text-green-500 hover:bg-green-50'
      }`;

    case 'bordered':
      return `${baseClasses} ${sizeClasses} border-b-2 ${
        isActive
          ? 'border-green-500 text-green-600'
          : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
      }`;

    case 'underline':
    default:
      return `${baseClasses} ${sizeClasses} border-b-2 ${
        isActive
          ? 'border-green-500 text-green-600'
          : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
      }`;
  }
};

const getContainerClasses = () => {
  switch (props.variant) {
    case 'pills':
      return 'flex p-1 space-x-1 bg-gray-100 rounded-lg';
    case 'bordered':
      return 'flex border-b border-gray-200';
    case 'underline':
    default:
      return 'flex border-b border-gray-200';
  }
};
</script>