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,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
+ }