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.
- package/README.md +232 -2
- package/dist/birus-ui.js +6919 -0
- package/dist/birus-ui.umd.cjs +10 -0
- package/dist/style.css +1 -0
- package/dist/tailwind-preset.js +70 -0
- package/dist/themes.css +217 -0
- package/package.json +41 -4
- package/src/components/BirusAccordion.vue +112 -0
- package/src/components/BirusAlert.vue +114 -0
- package/src/components/BirusAvatar.vue +67 -0
- package/src/components/BirusBadge.vue +64 -0
- package/src/components/BirusBreadcrumb.vue +61 -0
- package/src/components/BirusButton.vue +84 -0
- package/src/components/BirusCard.vue +89 -0
- package/src/components/BirusCheckbox.vue +106 -0
- package/src/components/BirusDialog.vue +127 -0
- package/src/components/BirusDrawer.vue +158 -0
- package/src/components/BirusDropdownMenu.vue +184 -0
- package/src/components/BirusEmptyState.vue +64 -0
- package/src/components/BirusHeading.vue +70 -0
- package/src/components/BirusInput.vue +197 -0
- package/src/components/BirusKbd.vue +43 -0
- package/src/components/BirusPagination.vue +132 -0
- package/src/components/BirusProgress.vue +70 -0
- package/src/components/BirusRadioGroup.vue +157 -0
- package/src/components/BirusSelect.vue +272 -0
- package/src/components/BirusSeparator.vue +72 -0
- package/src/components/BirusSkeleton.vue +46 -0
- package/src/components/BirusSlider.vue +96 -0
- package/src/components/BirusStatCard.vue +106 -0
- package/src/components/BirusStepper.vue +114 -0
- package/src/components/BirusSwitch.vue +76 -0
- package/src/components/BirusTable.vue +126 -0
- package/src/components/BirusTabs.vue +121 -0
- package/src/components/BirusText.vue +74 -0
- package/src/components/BirusTextarea.vue +84 -0
- package/src/components/BirusTimeline.vue +61 -0
- package/src/components/BirusToaster.vue +130 -0
- package/src/components/BirusTooltip.vue +63 -0
- package/src/composables/useBirusTheme.js +144 -0
- package/src/composables/useToast.js +63 -0
- package/src/index.js +116 -0
- package/src/lib-entry.js +3 -0
- 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>
|