@datametria/vue-components 1.1.1 → 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 -473
- 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,286 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="dm-time-picker" :class="{ 'dm-time-picker--disabled': disabled }">
|
|
3
|
+
<label v-if="label" :for="inputId" class="dm-time-picker__label">
|
|
4
|
+
{{ label }}
|
|
5
|
+
<span v-if="required" class="dm-time-picker__required" aria-label="obrigatório">*</span>
|
|
6
|
+
</label>
|
|
7
|
+
|
|
8
|
+
<div class="dm-time-picker__wrapper">
|
|
9
|
+
<input
|
|
10
|
+
:id="inputId"
|
|
11
|
+
ref="inputRef"
|
|
12
|
+
v-model="displayValue"
|
|
13
|
+
type="time"
|
|
14
|
+
class="dm-time-picker__input"
|
|
15
|
+
:class="{
|
|
16
|
+
'dm-time-picker__input--error': hasError,
|
|
17
|
+
'dm-time-picker__input--success': hasSuccess
|
|
18
|
+
}"
|
|
19
|
+
:disabled="disabled"
|
|
20
|
+
:required="required"
|
|
21
|
+
:min="min"
|
|
22
|
+
:max="max"
|
|
23
|
+
:step="step"
|
|
24
|
+
:aria-describedby="ariaDescribedBy"
|
|
25
|
+
:aria-invalid="hasError"
|
|
26
|
+
@input="handleInput"
|
|
27
|
+
@blur="handleBlur"
|
|
28
|
+
@focus="handleFocus"
|
|
29
|
+
/>
|
|
30
|
+
|
|
31
|
+
<div v-if="hasError || hasSuccess" class="dm-time-picker__icon">
|
|
32
|
+
<svg v-if="hasError" class="dm-time-picker__icon--error" viewBox="0 0 20 20" fill="currentColor">
|
|
33
|
+
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
|
34
|
+
</svg>
|
|
35
|
+
<svg v-else-if="hasSuccess" class="dm-time-picker__icon--success" viewBox="0 0 20 20" fill="currentColor">
|
|
36
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
|
37
|
+
</svg>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div v-if="errorMessage || successMessage || helperText" class="dm-time-picker__messages">
|
|
42
|
+
<p v-if="errorMessage" :id="`${inputId}-error`" class="dm-time-picker__error" role="alert">
|
|
43
|
+
{{ errorMessage }}
|
|
44
|
+
</p>
|
|
45
|
+
<p v-else-if="successMessage" :id="`${inputId}-success`" class="dm-time-picker__success">
|
|
46
|
+
{{ successMessage }}
|
|
47
|
+
</p>
|
|
48
|
+
<p v-else-if="helperText" :id="`${inputId}-helper`" class="dm-time-picker__helper">
|
|
49
|
+
{{ helperText }}
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<script setup lang="ts">
|
|
56
|
+
import { ref, computed, watch, nextTick } from 'vue'
|
|
57
|
+
|
|
58
|
+
interface Props {
|
|
59
|
+
modelValue?: string
|
|
60
|
+
label?: string
|
|
61
|
+
placeholder?: string
|
|
62
|
+
disabled?: boolean
|
|
63
|
+
required?: boolean
|
|
64
|
+
errorMessage?: string
|
|
65
|
+
successMessage?: string
|
|
66
|
+
helperText?: string
|
|
67
|
+
min?: string
|
|
68
|
+
max?: string
|
|
69
|
+
step?: number
|
|
70
|
+
format24h?: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface Emits {
|
|
74
|
+
(e: 'update:modelValue', value: string): void
|
|
75
|
+
(e: 'blur', event: FocusEvent): void
|
|
76
|
+
(e: 'focus', event: FocusEvent): void
|
|
77
|
+
(e: 'change', value: string): void
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
81
|
+
modelValue: '',
|
|
82
|
+
step: 60,
|
|
83
|
+
format24h: true
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const emit = defineEmits<Emits>()
|
|
87
|
+
|
|
88
|
+
// Refs
|
|
89
|
+
const inputRef = ref<HTMLInputElement>()
|
|
90
|
+
|
|
91
|
+
// Computed
|
|
92
|
+
const inputId = computed(() => `dm-time-picker-${Math.random().toString(36).substr(2, 9)}`)
|
|
93
|
+
|
|
94
|
+
const hasError = computed(() => !!props.errorMessage)
|
|
95
|
+
const hasSuccess = computed(() => !!props.successMessage && !hasError.value)
|
|
96
|
+
|
|
97
|
+
const ariaDescribedBy = computed(() => {
|
|
98
|
+
const ids = []
|
|
99
|
+
if (props.errorMessage) ids.push(`${inputId.value}-error`)
|
|
100
|
+
else if (props.successMessage) ids.push(`${inputId.value}-success`)
|
|
101
|
+
else if (props.helperText) ids.push(`${inputId.value}-helper`)
|
|
102
|
+
return ids.length > 0 ? ids.join(' ') : undefined
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const displayValue = computed({
|
|
106
|
+
get: () => props.modelValue,
|
|
107
|
+
set: (value: string) => {
|
|
108
|
+
emit('update:modelValue', value)
|
|
109
|
+
emit('change', value)
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Methods
|
|
114
|
+
const handleInput = (event: Event) => {
|
|
115
|
+
const target = event.target as HTMLInputElement
|
|
116
|
+
displayValue.value = target.value
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const handleBlur = (event: FocusEvent) => {
|
|
120
|
+
emit('blur', event)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const handleFocus = (event: FocusEvent) => {
|
|
124
|
+
emit('focus', event)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const focus = () => {
|
|
128
|
+
nextTick(() => {
|
|
129
|
+
inputRef.value?.focus()
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const blur = () => {
|
|
134
|
+
inputRef.value?.blur()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Watch for external changes
|
|
138
|
+
watch(() => props.modelValue, (newValue) => {
|
|
139
|
+
if (inputRef.value && inputRef.value.value !== newValue) {
|
|
140
|
+
inputRef.value.value = newValue
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// Expose methods
|
|
145
|
+
defineExpose({
|
|
146
|
+
focus,
|
|
147
|
+
blur,
|
|
148
|
+
inputRef
|
|
149
|
+
})
|
|
150
|
+
</script>
|
|
151
|
+
|
|
152
|
+
<style scoped>
|
|
153
|
+
.dm-time-picker {
|
|
154
|
+
@apply w-full;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.dm-time-picker--disabled {
|
|
158
|
+
@apply opacity-60 cursor-not-allowed;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.dm-time-picker__label {
|
|
162
|
+
@apply block text-sm font-medium text-gray-700 mb-1;
|
|
163
|
+
color: var(--dm-gray-700);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
[data-theme="dark"] .dm-time-picker__label {
|
|
167
|
+
color: var(--dm-text-secondary);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.dm-time-picker__required {
|
|
171
|
+
@apply text-red-500 ml-1;
|
|
172
|
+
color: var(--dm-error);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.dm-time-picker__wrapper {
|
|
176
|
+
@apply relative;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.dm-time-picker__input {
|
|
180
|
+
@apply w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm;
|
|
181
|
+
@apply focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500;
|
|
182
|
+
@apply disabled:bg-gray-50 disabled:cursor-not-allowed;
|
|
183
|
+
@apply transition-colors duration-200;
|
|
184
|
+
|
|
185
|
+
border-color: var(--dm-gray-300, #d1d5db);
|
|
186
|
+
background-color: var(--dm-bg-primary, #ffffff);
|
|
187
|
+
color: var(--dm-text-primary);
|
|
188
|
+
border-radius: var(--dm-radius);
|
|
189
|
+
transition: var(--dm-transition);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.dm-time-picker__input:focus {
|
|
193
|
+
box-shadow: var(--dm-focus-ring);
|
|
194
|
+
border-color: var(--dm-primary);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.dm-time-picker__input--error {
|
|
198
|
+
@apply border-red-500 focus:ring-red-500 focus:border-red-500;
|
|
199
|
+
border-color: var(--dm-error);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.dm-time-picker__input--error:focus {
|
|
203
|
+
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.dm-time-picker__input--success {
|
|
207
|
+
@apply border-green-500 focus:ring-green-500 focus:border-green-500;
|
|
208
|
+
border-color: var(--dm-success);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.dm-time-picker__input--success:focus {
|
|
212
|
+
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
[data-theme="dark"] .dm-time-picker__input {
|
|
216
|
+
background-color: var(--dm-bg-secondary);
|
|
217
|
+
border-color: var(--dm-gray-600, #4b5563);
|
|
218
|
+
color: var(--dm-text-primary);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
[data-theme="dark"] .dm-time-picker__input:disabled {
|
|
222
|
+
background-color: var(--dm-gray-700, #374151);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.dm-time-picker__icon {
|
|
226
|
+
@apply absolute right-3 top-1/2 transform -translate-y-1/2 pointer-events-none;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.dm-time-picker__icon--error {
|
|
230
|
+
@apply w-5 h-5 text-red-500;
|
|
231
|
+
color: var(--dm-error);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.dm-time-picker__icon--success {
|
|
235
|
+
@apply w-5 h-5 text-green-500;
|
|
236
|
+
color: var(--dm-success);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.dm-time-picker__messages {
|
|
240
|
+
@apply mt-1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.dm-time-picker__error {
|
|
244
|
+
@apply text-sm text-red-600;
|
|
245
|
+
color: var(--dm-error);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.dm-time-picker__success {
|
|
249
|
+
@apply text-sm text-green-600;
|
|
250
|
+
color: var(--dm-success);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.dm-time-picker__helper {
|
|
254
|
+
@apply text-sm text-gray-500;
|
|
255
|
+
color: var(--dm-gray-500, #6b7280);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
[data-theme="dark"] .dm-time-picker__helper {
|
|
259
|
+
color: var(--dm-text-secondary);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* Responsive adjustments */
|
|
263
|
+
@media (max-width: 640px) {
|
|
264
|
+
.dm-time-picker__input {
|
|
265
|
+
@apply text-base; /* Prevent zoom on iOS */
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* High contrast mode support */
|
|
270
|
+
@media (prefers-contrast: high) {
|
|
271
|
+
.dm-time-picker__input {
|
|
272
|
+
@apply border-2;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.dm-time-picker__input:focus {
|
|
276
|
+
@apply border-4;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* Reduced motion support */
|
|
281
|
+
@media (prefers-reduced-motion: reduce) {
|
|
282
|
+
.dm-time-picker__input {
|
|
283
|
+
@apply transition-none;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
</style>
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="triggerRef"
|
|
4
|
+
class="dm-tooltip-trigger"
|
|
5
|
+
@mouseenter="handleMouseEnter"
|
|
6
|
+
@mouseleave="handleMouseLeave"
|
|
7
|
+
@focus="handleFocus"
|
|
8
|
+
@blur="handleBlur"
|
|
9
|
+
@click="handleClick"
|
|
10
|
+
>
|
|
11
|
+
<slot />
|
|
12
|
+
|
|
13
|
+
<Teleport to="body">
|
|
14
|
+
<Transition
|
|
15
|
+
name="tooltip"
|
|
16
|
+
@enter="onEnter"
|
|
17
|
+
@leave="onLeave"
|
|
18
|
+
>
|
|
19
|
+
<div
|
|
20
|
+
v-if="isVisible"
|
|
21
|
+
ref="tooltipRef"
|
|
22
|
+
:id="tooltipId"
|
|
23
|
+
class="dm-tooltip"
|
|
24
|
+
:class="[
|
|
25
|
+
`dm-tooltip--${placement}`,
|
|
26
|
+
`dm-tooltip--${variant}`,
|
|
27
|
+
{ 'dm-tooltip--arrow': showArrow }
|
|
28
|
+
]"
|
|
29
|
+
:style="tooltipStyle"
|
|
30
|
+
role="tooltip"
|
|
31
|
+
:aria-hidden="!isVisible"
|
|
32
|
+
>
|
|
33
|
+
<div class="dm-tooltip__content">
|
|
34
|
+
<slot name="content">
|
|
35
|
+
{{ content }}
|
|
36
|
+
</slot>
|
|
37
|
+
</div>
|
|
38
|
+
<div v-if="showArrow" class="dm-tooltip__arrow"></div>
|
|
39
|
+
</div>
|
|
40
|
+
</Transition>
|
|
41
|
+
</Teleport>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script setup lang="ts">
|
|
46
|
+
import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue'
|
|
47
|
+
|
|
48
|
+
type Placement = 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end'
|
|
49
|
+
type Variant = 'dark' | 'light' | 'primary' | 'error' | 'warning' | 'success'
|
|
50
|
+
type Trigger = 'hover' | 'click' | 'focus' | 'manual'
|
|
51
|
+
|
|
52
|
+
interface Props {
|
|
53
|
+
content?: string
|
|
54
|
+
placement?: Placement
|
|
55
|
+
variant?: Variant
|
|
56
|
+
trigger?: Trigger
|
|
57
|
+
disabled?: boolean
|
|
58
|
+
showArrow?: boolean
|
|
59
|
+
delay?: number
|
|
60
|
+
hideDelay?: number
|
|
61
|
+
offset?: number
|
|
62
|
+
maxWidth?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface Emits {
|
|
66
|
+
(e: 'show'): void
|
|
67
|
+
(e: 'hide'): void
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
71
|
+
placement: 'top',
|
|
72
|
+
variant: 'dark',
|
|
73
|
+
trigger: 'hover',
|
|
74
|
+
showArrow: true,
|
|
75
|
+
delay: 100,
|
|
76
|
+
hideDelay: 100,
|
|
77
|
+
offset: 8,
|
|
78
|
+
maxWidth: '200px'
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const emit = defineEmits<Emits>()
|
|
82
|
+
|
|
83
|
+
// Refs
|
|
84
|
+
const triggerRef = ref<HTMLElement>()
|
|
85
|
+
const tooltipRef = ref<HTMLElement>()
|
|
86
|
+
const isVisible = ref(false)
|
|
87
|
+
const showTimeout = ref<number>()
|
|
88
|
+
const hideTimeout = ref<number>()
|
|
89
|
+
|
|
90
|
+
// Computed
|
|
91
|
+
const tooltipId = computed(() => `dm-tooltip-${Math.random().toString(36).substr(2, 9)}`)
|
|
92
|
+
|
|
93
|
+
const tooltipStyle = ref<Record<string, string>>({})
|
|
94
|
+
|
|
95
|
+
// Methods
|
|
96
|
+
const calculatePosition = async () => {
|
|
97
|
+
if (!triggerRef.value || !tooltipRef.value) return
|
|
98
|
+
|
|
99
|
+
await nextTick()
|
|
100
|
+
|
|
101
|
+
const trigger = triggerRef.value.getBoundingClientRect()
|
|
102
|
+
const tooltip = tooltipRef.value.getBoundingClientRect()
|
|
103
|
+
const viewport = {
|
|
104
|
+
width: window.innerWidth,
|
|
105
|
+
height: window.innerHeight
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let top = 0
|
|
109
|
+
let left = 0
|
|
110
|
+
|
|
111
|
+
// Calculate base position
|
|
112
|
+
switch (props.placement) {
|
|
113
|
+
case 'top':
|
|
114
|
+
case 'top-start':
|
|
115
|
+
case 'top-end':
|
|
116
|
+
top = trigger.top - tooltip.height - props.offset
|
|
117
|
+
break
|
|
118
|
+
case 'bottom':
|
|
119
|
+
case 'bottom-start':
|
|
120
|
+
case 'bottom-end':
|
|
121
|
+
top = trigger.bottom + props.offset
|
|
122
|
+
break
|
|
123
|
+
case 'left':
|
|
124
|
+
top = trigger.top + (trigger.height - tooltip.height) / 2
|
|
125
|
+
left = trigger.left - tooltip.width - props.offset
|
|
126
|
+
break
|
|
127
|
+
case 'right':
|
|
128
|
+
top = trigger.top + (trigger.height - tooltip.height) / 2
|
|
129
|
+
left = trigger.right + props.offset
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Calculate horizontal position for top/bottom placements
|
|
134
|
+
if (props.placement.startsWith('top') || props.placement.startsWith('bottom')) {
|
|
135
|
+
switch (props.placement) {
|
|
136
|
+
case 'top':
|
|
137
|
+
case 'bottom':
|
|
138
|
+
left = trigger.left + (trigger.width - tooltip.width) / 2
|
|
139
|
+
break
|
|
140
|
+
case 'top-start':
|
|
141
|
+
case 'bottom-start':
|
|
142
|
+
left = trigger.left
|
|
143
|
+
break
|
|
144
|
+
case 'top-end':
|
|
145
|
+
case 'bottom-end':
|
|
146
|
+
left = trigger.right - tooltip.width
|
|
147
|
+
break
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Viewport boundary checks
|
|
152
|
+
if (left < 0) {
|
|
153
|
+
left = 8
|
|
154
|
+
} else if (left + tooltip.width > viewport.width) {
|
|
155
|
+
left = viewport.width - tooltip.width - 8
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (top < 0) {
|
|
159
|
+
top = 8
|
|
160
|
+
} else if (top + tooltip.height > viewport.height) {
|
|
161
|
+
top = viewport.height - tooltip.height - 8
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
tooltipStyle.value = {
|
|
165
|
+
position: 'fixed',
|
|
166
|
+
top: `${top}px`,
|
|
167
|
+
left: `${left}px`,
|
|
168
|
+
maxWidth: props.maxWidth,
|
|
169
|
+
zIndex: '9999'
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const show = () => {
|
|
174
|
+
if (props.disabled || isVisible.value) return
|
|
175
|
+
|
|
176
|
+
clearTimeout(hideTimeout.value)
|
|
177
|
+
|
|
178
|
+
showTimeout.value = window.setTimeout(async () => {
|
|
179
|
+
isVisible.value = true
|
|
180
|
+
emit('show')
|
|
181
|
+
await calculatePosition()
|
|
182
|
+
}, props.delay)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const hide = () => {
|
|
186
|
+
clearTimeout(showTimeout.value)
|
|
187
|
+
|
|
188
|
+
hideTimeout.value = window.setTimeout(() => {
|
|
189
|
+
isVisible.value = false
|
|
190
|
+
emit('hide')
|
|
191
|
+
}, props.hideDelay)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const handleMouseEnter = () => {
|
|
195
|
+
if (props.trigger === 'hover') {
|
|
196
|
+
show()
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const handleMouseLeave = () => {
|
|
201
|
+
if (props.trigger === 'hover') {
|
|
202
|
+
hide()
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const handleFocus = () => {
|
|
207
|
+
if (props.trigger === 'focus') {
|
|
208
|
+
show()
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const handleBlur = () => {
|
|
213
|
+
if (props.trigger === 'focus') {
|
|
214
|
+
hide()
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const handleClick = () => {
|
|
219
|
+
if (props.trigger === 'click') {
|
|
220
|
+
if (isVisible.value) {
|
|
221
|
+
hide()
|
|
222
|
+
} else {
|
|
223
|
+
show()
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const onEnter = () => {
|
|
229
|
+
calculatePosition()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const onLeave = () => {
|
|
233
|
+
tooltipStyle.value = {}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Handle window resize
|
|
237
|
+
const handleResize = () => {
|
|
238
|
+
if (isVisible.value) {
|
|
239
|
+
calculatePosition()
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Handle scroll
|
|
244
|
+
const handleScroll = () => {
|
|
245
|
+
if (isVisible.value) {
|
|
246
|
+
calculatePosition()
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Lifecycle
|
|
251
|
+
onMounted(() => {
|
|
252
|
+
window.addEventListener('resize', handleResize)
|
|
253
|
+
window.addEventListener('scroll', handleScroll, true)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
onUnmounted(() => {
|
|
257
|
+
clearTimeout(showTimeout.value)
|
|
258
|
+
clearTimeout(hideTimeout.value)
|
|
259
|
+
window.removeEventListener('resize', handleResize)
|
|
260
|
+
window.removeEventListener('scroll', handleScroll, true)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// Expose methods
|
|
264
|
+
defineExpose({
|
|
265
|
+
show,
|
|
266
|
+
hide,
|
|
267
|
+
isVisible: computed(() => isVisible.value)
|
|
268
|
+
})
|
|
269
|
+
</script>
|
|
270
|
+
|
|
271
|
+
<style scoped>
|
|
272
|
+
.dm-tooltip-trigger {
|
|
273
|
+
@apply inline-block;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.dm-tooltip {
|
|
277
|
+
@apply absolute z-50 px-2 py-1 text-sm rounded shadow-lg pointer-events-none;
|
|
278
|
+
border-radius: var(--dm-radius);
|
|
279
|
+
font-size: var(--dm-text-sm);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* Variants */
|
|
283
|
+
.dm-tooltip--dark {
|
|
284
|
+
@apply bg-gray-900 text-white;
|
|
285
|
+
background-color: var(--dm-gray-900);
|
|
286
|
+
color: var(--dm-bg-primary, #ffffff);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.dm-tooltip--light {
|
|
290
|
+
@apply bg-white text-gray-900 border border-gray-200;
|
|
291
|
+
background-color: var(--dm-bg-primary, #ffffff);
|
|
292
|
+
color: var(--dm-text-primary);
|
|
293
|
+
border-color: var(--dm-gray-200, #e5e7eb);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.dm-tooltip--primary {
|
|
297
|
+
@apply text-white;
|
|
298
|
+
background: var(--gradient-primary);
|
|
299
|
+
color: var(--dm-bg-primary, #ffffff);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.dm-tooltip--error {
|
|
303
|
+
@apply text-white;
|
|
304
|
+
background-color: var(--dm-error);
|
|
305
|
+
color: var(--dm-bg-primary, #ffffff);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.dm-tooltip--warning {
|
|
309
|
+
@apply text-white;
|
|
310
|
+
background-color: var(--dm-warning);
|
|
311
|
+
color: var(--dm-bg-primary, #ffffff);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.dm-tooltip--success {
|
|
315
|
+
@apply text-white;
|
|
316
|
+
background-color: var(--dm-success);
|
|
317
|
+
color: var(--dm-bg-primary, #ffffff);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* Dark mode adjustments */
|
|
321
|
+
[data-theme="dark"] .dm-tooltip--dark {
|
|
322
|
+
background-color: var(--dm-gray-800, #1f2937);
|
|
323
|
+
color: var(--dm-text-primary);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
[data-theme="dark"] .dm-tooltip--light {
|
|
327
|
+
background-color: var(--dm-bg-secondary);
|
|
328
|
+
color: var(--dm-text-primary);
|
|
329
|
+
border-color: var(--dm-gray-600, #4b5563);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.dm-tooltip__content {
|
|
333
|
+
@apply relative z-10;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/* Arrow styles */
|
|
337
|
+
.dm-tooltip--arrow .dm-tooltip__arrow {
|
|
338
|
+
@apply absolute w-2 h-2 transform rotate-45;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.dm-tooltip--top .dm-tooltip__arrow {
|
|
342
|
+
@apply bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.dm-tooltip--bottom .dm-tooltip__arrow {
|
|
346
|
+
@apply top-0 left-1/2 -translate-x-1/2 -translate-y-1/2;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.dm-tooltip--left .dm-tooltip__arrow {
|
|
350
|
+
@apply right-0 top-1/2 translate-x-1/2 -translate-y-1/2;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.dm-tooltip--right .dm-tooltip__arrow {
|
|
354
|
+
@apply left-0 top-1/2 -translate-x-1/2 -translate-y-1/2;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* Arrow colors */
|
|
358
|
+
.dm-tooltip--dark .dm-tooltip__arrow {
|
|
359
|
+
background-color: var(--dm-gray-900);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.dm-tooltip--light .dm-tooltip__arrow {
|
|
363
|
+
background-color: var(--dm-bg-primary, #ffffff);
|
|
364
|
+
border: 1px solid var(--dm-gray-200, #e5e7eb);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.dm-tooltip--primary .dm-tooltip__arrow {
|
|
368
|
+
background-color: var(--dm-primary);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.dm-tooltip--error .dm-tooltip__arrow {
|
|
372
|
+
background-color: var(--dm-error);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.dm-tooltip--warning .dm-tooltip__arrow {
|
|
376
|
+
background-color: var(--dm-warning);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.dm-tooltip--success .dm-tooltip__arrow {
|
|
380
|
+
background-color: var(--dm-success);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* Transitions */
|
|
384
|
+
.tooltip-enter-active,
|
|
385
|
+
.tooltip-leave-active {
|
|
386
|
+
transition: opacity var(--transition-fast), transform var(--transition-fast);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.tooltip-enter-from,
|
|
390
|
+
.tooltip-leave-to {
|
|
391
|
+
opacity: 0;
|
|
392
|
+
transform: scale(0.95);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* Reduced motion support */
|
|
396
|
+
@media (prefers-reduced-motion: reduce) {
|
|
397
|
+
.tooltip-enter-active,
|
|
398
|
+
.tooltip-leave-active {
|
|
399
|
+
transition: none;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* High contrast mode support */
|
|
404
|
+
@media (prefers-contrast: high) {
|
|
405
|
+
.dm-tooltip {
|
|
406
|
+
@apply border-2 border-current;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
</style>
|