@datametria/vue-components 1.1.3 → 2.0.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 (97) hide show
  1. package/README.md +548 -590
  2. package/dist/index.es.js +2460 -1440
  3. package/dist/index.umd.js +10 -10
  4. package/dist/vue-components.css +1 -1
  5. package/package.json +102 -98
  6. package/src/components/DatametriaAlert.vue +38 -24
  7. package/src/components/DatametriaAutocomplete.vue +184 -138
  8. package/src/components/DatametriaAvatar.vue +177 -33
  9. package/src/components/DatametriaBadge.vue +31 -23
  10. package/src/components/DatametriaBreadcrumb.vue +21 -21
  11. package/src/components/DatametriaButton.vue +38 -18
  12. package/src/components/DatametriaCard.vue +12 -12
  13. package/src/components/DatametriaCheckbox.vue +8 -8
  14. package/src/components/DatametriaChip.vue +29 -33
  15. package/src/components/DatametriaContainer.vue +4 -4
  16. package/src/components/DatametriaDatePicker.vue +686 -68
  17. package/src/components/DatametriaDivider.vue +13 -13
  18. package/src/components/DatametriaFileUpload.vue +272 -140
  19. package/src/components/DatametriaGrid.vue +3 -3
  20. package/src/components/DatametriaInput.vue +15 -15
  21. package/src/components/DatametriaMenu.vue +604 -619
  22. package/src/components/DatametriaModal.vue +16 -16
  23. package/src/components/DatametriaNavbar.vue +57 -54
  24. package/src/components/DatametriaPasswordInput.vue +430 -0
  25. package/src/components/DatametriaProgress.vue +18 -18
  26. package/src/components/DatametriaRadio.vue +20 -20
  27. package/src/components/DatametriaSelect.vue +15 -15
  28. package/src/components/DatametriaSkeleton.vue +243 -239
  29. package/src/components/DatametriaSlider.vue +395 -407
  30. package/src/components/DatametriaSortableTable.vue +585 -0
  31. package/src/components/DatametriaSpinner.vue +7 -7
  32. package/src/components/DatametriaSwitch.vue +16 -16
  33. package/src/components/DatametriaTable.vue +14 -14
  34. package/src/components/DatametriaTextarea.vue +28 -28
  35. package/src/components/DatametriaTimePicker.vue +285 -285
  36. package/src/components/DatametriaToast.vue +32 -19
  37. package/src/components/DatametriaTooltip.vue +408 -408
  38. package/src/components/__tests__/DatametriaAlert.test.js +36 -0
  39. package/src/components/__tests__/DatametriaAlert.test.ts +190 -0
  40. package/src/components/__tests__/DatametriaAutocomplete.test.ts +180 -0
  41. package/src/components/__tests__/DatametriaAvatar.test.ts +152 -0
  42. package/src/components/__tests__/DatametriaBadge.test.js +30 -0
  43. package/src/components/__tests__/DatametriaBadge.test.ts +167 -0
  44. package/src/components/__tests__/DatametriaBreadcrumb.test.ts +75 -0
  45. package/src/components/__tests__/DatametriaButton.test.js +31 -0
  46. package/src/components/__tests__/DatametriaButton.test.ts +283 -0
  47. package/src/components/__tests__/DatametriaCard.test.ts +201 -0
  48. package/src/components/__tests__/DatametriaCheckbox.test.ts +47 -0
  49. package/src/components/__tests__/DatametriaChip.test.js +39 -0
  50. package/src/components/__tests__/DatametriaContainer.test.ts +52 -0
  51. package/src/components/__tests__/DatametriaDatePicker.test.ts +234 -0
  52. package/src/components/__tests__/DatametriaDivider.test.ts +54 -0
  53. package/src/components/__tests__/DatametriaFileUpload.test.ts +291 -0
  54. package/src/components/__tests__/DatametriaGrid.test.ts +31 -0
  55. package/src/components/__tests__/DatametriaInput.test.ts +72 -0
  56. package/src/components/__tests__/DatametriaMenu.test.ts +366 -0
  57. package/src/components/__tests__/DatametriaModal.test.ts +86 -0
  58. package/src/components/__tests__/DatametriaNavbar.test.js +49 -0
  59. package/src/components/__tests__/DatametriaNavbar.test.ts +203 -0
  60. package/src/components/__tests__/DatametriaPasswordInput.test.js +305 -0
  61. package/src/components/__tests__/DatametriaProgress.test.ts +90 -0
  62. package/src/components/__tests__/DatametriaRadio.test.ts +77 -0
  63. package/src/components/__tests__/DatametriaSelect.test.ts +77 -0
  64. package/src/components/__tests__/DatametriaSlider.test.ts +261 -0
  65. package/src/components/__tests__/DatametriaSortableTable.test.js +168 -0
  66. package/src/components/__tests__/DatametriaSpinner.test.ts +156 -0
  67. package/src/components/__tests__/DatametriaSwitch.test.ts +64 -0
  68. package/src/components/__tests__/DatametriaTable.test.ts +97 -0
  69. package/src/components/__tests__/DatametriaTextarea.test.ts +66 -0
  70. package/src/components/__tests__/DatametriaToast.test.js +49 -0
  71. package/src/components/__tests__/DatametriaToast.test.ts +99 -0
  72. package/src/composables/useAccessibilityScale.ts +94 -94
  73. package/src/composables/useBreakpoints.ts +82 -82
  74. package/src/composables/useHapticFeedback.ts +439 -439
  75. package/src/composables/useRipple.ts +218 -218
  76. package/src/index.ts +68 -61
  77. package/src/stories/Variants.stories.js +96 -0
  78. package/src/styles/design-tokens.css +623 -623
  79. package/src/theme/ThemeProvider.vue +96 -0
  80. package/src/theme/__tests__/ThemeProvider.test.ts +208 -0
  81. package/src/theme/__tests__/constants.test.ts +31 -0
  82. package/src/theme/__tests__/presets.test.ts +166 -0
  83. package/src/theme/__tests__/tokens.test.ts +155 -0
  84. package/src/theme/__tests__/types.test.ts +153 -0
  85. package/src/theme/__tests__/useTheme.test.ts +146 -0
  86. package/src/theme/constants.ts +14 -0
  87. package/src/theme/index.ts +12 -0
  88. package/src/theme/presets/datametria.ts +94 -0
  89. package/src/theme/presets/default.ts +94 -0
  90. package/src/theme/presets/index.ts +8 -0
  91. package/src/theme/tokens/colors.ts +28 -0
  92. package/src/theme/tokens/index.ts +47 -0
  93. package/src/theme/tokens/spacing.ts +21 -0
  94. package/src/theme/tokens/typography.ts +35 -0
  95. package/src/theme/types.ts +111 -0
  96. package/src/theme/useTheme.ts +28 -0
  97. package/src/types/index.ts +19 -0
