@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,227 @@
1
+ <template>
2
+ <nav
3
+ class="dm-navbar"
4
+ :class="[
5
+ `dm-navbar--${props.variant}`,
6
+ { 'dm-navbar--sticky': props.sticky },
7
+ { 'dm-navbar--transparent': props.transparent },
8
+ { 'dm-navbar--bordered': props.bordered }
9
+ ]"
10
+ role="navigation"
11
+ :aria-label="props.ariaLabel"
12
+ >
13
+ <div class="dm-navbar__container">
14
+ <div class="dm-navbar__brand">
15
+ <slot name="brand">
16
+ <a href="/" class="dm-navbar__logo">{{ brand }}</a>
17
+ </slot>
18
+ </div>
19
+
20
+ <button
21
+ v-if="$slots.menu"
22
+ class="dm-navbar__toggle"
23
+ :aria-expanded="isMenuOpen"
24
+ aria-controls="navbar-menu"
25
+ @click="toggleMenu"
26
+ >
27
+ <span class="dm-navbar__toggle-icon"></span>
28
+ </button>
29
+
30
+ <div
31
+ id="navbar-menu"
32
+ class="dm-navbar__menu"
33
+ :class="{ 'dm-navbar__menu--open': isMenuOpen }"
34
+ >
35
+ <slot name="menu"></slot>
36
+ </div>
37
+
38
+ <div v-if="$slots.actions" class="dm-navbar__actions">
39
+ <slot name="actions"></slot>
40
+ </div>
41
+ </div>
42
+ </nav>
43
+ </template>
44
+
45
+ <script setup lang="ts">
46
+ import { ref } from 'vue'
47
+
48
+ interface Props {
49
+ brand?: string
50
+ variant?: 'light' | 'dark' | 'transparent'
51
+ sticky?: boolean
52
+ transparent?: boolean
53
+ bordered?: boolean
54
+ ariaLabel?: string
55
+ }
56
+
57
+ const props = withDefaults(defineProps<Props>(), {
58
+ brand: '',
59
+ variant: 'light',
60
+ sticky: false,
61
+ transparent: false,
62
+ bordered: false,
63
+ ariaLabel: 'Main navigation'
64
+ })
65
+
66
+ const isMenuOpen = ref(false)
67
+
68
+ const toggleMenu = () => {
69
+ isMenuOpen.value = !isMenuOpen.value
70
+ }
71
+ </script>
72
+
73
+ <style scoped>
74
+ .dm-navbar {
75
+ width: 100%;
76
+ background: var(--dm-white);
77
+ border-bottom: 1px solid var(--dm-gray-200);
78
+ position: sticky;
79
+ top: 0;
80
+ z-index: 100;
81
+ }
82
+
83
+ .dm-navbar--dark {
84
+ background: var(--dm-gray-900);
85
+ border-bottom-color: var(--dm-gray-800);
86
+ }
87
+
88
+ .dm-navbar--transparent {
89
+ background: transparent;
90
+ border-bottom: none;
91
+ }
92
+
93
+ .dm-navbar__container {
94
+ max-width: 1280px;
95
+ margin: 0 auto;
96
+ padding: var(--dm-space-4);
97
+ display: flex;
98
+ align-items: center;
99
+ gap: var(--dm-space-4);
100
+ }
101
+
102
+ .dm-navbar__brand {
103
+ flex-shrink: 0;
104
+ }
105
+
106
+ .dm-navbar__logo {
107
+ font-size: var(--dm-text-lg);
108
+ font-weight: 600;
109
+ color: var(--dm-text-primary);
110
+ text-decoration: none;
111
+ }
112
+
113
+ .dm-navbar--dark .dm-navbar__logo {
114
+ color: var(--dm-white);
115
+ }
116
+
117
+ .dm-navbar__toggle {
118
+ display: none;
119
+ width: 44px;
120
+ height: 44px;
121
+ padding: var(--dm-space-2);
122
+ border: none;
123
+ background: transparent;
124
+ cursor: pointer;
125
+ margin-left: auto;
126
+ }
127
+
128
+ .dm-navbar__toggle-icon {
129
+ display: block;
130
+ width: 24px;
131
+ height: 2px;
132
+ background: var(--dm-text-primary);
133
+ position: relative;
134
+ transition: var(--dm-transition);
135
+ }
136
+
137
+ .dm-navbar__toggle-icon::before,
138
+ .dm-navbar__toggle-icon::after {
139
+ content: '';
140
+ position: absolute;
141
+ width: 24px;
142
+ height: 2px;
143
+ background: var(--dm-text-primary);
144
+ transition: var(--dm-transition);
145
+ }
146
+
147
+ .dm-navbar__toggle-icon::before {
148
+ top: -8px;
149
+ }
150
+
151
+ .dm-navbar__toggle-icon::after {
152
+ bottom: -8px;
153
+ }
154
+
155
+ .dm-navbar__menu {
156
+ display: flex;
157
+ align-items: center;
158
+ gap: var(--dm-space-4);
159
+ flex: 1;
160
+ }
161
+
162
+ .dm-navbar__actions {
163
+ display: flex;
164
+ align-items: center;
165
+ gap: var(--dm-space-2);
166
+ margin-left: auto;
167
+ }
168
+
169
+ @media (max-width: 768px) {
170
+ .dm-navbar__toggle {
171
+ display: flex;
172
+ align-items: center;
173
+ justify-content: center;
174
+ }
175
+
176
+ .dm-navbar__menu {
177
+ position: absolute;
178
+ top: 100%;
179
+ left: 0;
180
+ right: 0;
181
+ background: var(--dm-white);
182
+ border-bottom: 1px solid var(--dm-gray-200);
183
+ flex-direction: column;
184
+ align-items: stretch;
185
+ padding: var(--dm-space-4);
186
+ display: none;
187
+ }
188
+
189
+ .dm-navbar--dark .dm-navbar__menu {
190
+ background: var(--dm-gray-900);
191
+ border-bottom-color: var(--dm-gray-800);
192
+ }
193
+
194
+ .dm-navbar__menu--open {
195
+ display: flex;
196
+ }
197
+
198
+ .dm-navbar__actions {
199
+ margin-left: 0;
200
+ margin-top: var(--dm-space-4);
201
+ }
202
+ }
203
+
204
+ @media (prefers-color-scheme: dark) {
205
+ .dm-navbar {
206
+ background: var(--dm-gray-900);
207
+ border-bottom-color: var(--dm-gray-800);
208
+ }
209
+
210
+ .dm-navbar__logo {
211
+ color: var(--dm-white);
212
+ }
213
+
214
+ .dm-navbar__toggle-icon,
215
+ .dm-navbar__toggle-icon::before,
216
+ .dm-navbar__toggle-icon::after {
217
+ background: var(--dm-white);
218
+ }
219
+
220
+ @media (max-width: 768px) {
221
+ .dm-navbar__menu {
222
+ background: var(--dm-gray-900);
223
+ border-bottom-color: var(--dm-gray-800);
224
+ }
225
+ }
226
+ }
227
+ </style>
@@ -0,0 +1,113 @@
1
+ <template>
2
+ <div class="dm-progress">
3
+ <div v-if="label" class="dm-progress__header">
4
+ <span class="dm-progress__label">{{ label }}</span>
5
+ <span v-if="showValue" class="dm-progress__value">{{ value }}%</span>
6
+ </div>
7
+ <div
8
+ class="dm-progress__track"
9
+ role="progressbar"
10
+ :aria-valuenow="value"
11
+ :aria-valuemin="0"
12
+ :aria-valuemax="100"
13
+ :aria-label="ariaLabel || label"
14
+ >
15
+ <div
16
+ class="dm-progress__bar"
17
+ :class="`dm-progress__bar--${variant}`"
18
+ :style="{ width: `${clampedValue}%` }"
19
+ ></div>
20
+ </div>
21
+ </div>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import { computed } from 'vue'
26
+
27
+ interface Props {
28
+ value: number
29
+ label?: string
30
+ variant?: 'primary' | 'success' | 'warning' | 'error'
31
+ showValue?: boolean
32
+ ariaLabel?: string
33
+ }
34
+
35
+ const props = withDefaults(defineProps<Props>(), {
36
+ variant: 'primary',
37
+ showValue: true
38
+ })
39
+
40
+ const clampedValue = computed(() => {
41
+ return Math.min(Math.max(props.value, 0), 100)
42
+ })
43
+ </script>
44
+
45
+ <style scoped>
46
+ .dm-progress {
47
+ display: flex;
48
+ flex-direction: column;
49
+ gap: var(--dm-space-2);
50
+ }
51
+
52
+ .dm-progress__header {
53
+ display: flex;
54
+ justify-content: space-between;
55
+ align-items: center;
56
+ }
57
+
58
+ .dm-progress__label {
59
+ font-size: var(--dm-text-sm);
60
+ font-weight: 500;
61
+ color: var(--dm-text-primary);
62
+ }
63
+
64
+ .dm-progress__value {
65
+ font-size: var(--dm-text-sm);
66
+ font-weight: 600;
67
+ color: var(--dm-text-secondary);
68
+ }
69
+
70
+ .dm-progress__track {
71
+ width: 100%;
72
+ height: 8px;
73
+ background: var(--dm-gray-200);
74
+ border-radius: 4px;
75
+ overflow: hidden;
76
+ }
77
+
78
+ .dm-progress__bar {
79
+ height: 100%;
80
+ border-radius: 4px;
81
+ transition: width 0.3s ease;
82
+ }
83
+
84
+ .dm-progress__bar--primary {
85
+ background: var(--dm-primary);
86
+ }
87
+
88
+ .dm-progress__bar--success {
89
+ background: var(--dm-success);
90
+ }
91
+
92
+ .dm-progress__bar--warning {
93
+ background: var(--dm-warning);
94
+ }
95
+
96
+ .dm-progress__bar--error {
97
+ background: var(--dm-error);
98
+ }
99
+
100
+ @media (prefers-color-scheme: dark) {
101
+ .dm-progress__track {
102
+ background: var(--dm-gray-700);
103
+ }
104
+
105
+ .dm-progress__label {
106
+ color: var(--dm-white);
107
+ }
108
+
109
+ .dm-progress__value {
110
+ color: var(--dm-gray-400);
111
+ }
112
+ }
113
+ </style>
@@ -0,0 +1,138 @@
1
+ <template>
2
+ <div class="dm-radio">
3
+ <label class="dm-radio__label">
4
+ <input
5
+ type="radio"
6
+ class="dm-radio__input"
7
+ :value="value"
8
+ :checked="modelValue === value"
9
+ :disabled="disabled"
10
+ :name="name"
11
+ :aria-label="ariaLabel"
12
+ :aria-describedby="error ? `${name}-error` : undefined"
13
+ @change="handleChange"
14
+ />
15
+ <span class="dm-radio__checkmark"></span>
16
+ <span v-if="label" class="dm-radio__text">{{ label }}</span>
17
+ </label>
18
+ <p v-if="error" :id="`${name}-error`" class="dm-radio__error">{{ error }}</p>
19
+ </div>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ interface Props {
24
+ modelValue?: string | number | boolean
25
+ value: string | number | boolean
26
+ label?: string
27
+ name?: string
28
+ disabled?: boolean
29
+ error?: string
30
+ ariaLabel?: string
31
+ }
32
+
33
+ const props = withDefaults(defineProps<Props>(), {
34
+ disabled: false
35
+ })
36
+
37
+ const emit = defineEmits<{
38
+ 'update:modelValue': [value: string | number | boolean]
39
+ }>()
40
+
41
+ const handleChange = () => {
42
+ if (!props.disabled) {
43
+ emit('update:modelValue', props.value)
44
+ }
45
+ }
46
+ </script>
47
+
48
+ <style scoped>
49
+ .dm-radio {
50
+ display: flex;
51
+ flex-direction: column;
52
+ gap: var(--dm-space-1);
53
+ }
54
+
55
+ .dm-radio__label {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ gap: var(--dm-space-2);
59
+ cursor: pointer;
60
+ min-height: 44px;
61
+ padding: var(--dm-space-2);
62
+ user-select: none;
63
+ }
64
+
65
+ .dm-radio__label:has(.dm-radio__input:disabled) {
66
+ cursor: not-allowed;
67
+ opacity: 0.5;
68
+ }
69
+
70
+ .dm-radio__input {
71
+ position: absolute;
72
+ opacity: 0;
73
+ width: 0;
74
+ height: 0;
75
+ }
76
+
77
+ .dm-radio__checkmark {
78
+ position: relative;
79
+ width: 20px;
80
+ height: 20px;
81
+ border: 2px solid var(--dm-gray-400);
82
+ border-radius: 50%;
83
+ background: var(--dm-white);
84
+ transition: var(--dm-transition);
85
+ flex-shrink: 0;
86
+ }
87
+
88
+ .dm-radio__input:checked + .dm-radio__checkmark {
89
+ border-color: var(--dm-primary);
90
+ background: var(--dm-primary);
91
+ }
92
+
93
+ .dm-radio__input:checked + .dm-radio__checkmark::after {
94
+ content: '';
95
+ position: absolute;
96
+ top: 50%;
97
+ left: 50%;
98
+ transform: translate(-50%, -50%);
99
+ width: 8px;
100
+ height: 8px;
101
+ border-radius: 50%;
102
+ background: var(--dm-white);
103
+ }
104
+
105
+ .dm-radio__input:focus-visible + .dm-radio__checkmark {
106
+ outline: var(--dm-focus-ring);
107
+ outline-offset: 2px;
108
+ }
109
+
110
+ .dm-radio__input:disabled + .dm-radio__checkmark {
111
+ background: var(--dm-gray-100);
112
+ border-color: var(--dm-gray-300);
113
+ }
114
+
115
+ .dm-radio__text {
116
+ color: var(--dm-text-primary);
117
+ font-size: var(--dm-text-base);
118
+ line-height: 1.5;
119
+ }
120
+
121
+ .dm-radio__error {
122
+ color: var(--dm-error);
123
+ font-size: var(--dm-text-sm);
124
+ margin: 0;
125
+ }
126
+
127
+ @media (prefers-color-scheme: dark) {
128
+ .dm-radio__checkmark {
129
+ background: var(--dm-gray-800);
130
+ border-color: var(--dm-gray-600);
131
+ }
132
+
133
+ .dm-radio__input:disabled + .dm-radio__checkmark {
134
+ background: var(--dm-gray-900);
135
+ border-color: var(--dm-gray-700);
136
+ }
137
+ }
138
+ </style>
@@ -0,0 +1,112 @@
1
+ <template>
2
+ <div class="datametria-select">
3
+ <label v-if="label" :for="selectId" class="datametria-select__label">
4
+ {{ label }}
5
+ <span v-if="required" class="datametria-select__required">*</span>
6
+ </label>
7
+
8
+ <select
9
+ :id="selectId"
10
+ :value="modelValue"
11
+ :disabled="disabled"
12
+ :required="required"
13
+ :class="selectClasses"
14
+ @change="$emit('update:modelValue', ($event.target as HTMLSelectElement).value)"
15
+ >
16
+ <option v-if="placeholder" value="" disabled>{{ placeholder }}</option>
17
+ <option v-for="option in options" :key="option.value" :value="option.value">
18
+ {{ option.label }}
19
+ </option>
20
+ </select>
21
+
22
+ <p v-if="errorMessage" class="datametria-select__error">{{ errorMessage }}</p>
23
+ </div>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ import { computed } from 'vue'
28
+
29
+ interface Option {
30
+ value: string | number
31
+ label: string
32
+ }
33
+
34
+ interface Props {
35
+ modelValue?: string | number
36
+ options: Option[]
37
+ label?: string
38
+ placeholder?: string
39
+ errorMessage?: string
40
+ disabled?: boolean
41
+ required?: boolean
42
+ }
43
+
44
+ const props = withDefaults(defineProps<Props>(), {
45
+ modelValue: '',
46
+ disabled: false,
47
+ required: false
48
+ })
49
+
50
+ defineEmits<{
51
+ 'update:modelValue': [value: string]
52
+ }>()
53
+
54
+ const selectId = computed(() => `select-${Math.random().toString(36).substr(2, 9)}`)
55
+
56
+ const selectClasses = computed(() => [
57
+ 'datametria-select__field',
58
+ {
59
+ 'datametria-select__field--error': props.errorMessage,
60
+ 'datametria-select__field--disabled': props.disabled
61
+ }
62
+ ])
63
+ </script>
64
+
65
+ <style scoped>
66
+ .datametria-select {
67
+ display: flex;
68
+ flex-direction: column;
69
+ gap: 0.5rem;
70
+ }
71
+
72
+ .datametria-select__label {
73
+ font-size: 0.875rem;
74
+ font-weight: 500;
75
+ color: #374151;
76
+ }
77
+
78
+ .datametria-select__required {
79
+ color: #ef4444;
80
+ }
81
+
82
+ .datametria-select__field {
83
+ padding: 0.75rem;
84
+ border: 1px solid #d1d5db;
85
+ border-radius: 0.375rem;
86
+ font-size: 1rem;
87
+ background: white;
88
+ cursor: pointer;
89
+ transition: all 0.2s;
90
+ }
91
+
92
+ .datametria-select__field:focus {
93
+ outline: none;
94
+ border-color: #0072CE;
95
+ box-shadow: 0 0 0 3px rgba(0, 114, 206, 0.1);
96
+ }
97
+
98
+ .datametria-select__field--error {
99
+ border-color: #ef4444;
100
+ }
101
+
102
+ .datametria-select__field--disabled {
103
+ background: #f3f4f6;
104
+ cursor: not-allowed;
105
+ }
106
+
107
+ .datametria-select__error {
108
+ font-size: 0.875rem;
109
+ color: #ef4444;
110
+ margin: 0;
111
+ }
112
+ </style>
@@ -0,0 +1,112 @@
1
+ <template>
2
+ <div
3
+ class="dm-spinner"
4
+ :class="[`dm-spinner--${size}`, `dm-spinner--${variant}`]"
5
+ role="status"
6
+ :aria-label="ariaLabel"
7
+ >
8
+ <svg class="dm-spinner__svg" viewBox="0 0 50 50">
9
+ <circle
10
+ class="dm-spinner__circle"
11
+ cx="25"
12
+ cy="25"
13
+ r="20"
14
+ fill="none"
15
+ stroke-width="4"
16
+ ></circle>
17
+ </svg>
18
+ <span v-if="label" class="dm-spinner__label">{{ label }}</span>
19
+ </div>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ interface Props {
24
+ size?: 'sm' | 'md' | 'lg'
25
+ variant?: 'primary' | 'secondary' | 'white'
26
+ label?: string
27
+ ariaLabel?: string
28
+ }
29
+
30
+ withDefaults(defineProps<Props>(), {
31
+ size: 'md',
32
+ variant: 'primary',
33
+ ariaLabel: 'Carregando'
34
+ })
35
+ </script>
36
+
37
+ <style scoped>
38
+ .dm-spinner {
39
+ display: inline-flex;
40
+ flex-direction: column;
41
+ align-items: center;
42
+ gap: var(--dm-space-2);
43
+ }
44
+
45
+ .dm-spinner__svg {
46
+ animation: dm-spinner-rotate 2s linear infinite;
47
+ }
48
+
49
+ .dm-spinner--sm .dm-spinner__svg {
50
+ width: 20px;
51
+ height: 20px;
52
+ }
53
+
54
+ .dm-spinner--md .dm-spinner__svg {
55
+ width: 40px;
56
+ height: 40px;
57
+ }
58
+
59
+ .dm-spinner--lg .dm-spinner__svg {
60
+ width: 60px;
61
+ height: 60px;
62
+ }
63
+
64
+ .dm-spinner__circle {
65
+ stroke-linecap: round;
66
+ animation: dm-spinner-dash 1.5s ease-in-out infinite;
67
+ }
68
+
69
+ .dm-spinner--primary .dm-spinner__circle {
70
+ stroke: var(--dm-primary);
71
+ }
72
+
73
+ .dm-spinner--secondary .dm-spinner__circle {
74
+ stroke: var(--dm-secondary);
75
+ }
76
+
77
+ .dm-spinner--white .dm-spinner__circle {
78
+ stroke: var(--dm-white);
79
+ }
80
+
81
+ .dm-spinner__label {
82
+ font-size: var(--dm-text-sm);
83
+ color: var(--dm-text-secondary);
84
+ }
85
+
86
+ @keyframes dm-spinner-rotate {
87
+ 100% {
88
+ transform: rotate(360deg);
89
+ }
90
+ }
91
+
92
+ @keyframes dm-spinner-dash {
93
+ 0% {
94
+ stroke-dasharray: 1, 150;
95
+ stroke-dashoffset: 0;
96
+ }
97
+ 50% {
98
+ stroke-dasharray: 90, 150;
99
+ stroke-dashoffset: -35;
100
+ }
101
+ 100% {
102
+ stroke-dasharray: 90, 150;
103
+ stroke-dashoffset: -124;
104
+ }
105
+ }
106
+
107
+ @media (prefers-color-scheme: dark) {
108
+ .dm-spinner__label {
109
+ color: var(--dm-gray-400);
110
+ }
111
+ }
112
+ </style>