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,74 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { cn, textVariants } from '@birus-ui/core';
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
as: {
|
|
7
|
+
type: String,
|
|
8
|
+
default: '',
|
|
9
|
+
},
|
|
10
|
+
variant: {
|
|
11
|
+
type: String,
|
|
12
|
+
default: 'body',
|
|
13
|
+
},
|
|
14
|
+
tone: {
|
|
15
|
+
type: String,
|
|
16
|
+
default: 'default',
|
|
17
|
+
},
|
|
18
|
+
weight: {
|
|
19
|
+
type: String,
|
|
20
|
+
default: 'normal',
|
|
21
|
+
},
|
|
22
|
+
align: {
|
|
23
|
+
type: String,
|
|
24
|
+
default: 'left',
|
|
25
|
+
},
|
|
26
|
+
truncate: {
|
|
27
|
+
type: Boolean,
|
|
28
|
+
default: false,
|
|
29
|
+
},
|
|
30
|
+
clamp: {
|
|
31
|
+
type: Number,
|
|
32
|
+
default: 0,
|
|
33
|
+
},
|
|
34
|
+
class: {
|
|
35
|
+
type: String,
|
|
36
|
+
default: '',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const tag = computed(() => {
|
|
41
|
+
if (props.as) return props.as;
|
|
42
|
+
if (props.variant === 'code') return 'code';
|
|
43
|
+
if (props.variant === 'quote') return 'blockquote';
|
|
44
|
+
if (props.variant === 'overline' || props.variant === 'caption') return 'span';
|
|
45
|
+
return 'p';
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const clampClasses = {
|
|
49
|
+
1: 'line-clamp-1',
|
|
50
|
+
2: 'line-clamp-2',
|
|
51
|
+
3: 'line-clamp-3',
|
|
52
|
+
4: 'line-clamp-4',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const classes = computed(() =>
|
|
56
|
+
cn(
|
|
57
|
+
textVariants({
|
|
58
|
+
variant: props.variant,
|
|
59
|
+
tone: props.tone,
|
|
60
|
+
weight: props.weight,
|
|
61
|
+
align: props.align,
|
|
62
|
+
}),
|
|
63
|
+
props.truncate && 'truncate block',
|
|
64
|
+
props.clamp > 0 && clampClasses[props.clamp],
|
|
65
|
+
props.class
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<template>
|
|
71
|
+
<component :is="tag" :class="classes">
|
|
72
|
+
<slot />
|
|
73
|
+
</component>
|
|
74
|
+
</template>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref } from 'vue';
|
|
3
|
+
import { textareaVariants, cn } from '@birus-ui/core';
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
modelValue: { type: String, default: '' },
|
|
7
|
+
placeholder: { type: String, default: '' },
|
|
8
|
+
label: { type: String, default: '' },
|
|
9
|
+
hint: { type: String, default: '' },
|
|
10
|
+
errorMessage: { type: String, default: '' },
|
|
11
|
+
state: { type: String, default: 'default' },
|
|
12
|
+
rows: { type: Number, default: 4 },
|
|
13
|
+
maxlength: { type: Number, default: undefined },
|
|
14
|
+
autoResize: { type: Boolean, default: false },
|
|
15
|
+
disabled: { type: Boolean, default: false },
|
|
16
|
+
readonly: { type: Boolean, default: false },
|
|
17
|
+
required: { type: Boolean, default: false },
|
|
18
|
+
id: { type: String, default: undefined },
|
|
19
|
+
class: { type: String, default: '' },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const emit = defineEmits(['update:modelValue']);
|
|
23
|
+
|
|
24
|
+
const textareaRef = ref(null);
|
|
25
|
+
const uid = props.id || `birus-textarea-${Math.random().toString(36).slice(2, 9)}`;
|
|
26
|
+
|
|
27
|
+
const resolvedState = computed(() => (props.errorMessage ? 'error' : props.state));
|
|
28
|
+
const stringLength = computed(() => String(props.modelValue ?? '').length);
|
|
29
|
+
|
|
30
|
+
const classes = computed(() =>
|
|
31
|
+
cn(
|
|
32
|
+
textareaVariants({
|
|
33
|
+
state: resolvedState.value,
|
|
34
|
+
}),
|
|
35
|
+
props.autoResize && 'resize-none overflow-hidden',
|
|
36
|
+
props.class,
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
function onInput(event) {
|
|
41
|
+
const target = event.target;
|
|
42
|
+
if (props.autoResize && textareaRef.value) {
|
|
43
|
+
textareaRef.value.style.height = 'auto';
|
|
44
|
+
textareaRef.value.style.height = `${textareaRef.value.scrollHeight}px`;
|
|
45
|
+
}
|
|
46
|
+
emit('update:modelValue', target.value);
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<div class="w-full space-y-1.5">
|
|
52
|
+
<div v-if="label || maxlength" class="flex items-center justify-between gap-2">
|
|
53
|
+
<label v-if="label" :for="uid" class="text-sm font-medium text-fg leading-none">
|
|
54
|
+
{{ label }}
|
|
55
|
+
<span v-if="required" class="text-destructive ml-0.5">*</span>
|
|
56
|
+
</label>
|
|
57
|
+
<span v-if="maxlength" class="text-xs text-muted-fg tabular-nums ml-auto">
|
|
58
|
+
{{ stringLength }}/{{ maxlength }}
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<textarea
|
|
63
|
+
:id="uid"
|
|
64
|
+
ref="textareaRef"
|
|
65
|
+
:value="modelValue"
|
|
66
|
+
:rows="rows"
|
|
67
|
+
:placeholder="placeholder"
|
|
68
|
+
:disabled="disabled"
|
|
69
|
+
:readonly="readonly"
|
|
70
|
+
:required="required"
|
|
71
|
+
:maxlength="maxlength"
|
|
72
|
+
:aria-invalid="resolvedState === 'error' ? 'true' : undefined"
|
|
73
|
+
:class="classes"
|
|
74
|
+
@input="onInput"
|
|
75
|
+
/>
|
|
76
|
+
|
|
77
|
+
<p
|
|
78
|
+
v-if="errorMessage || hint"
|
|
79
|
+
:class="['text-xs', errorMessage ? 'text-destructive font-medium' : 'text-muted-fg']"
|
|
80
|
+
>
|
|
81
|
+
{{ errorMessage || hint }}
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
</template>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { cn } from '@birus-ui/core';
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
items: {
|
|
6
|
+
type: Array,
|
|
7
|
+
default: () => [],
|
|
8
|
+
},
|
|
9
|
+
class: {
|
|
10
|
+
type: String,
|
|
11
|
+
default: '',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const dotStyles = {
|
|
16
|
+
primary: 'bg-birus-primary ring-birus-primary/20',
|
|
17
|
+
success: 'bg-birus-success ring-birus-success/20',
|
|
18
|
+
warning: 'bg-birus-warning ring-birus-warning/20',
|
|
19
|
+
destructive: 'bg-birus-destructive ring-birus-destructive/20',
|
|
20
|
+
info: 'bg-birus-info ring-birus-info/20',
|
|
21
|
+
muted: 'bg-birus-muted-fg/50 ring-birus-muted',
|
|
22
|
+
};
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<ol :class="cn('relative space-y-6 before:absolute before:left-[11px] before:top-2 before:bottom-2 before:w-[2px] before:bg-birus-border', props.class)">
|
|
27
|
+
<li
|
|
28
|
+
v-for="(item, index) in items"
|
|
29
|
+
:key="item.id || index"
|
|
30
|
+
class="relative pl-8 group"
|
|
31
|
+
>
|
|
32
|
+
<span
|
|
33
|
+
class="absolute left-0 top-1.5 flex h-6 w-6 items-center justify-center rounded-full bg-birus-card border border-birus-border shadow-xs"
|
|
34
|
+
>
|
|
35
|
+
<span
|
|
36
|
+
class="h-2.5 w-2.5 rounded-full ring-4 transition-transform group-hover:scale-110"
|
|
37
|
+
:class="dotStyles[item.status || 'primary'] || dotStyles.primary"
|
|
38
|
+
/>
|
|
39
|
+
</span>
|
|
40
|
+
|
|
41
|
+
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-1">
|
|
42
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
43
|
+
<h4 class="text-sm font-semibold text-birus-fg">{{ item.title }}</h4>
|
|
44
|
+
<span
|
|
45
|
+
v-if="item.badge"
|
|
46
|
+
class="inline-flex items-center rounded-full bg-birus-primary/10 px-2 py-0.5 text-[11px] font-medium text-birus-primary border border-birus-primary/20"
|
|
47
|
+
>
|
|
48
|
+
{{ item.badge }}
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
<time v-if="item.time" class="text-xs font-mono text-birus-muted-fg shrink-0">
|
|
52
|
+
{{ item.time }}
|
|
53
|
+
</time>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<p v-if="item.description" class="mt-1 text-xs sm:text-sm text-birus-muted-fg leading-relaxed">
|
|
57
|
+
{{ item.description }}
|
|
58
|
+
</p>
|
|
59
|
+
</li>
|
|
60
|
+
</ol>
|
|
61
|
+
</template>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useToast } from '../composables/useToast.js';
|
|
3
|
+
|
|
4
|
+
defineProps({
|
|
5
|
+
position: { type: String, default: 'bottom-right' },
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const { toasts, dismiss } = useToast();
|
|
9
|
+
|
|
10
|
+
function getPositionClasses(pos) {
|
|
11
|
+
switch (pos) {
|
|
12
|
+
case 'top-right':
|
|
13
|
+
return 'top-4 right-4';
|
|
14
|
+
case 'top-left':
|
|
15
|
+
return 'top-4 left-4';
|
|
16
|
+
case 'bottom-left':
|
|
17
|
+
return 'bottom-4 left-4';
|
|
18
|
+
default:
|
|
19
|
+
return 'bottom-4 right-4';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getAccentClasses(variant) {
|
|
24
|
+
switch (variant) {
|
|
25
|
+
case 'success':
|
|
26
|
+
return 'border-success/40 bg-card';
|
|
27
|
+
case 'warning':
|
|
28
|
+
return 'border-warning/40 bg-card';
|
|
29
|
+
case 'destructive':
|
|
30
|
+
return 'border-destructive/40 bg-card';
|
|
31
|
+
case 'info':
|
|
32
|
+
return 'border-info/40 bg-card';
|
|
33
|
+
default:
|
|
34
|
+
return 'border-border bg-card';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getBadgeBarClass(variant) {
|
|
39
|
+
switch (variant) {
|
|
40
|
+
case 'success':
|
|
41
|
+
return 'bg-success';
|
|
42
|
+
case 'warning':
|
|
43
|
+
return 'bg-warning';
|
|
44
|
+
case 'destructive':
|
|
45
|
+
return 'bg-destructive';
|
|
46
|
+
case 'info':
|
|
47
|
+
return 'bg-info';
|
|
48
|
+
default:
|
|
49
|
+
return 'bg-primary';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<Teleport to="body">
|
|
56
|
+
<div
|
|
57
|
+
:class="['fixed z-[60] flex flex-col gap-2.5 w-full max-w-sm pointer-events-none px-4 sm:px-0', getPositionClasses(position)]"
|
|
58
|
+
aria-live="polite"
|
|
59
|
+
>
|
|
60
|
+
<TransitionGroup
|
|
61
|
+
enter-active-class="transition duration-200 ease-out"
|
|
62
|
+
enter-from-class="opacity-0 translate-y-2 scale-95"
|
|
63
|
+
enter-to-class="opacity-100 translate-y-0 scale-100"
|
|
64
|
+
leave-active-class="transition duration-150 ease-in"
|
|
65
|
+
leave-from-class="opacity-100 scale-100"
|
|
66
|
+
leave-to-class="opacity-0 translate-x-4 scale-95"
|
|
67
|
+
>
|
|
68
|
+
<div
|
|
69
|
+
v-for="item in toasts"
|
|
70
|
+
:key="item.id"
|
|
71
|
+
:class="[
|
|
72
|
+
'pointer-events-auto relative overflow-hidden rounded-birus-lg border p-4 shadow-xl shadow-black/25 text-card-fg flex items-start gap-3',
|
|
73
|
+
getAccentClasses(item.variant),
|
|
74
|
+
]"
|
|
75
|
+
>
|
|
76
|
+
<span :class="['w-1 self-stretch rounded-full shrink-0', getBadgeBarClass(item.variant)]" />
|
|
77
|
+
|
|
78
|
+
<div class="flex-1 min-w-0 space-y-1">
|
|
79
|
+
<div class="text-sm font-semibold leading-tight text-fg">{{ item.title }}</div>
|
|
80
|
+
<p v-if="item.description" class="text-xs text-muted-fg leading-relaxed">
|
|
81
|
+
{{ item.description }}
|
|
82
|
+
</p>
|
|
83
|
+
<button
|
|
84
|
+
v-if="item.action"
|
|
85
|
+
type="button"
|
|
86
|
+
class="mt-1.5 inline-flex text-xs font-semibold text-primary hover:underline"
|
|
87
|
+
@click="() => { item.action?.onClick(); dismiss(item.id); }"
|
|
88
|
+
>
|
|
89
|
+
{{ item.action.label }}
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
class="shrink-0 rounded-birus-sm p-1 text-muted-fg hover:text-fg hover:bg-surface-hover transition-colors"
|
|
96
|
+
aria-label="Fechar notificação"
|
|
97
|
+
@click="dismiss(item.id)"
|
|
98
|
+
>
|
|
99
|
+
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
100
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M18 6L6 18M6 6l12 12" />
|
|
101
|
+
</svg>
|
|
102
|
+
</button>
|
|
103
|
+
|
|
104
|
+
<div
|
|
105
|
+
v-if="item.duration > 0"
|
|
106
|
+
:class="['absolute bottom-0 left-0 h-0.5 opacity-60 birus-toast-progress', getBadgeBarClass(item.variant)]"
|
|
107
|
+
:style="{ animationDuration: `${item.duration}ms` }"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</TransitionGroup>
|
|
111
|
+
</div>
|
|
112
|
+
</Teleport>
|
|
113
|
+
</template>
|
|
114
|
+
|
|
115
|
+
<style scoped>
|
|
116
|
+
@keyframes birus-toast-shrink {
|
|
117
|
+
from {
|
|
118
|
+
width: 100%;
|
|
119
|
+
}
|
|
120
|
+
to {
|
|
121
|
+
width: 0%;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.birus-toast-progress {
|
|
126
|
+
animation-name: birus-toast-shrink;
|
|
127
|
+
animation-timing-function: linear;
|
|
128
|
+
animation-fill-mode: forwards;
|
|
129
|
+
}
|
|
130
|
+
</style>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onBeforeUnmount, ref } from 'vue';
|
|
3
|
+
import { tooltipContentVariants, cn } from '@birus-ui/core';
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
content: { type: String, required: true },
|
|
7
|
+
position: { type: String, default: 'top' },
|
|
8
|
+
delay: { type: Number, default: 150 },
|
|
9
|
+
disabled: { type: Boolean, default: false },
|
|
10
|
+
class: { type: String, default: '' },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const visible = ref(false);
|
|
14
|
+
let timer = null;
|
|
15
|
+
|
|
16
|
+
const tooltipClasses = computed(() =>
|
|
17
|
+
cn(tooltipContentVariants({ position: props.position }), props.class),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
function show() {
|
|
21
|
+
if (props.disabled) return;
|
|
22
|
+
if (timer) clearTimeout(timer);
|
|
23
|
+
timer = setTimeout(() => {
|
|
24
|
+
visible.value = true;
|
|
25
|
+
}, props.delay);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hide() {
|
|
29
|
+
if (timer) {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
timer = null;
|
|
32
|
+
}
|
|
33
|
+
visible.value = false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onBeforeUnmount(() => {
|
|
37
|
+
if (timer) clearTimeout(timer);
|
|
38
|
+
});
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<div
|
|
43
|
+
class="relative inline-flex"
|
|
44
|
+
@mouseenter="show"
|
|
45
|
+
@mouseleave="hide"
|
|
46
|
+
@focusin="show"
|
|
47
|
+
@focusout="hide"
|
|
48
|
+
>
|
|
49
|
+
<slot />
|
|
50
|
+
<Transition
|
|
51
|
+
enter-active-class="transition duration-150 ease-out"
|
|
52
|
+
enter-from-class="opacity-0 scale-95"
|
|
53
|
+
enter-to-class="opacity-100 scale-100"
|
|
54
|
+
leave-active-class="transition duration-100 ease-in"
|
|
55
|
+
leave-from-class="opacity-100 scale-100"
|
|
56
|
+
leave-to-class="opacity-0 scale-95"
|
|
57
|
+
>
|
|
58
|
+
<div v-if="visible && content" role="tooltip" :class="['absolute', tooltipClasses]">
|
|
59
|
+
{{ content }}
|
|
60
|
+
</div>
|
|
61
|
+
</Transition>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { reactive, computed, onMounted, watch } from 'vue';
|
|
2
|
+
import {
|
|
3
|
+
applyBirusTheme,
|
|
4
|
+
DEFAULT_BIRUS_THEME,
|
|
5
|
+
generateThemeCss,
|
|
6
|
+
BIRUS_PALETTES,
|
|
7
|
+
BIRUS_RADIUS_MAP,
|
|
8
|
+
BIRUS_FONTS,
|
|
9
|
+
BIRUS_TYPE_SCALE,
|
|
10
|
+
} from '@birus-ui/core';
|
|
11
|
+
|
|
12
|
+
const STORAGE_KEY = 'birus-ui-theme-config';
|
|
13
|
+
|
|
14
|
+
const themeState = reactive({ ...DEFAULT_BIRUS_THEME });
|
|
15
|
+
let initialized = false;
|
|
16
|
+
|
|
17
|
+
function loadStoredTheme() {
|
|
18
|
+
if (typeof window === 'undefined') return null;
|
|
19
|
+
try {
|
|
20
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
21
|
+
return raw ? JSON.parse(raw) : null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function persistTheme(config) {
|
|
28
|
+
if (typeof window === 'undefined') return;
|
|
29
|
+
try {
|
|
30
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
|
|
31
|
+
} catch {
|
|
32
|
+
// Ignorar restrições de storage
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function initBirusTheme(initial) {
|
|
37
|
+
const stored = loadStoredTheme();
|
|
38
|
+
if (stored?.palette && stored.palette in BIRUS_PALETTES) {
|
|
39
|
+
themeState.palette = stored.palette;
|
|
40
|
+
} else if (initial?.palette) {
|
|
41
|
+
themeState.palette = initial.palette;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (stored?.mode === 'light' || stored?.mode === 'dark') {
|
|
45
|
+
themeState.mode = stored.mode;
|
|
46
|
+
} else if (initial?.mode) {
|
|
47
|
+
themeState.mode = initial.mode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (stored?.radius && stored.radius in BIRUS_RADIUS_MAP) {
|
|
51
|
+
themeState.radius = stored.radius;
|
|
52
|
+
} else if (initial?.radius) {
|
|
53
|
+
themeState.radius = initial.radius;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (stored?.font && stored.font in BIRUS_FONTS) {
|
|
57
|
+
themeState.font = stored.font;
|
|
58
|
+
} else if (initial?.font) {
|
|
59
|
+
themeState.font = initial.font;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (stored?.tokens && typeof stored.tokens === 'object') {
|
|
63
|
+
themeState.tokens = { ...stored.tokens, ...(initial?.tokens || {}) };
|
|
64
|
+
} else if (initial?.tokens && typeof initial.tokens === 'object') {
|
|
65
|
+
themeState.tokens = { ...initial.tokens };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
applyBirusTheme(themeState);
|
|
69
|
+
initialized = true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useBirusTheme() {
|
|
73
|
+
onMounted(() => {
|
|
74
|
+
if (!initialized) {
|
|
75
|
+
initBirusTheme();
|
|
76
|
+
} else {
|
|
77
|
+
applyBirusTheme(themeState);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
watch(
|
|
82
|
+
() => ({ ...themeState }),
|
|
83
|
+
(next) => {
|
|
84
|
+
applyBirusTheme(next);
|
|
85
|
+
persistTheme(next);
|
|
86
|
+
},
|
|
87
|
+
{ deep: true },
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const setPalette = (palette) => {
|
|
91
|
+
themeState.palette = palette;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const setMode = (mode) => {
|
|
95
|
+
themeState.mode = mode;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const toggleMode = () => {
|
|
99
|
+
themeState.mode = themeState.mode === 'dark' ? 'light' : 'dark';
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const setRadius = (radius) => {
|
|
103
|
+
themeState.radius = radius;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const setFont = (font) => {
|
|
107
|
+
themeState.font = font;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const setCustomTokens = (customTokens = {}) => {
|
|
111
|
+
themeState.tokens = { ...(themeState.tokens || {}), ...customTokens };
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const clearCustomTokens = () => {
|
|
115
|
+
themeState.tokens = {};
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const resetTheme = () => {
|
|
119
|
+
themeState.palette = DEFAULT_BIRUS_THEME.palette;
|
|
120
|
+
themeState.mode = DEFAULT_BIRUS_THEME.mode;
|
|
121
|
+
themeState.radius = DEFAULT_BIRUS_THEME.radius;
|
|
122
|
+
themeState.font = DEFAULT_BIRUS_THEME.font;
|
|
123
|
+
themeState.tokens = {};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const themeCss = computed(() => generateThemeCss(themeState));
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
theme: themeState,
|
|
130
|
+
palettes: Object.values(BIRUS_PALETTES),
|
|
131
|
+
radii: Object.values(BIRUS_RADIUS_MAP),
|
|
132
|
+
fonts: Object.values(BIRUS_FONTS),
|
|
133
|
+
typeScale: BIRUS_TYPE_SCALE,
|
|
134
|
+
themeCss,
|
|
135
|
+
setPalette,
|
|
136
|
+
setMode,
|
|
137
|
+
toggleMode,
|
|
138
|
+
setRadius,
|
|
139
|
+
setFont,
|
|
140
|
+
setCustomTokens,
|
|
141
|
+
clearCustomTokens,
|
|
142
|
+
resetTheme,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ref, readonly } from 'vue';
|
|
2
|
+
|
|
3
|
+
const toasts = ref([]);
|
|
4
|
+
const timers = new Map();
|
|
5
|
+
|
|
6
|
+
let idCounter = 0;
|
|
7
|
+
|
|
8
|
+
function dismissToast(id) {
|
|
9
|
+
const timer = timers.get(id);
|
|
10
|
+
if (timer) {
|
|
11
|
+
clearTimeout(timer);
|
|
12
|
+
timers.delete(id);
|
|
13
|
+
}
|
|
14
|
+
toasts.value = toasts.value.filter((item) => item.id !== id);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function addToast(options) {
|
|
18
|
+
const id = `birus-toast-${++idCounter}-${Date.now()}`;
|
|
19
|
+
const duration = options.duration ?? 4500;
|
|
20
|
+
const item = {
|
|
21
|
+
id,
|
|
22
|
+
title: options.title,
|
|
23
|
+
description: options.description,
|
|
24
|
+
variant: options.variant ?? 'default',
|
|
25
|
+
duration,
|
|
26
|
+
action: options.action,
|
|
27
|
+
createdAt: Date.now(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
toasts.value = [item, ...toasts.value].slice(0, 5);
|
|
31
|
+
|
|
32
|
+
if (duration > 0) {
|
|
33
|
+
const timer = setTimeout(() => {
|
|
34
|
+
dismissToast(id);
|
|
35
|
+
}, duration);
|
|
36
|
+
timers.set(id, timer);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function clearToasts() {
|
|
43
|
+
timers.forEach((t) => clearTimeout(t));
|
|
44
|
+
timers.clear();
|
|
45
|
+
toasts.value = [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function useToast() {
|
|
49
|
+
return {
|
|
50
|
+
toasts: readonly(toasts),
|
|
51
|
+
toast: addToast,
|
|
52
|
+
dismiss: dismissToast,
|
|
53
|
+
clear: clearToasts,
|
|
54
|
+
success: (title, description) =>
|
|
55
|
+
addToast({ title, description, variant: 'success' }),
|
|
56
|
+
error: (title, description) =>
|
|
57
|
+
addToast({ title, description, variant: 'destructive' }),
|
|
58
|
+
warning: (title, description) =>
|
|
59
|
+
addToast({ title, description, variant: 'warning' }),
|
|
60
|
+
info: (title, description) =>
|
|
61
|
+
addToast({ title, description, variant: 'info' }),
|
|
62
|
+
};
|
|
63
|
+
}
|