@@ -9,6 +9,7 @@
9
9
  :id="inputId"
10
10
  v-model="searchQuery"
11
11
  type="text"
12
+ role="combobox"
12
13
  class="dm-autocomplete__input"
13
14
  :class="{ 'dm-autocomplete__input--error': error }"
14
15
  :placeholder="placeholder"
@@ -18,126 +19,150 @@
18
19
  :aria-expanded="isOpen"
19
20
  :aria-controls="`${inputId}-listbox`"
20
21
  :aria-activedescendant="activeOptionId"
21
- role="combobox"
22
- autocomplete="off"
23
- @input="handleInput"
24
22
  @focus="handleFocus"
25
23
  @blur="handleBlur"
26
24
  @keydown="handleKeydown"
25
+ @input="handleInput"
27
26
  />
28
- <ul
29
- v-if="isOpen && filteredOptions.length"
30
- :id="`${inputId}-listbox`"
31
- class="dm-autocomplete__dropdown"
32
- role="listbox"
27
+ <div v-if="loading" class="dm-autocomplete__loading">
28
+ <span class="dm-autocomplete__spinner"></span>
29
+ </div>
30
+ </div>
31
+
32
+ <div
33
+ v-if="isOpen"
34
+ :id="`${inputId}-listbox`"
35
+ class="dm-autocomplete__dropdown"
36
+ role="listbox"
37
+ >
38
+ <div
39
+ v-if="filteredOptions.length === 0"
40
+ class="dm-autocomplete__no-results"
33
41
  >
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
42
+ No results found
43
+ </div>
44
+ <div
45
+ v-for="(option, index) in filteredOptions"
46
+ :key="option.value"
47
+ :id="`${inputId}-option-${index}`"
48
+ class="dm-autocomplete__option"
49
+ :class="{ 'dm-autocomplete__option--active': index === activeIndex }"
50
+ role="option"
51
+ :aria-selected="index === activeIndex"
52
+ @click="selectOption(option)"
53
+ @mouseenter="activeIndex = index"
54
+ >
55
+ {{ option.label }}
50
56
  </div>
