@hina-ui/vue 1.4.0 → 1.6.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 (107) hide show
  1. package/dist/index.js +1222 -599
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/components/app-shell/AppShell.vue.d.ts +1 -0
  4. package/dist/types/components/app-shell/AppShell.vue.d.ts.map +1 -1
  5. package/dist/types/components/combobox/Combobox.vue.d.ts +2 -1
  6. package/dist/types/components/combobox/Combobox.vue.d.ts.map +1 -1
  7. package/dist/types/components/combobox/composables/useCombobox.d.ts +25 -0
  8. package/dist/types/components/combobox/composables/useCombobox.d.ts.map +1 -0
  9. package/dist/types/components/combobox/composables/useOptionLabels.d.ts +3 -0
  10. package/dist/types/components/combobox/composables/useOptionLabels.d.ts.map +1 -0
  11. package/dist/types/components/dialog/Dialog.vue.d.ts +3 -1
  12. package/dist/types/components/dialog/Dialog.vue.d.ts.map +1 -1
  13. package/dist/types/components/drawer/Drawer.vue.d.ts +21 -14
  14. package/dist/types/components/drawer/Drawer.vue.d.ts.map +1 -1
  15. package/dist/types/components/drawer/drawer.variants.d.ts +10 -1
  16. package/dist/types/components/drawer/drawer.variants.d.ts.map +1 -1
  17. package/dist/types/components/dropdown-menu/DropdownMenu.vue.d.ts +4 -1
  18. package/dist/types/components/dropdown-menu/DropdownMenu.vue.d.ts.map +1 -1
  19. package/dist/types/components/hover-card/HoverCard.vue.d.ts +4 -1
  20. package/dist/types/components/hover-card/HoverCard.vue.d.ts.map +1 -1
  21. package/dist/types/components/hover-card/HoverCardAnchor.vue.d.ts +6 -4
  22. package/dist/types/components/hover-card/HoverCardAnchor.vue.d.ts.map +1 -1
  23. package/dist/types/components/hover-card/composables/useHoverCardAnchor.d.ts +9 -4
  24. package/dist/types/components/hover-card/composables/useHoverCardAnchor.d.ts.map +1 -1
  25. package/dist/types/components/multi-combobox/MultiCombobox.vue.d.ts +1 -0
  26. package/dist/types/components/multi-combobox/MultiCombobox.vue.d.ts.map +1 -1
  27. package/dist/types/components/multi-combobox/composables/useMultiCombobox.d.ts +2 -1
  28. package/dist/types/components/multi-combobox/composables/useMultiCombobox.d.ts.map +1 -1
  29. package/dist/types/components/nav-link/NavLink.vue.d.ts.map +1 -1
  30. package/dist/types/components/nav-link/nav-link.variants.d.ts +1 -18
  31. package/dist/types/components/nav-link/nav-link.variants.d.ts.map +1 -1
  32. package/dist/types/components/popover/Popover.vue.d.ts +4 -1
  33. package/dist/types/components/popover/Popover.vue.d.ts.map +1 -1
  34. package/dist/types/components/sheet/Sheet.vue.d.ts +21 -14
  35. package/dist/types/components/sheet/Sheet.vue.d.ts.map +1 -1
  36. package/dist/types/components/sheet/sheet.variants.d.ts +54 -3
  37. package/dist/types/components/sheet/sheet.variants.d.ts.map +1 -1
  38. package/dist/types/components/sidebar/Sidebar.vue.d.ts +15 -11
  39. package/dist/types/components/sidebar/Sidebar.vue.d.ts.map +1 -1
  40. package/dist/types/components/sidebar/SidebarGroup.vue.d.ts.map +1 -1
  41. package/dist/types/components/sidebar/SidebarLabel.vue.d.ts +20 -0
  42. package/dist/types/components/sidebar/SidebarLabel.vue.d.ts.map +1 -0
  43. package/dist/types/components/sidebar/sidebar.variants.d.ts +46 -0
  44. package/dist/types/components/sidebar/sidebar.variants.d.ts.map +1 -0
  45. package/dist/types/components/tree/Tree.vue.d.ts +37 -0
  46. package/dist/types/components/tree/Tree.vue.d.ts.map +1 -0
  47. package/dist/types/components/tree/TreeCheck.vue.d.ts +8 -0
  48. package/dist/types/components/tree/TreeCheck.vue.d.ts.map +1 -0
  49. package/dist/types/components/tree/composables/useTree.d.ts +23 -0
  50. package/dist/types/components/tree/composables/useTree.d.ts.map +1 -0
  51. package/dist/types/components/tree/tree.variants.d.ts +5 -0
  52. package/dist/types/components/tree/tree.variants.d.ts.map +1 -0
  53. package/dist/types/components/tree/types.d.ts +18 -0
  54. package/dist/types/components/tree/types.d.ts.map +1 -0
  55. package/dist/types/components/tree/utils/selection.d.ts +16 -0
  56. package/dist/types/components/tree/utils/selection.d.ts.map +1 -0
  57. package/dist/types/index.d.ts +4 -0
  58. package/dist/types/index.d.ts.map +1 -1
  59. package/dist/types/lib/anchored-overlay.d.ts +12 -2
  60. package/dist/types/lib/anchored-overlay.d.ts.map +1 -1
  61. package/dist/types/lib/overlay-anchor.d.ts +16 -0
  62. package/dist/types/lib/overlay-anchor.d.ts.map +1 -0
  63. package/dist/types/lib/scroll-viewport.d.ts +9 -0
  64. package/dist/types/lib/scroll-viewport.d.ts.map +1 -0
  65. package/dist/types/locale/en-US.d.ts.map +1 -1
  66. package/dist/types/locale/index.d.ts.map +1 -1
  67. package/dist/types/locale/types.d.ts +3 -0
  68. package/dist/types/locale/types.d.ts.map +1 -1
  69. package/dist/types/locale/zh-CN.d.ts.map +1 -1
  70. package/package.json +1 -1
  71. package/src/components/app-shell/AppShell.vue +2 -1
  72. package/src/components/combobox/Combobox.vue +14 -23
  73. package/src/components/combobox/composables/useCombobox.ts +63 -0
  74. package/src/components/combobox/composables/useOptionLabels.ts +20 -0
  75. package/src/components/dialog/Dialog.vue +5 -1
  76. package/src/components/drawer/Drawer.vue +69 -32
  77. package/src/components/drawer/drawer.variants.ts +6 -2
  78. package/src/components/dropdown-menu/DropdownMenu.vue +18 -5
  79. package/src/components/hover-card/HoverCard.vue +6 -1
  80. package/src/components/hover-card/HoverCardAnchor.vue +3 -1
  81. package/src/components/hover-card/composables/useHoverCardAnchor.ts +33 -42
  82. package/src/components/multi-combobox/MultiCombobox.vue +2 -0
  83. package/src/components/multi-combobox/composables/useMultiCombobox.ts +5 -10
  84. package/src/components/nav-link/NavLink.vue +7 -1
  85. package/src/components/nav-link/nav-link.variants.ts +1 -10
  86. package/src/components/popover/Popover.vue +17 -7
  87. package/src/components/sheet/Sheet.vue +67 -24
  88. package/src/components/sheet/sheet.variants.ts +17 -4
  89. package/src/components/sidebar/Sidebar.vue +43 -33
  90. package/src/components/sidebar/SidebarGroup.vue +12 -11
  91. package/src/components/sidebar/SidebarLabel.vue +24 -0
  92. package/src/components/sidebar/sidebar.variants.ts +36 -0
  93. package/src/components/tree/Tree.vue +132 -0
  94. package/src/components/tree/TreeCheck.vue +33 -0
  95. package/src/components/tree/composables/useTree.ts +81 -0
  96. package/src/components/tree/tree.variants.ts +22 -0
  97. package/src/components/tree/types.ts +20 -0
  98. package/src/components/tree/utils/selection.ts +78 -0
  99. package/src/index.ts +4 -0
  100. package/src/lib/anchored-overlay.ts +28 -19
  101. package/src/lib/overlay-anchor.ts +65 -0
  102. package/src/lib/scroll-viewport.ts +8 -0
  103. package/src/locale/en-US.ts +1 -0
  104. package/src/locale/index.ts +1 -0
  105. package/src/locale/types.ts +3 -0
  106. package/src/locale/zh-CN.ts +1 -0
  107. package/src/styles/motion.css +40 -0
