@datametria/vue-components 2.3.0 → 2.4.0

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 (128) hide show
  1. package/README.md +625 -594
  2. package/dist/index.es.js +1962 -1887
  3. package/dist/index.umd.js +6 -6
  4. package/dist/src/components/DatametriaForm.vue.d.ts +1 -1
  5. package/dist/src/components/DatametriaInput.vue.d.ts +9 -1
  6. package/dist/src/components/DatametriaSelect.vue.d.ts +10 -1
  7. package/dist/vue-components.css +1 -1
  8. package/package.json +103 -105
  9. package/src/components/DatametriaAlert.vue +151 -133
  10. package/src/components/DatametriaAutocomplete.vue +250 -229
  11. package/src/components/DatametriaAvatar.vue +256 -238
  12. package/src/components/DatametriaBadge.vue +101 -96
  13. package/src/components/DatametriaBreadcrumb.vue +132 -128
  14. package/src/components/DatametriaButton.vue +191 -173
  15. package/src/components/DatametriaCard.vue +84 -66
  16. package/src/components/DatametriaCheckbox.vue +197 -193
  17. package/src/components/DatametriaCheckboxGroup.vue +56 -38
  18. package/src/components/DatametriaChip.vue +159 -141
  19. package/src/components/DatametriaContainer.vue +70 -52
  20. package/src/components/DatametriaDataTable.vue +318 -300
  21. package/src/components/DatametriaDatePicker.vue +396 -378
  22. package/src/components/DatametriaDialog.vue +297 -293
  23. package/src/components/DatametriaDivider.vue +105 -98
  24. package/src/components/DatametriaDropdown.vue +356 -350
  25. package/src/components/DatametriaEmpty.vue +155 -151
  26. package/src/components/DatametriaFileUpload.vue +413 -395
  27. package/src/components/DatametriaFloatingBar.vue +144 -126
  28. package/src/components/DatametriaForm.vue +174 -156
  29. package/src/components/DatametriaFormItem.vue +183 -179
  30. package/src/components/DatametriaGrid.vue +55 -37
  31. package/src/components/DatametriaInput.vue +314 -263
  32. package/src/components/DatametriaMenu.vue +618 -600
  33. package/src/components/DatametriaModal.vue +147 -129
  34. package/src/components/DatametriaNavbar.vue +277 -223
  35. package/src/components/DatametriaPagination.vue +375 -371
  36. package/src/components/DatametriaPasswordInput.vue +444 -426
  37. package/src/components/DatametriaPopconfirm.vue +240 -234
  38. package/src/components/DatametriaProgress.vue +228 -224
  39. package/src/components/DatametriaRadio.vue +151 -147
  40. package/src/components/DatametriaRadioGroup.vue +55 -37
  41. package/src/components/DatametriaResult.vue +135 -131
  42. package/src/components/DatametriaSelect.vue +311 -211
  43. package/src/components/DatametriaSidebar.vue +294 -222
  44. package/src/components/DatametriaSkeleton.vue +257 -234
  45. package/src/components/DatametriaSlider.vue +409 -391
  46. package/src/components/DatametriaSortableTable.vue +820 -802
  47. package/src/components/DatametriaSpinner.vue +114 -110
  48. package/src/components/DatametriaSteps.vue +318 -312
  49. package/src/components/DatametriaSwitch.vue +146 -142
  50. package/src/components/DatametriaTabPane.vue +94 -76
  51. package/src/components/DatametriaTable.vue +118 -100
  52. package/src/components/DatametriaTabs.vue +315 -297
  53. package/src/components/DatametriaTextarea.vue +213 -195
  54. package/src/components/DatametriaTimePicker.vue +317 -299
  55. package/src/components/DatametriaToast.vue +176 -176
  56. package/src/components/DatametriaTooltip.vue +421 -400
  57. package/src/components/DatametriaTree.vue +126 -122
  58. package/src/components/DatametriaTreeNode.vue +176 -172
  59. package/src/components/DatametriaUpload.vue +379 -361
  60. package/src/components/__tests__/DatametriaAlert.test.js +35 -35
  61. package/src/components/__tests__/DatametriaAlert.test.ts +190 -190
  62. package/src/components/__tests__/DatametriaAvatar.test.ts +151 -151
  63. package/src/components/__tests__/DatametriaBadge.test.js +29 -29
  64. package/src/components/__tests__/DatametriaBadge.test.ts +167 -167
  65. package/src/components/__tests__/DatametriaBreadcrumb.test.ts +187 -0
  66. package/src/components/__tests__/DatametriaButton.test.js +30 -30
  67. package/src/components/__tests__/DatametriaButton.test.ts +283 -283
  68. package/src/components/__tests__/DatametriaCard.test.ts +201 -201
  69. package/src/components/__tests__/DatametriaCheckbox.test.ts +204 -0
  70. package/src/components/__tests__/DatametriaChip.test.js +38 -38
  71. package/src/components/__tests__/DatametriaContainer.test.ts +52 -52
  72. package/src/components/__tests__/DatametriaDialog.test.ts +338 -0
  73. package/src/components/__tests__/DatametriaDivider.test.ts +54 -54
  74. package/src/components/__tests__/DatametriaDropdown.test.ts +357 -0
  75. package/src/components/__tests__/DatametriaEmpty.test.ts +261 -0
  76. package/src/components/__tests__/DatametriaFileUpload.test.ts +290 -290
  77. package/src/components/__tests__/DatametriaFloatingBar.test.ts +137 -137
  78. package/src/components/__tests__/DatametriaForm.test.ts +96 -0
  79. package/src/components/__tests__/DatametriaFormItem.test.ts +58 -0
  80. package/src/components/__tests__/DatametriaGrid.test.ts +31 -31
  81. package/src/components/__tests__/DatametriaInput.test.ts +72 -72
  82. package/src/components/__tests__/DatametriaMenu.test.ts +366 -366
  83. package/src/components/__tests__/DatametriaModal.test.ts +86 -86
  84. package/src/components/__tests__/DatametriaNavbar.test.js +48 -48
  85. package/src/components/__tests__/DatametriaNavbar.test.ts +203 -203
  86. package/src/components/__tests__/DatametriaPasswordInput.test.js +305 -305
  87. package/src/components/__tests__/DatametriaRadio.test.ts +195 -0
  88. package/src/components/__tests__/DatametriaSelect.test.ts +77 -77
  89. package/src/components/__tests__/DatametriaSidebar.test.ts +169 -169
  90. package/src/components/__tests__/DatametriaSlider.test.ts +261 -261
  91. package/src/components/__tests__/DatametriaSortableTable.test.js +168 -168
  92. package/src/components/__tests__/DatametriaSpinner.test.ts +156 -156
  93. package/src/components/__tests__/DatametriaSteps.test.ts +211 -0
  94. package/src/components/__tests__/DatametriaSwitch.test.ts +129 -0
  95. package/src/components/__tests__/DatametriaTabPane.test.ts +205 -0
  96. package/src/components/__tests__/DatametriaTable.test.ts +97 -97
  97. package/src/components/__tests__/DatametriaTabs.test.ts +232 -232
  98. package/src/components/__tests__/DatametriaToast.test.js +48 -48
  99. package/src/components/__tests__/DatametriaToast.test.ts +99 -99
  100. package/src/components/__tests__/DatametriaTree.test.ts +376 -0
  101. package/src/components/__tests__/index.test.ts +48 -0
  102. package/src/composables/useAccessibilityScale.ts +94 -94
  103. package/src/composables/useBreakpoints.ts +82 -82
  104. package/src/composables/useHapticFeedback.ts +439 -439
  105. package/src/composables/useRipple.ts +218 -218
  106. package/src/composables/useTheme.ts +5 -1
  107. package/src/index.ts +84 -84
  108. package/src/stories/Variants.stories.js +95 -95
  109. package/src/styles/design-tokens.css +623 -623
  110. package/src/theme/ThemeProvider.vue +96 -96
  111. package/src/theme/__tests__/ThemeProvider.test.ts +208 -208
  112. package/src/theme/__tests__/constants.test.ts +31 -31
  113. package/src/theme/__tests__/presets.test.ts +166 -166
  114. package/src/theme/__tests__/tokens.test.ts +155 -155
  115. package/src/theme/__tests__/types.test.ts +153 -153
  116. package/src/theme/__tests__/useTheme.test.ts +146 -146
  117. package/src/theme/constants.ts +14 -14
  118. package/src/theme/index.ts +12 -12
  119. package/src/theme/presets/datametria.ts +94 -94
  120. package/src/theme/presets/default.ts +94 -94
  121. package/src/theme/presets/index.ts +8 -8
  122. package/src/theme/tokens/colors.ts +28 -28
  123. package/src/theme/tokens/index.ts +47 -47
  124. package/src/theme/tokens/spacing.ts +21 -21
  125. package/src/theme/tokens/typography.ts +35 -35
  126. package/src/theme/types.ts +111 -111
  127. package/src/theme/useTheme.ts +28 -28
  128. package/src/types/index.ts +55 -55