51
57
  </div>
52
- <p v-if="error" class="dm-autocomplete__error">{{ error }}</p>
58
+
59
+ <div v-if="multiple && selectedOptions.length > 0" class="dm-autocomplete__tags">
60
+ <span
61
+ v-for="option in selectedOptions"
62
+ :key="option.value"
63
+ class="dm-autocomplete__tag"
64
+ >
65
+ {{ option.label }}
66
+ <button
67
+ type="button"
68
+ class="dm-autocomplete__tag-remove"
69
+ @click="removeOption(option)"
70
+ >
71
+ ×
72
+ </button>
73
+ </span>
74
+ </div>
75
+
76
+ <div v-if="error" class="dm-autocomplete__error">
77
+ {{ error }}
78
+ </div>
53
79
  </div>
54
80
  </template>
55
81
 
56
82
  <script setup lang="ts">
57
- import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
83
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
84
+
85
+ interface Option {
86
+ value: string | number
87
+ label: string
88
+ [key: string]: any
89
+ }
58
90
 
59
91
  interface Props {
60
- modelValue?: string
61
- options: string[]
62
- label?: string
92
+ modelValue?: string | number | Option | Option[]
93
+ options: (Option | string)[]
63
94
  placeholder?: string
95
+ label?: string
96
+ error?: string
64
97
  disabled?: boolean
65
98
  required?: boolean
66
- error?: string
99
+ loading?: boolean
100
+ multiple?: boolean
67
101
  ariaLabel?: string
68
102
  }
69
103
 
70
104
  const props = withDefaults(defineProps<Props>(), {
71
- modelValue: '',
72
- options: () => [],
73
- disabled: false,
74
- required: false
105
+ placeholder: 'Search...',
106
+ multiple: false
75
107
  })
76
108
 
77
109
  const emit = defineEmits<{
78
- 'update:modelValue': [value: string]
110
+ 'update:modelValue': [value: string | number | Option | Option[]]
79
111
  }>()
80
112
 
81
- const inputId = `dm-autocomplete-${Math.random().toString(36).substr(2, 9)}`
82
113
  const autocompleteRef = ref<HTMLElement>()
83
- const searchQuery = ref(props.modelValue)
114
+ const searchQuery = ref('')
84
115
  const isOpen = ref(false)
85
116
  const activeIndex = ref(-1)
117
+ const selectedOptions = ref<Option[]>([])
86
118
 
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
119
+ const inputId = computed(() => `autocomplete-${Math.random().toString(36).substr(2, 9)}`)
120
+ const activeOptionId = computed(() =>
121
+ activeIndex.value >= 0 ? `${inputId.value}-option-${activeIndex.value}` : undefined
96
122
  )
97
123
 
