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,184 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
3
|
+
import { dropdownContentVariants, dropdownItemVariants, cn } from '@birus-ui/core';
|
|
4
|
+
import BirusButton from './BirusButton.vue';
|
|
5
|
+
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
items: { type: Array, default: () => [] },
|
|
8
|
+
align: { type: String, default: 'end' }, // 'start' | 'center' | 'end'
|
|
9
|
+
triggerLabel: { type: String, default: 'Opções' },
|
|
10
|
+
triggerVariant: { type: String, default: 'outline' },
|
|
11
|
+
triggerSize: { type: String, default: 'md' },
|
|
12
|
+
class: { type: String, default: '' },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits(['select']);
|
|
16
|
+
|
|
17
|
+
const rootRef = ref(null);
|
|
18
|
+
const isOpen = ref(false);
|
|
19
|
+
const highlightedIndex = ref(-1);
|
|
20
|
+
|
|
21
|
+
const contentClasses = computed(() =>
|
|
22
|
+
cn(dropdownContentVariants({ align: props.align }), props.class),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const actionableIndexes = computed(() =>
|
|
26
|
+
props.items
|
|
27
|
+
.map((item, idx) => (item.divider || item.header || item.disabled ? -1 : idx))
|
|
28
|
+
.filter((idx) => idx !== -1),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
function openMenu() {
|
|
32
|
+
isOpen.value = true;
|
|
33
|
+
highlightedIndex.value = actionableIndexes.value[0] ?? -1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function closeMenu() {
|
|
37
|
+
isOpen.value = false;
|
|
38
|
+
highlightedIndex.value = -1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toggleMenu() {
|
|
42
|
+
if (isOpen.value) {
|
|
43
|
+
closeMenu();
|
|
44
|
+
} else {
|
|
45
|
+
openMenu();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function selectItem(item) {
|
|
50
|
+
if (item.disabled || item.divider || item.header) return;
|
|
51
|
+
emit('select', item);
|
|
52
|
+
if (typeof item.onClick === 'function') {
|
|
53
|
+
item.onClick(item);
|
|
54
|
+
}
|
|
55
|
+
closeMenu();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function handleKeydown(event) {
|
|
59
|
+
if (!isOpen.value) {
|
|
60
|
+
if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
openMenu();
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (event.key === 'Escape') {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
closeMenu();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const list = actionableIndexes.value;
|
|
74
|
+
if (!list.length) return;
|
|
75
|
+
const currentPos = list.indexOf(highlightedIndex.value);
|
|
76
|
+
|
|
77
|
+
if (event.key === 'ArrowDown') {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
highlightedIndex.value = list[(currentPos + 1) % list.length];
|
|
80
|
+
} else if (event.key === 'ArrowUp') {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
highlightedIndex.value = list[(currentPos - 1 + list.length) % list.length];
|
|
83
|
+
} else if (event.key === 'Enter') {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
const item = props.items[highlightedIndex.value];
|
|
86
|
+
if (item) selectItem(item);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function handleClickOutside(event) {
|
|
91
|
+
if (rootRef.value && !rootRef.value.contains(event.target)) {
|
|
92
|
+
closeMenu();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
onMounted(() => {
|
|
97
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
onBeforeUnmount(() => {
|
|
101
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
102
|
+
});
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<template>
|
|
106
|
+
<div ref="rootRef" class="relative inline-block text-left" @keydown="handleKeydown">
|
|
107
|
+
<slot name="trigger" :is-open="isOpen" :toggle="toggleMenu" :open="openMenu" :close="closeMenu">
|
|
108
|
+
<BirusButton
|
|
109
|
+
:variant="triggerVariant"
|
|
110
|
+
:size="triggerSize"
|
|
111
|
+
aria-haspopup="menu"
|
|
112
|
+
:aria-expanded="isOpen ? 'true' : 'false'"
|
|
113
|
+
@click="toggleMenu"
|
|
114
|
+
>
|
|
115
|
+
<span>{{ triggerLabel }}</span>
|
|
116
|
+
<template #suffix>
|
|
117
|
+
<svg
|
|
118
|
+
:class="['h-4 w-4 transition-transform duration-150', isOpen && 'rotate-180']"
|
|
119
|
+
viewBox="0 0 24 24"
|
|
120
|
+
fill="none"
|
|
121
|
+
stroke="currentColor"
|
|
122
|
+
stroke-width="2"
|
|
123
|
+
>
|
|
124
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 9l6 6 6-6" />
|
|
125
|
+
</svg>
|
|
126
|
+
</template>
|
|
127
|
+
</BirusButton>
|
|
128
|
+
</slot>
|
|
129
|
+
|
|
130
|
+
<Transition
|
|
131
|
+
enter-active-class="transition duration-150 ease-out"
|
|
132
|
+
enter-from-class="opacity-0 scale-95 -translate-y-1"
|
|
133
|
+
enter-to-class="opacity-100 scale-100 translate-y-0"
|
|
134
|
+
leave-active-class="transition duration-100 ease-in"
|
|
135
|
+
leave-from-class="opacity-100 scale-100"
|
|
136
|
+
leave-to-class="opacity-0 scale-95"
|
|
137
|
+
>
|
|
138
|
+
<div v-if="isOpen" role="menu" :class="contentClasses">
|
|
139
|
+
<template v-for="(item, index) in items" :key="item.id || index">
|
|
140
|
+
<div
|
|
141
|
+
v-if="item.divider"
|
|
142
|
+
class="my-1 h-px bg-border"
|
|
143
|
+
role="separator"
|
|
144
|
+
/>
|
|
145
|
+
<div
|
|
146
|
+
v-else-if="item.header"
|
|
147
|
+
class="px-2.5 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-fg"
|
|
148
|
+
>
|
|
149
|
+
{{ item.header }}
|
|
150
|
+
</div>
|
|
151
|
+
<button
|
|
152
|
+
v-else
|
|
153
|
+
type="button"
|
|
154
|
+
role="menuitem"
|
|
155
|
+
:disabled="item.disabled"
|
|
156
|
+
:class="[
|
|
157
|
+
dropdownItemVariants({
|
|
158
|
+
variant: item.variant || 'default',
|
|
159
|
+
disabled: Boolean(item.disabled),
|
|
160
|
+
}),
|
|
161
|
+
highlightedIndex === index && 'bg-surface-hover',
|
|
162
|
+
]"
|
|
163
|
+
@mouseenter="highlightedIndex = index"
|
|
164
|
+
@click="selectItem(item)"
|
|
165
|
+
>
|
|
166
|
+
<div class="flex flex-col min-w-0">
|
|
167
|
+
<span class="font-medium truncate">{{ item.label }}</span>
|
|
168
|
+
<span v-if="item.description" class="text-xs text-muted-fg truncate">
|
|
169
|
+
{{ item.description }}
|
|
170
|
+
</span>
|
|
171
|
+
</div>
|
|
172
|
+
<kbd
|
|
173
|
+
v-if="item.shortcut"
|
|
174
|
+
class="ml-auto rounded border border-border bg-surface px-1.5 py-0.5 text-[10px] font-mono text-muted-fg shrink-0"
|
|
175
|
+
>
|
|
176
|
+
{{ item.shortcut }}
|
|
177
|
+
</kbd>
|
|
178
|
+
</button>
|
|
179
|
+
</template>
|
|
180
|
+
<slot :close="closeMenu" />
|
|
181
|
+
</div>
|
|
182
|
+
</Transition>
|
|
183
|
+
</div>
|
|
184
|
+
</template>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { cn } from '@birus-ui/core';
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
title: {
|
|
7
|
+
type: String,
|
|
8
|
+
default: 'Nenhum item encontrado',
|
|
9
|
+
},
|
|
10
|
+
description: {
|
|
11
|
+
type: String,
|
|
12
|
+
default: '',
|
|
13
|
+
},
|
|
14
|
+
variant: {
|
|
15
|
+
type: String,
|
|
16
|
+
default: 'default',
|
|
17
|
+
},
|
|
18
|
+
class: {
|
|
19
|
+
type: String,
|
|
20
|
+
default: '',
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const containerClasses = computed(() =>
|
|
25
|
+
cn(
|
|
26
|
+
'flex flex-col items-center justify-center text-center rounded-birus-lg transition-all',
|
|
27
|
+
props.variant === 'default' && 'border border-birus-border bg-birus-card/60 p-8 sm:p-10',
|
|
28
|
+
props.variant === 'dashed' && 'border-2 border-dashed border-birus-border bg-birus-surface/30 p-8 sm:p-10',
|
|
29
|
+
props.variant === 'compact' && 'border border-birus-border bg-birus-surface/20 p-5',
|
|
30
|
+
props.class
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<div :class="containerClasses">
|
|
37
|
+
<div
|
|
38
|
+
class="mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-birus-primary/10 text-birus-primary ring-4 ring-birus-primary/5"
|
|
39
|
+
>
|
|
40
|
+
<slot name="icon">
|
|
41
|
+
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
|
|
42
|
+
<path
|
|
43
|
+
stroke-linecap="round"
|
|
44
|
+
stroke-linejoin="round"
|
|
45
|
+
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
|
46
|
+
/>
|
|
47
|
+
</svg>
|
|
48
|
+
</slot>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<h3 class="text-base font-semibold text-birus-fg">{{ title }}</h3>
|
|
52
|
+
<p v-if="description" class="mt-1.5 max-w-sm text-sm text-birus-muted-fg leading-relaxed">
|
|
53
|
+
{{ description }}
|
|
54
|
+
</p>
|
|
55
|
+
|
|
56
|
+
<div v-if="$slots.default" class="mt-4 w-full max-w-sm">
|
|
57
|
+
<slot />
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div v-if="$slots.actions" class="mt-5 flex flex-wrap items-center justify-center gap-2.5">
|
|
61
|
+
<slot name="actions" />
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { cn, headingVariants } from '@birus-ui/core';
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
as: {
|
|
7
|
+
type: String,
|
|
8
|
+
default: 'h2',
|
|
9
|
+
},
|
|
10
|
+
size: {
|
|
11
|
+
type: String,
|
|
12
|
+
default: 'xl',
|
|
13
|
+
},
|
|
14
|
+
weight: {
|
|
15
|
+
type: String,
|
|
16
|
+
default: 'bold',
|
|
17
|
+
},
|
|
18
|
+
tracking: {
|
|
19
|
+
type: String,
|
|
20
|
+
default: 'tight',
|
|
21
|
+
},
|
|
22
|
+
align: {
|
|
23
|
+
type: String,
|
|
24
|
+
default: 'left',
|
|
25
|
+
},
|
|
26
|
+
gradient: {
|
|
27
|
+
type: Boolean,
|
|
28
|
+
default: false,
|
|
29
|
+
},
|
|
30
|
+
balance: {
|
|
31
|
+
type: Boolean,
|
|
32
|
+
default: false,
|
|
33
|
+
},
|
|
34
|
+
accentLine: {
|
|
35
|
+
type: Boolean,
|
|
36
|
+
default: false,
|
|
37
|
+
},
|
|
38
|
+
class: {
|
|
39
|
+
type: String,
|
|
40
|
+
default: '',
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const classes = computed(() =>
|
|
45
|
+
cn(
|
|
46
|
+
headingVariants({
|
|
47
|
+
size: props.size,
|
|
48
|
+
weight: props.weight,
|
|
49
|
+
tracking: props.tracking,
|
|
50
|
+
align: props.align,
|
|
51
|
+
gradient: props.gradient,
|
|
52
|
+
}),
|
|
53
|
+
props.balance && 'text-balance',
|
|
54
|
+
props.class
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<template>
|
|
60
|
+
<div class="inline-flex flex-col gap-1.5" :class="align === 'center' ? 'items-center w-full' : align === 'right' ? 'items-end w-full' : 'items-start'">
|
|
61
|
+
<component :is="as" :class="classes">
|
|
62
|
+
<slot />
|
|
63
|
+
</component>
|
|
64
|
+
<span
|
|
65
|
+
v-if="accentLine"
|
|
66
|
+
class="h-1 w-12 rounded-full bg-gradient-to-r from-birus-primary to-birus-info"
|
|
67
|
+
aria-hidden="true"
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, useSlots } from 'vue';
|
|
3
|
+
import { inputVariants, cn } from '@birus-ui/core';
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
modelValue: { type: [String, Number], default: '' },
|
|
7
|
+
type: { type: String, default: 'text' },
|
|
8
|
+
placeholder: { type: String, default: '' },
|
|
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
|
+
prefixText: { type: String, default: '' },
|
|
15
|
+
suffixText: { type: String, default: '' },
|
|
16
|
+
clearable: { type: Boolean, default: false },
|
|
17
|
+
maxlength: { type: Number, default: undefined },
|
|
18
|
+
disabled: { type: Boolean, default: false },
|
|
19
|
+
readonly: { type: Boolean, default: false },
|
|
20
|
+
required: { type: Boolean, default: false },
|
|
21
|
+
id: { type: String, default: undefined },
|
|
22
|
+
class: { type: String, default: '' },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const emit = defineEmits(['update:modelValue', 'clear', 'focus', 'blur']);
|
|
26
|
+
|
|
27
|
+
const slots = useSlots();
|
|
28
|
+
const showPassword = ref(false);
|
|
29
|
+
const uid = props.id || `birus-input-${Math.random().toString(36).slice(2, 9)}`;
|
|
30
|
+
|
|
31
|
+
const resolvedState = computed(() => (props.errorMessage ? 'error' : props.state));
|
|
32
|
+
|
|
33
|
+
const effectiveType = computed(() => {
|
|
34
|
+
if (props.type === 'password') {
|
|
35
|
+
return showPassword.value ? 'text' : 'password';
|
|
36
|
+
}
|
|
37
|
+
return props.type;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const stringLength = computed(() => String(props.modelValue ?? '').length);
|
|
41
|
+
const hasValue = computed(() => stringLength.value > 0);
|
|
42
|
+
const hasLeftSlot = computed(() => Boolean(slots.left));
|
|
43
|
+
const hasRightControls = computed(
|
|
44
|
+
() =>
|
|
45
|
+
Boolean(slots.right) ||
|
|
46
|
+
props.type === 'password' ||
|
|
47
|
+
(props.clearable && hasValue.value && !props.disabled),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const inputClasses = computed(() =>
|
|
51
|
+
cn(
|
|
52
|
+
inputVariants({
|
|
53
|
+
state: resolvedState.value,
|
|
54
|
+
size: props.size,
|
|
55
|
+
}),
|
|
56
|
+
hasLeftSlot.value && 'pl-9',
|
|
57
|
+
hasRightControls.value && 'pr-10',
|
|
58
|
+
props.prefixText && 'rounded-l-none',
|
|
59
|
+
props.suffixText && 'rounded-r-none',
|
|
60
|
+
props.class,
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
function onInput(event) {
|
|
65
|
+
emit('update:modelValue', event.target.value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleClear() {
|
|
69
|
+
emit('update:modelValue', '');
|
|
70
|
+
emit('clear');
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<div class="w-full space-y-1.5">
|
|
76
|
+
<div v-if="label || maxlength" class="flex items-center justify-between gap-2">
|
|
77
|
+
<label
|
|
78
|
+
v-if="label"
|
|
79
|
+
:for="uid"
|
|
80
|
+
class="text-sm font-medium text-fg leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
81
|
+
>
|
|
82
|
+
{{ label }}
|
|
83
|
+
<span v-if="required" class="text-destructive ml-0.5">*</span>
|
|
84
|
+
</label>
|
|
85
|
+
<span v-if="maxlength" class="text-xs text-muted-fg tabular-nums ml-auto">
|
|
86
|
+
{{ stringLength }}/{{ maxlength }}
|
|
87
|
+
</span>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="flex items-stretch">
|
|
91
|
+
<span
|
|
92
|
+
v-if="prefixText"
|
|
93
|
+
class="inline-flex items-center rounded-l-birus border border-r-0 border-input bg-surface px-3 text-xs font-mono text-muted-fg select-none"
|
|
94
|
+
>
|
|
95
|
+
{{ prefixText }}
|
|
96
|
+
</span>
|
|
97
|
+
|
|
98
|
+
<div class="relative flex flex-1 items-center">
|
|
99
|
+
<span
|
|
100
|
+
v-if="hasLeftSlot"
|
|
101
|
+
class="pointer-events-none absolute left-3 flex items-center text-muted-fg"
|
|
102
|
+
>
|
|
103
|
+
<slot name="left" />
|
|
104
|
+
</span>
|
|
105
|
+
|
|
106
|
+
<input
|
|
107
|
+
:id="uid"
|
|
108
|
+
:type="effectiveType"
|
|
109
|
+
:value="modelValue"
|
|
110
|
+
:placeholder="placeholder"
|
|
111
|
+
:disabled="disabled"
|
|
112
|
+
:readonly="readonly"
|
|
113
|
+
:required="required"
|
|
114
|
+
:maxlength="maxlength"
|
|
115
|
+
:aria-invalid="resolvedState === 'error' ? 'true' : undefined"
|
|
116
|
+
:aria-describedby="errorMessage || hint ? `${uid}-desc` : undefined"
|
|
117
|
+
:class="inputClasses"
|
|
118
|
+
@input="onInput"
|
|
119
|
+
@focus="emit('focus', $event)"
|
|
120
|
+
@blur="emit('blur', $event)"
|
|
121
|
+
/>
|
|
122
|
+
|
|
123
|
+
<div
|
|
124
|
+
v-if="hasRightControls"
|
|
125
|
+
class="absolute right-2.5 flex items-center gap-1 text-muted-fg"
|
|
126
|
+
>
|
|
127
|
+
<button
|
|
128
|
+
v-if="clearable && hasValue && !disabled && !readonly"
|
|
129
|
+
type="button"
|
|
130
|
+
class="inline-flex h-6 w-6 items-center justify-center rounded-birus-sm hover:bg-surface-hover hover:text-fg transition-colors"
|
|
131
|
+
aria-label="Limpar campo"
|
|
132
|
+
@click="handleClear"
|
|
133
|
+
>
|
|
134
|
+
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
135
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M18 6L6 18M6 6l12 12" />
|
|
136
|
+
</svg>
|
|
137
|
+
</button>
|
|
138
|
+
|
|
139
|
+
<button
|
|
140
|
+
v-if="type === 'password'"
|
|
141
|
+
type="button"
|
|
142
|
+
class="inline-flex h-6 w-6 items-center justify-center rounded-birus-sm hover:bg-surface-hover hover:text-fg transition-colors"
|
|
143
|
+
:aria-label="showPassword ? 'Ocultar senha' : 'Mostrar senha'"
|
|
144
|
+
@click="showPassword = !showPassword"
|
|
145
|
+
>
|
|
146
|
+
<svg
|
|
147
|
+
v-if="showPassword"
|
|
148
|
+
class="h-4 w-4"
|
|
149
|
+
viewBox="0 0 24 24"
|
|
150
|
+
fill="none"
|
|
151
|
+
stroke="currentColor"
|
|
152
|
+
stroke-width="2"
|
|
153
|
+
>
|
|
154
|
+
<path
|
|
155
|
+
stroke-linecap="round"
|
|
156
|
+
stroke-linejoin="round"
|
|
157
|
+
d="M3 3l18 18M10.584 10.587a2 2 0 002.828 2.83M9.363 5.365A9.466 9.466 0 0112 5c4.478 0 8.268 2.943 9.543 7a9.97 9.97 0 01-1.563 3.029m-5.858.908a3 3 0 01-4.243-4.243M3.735 7.736A9.969 9.969 0 002.458 12c1.274 4.057 5.064 7 9.542 7a9.46 9.46 0 003.32-.6"
|
|
158
|
+
/>
|
|
159
|
+
</svg>
|
|
160
|
+
<svg
|
|
161
|
+
v-else
|
|
162
|
+
class="h-4 w-4"
|
|
163
|
+
viewBox="0 0 24 24"
|
|
164
|
+
fill="none"
|
|
165
|
+
stroke="currentColor"
|
|
166
|
+
stroke-width="2"
|
|
167
|
+
>
|
|
168
|
+
<path
|
|
169
|
+
stroke-linecap="round"
|
|
170
|
+
stroke-linejoin="round"
|
|
171
|
+
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
|
172
|
+
/>
|
|
173
|
+
<circle cx="12" cy="12" r="3" />
|
|
174
|
+
</svg>
|
|
175
|
+
</button>
|
|
176
|
+
|
|
177
|
+
<slot name="right" />
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<span
|
|
182
|
+
v-if="suffixText"
|
|
183
|
+
class="inline-flex items-center rounded-r-birus border border-l-0 border-input bg-surface px-3 text-xs font-mono text-muted-fg select-none"
|
|
184
|
+
>
|
|
185
|
+
{{ suffixText }}
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<p
|
|
190
|
+
v-if="errorMessage || hint"
|
|
191
|
+
:id="`${uid}-desc`"
|
|
192
|
+
:class="['text-xs', errorMessage ? 'text-destructive font-medium' : 'text-muted-fg']"
|
|
193
|
+
>
|
|
194
|
+
{{ errorMessage || hint }}
|
|
195
|
+
</p>
|
|
196
|
+
</div>
|
|
197
|
+
</template>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { kbdVariants, cn } from '@birus-ui/core';
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
keys: { type: [Array, String], default: () => [] },
|
|
7
|
+
size: { type: String, default: 'md' }, // 'sm' | 'md' | 'lg'
|
|
8
|
+
variant: { type: String, default: 'default' }, // 'default' | 'outline' | 'primary'
|
|
9
|
+
class: { type: String, default: '' },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const normalizedKeys = computed(() => {
|
|
13
|
+
if (Array.isArray(props.keys)) return props.keys;
|
|
14
|
+
if (typeof props.keys === 'string' && props.keys.trim()) {
|
|
15
|
+
return props.keys.split('+').map((k) => k.trim());
|
|
16
|
+
}
|
|
17
|
+
return [];
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const keyClasses = computed(() =>
|
|
21
|
+
cn(kbdVariants({ size: props.size, variant: props.variant }), props.class),
|
|
22
|
+
);
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<span class="inline-flex items-center gap-1 align-middle">
|
|
27
|
+
<template v-if="normalizedKeys.length > 0">
|
|
28
|
+
<template v-for="(k, idx) in normalizedKeys" :key="`${k}-${idx}`">
|
|
29
|
+
<kbd :class="keyClasses">{{ k }}</kbd>
|
|
30
|
+
<span
|
|
31
|
+
v-if="idx < normalizedKeys.length - 1"
|
|
32
|
+
class="text-[10px] text-muted-fg font-mono select-none"
|
|
33
|
+
aria-hidden="true"
|
|
34
|
+
>
|
|
35
|
+
+
|
|
36
|
+
</span>
|
|
37
|
+
</template>
|
|
38
|
+
</template>
|
|
39
|
+
<kbd v-else :class="keyClasses">
|
|
40
|
+
<slot />
|
|
41
|
+
</kbd>
|
|
42
|
+
</span>
|
|
43
|
+
</template>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { paginationItemVariants, cn } from '@birus-ui/core';
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
modelValue: { type: Number, default: 1 },
|
|
7
|
+
totalPages: { type: Number, default: 10 },
|
|
8
|
+
siblingCount: { type: Number, default: 1 },
|
|
9
|
+
variant: { type: String, default: 'default' }, // 'default' | 'outline' | 'pills'
|
|
10
|
+
size: { type: String, default: 'md' },
|
|
11
|
+
showEdges: { type: Boolean, default: true },
|
|
12
|
+
disabled: { type: Boolean, default: false },
|
|
13
|
+
class: { type: String, default: '' },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const emit = defineEmits(['update:modelValue', 'change']);
|
|
17
|
+
|
|
18
|
+
const pages = computed(() => {
|
|
19
|
+
const total = Math.max(1, props.totalPages);
|
|
20
|
+
const current = Math.min(total, Math.max(1, props.modelValue));
|
|
21
|
+
const siblings = props.siblingCount;
|
|
22
|
+
|
|
23
|
+
const totalNumbers = siblings * 2 + 5;
|
|
24
|
+
if (total <= totalNumbers) {
|
|
25
|
+
return Array.from({ length: total }, (_, i) => i + 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const leftSibling = Math.max(current - siblings, 1);
|
|
29
|
+
const rightSibling = Math.min(current + siblings, total);
|
|
30
|
+
|
|
31
|
+
const showLeftDots = leftSibling > 2;
|
|
32
|
+
const showRightDots = rightSibling < total - 1;
|
|
33
|
+
|
|
34
|
+
if (!showLeftDots && showRightDots) {
|
|
35
|
+
const leftCount = 3 + 2 * siblings;
|
|
36
|
+
const leftRange = Array.from({ length: leftCount }, (_, i) => i + 1);
|
|
37
|
+
return [...leftRange, '...', total];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (showLeftDots && !showRightDots) {
|
|
41
|
+
const rightCount = 3 + 2 * siblings;
|
|
42
|
+
const rightRange = Array.from({ length: rightCount }, (_, i) => total - rightCount + i + 1);
|
|
43
|
+
return [1, '...', ...rightRange];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const middleRange = Array.from(
|
|
47
|
+
{ length: rightSibling - leftSibling + 1 },
|
|
48
|
+
(_, i) => leftSibling + i,
|
|
49
|
+
);
|
|
50
|
+
return [1, '...', ...middleRange, '...', total];
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function goToPage(page) {
|
|
54
|
+
if (props.disabled) return;
|
|
55
|
+
const clamped = Math.min(props.totalPages, Math.max(1, page));
|
|
56
|
+
if (clamped !== props.modelValue) {
|
|
57
|
+
emit('update:modelValue', clamped);
|
|
58
|
+
emit('change', clamped);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<template>
|
|
64
|
+
<nav role="navigation" aria-label="Paginação" :class="cn('inline-flex items-center gap-1.5 flex-wrap', props.class)">
|
|
65
|
+
<!-- Primeira página -->
|
|
66
|
+
<button
|
|
67
|
+
v-if="showEdges"
|
|
68
|
+
type="button"
|
|
69
|
+
:disabled="disabled || modelValue <= 1"
|
|
70
|
+
:class="paginationItemVariants({ variant, size, active: false })"
|
|
71
|
+
aria-label="Primeira página"
|
|
72
|
+
@click="goToPage(1)"
|
|
73
|
+
>
|
|
74
|
+
«
|
|
75
|
+
</button>
|
|
76
|
+
|
|
77
|
+
<!-- Anterior -->
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
:disabled="disabled || modelValue <= 1"
|
|
81
|
+
:class="paginationItemVariants({ variant, size, active: false })"
|
|
82
|
+
aria-label="Página anterior"
|
|
83
|
+
@click="goToPage(modelValue - 1)"
|
|
84
|
+
>
|
|
85
|
+
‹ Anterior
|
|
86
|
+
</button>
|
|
87
|
+
|
|
88
|
+
<!-- Números -->
|
|
89
|
+
<template v-for="(item, idx) in pages" :key="`${item}-${idx}`">
|
|
90
|
+
<span
|
|
91
|
+
v-if="item === '...'"
|
|
92
|
+
class="px-2 text-xs text-muted-fg select-none"
|
|
93
|
+
aria-hidden="true"
|
|
94
|
+
>
|
|
95
|
+
•••
|
|
96
|
+
</span>
|
|
97
|
+
<button
|
|
98
|
+
v-else
|
|
99
|
+
type="button"
|
|
100
|
+
:disabled="disabled"
|
|
101
|
+
:aria-current="modelValue === item ? 'page' : undefined"
|
|
102
|
+
:class="paginationItemVariants({ variant, size, active: modelValue === item })"
|
|
103
|
+
@click="goToPage(item)"
|
|
104
|
+
>
|
|
105
|
+
{{ item }}
|
|
106
|
+
</button>
|
|
107
|
+
</template>
|
|
108
|
+
|
|
109
|
+
<!-- Próxima -->
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
:disabled="disabled || modelValue >= totalPages"
|
|
113
|
+
:class="paginationItemVariants({ variant, size, active: false })"
|
|
114
|
+
aria-label="Próxima página"
|
|
115
|
+
@click="goToPage(modelValue + 1)"
|
|
116
|
+
>
|
|
117
|
+
Próxima ›
|
|
118
|
+
</button>
|
|
119
|
+
|
|
120
|
+
<!-- Última página -->
|
|
121
|
+
<button
|
|
122
|
+
v-if="showEdges"
|
|
123
|
+
type="button"
|
|
124
|
+
:disabled="disabled || modelValue >= totalPages"
|
|
125
|
+
:class="paginationItemVariants({ variant, size, active: false })"
|
|
126
|
+
aria-label="Última página"
|
|
127
|
+
@click="goToPage(totalPages)"
|
|
128
|
+
>
|
|
129
|
+
»
|
|
130
|
+
</button>
|
|
131
|
+
</nav>
|
|
132
|
+
</template>
|