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,70 @@
1
+ <script setup>
2
+ import { computed } from 'vue';
3
+ import { progressTrackVariants, progressBarVariants, cn } from '@birus-ui/core';
4
+
5
+ const props = defineProps({
6
+ value: { type: Number, default: 0 },
7
+ max: { type: Number, default: 100 },
8
+ variant: { type: String, default: 'default' },
9
+ size: { type: String, default: 'md' },
10
+ showValue: { type: Boolean, default: false },
11
+ label: { type: String, default: '' },
12
+ indeterminate: { type: Boolean, default: false },
13
+ class: { type: String, default: '' },
14
+ });
15
+
16
+ const percentage = computed(() => {
17
+ if (props.max <= 0) return 0;
18
+ return Math.min(100, Math.max(0, Math.round((props.value / props.max) * 100)));
19
+ });
20
+
21
+ const trackClasses = computed(() =>
22
+ cn(progressTrackVariants({ size: props.size }), props.class),
23
+ );
24
+
25
+ const barClasses = computed(() =>
26
+ cn(
27
+ progressBarVariants({ variant: props.variant }),
28
+ props.indeterminate && 'birus-progress-indeterminate w-1/3',
29
+ ),
30
+ );
31
+ </script>
32
+
33
+ <template>
34
+ <div class="w-full space-y-1.5">
35
+ <div v-if="label || showValue" class="flex items-center justify-between text-xs font-medium">
36
+ <span v-if="label" class="text-fg">{{ label }}</span>
37
+ <span v-if="showValue && !indeterminate" class="text-muted-fg tabular-nums ml-auto">
38
+ {{ percentage }}%
39
+ </span>
40
+ </div>
41
+
42
+ <div
43
+ role="progressbar"
44
+ :aria-valuemin="0"
45
+ :aria-valuemax="max"
46
+ :aria-valuenow="indeterminate ? undefined : value"
47
+ :class="trackClasses"
48
+ >
49
+ <div
50
+ :class="barClasses"
51
+ :style="indeterminate ? undefined : { width: `${percentage}%` }"
52
+ />
53
+ </div>
54
+ </div>
55
+ </template>
56
+
57
+ <style scoped>
58
+ @keyframes birus-indeterminate {
59
+ 0% {
60
+ transform: translateX(-100%);
61
+ }
62
+ 100% {
63
+ transform: translateX(300%);
64
+ }
65
+ }
66
+
67
+ .birus-progress-indeterminate {
68
+ animation: birus-indeterminate 1.4s infinite ease-in-out;
69
+ }
70
+ </style>
@@ -0,0 +1,157 @@
1
+ <script setup>
2
+ import { computed, ref } from 'vue';
3
+ import { radioCircleVariants, radioCardVariants, cn } from '@birus-ui/core';
4
+ import BirusBadge from './BirusBadge.vue';
5
+
6
+ const props = defineProps({
7
+ modelValue: { type: [String, Number], default: '' },
8
+ options: { type: Array, default: () => [] },
9
+ variant: { type: String, default: 'list' }, // 'list' | 'cards'
10
+ orientation: { type: String, default: 'vertical' }, // 'vertical' | 'horizontal'
11
+ size: { type: String, default: 'md' },
12
+ label: { type: String, default: '' },
13
+ disabled: { type: Boolean, default: false },
14
+ class: { type: String, default: '' },
15
+ });
16
+
17
+ const emit = defineEmits(['update:modelValue', 'change']);
18
+ const itemRefs = ref([]);
19
+
20
+ const dotSizeClass = computed(() => {
21
+ switch (props.size) {
22
+ case 'sm':
23
+ return 'h-2 w-2';
24
+ case 'lg':
25
+ return 'h-3 w-3';
26
+ default:
27
+ return 'h-2.5 w-2.5';
28
+ }
29
+ });
30
+
31
+ function selectOption(option) {
32
+ if (props.disabled || option.disabled) return;
33
+ emit('update:modelValue', option.value);
34
+ emit('change', option);
35
+ }
36
+
37
+ function handleKeydown(event, index) {
38
+ const enabledIndexes = props.options
39
+ .map((opt, i) => (props.disabled || opt.disabled ? -1 : i))
40
+ .filter((i) => i !== -1);
41
+
42
+ if (!enabledIndexes.length) return;
43
+
44
+ const pos = enabledIndexes.indexOf(index);
45
+ let nextPos = pos;
46
+
47
+ if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
48
+ nextPos = (pos + 1) % enabledIndexes.length;
49
+ } else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
50
+ nextPos = (pos - 1 + enabledIndexes.length) % enabledIndexes.length;
51
+ } else {
52
+ return;
53
+ }
54
+
55
+ event.preventDefault();
56
+ const nextIndex = enabledIndexes[nextPos];
57
+ const nextOpt = props.options[nextIndex];
58
+ if (nextOpt) {
59
+ selectOption(nextOpt);
60
+ itemRefs.value[nextIndex]?.focus();
61
+ }
62
+ }
63
+ </script>
64
+
65
+ <template>
66
+ <div role="radiogroup" :aria-label="label || 'Grupo de opções'" :class="cn('w-full space-y-2', props.class)">
67
+ <div v-if="label" class="text-sm font-medium text-fg leading-none mb-2">
68
+ {{ label }}
69
+ </div>
70
+
71
+ <div
72
+ :class="[
73
+ variant === 'cards'
74
+ ? orientation === 'horizontal'
75
+ ? 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3'
76
+ : 'space-y-2.5'
77
+ : orientation === 'horizontal'
78
+ ? 'flex flex-wrap items-center gap-5'
79
+ : 'space-y-2.5',
80
+ ]"
81
+ >
82
+ <template v-for="(opt, index) in options" :key="String(opt.value)">
83
+ <!-- Modo Radio Cards -->
84
+ <button
85
+ v-if="variant === 'cards'"
86
+ :ref="(el) => { if (el) itemRefs[index] = el; }"
87
+ type="button"
88
+ role="radio"
89
+ :aria-checked="modelValue === opt.value ? 'true' : 'false'"
90
+ :disabled="disabled || opt.disabled"
91
+ :class="
92
+ cn(
93
+ 'w-full',
94
+ radioCardVariants({
95
+ checked: modelValue === opt.value,
96
+ disabled: Boolean(disabled || opt.disabled),
97
+ }),
98
+ )
99
+ "
100
+ @click="selectOption(opt)"
101
+ @keydown="handleKeydown($event, index)"
102
+ >
103
+ <div class="space-y-1 min-w-0">
104
+ <div class="flex items-center gap-2">
105
+ <span class="text-sm font-semibold text-fg">{{ opt.label }}</span>
106
+ <BirusBadge v-if="opt.badge" size="sm" :variant="modelValue === opt.value ? 'default' : 'secondary'">
107
+ {{ opt.badge }}
108
+ </BirusBadge>
109
+ </div>
110
+ <p v-if="opt.description" class="text-xs text-muted-fg leading-relaxed">
111
+ {{ opt.description }}
112
+ </p>
113
+ </div>
114
+
115
+ <span :class="radioCircleVariants({ size, checked: modelValue === opt.value })">
116
+ <span
117
+ v-if="modelValue === opt.value"
118
+ :class="['rounded-full bg-primary transition-transform', dotSizeClass]"
119
+ />
120
+ </span>
121
+ </button>
122
+
123
+ <!-- Modo Lista Tradicional -->
124
+ <div
125
+ v-else
126
+ :class="[
127
+ 'inline-flex items-start gap-2.5',
128
+ disabled || opt.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
129
+ ]"
130
+ @click="selectOption(opt)"
131
+ >
132
+ <button
133
+ :ref="(el) => { if (el) itemRefs[index] = el; }"
134
+ type="button"
135
+ role="radio"
136
+ :aria-checked="modelValue === opt.value ? 'true' : 'false'"
137
+ :disabled="disabled || opt.disabled"
138
+ :class="radioCircleVariants({ size, checked: modelValue === opt.value })"
139
+ @keydown="handleKeydown($event, index)"
140
+ >
141
+ <span
142
+ v-if="modelValue === opt.value"
143
+ :class="['rounded-full bg-primary transition-transform', dotSizeClass]"
144
+ />
145
+ </button>
146
+
147
+ <div class="flex flex-col gap-0.5 select-none">
148
+ <span class="text-sm font-medium leading-snug text-fg">{{ opt.label }}</span>
149
+ <span v-if="opt.description" class="text-xs text-muted-fg leading-relaxed">
150
+ {{ opt.description }}
151
+ </span>
152
+ </div>
153
+ </div>
154
+ </template>
155
+ </div>
156
+ </div>
157
+ </template>
@@ -0,0 +1,272 @@
1
+ <script setup>
2
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
3
+ import { selectTriggerVariants, cn } from '@birus-ui/core';
4
+
5
+ const props = defineProps({
6
+ modelValue: { type: [String, Number], default: null },
7
+ options: { type: Array, default: () => [] },
8
+ placeholder: { type: String, default: 'Selecione uma opção...' },
9
+ label: { type: String, default: '' },
10
+ hint: { type: String, default: '' },
11
+ errorMessage: { type: String, default: '' },
12
+ state: { type: String, default: 'default' },
13
+ size: { type: String, default: 'md' },
14
+ searchable: { type: Boolean, default: false },
15
+ clearable: { type: Boolean, default: false },
16
+ searchPlaceholder: { type: String, default: 'Buscar opção...' },
17
+ disabled: { type: Boolean, default: false },
18
+ required: { type: Boolean, default: false },
19
+ class: { type: String, default: '' },
20
+ });
21
+
22
+ const emit = defineEmits(['update:modelValue', 'change']);
23
+
24
+ const rootRef = ref(null);
25
+ const searchInputRef = ref(null);
26
+ const isOpen = ref(false);
27
+ const searchQuery = ref('');
28
+ const highlightedIndex = ref(-1);
29
+
30
+ const resolvedState = computed(() => (props.errorMessage ? 'error' : props.state));
31
+
32
+ const triggerClasses = computed(() =>
33
+ cn(
34
+ selectTriggerVariants({
35
+ state: resolvedState.value,
36
+ size: props.size,
37
+ }),
38
+ props.class,
39
+ ),
40
+ );
41
+
42
+ const selectedOption = computed(
43
+ () => props.options.find((opt) => opt.value === props.modelValue) ?? null,
44
+ );
45
+
46
+ const filteredOptions = computed(() => {
47
+ const q = searchQuery.value.trim().toLowerCase();
48
+ if (!q) return props.options;
49
+ return props.options.filter(
50
+ (opt) =>
51
+ opt.label.toLowerCase().includes(q) ||
52
+ (opt.description && opt.description.toLowerCase().includes(q)),
53
+ );
54
+ });
55
+
56
+ async function openDropdown() {
57
+ if (props.disabled) return;
58
+ isOpen.value = true;
59
+ searchQuery.value = '';
60
+ const currentIdx = filteredOptions.value.findIndex((o) => o.value === props.modelValue);
61
+ highlightedIndex.value = currentIdx >= 0 ? currentIdx : 0;
62
+ if (props.searchable) {
63
+ await nextTick();
64
+ searchInputRef.value?.focus();
65
+ }
66
+ }
67
+
68
+ function closeDropdown() {
69
+ isOpen.value = false;
70
+ searchQuery.value = '';
71
+ }
72
+
73
+ function toggleDropdown() {
74
+ if (isOpen.value) {
75
+ closeDropdown();
76
+ } else {
77
+ openDropdown();
78
+ }
79
+ }
80
+
81
+ function selectOption(option) {
82
+ if (option.disabled) return;
83
+ emit('update:modelValue', option.value);
84
+ emit('change', option);
85
+ closeDropdown();
86
+ }
87
+
88
+ function clearSelection() {
89
+ emit('update:modelValue', '');
90
+ }
91
+
92
+ function moveHighlight(delta) {
93
+ const list = filteredOptions.value;
94
+ if (!list.length) return;
95
+ let idx = highlightedIndex.value;
96
+ for (let i = 0; i < list.length; i++) {
97
+ idx = (idx + delta + list.length) % list.length;
98
+ if (!list[idx].disabled) {
99
+ highlightedIndex.value = idx;
100
+ return;
101
+ }
102
+ }
103
+ }
104
+
105
+ function handleKeydown(event) {
106
+ if (props.disabled) return;
107
+ if (!isOpen.value) {
108
+ if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
109
+ event.preventDefault();
110
+ openDropdown();
111
+ }
112
+ return;
113
+ }
114
+
115
+ if (event.key === 'Escape') {
116
+ event.preventDefault();
117
+ closeDropdown();
118
+ } else if (event.key === 'ArrowDown') {
119
+ event.preventDefault();
120
+ moveHighlight(1);
121
+ } else if (event.key === 'ArrowUp') {
122
+ event.preventDefault();
123
+ moveHighlight(-1);
124
+ } else if (event.key === 'Enter') {
125
+ event.preventDefault();
126
+ const target = filteredOptions.value[highlightedIndex.value];
127
+ if (target) {
128
+ selectOption(target);
129
+ }
130
+ }
131
+ }
132
+
133
+ function handleClickOutside(event) {
134
+ if (rootRef.value && !rootRef.value.contains(event.target)) {
135
+ closeDropdown();
136
+ }
137
+ }
138
+
139
+ watch(searchQuery, () => {
140
+ highlightedIndex.value = 0;
141
+ });
142
+
143
+ onMounted(() => {
144
+ document.addEventListener('mousedown', handleClickOutside);
145
+ });
146
+
147
+ onBeforeUnmount(() => {
148
+ document.removeEventListener('mousedown', handleClickOutside);
149
+ });
150
+ </script>
151
+
152
+ <template>
153
+ <div ref="rootRef" class="relative w-full space-y-1.5" @keydown="handleKeydown">
154
+ <label v-if="label" class="block text-sm font-medium text-fg leading-none">
155
+ {{ label }}
156
+ <span v-if="required" class="text-destructive ml-0.5">*</span>
157
+ </label>
158
+
159
+ <button
160
+ type="button"
161
+ role="combobox"
162
+ :aria-expanded="isOpen ? 'true' : 'false'"
163
+ aria-haspopup="listbox"
164
+ :disabled="disabled"
165
+ :class="triggerClasses"
166
+ @click="toggleDropdown"
167
+ >
168
+ <span :class="['truncate', selectedOption ? 'text-fg' : 'text-muted-fg']">
169
+ {{ selectedOption ? selectedOption.label : placeholder }}
170
+ </span>
171
+ <span class="flex items-center gap-1 shrink-0">
172
+ <span
173
+ v-if="clearable && selectedOption && !disabled"
174
+ role="button"
175
+ tabindex="-1"
176
+ class="rounded-full p-0.5 text-muted-fg hover:text-fg hover:bg-surface-hover"
177
+ aria-label="Limpar seleção"
178
+ @click.stop="clearSelection"
179
+ >
180
+ <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
181
+ <path stroke-linecap="round" stroke-linejoin="round" d="M18 6L6 18M6 6l12 12" />
182
+ </svg>
183
+ </span>
184
+ <svg
185
+ :class="['h-4 w-4 text-muted-fg transition-transform duration-200', isOpen && 'rotate-180']"
186
+ viewBox="0 0 24 24"
187
+ fill="none"
188
+ stroke="currentColor"
189
+ stroke-width="2"
190
+ >
191
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 9l6 6 6-6" />
192
+ </svg>
193
+ </span>
194
+ </button>
195
+
196
+ <Transition
197
+ enter-active-class="transition duration-150 ease-out"
198
+ enter-from-class="opacity-0 -translate-y-1 scale-[0.98]"
199
+ enter-to-class="opacity-100 translate-y-0 scale-100"
200
+ leave-active-class="transition duration-100 ease-in"
201
+ leave-from-class="opacity-100 scale-100"
202
+ leave-to-class="opacity-0 scale-[0.98]"
203
+ >
204
+ <div
205
+ v-if="isOpen"
206
+ class="absolute left-0 right-0 z-50 mt-1.5 rounded-birus border border-border bg-popover p-1.5 text-popover-fg shadow-xl"
207
+ >
208
+ <div v-if="searchable" class="mb-1.5 px-1">
209
+ <input
210
+ ref="searchInputRef"
211
+ v-model="searchQuery"
212
+ type="text"
213
+ :placeholder="searchPlaceholder"
214
+ class="w-full rounded-birus-sm border border-input bg-bg px-2.5 py-1.5 text-xs text-fg placeholder:text-muted-fg focus:outline-none focus:border-primary"
215
+ @keydown.stop="handleKeydown"
216
+ />
217
+ </div>
218
+
219
+ <ul role="listbox" class="max-h-60 overflow-y-auto space-y-0.5">
220
+ <li
221
+ v-for="(option, index) in filteredOptions"
222
+ :key="String(option.value)"
223
+ role="option"
224
+ :aria-selected="option.value === modelValue ? 'true' : 'false'"
225
+ :class="[
226
+ 'flex items-start justify-between gap-2 rounded-birus-sm px-2.5 py-2 text-sm transition-colors',
227
+ option.disabled
228
+ ? 'opacity-45 cursor-not-allowed'
229
+ : 'cursor-pointer',
230
+ !option.disabled && (highlightedIndex === index || option.value === modelValue)
231
+ ? 'bg-accent text-accent-fg'
232
+ : !option.disabled && 'hover:bg-surface-hover',
233
+ ]"
234
+ @mouseenter="highlightedIndex = index"
235
+ @click="selectOption(option)"
236
+ >
237
+ <div class="min-w-0">
238
+ <div class="font-medium leading-tight truncate">{{ option.label }}</div>
239
+ <div v-if="option.description" class="text-xs text-muted-fg mt-0.5 leading-snug">
240
+ {{ option.description }}
241
+ </div>
242
+ </div>
243
+ <svg
244
+ v-if="option.value === modelValue"
245
+ class="h-4 w-4 shrink-0 text-primary mt-0.5"
246
+ viewBox="0 0 24 24"
247
+ fill="none"
248
+ stroke="currentColor"
249
+ stroke-width="2.5"
250
+ >
251
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
252
+ </svg>
253
+ </li>
254
+
255
+ <li
256
+ v-if="filteredOptions.length === 0"
257
+ class="px-3 py-4 text-center text-xs text-muted-fg"
258
+ >
259
+ Nenhuma opção encontrada.
260
+ </li>
261
+ </ul>
262
+ </div>
263
+ </Transition>
264
+
265
+ <p
266
+ v-if="errorMessage || hint"
267
+ :class="['text-xs', errorMessage ? 'text-destructive font-medium' : 'text-muted-fg']"
268
+ >
269
+ {{ errorMessage || hint }}
270
+ </p>
271
+ </div>
272
+ </template>
@@ -0,0 +1,72 @@
1
+ <script setup>
2
+ import { computed } from 'vue';
3
+ import { cn, separatorVariants } from '@birus-ui/core';
4
+
5
+ const props = defineProps({
6
+ orientation: {
7
+ type: String,
8
+ default: 'horizontal',
9
+ },
10
+ variant: {
11
+ type: String,
12
+ default: 'solid',
13
+ },
14
+ label: {
15
+ type: String,
16
+ default: '',
17
+ },
18
+ labelPosition: {
19
+ type: String,
20
+ default: 'center',
21
+ },
22
+ decorative: {
23
+ type: Boolean,
24
+ default: true,
25
+ },
26
+ class: {
27
+ type: String,
28
+ default: '',
29
+ },
30
+ });
31
+
32
+ const lineClasses = computed(() =>
33
+ cn(
34
+ separatorVariants({
35
+ orientation: props.orientation,
36
+ variant: props.variant,
37
+ }),
38
+ props.class
39
+ )
40
+ );
41
+ </script>
42
+
43
+ <template>
44
+ <div
45
+ v-if="label && orientation === 'horizontal'"
46
+ :role="decorative ? 'none' : 'separator'"
47
+ :aria-orientation="orientation"
48
+ class="relative flex items-center w-full my-2"
49
+ >
50
+ <div
51
+ v-if="labelPosition !== 'start'"
52
+ class="flex-1"
53
+ :class="separatorVariants({ orientation: 'horizontal', variant })"
54
+ />
55
+ <span
56
+ class="px-3 text-xs font-medium uppercase tracking-wider text-birus-muted-fg select-none shrink-0"
57
+ >
58
+ <slot>{{ label }}</slot>
59
+ </span>
60
+ <div
61
+ v-if="labelPosition !== 'end'"
62
+ class="flex-1"
63
+ :class="separatorVariants({ orientation: 'horizontal', variant })"
64
+ />
65
+ </div>
66
+ <div
67
+ v-else
68
+ :role="decorative ? 'none' : 'separator'"
69
+ :aria-orientation="orientation"
70
+ :class="lineClasses"
71
+ />
72
+ </template>
@@ -0,0 +1,46 @@
1
+ <script setup>
2
+ import { computed } from 'vue';
3
+ import { cn } from '@birus-ui/core';
4
+
5
+ const props = defineProps({
6
+ variant: { type: String, default: 'rect' },
7
+ width: { type: String, default: undefined },
8
+ height: { type: String, default: undefined },
9
+ lines: { type: Number, default: 3 },
10
+ class: { type: String, default: '' },
11
+ });
12
+
13
+ const basePulse = 'animate-pulse bg-muted';
14
+
15
+ const singleClasses = computed(() => {
16
+ switch (props.variant) {
17
+ case 'circle':
18
+ return cn(basePulse, 'rounded-full h-10 w-10', props.class);
19
+ case 'card':
20
+ return cn(basePulse, 'rounded-birus-lg h-36 w-full', props.class);
21
+ default:
22
+ return cn(basePulse, 'rounded-birus h-4 w-full', props.class);
23
+ }
24
+ });
25
+ </script>
26
+
27
+ <template>
28
+ <div v-if="variant === 'text'" class="space-y-2.5 w-full" aria-hidden="true">
29
+ <div
30
+ v-for="n in lines"
31
+ :key="n"
32
+ :class="[
33
+ basePulse,
34
+ 'h-3.5 rounded-birus-sm',
35
+ n === lines && lines > 1 ? 'w-3/4' : 'w-full',
36
+ props.class,
37
+ ]"
38
+ />
39
+ </div>
40
+ <div
41
+ v-else
42
+ :class="singleClasses"
43
+ :style="{ width: width, height: height }"
44
+ aria-hidden="true"
45
+ />
46
+ </template>