@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,292 @@
1
+ <template>
2
+ <div class="dm-autocomplete" ref="autocompleteRef">
3
+ <label v-if="label" :for="inputId" class="dm-autocomplete__label">
4
+ {{ label }}
5
+ <span v-if="required" class="dm-autocomplete__required">*</span>
6
+ </label>
7
+ <div class="dm-autocomplete__wrapper">
8
+ <input
9
+ :id="inputId"
10
+ v-model="searchQuery"
11
+ type="text"
12
+ class="dm-autocomplete__input"
13
+ :class="{ 'dm-autocomplete__input--error': error }"
14
+ :placeholder="placeholder"
15
+ :disabled="disabled"
16
+ :required="required"
17
+ :aria-label="ariaLabel"
18
+ :aria-expanded="isOpen"
19
+ :aria-controls="`${inputId}-listbox`"
20
+ :aria-activedescendant="activeOptionId"
21
+ role="combobox"
22
+ autocomplete="off"
23
+ @input="handleInput"
24
+ @focus="handleFocus"
25
+ @blur="handleBlur"
26
+ @keydown="handleKeydown"
27
+ />
28
+ <ul
29
+ v-if="isOpen && filteredOptions.length"
30
+ :id="`${inputId}-listbox`"
31
+ class="dm-autocomplete__dropdown"
32
+ role="listbox"
33
+ >
34
+ <li
35
+ v-for="(option, index) in filteredOptions"
36
+ :key="index"
37
+ :id="`${inputId}-option-${index}`"
38
+ class="dm-autocomplete__option"
39
+ :class="{ 'dm-autocomplete__option--active': index === activeIndex }"
40
+ role="option"
41
+ :aria-selected="index === activeIndex"
42
+ @click="selectOption(option)"
43
+ @mouseenter="activeIndex = index"
44
+ >
45
+ {{ option }}
46
+ </li>
47
+ </ul>
48
+ <div v-else-if="isOpen && !filteredOptions.length" class="dm-autocomplete__no-results">
49
+ No results
50
+ </div>
51
+ </div>
52
+ <p v-if="error" class="dm-autocomplete__error">{{ error }}</p>
53
+ </div>
54
+ </template>
55
+
56
+ <script setup lang="ts">
57
+ import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
58
+
59
+ interface Props {
60
+ modelValue?: string
61
+ options: string[]
62
+ label?: string
63
+ placeholder?: string
64
+ disabled?: boolean
65
+ required?: boolean
66
+ error?: string
67
+ ariaLabel?: string
68
+ }
69
+
70
+ const props = withDefaults(defineProps<Props>(), {
71
+ modelValue: '',
72
+ options: () => [],
73
+ disabled: false,
74
+ required: false
75
+ })
76
+
77
+ const emit = defineEmits<{
78
+ 'update:modelValue': [value: string]
79
+ }>()
80
+
81
+ const inputId = `dm-autocomplete-${Math.random().toString(36).substr(2, 9)}`
82
+ const autocompleteRef = ref<HTMLElement>()
83
+ const searchQuery = ref(props.modelValue)
84
+ const isOpen = ref(false)
85
+ const activeIndex = ref(-1)
86
+
87
+ const filteredOptions = computed(() => {
88
+ if (!searchQuery.value) return props.options
89
+ return props.options.filter(option =>
90
+ option.toLowerCase().includes(searchQuery.value.toLowerCase())
91
+ )
92
+ })
93
+
94
+ const activeOptionId = computed(() =>
95
+ activeIndex.value >= 0 ? `${inputId}-option-${activeIndex.value}` : undefined
96
+ )
97
+
98
+ watch(() => props.modelValue, (newValue) => {
99
+ searchQuery.value = newValue
100
+ })
101
+
102
+ const handleInput = () => {
103
+ isOpen.value = true
104
+ activeIndex.value = -1
105
+ emit('update:modelValue', searchQuery.value)
106
+ }
107
+
108
+ const handleFocus = () => {
109
+ if (!props.disabled) {
110
+ isOpen.value = true
111
+ }
112
+ }
113
+
114
+ const handleBlur = () => {
115
+ setTimeout(() => {
116
+ isOpen.value = false
117
+ }, 200)
118
+ }
119
+
120
+ const selectOption = (option: string) => {
121
+ searchQuery.value = option
122
+ emit('update:modelValue', option)
123
+ isOpen.value = false
124
+ activeIndex.value = -1
125
+ }
126
+
127
+ const handleKeydown = (event: KeyboardEvent) => {
128
+ if (!isOpen.value && event.key !== 'Escape') {
129
+ isOpen.value = true
130
+ return
131
+ }
132
+
133
+ switch (event.key) {
134
+ case 'ArrowDown':
135
+ event.preventDefault()
136
+ activeIndex.value = Math.min(activeIndex.value + 1, filteredOptions.value.length - 1)
137
+ break
138
+ case 'ArrowUp':
139
+ event.preventDefault()
140
+ activeIndex.value = Math.max(activeIndex.value - 1, 0)
141
+ break
142
+ case 'Enter':
143
+ event.preventDefault()
144
+ if (activeIndex.value >= 0) {
145
+ selectOption(filteredOptions.value[activeIndex.value])
146
+ }
147
+ break
148
+ case 'Escape':
149
+ isOpen.value = false
150
+ activeIndex.value = -1
151
+ break
152
+ }
153
+ }
154
+
155
+ const handleClickOutside = (event: MouseEvent) => {
156
+ if (autocompleteRef.value && !autocompleteRef.value.contains(event.target as Node)) {
157
+ isOpen.value = false
158
+ }
159
+ }
160
+
161
+ onMounted(() => {
162
+ document.addEventListener('click', handleClickOutside)
163
+ })
164
+
165
+ onUnmounted(() => {
166
+ document.removeEventListener('click', handleClickOutside)
167
+ })
168
+ </script>
169
+
170
+ <style scoped>
171
+ .dm-autocomplete {
172
+ display: flex;
173
+ flex-direction: column;
174
+ gap: var(--dm-space-2);
175
+ position: relative;
176
+ }
177
+
178
+ .dm-autocomplete__label {
179
+ color: var(--dm-text-primary);
180
+ font-size: var(--dm-text-sm);
181
+ font-weight: 500;
182
+ }
183
+
184
+ .dm-autocomplete__required {
185
+ color: var(--dm-error);
186
+ }
187
+
188
+ .dm-autocomplete__wrapper {
189
+ position: relative;
190
+ }
191
+
192
+ .dm-autocomplete__input {
193
+ width: 100%;
194
+ min-height: 44px;
195
+ padding: var(--dm-space-3);
196
+ border: 1px solid var(--dm-gray-300);
197
+ border-radius: var(--dm-radius);
198
+ font-size: var(--dm-text-base);
199
+ color: var(--dm-text-primary);
200
+ background: var(--dm-white);
201
+ transition: var(--dm-transition);
202
+ }
203
+
204
+ .dm-autocomplete__input::placeholder {
205
+ color: var(--dm-gray-400);
206
+ }
207
+
208
+ .dm-autocomplete__input:hover:not(:disabled) {
209
+ border-color: var(--dm-gray-400);
210
+ }
211
+
212
+ .dm-autocomplete__input:focus {
213
+ outline: var(--dm-focus-ring);
214
+ outline-offset: 0;
215
+ border-color: var(--dm-primary);
216
+ }
217
+
218
+ .dm-autocomplete__input:disabled {
219
+ background: var(--dm-gray-100);
220
+ cursor: not-allowed;
221
+ opacity: 0.6;
222
+ }
223
+
224
+ .dm-autocomplete__input--error {
225
+ border-color: var(--dm-error);
226
+ }
227
+
228
+ .dm-autocomplete__dropdown {
229
+ position: absolute;
230
+ top: calc(100% + 4px);
231
+ left: 0;
232
+ right: 0;
233
+ max-height: 240px;
234
+ overflow-y: auto;
235
+ background: var(--dm-white);
236
+ border: 1px solid var(--dm-gray-300);
237
+ border-radius: var(--dm-radius);
238
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
239
+ list-style: none;
240
+ margin: 0;
241
+ padding: var(--dm-space-1);
242
+ z-index: 1000;
243
+ }
244
+
245
+ .dm-autocomplete__option {
246
+ padding: var(--dm-space-2) var(--dm-space-3);
247
+ cursor: pointer;
248
+ border-radius: var(--dm-radius);
249
+ transition: var(--dm-transition);
250
+ color: var(--dm-text-primary);
251
+ }
252
+
253
+ .dm-autocomplete__option:hover,
254
+ .dm-autocomplete__option--active {
255
+ background: var(--dm-gray-100);
256
+ }
257
+
258
+ .dm-autocomplete__no-results {
259
+ padding: var(--dm-space-3);
260
+ text-align: center;
261
+ color: var(--dm-gray-500);
262
+ font-size: var(--dm-text-sm);
263
+ }
264
+
265
+ .dm-autocomplete__error {
266
+ color: var(--dm-error);
267
+ font-size: var(--dm-text-sm);
268
+ margin: 0;
269
+ }
270
+
271
+ @media (prefers-color-scheme: dark) {
272
+ .dm-autocomplete__input {
273
+ background: var(--dm-gray-800);
274
+ border-color: var(--dm-gray-600);
275
+ color: var(--dm-white);
276
+ }
277
+
278
+ .dm-autocomplete__input:disabled {
279
+ background: var(--dm-gray-900);
280
+ }
281
+
282
+ .dm-autocomplete__dropdown {
283
+ background: var(--dm-gray-800);
284
+ border-color: var(--dm-gray-600);
285
+ }
286
+
287
+ .dm-autocomplete__option:hover,
288
+ .dm-autocomplete__option--active {
289
+ background: var(--dm-gray-700);
290
+ }
291
+ }
292
+ </style>
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <div
3
+ class="dm-avatar"
4
+ :class="[`dm-avatar--${size}`, { 'dm-avatar--rounded': rounded }]"
5
+ :aria-label="ariaLabel || name"
6
+ >
7
+ <img
8
+ v-if="src"
9
+ :src="src"
10
+ :alt="alt || name"
11
+ class="dm-avatar__image"
12
+ @error="handleImageError"
13
+ />
14
+ <span v-else class="dm-avatar__initials">{{ initials }}</span>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { computed, ref } from 'vue'
20
+
21
+ interface Props {
22
+ src?: string
23
+ name?: string
24
+ alt?: string
25
+ size?: 'sm' | 'md' | 'lg' | 'xl'
26
+ rounded?: boolean
27
+ ariaLabel?: string
28
+ }
29
+
30
+ const props = withDefaults(defineProps<Props>(), {
31
+ size: 'md',
32
+ rounded: false
33
+ })
34
+
35
+ const imageError = ref(false)
36
+
37
+ const initials = computed(() => {
38
+ if (!props.name) return '?'
39
+ const names = props.name.trim().split(' ')
40
+ if (names.length === 1) return names[0].charAt(0).toUpperCase()
41
+ return (names[0].charAt(0) + names[names.length - 1].charAt(0)).toUpperCase()
42
+ })
43
+
44
+ const handleImageError = () => {
45
+ imageError.value = true
46
+ }
47
+ </script>
48
+
49
+ <style scoped>
50
+ .dm-avatar {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ background: var(--dm-primary);
55
+ color: var(--dm-white);
56
+ font-weight: 600;
57
+ overflow: hidden;
58
+ flex-shrink: 0;
59
+ border-radius: 50%;
60
+ }
61
+
62
+ .dm-avatar--sm {
63
+ width: 32px;
64
+ height: 32px;
65
+ font-size: var(--dm-text-xs);
66
+ }
67
+
68
+ .dm-avatar--md {
69
+ width: 40px;
70
+ height: 40px;
71
+ font-size: var(--dm-text-sm);
72
+ }
73
+
74
+ .dm-avatar--lg {
75
+ width: 56px;
76
+ height: 56px;
77
+ font-size: var(--dm-text-base);
78
+ }
79
+
80
+ .dm-avatar--xl {
81
+ width: 80px;
82
+ height: 80px;
83
+ font-size: var(--dm-text-lg);
84
+ }
85
+
86
+ .dm-avatar--rounded {
87
+ border-radius: var(--dm-radius);
88
+ }
89
+
90
+ .dm-avatar__image {
91
+ width: 100%;
92
+ height: 100%;
93
+ object-fit: cover;
94
+ }
95
+
96
+ .dm-avatar__initials {
97
+ user-select: none;
98
+ }
99
+ </style>
@@ -0,0 +1,90 @@
1
+ <template>
2
+ <span
3
+ class="dm-badge"
4
+ :class="[`dm-badge--${variant}`, `dm-badge--${size}`]"
5
+ :aria-label="ariaLabel"
6
+ >
7
+ <slot>{{ label }}</slot>
8
+ </span>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ interface Props {
13
+ label?: string
14
+ variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info'
15
+ size?: 'sm' | 'md' | 'lg'
16
+ ariaLabel?: string
17
+ }
18
+
19
+ withDefaults(defineProps<Props>(), {
20
+ variant: 'primary',
21
+ size: 'md'
22
+ })
23
+ </script>
24
+
25
+ <style scoped>
26
+ .dm-badge {
27
+ display: inline-flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ font-weight: 600;
31
+ border-radius: 12px;
32
+ white-space: nowrap;
33
+ user-select: none;
34
+ }
35
+
36
+ .dm-badge--sm {
37
+ padding: 2px 8px;
38
+ font-size: var(--dm-text-xs);
39
+ min-height: 20px;
40
+ }
41
+
42
+ .dm-badge--md {
43
+ padding: 4px 12px;
44
+ font-size: var(--dm-text-sm);
45
+ min-height: 24px;
46
+ }
47
+
48
+ .dm-badge--lg {
49
+ padding: 6px 16px;
50
+ font-size: var(--dm-text-base);
51
+ min-height: 32px;
52
+ }
53
+
54
+ .dm-badge--primary {
55
+ background: var(--dm-primary);
56
+ color: var(--dm-white);
57
+ }
58
+
59
+ .dm-badge--secondary {
60
+ background: var(--dm-secondary);
61
+ color: var(--dm-white);
62
+ }
63
+
64
+ .dm-badge--success {
65
+ background: var(--dm-success);
66
+ color: var(--dm-white);
67
+ }
68
+
69
+ .dm-badge--warning {
70
+ background: var(--dm-warning);
71
+ color: var(--dm-gray-900);
72
+ }
73
+
74
+ .dm-badge--error {
75
+ background: var(--dm-error);
76
+ color: var(--dm-white);
77
+ }
78
+
79
+ .dm-badge--info {
80
+ background: var(--dm-gray-200);
81
+ color: var(--dm-gray-900);
82
+ }
83
+
84
+ @media (prefers-color-scheme: dark) {
85
+ .dm-badge--info {
86
+ background: var(--dm-gray-700);
87
+ color: var(--dm-white);
88
+ }
89
+ }
90
+ </style>
@@ -0,0 +1,144 @@
1
+ <template>
2
+ <nav class="dm-breadcrumb" aria-label="Breadcrumb">
3
+ <ol class="dm-breadcrumb__list">
4
+ <li
5
+ v-for="(item, index) in items"
6
+ :key="index"
7
+ class="dm-breadcrumb__item"
8
+ >
9
+ <a
10
+ v-if="item.href && index < items.length - 1"
11
+ :href="item.href"
12
+ class="dm-breadcrumb__link"
13
+ @click="handleClick($event, item, index)"
14
+ >
15
+ {{ item.label }}
16
+ </a>
17
+ <span
18
+ v-else
19
+ class="dm-breadcrumb__current"
20
+ :aria-current="index === items.length - 1 ? 'page' : undefined"
21
+ >
22
+ {{ item.label }}
23
+ </span>
24
+ <span
25
+ v-if="index < items.length - 1"
26
+ class="dm-breadcrumb__separator"
27
+ aria-hidden="true"
28
+ >
29
+ {{ separator }}
30
+ </span>
31
+ </li>
32
+ </ol>
33
+ </nav>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ interface BreadcrumbItem {
38
+ label: string
39
+ href?: string
40
+ }
41
+
42
+ interface Props {
43
+ items: BreadcrumbItem[]
44
+ separator?: string
45
+ }
46
+
47
+ withDefaults(defineProps<Props>(), {
48
+ separator: '/'
49
+ })
50
+
51
+ const emit = defineEmits<{
52
+ click: [item: BreadcrumbItem, index: number]
53
+ }>()
54
+
55
+ const handleClick = (_event: Event, item: BreadcrumbItem, index: number) => {
56
+ emit('click', item, index)
57
+ }
58
+ </script>
59
+
60
+ <style scoped>
61
+ .dm-breadcrumb {
62
+ display: block;
63
+ }
64
+
65
+ .dm-breadcrumb__list {
66
+ display: flex;
67
+ flex-wrap: wrap;
68
+ align-items: center;
69
+ gap: var(--dm-space-2);
70
+ list-style: none;
71
+ margin: 0;
72
+ padding: 0;
73
+ }
74
+
75
+ .dm-breadcrumb__item {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: var(--dm-space-2);
79
+ }
80
+
81
+ .dm-breadcrumb__link {
82
+ color: var(--dm-primary);
83
+ text-decoration: none;
84
+ font-size: var(--dm-text-sm);
85
+ transition: var(--dm-transition);
86
+ padding: var(--dm-space-1) var(--dm-space-2);
87
+ border-radius: var(--dm-radius);
88
+ min-height: 32px;
89
+ display: inline-flex;
90
+ align-items: center;
91
+ }
92
+
93
+ .dm-breadcrumb__link:hover {
94
+ text-decoration: underline;
95
+ background: var(--dm-gray-100);
96
+ }
97
+
98
+ .dm-breadcrumb__link:focus-visible {
99
+ outline: var(--dm-focus-ring);
100
+ outline-offset: 2px;
101
+ }
102
+
103
+ .dm-breadcrumb__current {
104
+ color: var(--dm-text-primary);
105
+ font-size: var(--dm-text-sm);
106
+ font-weight: 500;
107
+ padding: var(--dm-space-1) var(--dm-space-2);
108
+ }
109
+
110
+ .dm-breadcrumb__separator {
111
+ color: var(--dm-gray-400);
112
+ font-size: var(--dm-text-sm);
113
+ user-select: none;
114
+ }
115
+
116
+ @media (max-width: 640px) {
117
+ .dm-breadcrumb__list {
118
+ gap: var(--dm-space-1);
119
+ }
120
+
121
+ .dm-breadcrumb__item {
122
+ gap: var(--dm-space-1);
123
+ }
124
+
125
+ .dm-breadcrumb__link,
126
+ .dm-breadcrumb__current {
127
+ font-size: var(--dm-text-xs);
128
+ }
129
+ }
130
+
131
+ @media (prefers-color-scheme: dark) {
132
+ .dm-breadcrumb__link:hover {
133
+ background: var(--dm-gray-800);
134
+ }
135
+
136
+ .dm-breadcrumb__current {
137
+ color: var(--dm-white);
138
+ }
139
+
140
+ .dm-breadcrumb__separator {
141
+ color: var(--dm-gray-600);
142
+ }
143
+ }
144
+ </style>