birus-ui 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +232 -2
  2. package/dist/birus-ui.js +6919 -0
  3. package/dist/birus-ui.umd.cjs +10 -0
  4. package/dist/style.css +1 -0
  5. package/dist/tailwind-preset.js +70 -0
  6. package/dist/themes.css +217 -0
  7. package/package.json +41 -4
  8. package/src/components/BirusAccordion.vue +112 -0
  9. package/src/components/BirusAlert.vue +114 -0
  10. package/src/components/BirusAvatar.vue +67 -0
  11. package/src/components/BirusBadge.vue +64 -0
  12. package/src/components/BirusBreadcrumb.vue +61 -0
  13. package/src/components/BirusButton.vue +84 -0
  14. package/src/components/BirusCard.vue +89 -0
  15. package/src/components/BirusCheckbox.vue +106 -0
  16. package/src/components/BirusDialog.vue +127 -0
  17. package/src/components/BirusDrawer.vue +158 -0
  18. package/src/components/BirusDropdownMenu.vue +184 -0
  19. package/src/components/BirusEmptyState.vue +64 -0
  20. package/src/components/BirusHeading.vue +70 -0
  21. package/src/components/BirusInput.vue +197 -0
  22. package/src/components/BirusKbd.vue +43 -0
  23. package/src/components/BirusPagination.vue +132 -0
  24. package/src/components/BirusProgress.vue +70 -0
  25. package/src/components/BirusRadioGroup.vue +157 -0
  26. package/src/components/BirusSelect.vue +272 -0
  27. package/src/components/BirusSeparator.vue +72 -0
  28. package/src/components/BirusSkeleton.vue +46 -0
  29. package/src/components/BirusSlider.vue +96 -0
  30. package/src/components/BirusStatCard.vue +106 -0
  31. package/src/components/BirusStepper.vue +114 -0
  32. package/src/components/BirusSwitch.vue +76 -0
  33. package/src/components/BirusTable.vue +126 -0
  34. package/src/components/BirusTabs.vue +121 -0
  35. package/src/components/BirusText.vue +74 -0
  36. package/src/components/BirusTextarea.vue +84 -0
  37. package/src/components/BirusTimeline.vue +61 -0
  38. package/src/components/BirusToaster.vue +130 -0
  39. package/src/components/BirusTooltip.vue +63 -0
  40. package/src/composables/useBirusTheme.js +144 -0
  41. package/src/composables/useToast.js +63 -0
  42. package/src/index.js +116 -0
  43. package/src/lib-entry.js +3 -0
  44. package/src/style.css +5 -0
