@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,620 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="dm-menu" :class="{ 'dm-menu--disabled': disabled }">
|
|
3
|
+
<div
|
|
4
|
+
ref="triggerRef"
|
|
5
|
+
class="dm-menu__trigger"
|
|
6
|
+
:aria-expanded="isOpen"
|
|
7
|
+
:aria-haspopup="true"
|
|
8
|
+
:aria-controls="menuId"
|
|
9
|
+
@click="handleTriggerClick"
|
|
10
|
+
@keydown="handleTriggerKeydown"
|
|
11
|
+
>
|
|
12
|
+
<slot name="trigger" :isOpen="isOpen" :toggle="toggle">
|
|
13
|
+
<button
|
|
14
|
+
type="button"
|
|
15
|
+
class="dm-menu__button"
|
|
16
|
+
:disabled="disabled"
|
|
17
|
+
>
|
|
18
|
+
{{ triggerText }}
|
|
19
|
+
<svg
|
|
20
|
+
class="dm-menu__chevron"
|
|
21
|
+
:class="{ 'dm-menu__chevron--open': isOpen }"
|
|
22
|
+
viewBox="0 0 20 20"
|
|
23
|
+
fill="currentColor"
|
|
24
|
+
>
|
|
25
|
+
<path
|
|
26
|
+
fill-rule="evenodd"
|
|
27
|
+
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
|
28
|
+
clip-rule="evenodd"
|
|
29
|
+
/>
|
|
30
|
+
</svg>
|
|
31
|
+
</button>
|
|
32
|
+
</slot>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<Teleport to="body">
|
|
36
|
+
<Transition
|
|
37
|
+
name="menu"
|
|
38
|
+
@enter="onEnter"
|
|
39
|
+
@leave="onLeave"
|
|
40
|
+
>
|
|
41
|
+
<div
|
|
42
|
+
v-if="isOpen"
|
|
43
|
+
ref="menuRef"
|
|
44
|
+
:id="menuId"
|
|
45
|
+
class="dm-menu__dropdown"
|
|
46
|
+
:class="[
|
|
47
|
+
`dm-menu__dropdown--${placement}`,
|
|
48
|
+
{ 'dm-menu__dropdown--full-width': fullWidth }
|
|
49
|
+
]"
|
|
50
|
+
:style="menuStyle"
|
|
51
|
+
role="menu"
|
|
52
|
+
:aria-labelledby="triggerId"
|
|
53
|
+
@keydown="handleMenuKeydown"
|
|
54
|
+
@click="handleMenuClick"
|
|
55
|
+
>
|
|
56
|
+
<div class="dm-menu__content">
|
|
57
|
+
<slot :close="close" :focusedIndex="focusedIndex">
|
|
58
|
+
<div
|
|
59
|
+
v-for="(item, index) in items"
|
|
60
|
+
:key="item.key || index"
|
|
61
|
+
class="dm-menu__item"
|
|
62
|
+
:class="{
|
|
63
|
+
'dm-menu__item--focused': focusedIndex === index,
|
|
64
|
+
'dm-menu__item--disabled': item.disabled,
|
|
65
|
+
'dm-menu__item--divider': item.divider
|
|
66
|
+
}"
|
|
67
|
+
role="menuitem"
|
|
68
|
+
:tabindex="item.disabled ? -1 : 0"
|
|
69
|
+
:aria-disabled="item.disabled"
|
|
70
|
+
@click="handleItemClick(item, index)"
|
|
71
|
+
@mouseenter="focusedIndex = index"
|
|
72
|
+
>
|
|
73
|
+
<div v-if="item.divider" class="dm-menu__divider"></div>
|
|
74
|
+
<template v-else>
|
|
75
|
+
<div v-if="item.icon" class="dm-menu__item-icon">
|
|
76
|
+
<component :is="item.icon" />
|
|
77
|
+
</div>
|
|
78
|
+
<div class="dm-menu__item-content">
|
|
79
|
+
<div class="dm-menu__item-label">{{ item.label }}</div>
|
|
80
|
+
<div v-if="item.description" class="dm-menu__item-description">
|
|
81
|
+
{{ item.description }}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<div v-if="item.shortcut" class="dm-menu__item-shortcut">
|
|
85
|
+
{{ item.shortcut }}
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
</div>
|
|
89
|
+
</slot>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</Transition>
|
|
93
|
+
</Teleport>
|
|
94
|
+
|
|
95
|
+
<!-- Backdrop for mobile -->
|
|
96
|
+
<Teleport to="body">
|
|
97
|
+
<Transition name="backdrop">
|
|
98
|
+
<div
|
|
99
|
+
v-if="isOpen && showBackdrop"
|
|
100
|
+
class="dm-menu__backdrop"
|
|
101
|
+
@click="close"
|
|
102
|
+
></div>
|
|
103
|
+
</Transition>
|
|
104
|
+
</Teleport>
|
|
105
|
+
</div>
|
|
106
|
+
</template>
|
|
107
|
+
|
|
108
|
+
<script setup lang="ts">
|
|
109
|
+
import { ref, computed, nextTick, onMounted, onUnmounted, watch } from 'vue'
|
|
110
|
+
|
|
111
|
+
interface MenuItem {
|
|
112
|
+
key?: string
|
|
113
|
+
label?: string
|
|
114
|
+
description?: string
|
|
115
|
+
icon?: any
|
|
116
|
+
shortcut?: string
|
|
117
|
+
disabled?: boolean
|
|
118
|
+
divider?: boolean
|
|
119
|
+
action?: () => void
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type Placement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end' | 'left' | 'right'
|
|
123
|
+
|
|
124
|
+
interface Props {
|
|
125
|
+
items?: MenuItem[]
|
|
126
|
+
triggerText?: string
|
|
127
|
+
placement?: Placement
|
|
128
|
+
disabled?: boolean
|
|
129
|
+
fullWidth?: boolean
|
|
130
|
+
showBackdrop?: boolean
|
|
131
|
+
closeOnItemClick?: boolean
|
|
132
|
+
offset?: number
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface Emits {
|
|
136
|
+
(e: 'open'): void
|
|
137
|
+
(e: 'close'): void
|
|
138
|
+
(e: 'item-click', item: MenuItem, index: number): void
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
142
|
+
items: () => [],
|
|
143
|
+
triggerText: 'Menu',
|
|
144
|
+
placement: 'bottom-start',
|
|
145
|
+
showBackdrop: false,
|
|
146
|
+
closeOnItemClick: true,
|
|
147
|
+
offset: 4
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const emit = defineEmits<Emits>()
|
|
151
|
+
|
|
152
|
+
// Refs
|
|
153
|
+
const triggerRef = ref<HTMLElement>()
|
|
154
|
+
const menuRef = ref<HTMLElement>()
|
|
155
|
+
const isOpen = ref(false)
|
|
156
|
+
const focusedIndex = ref(-1)
|
|
157
|
+
|
|
158
|
+
// Computed
|
|
159
|
+
const menuId = computed(() => `dm-menu-${Math.random().toString(36).substr(2, 9)}`)
|
|
160
|
+
const triggerId = computed(() => `dm-menu-trigger-${Math.random().toString(36).substr(2, 9)}`)
|
|
161
|
+
|
|
162
|
+
const menuStyle = ref<Record<string, string>>({})
|
|
163
|
+
|
|
164
|
+
// const enabledItems = computed(() =>
|
|
165
|
+
// props.items.filter(item => !item.disabled && !item.divider)
|
|
166
|
+
// )
|
|
167
|
+
|
|
168
|
+
// Methods
|
|
169
|
+
const calculatePosition = async () => {
|
|
170
|
+
if (!triggerRef.value || !menuRef.value) return
|
|
171
|
+
|
|
172
|
+
await nextTick()
|
|
173
|
+
|
|
174
|
+
const trigger = triggerRef.value.getBoundingClientRect()
|
|
175
|
+
const menu = menuRef.value.getBoundingClientRect()
|
|
176
|
+
const viewport = {
|
|
177
|
+
width: window.innerWidth,
|
|
178
|
+
height: window.innerHeight
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let top = 0
|
|
182
|
+
let left = 0
|
|
183
|
+
|
|
184
|
+
// Calculate base position
|
|
185
|
+
switch (props.placement) {
|
|
186
|
+
case 'bottom-start':
|
|
187
|
+
top = trigger.bottom + props.offset
|
|
188
|
+
left = trigger.left
|
|
189
|
+
break
|
|
190
|
+
case 'bottom-end':
|
|
191
|
+
top = trigger.bottom + props.offset
|
|
192
|
+
left = trigger.right - menu.width
|
|
193
|
+
break
|
|
194
|
+
case 'top-start':
|
|
195
|
+
top = trigger.top - menu.height - props.offset
|
|
196
|
+
left = trigger.left
|
|
197
|
+
break
|
|
198
|
+
case 'top-end':
|
|
199
|
+
top = trigger.top - menu.height - props.offset
|
|
200
|
+
left = trigger.right - menu.width
|
|
201
|
+
break
|
|
202
|
+
case 'left':
|
|
203
|
+
top = trigger.top
|
|
204
|
+
left = trigger.left - menu.width - props.offset
|
|
205
|
+
break
|
|
206
|
+
case 'right':
|
|
207
|
+
top = trigger.top
|
|
208
|
+
left = trigger.right + props.offset
|
|
209
|
+
break
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Viewport boundary checks
|
|
213
|
+
if (left < 8) {
|
|
214
|
+
left = 8
|
|
215
|
+
} else if (left + menu.width > viewport.width - 8) {
|
|
216
|
+
left = viewport.width - menu.width - 8
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (top < 8) {
|
|
220
|
+
top = 8
|
|
221
|
+
} else if (top + menu.height > viewport.height - 8) {
|
|
222
|
+
top = viewport.height - menu.height - 8
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const width = props.fullWidth ? `${trigger.width}px` : 'auto'
|
|
226
|
+
|
|
227
|
+
menuStyle.value = {
|
|
228
|
+
position: 'fixed',
|
|
229
|
+
top: `${top}px`,
|
|
230
|
+
left: `${left}px`,
|
|
231
|
+
width,
|
|
232
|
+
zIndex: '9999'
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const open = async () => {
|
|
237
|
+
if (props.disabled || isOpen.value) return
|
|
238
|
+
|
|
239
|
+
isOpen.value = true
|
|
240
|
+
focusedIndex.value = -1
|
|
241
|
+
emit('open')
|
|
242
|
+
|
|
243
|
+
await calculatePosition()
|
|
244
|
+
|
|
245
|
+
// Focus first enabled item
|
|
246
|
+
nextTick(() => {
|
|
247
|
+
const firstEnabledIndex = props.items.findIndex(item => !item.disabled && !item.divider)
|
|
248
|
+
if (firstEnabledIndex !== -1) {
|
|
249
|
+
focusedIndex.value = firstEnabledIndex
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const close = () => {
|
|
255
|
+
if (!isOpen.value) return
|
|
256
|
+
|
|
257
|
+
isOpen.value = false
|
|
258
|
+
focusedIndex.value = -1
|
|
259
|
+
emit('close')
|
|
260
|
+
|
|
261
|
+
// Return focus to trigger
|
|
262
|
+
nextTick(() => {
|
|
263
|
+
triggerRef.value?.focus()
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const toggle = () => {
|
|
268
|
+
if (isOpen.value) {
|
|
269
|
+
close()
|
|
270
|
+
} else {
|
|
271
|
+
open()
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const handleTriggerClick = () => {
|
|
276
|
+
toggle()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const handleTriggerKeydown = (event: KeyboardEvent) => {
|
|
280
|
+
switch (event.key) {
|
|
281
|
+
case 'Enter':
|
|
282
|
+
case ' ':
|
|
283
|
+
case 'ArrowDown':
|
|
284
|
+
event.preventDefault()
|
|
285
|
+
open()
|
|
286
|
+
break
|
|
287
|
+
case 'ArrowUp':
|
|
288
|
+
event.preventDefault()
|
|
289
|
+
open()
|
|
290
|
+
// Focus last item
|
|
291
|
+
nextTick(() => {
|
|
292
|
+
const lastEnabledIndex = props.items.map((item, index) => ({ item, index }))
|
|
293
|
+
.filter(({ item }) => !item.disabled && !item.divider)
|
|
294
|
+
.pop()?.index ?? -1
|
|
295
|
+
if (lastEnabledIndex !== -1) {
|
|
296
|
+
focusedIndex.value = lastEnabledIndex
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
break
|
|
300
|
+
case 'Escape':
|
|
301
|
+
close()
|
|
302
|
+
break
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const handleMenuKeydown = (event: KeyboardEvent) => {
|
|
307
|
+
switch (event.key) {
|
|
308
|
+
case 'ArrowDown':
|
|
309
|
+
event.preventDefault()
|
|
310
|
+
focusNext()
|
|
311
|
+
break
|
|
312
|
+
case 'ArrowUp':
|
|
313
|
+
event.preventDefault()
|
|
314
|
+
focusPrevious()
|
|
315
|
+
break
|
|
316
|
+
case 'Enter':
|
|
317
|
+
case ' ':
|
|
318
|
+
event.preventDefault()
|
|
319
|
+
if (focusedIndex.value !== -1) {
|
|
320
|
+
const item = props.items[focusedIndex.value]
|
|
321
|
+
handleItemClick(item, focusedIndex.value)
|
|
322
|
+
}
|
|
323
|
+
break
|
|
324
|
+
case 'Escape':
|
|
325
|
+
event.preventDefault()
|
|
326
|
+
close()
|
|
327
|
+
break
|
|
328
|
+
case 'Tab':
|
|
329
|
+
close()
|
|
330
|
+
break
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const focusNext = () => {
|
|
335
|
+
const enabledIndices = props.items
|
|
336
|
+
.map((item, index) => ({ item, index }))
|
|
337
|
+
.filter(({ item }) => !item.disabled && !item.divider)
|
|
338
|
+
.map(({ index }) => index)
|
|
339
|
+
|
|
340
|
+
if (enabledIndices.length === 0) return
|
|
341
|
+
|
|
342
|
+
const currentIndex = enabledIndices.indexOf(focusedIndex.value)
|
|
343
|
+
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % enabledIndices.length
|
|
344
|
+
focusedIndex.value = enabledIndices[nextIndex]
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const focusPrevious = () => {
|
|
348
|
+
const enabledIndices = props.items
|
|
349
|
+
.map((item, index) => ({ item, index }))
|
|
350
|
+
.filter(({ item }) => !item.disabled && !item.divider)
|
|
351
|
+
.map(({ index }) => index)
|
|
352
|
+
|
|
353
|
+
if (enabledIndices.length === 0) return
|
|
354
|
+
|
|
355
|
+
const currentIndex = enabledIndices.indexOf(focusedIndex.value)
|
|
356
|
+
const prevIndex = currentIndex === -1 ? enabledIndices.length - 1 : (currentIndex - 1 + enabledIndices.length) % enabledIndices.length
|
|
357
|
+
focusedIndex.value = enabledIndices[prevIndex]
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const handleItemClick = (item: MenuItem, index: number) => {
|
|
361
|
+
if (item.disabled || item.divider) return
|
|
362
|
+
|
|
363
|
+
emit('item-click', item, index)
|
|
364
|
+
|
|
365
|
+
if (item.action) {
|
|
366
|
+
item.action()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (props.closeOnItemClick) {
|
|
370
|
+
close()
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const handleMenuClick = (event: Event) => {
|
|
375
|
+
event.stopPropagation()
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const onEnter = () => {
|
|
379
|
+
calculatePosition()
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const onLeave = () => {
|
|
383
|
+
menuStyle.value = {}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Handle clicks outside
|
|
387
|
+
const handleClickOutside = (event: Event) => {
|
|
388
|
+
if (!isOpen.value) return
|
|
389
|
+
|
|
390
|
+
const target = event.target as Node
|
|
391
|
+
if (
|
|
392
|
+
triggerRef.value?.contains(target) ||
|
|
393
|
+
menuRef.value?.contains(target)
|
|
394
|
+
) {
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
close()
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Handle window resize
|
|
402
|
+
const handleResize = () => {
|
|
403
|
+
if (isOpen.value) {
|
|
404
|
+
calculatePosition()
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Lifecycle
|
|
409
|
+
onMounted(() => {
|
|
410
|
+
document.addEventListener('click', handleClickOutside)
|
|
411
|
+
window.addEventListener('resize', handleResize)
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
onUnmounted(() => {
|
|
415
|
+
document.removeEventListener('click', handleClickOutside)
|
|
416
|
+
window.removeEventListener('resize', handleResize)
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
// Watch for items changes
|
|
420
|
+
watch(() => props.items, () => {
|
|
421
|
+
if (focusedIndex.value >= props.items.length) {
|
|
422
|
+
focusedIndex.value = -1
|
|
423
|
+
}
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
// Expose methods
|
|
427
|
+
defineExpose({
|
|
428
|
+
open,
|
|
429
|
+
close,
|
|
430
|
+
toggle,
|
|
431
|
+
isOpen: computed(() => isOpen.value)
|
|
432
|
+
})
|
|
433
|
+
</script>
|
|
434
|
+
|
|
435
|
+
<style scoped>
|
|
436
|
+
.dm-menu {
|
|
437
|
+
@apply relative inline-block;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.dm-menu--disabled {
|
|
441
|
+
@apply opacity-60 cursor-not-allowed;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.dm-menu__trigger {
|
|
445
|
+
@apply inline-block;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.dm-menu__button {
|
|
449
|
+
@apply inline-flex items-center justify-between px-4 py-2 text-sm font-medium;
|
|
450
|
+
@apply bg-white border border-gray-300 rounded-md shadow-sm;
|
|
451
|
+
@apply hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500;
|
|
452
|
+
@apply disabled:opacity-50 disabled:cursor-not-allowed;
|
|
453
|
+
@apply transition-colors duration-200;
|
|
454
|
+
|
|
455
|
+
background-color: var(--dm-bg-primary, #ffffff);
|
|
456
|
+
border-color: var(--dm-gray-300, #d1d5db);
|
|
457
|
+
color: var(--dm-text-primary);
|
|
458
|
+
border-radius: var(--dm-radius);
|
|
459
|
+
transition: var(--dm-transition);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.dm-menu__button:focus {
|
|
463
|
+
box-shadow: var(--dm-focus-ring);
|
|
464
|
+
border-color: var(--dm-primary);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
[data-theme="dark"] .dm-menu__button {
|
|
468
|
+
background-color: var(--dm-bg-secondary);
|
|
469
|
+
border-color: var(--dm-gray-600, #4b5563);
|
|
470
|
+
color: var(--dm-text-primary);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.dm-menu__chevron {
|
|
474
|
+
@apply w-4 h-4 ml-2 transition-transform duration-200;
|
|
475
|
+
transition: var(--dm-transition);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.dm-menu__chevron--open {
|
|
479
|
+
@apply transform rotate-180;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.dm-menu__dropdown {
|
|
483
|
+
@apply bg-white border border-gray-200 rounded-md shadow-lg;
|
|
484
|
+
@apply min-w-48 max-w-xs;
|
|
485
|
+
|
|
486
|
+
background-color: var(--dm-bg-primary, #ffffff);
|
|
487
|
+
border-color: var(--dm-gray-200, #e5e7eb);
|
|
488
|
+
border-radius: var(--dm-radius);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
[data-theme="dark"] .dm-menu__dropdown {
|
|
492
|
+
background-color: var(--dm-bg-secondary);
|
|
493
|
+
border-color: var(--dm-gray-600, #4b5563);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.dm-menu__content {
|
|
497
|
+
@apply py-1;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.dm-menu__item {
|
|
501
|
+
@apply flex items-center px-4 py-2 text-sm cursor-pointer;
|
|
502
|
+
@apply hover:bg-gray-100 focus:bg-gray-100 focus:outline-none;
|
|
503
|
+
@apply transition-colors duration-150;
|
|
504
|
+
|
|
505
|
+
color: var(--dm-text-primary);
|
|
506
|
+
transition: var(--transition-fast);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.dm-menu__item--focused {
|
|
510
|
+
@apply bg-gray-100;
|
|
511
|
+
background-color: var(--dm-gray-100, #f3f4f6);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.dm-menu__item--disabled {
|
|
515
|
+
@apply opacity-50 cursor-not-allowed;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.dm-menu__item--disabled:hover,
|
|
519
|
+
.dm-menu__item--disabled:focus {
|
|
520
|
+
@apply bg-transparent;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
[data-theme="dark"] .dm-menu__item {
|
|
524
|
+
color: var(--dm-text-primary);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
[data-theme="dark"] .dm-menu__item:hover,
|
|
528
|
+
[data-theme="dark"] .dm-menu__item--focused {
|
|
529
|
+
background-color: var(--dm-gray-700, #374151);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.dm-menu__item--divider {
|
|
533
|
+
@apply p-0 cursor-default;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.dm-menu__divider {
|
|
537
|
+
@apply border-t border-gray-200 my-1;
|
|
538
|
+
border-color: var(--dm-gray-200, #e5e7eb);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
[data-theme="dark"] .dm-menu__divider {
|
|
542
|
+
border-color: var(--dm-gray-600, #4b5563);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.dm-menu__item-icon {
|
|
546
|
+
@apply w-4 h-4 mr-3 flex-shrink-0;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.dm-menu__item-content {
|
|
550
|
+
@apply flex-1 min-w-0;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.dm-menu__item-label {
|
|
554
|
+
@apply font-medium;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.dm-menu__item-description {
|
|
558
|
+
@apply text-xs text-gray-500 mt-0.5;
|
|
559
|
+
color: var(--dm-gray-500, #6b7280);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
[data-theme="dark"] .dm-menu__item-description {
|
|
563
|
+
color: var(--dm-text-secondary);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.dm-menu__item-shortcut {
|
|
567
|
+
@apply text-xs text-gray-400 ml-4 flex-shrink-0;
|
|
568
|
+
color: var(--dm-gray-400, #9ca3af);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
[data-theme="dark"] .dm-menu__item-shortcut {
|
|
572
|
+
color: var(--dm-gray-500, #6b7280);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.dm-menu__backdrop {
|
|
576
|
+
@apply fixed inset-0 bg-black bg-opacity-25 z-40;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/* Transitions */
|
|
580
|
+
.menu-enter-active,
|
|
581
|
+
.menu-leave-active {
|
|
582
|
+
transition: opacity var(--transition-fast), transform var(--transition-fast);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.menu-enter-from,
|
|
586
|
+
.menu-leave-to {
|
|
587
|
+
opacity: 0;
|
|
588
|
+
transform: scale(0.95) translateY(-10px);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.backdrop-enter-active,
|
|
592
|
+
.backdrop-leave-active {
|
|
593
|
+
transition: opacity var(--transition-fast);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.backdrop-enter-from,
|
|
597
|
+
.backdrop-leave-to {
|
|
598
|
+
opacity: 0;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/* Reduced motion support */
|
|
602
|
+
@media (prefers-reduced-motion: reduce) {
|
|
603
|
+
.dm-menu__chevron,
|
|
604
|
+
.dm-menu__item,
|
|
605
|
+
.menu-enter-active,
|
|
606
|
+
.menu-leave-active,
|
|
607
|
+
.backdrop-enter-active,
|
|
608
|
+
.backdrop-leave-active {
|
|
609
|
+
transition: none;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/* Mobile optimizations */
|
|
614
|
+
@media (max-width: 640px) {
|
|
615
|
+
.dm-menu__dropdown {
|
|
616
|
+
@apply max-w-none;
|
|
617
|
+
width: calc(100vw - 2rem);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
</style>
|