@@ -0,0 +1,20 @@
1
+ import { reactive, watch } from 'vue'
2
+ import { flattenOptions, type SelectItems, type SelectOption } from '../../select/types'
3
+
4
+ export function useOptionLabels<T extends SelectOption>(
5
+ options: () => SelectItems<T>,
6
+ selectedOptions: () => T[] | undefined,
7
+ ) {
8
+ const labels = reactive(new Map<string | number, string>())
9
+
10
+ watch(
11
+ [options, selectedOptions],
12
+ ([items, selected]) => {
13
+ for (const option of flattenOptions(items)) labels.set(option.value, option.label)
14
+ for (const option of selected ?? []) labels.set(option.value, option.label)
15
+ },
16
+ { immediate: true, deep: true },
17
+ )
18
+
19
+ return labels
20
+ }
@@ -15,6 +15,7 @@
15
15
  import Heading from '../heading/Heading.vue'
16
16
  import Text from '../text/Text.vue'
17
17
  import { useOverlayPortal } from '../../lib/overlay-portal'
18
+ import { useScrollViewport } from '../../lib/scroll-viewport'
18
19
  import { cn } from '../../lib/cn'
19
20
  import { dialogWrapper, dialogCard, type DialogVariants } from './dialog.variants'
20
21
 
@@ -47,6 +48,9 @@
47
48
  if (props.locked) e.preventDefault()
