@datametria/vue-components 1.2.0 → 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 -657
  2. package/dist/index.es.js +2353 -1364
  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 +137 -137
  7. package/src/components/DatametriaAutocomplete.vue +184 -138
  8. package/src/components/DatametriaAvatar.vue +177 -33
  9. package/src/components/DatametriaBadge.vue +98 -98
  10. package/src/components/DatametriaBreadcrumb.vue +21 -21
  11. package/src/components/DatametriaButton.vue +177 -165
  12. package/src/components/DatametriaCard.vue +12 -12
  13. package/src/components/DatametriaCheckbox.vue +8 -8
  14. package/src/components/DatametriaChip.vue +145 -149
  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 +230 -252
  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 +176 -176
  37. package/src/components/DatametriaTooltip.vue +408 -408
  38. package/src/components/__tests__/DatametriaAlert.test.js +35 -35
  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 +29 -29
  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 +30 -30
  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 +38 -38
  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 +48 -48
  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 +48 -48
  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 +95 -95
  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
@@ -1,408 +1,396 @@
1
- <template>
2
- <div class="dm-slider" :class="{ 'dm-slider--disabled': disabled }">
3
- <div v-if="label || showValue" class="dm-slider__header">
4
- <label v-if="label" :for="inputId" class="dm-slider__label">
5
- {{ label }}
6
- <span v-if="required" class="dm-slider__required" aria-label="obrigatório">*</span>
7
- </label>
8
- <span v-if="showValue" class="dm-slider__value">{{ displayValue }}</span>
9
- </div>
10
-
11
- <div class="dm-slider__wrapper">
12
- <div class="dm-slider__track" @click="handleTrackClick">
13
- <div
14
- class="dm-slider__progress"
15
- :style="{ width: `${progressPercentage}%` }"
16
- ></div>
17
- <div
18
- class="dm-slider__thumb"
19
- :style="{ left: `${progressPercentage}%` }"
20
- @mousedown="handleMouseDown"
21
- @touchstart="handleTouchStart"
22
- ></div>
23
- </div>
24
-
25
- <input
26
- :id="inputId"
27
- ref="inputRef"
28
- type="range"
29
- class="dm-slider__input"
30
- :value="modelValue"
31
- :min="min"
32
- :max="max"
33
- :step="step"
34
- :disabled="disabled"
35
- :required="required"
36
- :aria-label="ariaLabel"
37
- :aria-describedby="ariaDescribedBy"
38
- :aria-valuemin="min"
39
- :aria-valuemax="max"
40
- :aria-valuenow="modelValue"
41
- :aria-valuetext="ariaValueText"
42
- @input="handleInput"
43
- @change="handleChange"
44
- @focus="handleFocus"
45
- @blur="handleBlur"
46
- />
47
- </div>
48
-
49
- <div v-if="showMinMax" class="dm-slider__range">
50
- <span class="dm-slider__min">{{ formatValue(min) }}</span>
51
- <span class="dm-slider__max">{{ formatValue(max) }}</span>
52
- </div>
53
-
54
- <div v-if="errorMessage || helperText" class="dm-slider__messages">
55
- <p v-if="errorMessage" :id="`${inputId}-error`" class="dm-slider__error" role="alert">
56
- {{ errorMessage }}
57
- </p>
58
- <p v-else-if="helperText" :id="`${inputId}-helper`" class="dm-slider__helper">
59
- {{ helperText }}
60
- </p>
61
- </div>
62
- </div>
63
- </template>
64
-
65
- <script setup lang="ts">
66
- import { ref, computed, nextTick } from 'vue'
67
-
68
- interface Props {
69
- modelValue: number
70
- min?: number
71
- max?: number
72
- step?: number
73
- label?: string
74
- disabled?: boolean
75
- required?: boolean
76
- showValue?: boolean
77
- showMinMax?: boolean
78
- errorMessage?: string
79
- helperText?: string
80
- ariaLabel?: string
81
- formatter?: (value: number) => string
82
- }
83
-
84
- interface Emits {
85
- (e: 'update:modelValue', value: number): void
86
- (e: 'change', value: number): void
87
- (e: 'input', value: number): void
88
- (e: 'focus', event: FocusEvent): void
89
- (e: 'blur', event: FocusEvent): void
90
- }
91
-
92
- const props = withDefaults(defineProps<Props>(), {
93
- min: 0,
94
- max: 100,
95
- step: 1,
96
- showValue: true,
97
- showMinMax: false
98
- })
99
-
100
- const emit = defineEmits<Emits>()
101
-
102
- // Refs
103
- const inputRef = ref<HTMLInputElement>()
104
- const isDragging = ref(false)
105
-
106
- // Computed
107
- const inputId = computed(() => `dm-slider-${Math.random().toString(36).substr(2, 9)}`)
108
-
109
- const progressPercentage = computed(() => {
110
- const range = props.max - props.min
111
- const value = props.modelValue - props.min
112
- return (value / range) * 100
113
- })
114
-
115
- const displayValue = computed(() => {
116
- return props.formatter ? props.formatter(props.modelValue) : props.modelValue.toString()
117
- })
118
-
119
- const ariaDescribedBy = computed(() => {
120
- const ids = []
121
- if (props.errorMessage) ids.push(`${inputId.value}-error`)
122
- else if (props.helperText) ids.push(`${inputId.value}-helper`)
123
- return ids.length > 0 ? ids.join(' ') : undefined
124
- })
125
-
126
- const ariaValueText = computed(() => {
127
- return props.formatter ? props.formatter(props.modelValue) : `${props.modelValue}`
128
- })
129
-
130
- // Methods
131
- const formatValue = (value: number): string => {
132
- return props.formatter ? props.formatter(value) : value.toString()
133
- }
134
-
135
- const handleInput = (event: Event) => {
136
- const target = event.target as HTMLInputElement
137
- const value = parseFloat(target.value)
138
- emit('update:modelValue', value)
139
- emit('input', value)
140
- }
141
-
142
- const handleChange = (event: Event) => {
143
- const target = event.target as HTMLInputElement
144
- const value = parseFloat(target.value)
145
- emit('change', value)
146
- }
147
-
148
- const handleFocus = (event: FocusEvent) => {
149
- emit('focus', event)
150
- }
151
-
152
- const handleBlur = (event: FocusEvent) => {
153
- emit('blur', event)
154
- }
155
-
156
- const handleTrackClick = (event: MouseEvent) => {
157
- if (props.disabled) return
158
-
159
- const track = event.currentTarget as HTMLElement
160
- const rect = track.getBoundingClientRect()
161
- const percentage = (event.clientX - rect.left) / rect.width
162
- const range = props.max - props.min
163
- const newValue = props.min + (percentage * range)
164
- const steppedValue = Math.round(newValue / props.step) * props.step
165
- const clampedValue = Math.max(props.min, Math.min(props.max, steppedValue))
166
-
167
- emit('update:modelValue', clampedValue)
168
- emit('change', clampedValue)
169
- }
170
-
171
- const handleMouseDown = (event: MouseEvent) => {
172
- if (props.disabled) return
173
-
174
- isDragging.value = true
175
- event.preventDefault()
176
-
177
- const handleMouseMove = (e: MouseEvent) => {
178
- if (!isDragging.value) return
179
-
180
- const track = (event.target as HTMLElement).parentElement
181
- if (!track) return
182
-
183
- const rect = track.getBoundingClientRect()
184
- const percentage = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
185
- const range = props.max - props.min
186
- const newValue = props.min + (percentage * range)
187
- const steppedValue = Math.round(newValue / props.step) * props.step
188
- const clampedValue = Math.max(props.min, Math.min(props.max, steppedValue))
189
-
190
- emit('update:modelValue', clampedValue)
191
- emit('input', clampedValue)
192
- }
193
-
194
- const handleMouseUp = () => {
195
- if (isDragging.value) {
196
- isDragging.value = false
197
- emit('change', props.modelValue)
198
- }
199
- document.removeEventListener('mousemove', handleMouseMove)
200
- document.removeEventListener('mouseup', handleMouseUp)
201
- }
202
-
203
- document.addEventListener('mousemove', handleMouseMove)
204
- document.addEventListener('mouseup', handleMouseUp)
205
- }
206
-
207
- const handleTouchStart = (event: TouchEvent) => {
208
- if (props.disabled) return
209
-
210
- isDragging.value = true
211
- event.preventDefault()
212
-
213
- const handleTouchMove = (e: TouchEvent) => {
214
- if (!isDragging.value) return
215
-
216
- const track = (event.target as HTMLElement).parentElement
217
- if (!track) return
218
-
219
- const rect = track.getBoundingClientRect()
220
- const touch = e.touches[0]
221
- const percentage = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width))
222
- const range = props.max - props.min
223
- const newValue = props.min + (percentage * range)
224
- const steppedValue = Math.round(newValue / props.step) * props.step
225
- const clampedValue = Math.max(props.min, Math.min(props.max, steppedValue))
226
-
227
- emit('update:modelValue', clampedValue)
228
- emit('input', clampedValue)
229
- }
230
-
231
- const handleTouchEnd = () => {
232
- if (isDragging.value) {
233
- isDragging.value = false
234
- emit('change', props.modelValue)
235
- }
236
- document.removeEventListener('touchmove', handleTouchMove)
237
- document.removeEventListener('touchend', handleTouchEnd)
238
- }
239
-
240
- document.addEventListener('touchmove', handleTouchMove)
241
- document.addEventListener('touchend', handleTouchEnd)
242
- }
243
-
244
- const focus = () => {
245
- nextTick(() => {
246
- inputRef.value?.focus()
247
- })
248
- }
249
-
250
- const blur = () => {
251
- inputRef.value?.blur()
252
- }
253
-
254
- // Expose methods
255
- defineExpose({
256
- focus,
257
- blur,
258
- inputRef
259
- })
260
- </script>
261
-
262
- <style scoped>
263
- .dm-slider {
264
- @apply w-full;
265
- }
266
-
267
- .dm-slider--disabled {
268
- @apply opacity-60 cursor-not-allowed;
269
- }
270
-
271
- .dm-slider__header {
272
- @apply flex justify-between items-center mb-2;
273
- }
274
-
275
- .dm-slider__label {
276
- @apply text-sm font-medium text-gray-700;
277
- color: var(--dm-gray-700);
278
- }
279
-
280
- [data-theme="dark"] .dm-slider__label {
281
- color: var(--dm-text-secondary);
282
- }
283
-
284
- .dm-slider__required {
285
- @apply text-red-500 ml-1;
286
- color: var(--dm-error);
287
- }
288
-
289
- .dm-slider__value {
290
- @apply text-sm font-medium text-gray-900;
291
- color: var(--dm-text-primary);
292
- }
293
-
294
- [data-theme="dark"] .dm-slider__value {
295
- color: var(--dm-text-primary);
296
- }
297
-
298
- .dm-slider__wrapper {
299
- @apply relative mb-2;
300
- }
301
-
302
- .dm-slider__track {
303
- @apply relative h-2 bg-gray-200 rounded-full cursor-pointer;
304
- background-color: var(--dm-gray-200, #e5e7eb);
305
- border-radius: var(--dm-radius);
306
- }
307
-
308
- [data-theme="dark"] .dm-slider__track {
309
- background-color: var(--dm-gray-600, #4b5563);
310
- }
311
-
312
- .dm-slider__progress {
313
- @apply absolute top-0 left-0 h-full bg-blue-600 rounded-full transition-all duration-200;
314
- background: var(--gradient-primary);
315
- border-radius: var(--dm-radius);
316
- transition: var(--dm-transition);
317
- }
318
-
319
- .dm-slider__thumb {
320
- @apply absolute top-1/2 w-5 h-5 bg-white border-2 border-blue-600 rounded-full shadow-md cursor-grab;
321
- @apply transform -translate-x-1/2 -translate-y-1/2 transition-all duration-200;
322
- @apply hover:scale-110 focus:scale-110;
323
-
324
- background-color: var(--dm-bg-primary, #ffffff);
325
- border-color: var(--dm-primary);
326
- transition: var(--dm-transition);
327
- }
328
-
329
- .dm-slider__thumb:active {
330
- @apply cursor-grabbing scale-110;
331
- }
332
-
333
- [data-theme="dark"] .dm-slider__thumb {
334
- background-color: var(--dm-bg-primary);
335
- border-color: var(--dm-primary);
336
- }
337
-
338
- .dm-slider__input {
339
- @apply absolute inset-0 w-full h-full opacity-0 cursor-pointer;
340
- @apply focus:outline-none;
341
- }
342
-
343
- .dm-slider__input:focus + .dm-slider__track .dm-slider__thumb {
344
- box-shadow: var(--dm-focus-ring);
345
- }
346
-
347
- .dm-slider__range {
348
- @apply flex justify-between text-xs text-gray-500;
349
- color: var(--dm-gray-500, #6b7280);
350
- }
351
-
352
- [data-theme="dark"] .dm-slider__range {
353
- color: var(--dm-text-secondary);
354
- }
355
-
356
- .dm-slider__min,
357
- .dm-slider__max {
358
- @apply select-none;
359
- }
360
-
361
- .dm-slider__messages {
362
- @apply mt-1;
363
- }
364
-
365
- .dm-slider__error {
366
- @apply text-sm text-red-600;
367
- color: var(--dm-error);
368
- }
369
-
370
- .dm-slider__helper {
371
- @apply text-sm text-gray-500;
372
- color: var(--dm-gray-500, #6b7280);
373
- }
374
-
375
- [data-theme="dark"] .dm-slider__helper {
376
- color: var(--dm-text-secondary);
377
- }
378
-
379
- /* High contrast mode support */
380
- @media (prefers-contrast: high) {
381
- .dm-slider__track {
382
- @apply border border-gray-400;
383
- }
384
-
385
- .dm-slider__thumb {
386
- @apply border-4;
387
- }
388
- }
389
-
390
- /* Reduced motion support */
391
- @media (prefers-reduced-motion: reduce) {
392
- .dm-slider__progress,
393
- .dm-slider__thumb {
394
- @apply transition-none;
395
- }
396
- }
397
-
398
- /* Touch device optimizations */
399
- @media (pointer: coarse) {
400
- .dm-slider__thumb {
401
- @apply w-6 h-6;
402
- }
403
-
404
- .dm-slider__track {
405
- @apply h-3;
406
- }
407
- }
1
+ <template>
2
+ <div class="dm-slider" :class="{ 'dm-slider--disabled': disabled }">
3
+ <div v-if="label || showValue" class="dm-slider__header">
4
+ <label v-if="label" :for="inputId" class="dm-slider__label">
5
+ {{ label }}
6
+ <span v-if="required" class="dm-slider__required" aria-label="obrigatório">*</span>
7
+ </label>
8
+ <span v-if="showValue" class="dm-slider__value">{{ displayValue }}</span>
9
+ </div>
10
+
11
+ <div class="dm-slider__wrapper">
12
+ <div class="dm-slider__track" @click="handleTrackClick">
13
+ <div
14
+ class="dm-slider__progress"
15
+ :style="{ width: `${progressPercentage}%` }"
16
+ ></div>
17
+ <div
18
+ class="dm-slider__thumb"
19
+ :style="{ left: `${progressPercentage}%` }"
20
+ @mousedown="handleMouseDown"
21
+ @touchstart="handleTouchStart"
22
+ ></div>
23
+ </div>
24
+
25
+ <input
26
+ :id="inputId"
27
+ ref="inputRef"
28
+ type="range"
29
+ class="dm-slider__input"
30
+ :value="modelValue"
31
+ :min="min"
32
+ :max="max"
33
+ :step="step"
34
+ :disabled="disabled"
35
+ :required="required"
36
+ :aria-label="ariaLabel"
37
+ :aria-describedby="ariaDescribedBy"
38
+ :aria-valuemin="min"
39
+ :aria-valuemax="max"
40
+ :aria-valuenow="modelValue"
41
+ :aria-valuetext="ariaValueText"
42
+ @input="handleInput"
43
+ @change="handleChange"
44
+ @focus="handleFocus"
45
+ @blur="handleBlur"
46
+ />
47
+ </div>
48
+
49
+ <div v-if="showMinMax" class="dm-slider__range">
50
+ <span class="dm-slider__min">{{ formatValue(min) }}</span>
51
+ <span class="dm-slider__max">{{ formatValue(max) }}</span>
52
+ </div>
53
+
54
+ <div v-if="errorMessage || helperText" class="dm-slider__messages">
55
+ <p v-if="errorMessage" :id="`${inputId}-error`" class="dm-slider__error" role="alert">
56
+ {{ errorMessage }}
57
+ </p>
58
+ <p v-else-if="helperText" :id="`${inputId}-helper`" class="dm-slider__helper">
59
+ {{ helperText }}
60
+ </p>
61
+ </div>
62
+ </div>
63
+ </template>
64
+
65
+ <script setup lang="ts">
66
+ import { ref, computed, nextTick } from 'vue'
67
+
68
+ interface Props {
69
+ modelValue: number
70
+ min?: number
71
+ max?: number
72
+ step?: number
73
+ label?: string
74
+ disabled?: boolean
75
+ required?: boolean
76
+ showValue?: boolean
77
+ showMinMax?: boolean
78
+ errorMessage?: string
79
+ helperText?: string
80
+ ariaLabel?: string
81
+ formatter?: (value: number) => string
82
+ }
83
+
84
+ interface Emits {
85
+ (e: 'update:modelValue', value: number): void
86
+ (e: 'change', value: number): void
87
+ (e: 'input', value: number): void
88
+ (e: 'focus', event: FocusEvent): void
89
+ (e: 'blur', event: FocusEvent): void
90
+ }
91
+
92
+ const props = withDefaults(defineProps<Props>(), {
93
+ min: 0,
94
+ max: 100,
95
+ step: 1,
96
+ showValue: true,
97
+ showMinMax: false
98
+ })
99
+
100
+ const emit = defineEmits<Emits>()
101
+
102
+ // Refs
103
+ const inputRef = ref<HTMLInputElement>()
104
+ const isDragging = ref(false)
105
+
106
+ // Computed
107
+ const inputId = computed(() => `dm-slider-${Math.random().toString(36).substr(2, 9)}`)
108
+
109
+ const progressPercentage = computed(() => {
110
+ const range = props.max - props.min
111
+ const value = props.modelValue - props.min
112
+ return (value / range) * 100
113
+ })
114
+
115
+ const displayValue = computed(() => {
116
+ return props.formatter ? props.formatter(props.modelValue) : props.modelValue.toString()
117
+ })
118
+
119
+ const ariaDescribedBy = computed(() => {
120
+ const ids = []
121
+ if (props.errorMessage) ids.push(`${inputId.value}-error`)
122
+ else if (props.helperText) ids.push(`${inputId.value}-helper`)
123
+ return ids.length > 0 ? ids.join(' ') : undefined
124
+ })
125
+
126
+ const ariaValueText = computed(() => {
127
+ return props.formatter ? props.formatter(props.modelValue) : `${props.modelValue}`
128
+ })
129
+
130
+ // Methods
131
+ const formatValue = (value: number): string => {
132
+ return props.formatter ? props.formatter(value) : value.toString()
133
+ }
134
+
135
+ const handleInput = (event: Event) => {
136
+ const target = event.target as HTMLInputElement
137
+ const value = parseFloat(target.value)
138
+ emit('update:modelValue', value)
139
+ emit('input', value)
140
+ }
141
+
142
+ const handleChange = (event: Event) => {
143
+ const target = event.target as HTMLInputElement
144
+ const value = parseFloat(target.value)
145
+ emit('change', value)
146
+ }
147
+
148
+ const handleFocus = (event: FocusEvent) => {
149
+ emit('focus', event)
150
+ }
151
+
152
+ const handleBlur = (event: FocusEvent) => {
153
+ emit('blur', event)
154
+ }
155
+
156
+ const handleTrackClick = (event: MouseEvent) => {
157
+ if (props.disabled) return
158
+
159
+ const track = event.currentTarget as HTMLElement
160
+ const rect = track.getBoundingClientRect()
161
+ const percentage = (event.clientX - rect.left) / rect.width
162
+ const range = props.max - props.min
163
+ const newValue = props.min + (percentage * range)
164
+ const steppedValue = Math.round(newValue / props.step) * props.step
165
+ const clampedValue = Math.max(props.min, Math.min(props.max, steppedValue))
166
+
167
+ emit('update:modelValue', clampedValue)
168
+ emit('change', clampedValue)
169
+ }
170
+
171
+ const handleMouseDown = (event: MouseEvent) => {
172
+ if (props.disabled) return
173
+
174
+ isDragging.value = true
175
+ event.preventDefault()
176
+
177
+ const handleMouseMove = (e: MouseEvent) => {
178
+ if (!isDragging.value) return
179
+
180
+ const track = (event.target as HTMLElement).parentElement
181
+ if (!track) return
182
+
183
+ const rect = track.getBoundingClientRect()
184
+ const percentage = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
185
+ const range = props.max - props.min
186
+ const newValue = props.min + (percentage * range)
187
+ const steppedValue = Math.round(newValue / props.step) * props.step
188
+ const clampedValue = Math.max(props.min, Math.min(props.max, steppedValue))
189
+
190
+ emit('update:modelValue', clampedValue)
191
+ emit('input', clampedValue)
192
+ }
193
+
194
+ const handleMouseUp = () => {
195
+ if (isDragging.value) {
196
+ isDragging.value = false
197
+ emit('change', props.modelValue)
198
+ }
199
+ document.removeEventListener('mousemove', handleMouseMove)
200
+ document.removeEventListener('mouseup', handleMouseUp)
201
+ }
202
+
203
+ document.addEventListener('mousemove', handleMouseMove)
204
+ document.addEventListener('mouseup', handleMouseUp)
205
+ }
206
+
207
+ const handleTouchStart = (event: TouchEvent) => {
208
+ if (props.disabled) return
209
+
210
+ isDragging.value = true
211
+ event.preventDefault()
212
+
213
+ const handleTouchMove = (e: TouchEvent) => {
214
+ if (!isDragging.value) return
215
+
216
+ const track = (event.target as HTMLElement).parentElement
217
+ if (!track) return
218
+
219
+ const rect = track.getBoundingClientRect()
220
+ const touch = e.touches[0]
221
+ const percentage = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width))
222
+ const range = props.max - props.min
223
+ const newValue = props.min + (percentage * range)
224
+ const steppedValue = Math.round(newValue / props.step) * props.step
225
+ const clampedValue = Math.max(props.min, Math.min(props.max, steppedValue))
226
+
227
+ emit('update:modelValue', clampedValue)
228
+ emit('input', clampedValue)
229
+ }
230
+
231
+ const handleTouchEnd = () => {
232
+ if (isDragging.value) {
233
+ isDragging.value = false
234
+ emit('change', props.modelValue)
235
+ }
236
+ document.removeEventListener('touchmove', handleTouchMove)
237
+ document.removeEventListener('touchend', handleTouchEnd)
238
+ }
239
+
240
+ document.addEventListener('touchmove', handleTouchMove)
241
+ document.addEventListener('touchend', handleTouchEnd)
242
+ }
243
+
244
+ const focus = () => {
245
+ nextTick(() => {
246
+ inputRef.value?.focus()
247
+ })
248
+ }
249
+
250
+ const blur = () => {
251
+ inputRef.value?.blur()
252
+ }
253
+
254
+ // Expose methods
255
+ defineExpose({
256
+ focus,
257
+ blur,
258
+ inputRef
259
+ })
260
+ </script>
261
+
262
+ <style scoped>
263
+ .dm-slider {
264
+ @apply w-full;
265
+ }
266
+
267
+ .dm-slider--disabled {
268
+ @apply opacity-60 cursor-not-allowed;
269
+ }
270
+
271
+ .dm-slider__header {
272
+ @apply flex justify-between items-center;
273
+ margin-bottom: var(--dm-spacing-2, 0.5rem);
274
+ }
275
+
276
+ .dm-slider__label {
277
+ font-size: var(--dm-font-size-sm, 0.875rem);
278
+ font-weight: var(--dm-font-weight-medium, 500);
279
+ color: var(--dm-neutral-700, #374151);
280
+ }
281
+
282
+ .dm-slider__required {
283
+ color: var(--dm-error, #ef4444);
284
+ margin-left: var(--dm-spacing-1, 0.25rem);
285
+ }
286
+
287
+ .dm-slider__value {
288
+ font-size: var(--dm-font-size-sm, 0.875rem);
289
+ font-weight: var(--dm-font-weight-medium, 500);
290
+ color: var(--dm-neutral-900, #111827);
291
+ }
292
+
293
+ .dm-slider__wrapper {
294
+ @apply relative;
295
+ margin-bottom: var(--dm-spacing-2, 0.5rem);
296
+ }
297
+
298
+ .dm-slider__track {
299
+ @apply relative cursor-pointer;
300
+ height: 0.5rem;
301
+ background-color: var(--dm-neutral-200, #e5e7eb);
302
+ border-radius: var(--dm-radius-full, 9999px);
303
+ }
304
+
305
+ .dm-slider__progress {
306
+ @apply absolute top-0 left-0 h-full;
307
+ background: var(--dm-primary, #0072CE);
308
+ border-radius: var(--dm-radius-full, 9999px);
309
+ transition: width 0.2s ease;
310
+ }
311
+
312
+ .dm-slider__thumb {
313
+ @apply absolute top-1/2 cursor-grab;
314
+ @apply transform -translate-x-1/2 -translate-y-1/2;
315
+ width: 1.25rem;
316
+ height: 1.25rem;
317
+ background-color: var(--dm-neutral-50, #ffffff);
318
+ border: 2px solid var(--dm-primary, #0072CE);
319
+ border-radius: var(--dm-radius-full, 9999px);
320
+ box-shadow: var(--dm-shadow-md, 0 4px 6px -1px rgba(0, 0, 0, 0.1));
321
+ transition: transform 0.2s ease;
322
+ }
323
+
324
+ .dm-slider__thumb:hover {
325
+ transform: translate(-50%, -50%) scale(1.1);
326
+ }
327
+
328
+ .dm-slider__thumb:active {
329
+ @apply cursor-grabbing;
330
+ transform: translate(-50%, -50%) scale(1.1);
331
+ }
332
+
333
+ .dm-slider__input {
334
+ @apply absolute inset-0 w-full h-full opacity-0 cursor-pointer;
335
+ @apply focus:outline-none;
336
+ }
337
+
338
+ .dm-slider__input:focus ~ .dm-slider__track .dm-slider__thumb {
339
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-primary, #0072CE) 20%, transparent);
340
+ }
341
+
342
+ .dm-slider__range {
343
+ @apply flex justify-between;
344
+ font-size: var(--dm-font-size-xs, 0.75rem);
345
+ color: var(--dm-neutral-500, #6b7280);
346
+ }
347
+
348
+ .dm-slider__min,
349
+ .dm-slider__max {
350
+ @apply select-none;
351
+ }
352
+
353
+ .dm-slider__messages {
354
+ margin-top: var(--dm-spacing-1, 0.25rem);
355
+ }
356
+
357
+ .dm-slider__error {
358
+ font-size: var(--dm-font-size-sm, 0.875rem);
359
+ color: var(--dm-error, #ef4444);
360
+ }
361
+
362
+ .dm-slider__helper {
363
+ font-size: var(--dm-font-size-sm, 0.875rem);
364
+ color: var(--dm-neutral-500, #6b7280);
365
+ }
366
+
367
+ /* High contrast mode support */
368
+ @media (prefers-contrast: high) {
369
+ .dm-slider__track {
370
+ @apply border border-gray-400;
371
+ }
372
+
373
+ .dm-slider__thumb {
374
+ @apply border-4;
375
+ }
376
+ }
377
+
378
+ /* Reduced motion support */
379
+ @media (prefers-reduced-motion: reduce) {
380
+ .dm-slider__progress,
381
+ .dm-slider__thumb {
382
+ @apply transition-none;
383
+ }
384
+ }
385
+
386
+ /* Touch device optimizations */
387
+ @media (pointer: coarse) {
388
+ .dm-slider__thumb {
389
+ @apply w-6 h-6;
390
+ }
391
+
392
+ .dm-slider__track {
393
+ @apply h-3;
394
+ }
395
+ }
408
396
  </style>