@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,100 @@
1
+ <template>
2
+ <div
3
+ class="dm-divider"
4
+ :class="[`dm-divider--${orientation}`, { 'dm-divider--dashed': dashed }]"
5
+ :role="orientation === 'horizontal' ? 'separator' : undefined"
6
+ :aria-orientation="orientation"
7
+ >
8
+ <span v-if="$slots.default || label" class="dm-divider__label">
9
+ <slot>{{ label }}</slot>
10
+ </span>
11
+ </div>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ interface Props {
16
+ orientation?: 'horizontal' | 'vertical'
17
+ dashed?: boolean
18
+ label?: string
19
+ }
20
+
21
+ withDefaults(defineProps<Props>(), {
22
+ orientation: 'horizontal',
23
+ dashed: false
24
+ })
25
+ </script>
26
+
27
+ <style scoped>
28
+ .dm-divider {
29
+ display: flex;
30
+ align-items: center;
31
+ color: var(--dm-text-secondary);
32
+ font-size: var(--dm-text-sm);
33
+ }
34
+
35
+ .dm-divider--horizontal {
36
+ width: 100%;
37
+ margin: var(--dm-space-4) 0;
38
+ }
39
+
40
+ .dm-divider--horizontal::before,
41
+ .dm-divider--horizontal::after {
42
+ content: '';
43
+ flex: 1;
44
+ height: 1px;
45
+ background: var(--dm-gray-300);
46
+ }
47
+
48
+ .dm-divider--horizontal .dm-divider__label {
49
+ padding: 0 var(--dm-space-3);
50
+ }
51
+
52
+ .dm-divider--vertical {
53
+ height: 100%;
54
+ flex-direction: column;
55
+ margin: 0 var(--dm-space-4);
56
+ }
57
+
58
+ .dm-divider--vertical::before,
59
+ .dm-divider--vertical::after {
60
+ content: '';
61
+ flex: 1;
62
+ width: 1px;
63
+ background: var(--dm-gray-300);
64
+ }
65
+
66
+ .dm-divider--vertical .dm-divider__label {
67
+ padding: var(--dm-space-3) 0;
68
+ }
69
+
70
+ .dm-divider--dashed::before,
71
+ .dm-divider--dashed::after {
72
+ background: none;
73
+ border-top: 1px dashed var(--dm-gray-300);
74
+ }
75
+
76
+ .dm-divider--vertical.dm-divider--dashed::before,
77
+ .dm-divider--vertical.dm-divider--dashed::after {
78
+ border-top: none;
79
+ border-left: 1px dashed var(--dm-gray-300);
80
+ }
81
+
82
+ @media (prefers-color-scheme: dark) {
83
+ .dm-divider--horizontal::before,
84
+ .dm-divider--horizontal::after,
85
+ .dm-divider--vertical::before,
86
+ .dm-divider--vertical::after {
87
+ background: var(--dm-gray-700);
88
+ }
89
+
90
+ .dm-divider--dashed::before,
91
+ .dm-divider--dashed::after {
92
+ border-color: var(--dm-gray-700);
93
+ }
94
+
95
+ .dm-divider--vertical.dm-divider--dashed::before,
96
+ .dm-divider--vertical.dm-divider--dashed::after {
97
+ border-color: var(--dm-gray-700);
98
+ }
99
+ }
100
+ </style>
@@ -0,0 +1,268 @@
1
+ <template>
2
+ <div class="dm-file-upload">
3
+ <label v-if="label" class="dm-file-upload__label">
4
+ {{ label }}
5
+ <span v-if="required" class="dm-file-upload__required">*</span>
6
+ </label>
7
+ <div
8
+ class="dm-file-upload__dropzone"
9
+ :class="{ 'dm-file-upload--dragging': isDragOver, 'dm-file-upload__dropzone--dragover': isDragOver, 'dm-file-upload__dropzone--error': error }"
10
+ @dragover.prevent="handleDragOver"
11
+ @dragleave.prevent="handleDragLeave"
12
+ @drop.prevent="handleDrop"
13
+ @click="triggerFileInput"
14
+ >
15
+ <input
16
+ ref="fileInput"
17
+ type="file"
18
+ class="dm-file-upload__input"
19
+ :accept="accept"
20
+ :multiple="multiple"
21
+ :disabled="disabled"
22
+ :required="required"
23
+ @change="handleFileChange"
24
+ />
25
+ <div class="dm-file-upload__content">
26
+ <svg class="dm-file-upload__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
27
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
28
+ </svg>
29
+ <p class="dm-file-upload__text">
30
+ <span class="dm-file-upload__text--primary">Clique para selecionar</span>
31
+ ou arraste arquivos aqui
32
+ </p>
33
+ <p v-if="accept" class="dm-file-upload__hint">{{ accept }}</p>
34
+ </div>
35
+ </div>
36
+ <div v-if="selectedFiles.length" class="dm-file-upload__files">
37
+ <div v-for="(file, index) in selectedFiles" :key="index" class="dm-file-upload__file">
38
+ <span class="dm-file-upload__filename">{{ file.name }}</span>
39
+ <span class="dm-file-upload__filesize">{{ formatFileSize(file.size) }}</span>
40
+ <button
41
+ type="button"
42
+ class="dm-file-upload__remove"
43
+ @click="removeFile(index)"
44
+ aria-label="Remover arquivo"
45
+ >×</button>
46
+ </div>
47
+ </div>
48
+ <p v-if="error" class="dm-file-upload__error">{{ error }}</p>
49
+ </div>
50
+ </template>
51
+
52
+ <script setup lang="ts">
53
+ import { ref } from 'vue'
54
+
55
+ interface Props {
56
+ modelValue?: File[]
57
+ label?: string
58
+ accept?: string
59
+ multiple?: boolean
60
+ disabled?: boolean
61
+ required?: boolean
62
+ error?: string
63
+ }
64
+
65
+ const props = withDefaults(defineProps<Props>(), {
66
+ modelValue: () => [],
67
+ multiple: false,
68
+ disabled: false,
69
+ required: false
70
+ })
71
+
72
+ const emit = defineEmits<{
73
+ 'update:modelValue': [files: File[]]
74
+ }>()
75
+
76
+ const fileInput = ref<HTMLInputElement>()
77
+ const selectedFiles = ref<File[]>(props.modelValue)
78
+ const isDragOver = ref(false)
79
+
80
+ const triggerFileInput = () => {
81
+ if (!props.disabled) {
82
+ fileInput.value?.click()
83
+ }
84
+ }
85
+
86
+ const handleFileChange = (event: Event) => {
87
+ const files = Array.from((event.target as HTMLInputElement).files || [])
88
+ updateFiles(files)
89
+ }
90
+
91
+ const handleDragOver = () => {
92
+ if (!props.disabled) {
93
+ isDragOver.value = true
94
+ }
95
+ }
96
+
97
+ const handleDragLeave = () => {
98
+ isDragOver.value = false
99
+ }
100
+
101
+ const handleDrop = (event: DragEvent) => {
102
+ isDragOver.value = false
103
+ if (!props.disabled) {
104
+ const files = Array.from(event.dataTransfer?.files || [])
105
+ updateFiles(files)
106
+ }
107
+ }
108
+
109
+ const updateFiles = (files: File[]) => {
110
+ selectedFiles.value = props.multiple ? [...selectedFiles.value, ...files] : files
111
+ emit('update:modelValue', selectedFiles.value)
112
+ }
113
+
114
+ const removeFile = (index: number) => {
115
+ selectedFiles.value.splice(index, 1)
116
+ emit('update:modelValue', selectedFiles.value)
117
+ }
118
+
119
+ const formatFileSize = (bytes: number): string => {
120
+ if (bytes === 0) return '0 B'
121
+ const k = 1024
122
+ const sizes = ['B', 'KB', 'MB', 'GB']
123
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
124
+ return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
125
+ }
126
+ </script>
127
+
128
+ <style scoped>
129
+ .dm-file-upload {
130
+ display: flex;
131
+ flex-direction: column;
132
+ gap: var(--dm-space-2);
133
+ }
134
+
135
+ .dm-file-upload__label {
136
+ color: var(--dm-text-primary);
137
+ font-size: var(--dm-text-sm);
138
+ font-weight: 500;
139
+ }
140
+
141
+ .dm-file-upload__required {
142
+ color: var(--dm-error);
143
+ }
144
+
145
+ .dm-file-upload__dropzone {
146
+ border: 2px dashed var(--dm-gray-300);
147
+ border-radius: var(--dm-radius);
148
+ padding: var(--dm-space-6);
149
+ text-align: center;
150
+ cursor: pointer;
151
+ transition: var(--dm-transition);
152
+ background: var(--dm-white);
153
+ }
154
+
155
+ .dm-file-upload__dropzone:hover {
156
+ border-color: var(--dm-primary);
157
+ background: var(--dm-gray-50);
158
+ }
159
+
160
+ .dm-file-upload__dropzone--dragover {
161
+ border-color: var(--dm-primary);
162
+ background: var(--dm-primary-light, rgba(0, 114, 206, 0.1));
163
+ }
164
+
165
+ .dm-file-upload__dropzone--error {
166
+ border-color: var(--dm-error);
167
+ }
168
+
169
+ .dm-file-upload__input {
170
+ display: none;
171
+ }
172
+
173
+ .dm-file-upload__content {
174
+ display: flex;
175
+ flex-direction: column;
176
+ align-items: center;
177
+ gap: var(--dm-space-2);
178
+ }
179
+
180
+ .dm-file-upload__icon {
181
+ width: 48px;
182
+ height: 48px;
183
+ color: var(--dm-gray-400);
184
+ }
185
+
186
+ .dm-file-upload__text {
187
+ color: var(--dm-text-secondary);
188
+ font-size: var(--dm-text-sm);
189
+ margin: 0;
190
+ }
191
+
192
+ .dm-file-upload__text--primary {
193
+ color: var(--dm-primary);
194
+ font-weight: 500;
195
+ }
196
+
197
+ .dm-file-upload__hint {
198
+ color: var(--dm-gray-500);
199
+ font-size: var(--dm-text-xs);
200
+ margin: 0;
201
+ }
202
+
203
+ .dm-file-upload__files {
204
+ display: flex;
205
+ flex-direction: column;
206
+ gap: var(--dm-space-2);
207
+ }
208
+
209
+ .dm-file-upload__file {
210
+ display: flex;
211
+ align-items: center;
212
+ gap: var(--dm-space-2);
213
+ padding: var(--dm-space-2) var(--dm-space-3);
214
+ background: var(--dm-gray-50);
215
+ border-radius: var(--dm-radius);
216
+ }
217
+
218
+ .dm-file-upload__filename {
219
+ flex: 1;
220
+ font-size: var(--dm-text-sm);
221
+ color: var(--dm-text-primary);
222
+ overflow: hidden;
223
+ text-overflow: ellipsis;
224
+ white-space: nowrap;
225
+ }
226
+
227
+ .dm-file-upload__filesize {
228
+ font-size: var(--dm-text-xs);
229
+ color: var(--dm-gray-500);
230
+ }
231
+
232
+ .dm-file-upload__remove {
233
+ width: 24px;
234
+ height: 24px;
235
+ border: none;
236
+ background: transparent;
237
+ color: var(--dm-gray-500);
238
+ font-size: 24px;
239
+ line-height: 1;
240
+ cursor: pointer;
241
+ transition: var(--dm-transition);
242
+ }
243
+
244
+ .dm-file-upload__remove:hover {
245
+ color: var(--dm-error);
246
+ }
247
+
248
+ .dm-file-upload__error {
249
+ color: var(--dm-error);
250
+ font-size: var(--dm-text-sm);
251
+ margin: 0;
252
+ }
253
+
254
+ @media (prefers-color-scheme: dark) {
255
+ .dm-file-upload__dropzone {
256
+ background: var(--dm-gray-800);
257
+ border-color: var(--dm-gray-600);
258
+ }
259
+
260
+ .dm-file-upload__dropzone:hover {
261
+ background: var(--dm-gray-700);
262
+ }
263
+
264
+ .dm-file-upload__file {
265
+ background: var(--dm-gray-700);
266
+ }
267
+ }
268
+ </style>
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <div
3
+ class="dm-grid"
4
+ :style="{
5
+ '--dm-grid-cols': cols,
6
+ '--dm-grid-gap': gap
7
+ }"
8
+ >
9
+ <slot></slot>
10
+ </div>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ interface Props {
15
+ cols?: number | string
16
+ gap?: string
17
+ }
18
+
19
+ withDefaults(defineProps<Props>(), {
20
+ cols: 12,
21
+ gap: 'var(--dm-space-4)'
22
+ })
23
+ </script>
24
+
25
+ <style scoped>
26
+ .dm-grid {
27
+ display: grid;
28
+ grid-template-columns: repeat(var(--dm-grid-cols, 12), 1fr);
29
+ gap: var(--dm-grid-gap, var(--dm-space-4));
30
+ }
31
+
32
+ @media (max-width: 1024px) {
33
+ .dm-grid {
34
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
35
+ }
36
+ }
37
+
38
+ @media (max-width: 640px) {
39
+ .dm-grid {
40
+ grid-template-columns: 1fr;
41
+ gap: var(--dm-space-3);
42
+ }
43
+ }
44
+ </style>
@@ -0,0 +1,102 @@
1
+ <template>
2
+ <div class="datametria-input">
3
+ <label v-if="label" :for="inputId" class="datametria-input__label">
4
+ {{ label }}
5
+ <span v-if="required" class="datametria-input__required">*</span>
6
+ </label>
7
+
8
+ <input
9
+ :id="inputId"
10
+ :value="modelValue"
11
+ :placeholder="placeholder"
12
+ :disabled="disabled"
13
+ :required="required"
14
+ :class="inputClasses"
15
+ @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
16
+ />
17
+
18
+ <p v-if="errorMessage" class="datametria-input__error">
19
+ {{ errorMessage }}
20
+ </p>
21
+ </div>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import { computed } from 'vue'
26
+
27
+ interface Props {
28
+ modelValue?: string | number
29
+ label?: string
30
+ placeholder?: string
31
+ errorMessage?: string
32
+ disabled?: boolean
33
+ required?: boolean
34
+ }
35
+
36
+ const props = withDefaults(defineProps<Props>(), {
37
+ modelValue: '',
38
+ disabled: false,
39
+ required: false
40
+ })
41
+
42
+ defineEmits<{
43
+ 'update:modelValue': [value: string]
44
+ }>()
45
+
46
+ const inputId = computed(() => `input-${Math.random().toString(36).substr(2, 9)}`)
47
+
48
+ const inputClasses = computed(() => [
49
+ 'datametria-input__field',
50
+ {
51
+ 'datametria-input__field--error': props.errorMessage,
52
+ 'datametria-input__field--disabled': props.disabled
53
+ }
54
+ ])
55
+ </script>
56
+
57
+ <style scoped>
58
+ .datametria-input {
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: 0.5rem;
62
+ }
63
+
64
+ .datametria-input__label {
65
+ font-size: 0.875rem;
66
+ font-weight: 500;
67
+ color: #374151;
68
+ }
69
+
70
+ .datametria-input__required {
71
+ color: #ef4444;
72
+ }
73
+
74
+ .datametria-input__field {
75
+ padding: 0.75rem;
76
+ border: 1px solid #d1d5db;
77
+ border-radius: 0.375rem;
78
+ font-size: 1rem;
79
+ transition: all 0.2s;
80
+ }
81
+
82
+ .datametria-input__field:focus {
83
+ outline: none;
84
+ border-color: #0072CE;
85
+ box-shadow: 0 0 0 3px rgba(0, 114, 206, 0.1);
86
+ }
87
+
88
+ .datametria-input__field--error {
89
+ border-color: #ef4444;
90
+ }
91
+
92
+ .datametria-input__field--disabled {
93
+ background: #f3f4f6;
94
+ cursor: not-allowed;
95
+ }
96
+
97
+ .datametria-input__error {
98
+ font-size: 0.875rem;
99
+ color: #ef4444;
100
+ margin: 0;
101
+ }
102
+ </style>
@@ -0,0 +1,135 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <Transition name="modal">
4
+ <div v-if="modelValue" class="datametria-modal" @click.self="handleClose">
5
+ <div class="datametria-modal__content" :style="{ maxWidth: size }">
6
+ <div v-if="title || $slots.header" class="datametria-modal__header">
7
+ <slot name="header">
8
+ <h3 class="datametria-modal__title">{{ title }}</h3>
9
+ </slot>
10
+ <button
11
+ v-if="closable"
12
+ class="datametria-modal__close"
13
+ @click="handleClose"
14
+ >
15
+ ×
16
+ </button>
17
+ </div>
18
+
19
+ <div class="datametria-modal__body">
20
+ <slot />
21
+ </div>
22
+
23
+ <div v-if="$slots.footer" class="datametria-modal__footer">
24
+ <slot name="footer" />
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </Transition>
29
+ </Teleport>
30
+ </template>
31
+
32
+ <script setup lang="ts">
33
+ interface Props {
34
+ modelValue: boolean
35
+ title?: string
36
+ size?: string
37
+ closable?: boolean
38
+ }
39
+
40
+ const props = withDefaults(defineProps<Props>(), {
41
+ size: '32rem',
42
+ closable: true
43
+ })
44
+
45
+ const emit = defineEmits<{
46
+ 'update:modelValue': [value: boolean]
47
+ close: []
48
+ }>()
49
+
50
+ const handleClose = () => {
51
+ if (props.closable) {
52
+ emit('update:modelValue', false)
53
+ emit('close')
54
+ }
55
+ }
56
+ </script>
57
+
58
+ <style scoped>
59
+ .datametria-modal {
60
+ position: fixed;
61
+ inset: 0;
62
+ background: rgba(0, 0, 0, 0.5);
63
+ display: flex;
64
+ align-items: center;
65
+ justify-content: center;
66
+ z-index: 1000;
67
+ padding: 1rem;
68
+ }
69
+
70
+ .datametria-modal__content {
71
+ background: white;
72
+ border-radius: 0.5rem;
73
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
74
+ width: 100%;
75
+ max-height: 90vh;
76
+ overflow: auto;
77
+ }
78
+
79
+ .datametria-modal__header {
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: space-between;
83
+ padding: 1.5rem;
84
+ border-bottom: 1px solid #e5e7eb;
85
+ }
86
+
87
+ .datametria-modal__title {
88
+ margin: 0;
89
+ font-size: 1.25rem;
90
+ font-weight: 600;
91
+ color: #111827;
92
+ }
93
+
94
+ .datametria-modal__close {
95
+ background: none;
96
+ border: none;
97
+ font-size: 2rem;
98
+ line-height: 1;
99
+ cursor: pointer;
100
+ color: #6b7280;
101
+ padding: 0;
102
+ width: 2rem;
103
+ height: 2rem;
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ border-radius: 0.25rem;
108
+ transition: all 0.2s;
109
+ }
110
+
111
+ .datametria-modal__close:hover {
112
+ background: #f3f4f6;
113
+ color: #111827;
114
+ }
115
+
116
+ .datametria-modal__body {
117
+ padding: 1.5rem;
118
+ }
119
+
120
+ .datametria-modal__footer {
121
+ padding: 1.5rem;
122
+ border-top: 1px solid #e5e7eb;
123
+ background: #f9fafb;
124
+ }
125
+
126
+ .modal-enter-active,
127
+ .modal-leave-active {
128
+ transition: opacity 0.3s;
129
+ }
130
+
131
+ .modal-enter-from,
132
+ .modal-leave-to {
133
+ opacity: 0;
134
+ }
135
+ </style>