48
49
  }
49
50
 
51
+ const { scrollArea, viewport } = useScrollViewport()
52
+ defineExpose({ viewport })
53
+
50
54
  const open = defineModel<boolean>('open')
51
55
  const { content, present } = useOverlayPortal(open)
52
56
 
@@ -117,7 +121,7 @@
117
121
  <CloseButton :disabled="props.locked" class="-mt-1.5 -me-1.5 shrink-0" />
118
122
  </DialogClose>
119
123
  </div>
120
- <ScrollArea v-if="$slots.content" class="min-h-0">
124
+ <ScrollArea v-if="$slots.content" ref="scrollArea" class="min-h-0">
121
125
  <div class="px-(--hn-panel-p) py-1">
122
126
  <slot name="content" :close="close" />
123
127
  </div>
@@ -14,8 +14,9 @@
14
14
  import ScrollArea from '../scroll-area/ScrollArea.vue'
15
15
  import Heading from '../heading/Heading.vue'
16
16
  import Text from '../text/Text.vue'
17
+ import { useOverlayPortal } from '../../lib/overlay-portal'
18
+ import { useScrollViewport } from '../../lib/scroll-viewport'
17
19
  import { cn } from '../../lib/cn'
18
- import { useUiLocale } from '../../locale'
19
20
  import { drawerCard } from './drawer.variants'
20
21
 
21
22
  defineOptions({ name: 'HnDrawer' })
@@ -26,18 +27,32 @@
26
27
  description?: string
27
28
  side?: 'start' | 'end'
28
29
  size?: 'sm' | 'md' | 'lg'
30
+ header?: boolean
31
+ closable?: boolean
29
32
  locked?: boolean
30
33
  class?: string
31
34
  }>(),
32
- { side: 'end', size: 'md', locked: false },
35
+ { side: 'end', size: 'md', header: true, closable: true, locked: false },
33
36
  )
34
37
 
38
+ const slots = defineSlots<{
39
+ default?(): unknown
40
+ icon?(): unknown
41
+ title?(): unknown
42
+ body?(props: { close: () => void }): unknown
43
+ content?(props: { close: () => void }): unknown
44
+ footer?(props: { close: () => void }): unknown
45
+ }>()
46
+
35
47
  function guard(e: Event) {
36
48
  if (props.locked) e.preventDefault()
37
49
  }
38
50
 
51
+ const { scrollArea, viewport } = useScrollViewport()
52
+ defineExpose({ viewport })
53
+
39
54
  const open = defineModel<boolean>('open')
40
- const t = useUiLocale()
55
+ const { content, present } = useOverlayPortal(open)
41
56
 