@@ -0,0 +1,96 @@
1
+ <script setup>
2
+ import { computed } from 'vue';
3
+ import { sliderTrackVariants, sliderFillVariants, cn } from '@birus-ui/core';
4
+
5
+ const props = defineProps({
6
+ modelValue: { type: Number, default: 50 },
7
+ min: { type: Number, default: 0 },
8
+ max: { type: Number, default: 100 },
9
+ step: { type: Number, default: 1 },
10
+ size: { type: String, default: 'md' },
11
+ variant: { type: String, default: 'default' },
12
+ label: { type: String, default: '' },
13
+ showValue: { type: Boolean, default: true },
14
+ valueSuffix: { type: String, default: '' },
15
+ disabled: { type: Boolean, default: false },
16
+ class: { type: String, default: '' },
17
+ });
18
+
19
+ const emit = defineEmits(['update:modelValue', 'change']);
20
+
21
+ const percentage = computed(() => {
22
+ const range = props.max - props.min;
23
+ if (range <= 0) return 0;
24
+ const clamped = Math.min(props.max, Math.max(props.min, props.modelValue));
25
+ return ((clamped - props.min) / range) * 100;
26
+ });
27
+
28
+ const thumbSizeClass = computed(() => {
29
+ switch (props.size) {
30
+ case 'sm':
31
+ return 'h-4 w-4';
32
+ case 'lg':
33
+ return 'h-6 w-6';
34
+ default:
35
+ return 'h-5 w-5';
36
+ }
37
+ });
38
+
39
+ function onInput(event) {
40
+ const val = Number(event.target.value);
41
+ emit('update:modelValue', val);
42
+ }
43
+
44
+ function onChange(event) {
45
+ emit('change', Number(event.target.value));
46
+ }
47
+ </script>
48
+
49
+ <template>
50
+ <div :class="cn('w-full space-y-2', disabled && 'opacity-50', props.class)">
51
+ <div v-if="label || showValue" class="flex items-center justify-between gap-2">
52
+ <label v-if="label" class="text-sm font-medium text-fg leading-none">
53
+ {{ label }}
54
+ </label>
55
+ <span
56
+ v-if="showValue"
57
+ class="inline-flex items-center rounded-birus-sm border border-border bg-surface px-2 py-0.5 text-xs font-mono font-semibold text-fg tabular-nums ml-auto"
58
+ >
59
+ {{ modelValue }}{{ valueSuffix }}
60
+ </span>
61
+ </div>
62
+
63
+ <div class="relative flex items-center py-2">
64
+ <!-- Trilha visual -->
65
+ <div :class="sliderTrackVariants({ size })">
66
+ <div
67
+ :class="sliderFillVariants({ variant })"
68
+ :style="{ width: `${percentage}%` }"
69
+ />
70
+ </div>
71
+
72
+ <!-- Thumb visual posicionado -->
73
+ <span
74
+ :class="[
75
+ 'pointer-events-none absolute -translate-x-1/2 rounded-full border-2 border-primary bg-card shadow-md transition-transform duration-75',
76
+ thumbSizeClass,
77
+ ]"
78
+ :style="{ left: `${percentage}%` }"
79
+ />
80
+
81
+ <!-- Input range nativo invisível para acessibilidade total (teclado + touch + mouse) -->
82
+ <input
83
+ type="range"
84
+ :value="modelValue"
85
+ :min="min"
86
+ :max="max"
87
+ :step="step"
88
+ :disabled="disabled"
89
+ :aria-label="label || 'Controle deslizante'"
90
+ class="absolute inset-0 h-full w-full cursor-pointer opacity-0 disabled:cursor-not-allowed"
91
+ @input="onInput"
92
+ @change="onChange"
93
+ />
94
+ </div>
95
+ </div>
96
+ </template>
@@ -0,0 +1,106 @@
1
+ <script setup>
2
+ import { computed } from 'vue';
3
+ import { cn, statCardVariants, statTrendVariants } from '@birus-ui/core';
4
+
5
+ const props = defineProps({
6
+ label: {
7
+ type: String,
8
+ required: true,
9
+ },
10
+ value: {
11
+ type: [String, Number],
12
+ required: true,
13
+ },
14
+ trend: {
15
+ type: String,
16
+ default: '',
17
+ },
18
+ trendDirection: {
19
+ type: String,
20
+ default: 'neutral',
21
+ },
22
+ caption: {
23
+ type: String,
24
+ default: '',
25
+ },
26
+ progress: {
27
+ type: Number,
28
+ default: null,
29
+ },
30
+ variant: {
31
+ type: String,
32
+ default: 'default',
33
+ },
34
+ class: {
35
+ type: String,
36
+ default: '',
37
+ },
38
+ });
39
+
40
+ const cardClasses = computed(() =>
41
+ cn(statCardVariants({ variant: props.variant }), props.class)
42
+ );
43
+ const trendClasses = computed(() =>
44
+ cn(statTrendVariants({ direction: props.trendDirection }))
45
+ );
46
+ </script>
47
+
48
+ <template>
49
+ <div :class="cardClasses">
50
+ <div class="flex items-start justify-between gap-3">
51
+ <div class="space-y-1">
52
+ <p class="text-xs font-medium uppercase tracking-wider text-birus-muted-fg">
53
+ {{ label }}
54
+ </p>
55
+ <div class="flex items-baseline gap-2.5">
56
+ <span class="text-2xl sm:text-3xl font-bold tracking-tight text-birus-fg">
57
+ {{ value }}
58
+ </span>
59
+ <span v-if="trend" :class="trendClasses">
60
+ <svg
61
+ v-if="trendDirection === 'up'"
62
+ class="h-3 w-3"
63
+ fill="none"
64
+ viewBox="0 0 24 24"
65
+ stroke="currentColor"
66
+ stroke-width="2.5"
67
+ >
68
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 10l7-7m0 0l7 7m-7-7v18" />
69
+ </svg>
70
+ <svg
71
+ v-else-if="trendDirection === 'down'"
72
+ class="h-3 w-3"
73
+ fill="none"
74
+ viewBox="0 0 24 24"
75
+ stroke="currentColor"
76
+ stroke-width="2.5"
77
+ >
78
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
79
+ </svg>
80
+ {{ trend }}
81
+ </span>
82
+ </div>
83
+ </div>
84
+
85
+ <div
86
+ v-if="$slots.icon"
87
+ class="flex h-10 w-10 shrink-0 items-center justify-center rounded-birus bg-birus-primary/10 text-birus-primary"
88
+ >
89
+ <slot name="icon" />
90
+ </div>
91
+ </div>
92
+
93
+ <div v-if="progress !== null && progress !== undefined" class="mt-4">
94
+ <div class="h-1.5 w-full overflow-hidden rounded-full bg-birus-muted">
95
+ <div
96
+ class="h-full rounded-full bg-birus-primary transition-all duration-300"
97
+ :style="{ width: `${Math.min(100, Math.max(0, progress))}%` }"
98
+ />
99
+ </div>
100
+ </div>
101
+
102
+ <p v-if="caption || $slots.footer" class="mt-3 text-xs text-birus-muted-fg">
103
+ <slot name="footer">{{ caption }}</slot>
104
+ </p>
105
+ </div>
106
+ </template>
@@ -0,0 +1,114 @@
1
+ <script setup>
2
+ import { cn } from '@birus-ui/core';
3
+
4
+ const props = defineProps({
5
+ steps: {
6
+ type: Array,
7
+ default: () => [],
8
+ },
9
+ modelValue: {
10
+ type: Number,
11
+ default: 0,
12
+ },
13
+ orientation: {
14
+ type: String,
15
+ default: 'horizontal',
16
+ },
17
+ clickable: {
18
+ type: Boolean,
19
+ default: true,
20
+ },
21
+ class: {
22
+ type: String,
23
+ default: '',
24
+ },
25
+ });
26
+
27
+ const emit = defineEmits(['update:modelValue', 'change']);
28
+
29
+ function selectStep(index) {
30
+ if (!props.clickable) return;
31
+ emit('update:modelValue', index);
32
+ emit('change', index);
33
+ }
34
+ </script>
35
+
36
+ <template>
37
+ <nav
38
+ aria-label="Progresso de etapas"
39
+ :class="
40
+ cn(
41
+ orientation === 'vertical'
42
+ ? 'flex flex-col space-y-4'
43
+ : 'flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 w-full',
44
+ props.class
45
+ )
46
+ "
47
+ >
48
+ <div
49
+ v-for="(step, idx) in steps"
50
+ :key="step.id || idx"
51
+ :class="
52
+ cn(
53
+ 'flex items-center gap-3',
54
+ orientation === 'horizontal' && idx < steps.length - 1 ? 'flex-1 w-full' : ''
55
+ )
56
+ "
57
+ >
58
+ <button
59
+ type="button"
60
+ :disabled="!clickable"
61
+ :aria-current="idx === modelValue ? 'step' : undefined"
62
+ class="group flex items-center gap-3 text-left focus:outline-none disabled:cursor-default"
63
+ @click="selectStep(idx)"
64
+ >
65
+ <span
66
+ :class="
67
+ cn(
68
+ 'flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-bold transition-all border',
69
+ idx < modelValue
70
+ ? 'bg-birus-primary text-birus-primary-fg border-birus-primary shadow-sm'
71
+ : idx === modelValue
72
+ ? 'bg-birus-primary/15 text-birus-primary border-birus-primary ring-4 ring-birus-primary/15'
73
+ : 'bg-birus-surface text-birus-muted-fg border-birus-border group-hover:border-birus-primary/40'
74
+ )
75
+ "
76
+ >
77
+ <svg
78
+ v-if="idx < modelValue"
79
+ class="h-4 w-4"
80
+ fill="none"
81
+ viewBox="0 0 24 24"
82
+ stroke="currentColor"
83
+ stroke-width="2.5"
84
+ >
85
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
86
+ </svg>
87
+ <span v-else>{{ idx + 1 }}</span>
88
+ </span>
89
+
90
+ <div class="min-w-0">
91
+ <p
92
+ :class="
93
+ cn(
94
+ 'text-xs sm:text-sm font-semibold transition-colors',
95
+ idx <= modelValue ? 'text-birus-fg' : 'text-birus-muted-fg'
96
+ )
97
+ "
98
+ >
99
+ {{ step.title }}
100
+ </p>
101
+ <p v-if="step.description" class="text-[11px] text-birus-muted-fg truncate max-w-[180px]">
102
+ {{ step.description }}
103
+ </p>
104
+ </div>
105
+ </button>
106
+
107
+ <div
108
+ v-if="orientation === 'horizontal' && idx < steps.length - 1"
109
+ class="hidden sm:block flex-1 h-[2px] mx-2 rounded-full transition-colors"
110
+ :class="idx < modelValue ? 'bg-birus-primary' : 'bg-birus-border'"
111
+ />
112
+ </div>
113
+ </nav>
114
+ </template>
@@ -0,0 +1,76 @@
1
+ <script setup>
2
+ import { computed } from 'vue';
3
+ import { switchTrackVariants, switchThumbVariants, cn } from '@birus-ui/core';
4
+
5
+ const props = defineProps({
6
+ modelValue: { type: Boolean, default: false },
7
+ size: { type: String, default: 'md' },
8
+ label: { type: String, default: '' },
9
+ description: { type: String, default: '' },
10
+ disabled: { type: Boolean, default: false },
11
+ id: { type: String, default: undefined },
12
+ class: { type: String, default: '' },
13
+ });
14
+
15
+ const emit = defineEmits(['update:modelValue']);
16
+
17
+ const uid = props.id || `birus-switch-${Math.random().toString(36).slice(2, 9)}`;
18
+
19
+ const trackClasses = computed(() =>
20
+ cn(
21
+ switchTrackVariants({
22
+ size: props.size,
23
+ checked: props.modelValue,
24
+ }),
25
+ props.class,
26
+ ),
27
+ );
28
+
29
+ const thumbClasses = computed(() =>
30
+ switchThumbVariants({
31
+ size: props.size,
32
+ checked: props.modelValue,
33
+ }),
34
+ );
35
+
36
+ function toggle() {
37
+ if (props.disabled) return;
38
+ emit('update:modelValue', !props.modelValue);
39
+ }
40
+ </script>
41
+
42
+ <template>
43
+ <div class="inline-flex items-start gap-3">
44
+ <button
45
+ :id="uid"
46
+ type="button"
47
+ role="switch"
48
+ :aria-checked="modelValue ? 'true' : 'false'"
49
+ :aria-labelledby="label ? `${uid}-label` : undefined"
50
+ :disabled="disabled"
51
+ :class="trackClasses"
52
+ @click="toggle"
53
+ @keydown.enter.prevent="toggle"
54
+ >
55
+ <span :class="thumbClasses" />
56
+ </button>
57
+
58
+ <div v-if="label || description || $slots.default" class="flex flex-col gap-0.5">
59
+ <label
60
+ v-if="label || $slots.default"
61
+ :id="`${uid}-label`"
62
+ :for="uid"
63
+ :class="[
64
+ 'text-sm font-medium leading-snug text-fg select-none',
65
+ disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
66
+ ]"
67
+ @click.prevent="toggle"
68
+ >
69
+ <slot>{{ label }}</slot>
70
+ </label>
71
+ <p v-if="description" class="text-xs text-muted-fg leading-relaxed">
72
+ {{ description }}
73
+ </p>
74
+ </div>
75
+ </div>
76
+ </template>
@@ -0,0 +1,126 @@
1
+ <script setup>
2
+ import { computed, ref } from 'vue';
3
+ import { tableCellVariants, cn } from '@birus-ui/core';
4
+
5
+ const props = defineProps({
6
+ columns: { type: Array, default: () => [] }, // [{ key, label, sortable, align }]
7
+ rows: { type: Array, default: () => [] },
8
+ striped: { type: Boolean, default: false },
9
+ hoverable: { type: Boolean, default: true },
10
+ density: { type: String, default: 'default' }, // 'compact' | 'default' | 'relaxed'
11
+ emptyText: { type: String, default: 'Nenhum registro encontrado.' },
12
+ caption: { type: String, default: '' },
13
+ class: { type: String, default: '' },
14
+ });
15
+
16
+ const emit = defineEmits(['row-click', 'sort']);
17
+
18
+ const sortKey = ref('');
19
+ const sortOrder = ref('asc'); // 'asc' | 'desc'
20
+
21
+ const sortedRows = computed(() => {
22
+ if (!sortKey.value) return props.rows;
23
+ const key = sortKey.value;
24
+ const dir = sortOrder.value === 'asc' ? 1 : -1;
25
+
26
+ return [...props.rows].sort((a, b) => {
27
+ const valA = a[key];
28
+ const valB = b[key];
29
+ if (valA === valB) return 0;
30
+ if (typeof valA === 'number' && typeof valB === 'number') {
31
+ return (valA - valB) * dir;
32
+ }
33
+ return String(valA ?? '').localeCompare(String(valB ?? '')) * dir;
34
+ });
35
+ });
36
+
37
+ function toggleSort(col) {
38
+ if (!col.sortable) return;
39
+ if (sortKey.value === col.key) {
40
+ if (sortOrder.value === 'asc') {
41
+ sortOrder.value = 'desc';
42
+ } else {
43
+ sortKey.value = '';
44
+ sortOrder.value = 'asc';
45
+ }
46
+ } else {
47
+ sortKey.value = col.key;
48
+ sortOrder.value = 'asc';
49
+ }
50
+ emit('sort', { key: sortKey.value, order: sortOrder.value });
51
+ }
52
+
53
+ function getAlignClass(align) {
54
+ if (align === 'right') return 'text-right';
55
+ if (align === 'center') return 'text-center';
56
+ return 'text-left';
57
+ }
58
+ </script>
59
+
60
+ <template>
61
+ <div :class="cn('w-full overflow-x-auto rounded-birus-lg border border-border bg-card', props.class)">
62
+ <table class="w-full border-collapse text-sm">
63
+ <caption
64
+ v-if="caption"
65
+ class="border-b border-border bg-surface/50 px-4 py-2.5 text-left text-xs text-muted-fg"
66
+ >
67
+ {{ caption }}
68
+ </caption>
69
+
70
+ <thead class="border-b border-border bg-surface text-xs uppercase tracking-wider text-muted-fg">
71
+ <tr>
72
+ <th
73
+ v-for="col in columns"
74
+ :key="col.key"
75
+ :class="[
76
+ tableCellVariants({ density }),
77
+ getAlignClass(col.align),
78
+ 'font-semibold select-none',
79
+ col.sortable && 'cursor-pointer hover:text-fg transition-colors',
80
+ ]"
81
+ @click="toggleSort(col)"
82
+ >
83
+ <span class="inline-flex items-center gap-1.5">
84
+ <span>{{ col.label }}</span>
85
+ <span v-if="col.sortable" class="text-[10px]">
86
+ <template v-if="sortKey === col.key">
87
+ {{ sortOrder === 'asc' ? '▲' : '▼' }}
88
+ </template>
89
+ <span v-else class="opacity-35">⇅</span>
90
+ </span>
91
+ </span>
92
+ </th>
93
+ </tr>
94
+ </thead>
95
+
96
+ <tbody class="divide-y divide-border">
97
+ <tr
98
+ v-for="(row, rIdx) in sortedRows"
99
+ :key="row.id || rIdx"
100
+ :class="[
101
+ 'transition-colors',
102
+ striped && rIdx % 2 === 1 && 'bg-surface/40',
103
+ hoverable && 'hover:bg-surface-hover/70',
104
+ ]"
105
+ @click="emit('row-click', row)"
106
+ >
107
+ <td
108
+ v-for="col in columns"
109
+ :key="col.key"
110
+ :class="[tableCellVariants({ density }), getAlignClass(col.align), 'text-fg']"
111
+ >
112
+ <slot :name="`cell-${col.key}`" :row="row" :value="row[col.key]">
113
+ {{ row[col.key] }}
114
+ </slot>
115
+ </td>
116
+ </tr>
117
+
118
+ <tr v-if="sortedRows.length === 0">
119
+ <td :colspan="Math.max(1, columns.length)" class="px-4 py-8 text-center text-sm text-muted-fg">
120
+ {{ emptyText }}
121
+ </td>
122
+ </tr>
123
+ </tbody>
124
+ </table>
125
+ </div>
126
+ </template>
@@ -0,0 +1,121 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from 'vue';
3
+ import { tabsListVariants, tabsTriggerVariants, cn } from '@birus-ui/core';
4
+
5
+ const props = defineProps({
6
+ modelValue: { type: String, default: '' },
7
+ defaultValue: { type: String, default: '' },
8
+ tabs: { type: Array, default: () => [] },
9
+ variant: { type: String, default: 'segmented' },
10
+ fullWidth: { type: Boolean, default: false },
11
+ class: { type: String, default: '' },
12
+ });
13
+
14
+ const emit = defineEmits(['update:modelValue', 'change']);
15
+
16
+ const triggerRefs = ref([]);
17
+ const internalValue = ref(
18
+ props.modelValue || props.defaultValue || props.tabs[0]?.value || ''
19
+ );
20
+
21
+ watch(
22
+ () => props.modelValue,
23
+ (newVal) => {
24
+ if (newVal !== undefined && newVal !== '') {
25
+ internalValue.value = newVal;
26
+ }
27
+ }
28
+ );
29
+
30
+ const activeValue = computed(() => {
31
+ if (internalValue.value && props.tabs.some((t) => t.value === internalValue.value)) {
32
+ return internalValue.value;
33
+ }
34
+ return props.tabs[0]?.value ?? '';
35
+ });
36
+
37
+ const listClasses = computed(() =>
38
+ cn(
39
+ tabsListVariants({ variant: props.variant, fullWidth: props.fullWidth }),
40
+ props.class,
41
+ ),
42
+ );
43
+
44
+ function selectTab(tab) {
45
+ if (tab.disabled) return;
46
+ internalValue.value = tab.value;
47
+ emit('update:modelValue', tab.value);
48
+ emit('change', tab.value);
49
+ }
50
+
51
+ function handleKeydown(event, index) {
52
+ const enabledIndexes = props.tabs
53
+ .map((t, i) => (t.disabled ? -1 : i))
54
+ .filter((i) => i !== -1);
55
+
56
+ if (enabledIndexes.length === 0) return;
57
+
58
+ const currentPos = enabledIndexes.indexOf(index);
59
+ let targetPos = currentPos;
60
+
61
+ if (event.key === 'ArrowRight') {
62
+ targetPos = (currentPos + 1) % enabledIndexes.length;
63
+ } else if (event.key === 'ArrowLeft') {
64
+ targetPos = (currentPos - 1 + enabledIndexes.length) % enabledIndexes.length;
65
+ } else if (event.key === 'Home') {
66
+ targetPos = 0;
67
+ } else if (event.key === 'End') {
68
+ targetPos = enabledIndexes.length - 1;
69
+ } else {
70
+ return;
71
+ }
72
+
73
+ event.preventDefault();
74
+ const nextIndex = enabledIndexes[targetPos];
75
+ const nextTab = props.tabs[nextIndex];
76
+ if (nextTab) {
77
+ selectTab(nextTab);
78
+ triggerRefs.value[nextIndex]?.focus();
79
+ }
80
+ }
81
+ </script>
82
+
83
+ <template>
84
+ <div class="w-full">
85
+ <div role="tablist" :class="listClasses">
86
+ <button
87
+ v-for="(tab, index) in tabs"
88
+ :key="tab.value"
89
+ :ref="(el) => { if (el) triggerRefs[index] = el; }"
90
+ type="button"
91
+ role="tab"
92
+ :aria-selected="activeValue === tab.value ? 'true' : 'false'"
93
+ :tabindex="activeValue === tab.value ? 0 : -1"
94
+ :disabled="tab.disabled"
95
+ :class="tabsTriggerVariants({ variant, active: activeValue === tab.value, fullWidth })"
96
+ @click="selectTab(tab)"
97
+ @keydown="handleKeydown($event, index)"
98
+ >
99
+ <slot :name="`icon-${tab.value}`" />
100
+ <span>{{ tab.label }}</span>
101
+ <span
102
+ v-if="tab.badge !== undefined"
103
+ :class="[
104
+ 'inline-flex items-center justify-center rounded-full px-1.5 py-0.5 text-[10px] font-semibold leading-none',
105
+ activeValue === tab.value
106
+ ? 'bg-primary/15 text-primary'
107
+ : 'bg-surface-hover text-muted-fg',
108
+ ]"
109
+ >
110
+ {{ tab.badge }}
111
+ </span>
112
+ </button>
113
+ </div>
114
+
115
+ <div v-if="$slots.default || $slots[`panel-${activeValue}`]" class="mt-4" role="tabpanel">
116
+ <slot :name="`panel-${activeValue}`" :active-tab="activeValue">
117
+ <slot :active-tab="activeValue" />
118
+ </slot>
119
+ </div>
120
+ </div>
121
+ </template>