@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,137 @@
1
+ <template>
2
+ <div class="dm-switch">
3
+ <label class="dm-switch__label">
4
+ <input
5
+ type="checkbox"
6
+ class="dm-switch__input"
7
+ :checked="modelValue"
8
+ :disabled="disabled"
9
+ :aria-label="ariaLabel"
10
+ :aria-checked="modelValue"
11
+ role="switch"
12
+ @change="handleChange"
13
+ />
14
+ <span class="dm-switch__track">
15
+ <span class="dm-switch__thumb"></span>
16
+ </span>
17
+ <span v-if="label" class="dm-switch__text">{{ label }}</span>
18
+ </label>
19
+ <p v-if="error" class="dm-switch__error">{{ error }}</p>
20
+ </div>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ interface Props {
25
+ modelValue?: boolean
26
+ label?: string
27
+ disabled?: boolean
28
+ error?: string
29
+ ariaLabel?: string
30
+ }
31
+
32
+ const props = withDefaults(defineProps<Props>(), {
33
+ modelValue: false,
34
+ disabled: false
35
+ })
36
+
37
+ const emit = defineEmits<{
38
+ 'update:modelValue': [value: boolean]
39
+ }>()
40
+
41
+ const handleChange = (event: Event) => {
42
+ if (!props.disabled) {
43
+ emit('update:modelValue', (event.target as HTMLInputElement).checked)
44
+ }
45
+ }
46
+ </script>
47
+
48
+ <style scoped>
49
+ .dm-switch {
50
+ display: flex;
51
+ flex-direction: column;
52
+ gap: var(--dm-space-1);
53
+ }
54
+
55
+ .dm-switch__label {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ gap: var(--dm-space-3);
59
+ cursor: pointer;
60
+ min-height: 44px;
61
+ padding: var(--dm-space-2);
62
+ user-select: none;
63
+ }
64
+
65
+ .dm-switch__label:has(.dm-switch__input:disabled) {
66
+ cursor: not-allowed;
67
+ opacity: 0.5;
68
+ }
69
+
70
+ .dm-switch__input {
71
+ position: absolute;
72
+ opacity: 0;
73
+ width: 0;
74
+ height: 0;
75
+ }
76
+
77
+ .dm-switch__track {
78
+ position: relative;
79
+ width: 44px;
80
+ height: 24px;
81
+ background: var(--dm-gray-300);
82
+ border-radius: 12px;
83
+ transition: var(--dm-transition);
84
+ flex-shrink: 0;
85
+ }
86
+
87
+ .dm-switch__thumb {
88
+ position: absolute;
89
+ top: 2px;
90
+ left: 2px;
91
+ width: 20px;
92
+ height: 20px;
93
+ background: var(--dm-white);
94
+ border-radius: 50%;
95
+ transition: var(--dm-transition);
96
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
97
+ }
98
+
99
+ .dm-switch__input:checked + .dm-switch__track {
100
+ background: var(--dm-primary);
101
+ }
102
+
103
+ .dm-switch__input:checked + .dm-switch__track .dm-switch__thumb {
104
+ transform: translateX(20px);
105
+ }
106
+
107
+ .dm-switch__input:focus-visible + .dm-switch__track {
108
+ outline: var(--dm-focus-ring);
109
+ outline-offset: 2px;
110
+ }
111
+
112
+ .dm-switch__input:disabled + .dm-switch__track {
113
+ background: var(--dm-gray-200);
114
+ }
115
+
116
+ .dm-switch__text {
117
+ color: var(--dm-text-primary);
118
+ font-size: var(--dm-text-base);
119
+ line-height: 1.5;
120
+ }
121
+
122
+ .dm-switch__error {
123
+ color: var(--dm-error);
124
+ font-size: var(--dm-text-sm);
125
+ margin: 0;
126
+ }
127
+
128
+ @media (prefers-color-scheme: dark) {
129
+ .dm-switch__track {
130
+ background: var(--dm-gray-700);
131
+ }
132
+
133
+ .dm-switch__input:disabled + .dm-switch__track {
134
+ background: var(--dm-gray-800);
135
+ }
136
+ }
137
+ </style>
@@ -0,0 +1,105 @@
1
+ <template>
2
+ <div class="datametria-table">
3
+ <table class="datametria-table__table">
4
+ <thead class="datametria-table__thead">
5
+ <tr>
6
+ <th
7
+ v-for="column in columns"
8
+ :key="column.key"
9
+ class="datametria-table__th"
10
+ :style="{ width: column.width }"
11
+ >
12
+ {{ column.label }}
13
+ </th>
14
+ </tr>
15
+ </thead>
16
+ <tbody class="datametria-table__tbody">
17
+ <tr
18
+ v-for="(row, index) in data"
19
+ :key="index"
20
+ class="datametria-table__tr"
21
+ >
22
+ <td
23
+ v-for="column in columns"
24
+ :key="column.key"
25
+ class="datametria-table__td"
26
+ >
27
+ <slot :name="`cell-${column.key}`" :row="row" :value="row[column.key]">
28
+ {{ row[column.key] }}
29
+ </slot>
30
+ </td>
31
+ </tr>
32
+ </tbody>
33
+ </table>
34
+
35
+ <div v-if="data.length === 0" class="datametria-table__empty">
36
+ <slot name="empty">Nenhum dado disponível</slot>
37
+ </div>
38
+ </div>
39
+ </template>
40
+
41
+ <script setup lang="ts">
42
+ interface Column {
43
+ key: string
44
+ label: string
45
+ width?: string
46
+ }
47
+
48
+ interface Props {
49
+ columns: Column[]
50
+ data: Record<string, any>[]
51
+ }
52
+
53
+ defineProps<Props>()
54
+ </script>
55
+
56
+ <style scoped>
57
+ .datametria-table {
58
+ width: 100%;
59
+ overflow-x: auto;
60
+ }
61
+
62
+ .datametria-table__table {
63
+ width: 100%;
64
+ border-collapse: collapse;
65
+ }
66
+
67
+ .datametria-table__thead {
68
+ background: #f9fafb;
69
+ border-bottom: 2px solid #e5e7eb;
70
+ }
71
+
72
+ .datametria-table__th {
73
+ padding: 0.75rem 1rem;
74
+ text-align: left;
75
+ font-size: 0.875rem;
76
+ font-weight: 600;
77
+ color: #374151;
78
+ }
79
+
80
+ .datametria-table__tbody {
81
+ background: white;
82
+ }
83
+
84
+ .datametria-table__tr {
85
+ border-bottom: 1px solid #e5e7eb;
86
+ transition: background 0.2s;
87
+ }
88
+
89
+ .datametria-table__tr:hover {
90
+ background: #f9fafb;
91
+ }
92
+
93
+ .datametria-table__td {
94
+ padding: 0.75rem 1rem;
95
+ font-size: 0.875rem;
96
+ color: #111827;
97
+ }
98
+
99
+ .datametria-table__empty {
100
+ padding: 3rem;
101
+ text-align: center;
102
+ color: #6b7280;
103
+ font-size: 0.875rem;
104
+ }
105
+ </style>
@@ -0,0 +1,180 @@
1
+ <template>
2
+ <div class="dm-tabs">
3
+ <div class="dm-tabs__header" role="tablist" :aria-label="ariaLabel">
4
+ <button
5
+ v-for="(tab, index) in tabs"
6
+ :key="index"
7
+ :id="`tab-${index}`"
8
+ class="dm-tabs__tab"
9
+ :class="{ 'dm-tabs__tab--active': activeTab === index }"
10
+ role="tab"
11
+ :aria-selected="activeTab === index"
12
+ :aria-controls="`panel-${index}`"
13
+ :tabindex="activeTab === index ? 0 : -1"
14
+ @click="selectTab(index)"
15
+ @keydown="handleKeydown($event, index)"
16
+ >
17
+ {{ tab }}
18
+ </button>
19
+ <div
20
+ class="dm-tabs__indicator"
21
+ :style="{ transform: `translateX(${activeTab * 100}%)` }"
22
+ ></div>
23
+ </div>
24
+ <div class="dm-tabs__panels">
25
+ <div
26
+ v-for="(_, index) in tabs"
27
+ :key="index"
28
+ :id="`panel-${index}`"
29
+ class="dm-tabs__panel"
30
+ :class="{ 'dm-tabs__panel--active': activeTab === index }"
31
+ role="tabpanel"
32
+ :aria-labelledby="`tab-${index}`"
33
+ :hidden="activeTab !== index"
34
+ >
35
+ <slot :name="`panel-${index}`"></slot>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </template>
40
+
41
+ <script setup lang="ts">
42
+ import { ref, watch } from 'vue'
43
+
44
+ interface Props {
45
+ tabs: string[]
46
+ modelValue?: number
47
+ ariaLabel?: string
48
+ }
49
+
50
+ const props = withDefaults(defineProps<Props>(), {
51
+ modelValue: 0,
52
+ ariaLabel: 'Tabs'
53
+ })
54
+
55
+ const emit = defineEmits<{
56
+ 'update:modelValue': [index: number]
57
+ }>()
58
+
59
+ const activeTab = ref(props.modelValue)
60
+
61
+ watch(() => props.modelValue, (newValue) => {
62
+ activeTab.value = newValue
63
+ })
64
+
65
+ const selectTab = (index: number) => {
66
+ activeTab.value = index
67
+ emit('update:modelValue', index)
68
+ }
69
+
70
+ const handleKeydown = (event: KeyboardEvent, index: number) => {
71
+ let newIndex = index
72
+
73
+ switch (event.key) {
74
+ case 'ArrowLeft':
75
+ event.preventDefault()
76
+ newIndex = index > 0 ? index - 1 : props.tabs.length - 1
77
+ break
78
+ case 'ArrowRight':
79
+ event.preventDefault()
80
+ newIndex = index < props.tabs.length - 1 ? index + 1 : 0
81
+ break
82
+ case 'Home':
83
+ event.preventDefault()
84
+ newIndex = 0
85
+ break
86
+ case 'End':
87
+ event.preventDefault()
88
+ newIndex = props.tabs.length - 1
89
+ break
90
+ default:
91
+ return
92
+ }
93
+
94
+ selectTab(newIndex)
95
+ document.getElementById(`tab-${newIndex}`)?.focus()
96
+ }
97
+ </script>
98
+
99
+ <style scoped>
100
+ .dm-tabs {
101
+ display: flex;
102
+ flex-direction: column;
103
+ }
104
+
105
+ .dm-tabs__header {
106
+ display: flex;
107
+ position: relative;
108
+ border-bottom: 2px solid var(--dm-gray-200);
109
+ overflow-x: auto;
110
+ scrollbar-width: none;
111
+ }
112
+
113
+ .dm-tabs__header::-webkit-scrollbar {
114
+ display: none;
115
+ }
116
+
117
+ .dm-tabs__tab {
118
+ flex: 1;
119
+ min-width: max-content;
120
+ padding: var(--dm-space-3) var(--dm-space-4);
121
+ border: none;
122
+ background: transparent;
123
+ color: var(--dm-text-secondary);
124
+ font-size: var(--dm-text-base);
125
+ font-weight: 500;
126
+ cursor: pointer;
127
+ transition: var(--dm-transition);
128
+ position: relative;
129
+ white-space: nowrap;
130
+ }
131
+
132
+ .dm-tabs__tab:hover {
133
+ color: var(--dm-text-primary);
134
+ }
135
+
136
+ .dm-tabs__tab:focus-visible {
137
+ outline: var(--dm-focus-ring);
138
+ outline-offset: -2px;
139
+ }
140
+
141
+ .dm-tabs__tab--active {
142
+ color: var(--dm-primary);
143
+ }
144
+
145
+ .dm-tabs__indicator {
146
+ position: absolute;
147
+ bottom: -2px;
148
+ left: 0;
149
+ width: calc(100% / var(--tab-count, 1));
150
+ height: 2px;
151
+ background: var(--dm-primary);
152
+ transition: transform 0.3s ease;
153
+ }
154
+
155
+ .dm-tabs__panels {
156
+ padding: var(--dm-space-4);
157
+ }
158
+
159
+ .dm-tabs__panel {
160
+ display: none;
161
+ }
162
+
163
+ .dm-tabs__panel--active {
164
+ display: block;
165
+ }
166
+
167
+ @media (prefers-color-scheme: dark) {
168
+ .dm-tabs__header {
169
+ border-bottom-color: var(--dm-gray-700);
170
+ }
171
+
172
+ .dm-tabs__tab {
173
+ color: var(--dm-gray-400);
174
+ }
175
+
176
+ .dm-tabs__tab:hover {
177
+ color: var(--dm-white);
178
+ }
179
+ }
180
+ </style>
@@ -0,0 +1,159 @@
1
+ <template>
2
+ <div class="dm-textarea">
3
+ <label v-if="label" :for="inputId" class="dm-textarea__label">
4
+ {{ label }}
5
+ <span v-if="required" class="dm-textarea__required">*</span>
6
+ </label>
7
+ <textarea
8
+ :id="inputId"
9
+ v-model="internalValue"
10
+ class="dm-textarea__input"
11
+ :class="{ 'dm-textarea__input--error': error }"
12
+ :placeholder="placeholder"
13
+ :disabled="disabled"
14
+ :required="required"
15
+ :rows="rows"
16
+ :maxlength="maxLength"
17
+ :aria-label="ariaLabel"
18
+ :aria-describedby="error ? `${inputId}-error` : undefined"
19
+ :aria-invalid="!!error"
20
+ @input="handleInput"
21
+ />
22
+ <div v-if="maxLength || error" class="dm-textarea__footer">
23
+ <p v-if="error" :id="`${inputId}-error`" class="dm-textarea__error">{{ error }}</p>
24
+ <span v-if="maxLength" class="dm-textarea__counter">
25
+ {{ internalValue?.length || 0 }}/{{ maxLength }}
26
+ </span>
27
+ </div>
28
+ </div>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ import { ref, watch } from 'vue'
33
+
34
+ interface Props {
35
+ modelValue?: string
36
+ label?: string
37
+ placeholder?: string
38
+ disabled?: boolean
39
+ required?: boolean
40
+ error?: string
41
+ rows?: number
42
+ maxLength?: number
43
+ ariaLabel?: string
44
+ }
45
+
46
+ const props = withDefaults(defineProps<Props>(), {
47
+ modelValue: '',
48
+ disabled: false,
49
+ required: false,
50
+ rows: 4
51
+ })
52
+
53
+ const emit = defineEmits<{
54
+ 'update:modelValue': [value: string]
55
+ }>()
56
+
57
+ const inputId = `dm-textarea-${Math.random().toString(36).substr(2, 9)}`
58
+ const internalValue = ref(props.modelValue)
59
+
60
+ watch(() => props.modelValue, (newValue) => {
61
+ internalValue.value = newValue
62
+ })
63
+
64
+ const handleInput = () => {
65
+ emit('update:modelValue', internalValue.value || '')
66
+ }
67
+ </script>
68
+
69
+ <style scoped>
70
+ .dm-textarea {
71
+ display: flex;
72
+ flex-direction: column;
73
+ gap: var(--dm-space-2);
74
+ }
75
+
76
+ .dm-textarea__label {
77
+ color: var(--dm-text-primary);
78
+ font-size: var(--dm-text-sm);
79
+ font-weight: 500;
80
+ }
81
+
82
+ .dm-textarea__required {
83
+ color: var(--dm-error);
84
+ }
85
+
86
+ .dm-textarea__input {
87
+ width: 100%;
88
+ padding: var(--dm-space-3);
89
+ border: 1px solid var(--dm-gray-300);
90
+ border-radius: var(--dm-radius);
91
+ font-size: var(--dm-text-base);
92
+ color: var(--dm-text-primary);
93
+ background: var(--dm-white);
94
+ transition: var(--dm-transition);
95
+ resize: vertical;
96
+ font-family: inherit;
97
+ line-height: 1.5;
98
+ }
99
+
100
+ .dm-textarea__input::placeholder {
101
+ color: var(--dm-gray-400);
102
+ }
103
+
104
+ .dm-textarea__input:hover:not(:disabled) {
105
+ border-color: var(--dm-gray-400);
106
+ }
107
+
108
+ .dm-textarea__input:focus {
109
+ outline: var(--dm-focus-ring);
110
+ outline-offset: 0;
111
+ border-color: var(--dm-primary);
112
+ }
113
+
114
+ .dm-textarea__input:disabled {
115
+ background: var(--dm-gray-100);
116
+ cursor: not-allowed;
117
+ opacity: 0.6;
118
+ }
119
+
120
+ .dm-textarea__input--error {
121
+ border-color: var(--dm-error);
122
+ }
123
+
124
+ .dm-textarea__input--error:focus {
125
+ outline-color: var(--dm-error);
126
+ }
127
+
128
+ .dm-textarea__footer {
129
+ display: flex;
130
+ justify-content: space-between;
131
+ align-items: center;
132
+ gap: var(--dm-space-2);
133
+ }
134
+
135
+ .dm-textarea__error {
136
+ color: var(--dm-error);
137
+ font-size: var(--dm-text-sm);
138
+ margin: 0;
139
+ flex: 1;
140
+ }
141
+
142
+ .dm-textarea__counter {
143
+ color: var(--dm-gray-500);
144
+ font-size: var(--dm-text-sm);
145
+ white-space: nowrap;
146
+ }
147
+
148
+ @media (prefers-color-scheme: dark) {
149
+ .dm-textarea__input {
150
+ background: var(--dm-gray-800);
151
+ border-color: var(--dm-gray-600);
152
+ color: var(--dm-white);
153
+ }
154
+
155
+ .dm-textarea__input:disabled {
156
+ background: var(--dm-gray-900);
157
+ }
158
+ }
159
+ </style>