@datametria/vue-components 1.1.1

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/ACCESSIBILITY.md +78 -0
  2. package/DESIGN-SYSTEM.md +70 -0
  3. package/LICENSE +21 -0
  4. package/PROGRESS.md +327 -0
  5. package/README.md +473 -0
  6. package/dist/index.es.js +1405 -0
  7. package/dist/index.umd.js +1 -0
  8. package/dist/vue-components.css +1 -0
  9. package/package.json +98 -0
  10. package/src/components/DatametriaAlert.vue +123 -0
  11. package/src/components/DatametriaAutocomplete.vue +292 -0
  12. package/src/components/DatametriaAvatar.vue +99 -0
  13. package/src/components/DatametriaBadge.vue +90 -0
  14. package/src/components/DatametriaBreadcrumb.vue +144 -0
  15. package/src/components/DatametriaButton.vue +157 -0
  16. package/src/components/DatametriaCard.vue +72 -0
  17. package/src/components/DatametriaCheckbox.vue +82 -0
  18. package/src/components/DatametriaChip.vue +149 -0
  19. package/src/components/DatametriaContainer.vue +57 -0
  20. package/src/components/DatametriaDatePicker.vue +140 -0
  21. package/src/components/DatametriaDivider.vue +100 -0
  22. package/src/components/DatametriaFileUpload.vue +268 -0
  23. package/src/components/DatametriaGrid.vue +44 -0
  24. package/src/components/DatametriaInput.vue +102 -0
  25. package/src/components/DatametriaModal.vue +135 -0
  26. package/src/components/DatametriaNavbar.vue +227 -0
  27. package/src/components/DatametriaProgress.vue +113 -0
  28. package/src/components/DatametriaRadio.vue +138 -0
  29. package/src/components/DatametriaSelect.vue +112 -0
  30. package/src/components/DatametriaSpinner.vue +112 -0
  31. package/src/components/DatametriaSwitch.vue +137 -0
  32. package/src/components/DatametriaTable.vue +105 -0
  33. package/src/components/DatametriaTabs.vue +180 -0
  34. package/src/components/DatametriaTextarea.vue +159 -0
  35. package/src/components/DatametriaToast.vue +163 -0
  36. package/src/composables/useAPI.ts +78 -0
  37. package/src/composables/useClipboard.ts +42 -0
  38. package/src/composables/useDebounce.ts +16 -0
  39. package/src/composables/useLocalStorage.ts +26 -0
  40. package/src/composables/useTheme.ts +66 -0
  41. package/src/composables/useValidation.ts +39 -0
  42. package/src/index.ts +52 -0
  43. package/src/styles/design-tokens.css +31 -0
  44. package/src/types/index.ts +34 -0
