@datametria/vue-components 1.1.2 → 1.1.3
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.
- package/README.md +590 -472
- package/dist/index.es.js +1879 -669
- package/dist/index.umd.js +74 -1
- package/dist/vue-components.css +1 -1
- package/package.json +98 -98
- package/src/components/DatametriaMenu.vue +620 -0
- package/src/components/DatametriaSkeleton.vue +240 -0
- package/src/components/DatametriaSlider.vue +408 -0
- package/src/components/DatametriaTimePicker.vue +286 -0
- package/src/components/DatametriaTooltip.vue +409 -0
- package/src/composables/useAccessibilityScale.ts +95 -0
- package/src/composables/useBreakpoints.ts +83 -0
- package/src/composables/useHapticFeedback.ts +440 -0
- package/src/composables/useRipple.ts +219 -0
- package/src/index.ts +61 -52
- package/src/styles/design-tokens.css +623 -31
- package/ACCESSIBILITY.md +0 -78
- package/DESIGN-SYSTEM.md +0 -70
- package/PROGRESS.md +0 -327
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="datametria-skeleton"
|
|
4
|
+
:class="[
|
|
5
|
+
`datametria-skeleton--${variant}`,
|
|
6
|
+
{ 'datametria-skeleton--animated': animated }
|
|
7
|
+
]"
|
|
8
|
+
:style="skeletonStyle"
|
|
9
|
+
:aria-label="ariaLabel"
|
|
10
|
+
role="status"
|
|
11
|
+
aria-live="polite"
|
|
12
|
+
>
|
|
13
|
+
<div v-if="variant === 'text'" class="datametria-skeleton__text">
|
|
14
|
+
<div
|
|
15
|
+
v-for="line in lines"
|
|
16
|
+
:key="line"
|
|
17
|
+
class="datametria-skeleton__line shimmer"
|
|
18
|
+
:style="getLineStyle(line)"
|
|
19
|
+
></div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div v-else-if="variant === 'avatar'" class="datametria-skeleton__avatar shimmer"></div>
|
|
23
|
+
|
|
24
|
+
<div v-else-if="variant === 'card'" class="datametria-skeleton__card">
|
|
25
|
+
<div class="datametria-skeleton__card-header shimmer"></div>
|
|
26
|
+
<div class="datametria-skeleton__card-content">
|
|
27
|
+
<div class="datametria-skeleton__line shimmer" style="width:100%"></div>
|
|
28
|
+
<div class="datametria-skeleton__line shimmer" style="width:80%"></div>
|
|
29
|
+
<div class="datametria-skeleton__line shimmer" style="width:60%"></div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div v-else-if="variant === 'button'" class="datametria-skeleton__button shimmer"></div>
|
|
34
|
+
|
|
35
|
+
<div v-else class="datametria-skeleton__custom shimmer"></div>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import { computed } from 'vue'
|
|
41
|
+
|
|
42
|
+
type SkeletonVariant = 'text' | 'avatar' | 'card' | 'button' | 'custom'
|
|
43
|
+
|
|
44
|
+
interface Props {
|
|
45
|
+
variant?: SkeletonVariant
|
|
46
|
+
width?: string | number
|
|
47
|
+
height?: string | number
|
|
48
|
+
lines?: number
|
|
49
|
+
animated?: boolean
|
|
50
|
+
ariaLabel?: string
|
|
51
|
+
lastLineWidth?: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
55
|
+
variant: 'text',
|
|
56
|
+
lines: 3,
|
|
57
|
+
animated: true,
|
|
58
|
+
ariaLabel: 'Carregando conteúdo...',
|
|
59
|
+
lastLineWidth: 75
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Computed
|
|
63
|
+
const skeletonStyle = computed(() => {
|
|
64
|
+
const style: Record<string, string> = {}
|
|
65
|
+
|
|
66
|
+
if (props.width) {
|
|
67
|
+
style.width = typeof props.width === 'number' ? `${props.width}px` : props.width
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (props.height) {
|
|
71
|
+
style.height = typeof props.height === 'number' ? `${props.height}px` : props.height
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return style
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const getLineStyle = (lineNumber: number) => {
|
|
78
|
+
// Use 100% for all lines except the last one
|
|
79
|
+
const isLastLine = lineNumber === props.lines
|
|
80
|
+
const width = isLastLine ? `${props.lastLineWidth}%` : '100%'
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
width,
|
|
84
|
+
marginBottom: lineNumber < props.lines ? '0.5rem' : '0'
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<style scoped>
|
|
90
|
+
.datametria-skeleton {
|
|
91
|
+
@apply bg-gray-200 rounded;
|
|
92
|
+
background-color: var(--dm-gray-200, #e5e7eb);
|
|
93
|
+
border-radius: var(--dm-radius);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.datametria-skeleton--animated {
|
|
97
|
+
background: linear-gradient(90deg,
|
|
98
|
+
var(--dm-gray-200, #e5e7eb) 25%,
|
|
99
|
+
var(--dm-gray-100, #f3f4f6) 50%,
|
|
100
|
+
var(--dm-gray-200, #e5e7eb) 75%
|
|
101
|
+
);
|
|
102
|
+
background-size: 200% 100%;
|
|
103
|
+
animation: shimmer var(--shimmer-duration, 1.5s) ease-in-out infinite;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
[data-theme="dark"] .datametria-skeleton {
|
|
107
|
+
background-color: var(--dm-gray-700, #374151);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
[data-theme="dark"] .datametria-skeleton--animated {
|
|
111
|
+
background: linear-gradient(90deg,
|
|
112
|
+
var(--dm-gray-700, #374151) 25%,
|
|
113
|
+
var(--dm-gray-600, #4b5563) 50%,
|
|
114
|
+
var(--dm-gray-700, #374151) 75%
|
|
115
|
+
);
|
|
116
|
+
background-size: 200% 100%;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Shimmer class for individual elements */
|
|
120
|
+
.shimmer {
|
|
121
|
+
background: inherit;
|
|
122
|
+
animation: inherit;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Text variant */
|
|
126
|
+
.datametria-skeleton--text {
|
|
127
|
+
@apply w-full;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.datametria-skeleton__text {
|
|
131
|
+
@apply space-y-2;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.datametria-skeleton__line {
|
|
135
|
+
@apply h-4 rounded;
|
|
136
|
+
background: inherit;
|
|
137
|
+
border-radius: var(--dm-radius);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Avatar variant */
|
|
141
|
+
.datametria-skeleton--avatar {
|
|
142
|
+
@apply w-12 h-12 rounded-full;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.datametria-skeleton__avatar {
|
|
146
|
+
@apply w-full h-full rounded-full;
|
|
147
|
+
background: inherit;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* Card variant */
|
|
151
|
+
.datametria-skeleton--card {
|
|
152
|
+
@apply w-full p-4;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.datametria-skeleton__card-header {
|
|
156
|
+
@apply h-6 w-3/4 mb-4 rounded;
|
|
157
|
+
background: inherit;
|
|
158
|
+
border-radius: var(--dm-radius);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.datametria-skeleton__card-content {
|
|
162
|
+
@apply space-y-2;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.datametria-skeleton__card-content .datametria-skeleton__line {
|
|
166
|
+
background: inherit;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* Button variant */
|
|
170
|
+
.datametria-skeleton--button {
|
|
171
|
+
@apply h-10 w-24 rounded;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.datametria-skeleton__button {
|
|
175
|
+
@apply w-full h-full rounded;
|
|
176
|
+
background: inherit;
|
|
177
|
+
border-radius: var(--dm-radius);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* Custom variant */
|
|
181
|
+
.datametria-skeleton--custom {
|
|
182
|
+
@apply w-full h-4;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.datametria-skeleton__custom {
|
|
186
|
+
@apply w-full h-full rounded;
|
|
187
|
+
background: inherit;
|
|
188
|
+
border-radius: var(--dm-radius);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* Shimmer animation */
|
|
192
|
+
@keyframes shimmer {
|
|
193
|
+
0% {
|
|
194
|
+
background-position: -200% 0;
|
|
195
|
+
}
|
|
196
|
+
100% {
|
|
197
|
+
background-position: 200% 0;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* Reduced motion support */
|
|
202
|
+
@media (prefers-reduced-motion: reduce) {
|
|
203
|
+
.datametria-skeleton--animated {
|
|
204
|
+
animation: none;
|
|
205
|
+
background: var(--dm-gray-200, #e5e7eb);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
[data-theme="dark"] .datametria-skeleton--animated {
|
|
209
|
+
background: var(--dm-gray-700, #374151);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* High contrast mode support */
|
|
214
|
+
@media (prefers-contrast: high) {
|
|
215
|
+
.datametria-skeleton {
|
|
216
|
+
@apply border border-gray-400;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
[data-theme="dark"] .datametria-skeleton {
|
|
220
|
+
@apply border-gray-500;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Size variants */
|
|
225
|
+
.datametria-skeleton--sm .datametria-skeleton__line {
|
|
226
|
+
@apply h-3;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.datametria-skeleton--lg .datametria-skeleton__line {
|
|
230
|
+
@apply h-5;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.datametria-skeleton--sm.datametria-skeleton--avatar {
|
|
234
|
+
@apply w-8 h-8;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.datametria-skeleton--lg.datametria-skeleton--avatar {
|
|
238
|
+
@apply w-16 h-16;
|
|
239
|
+
}
|
|
240
|
+
</style>
|
|
@@ -0,0 +1,408 @@
|
|
|
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
|
+
}
|
|
408
|
+
</style>
|