daisy-ui-kit 5.0.0-pre.8 → 5.0.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 (93) hide show
  1. package/app/components/Accordion.vue +8 -5
  2. package/app/components/Alert.vue +2 -1
  3. package/app/components/Avatar.vue +10 -7
  4. package/app/components/AvatarGroup.vue +6 -2
  5. package/app/components/Badge.vue +19 -1
  6. package/app/components/Button.vue +67 -45
  7. package/app/components/Calendar.vue +151 -42
  8. package/app/components/CalendarInput.vue +229 -130
  9. package/app/components/CalendarSkeleton.vue +51 -10
  10. package/app/components/Card.vue +20 -2
  11. package/app/components/CardActions.vue +1 -1
  12. package/app/components/CardBody.vue +1 -1
  13. package/app/components/CardTitle.vue +1 -1
  14. package/app/components/Carousel.vue +2 -1
  15. package/app/components/Chat.vue +6 -1
  16. package/app/components/Checkbox.vue +1 -1
  17. package/app/components/Collapse.vue +38 -5
  18. package/app/components/CollapseTitle.vue +11 -1
  19. package/app/components/Countdown.vue +3 -3
  20. package/app/components/CountdownTimers.vue +4 -7
  21. package/app/components/Counter.vue +14 -3
  22. package/app/components/DaisyLink.vue +33 -15
  23. package/app/components/Dock.vue +5 -6
  24. package/app/components/DockItem.vue +5 -3
  25. package/app/components/Drawer.vue +15 -12
  26. package/app/components/DrawerContent.vue +9 -6
  27. package/app/components/DrawerSide.vue +9 -6
  28. package/app/components/Dropdown.vue +61 -50
  29. package/app/components/DropdownButton.vue +11 -4
  30. package/app/components/DropdownContent.vue +90 -20
  31. package/app/components/DropdownTarget.vue +10 -3
  32. package/app/components/Fab.vue +16 -0
  33. package/app/components/FabClose.vue +18 -0
  34. package/app/components/FabMainAction.vue +5 -0
  35. package/app/components/FabTrigger.vue +117 -0
  36. package/app/components/Fieldset.vue +5 -4
  37. package/app/components/FileInput.vue +1 -1
  38. package/app/components/Filter.vue +45 -38
  39. package/app/components/Flex.vue +8 -1
  40. package/app/components/FlexItem.vue +30 -27
  41. package/app/components/Footer.vue +16 -12
  42. package/app/components/FooterTitle.vue +8 -5
  43. package/app/components/Hero.vue +9 -6
  44. package/app/components/HeroContent.vue +9 -6
  45. package/app/components/Hover3D.vue +22 -0
  46. package/app/components/HoverGallery.vue +11 -0
  47. package/app/components/Indicator.vue +12 -5
  48. package/app/components/IndicatorItem.vue +21 -14
  49. package/app/components/Input.vue +44 -47
  50. package/app/components/Kbd.vue +2 -1
  51. package/app/components/Label.vue +32 -29
  52. package/app/components/MenuExpand.vue +5 -13
  53. package/app/components/MenuExpandToggle.vue +7 -1
  54. package/app/components/MenuItem.vue +6 -4
  55. package/app/components/Modal.vue +23 -17
  56. package/app/components/Progress.vue +13 -1
  57. package/app/components/Prose.vue +7 -2
  58. package/app/components/RadialProgress.vue +8 -8
  59. package/app/components/Radio.vue +1 -1
  60. package/app/components/RadioGroup.vue +2 -2
  61. package/app/components/Range.vue +186 -46
  62. package/app/components/RangeMeasure.vue +33 -30
  63. package/app/components/RangeMeasureTick.vue +4 -5
  64. package/app/components/Rating.vue +70 -53
  65. package/app/components/Select.vue +44 -47
  66. package/app/components/SkeletonText.vue +11 -0
  67. package/app/components/Stack.vue +5 -0
  68. package/app/components/Steps.vue +7 -2
  69. package/app/components/Swap.vue +4 -10
  70. package/app/components/Tab.vue +23 -5
  71. package/app/components/Text.vue +47 -23
  72. package/app/components/TextArea.vue +75 -30
  73. package/app/components/TextRotate.vue +24 -0
  74. package/app/components/ThemeController.vue +3 -4
  75. package/app/components/ThemeProvider.vue +47 -32
  76. package/app/components/TimelineLine.vue +1 -1
  77. package/app/components/TimelineStart.vue +2 -1
  78. package/app/components/Toast.vue +46 -8
  79. package/app/components/Toggle.vue +2 -2
  80. package/app/components/Tooltip.vue +111 -21
  81. package/app/components/TooltipContent.vue +279 -1
  82. package/app/components/TooltipTarget.vue +20 -0
  83. package/app/composables/__tests__/use-calendar.test.ts +239 -0
  84. package/app/composables/use-calendar.ts +288 -0
  85. package/app/composables/use-daisy-theme.ts +140 -0
  86. package/app/composables/use-toast.ts +345 -0
  87. package/app/composables/useSearch.ts +22 -0
  88. package/app/utils/drawer-utils.ts +15 -13
  89. package/app/utils/position-area.ts +40 -0
  90. package/nuxt.d.ts +13 -0
  91. package/nuxt.js +12 -9
  92. package/package.json +52 -27
  93. package/app/utils/random-string.ts +0 -19