@@ -0,0 +1,163 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <Transition name="dm-toast">
4
+ <div
5
+ v-if="isVisible"
6
+ class="dm-toast"
7
+ :class="`dm-toast--${variant}`"
8
+ role="alert"
9
+ :aria-live="variant === 'error' ? 'assertive' : 'polite'"
10
+ >
11
+ <div class="dm-toast__content">
12
+ <span class="dm-toast__message">{{ message }}</span>
13
+ <button
14
+ v-if="closable"
15
+ class="dm-toast__close"
16
+ @click="close"
17
+ aria-label="Fechar"
18
+ >×</button>
19
+ </div>
20
+ </div>
21
+ </Transition>
22
+ </Teleport>
23
+ </template>
24
+
25
+ <script setup lang="ts">
26
+ import { ref, watch, onMounted } from 'vue'
27
+
28
+ interface Props {
29
+ message: string
30
+ variant?: 'success' | 'error' | 'warning' | 'info'
31
+ duration?: number
32
+ closable?: boolean
33
+ modelValue?: boolean
34
+ }
35
+
36
+ const props = withDefaults(defineProps<Props>(), {
37
+ variant: 'info',
38
+ duration: 3000,
39
+ closable: true,
40
+ modelValue: false
41
+ })
42
+
43
+ const emit = defineEmits<{
44
+ 'update:modelValue': [value: boolean]
45
+ close: []
46
+ }>()
47
+
48
+ const isVisible = ref(props.modelValue)
49
+ let timer: ReturnType<typeof setTimeout> | null = null
50
+
51
+ watch(() => props.modelValue, (newValue) => {
52
+ isVisible.value = newValue
53
+ if (newValue && props.duration > 0) {
54
+ startTimer()
55
+ }
56
+ })
57
+
58
+ const startTimer = () => {
59
+ if (timer) clearTimeout(timer)
60
+ timer = setTimeout(() => {
61
+ close()
62
+ }, props.duration)
63
+ }
64
+
65
+ const close = () => {
66
+ isVisible.value = false
67
+ emit('update:modelValue', false)
68
+ emit('close')
69
+ if (timer) clearTimeout(timer)
70
+ }
71
+
72
+ onMounted(() => {
73
+ if (isVisible.value && props.duration > 0) {
74
+ startTimer()
75
+ }
76
+ })
77
+ </script>
78
+
79
+ <style scoped>
80
+ .dm-toast {
81
+ position: fixed;
82
+ top: var(--dm-space-4);
83
+ right: var(--dm-space-4);
84
+ min-width: 300px;
85
+ max-width: 500px;
86
+ padding: var(--dm-space-4);
87
+ border-radius: var(--dm-radius);
88
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
89
+ z-index: 9999;
90
+ }
91
+
92
+ .dm-toast--success {
93
+ background: var(--dm-success);
94
+ color: var(--dm-white);
95
+ }
96
+
97
+ .dm-toast--error {
98
+ background: var(--dm-error);
99
+ color: var(--dm-white);
100
+ }
101
+
102
+ .dm-toast--warning {
103
+ background: var(--dm-warning);
104
+ color: var(--dm-gray-900);
105
+ }
106
+
107
+ .dm-toast--info {
108
+ background: var(--dm-primary);
109
+ color: var(--dm-white);
110
+ }
111
+
112
+ .dm-toast__content {
113
+ display: flex;
114
+ align-items: center;
115
+ gap: var(--dm-space-3);
116
+ }
117
+
118
+ .dm-toast__message {
119
+ flex: 1;
120
+ font-size: var(--dm-text-sm);
121
+ line-height: 1.5;
122
+ }
123
+
124
+ .dm-toast__close {
125
+ width: 24px;
126
+ height: 24px;
127
+ border: none;
128
+ background: transparent;
129
+ color: inherit;
130
+ font-size: 24px;
131
+ line-height: 1;
132
+ cursor: pointer;
133
+ opacity: 0.8;
134
+ transition: var(--dm-transition);
135
+ }
136
+
137
+ .dm-toast__close:hover {
138
+ opacity: 1;
139
+ }
140
+
141
+ .dm-toast-enter-active,
142
+ .dm-toast-leave-active {
143
+ transition: all 0.3s ease;
144
+ }
145
+
146
+ .dm-toast-enter-from {
147
+ opacity: 0;
148
+ transform: translateX(100%);
149
+ }
150
+
151
+ .dm-toast-leave-to {
152
+ opacity: 0;
153
+ transform: translateY(-20px);
154
+ }
155
+
156
+ @media (max-width: 640px) {
157
+ .dm-toast {
158
+ left: var(--dm-space-4);
159
+ right: var(--dm-space-4);
160
+ min-width: auto;
161
+ }
162
+ }
163
+ </style>
@@ -0,0 +1,78 @@
1
+ import { ref } from 'vue'
2
+
3
+ export interface ApiConfig {
4
+ baseURL?: string
5
+ timeout?: number
6
+ headers?: Record<string, string>
7
+ }
8
+
9
+ export function useAPI(config: ApiConfig = {}) {
10
+ const loading = ref(false)
11
+ const error = ref<string | null>(null)
12
+ const data = ref<any>(null)
13
+
14
+ const request = async (url: string, options: RequestInit = {}) => {
15
+ loading.value = true
16
+ error.value = null
17
+
18
+ try {
19
+ const fullUrl = config.baseURL ? `${config.baseURL}${url}` : url
20
+
21
+ const response = await fetch(fullUrl, {
22
+ ...options,
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ ...config.headers,
26
+ ...options.headers
27
+ }
28
+ })
29
+
30
+ if (!response.ok) {
31
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
32
+ }
33
+
34
+ const result = await response.json()
35
+ data.value = result
36
+ return result
37
+ } catch (err) {
38
+ error.value = (err as Error).message || 'Request failed'
39
+ throw err
40
+ } finally {
41
+ loading.value = false
42
+ }
43
+ }
44
+
45
+ const get = (url: string, options?: RequestInit) => {
46
+ return request(url, { ...options, method: 'GET' })
47
+ }
48
+
49
+ const post = (url: string, body: any, options?: RequestInit) => {
50
+ return request(url, {
51
+ ...options,
52
+ method: 'POST',
53
+ body: JSON.stringify(body)
54
+ })
55
+ }
56
+
57
+ const put = (url: string, body: any, options?: RequestInit) => {
58
+ return request(url, {
59
+ ...options,
60
+ method: 'PUT',
61
+ body: JSON.stringify(body)
62
+ })
63
+ }
64
+
65
+ const del = (url: string, options?: RequestInit) => {
66
+ return request(url, { ...options, method: 'DELETE' })
67
+ }
68
+
69
+ return {
70
+ loading,
71
+ error,
72
+ data,
73
+ get,
74
+ post,
75
+ put,
76
+ delete: del
77
+ }
78
+ }
@@ -0,0 +1,42 @@
1
+ import { ref } from 'vue'
2
+
3
+ export function useClipboard() {
4
+ const copied = ref(false)
5
+ const error = ref<string | null>(null)
6
+
7
+ const copy = async (text: string): Promise<boolean> => {
8
+ try {
9
+ await navigator.clipboard.writeText(text)
10
+ copied.value = true
11
+ error.value = null
12
+
13
+ setTimeout(() => {
14
+ copied.value = false
15
+ }, 2000)
16
+
17
+ return true
18
+ } catch (err) {
19
+ error.value = (err as Error).message || 'Copy failed'
20
+ copied.value = false
21
+ return false
22
+ }
23
+ }
24
+
25
+ const read = async (): Promise<string> => {
26
+ try {
27
+ const text = await navigator.clipboard.readText()
28
+ error.value = null
29
+ return text
30
+ } catch (err) {
31
+ error.value = (err as Error).message || 'Read failed'
32
+ return ''
33
+ }
34
+ }
35
+
36
+ return {
37
+ copied,
38
+ error,
39
+ copy,
40
+ read
41
+ }
42
+ }
@@ -0,0 +1,16 @@
1
+ import { ref, watch, type Ref } from 'vue'
2
+
3
+ export function useDebounce<T>(value: Ref<T>, delay: number = 300): Ref<T> {
4
+ const debouncedValue = ref<T>(value.value) as Ref<T>
5
+ let timeout: ReturnType<typeof setTimeout> | null = null
6
+
7
+ watch(value, (newValue) => {
8
+ if (timeout) clearTimeout(timeout)
9
+
10
+ timeout = setTimeout(() => {
11
+ debouncedValue.value = newValue
12
+ }, delay)
13
+ })
14
+
15
+ return debouncedValue
16
+ }
@@ -0,0 +1,26 @@
1
+ import { ref, watch, type Ref } from 'vue'
2
+
3
+ export function useLocalStorage<T>(key: string, defaultValue: T): Ref<T> {
4
+ const storedValue = localStorage.getItem(key)
5
+ let parsedValue = defaultValue
6
+
7
+ if (storedValue) {
8
+ try {
9
+ parsedValue = JSON.parse(storedValue)
10
+ } catch {
11
+ parsedValue = defaultValue
12
+ }
13
+ }
14
+
15
+ const data = ref<T>(parsedValue) as Ref<T>
16
+
17
+ watch(
18
+ data,
19
+ (newValue) => {
20
+ localStorage.setItem(key, JSON.stringify(newValue))
21
+ },
22
+ { deep: true }
23
+ )
24
+
25
+ return data
26
+ }
@@ -0,0 +1,66 @@
1
+ import { ref, computed } from 'vue'
2
+
3
+ let isDark = ref(false)
4
+ let initialized = false
5
+
6
+ // Reset function for testing
7
+ export function resetTheme() {
8
+ isDark = ref(false)
9
+ initialized = false
10
+ document.documentElement.classList.remove('dark')
11
+ }
12
+
13
+ export function useTheme() {
14
+ const toggleTheme = () => {
15
+ isDark.value = !isDark.value
16
+ document.documentElement.classList.toggle('dark', isDark.value)
17
+ localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
18
+ }
19
+
20
+ const setTheme = (theme: 'light' | 'dark') => {
21
+ isDark.value = theme === 'dark'
22
+ document.documentElement.classList.toggle('dark', isDark.value)
23
+ localStorage.setItem('theme', theme)
24
+ }
25
+
26
+ const initTheme = () => {
27
+ initialized = true
28
+
29
+ const savedTheme = localStorage.getItem('theme')
30
+ if (savedTheme) {
31
+ setTheme(savedTheme as 'light' | 'dark')
32
+ } else if (typeof window !== 'undefined' && window.matchMedia) {
33
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
34
+ setTheme(prefersDark ? 'dark' : 'light')
35
+ }
36
+
37
+ // Listen for system preference changes
38
+ if (typeof window !== 'undefined' && window.matchMedia) {
39
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
40
+ if (!localStorage.getItem('theme')) {
41
+ setTheme(e.matches ? 'dark' : 'light')
42
+ }
43
+ })
44
+ }
45
+ }
46
+
47
+ // Auto-initialize on first use
48
+ if (!initialized) {
49
+ initTheme()
50
+ }
51
+
52
+ const currentTheme = computed(() => isDark.value ? 'dark' : 'light')
53
+
54
+ const setDark = () => setTheme('dark')
55
+ const setLight = () => setTheme('light')
56
+
57
+ return {
58
+ isDark,
59
+ currentTheme,
60
+ toggle: toggleTheme,
61
+ setTheme,
62
+ setDark,
63
+ setLight,
64
+ initTheme
65
+ }
66
+ }
@@ -0,0 +1,39 @@
1
+ // Simple validators that return boolean
2
+ export const required = (value: any): boolean => {
3
+ return !!value
4
+ }
5
+
6
+ export const email = (value: any): boolean => {
7
+ if (!value) return true
8
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
9
+ }
10
+
11
+ export const minLength = (min: number) => (value: any): boolean => {
12
+ if (!value) return true
13
+ return value.length >= min
14
+ }
15
+
16
+ export const maxLength = (max: number) => (value: any): boolean => {
17
+ if (!value) return true
18
+ return value.length <= max
19
+ }
20
+
21
+ export const pattern = (regex: RegExp) => (value: any): boolean => {
22
+ if (!value) return true
23
+ return regex.test(value)
24
+ }
25
+
26
+ export const custom = (validator: (value: any) => boolean) => (value: any): boolean => {
27
+ return validator(value)
28
+ }
29
+
30
+ export function useValidation() {
31
+ return {
32
+ required,
33
+ email,
34
+ minLength,
35
+ maxLength,
36
+ pattern,
37
+ custom
38
+ }
39
+ }
package/src/index.ts ADDED
@@ -0,0 +1,52 @@
1
+ // Components - Form
2
+ export { default as DatametriaButton } from './components/DatametriaButton.vue'
3
+ export { default as DatametriaInput } from './components/DatametriaInput.vue'
4
+ export { default as DatametriaSelect } from './components/DatametriaSelect.vue'
5
+ export { default as DatametriaCheckbox } from './components/DatametriaCheckbox.vue'
6
+ export { default as DatametriaRadio } from './components/DatametriaRadio.vue'
7
+ export { default as DatametriaSwitch } from './components/DatametriaSwitch.vue'
8
+ export { default as DatametriaTextarea } from './components/DatametriaTextarea.vue'
9
+ export { default as DatametriaDatePicker } from './components/DatametriaDatePicker.vue'
10
+ export { default as DatametriaFileUpload } from './components/DatametriaFileUpload.vue'
11
+ export { default as DatametriaAutocomplete } from './components/DatametriaAutocomplete.vue'
12
+
13
+ // Components - Layout
14
+ export { default as DatametriaCard } from './components/DatametriaCard.vue'
15
+ export { default as DatametriaModal } from './components/DatametriaModal.vue'
16
+ export { default as DatametriaContainer } from './components/DatametriaContainer.vue'
17
+ export { default as DatametriaGrid } from './components/DatametriaGrid.vue'
18
+ export { default as DatametriaDivider } from './components/DatametriaDivider.vue'
19
+
20
+ // Components - Feedback
21
+ export { default as DatametriaAlert } from './components/DatametriaAlert.vue'
22
+ export { default as DatametriaToast } from './components/DatametriaToast.vue'
23
+ export { default as DatametriaProgress } from './components/DatametriaProgress.vue'
24
+ export { default as DatametriaSpinner } from './components/DatametriaSpinner.vue'
25
+
26
+ // Components - Data Display
27
+ export { default as DatametriaTable } from './components/DatametriaTable.vue'
28
+ export { default as DatametriaAvatar } from './components/DatametriaAvatar.vue'
29
+ export { default as DatametriaBadge } from './components/DatametriaBadge.vue'
30
+ export { default as DatametriaChip } from './components/DatametriaChip.vue'
31
+
32
+ // Components - Navigation
33
+ export { default as DatametriaNavbar } from './components/DatametriaNavbar.vue'
34
+ export { default as DatametriaBreadcrumb } from './components/DatametriaBreadcrumb.vue'
35
+ export { default as DatametriaTabs } from './components/DatametriaTabs.vue'
36
+
37
+ // Composables - Core
38
+ export { useTheme } from './composables/useTheme'
39
+ export { useValidation, required, email, minLength, maxLength, pattern, custom } from './composables/useValidation'
40
+ export { useAPI } from './composables/useAPI'
41
+
42
+ // Composables - Utility
43
+ export { useLocalStorage } from './composables/useLocalStorage'
44
+ export { useDebounce } from './composables/useDebounce'
45
+ export { useClipboard } from './composables/useClipboard'
46
+
47
+ // Types
48
+ export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'outline'
49
+ export type ButtonSize = 'sm' | 'md' | 'lg'
50
+ export type AlertVariant = 'info' | 'success' | 'warning' | 'error'
51
+ export type BadgeVariant = 'default' | 'primary' | 'success' | 'warning' | 'error' | 'info'
52
+ export type BadgeSize = 'sm' | 'md' | 'lg'
@@ -0,0 +1,31 @@
1
+ :root {
2
+ --dm-primary: #0072CE;
3
+ --dm-secondary: #4B0078;
4
+ --dm-success: #10b981;
5
+ --dm-error: #ef4444;
6
+ --dm-warning: #f59e0b;
7
+
8
+ --dm-gray-50: #f9fafb;
9
+ --dm-gray-100: #f3f4f6;
10
+ --dm-gray-700: #374151;
11
+ --dm-gray-900: #111827;
12
+
13
+ --dm-space-2: 0.5rem;
14
+ --dm-space-3: 0.75rem;
15
+ --dm-space-4: 1rem;
16
+
17
+ --dm-text-sm: 0.875rem;
18
+ --dm-text-base: 1rem;
19
+ --dm-text-lg: 1.125rem;
20
+
21
+ --dm-radius: 0.375rem;
22
+ --dm-transition: 200ms ease-in-out;
23
+ --dm-focus-ring: 0 0 0 3px rgba(0, 114, 206, 0.1);
24
+ }
25
+
26
+ @media (max-width: 640px) {
27
+ :root {
28
+ --dm-text-sm: 0.8rem;
29
+ --dm-text-base: 0.9rem;
30
+ }
31
+ }
@@ -0,0 +1,34 @@
1
+ export enum ButtonVariant {
2
+ PRIMARY = 'primary',
3
+ SECONDARY = 'secondary',
4
+ OUTLINE = 'outline',
5
+ GHOST = 'ghost'
6
+ }
7
+
8
+ export enum ButtonSize {
9
+ SM = 'sm',
10
+ MD = 'md',
11
+ LG = 'lg'
12
+ }
13
+
14
+ export interface ButtonProps {
15
+ variant?: ButtonVariant
16
+ size?: ButtonSize
17
+ disabled?: boolean
18
+ loading?: boolean
19
+ fullWidth?: boolean
20
+ }
21
+
22
+ export interface InputProps {
23
+ modelValue?: string | number
24
+ label?: string
25
+ placeholder?: string
26
+ errorMessage?: string
27
+ disabled?: boolean
28
+ required?: boolean
29
+ }
30
+
31
+ export interface CardProps {
32
+ title?: string
33
+ padding?: boolean
34
+ }