@@ -1,605 +1,623 @@
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
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 text-sm font-medium;
450
+ @apply focus:outline-none disabled:cursor-not-allowed;
451
+
452
+ padding: var(--dm-spacing-2, 0.5rem) var(--dm-spacing-4, 1rem);
453
+ background-color: var(--dm-neutral-50, #ffffff);
454
+ border: 1px solid var(--dm-neutral-300, #d1d5db);
455
+ border-radius: var(--dm-radius-md, 0.375rem);
456
+ color: var(--dm-neutral-900, #111827);
457
+ font-size: var(--dm-font-size-sm, 0.875rem);
458
+ font-weight: var(--dm-font-weight-medium, 500);
459
+ box-shadow: var(--dm-shadow-sm, 0 1px 2px 0 rgba(0, 0, 0, 0.05));
460
+ transition: all 0.2s ease;
461
+ }
462
+
463
+ .dm-menu__button:hover:not(:disabled) {
464
+ background-color: color-mix(in srgb, var(--dm-neutral-50, #ffffff) 95%, var(--dm-neutral-900, #111827) 5%);
465
+ }
466
+
467
+ .dm-menu__button:focus {
468
+ border-color: var(--dm-primary, #0072CE);
469
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-primary, #0072CE) 20%, transparent);
470
+ }
471
+
472
+ .dm-menu__button:disabled {
473
+ opacity: 0.5;
474
+ }
475
+
476
+ .dm-menu__chevron {
477
+ @apply w-4 h-4;
478
+ margin-left: var(--dm-spacing-2, 0.5rem);
479
+ transition: transform 0.2s ease;
480
+ }
481
+
482
+ .dm-menu__chevron--open {
483
+ @apply transform rotate-180;
484
+ }
485
+
486
+ .dm-menu__dropdown {
487
+ @apply min-w-48 max-w-xs;
488
+
489
+ background-color: var(--dm-neutral-50, #ffffff);
490
+ border: 1px solid var(--dm-neutral-200, #e5e7eb);
491
+ border-radius: var(--dm-radius-md, 0.375rem);
492
+ box-shadow: var(--dm-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.1));
493
+ }
494
+
495
+ .dm-menu__content {
496
+ padding: var(--dm-spacing-1, 0.25rem) 0;
497
+ }
498
+
499
+ .dm-menu__item {
500
+ @apply flex items-center cursor-pointer focus:outline-none;
501
+
502
+ padding: var(--dm-spacing-2, 0.5rem) var(--dm-spacing-4, 1rem);
503
+ color: var(--dm-neutral-900, #111827);
504
+ font-size: var(--dm-font-size-sm, 0.875rem);
505
+ transition: background-color 0.15s ease;
506
+ }
507
+
508
+ .dm-menu__item:hover:not(.dm-menu__item--disabled) {
509
+ background-color: var(--dm-neutral-100, #f3f4f6);
510
+ }
511
+
512
+ .dm-menu__item--focused {
513
+ background-color: var(--dm-neutral-100, #f3f4f6);
514
+ }
515
+
516
+ .dm-menu__item--disabled {
517
+ opacity: 0.5;
518
+ cursor: not-allowed;
519
+ }
520
+
521
+ .dm-menu__item--disabled:hover,
522
+ .dm-menu__item--disabled:focus {
523
+ background-color: transparent;
524
+ }
525
+
526
+ .dm-menu__item--divider {
527
+ @apply p-0 cursor-default;
528
+ }
529
+
530
+ .dm-menu__divider {
531
+ border-top: 1px solid var(--dm-neutral-200, #e5e7eb);
532
+ margin: var(--dm-spacing-1, 0.25rem) 0;
533
+ }
534
+
535
+ .dm-menu__item-icon {
536
+ @apply w-4 h-4 mr-3 flex-shrink-0;
537
+ }
538
+
539
+ .dm-menu__item-content {
540
+ @apply flex-1 min-w-0;
541
+ }
542
+
543
+ .dm-menu__item-label {
544
+ @apply font-medium;
545
+ }
546
+
547
+ .dm-menu__item-description {
548
+ color: var(--dm-neutral-500, #6b7280);
549
+ font-size: var(--dm-font-size-xs, 0.75rem);
550
+ margin-top: 0.125rem;
551
+ }
552
+
553
+ .dm-menu__item-shortcut {
554
+ @apply flex-shrink-0;
555
+ color: var(--dm-neutral-400, #9ca3af);
556
+ font-size: var(--dm-font-size-xs, 0.75rem);
557
+ margin-left: var(--dm-spacing-4, 1rem);
558
+ }
559
+
560
+ .dm-menu__backdrop {
561
+ @apply fixed inset-0 bg-black bg-opacity-25 z-40;
562
+ }
563
+
564
+ /* Transitions */
565
+ .menu-enter-active,
566
+ .menu-leave-active {
567
+ transition: opacity 0.15s ease, transform 0.15s ease;
568
+ }
569
+
570
+ .menu-enter-from,
571
+ .menu-leave-to {
572
+ opacity: 0;
573
+ transform: scale(0.95) translateY(-10px);
574
+ }
575
+
576
+ .backdrop-enter-active,
577
+ .backdrop-leave-active {
578
+ transition: opacity 0.15s ease;
579
+ }
580
+
581
+ .backdrop-enter-from,
582
+ .backdrop-leave-to {
583
+ opacity: 0;
584
+ }
585
+
586
+ /* Reduced motion support */
587
+ @media (prefers-reduced-motion: reduce) {
588
+ .dm-menu__chevron,
589
+ .dm-menu__item,
590
+ .menu-enter-active,
591
+ .menu-leave-active,
592
+ .backdrop-enter-active,
593
+ .backdrop-leave-active {
594
+ transition: none;
595
+ }
596
+ }
597
+
598
+ /* Mobile optimizations */
599
+ @media (max-width: 640px) {
600
+ .dm-menu__dropdown {
601
+ @apply max-w-none;
602
+ width: calc(100vw - 2rem);
603
+ }
604
+ }
605
+
606
+ /* Dark Mode Support - Hybrid Approach */
607
+
608
+ /* Fallback automático (sem JS) */
609
+ @media (prefers-color-scheme: dark) {
610
+ .dm-menu {
611
+ background: var(--dm-bg-color-dark, #1e1e1e);
612
+ color: var(--dm-text-primary-dark, #e0e0e0);
613
+ border-color: var(--dm-border-color-dark, #404040);
331
614
  }
332
615
  }
333
616
 
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 text-sm font-medium;
450
- @apply focus:outline-none disabled:cursor-not-allowed;
451
-
452
- padding: var(--dm-spacing-2, 0.5rem) var(--dm-spacing-4, 1rem);
453
- background-color: var(--dm-neutral-50, #ffffff);
454
- border: 1px solid var(--dm-neutral-300, #d1d5db);
455
- border-radius: var(--dm-radius-md, 0.375rem);
456
- color: var(--dm-neutral-900, #111827);
457
- font-size: var(--dm-font-size-sm, 0.875rem);
458
- font-weight: var(--dm-font-weight-medium, 500);
459
- box-shadow: var(--dm-shadow-sm, 0 1px 2px 0 rgba(0, 0, 0, 0.05));
460
- transition: all 0.2s ease;
461
- }
462
-
463
- .dm-menu__button:hover:not(:disabled) {
464
- background-color: color-mix(in srgb, var(--dm-neutral-50, #ffffff) 95%, var(--dm-neutral-900, #111827) 5%);
465
- }
466
-
467
- .dm-menu__button:focus {
468
- border-color: var(--dm-primary, #0072CE);
469
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-primary, #0072CE) 20%, transparent);
470
- }
471
-
472
- .dm-menu__button:disabled {
473
- opacity: 0.5;
474
- }
475
-
476
- .dm-menu__chevron {
477
- @apply w-4 h-4;
478
- margin-left: var(--dm-spacing-2, 0.5rem);
479
- transition: transform 0.2s ease;
480
- }
481
-
482
- .dm-menu__chevron--open {
483
- @apply transform rotate-180;
484
- }
485
-
486
- .dm-menu__dropdown {
487
- @apply min-w-48 max-w-xs;
488
-
489
- background-color: var(--dm-neutral-50, #ffffff);
490
- border: 1px solid var(--dm-neutral-200, #e5e7eb);
491
- border-radius: var(--dm-radius-md, 0.375rem);
492
- box-shadow: var(--dm-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.1));
493
- }
494
-
495
- .dm-menu__content {
496
- padding: var(--dm-spacing-1, 0.25rem) 0;
497
- }
498
-
499
- .dm-menu__item {
500
- @apply flex items-center cursor-pointer focus:outline-none;
501
-
502
- padding: var(--dm-spacing-2, 0.5rem) var(--dm-spacing-4, 1rem);
503
- color: var(--dm-neutral-900, #111827);
504
- font-size: var(--dm-font-size-sm, 0.875rem);
505
- transition: background-color 0.15s ease;
506
- }
507
-
508
- .dm-menu__item:hover:not(.dm-menu__item--disabled) {
509
- background-color: var(--dm-neutral-100, #f3f4f6);
510
- }
511
-
512
- .dm-menu__item--focused {
513
- background-color: var(--dm-neutral-100, #f3f4f6);
514
- }
515
-
516
- .dm-menu__item--disabled {
517
- opacity: 0.5;
518
- cursor: not-allowed;
519
- }
520
-
521
- .dm-menu__item--disabled:hover,
522
- .dm-menu__item--disabled:focus {
523
- background-color: transparent;
524
- }
525
-
526
- .dm-menu__item--divider {
527
- @apply p-0 cursor-default;
528
- }
529
-
530
- .dm-menu__divider {
531
- border-top: 1px solid var(--dm-neutral-200, #e5e7eb);
532
- margin: var(--dm-spacing-1, 0.25rem) 0;
533
- }
534
-
535
- .dm-menu__item-icon {
536
- @apply w-4 h-4 mr-3 flex-shrink-0;
537
- }
538
-
539
- .dm-menu__item-content {
540
- @apply flex-1 min-w-0;
541
- }
542
-
543
- .dm-menu__item-label {
544
- @apply font-medium;
545
- }
546
-
547
- .dm-menu__item-description {
548
- color: var(--dm-neutral-500, #6b7280);
549
- font-size: var(--dm-font-size-xs, 0.75rem);
550
- margin-top: 0.125rem;
551
- }
552
-
553
- .dm-menu__item-shortcut {
554
- @apply flex-shrink-0;
555
- color: var(--dm-neutral-400, #9ca3af);
556
- font-size: var(--dm-font-size-xs, 0.75rem);
557
- margin-left: var(--dm-spacing-4, 1rem);
558
- }
559
-
560
- .dm-menu__backdrop {
561
- @apply fixed inset-0 bg-black bg-opacity-25 z-40;
562
- }
563
-
564
- /* Transitions */
565
- .menu-enter-active,
566
- .menu-leave-active {
567
- transition: opacity 0.15s ease, transform 0.15s ease;
568
- }
569
-
570
- .menu-enter-from,
571
- .menu-leave-to {
572
- opacity: 0;
573
- transform: scale(0.95) translateY(-10px);
574
- }
575
-
576
- .backdrop-enter-active,
577
- .backdrop-leave-active {
578
- transition: opacity 0.15s ease;
579
- }
580
-
581
- .backdrop-enter-from,
582
- .backdrop-leave-to {
583
- opacity: 0;
584
- }
585
-
586
- /* Reduced motion support */
587
- @media (prefers-reduced-motion: reduce) {
588
- .dm-menu__chevron,
589
- .dm-menu__item,
590
- .menu-enter-active,
591
- .menu-leave-active,
592
- .backdrop-enter-active,
593
- .backdrop-leave-active {
594
- transition: none;
595
- }
596
- }
597
-
598
- /* Mobile optimizations */
599
- @media (max-width: 640px) {
600
- .dm-menu__dropdown {
601
- @apply max-w-none;
602
- width: calc(100vw - 2rem);
603
- }
617
+ /* Controle manual via useTheme() */
618
+ [data-theme="dark"] .dm-menu {
619
+ background: var(--dm-bg-color-dark, #1e1e1e);
620
+ color: var(--dm-text-primary-dark, #e0e0e0);
621
+ border-color: var(--dm-border-color-dark, #404040);
604
622
  }
605
623
  </style>