@@ -1,11 +1,11 @@
1
1
  <script setup lang="ts">
2
- import { computed } from 'vue';
2
+ import { computed } from 'vue'
3
3
 
4
- const { is = 'span' } = defineProps<{
4
+ const props = defineProps<{
5
5
  is?: any
6
6
  }>()
7
7
 
8
- const tag = computed(() => is)
8
+ const tag = computed(() => props.is || 'span')
9
9
  </script>
10
10
 
11
11
  <template>
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
- import { useIntervalFn } from '@vueuse/core';
3
- import { computed, ref, watch } from 'vue';
2
+ import { useIntervalFn } from '@vueuse/core'
3
+ import { computed, ref, watch } from 'vue'
4
4
 
5
5
  const { durationInSeconds = 0, untilDate } = defineProps<{
6
6
  durationInSeconds?: number
@@ -29,7 +29,7 @@ useIntervalFn(() => {
29
29
  timeLeft.value = calcTimeLeft()
30
30
  }, 1000)
31
31
 
32
- watch(timeLeft, (val) => {
32
+ watch(timeLeft, val => {
33
33
  if (val === 0) {
34
34
  emit('done')
35
35
  }
@@ -42,10 +42,7 @@ const totalDays = computed(() => Math.floor(totalHours.value / 24))
42
42
  const totalWeeks = computed(() => Math.floor(totalDays.value / 7))
43
43
  const totalMonths = computed(() => {
44
44
  const now = new Date()
45
- return (
46
- (targetDate.value.getFullYear() - now.getFullYear()) * 12
47
- + (targetDate.value.getMonth() - now.getMonth())
48
- )
45
+ return (targetDate.value.getFullYear() - now.getFullYear()) * 12 + (targetDate.value.getMonth() - now.getMonth())
49
46
  })
50
47
  const split = computed(() => {
51
48
  const days = totalDays.value
@@ -1,10 +1,21 @@
1
1
  <script setup lang="ts">
2
- const { is = 'span' } = defineProps<{
3
- value: number
2
+ import { computed } from 'vue'
3
+
4
+ const props = defineProps<{
5
+ value: number | string
4
6
  is?: any
7
+ digits?: 2 | 3 | '2' | '3'
5
8
  }>()
9
+
10
+ const style = computed(() => {
11
+ let css = `--value:${props.value};`
12
+ if (props.digits) {
13
+ css += `--digits:${props.digits};`
14
+ }
15
+ return css
16
+ })
6
17
  </script>
7
18
 
8
19
  <template>
9
- <component :is="is" v-bind="$attrs" :style="`--value:${value};`" />
20
+ <component :is="props.is || 'span'" v-bind="$attrs" :style="style" />
10
21
  </template>
@@ -1,25 +1,43 @@
1
1
  <script setup lang="ts">
2
- withDefaults(defineProps<{
3
- is?: string
4
- hover?: boolean
2
+ import { computed, resolveComponent } from 'vue'
5
3
 
6
- color?: 'neutral' | 'primary' | 'secondary' | 'accent' | 'success' | 'info' | 'warning' | 'error'
7
- neutral?: boolean
8
- primary?: boolean
9
- secondary?: boolean
10
- accent?: boolean
11
- success?: boolean
12
- info?: boolean
13
- warning?: boolean
14
- error?: boolean
15
- }>(), {
16
- is: 'a',
4
+ defineOptions({
5
+ inheritAttrs: false,
6
+ })
7
+
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ is?: string
11
+ hover?: boolean
12
+
13
+ color?: 'neutral' | 'primary' | 'secondary' | 'accent' | 'success' | 'info' | 'warning' | 'error'
14
+ neutral?: boolean
15
+ primary?: boolean
16
+ secondary?: boolean
17
+ accent?: boolean
18
+ success?: boolean
19
+ info?: boolean
20
+ warning?: boolean
21
+ error?: boolean
22
+ }>(),
23
+ {
24
+ is: 'a',
25
+ },
26
+ )
27
+ const NuxtLink = resolveComponent('NuxtLink')
28
+ const RouterLink = resolveComponent('RouterLink')
29
+
30
+ const resolvedComponent = computed(() => {
31
+ if (props.is === 'NuxtLink') return NuxtLink
32
+ if (props.is === 'RouterLink') return RouterLink
33
+ return props.is
17
34
  })
18
35
  </script>
19
36
 
20
37
  <template>
21
38
  <component
22
- :is="is"
39
+ :is="resolvedComponent"
40
+ v-bind="$attrs"
23
41
  class="link"
24
42
  :class="{
25
43
  'link-neutral': neutral || color === 'neutral',
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
- import { randomString } from '../utils/random-string';
2
+ import type { Ref } from 'vue'
3
+ import { provide, ref } from 'vue'
3
4
 
4
5
  const { size, xl, lg, md, sm, xs } = defineProps<{
5
6
  size?: string
@@ -13,16 +14,14 @@ const { size, xl, lg, md, sm, xs } = defineProps<{
13
14
  const activeItemId = ref<string | null>(null)
14
15
  const itemIds = ref<string[]>([])
15
16
 
16
- function registerItem() {
17
- const itemId = randomString()
17
+ function registerItem(itemId: string) {
18
18
  itemIds.value.push(itemId)
19
- function unregister() {
19
+ return function unregister() {
20
20
  itemIds.value = itemIds.value.filter(id => id !== itemId)
21
21
  if (activeItemId.value === itemId) {
22
22
  activeItemId.value = null
23
23
  }
24
24
  }
25
- return { id: itemId, unregister }
26
25
  }
27
26
 
28
27
  function setActiveItemId(itemId: string) {
@@ -31,7 +30,7 @@ function setActiveItemId(itemId: string) {
31
30
 
32
31
  export interface DockState {
33
32
  activeItemId: Ref<string | null>
34
- registerItem: () => { id: string, unregister: () => void }
33
+ registerItem: (itemId: string) => () => void
35
34
  setActiveItemId: (itemId: string) => void
36
35
  }
37
36
 
@@ -1,12 +1,14 @@
1
1
  <script setup lang="ts">
2
- import type { DockState } from './Dock.vue';
2
+ import type { DockState } from './Dock.vue'
3
+ import { inject, onUnmounted, useId } from 'vue'
3
4
 
4
5
  const { active } = defineProps<{
5
6
  active?: boolean
6
7
  }>()
7
8
 
9
+ const itemId = useId()
8
10
  const { registerItem, setActiveItemId, activeItemId } = inject<DockState>('dockState')!
9
- const { id: itemId, unregister } = registerItem()
11
+ const unregister = registerItem(itemId)
10
12
 
11
13
  onUnmounted(() => {
12
14
  unregister()
@@ -17,7 +19,7 @@ onUnmounted(() => {
17
19
  <div
18
20
  :id="`dock-item-${itemId}`"
19
21
  class="dock-item"
20
- :class="{ 'dock-active': active || (activeItemId === itemId) }"
22
+ :class="{ 'dock-active': active || activeItemId === itemId }"
21
23
  @click="setActiveItemId(itemId)"
22
24
  >
23
25
  <slot />
@@ -1,21 +1,24 @@
1
1
  <script setup lang="ts">
2
- import { computed, watch } from 'vue';
3
- import { createDrawerState } from '../utils/drawer-utils';
2
+ import { computed, watch } from 'vue'
3
+ import { createDrawerState } from '../utils/drawer-utils'
4
4
 
5
- const props = withDefaults(defineProps<{
6
- open?: boolean
7
- name?: string
8
- end?: boolean
9
- }>(), {
10
- name: 'drawer',
11
- })
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ open?: boolean
8
+ name?: string
9
+ end?: boolean
10
+ }>(),
11
+ {
12
+ name: 'drawer',
13
+ },
14
+ )
12
15
  const emit = defineEmits(['update:open'])
13
16
 
14
17
  // sync `open` prop with drawerState.isDrawerOpen
15
18
  const drawerState = createDrawerState(props.name)
16
19
  watch(
17
20
  () => props.open,
18
- (value) => {
21
+ value => {
19
22
  if (drawerState.isDrawerOpen !== value) {
20
23
  drawerState.isDrawerOpen = value
21
24
  }
@@ -24,7 +27,7 @@ watch(
24
27
  )
25
28
  watch(
26
29
  () => drawerState.isDrawerOpen,
27
- (value) => {
30
+ value => {
28
31
  if (props.open !== value) {
29
32
  emit('update:open', value)
30
33
  }
@@ -41,7 +44,7 @@ const classes = computed(() => {
41
44
 
42
45
  <template>
43
46
  <div class="drawer" :class="classes">
44
- <input :id="name" v-model="drawerState.isDrawerOpen" type="checkbox" class="drawer-toggle">
47
+ <input :id="name" v-model="drawerState.isDrawerOpen" type="checkbox" class="drawer-toggle" />
45
48
  <slot v-bind="drawerState" />
46
49
  </div>
47
50
  </template>
@@ -1,11 +1,14 @@
1
1
  <script setup lang="ts">
2
- import { createDrawerState } from '../utils/drawer-utils';
2
+ import { createDrawerState } from '../utils/drawer-utils'
3
3
 
4
- const props = withDefaults(defineProps<{
5
- name?: string
6
- }>(), {
7
- name: 'drawer',
8
- })
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ name?: string
7
+ }>(),
8
+ {
9
+ name: 'drawer',
10
+ },
11
+ )
9
12
 
10
13
  const drawerState = createDrawerState(props.name)
11
14
  </script>
@@ -1,11 +1,14 @@
1
1
  <script setup lang="ts">
2
- import { createDrawerState } from '../utils/drawer-utils';
2
+ import { createDrawerState } from '../utils/drawer-utils'
3
3
 
4
- const props = withDefaults(defineProps<{
5
- name?: string
6
- }>(), {
7
- name: 'drawer',
8
- })
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ name?: string
7
+ }>(),
8
+ {
9
+ name: 'drawer',
10
+ },
11
+ )
9
12
 
10
13
  const drawerState = createDrawerState(props.name)
11
14
  </script>
@@ -1,54 +1,69 @@
1
1
  <script setup lang="ts">
2
- import { autoUpdate, useFloating } from '@floating-ui/vue'
3
- import { onClickOutside, syncRefs, useElementHover } from '@vueuse/core'
4
- import { onMounted, provide, reactive, ref } from 'vue'
5
- import { randomString } from '../utils/random-string'
6
-
7
- const props = withDefaults(defineProps<{
8
- // allows passing a custom id to the dropdown. Required for SSR hydration.
9
- randomId?: string
10
- autoFocus?: boolean
11
-
12
- placement?: 'top' | 'top-start' | 'top-end' | 'right' | 'right-start' | 'right-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end'
13
- strategy?: 'fixed' | 'absolute'
14
-
15
- hover?: boolean
16
- delayEnter?: number
17
- delayLeave?: number
18
- closeOnClickOutside?: boolean
19
- }>(), {
20
- autoFocus: false,
21
- placement: 'bottom-start',
22
- hover: false,
23
- delayEnter: 0,
24
- delayLeave: 300,
25
- closeOnClickOutside: true,
26
- })
2
+ import { useId } from 'vue'
3
+ import { useElementHover } from '@vueuse/core'
4
+ import { onMounted, provide, ref, watch } from 'vue'
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ autoFocus?: boolean
9
+
10
+ placement?:
11
+ | 'top'
12
+ | 'top-start'
13
+ | 'top-end'
14
+ | 'right'
15
+ | 'right-start'
16
+ | 'right-end'
17
+ | 'bottom'
18
+ | 'bottom-start'
19
+ | 'bottom-end'
20
+ | 'left'
21
+ | 'left-start'
22
+ | 'left-end'
23
+
24
+ hover?: boolean
25
+ delayEnter?: number
26
+ delayLeave?: number
27
+ closeOnClickOutside?: boolean
28
+ }>(),
29
+ {
30
+ autoFocus: false,
31
+ placement: 'bottom-start',
32
+ hover: false,
33
+ delayEnter: 0,
34
+ delayLeave: 300,
35
+ closeOnClickOutside: true,
36
+ },
37
+ )
38
+
27
39
  // Dropdown Visibility
28
- const isOpen = defineModel('open')
40
+ const isOpen = defineModel('open', { default: false })
29
41
  provide('isDropdownOpen', isOpen)
30
42
 
31
43
  const autoFocus = ref(props.autoFocus)
32
44
  provide('dropdownAutoFocus', autoFocus)
33
45
 
34
- const randomValue = props.randomId || randomString(12)
35
- const wrapperId = `dropdown-wrapper-${randomValue}`
36
- const id = `dropdown-${randomValue}`
46
+ // Use Nuxt's useId() for unique IDs
47
+ const uniqueId = useId()
48
+ const wrapperId = `dropdown-wrapper-${uniqueId}`
49
+ const id = `dropdown-${uniqueId}`
37
50
  provide('dropdownId', id)
38
51
 
39
- // set up the floating ui instance
52
+ // Provide placement for CSS anchor positioning
53
+ provide('dropdownPlacement', ref(props.placement))
54
+
55
+ // Provide closeOnClickOutside for popover mode selection
56
+ provide('dropdownCloseOnClickOutside', ref(props.closeOnClickOutside))
57
+
58
+ // Provide hover mode for button behavior
59
+ provide('dropdownHover', ref(props.hover))
60
+
61
+ // References
40
62
  const buttonEl = ref(null)
41
63
  const contentEl = ref(null)
42
- const floatingConfig = reactive({
43
- placement: ref(props.placement),
44
- strategy: ref(props.strategy),
45
- whileElementsMounted: autoUpdate,
46
- })
47
- const { floatingStyles } = useFloating(buttonEl, contentEl, floatingConfig)
48
64
 
49
65
  provide('buttonEl', buttonEl)
50
66
  provide('contentEl', contentEl)
51
- provide('floatingStyles', floatingStyles)
52
67
 
53
68
  // Visibility Utils
54
69
  function toggle() {
@@ -67,29 +82,25 @@ provide('closeDropdown', close)
67
82
  const dropdownWrapper = ref(null)
68
83
 
69
84
  onMounted(() => {
70
- // Close when clicking outside the element
71
- // use a slight delay to avoid conflict with the focus trap in the DropdownContent.
72
- onClickOutside(contentEl, () => {
73
- if (props.closeOnClickOutside) {
74
- setTimeout(() => {
75
- isOpen.value = false
76
- }, 50)
77
- }
78
- })
79
-
80
- // Sync with top-level isHovered ref. For SSR compatibility.
85
+ // Note: closeOnClickOutside is handled automatically by popover="auto"
86
+ // The popover API provides "light dismiss" behavior by default
87
+
88
+ // Sync with hover state for SSR compatibility
81
89
  if (props.hover) {
82
90
  const hover = useElementHover(dropdownWrapper, {
83
91
  delayLeave: props.delayLeave,
84
92
  delayEnter: props.delayEnter,
85
93
  })
86
- syncRefs(hover, isOpen)
94
+
95
+ watch(hover, newValue => {
96
+ isOpen.value = newValue
97
+ })
87
98
  }
88
99
  })
89
100
  </script>
90
101
 
91
102
  <template>
92
- <div :id="wrapperId" ref="dropdownWrapper" class="relative inline-block floating-dropdown">
103
+ <div :id="wrapperId" ref="dropdownWrapper" class="relative inline-block">
93
104
  <slot v-bind="{ toggle, open, close }" />
94
105
  </div>
95
106
  </template>
@@ -3,14 +3,21 @@ import { inject } from 'vue'
3
3
 
4
4
  const id = inject('dropdownId')
5
5
  const isOpen = inject('isDropdownOpen')
6
- const toggleDropdown = inject('toggleDropdown')
7
6
  const buttonEl = inject('buttonEl')
8
-
9
- const toggle = toggleDropdown
7
+ const isHover = inject('dropdownHover')
10
8
  </script>
11
9
 
12
10
  <template>
13
- <Button :id="id" ref="buttonEl" :aria-expanded="isOpen" aria-haspopup="menu" class="dropdown-button" @click="toggle">
11
+ <Button
12
+ :id="id"
13
+ ref="buttonEl"
14
+ :aria-expanded="isOpen"
15
+ aria-haspopup="menu"
16
+ :popovertarget="`${id}-content`"
17
+ :popovertargetaction="isHover ? 'show' : undefined"
18
+ :style="{ 'anchor-name': `--${id}` }"
19
+ class="dropdown-button"
20
+ >
14
21
  <slot />
15
22
  </Button>
16
23
  </template>
@@ -1,19 +1,27 @@
1
1
  <script setup>
2
2
  import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
3
- import { inject, nextTick, ref, watchEffect } from 'vue'
3
+ import { computed, inject, nextTick, watch, watchEffect } from 'vue'
4
+ import { getPositionArea, getPositionFallbacks } from '../utils/position-area'
4
5
 
5
6
  const autoFocus = inject('dropdownAutoFocus')
6
7
  const id = inject('dropdownId')
7
8
  const isOpen = inject('isDropdownOpen')
8
- const isOpenDelayed = ref(isOpen.value)
9
9
  const contentEl = inject('contentEl')
10
- const floatingStyles = inject('floatingStyles')
10
+ const placement = inject('dropdownPlacement')
11
+ const closeOnClickOutside = inject('dropdownCloseOnClickOutside')
11
12
 
12
13
  // Dropdown Utils
13
14
  const toggle = inject('toggleDropdown')
14
15
  const open = inject('openDropdown')
15
16
  const close = inject('closeDropdown')
16
17
 
18
+ // Compute CSS position-area value based on placement
19
+ const positionArea = computed(() => getPositionArea(placement.value))
20
+ const positionFallbacks = computed(() => getPositionFallbacks(placement.value))
21
+
22
+ // Determine popover mode: "auto" for light dismiss, "manual" to disable it
23
+ const popoverMode = computed(() => (closeOnClickOutside.value ? 'auto' : 'manual'))
24
+
17
25
  let activate
18
26
  let deactivate
19
27
 
@@ -29,29 +37,91 @@ if (autoFocus.value) {
29
37
  }
30
38
  })
31
39
  }
32
- // const { activate, deactivate, hasFocus } = useFocusTrap(contentEl, { immediate: true })
33
-
34
- // synchronize isOpenDelayed with isOpen
35
- watchEffect(async () => {
36
- if (isOpen.value) {
37
- isOpenDelayed.value = true
38
- if (autoFocus.value) {
39
- await nextTick()
40
- activate()
40
+
41
+ // Sync popover state with isOpen model (for programmatic control)
42
+ watch(
43
+ isOpen,
44
+ async newValue => {
45
+ if (contentEl.value) {
46
+ try {
47
+ // Check current popover state
48
+ const isPopoverOpen = contentEl.value.matches(':popover-open')
49
+
50
+ // Only programmatically control if state differs
51
+ if (newValue && !isPopoverOpen) {
52
+ contentEl.value.showPopover()
53
+ if (autoFocus.value) {
54
+ await nextTick()
55
+ activate?.()
56
+ }
57
+ } else if (!newValue && isPopoverOpen) {
58
+ contentEl.value.hidePopover()
59
+ if (autoFocus.value) {
60
+ deactivate?.()
61
+ await nextTick()
62
+ }
63
+ }
64
+ } catch (e) {
65
+ // Silently handle if popover API is not supported
66
+ console.warn('Popover API not supported:', e)
67
+ }
41
68
  }
42
- }
43
- else {
44
- if (autoFocus.value) {
45
- deactivate()
46
- await nextTick()
69
+ },
70
+ { flush: 'post' },
71
+ )
72
+
73
+ // Listen to popover toggle events to sync back to isOpen model
74
+ function handleToggle(event) {
75
+ const newState = event.newState === 'open'
76
+
77
+ if (isOpen.value !== newState) {
78
+ isOpen.value = newState
79
+
80
+ if (newState && autoFocus.value) {
81
+ nextTick().then(() => activate?.())
82
+ } else if (!newState && autoFocus.value) {
83
+ deactivate?.()
47
84
  }
48
- isOpenDelayed.value = false
49
85
  }
50
- })
86
+ }
51
87
  </script>
52
88
 
53
89
  <template>
54
- <div v-if="isOpen" ref="contentEl" :style="floatingStyles" :aria-labelledby="id" role="menu">
90
+ <div
91
+ :id="`${id}-content`"
92
+ ref="contentEl"
93
+ :anchor="id"
94
+ :aria-labelledby="id"
95
+ role="menu"
96
+ :popover="popoverMode"
97
+ class="dropdown-content dropdown-popover"
98
+ :style="{
99
+ 'position-anchor': `--${id}`,
100
+ 'position-area': positionArea,
101
+ 'position-try-fallbacks': positionFallbacks,
102
+ }"
103
+ @toggle="handleToggle"
104
+ >
55
105
  <slot v-bind="{ toggle, open, close }" />
56
106
  </div>
57
107
  </template>
108
+
109
+ <style>
110
+ @layer components {
111
+ /* Reset default popover styles - in components layer so utilities can override */
112
+ .dropdown-popover[popover] {
113
+ border: none;
114
+ color: inherit;
115
+ overflow: visible;
116
+ /* Use auto for inset to allow anchor positioning to work */
117
+ inset: auto;
118
+ margin: 0;
119
+ background-color: transparent;
120
+ }
121
+
122
+ /* Position anchoring support */
123
+ .dropdown-popover[popover]:popover-open {
124
+ position: fixed;
125
+ }
126
+ }
127
+ </style>
@@ -3,12 +3,19 @@ import { inject } from 'vue'
3
3
 
4
4
  const id = inject('dropdownId')
5
5
  const isOpen = inject('isDropdownOpen')
6
- const toggle = inject('toggleDropdown')
7
6
  const buttonEl = inject('buttonEl')
8
7
  </script>
9
8
 
10
9
  <template>
11
- <div :id="id" ref="buttonEl" :aria-expanded="isOpen" aria-haspopup="menu" class="dropdown-target" @click="toggle">
10
+ <button
11
+ :id="id"
12
+ ref="buttonEl"
13
+ :aria-expanded="isOpen"
14
+ aria-haspopup="menu"
15
+ :popovertarget="`${id}-content`"
16
+ :style="{ 'anchor-name': `--${id}` }"
17
+ class="dropdown-target"
18
+ >
12
19
  <slot />
13
- </div>
20
+ </button>
14
21
  </template>
@@ -0,0 +1,16 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ flower?: boolean
4
+ }>()
5
+ </script>
6
+
7
+ <template>
8
+ <div
9
+ class="fab"
10
+ :class="{
11
+ 'fab-flower': flower,
12
+ }"
13
+ >
14
+ <slot />
15
+ </div>
16
+ </template>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ function handleMouseDown(event: MouseEvent) {
3
+ event.preventDefault()
4
+ const fab = (event.currentTarget as HTMLElement)?.closest('.fab')
5
+ if (fab instanceof HTMLElement) {
6
+ fab.blur()
7
+ }
8
+ if (document.activeElement instanceof HTMLElement) {
9
+ document.activeElement.blur()
10
+ }
11
+ }
12
+ </script>
13
+
14
+ <template>
15
+ <div class="fab-close" @mousedown="handleMouseDown">
16
+ <slot />
17
+ </div>
18
+ </template>
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <div class="fab-main-action">
3
+ <slot />
4
+ </div>
5
+ </template>