98
- watch(() => props.modelValue, (newValue) => {
99
- searchQuery.value = newValue
124
+ const filteredOptions = computed(() => {
125
+ const normalizedOptions = props.options.map(option =>
126
+ typeof option === 'string'
127
+ ? { value: option, label: option }
128
+ : option
129
+ )
130
+
131
+ if (!searchQuery.value) return normalizedOptions
132
+ return normalizedOptions.filter(option =>
133
+ option.label.toLowerCase().includes(searchQuery.value.toLowerCase())
134
+ )
100
135
  })
101
136
 
102
- const handleInput = () => {
103
- isOpen.value = true
104
- activeIndex.value = -1
105
- emit('update:modelValue', searchQuery.value)
106
- }
107
-
108
137
  const handleFocus = () => {
109
- if (!props.disabled) {
110
- isOpen.value = true
111
- }
138
+ isOpen.value = true
112
139
  }
113
140
 
114
141
  const handleBlur = () => {
115
142
  setTimeout(() => {
116
143
  isOpen.value = false
144
+ activeIndex.value = -1
117
145
  }, 200)
118
146
  }
119
147
 
120
- const selectOption = (option: string) => {
121
- searchQuery.value = option
122
- emit('update:modelValue', option)
123
- isOpen.value = false
148
+ const handleInput = () => {
149
+ isOpen.value = true
124
150
  activeIndex.value = -1
125
151
  }
126
152
 
127
153
  const handleKeydown = (event: KeyboardEvent) => {
128
- if (!isOpen.value && event.key !== 'Escape') {
129
- isOpen.value = true
130
- return
131
- }
132
-
133
154
  switch (event.key) {
134
155
  case 'ArrowDown':
135
156
  event.preventDefault()
136
- activeIndex.value = Math.min(activeIndex.value + 1, filteredOptions.value.length - 1)
157
+ if (activeIndex.value < filteredOptions.value.length - 1) {
158
+ activeIndex.value++
159
+ }
137
160
  break
138
161
  case 'ArrowUp':
139
162
  event.preventDefault()
140
- activeIndex.value = Math.max(activeIndex.value - 1, 0)
163
+ if (activeIndex.value > 0) {
164
+ activeIndex.value--
165
+ }
141
166
  break
142
167
  case 'Enter':
143
168
  event.preventDefault()
@@ -152,7 +177,25 @@ const handleKeydown = (event: KeyboardEvent) => {
152
177
  }
153
178
  }
154
179
 
155
- const handleClickOutside = (event: MouseEvent) => {
180
+ const selectOption = (option: Option) => {
181
+ if (props.multiple) {
182
+ if (!selectedOptions.value.find(o => o.value === option.value)) {
183
+ selectedOptions.value.push(option)
184
+ emit('update:modelValue', selectedOptions.value)
185
+ }
186
+ } else {
187
+ searchQuery.value = option.label
188
+ emit('update:modelValue', option)
189
+ isOpen.value = false
190
+ }
191
+ }
192
+
193
+ const removeOption = (option: Option) => {
194
+ selectedOptions.value = selectedOptions.value.filter(o => o.value !== option.value)
195
+ emit('update:modelValue', selectedOptions.value)
196
+ }
197
+
198
+ const handleClickOutside = (event: Event) => {
156
199
  if (autocompleteRef.value && !autocompleteRef.value.contains(event.target as Node)) {
157
200
  isOpen.value = false
158
201
  }
@@ -169,20 +212,19 @@ onUnmounted(() => {
169
212
 
170
213
  <style scoped>
171
214
  .dm-autocomplete {
172
- display: flex;
173
- flex-direction: column;
174
- gap: var(--dm-space-2);
175
215
  position: relative;
216
+ width: 100%;
176
217
  }
177
218
 
178
219
  .dm-autocomplete__label {
179
- color: var(--dm-text-primary);
180
- font-size: var(--dm-text-sm);
220
+ display: block;
221
+ margin-bottom: 0.5rem;
181
222
  font-weight: 500;
223
+ color: var(--dm-neutral-700, #374151);
182
224
  }
183
225
 
184
226
  .dm-autocomplete__required {
185
- color: var(--dm-error);
227
+ color: var(--dm-error, #ef4444);
186
228
  }
187
229
 
188
230
  .dm-autocomplete__wrapper {
@@ -191,102 +233,106 @@ onUnmounted(() => {
191
233
 
192
234
  .dm-autocomplete__input {
193
235
  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);
236
+ padding: 0.75rem;
237
+ border: 1px solid var(--dm-neutral-300, #d1d5db);
238
+ border-radius: var(--dm-radius-md, 0.375rem);
239
+ font-size: 1rem;
240
+ transition: border-color 0.2s;
202
241
  }
203
242
 
204
- .dm-autocomplete__input::placeholder {
205
- color: var(--dm-gray-400);
243
+ .dm-autocomplete__input:focus {
244
+ outline: none;
245
+ border-color: var(--dm-primary, #0072ce);
246
+ box-shadow: 0 0 0 3px rgba(0, 114, 206, 0.1);
206
247
  }
207
248
 
208
- .dm-autocomplete__input:hover:not(:disabled) {
209
- border-color: var(--dm-gray-400);
249
+ .dm-autocomplete__input--error {
250
+ border-color: var(--dm-error, #ef4444);
210
251
  }
211
252
 
212
- .dm-autocomplete__input:focus {
213
- outline: var(--dm-focus-ring);
214
- outline-offset: 0;
215
- border-color: var(--dm-primary);
253
+ .dm-autocomplete__loading {
254
+ position: absolute;
255
+ right: 0.75rem;
256
+ top: 50%;
257
+ transform: translateY(-50%);
216
258
  }
217
259
 
218
- .dm-autocomplete__input:disabled {
219
- background: var(--dm-gray-100);
220
- cursor: not-allowed;
221
- opacity: 0.6;
260
+ .dm-autocomplete__spinner {
261
+ display: inline-block;
262
+ width: 1rem;
263
+ height: 1rem;
264
+ border: 2px solid var(--dm-neutral-300, #d1d5db);
265
+ border-top: 2px solid var(--dm-primary, #0072ce);
266
+ border-radius: 50%;
267
+ animation: spin 1s linear infinite;
222
268
  }
223
269
 
224
- .dm-autocomplete__input--error {
225
- border-color: var(--dm-error);
270
+ @keyframes spin {
271
+ 0% { transform: rotate(0deg); }
272
+ 100% { transform: rotate(360deg); }
226
273
  }
227
274
 
228
275
  .dm-autocomplete__dropdown {
229
276
  position: absolute;
230
- top: calc(100% + 4px);
277
+ top: 100%;
231
278
  left: 0;
232
279
  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
280
  z-index: 1000;
281
+ background: white;
282
+ border: 1px solid var(--dm-neutral-300, #d1d5db);
283
+ border-radius: var(--dm-radius-md, 0.375rem);
284
+ box-shadow: var(--dm-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.1));
285
+ max-height: 200px;
286
+ overflow-y: auto;
243
287
  }
244
288
 
245
289
  .dm-autocomplete__option {
246
- padding: var(--dm-space-2) var(--dm-space-3);
290
+ padding: 0.75rem;
247
291
  cursor: pointer;
248
- border-radius: var(--dm-radius);
249
- transition: var(--dm-transition);
250
- color: var(--dm-text-primary);
292
+ transition: background-color 0.2s;
251
293
  }
252
294
 
253
295
  .dm-autocomplete__option:hover,
254
296
  .dm-autocomplete__option--active {
255
- background: var(--dm-gray-100);
297
+ background-color: var(--dm-neutral-100, #f3f4f6);
256
298
  }
257
299
 
258
300
  .dm-autocomplete__no-results {
259
- padding: var(--dm-space-3);
301
+ padding: 0.75rem;
302
+ color: var(--dm-neutral-500, #6b7280);
260
303
  text-align: center;
261
- color: var(--dm-gray-500);
262
- font-size: var(--dm-text-sm);
263
304
  }
264
305
 
265
- .dm-autocomplete__error {
266
- color: var(--dm-error);
267
- font-size: var(--dm-text-sm);
268
- margin: 0;
306
+ .dm-autocomplete__tags {
307
+ display: flex;
308
+ flex-wrap: wrap;
309
+ gap: 0.5rem;
310
+ margin-top: 0.5rem;
269
311
  }
270
312
 
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
- }
313
+ .dm-autocomplete__tag {
314
+ display: inline-flex;
315
+ align-items: center;
316
+ gap: 0.25rem;
317
+ padding: 0.25rem 0.5rem;
318
+ background-color: var(--dm-primary, #0072ce);
319
+ color: white;
320
+ border-radius: var(--dm-radius-sm, 0.25rem);
321
+ font-size: 0.875rem;
322
+ }
281
323
 
282
- .dm-autocomplete__dropdown {
283
- background: var(--dm-gray-800);
284
- border-color: var(--dm-gray-600);
285
- }
324
+ .dm-autocomplete__tag-remove {
325
+ background: none;
326
+ border: none;
327
+ color: white;
328
+ cursor: pointer;
329
+ font-size: 1.25rem;
330
+ line-height: 1;
331
+ }
286
332
 
287
- .dm-autocomplete__option:hover,
288
- .dm-autocomplete__option--active {
289
- background: var(--dm-gray-700);
290
- }
333
+ .dm-autocomplete__error {
334
+ margin-top: 0.25rem;
335
+ color: var(--dm-error, #ef4444);
336
+ font-size: 0.875rem;
291
337
  }
292
- </style>
338
+ </style>