@sakoa/ui 0.2.2 → 0.2.4
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/dist/components/ui/dropdown/SDropdownItem.d.ts +3 -0
- package/dist/saka-ui.css +1 -1
- package/dist/saka-ui.js +4770 -4747
- package/dist/saka-ui.umd.cjs +13 -13
- package/package.json +2 -1
- package/registry/source/components/ui/dropdown/SDropdown.vue +44 -4
- package/registry/source/components/ui/dropdown/SDropdownItem.vue +15 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sakoa/ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/saka-ui.umd.cjs",
|
|
6
6
|
"module": "./dist/saka-ui.js",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"class-variance-authority": "^0.7.1",
|
|
40
40
|
"clsx": "^2.1.1",
|
|
41
41
|
"highlight.js": "^11.11.1",
|
|
42
|
+
"lucide-vue-next": "^0.577.0",
|
|
42
43
|
"tailwind-merge": "^3.5.0",
|
|
43
44
|
"vue": "^3.5.29",
|
|
44
45
|
"vue-router": "^4.6.4"
|
|
@@ -45,12 +45,20 @@ export interface SDropdownContext {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export const SDropdownContextKey: InjectionKey<SDropdownContext> = Symbol('SDropdownContext')
|
|
48
|
+
|
|
49
|
+
export interface SDropdownParentContext {
|
|
50
|
+
registerChildRef: (ref: Ref<HTMLElement | null>) => void
|
|
51
|
+
unregisterChildRef: (ref: Ref<HTMLElement | null>) => void
|
|
52
|
+
cancelHide: () => void
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const SDropdownParentKey: InjectionKey<SDropdownParentContext> = Symbol('SDropdownParent')
|
|
48
56
|
</script>
|
|
49
57
|
|
|
50
58
|
<script setup lang="ts">
|
|
51
59
|
defineOptions({ inheritAttrs: false })
|
|
52
60
|
|
|
53
|
-
import { ref, computed, provide, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|
61
|
+
import { ref, computed, provide, inject, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|
54
62
|
import { cn } from '../../../lib/utils'
|
|
55
63
|
|
|
56
64
|
export interface Props {
|
|
@@ -157,6 +165,23 @@ const menuPosition = ref<{
|
|
|
157
165
|
let showTimeout: ReturnType<typeof setTimeout> | null = null
|
|
158
166
|
let hideTimeout: ReturnType<typeof setTimeout> | null = null
|
|
159
167
|
|
|
168
|
+
// Nested dropdown support: track child teleported menu refs
|
|
169
|
+
const childDropdownRefs = new Set<Ref<HTMLElement | null>>()
|
|
170
|
+
const parentDropdown = inject(SDropdownParentKey, null)
|
|
171
|
+
|
|
172
|
+
provide(SDropdownParentKey, {
|
|
173
|
+
registerChildRef: (childRef: Ref<HTMLElement | null>) => { childDropdownRefs.add(childRef) },
|
|
174
|
+
unregisterChildRef: (childRef: Ref<HTMLElement | null>) => { childDropdownRefs.delete(childRef) },
|
|
175
|
+
cancelHide: () => {
|
|
176
|
+
if (hideTimeout) {
|
|
177
|
+
clearTimeout(hideTimeout)
|
|
178
|
+
hideTimeout = null
|
|
179
|
+
}
|
|
180
|
+
// Propagate up the chain for deeply nested dropdowns
|
|
181
|
+
parentDropdown?.cancelHide()
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
160
185
|
// Computed
|
|
161
186
|
const isManual = computed(() => props.trigger === 'manual')
|
|
162
187
|
|
|
@@ -339,12 +364,15 @@ const calculatePosition = () => {
|
|
|
339
364
|
|
|
340
365
|
const open = async () => {
|
|
341
366
|
if (props.disabled || isOpen.value) return
|
|
342
|
-
|
|
367
|
+
|
|
343
368
|
if (hideTimeout) {
|
|
344
369
|
clearTimeout(hideTimeout)
|
|
345
370
|
hideTimeout = null
|
|
346
371
|
}
|
|
347
|
-
|
|
372
|
+
|
|
373
|
+
// Cancel parent hide timeout when a nested dropdown opens
|
|
374
|
+
parentDropdown?.cancelHide()
|
|
375
|
+
|
|
348
376
|
isOpen.value = true
|
|
349
377
|
emit('update:visible', true)
|
|
350
378
|
emit('open')
|
|
@@ -413,6 +441,8 @@ const handleMenuMouseEnter = () => {
|
|
|
413
441
|
hideTimeout = null
|
|
414
442
|
}
|
|
415
443
|
}
|
|
444
|
+
// Also cancel parent hide when entering a nested menu
|
|
445
|
+
parentDropdown?.cancelHide()
|
|
416
446
|
}
|
|
417
447
|
|
|
418
448
|
const handleMenuMouseLeave = () => {
|
|
@@ -427,7 +457,9 @@ const handleClickOutside = (event: MouseEvent) => {
|
|
|
427
457
|
triggerRef.value &&
|
|
428
458
|
dropdownRef.value &&
|
|
429
459
|
!triggerRef.value.contains(target) &&
|
|
430
|
-
!dropdownRef.value.contains(target)
|
|
460
|
+
!dropdownRef.value.contains(target) &&
|
|
461
|
+
// Don't close if click is inside a child dropdown's teleported menu
|
|
462
|
+
![...childDropdownRefs].some(childRef => childRef.value?.contains(target))
|
|
431
463
|
) {
|
|
432
464
|
close()
|
|
433
465
|
}
|
|
@@ -528,6 +560,11 @@ watch(isOpen, (val) => {
|
|
|
528
560
|
}
|
|
529
561
|
})
|
|
530
562
|
|
|
563
|
+
// Register this dropdown's menu ref with parent (for nested teleported menus)
|
|
564
|
+
if (parentDropdown) {
|
|
565
|
+
parentDropdown.registerChildRef(dropdownRef)
|
|
566
|
+
}
|
|
567
|
+
|
|
531
568
|
// Lifecycle
|
|
532
569
|
onMounted(() => {
|
|
533
570
|
document.addEventListener('mousedown', handleClickOutside)
|
|
@@ -536,6 +573,9 @@ onMounted(() => {
|
|
|
536
573
|
onBeforeUnmount(() => {
|
|
537
574
|
if (showTimeout) clearTimeout(showTimeout)
|
|
538
575
|
if (hideTimeout) clearTimeout(hideTimeout)
|
|
576
|
+
if (parentDropdown) {
|
|
577
|
+
parentDropdown.unregisterChildRef(dropdownRef)
|
|
578
|
+
}
|
|
539
579
|
document.removeEventListener('mousedown', handleClickOutside)
|
|
540
580
|
window.removeEventListener('scroll', calculatePosition, true)
|
|
541
581
|
window.removeEventListener('resize', calculatePosition)
|
|
@@ -27,6 +27,8 @@ export interface Props {
|
|
|
27
27
|
checked?: boolean
|
|
28
28
|
/** Custom color for this item */
|
|
29
29
|
color?: string
|
|
30
|
+
/** Custom icon color */
|
|
31
|
+
iconColor?: string
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -38,7 +40,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
38
40
|
disabled: false,
|
|
39
41
|
danger: false,
|
|
40
42
|
checked: undefined,
|
|
41
|
-
color: undefined
|
|
43
|
+
color: undefined,
|
|
44
|
+
iconColor: undefined
|
|
42
45
|
})
|
|
43
46
|
|
|
44
47
|
const emit = defineEmits<{
|
|
@@ -71,15 +74,21 @@ onBeforeUnmount(() => {
|
|
|
71
74
|
const sizeConfig = computed(() => ({
|
|
72
75
|
small: {
|
|
73
76
|
item: 'px-2 py-1 text-xs',
|
|
74
|
-
icon: 'text-sm'
|
|
77
|
+
icon: 'text-sm',
|
|
78
|
+
iconSize: 'w-3.5 h-3.5',
|
|
79
|
+
iconPx: 14
|
|
75
80
|
},
|
|
76
81
|
medium: {
|
|
77
82
|
item: 'px-2.5 py-1.5 text-sm',
|
|
78
|
-
icon: 'text-base'
|
|
83
|
+
icon: 'text-base',
|
|
84
|
+
iconSize: 'w-4 h-4',
|
|
85
|
+
iconPx: 16
|
|
79
86
|
},
|
|
80
87
|
large: {
|
|
81
88
|
item: 'px-3 py-2 text-base',
|
|
82
|
-
icon: 'text-lg'
|
|
89
|
+
icon: 'text-lg',
|
|
90
|
+
iconSize: 'w-5 h-5',
|
|
91
|
+
iconPx: 20
|
|
83
92
|
}
|
|
84
93
|
}[context?.size ?? 'medium']))
|
|
85
94
|
|
|
@@ -131,7 +140,7 @@ const handleClick = (event: MouseEvent) => {
|
|
|
131
140
|
/>
|
|
132
141
|
|
|
133
142
|
<!-- Leading icon -->
|
|
134
|
-
<component v-else-if="icon && isIconComponent(icon)" :is="icon" :
|
|
143
|
+
<component v-else-if="icon && isIconComponent(icon)" :is="icon" :size="sizeConfig.iconPx" :class="['mr-2.5 shrink-0', !iconColor && !danger ? 'text-muted-foreground group-hover:text-foreground' : '']" :style="iconColor ? { color: iconColor } : {}" />
|
|
135
144
|
<span
|
|
136
145
|
v-else-if="icon"
|
|
137
146
|
:class="['mdi', `mdi-${icon}`, sizeConfig.icon, 'mr-2.5', danger ? '' : 'text-muted-foreground group-hover:text-foreground']"
|
|
@@ -162,7 +171,7 @@ const handleClick = (event: MouseEvent) => {
|
|
|
162
171
|
</kbd>
|
|
163
172
|
|
|
164
173
|
<!-- Trailing icon -->
|
|
165
|
-
<component v-if="trailingIcon && isIconComponent(trailingIcon)" :is="trailingIcon" :
|
|
174
|
+
<component v-if="trailingIcon && isIconComponent(trailingIcon)" :is="trailingIcon" :size="sizeConfig.iconPx" class="text-muted-foreground" />
|
|
166
175
|
<span
|
|
167
176
|
v-else-if="trailingIcon"
|
|
168
177
|
:class="['mdi', `mdi-${trailingIcon}`, sizeConfig.icon, 'text-muted-foreground']"
|