@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakoa/ui",
3
- "version": "0.2.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" :class="[sizeConfig.icon, 'mr-2.5', danger ? '' : 'text-muted-foreground group-hover:text-foreground']" />
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" :class="[sizeConfig.icon, 'text-muted-foreground']" />
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']"