42
57
  function close() {
43
58
  open.value = false
@@ -49,45 +64,67 @@
49
64
  <DialogTrigger v-if="$slots.default" as-child>
50
65
  <slot />
51
66
  </DialogTrigger>
52
- <DialogPortal>
67
+ <DialogPortal v-if="present">
53
68
  <DialogOverlay class="hn-scrim" />
54
69
  <DialogContent as-child @escape-key-down="guard" @interact-outside="guard">
55
70
  <Card
71
+ ref="content"
72
+ v-bind="props.description ? {} : { 'aria-describedby': undefined }"
56
73
  :data-hn-side="props.side"
57
74
  :padded="false"
58
75
  :class="
59
- cn(
60
- 'hn-anim-drawer pointer-events-auto fixed inset-y-0 z-(--hn-z-overlay) flex flex-col gap-4',
61
- 'max-w-[calc(100vw-3rem)] rounded-none py-(--hn-panel-p) shadow-lg outline-none',
62
- drawerCard({ side: props.side, size: props.size }),
63
- props.class,
64
- )
76
+ cn(drawerCard({ side: props.side, size: props.size, padded: !slots.body }), props.class)
65
77
  "
66
78
  >
67
- <div class="flex shrink-0 items-start justify-between gap-4 px-(--hn-panel-p)">
68
- <div class="flex min-w-0 flex-col gap-1.5">
69
- <DialogTitle as-child>
70
- <Heading :level="2" size="lg">{{ props.title }}</Heading>
71
- </DialogTitle>
72
- <DialogDescription v-if="props.description" as-child>
73
- <Text tone="muted">{{ props.description }}</Text>
74
- </DialogDescription>
79
+ <template v-if="!props.header || slots.body">
80
+ <DialogTitle as-child>
81
+ <Heading :level="2" class="sr-only">{{ props.title }}</Heading>
82
+ </DialogTitle>
83
+ <DialogDescription v-if="props.description" class="sr-only">
84
+ {{ props.description }}
85
+ </DialogDescription>
86
+ </template>
87
+ <slot v-if="slots.body" name="body" :close="close" />
88
+ <template v-else>
89
+ <div
90
+ v-if="props.header"
91
+ class="flex shrink-0 items-start justify-between gap-4 px-(--hn-panel-p)"
92
+ >
93
+ <div class="flex min-w-0 flex-col gap-1.5">
94
+ <div class="flex min-w-0 items-center gap-2">
95
+ <span
96
+ v-if="slots.icon"
97
+ class="text-muted flex shrink-0 [&_svg]:size-5"
98
+ aria-hidden="true"
99
+ >
100
+ <slot name="icon" />
101
+ </span>
102
+ <DialogTitle as-child>
103
+ <Heading :level="2" size="lg" class="min-w-0">
104
+ <slot name="title">{{ props.title }}</slot>
105
+ </Heading>
106
+ </DialogTitle>
107
+ </div>
108
+ <DialogDescription v-if="props.description" as-child>
109
+ <Text tone="muted">{{ props.description }}</Text>
110
+ </DialogDescription>
111
+ </div>
112
+ <DialogClose v-if="props.closable" as-child>
113
+ <CloseButton :disabled="props.locked" class="-mt-1.5 -me-1.5 shrink-0" />
114
+ </DialogClose>
75
115
  </div>
76
- <DialogClose as-child>
77
- <CloseButton :disabled="props.locked" class="-mt-1.5 -me-1.5 shrink-0" />
78
- </DialogClose>
79
- </div>
80
- <ScrollArea v-if="$slots.content" class="min-h-0 grow">
81
- <div class="px-(--hn-panel-p) py-1">
82
- <slot name="content" :close="close" />
116
+ <ScrollArea v-if="$slots.content" ref="scrollArea" class="min-h-0 grow">
117
+ <div class="px-(--hn-panel-p) py-1">
118
+ <slot name="content" :close="close" />
119
+ </div>
120
+ </ScrollArea>
121
+ <div
122
+ v-if="$slots.footer"
123
+ class="flex shrink-0 justify-end gap-(--hn-inline-gap) px-(--hn-panel-p)"
124
+ >
125
+ <slot name="footer" :close="close" />
83
126
  </div>
84
- </ScrollArea>
85
- <div
86
- v-if="$slots.footer"
87
- class="flex shrink-0 justify-end gap-(--hn-inline-gap) px-(--hn-panel-p)"
88
- >
89
- <slot name="footer" :close="close" />
90
- </div>
127
+ </template>
91
128
  </Card>
92
129
  </DialogContent>
93
130
  </DialogPortal>
@@ -1,8 +1,12 @@
1
1
  import { tv, type VariantProps } from '../../lib/tv'
2
2
 
3
3
  export const drawerCard = tv({
4
- base: '',
4
+ base: [
5
+ 'hn-anim-drawer pointer-events-auto fixed inset-y-0 z-(--hn-z-overlay) flex flex-col',
6
+ 'max-w-[calc(100vw-3rem)] rounded-none shadow-lg outline-none',
7
+ ],
5
8
  variants: {
9
+ padded: { true: 'gap-4 py-(--hn-panel-p)' },
6
10
  side: {
7
11
  start: 'start-0',
8
12
  end: 'end-0',
@@ -13,7 +17,7 @@ export const drawerCard = tv({
13
17
  lg: 'w-120',
14
18
  },
15
19
  },
16
- defaultVariants: { side: 'end', size: 'md' },
20
+ defaultVariants: { side: 'end', size: 'md', padded: true },
17
21
  })
18
22
 
19
23
  export type DrawerVariants = VariantProps<typeof drawerCard>
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import type { OverlayAnchor, OverlayPositionStrategy } from '../../lib/overlay-anchor'
2
3
  import { computed } from 'vue'
3
4
  import {
4
5
  DropdownMenuRoot,
@@ -16,7 +17,8 @@
16
17
  const props = withDefaults(
17
18
  defineProps<{
18
19
  label?: string
19
- anchor?: HTMLElement | null
20
+ anchor?: OverlayAnchor | null
21
+ updatePositionStrategy?: OverlayPositionStrategy
20
22
  modal?: boolean
21
23
  dir?: 'ltr' | 'rtl'
22
24
  side?: 'top' | 'right' | 'bottom' | 'left'
@@ -24,13 +26,23 @@
24
26
  sideOffset?: number
25
27
  class?: string
26
28
  }>(),
27
- { side: 'bottom', align: 'center', sideOffset: 8, modal: true },
29
+ {
30
+ updatePositionStrategy: 'optimized',
31
+ side: 'bottom',
32
+ align: 'center',
33
+ sideOffset: 8,
34
+ modal: true,
35
+ },
28
36
  )
29
37
 
30
38
  const open = defineModel<boolean>('open')
31
39
  const emit = defineEmits<DropdownMenuContentEmits>()
32
40
  defineSlots<{ default?(): unknown; content?(): unknown }>()
33
- const { trigger, reference, visible, events } = useAnchoredOverlay(props, open, emit)
41
+ const { trigger, content, present, reference, visible, events } = useAnchoredOverlay(
42
+ props,
43
+ open,
44
+ emit,
45
+ )
34
46
 
35
47
  const pressOrigin = computed(() => {
36
48
  if (props.side === 'left') return 'right'
@@ -52,17 +64,18 @@
52
64
  >
53
65
  <slot />
54
66
  </DropdownMenuTrigger>
55
- <DropdownMenuPortal>
67
+ <DropdownMenuPortal v-if="present">
56
68
  <DropdownMenuContent
57
- v-if="reference"
58
69
  v-bind="{ 'aria-label': props.label, ...$attrs, ...events }"
59
70
  :reference="reference"
71
+ :update-position-strategy="props.updatePositionStrategy"
60
72
  as-child
61
73
  :side="props.side"
62
74
  :align="props.align"
63
75
  :side-offset="props.sideOffset"
64
76
  >
65
77
  <Card
78
+ ref="content"
66
79
  :padded="false"
67
80
  :class="
68
81
  cn(
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import type { OverlayAnchor, OverlayPositionStrategy } from '../../lib/overlay-anchor'
2
3
  import { HoverCardContent, HoverCardPortal, HoverCardRoot, HoverCardTrigger } from 'reka-ui'
3
4
  import { cn } from '../../lib/cn'
4
5
  import Card from '../card/Card.vue'
@@ -8,7 +9,8 @@
8
9
 
9
10
  const props = withDefaults(
10
11
  defineProps<{
11
- anchor?: HTMLElement | null
12
+ anchor?: OverlayAnchor | null
13
+ updatePositionStrategy?: OverlayPositionStrategy
12
14
  side?: 'top' | 'right' | 'bottom' | 'left'
13
15
  align?: 'start' | 'center' | 'end'
14
16
  sideOffset?: number
@@ -19,6 +21,7 @@
19
21
  positionerClass?: string
20
22
  }>(),
21
23
  {
24
+ updatePositionStrategy: 'optimized',
22
25
  side: 'bottom',
23
26
  align: 'center',
24
27
  sideOffset: 8,
@@ -41,6 +44,7 @@
41
44
  <HoverCardAnchor
42
45
  v-slot="{ reference, contentRef, present }"
43
46
  :anchor="props.anchor"
47
+ :update-position-strategy="props.updatePositionStrategy"
44
48
  :external="!$slots.default"
45
49
  :close-delay="props.closeDelay"
46
50
  :positioner-class="props.positionerClass"
@@ -48,6 +52,7 @@
48
52
  <HoverCardPortal v-if="present">
49
53
  <HoverCardContent
50
54
  :reference="reference"
55
+ :update-position-strategy="props.updatePositionStrategy"
51
56
  as-child
52
57
  :side="props.side"
53
58
  :align="props.align"
@@ -1,9 +1,11 @@
1
1
  <script setup lang="ts">
2
+ import type { OverlayAnchor, OverlayPositionStrategy } from '../../lib/overlay-anchor'
2
3
  import { useHoverCardAnchor } from './composables/useHoverCardAnchor'
3
4
 
4
5
  defineOptions({ name: 'HnHoverCardAnchor' })
5
6
  const props = defineProps<{
6
- anchor?: HTMLElement | null
7
+ anchor?: OverlayAnchor | null
8
+ updatePositionStrategy: OverlayPositionStrategy
7
9
  external: boolean
8
10
  closeDelay: number
9
11
  positionerClass?: string
@@ -1,12 +1,19 @@
1
- import { computed, onScopeDispose, shallowRef, watch, type ComponentPublicInstance } from 'vue'
1
+ import { computed, onScopeDispose, watch, type ComponentPublicInstance } from 'vue'
2
2
  import { useEventListener, useRafFn } from '@vueuse/core'
3
3
  import { injectHoverCardRootContext } from 'reka-ui'
4
+ import {
5
+ overlayAnchorElement,
6
+ useOverlayAnchor,
7
+ type OverlayAnchor,
8
+ type OverlayPositionStrategy,
9
+ } from '../../../lib/overlay-anchor'
4
10
  import { useOverlayPortal } from '../../../lib/overlay-portal'
5
11
  import { useOverlayPositionerClass } from '../../../lib/overlay-positioner'
6
12
  import { inPointerCorridor } from '../../../lib/pointer-corridor'
7
13
 
8
14
  interface HoverCardAnchorOptions {
9
- anchor?: HTMLElement | null
15
+ anchor?: OverlayAnchor | null
16
+ updatePositionStrategy: OverlayPositionStrategy
10
17
  external: boolean
11
18
  closeDelay: number
12
19
  positionerClass?: string
@@ -16,18 +23,18 @@ export function useHoverCardAnchor(props: HoverCardAnchorOptions) {
16
23
  const root = injectHoverCardRootContext()
17
24
  const original = { onOpen: root.onOpen, onClose: root.onClose, onDismiss: root.onDismiss }
18
25
  const anchor = computed(() => (props.external ? (props.anchor ?? undefined) : undefined))
26
+ const element = computed(() => overlayAnchorElement(anchor.value))
27
+ const virtual = computed(() => !!anchor.value && !element.value)
19
28
  const { content, present } = useOverlayPortal(root.open)
20
29
  useOverlayPositionerClass(content, () => props.positionerClass)
21
30
  const panel = computed(() =>
22
31
  props.external ? (content.value?.$el as HTMLElement | undefined) : undefined,
23
32
  )
24
- const liveAnchor = shallowRef<HTMLElement>()
25
- const retained = shallowRef<{
26
- getBoundingClientRect: () => DOMRect
27
- contextElement: HTMLElement
28
- }>()
29
- const reference = computed(() =>
30
- props.external ? (liveAnchor.value ?? retained.value) : undefined,
33
+ const reference = useOverlayAnchor(
34
+ () => (props.external ? anchor.value : root.triggerElement.value),
35
+ root.open,
36
+ present,
37
+ () => props.updatePositionStrategy,
31
38
  )
32
39
  let timer: ReturnType<typeof setTimeout> | undefined
33
40
  let tracking = false
@@ -45,6 +52,7 @@ export function useHoverCardAnchor(props: HoverCardAnchorOptions) {
45
52
 
46
53
  function close() {
47
54
  if (
55
+ virtual.value ||
48
56
  timer !== undefined ||
49
57
  !root.open.value ||
50
58
  root.hasSelectionRef.value ||
@@ -63,8 +71,8 @@ export function useHoverCardAnchor(props: HoverCardAnchorOptions) {
63
71
  const pointer = event as PointerEvent
64
72
  if (!root.open.value || !tracking || pointer.pointerType === 'touch') return
65
73
  const target = pointer.target as Node | null
66
- if (target && (anchor.value?.contains(target) || panel.value?.contains(target))) return keep()
67
- const a = anchor.value?.getBoundingClientRect()
74
+ if (target && (element.value?.contains(target) || panel.value?.contains(target))) return keep()
75
+ const a = element.value?.getBoundingClientRect()
68
76
  const b = panel.value?.getBoundingClientRect()
69
77
  if (a && b && inPointerCorridor({ x: pointer.clientX, y: pointer.clientY }, a, b)) cancel()
70
78
  else close()
@@ -72,33 +80,20 @@ export function useHoverCardAnchor(props: HoverCardAnchorOptions) {
72
80
 
73
81
  function leave(event: Event) {
74
82
  const pointer = event as PointerEvent
75
- if (pointer.pointerType === 'touch' || !root.open.value) return
83
+ if (virtual.value || pointer.pointerType === 'touch' || !root.open.value) return
76
84
  tracking = true
77
- const a = anchor.value?.getBoundingClientRect()
85
+ const a = element.value?.getBoundingClientRect()
78
86
  const b = panel.value?.getBoundingClientRect()
79
87
  if (a && b && inPointerCorridor({ x: pointer.clientX, y: pointer.clientY }, a, b)) cancel()
80
88
  else close()
81
89
  }
82
90
 
83
91
  function measure() {
84
- const element = anchor.value
85
- if (!element?.isConnected) {
86
- liveAnchor.value = undefined
92
+ if (!anchor.value || (element.value && !element.value.isConnected)) {
87
93
  if (root.open.value) dismiss()
88
94
  return
89
95
  }
90
- liveAnchor.value = element
91
- const rect = element.getBoundingClientRect()
92
- const previous = retained.value?.getBoundingClientRect()
93
- if (
94
- !previous ||
95
- retained.value?.contextElement !== element ||
96
- rect.x !== previous.x ||
97
- rect.y !== previous.y ||
98
- rect.width !== previous.width ||
99
- rect.height !== previous.height
100
- )
101
- retained.value = { getBoundingClientRect: () => rect, contextElement: element }
96
+ reference.value?.getBoundingClientRect()
102
97
  }
103
98
 
104
99
  const { pause, resume } = useRafFn(measure, { immediate: false })
@@ -112,35 +107,32 @@ export function useHoverCardAnchor(props: HoverCardAnchorOptions) {
112
107
  cancel()
113
108
  tracking = false
114
109
  pause()
115
- if (!props.external || !mounted) {
116
- liveAnchor.value = undefined
117
- return
118
- }
110
+ if (!props.external || !mounted) return
119
111
  if (element !== previous?.[0]) {
120
112
  root.hasSelectionRef.value = false
121
113
  root.isPointerDownOnContentRef.value = false
122
114
  }
123
115
  measure()
124
- resume()
116
+ if (!virtual.value) resume()
125
117
  },
126
118
  { immediate: true, flush: 'post' },
127
119
  )
128
120
 
129
- useEventListener(anchor, 'pointerenter', keep)
130
- useEventListener(anchor, 'pointerleave', leave)
131
- useEventListener(anchor, 'focusin', keep)
132
- useEventListener(anchor, 'focusout', event => {
121
+ useEventListener(element, 'pointerenter', keep)
122
+ useEventListener(element, 'pointerleave', leave)
123
+ useEventListener(element, 'focusin', keep)
124
+ useEventListener(element, 'focusout', event => {
133
125
  const target = (event as FocusEvent).relatedTarget as Node | null
134
- if (!target || (!anchor.value?.contains(target) && !panel.value?.contains(target))) close()
126
+ if (!target || (!element.value?.contains(target) && !panel.value?.contains(target))) close()
135
127
  })
136
128
  useEventListener(panel, 'pointerenter', keep)
137
129
  useEventListener(panel, 'pointerleave', leave)
138
- useEventListener(() => anchor.value?.ownerDocument, 'pointermove', track)
130
+ useEventListener(() => element.value?.ownerDocument, 'pointermove', track)
139
131
  useEventListener(
140
- () => anchor.value?.ownerDocument,
132
+ () => element.value?.ownerDocument,
141
133
  'scroll',
142
134
  event => {
143
- if (root.open.value && (event.target as Node | null)?.contains(anchor.value ?? null))
135
+ if (root.open.value && (event.target as Node | null)?.contains(element.value ?? null))
144
136
  dismiss()
145
137
  },
146
138
  { capture: true, passive: true },
@@ -153,7 +145,6 @@ export function useHoverCardAnchor(props: HoverCardAnchorOptions) {
153
145
 
154
146
  function contentRef(value: Element | ComponentPublicInstance | null) {
155
147
  content.value = value as ComponentPublicInstance | null
156
- if (!value && !root.open.value) retained.value = undefined
157
148
  }
158
149
 
159
150
  return { reference, contentRef, present }
@@ -33,6 +33,7 @@
33
33
  const props = withDefaults(
34
34
  defineProps<{
35
35
  options: SelectItems<T>
36
+ selectedOptions?: T[]
36
37
  placeholder?: string
37
38
  ignoreFilter?: boolean
38
39
  loading?: boolean
@@ -69,6 +70,7 @@
69
70
  const { selected, keyboard, input, remove, clear, onHostClick, onInputKeydown } =
70
71
  useMultiCombobox({
71
72
  options: () => props.options,
73
+ selectedOptions: () => props.selectedOptions,
72
74
  model,
73
75
  open,
74
76
  disabled: () => disabled.value,
@@ -1,9 +1,11 @@
1
- import { computed, reactive, ref, shallowRef, watch, type Ref } from 'vue'
1
+ import { computed, ref, shallowRef, watch, type Ref } from 'vue'
2
2
  import { focusFieldFrom } from '../../../lib/field-focus'
3
- import { flattenOptions, type SelectItems, type SelectOption } from '../../select/types'
3
+ import type { SelectItems, SelectOption } from '../../select/types'
4
+ import { useOptionLabels } from '../../combobox/composables/useOptionLabels'
4
5
 
5
6
  interface MultiComboboxOptions<T extends SelectOption> {
6
7
  options: () => SelectItems<T>
8
+ selectedOptions: () => T[] | undefined
7
9
  model: Ref<Array<string | number>>
8
10
  open: Ref<boolean>
9
11
  disabled: () => boolean
@@ -14,14 +16,7 @@ export function useMultiCombobox<T extends SelectOption>(options: MultiComboboxO
14
16
  const { model, open } = options
15
17
  const keyboard = ref(false)
16
18
  const input = shallowRef<{ $el: HTMLInputElement } | null>(null)
17
- const labels = reactive(new Map<string | number, string>())
18
- watch(
19
- options.options,
20
- options => {
21
- for (const option of flattenOptions(options)) labels.set(option.value, option.label)
22
- },
23
- { immediate: true, deep: true },
24
- )
19
+ const labels = useOptionLabels(options.options, options.selectedOptions)
25
20
 
26
21
  const selected = computed(() =>
27
22
  model.value.map(value => ({ value, label: labels.get(value) ?? String(value) })),
@@ -42,7 +42,13 @@
42
42
  :class="cn(navLink({ active: props.active }), props.class)"
43
43
  >
44
44
  <slot name="icon" />
45
- <span :aria-hidden="rail ? 'true' : undefined" data-hn-label :class="navLinkLabel({ rail })">
45
+ <span
46
+ :aria-hidden="rail ? 'true' : undefined"
47
+ :inert="rail"
48
+ :data-collapsed="rail ? '' : undefined"
49
+ data-hn-label
50
+ :class="navLinkLabel()"
51
+ >
46
52
  <slot />
47
53
  </span>
48
54
  </Primitive>
@@ -17,14 +17,5 @@ export const navLink = tv({
17
17
  })
18
18
 
19
19
  export const navLinkLabel = tv({
20
- base: 'shrink-0 whitespace-nowrap',
21
- variants: {
22
- rail: {
23
- true: 'opacity-0 [transition:opacity_calc(var(--hn-duration-fast)/2)_var(--hn-ease-exit)]',
24
- false: 'opacity-100 [transition:opacity_var(--hn-duration-fast)_var(--hn-ease-enter)]',
25
- },
26
- },
27
- defaultVariants: { rail: false },
20
+ base: 'hn-sidebar-label shrink-0 whitespace-nowrap',
28
21
  })
29
-
30
- export type NavLinkVariants = VariantProps<typeof navLink>
@@ -1,17 +1,18 @@
1
1
  <script setup lang="ts">
2
+ import type { OverlayAnchor, OverlayPositionStrategy } from '../../lib/overlay-anchor'
2
3
  import { computed } from 'vue'
3
4
  import { PopoverRoot, PopoverTrigger, PopoverPortal, type PopoverContentEmits } from 'reka-ui'
4
5
  import PopoverContent from './PopoverContent.vue'
5
6
  import { useAnchoredOverlay } from '../../lib/anchored-overlay'
6
7
  import Card from '../card/Card.vue'
7
- import { useOverlayPortal } from '../../lib/overlay-portal'
8
8
  import { cn } from '../../lib/cn'
9
9
 
10
10
  defineOptions({ name: 'HnPopover', inheritAttrs: false })
11
11
 
12
12
  const props = withDefaults(
13
13
  defineProps<{
14
- anchor?: HTMLElement | null
14
+ anchor?: OverlayAnchor | null
15
+ updatePositionStrategy?: OverlayPositionStrategy
15
16
  modal?: boolean
16
17
  side?: 'top' | 'right' | 'bottom' | 'left'
17
18
  align?: 'start' | 'center' | 'end'
@@ -19,18 +20,26 @@
19
20
  padded?: boolean
20
21
  class?: string
21
22
  }>(),
22
- { side: 'bottom', align: 'center', sideOffset: 8, padded: true, modal: true },
23
+ {
24
+ updatePositionStrategy: 'optimized',
25
+ side: 'bottom',
26
+ align: 'center',
27
+ sideOffset: 8,
28
+ padded: true,
29
+ modal: true,
30
+ },
23
31
  )
24
32
 
25
33
  const open = defineModel<boolean>('open')
26
34
  const emit = defineEmits<PopoverContentEmits>()
27
35
  defineSlots<{ default?(): unknown; content?(): unknown }>()
28
- const { trigger, reference, visible, events } = useAnchoredOverlay(props, open, emit, event =>
29
- emit('openAutoFocus', event),
36
+ const { trigger, content, present, reference, visible, events } = useAnchoredOverlay(
37
+ props,
38
+ open,
39
+ emit,
40
+ event => emit('openAutoFocus', event),
30
41
  )
31
42
 
32
- const { content, present } = useOverlayPortal(visible)
33
-
34
43
  const pressOrigin = computed(() => {
35
44
  if (props.side === 'left') return 'right'
36
45
  if (props.side === 'right') return 'left'
@@ -54,6 +63,7 @@
54
63
  <PopoverContent
55
64
  v-bind="{ ...$attrs, ...events }"
56
65
  :reference="reference"
66
+ :update-position-strategy="props.updatePositionStrategy"
57
67
  as-child
58
68
  :side="props.side"
59
69
  :align="props.align"