@hina-ui/vue 1.3.0 → 1.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 (223) hide show
  1. package/dist/index.js +4326 -2130
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/components/alert-dialog/AlertDialog.vue.d.ts +2 -2
  4. package/dist/types/components/alert-dialog/AlertDialog.vue.d.ts.map +1 -1
  5. package/dist/types/components/dialog/Dialog.vue.d.ts.map +1 -1
  6. package/dist/types/components/dialog/ModalContent.vue.d.ts +21 -0
  7. package/dist/types/components/dialog/ModalContent.vue.d.ts.map +1 -0
  8. package/dist/types/components/dropdown-menu/DropdownMenu.vue.d.ts.map +1 -1
  9. package/dist/types/components/file-upload/FileUpload.vue.d.ts +1 -1
  10. package/dist/types/components/file-upload/FileUpload.vue.d.ts.map +1 -1
  11. package/dist/types/components/hover-card/HoverCard.vue.d.ts +7 -7
  12. package/dist/types/components/hover-card/HoverCard.vue.d.ts.map +1 -1
  13. package/dist/types/components/hover-card/HoverCardAnchor.vue.d.ts +27 -0
  14. package/dist/types/components/hover-card/HoverCardAnchor.vue.d.ts.map +1 -0
  15. package/dist/types/components/hover-card/composables/useHoverCardAnchor.d.ts +17 -0
  16. package/dist/types/components/hover-card/composables/useHoverCardAnchor.d.ts.map +1 -0
  17. package/dist/types/components/image/Image.vue.d.ts +8 -3
  18. package/dist/types/components/image/Image.vue.d.ts.map +1 -1
  19. package/dist/types/components/image/composables/useImage.d.ts.map +1 -1
  20. package/dist/types/components/image/composables/useImageGroupState.d.ts.map +1 -1
  21. package/dist/types/components/image/image.variants.d.ts +23 -1
  22. package/dist/types/components/image/image.variants.d.ts.map +1 -1
  23. package/dist/types/components/lightbox/Lightbox.vue.d.ts.map +1 -1
  24. package/dist/types/components/lightbox/LightboxChrome.vue.d.ts +4 -0
  25. package/dist/types/components/lightbox/LightboxChrome.vue.d.ts.map +1 -1
  26. package/dist/types/components/lightbox/LightboxToolbar.vue.d.ts +4 -0
  27. package/dist/types/components/lightbox/LightboxToolbar.vue.d.ts.map +1 -1
  28. package/dist/types/components/lightbox/composables/useLightboxFrames.d.ts +4 -1
  29. package/dist/types/components/lightbox/composables/useLightboxFrames.d.ts.map +1 -1
  30. package/dist/types/components/lightbox/composables/useLightboxLarge.d.ts +1 -0
  31. package/dist/types/components/lightbox/composables/useLightboxLarge.d.ts.map +1 -1
  32. package/dist/types/components/lightbox/composables/useLightboxLayout.d.ts +17 -0
  33. package/dist/types/components/lightbox/composables/useLightboxLayout.d.ts.map +1 -0
  34. package/dist/types/components/lightbox/composables/useLightboxMotion.d.ts.map +1 -1
  35. package/dist/types/components/lightbox/composables/useLightboxPhase.d.ts +1 -1
  36. package/dist/types/components/lightbox/composables/useLightboxPhase.d.ts.map +1 -1
  37. package/dist/types/components/lightbox/composables/useLightboxPose.d.ts.map +1 -1
  38. package/dist/types/components/lightbox/composables/useLightboxRotation.d.ts +5 -1
  39. package/dist/types/components/lightbox/composables/useLightboxRotation.d.ts.map +1 -1
  40. package/dist/types/components/lightbox/composables/useLightboxZoom.d.ts +10 -1
  41. package/dist/types/components/lightbox/composables/useLightboxZoom.d.ts.map +1 -1
  42. package/dist/types/components/lightbox/lightbox.variants.d.ts +2 -0
  43. package/dist/types/components/lightbox/lightbox.variants.d.ts.map +1 -0
  44. package/dist/types/components/lightbox/types.d.ts +7 -1
  45. package/dist/types/components/lightbox/types.d.ts.map +1 -1
  46. package/dist/types/components/lightbox/utils/clip.d.ts +16 -0
  47. package/dist/types/components/lightbox/utils/clip.d.ts.map +1 -0
  48. package/dist/types/components/lightbox/utils/pose.d.ts +4 -4
  49. package/dist/types/components/lightbox/utils/pose.d.ts.map +1 -1
  50. package/dist/types/components/lightbox/utils/source.d.ts +4 -0
  51. package/dist/types/components/lightbox/utils/source.d.ts.map +1 -0
  52. package/dist/types/components/lightbox/utils/zoom.d.ts +8 -4
  53. package/dist/types/components/lightbox/utils/zoom.d.ts.map +1 -1
  54. package/dist/types/components/pagination/Pagination.vue.d.ts +59 -0
  55. package/dist/types/components/pagination/Pagination.vue.d.ts.map +1 -0
  56. package/dist/types/components/pagination/PaginationContent.vue.d.ts +23 -0
  57. package/dist/types/components/pagination/PaginationContent.vue.d.ts.map +1 -0
  58. package/dist/types/components/pagination/PaginationControl.vue.d.ts +10 -0
  59. package/dist/types/components/pagination/PaginationControl.vue.d.ts.map +1 -0
  60. package/dist/types/components/pagination/PaginationEllipsis.vue.d.ts +23 -0
  61. package/dist/types/components/pagination/PaginationEllipsis.vue.d.ts.map +1 -0
  62. package/dist/types/components/pagination/PaginationInfo.vue.d.ts +24 -0
  63. package/dist/types/components/pagination/PaginationInfo.vue.d.ts.map +1 -0
  64. package/dist/types/components/pagination/PaginationJump.vue.d.ts +7 -0
  65. package/dist/types/components/pagination/PaginationJump.vue.d.ts.map +1 -0
  66. package/dist/types/components/pagination/PaginationPage.vue.d.ts +20 -0
  67. package/dist/types/components/pagination/PaginationPage.vue.d.ts.map +1 -0
  68. package/dist/types/components/pagination/PaginationPages.vue.d.ts +27 -0
  69. package/dist/types/components/pagination/PaginationPages.vue.d.ts.map +1 -0
  70. package/dist/types/components/pagination/PaginationPopup.vue.d.ts +8 -0
  71. package/dist/types/components/pagination/PaginationPopup.vue.d.ts.map +1 -0
  72. package/dist/types/components/pagination/PaginationSize.vue.d.ts +7 -0
  73. package/dist/types/components/pagination/PaginationSize.vue.d.ts.map +1 -0
  74. package/dist/types/components/pagination/composables/usePagination.d.ts +25 -0
  75. package/dist/types/components/pagination/composables/usePagination.d.ts.map +1 -0
  76. package/dist/types/components/pagination/composables/usePaginationEllipsis.d.ts +34 -0
  77. package/dist/types/components/pagination/composables/usePaginationEllipsis.d.ts.map +1 -0
  78. package/dist/types/components/pagination/composables/usePaginationJump.d.ts +8 -0
  79. package/dist/types/components/pagination/composables/usePaginationJump.d.ts.map +1 -0
  80. package/dist/types/components/pagination/composables/usePaginationPageTooltip.d.ts +3 -0
  81. package/dist/types/components/pagination/composables/usePaginationPageTooltip.d.ts.map +1 -0
  82. package/dist/types/components/pagination/composables/usePaginationWindow.d.ts +188 -0
  83. package/dist/types/components/pagination/composables/usePaginationWindow.d.ts.map +1 -0
  84. package/dist/types/components/pagination/context.d.ts +19 -0
  85. package/dist/types/components/pagination/context.d.ts.map +1 -0
  86. package/dist/types/components/pagination/pagination.variants.d.ts +68 -0
  87. package/dist/types/components/pagination/pagination.variants.d.ts.map +1 -0
  88. package/dist/types/components/pagination/types.d.ts +23 -0
  89. package/dist/types/components/pagination/types.d.ts.map +1 -0
  90. package/dist/types/components/pagination/utils/ranges.d.ts +3 -0
  91. package/dist/types/components/pagination/utils/ranges.d.ts.map +1 -0
  92. package/dist/types/components/popover/Popover.vue.d.ts.map +1 -1
  93. package/dist/types/components/range-slider/RangeSlider.vue.d.ts +1 -0
  94. package/dist/types/components/range-slider/RangeSlider.vue.d.ts.map +1 -1
  95. package/dist/types/components/scroll-area/composables/useLayerLock.d.ts.map +1 -1
  96. package/dist/types/components/slider/Slider.vue.d.ts +1 -0
  97. package/dist/types/components/slider/Slider.vue.d.ts.map +1 -1
  98. package/dist/types/components/slider/slider.variants.d.ts.map +1 -1
  99. package/dist/types/components/tooltip/Tooltip.vue.d.ts +4 -15
  100. package/dist/types/components/tooltip/Tooltip.vue.d.ts.map +1 -1
  101. package/dist/types/components/tooltip/TooltipBubble.vue.d.ts +20 -0
  102. package/dist/types/components/tooltip/TooltipBubble.vue.d.ts.map +1 -0
  103. package/dist/types/components/tooltip/TooltipDirectiveRoot.d.ts +33 -0
  104. package/dist/types/components/tooltip/TooltipDirectiveRoot.d.ts.map +1 -0
  105. package/dist/types/components/tooltip/composables/useTooltipTarget.d.ts +2 -0
  106. package/dist/types/components/tooltip/composables/useTooltipTarget.d.ts.map +1 -0
  107. package/dist/types/components/tooltip/directive.d.ts +4 -0
  108. package/dist/types/components/tooltip/directive.d.ts.map +1 -0
  109. package/dist/types/components/tooltip/types.d.ts +14 -0
  110. package/dist/types/components/tooltip/types.d.ts.map +1 -0
  111. package/dist/types/components/tooltip/utils/directiveContext.d.ts +10 -0
  112. package/dist/types/components/tooltip/utils/directiveContext.d.ts.map +1 -0
  113. package/dist/types/components/tree-select/TreeSelect.vue.d.ts +6 -1
  114. package/dist/types/components/tree-select/TreeSelect.vue.d.ts.map +1 -1
  115. package/dist/types/components/tree-select/TreeSelectSearch.vue.d.ts +22 -0
  116. package/dist/types/components/tree-select/TreeSelectSearch.vue.d.ts.map +1 -0
  117. package/dist/types/components/tree-select/composables/useTreeSelect.d.ts +35 -0
  118. package/dist/types/components/tree-select/composables/useTreeSelect.d.ts.map +1 -0
  119. package/dist/types/components/tree-select/tree-select.variants.d.ts +1 -1
  120. package/dist/types/components/tree-select/tree-select.variants.d.ts.map +1 -1
  121. package/dist/types/components/tree-select/utils/filterTree.d.ts +7 -0
  122. package/dist/types/components/tree-select/utils/filterTree.d.ts.map +1 -0
  123. package/dist/types/index.d.ts +10 -0
  124. package/dist/types/index.d.ts.map +1 -1
  125. package/dist/types/lib/anchored-overlay.d.ts +3 -7
  126. package/dist/types/lib/anchored-overlay.d.ts.map +1 -1
  127. package/dist/types/lib/dialog-focus.d.ts +2 -0
  128. package/dist/types/lib/dialog-focus.d.ts.map +1 -0
  129. package/dist/types/lib/overlay-portal.d.ts +6 -0
  130. package/dist/types/lib/overlay-portal.d.ts.map +1 -0
  131. package/dist/types/lib/overlay-positioner.d.ts +3 -0
  132. package/dist/types/lib/overlay-positioner.d.ts.map +1 -0
  133. package/dist/types/lib/pointer-corridor.d.ts +7 -0
  134. package/dist/types/lib/pointer-corridor.d.ts.map +1 -0
  135. package/dist/types/lib/useDirection.d.ts +7 -0
  136. package/dist/types/lib/useDirection.d.ts.map +1 -0
  137. package/dist/types/locale/en-US.d.ts.map +1 -1
  138. package/dist/types/locale/index.d.ts.map +1 -1
  139. package/dist/types/locale/types.d.ts +13 -0
  140. package/dist/types/locale/types.d.ts.map +1 -1
  141. package/dist/types/locale/zh-CN.d.ts.map +1 -1
  142. package/package.json +1 -1
  143. package/src/components/alert-dialog/AlertDialog.vue +7 -4
  144. package/src/components/badge/badge.variants.ts +1 -1
  145. package/src/components/dialog/Dialog.vue +7 -4
  146. package/src/components/dialog/ModalContent.vue +19 -0
  147. package/src/components/dropdown-menu/DropdownMenu.vue +1 -1
  148. package/src/components/hover-card/HoverCard.vue +34 -16
  149. package/src/components/hover-card/HoverCardAnchor.vue +16 -0
  150. package/src/components/hover-card/composables/useHoverCardAnchor.ts +160 -0
  151. package/src/components/image/Image.vue +11 -12
  152. package/src/components/image/composables/useImage.ts +6 -3
  153. package/src/components/image/composables/useImageGroupState.ts +9 -1
  154. package/src/components/image/image.variants.ts +13 -1
  155. package/src/components/lightbox/Lightbox.vue +23 -17
  156. package/src/components/lightbox/LightboxChrome.vue +7 -1
  157. package/src/components/lightbox/LightboxToolbar.vue +16 -2
  158. package/src/components/lightbox/composables/useLightboxFrames.ts +108 -12
  159. package/src/components/lightbox/composables/useLightboxLarge.ts +4 -1
  160. package/src/components/lightbox/composables/useLightboxLayout.ts +44 -0
  161. package/src/components/lightbox/composables/useLightboxMotion.ts +3 -1
  162. package/src/components/lightbox/composables/useLightboxPhase.ts +12 -4
  163. package/src/components/lightbox/composables/useLightboxPose.ts +20 -19
  164. package/src/components/lightbox/composables/useLightboxRotation.ts +14 -4
  165. package/src/components/lightbox/composables/useLightboxZoom.ts +51 -9
  166. package/src/components/lightbox/lightbox.variants.ts +5 -0
  167. package/src/components/lightbox/types.ts +5 -1
  168. package/src/components/lightbox/utils/clip.ts +117 -0
  169. package/src/components/lightbox/utils/pose.ts +36 -19
  170. package/src/components/lightbox/utils/source.ts +17 -0
  171. package/src/components/lightbox/utils/zoom.ts +21 -7
  172. package/src/components/pagination/Pagination.vue +118 -0
  173. package/src/components/pagination/PaginationContent.vue +29 -0
  174. package/src/components/pagination/PaginationControl.vue +42 -0
  175. package/src/components/pagination/PaginationEllipsis.vue +67 -0
  176. package/src/components/pagination/PaginationInfo.vue +18 -0
  177. package/src/components/pagination/PaginationJump.vue +32 -0
  178. package/src/components/pagination/PaginationPage.vue +34 -0
  179. package/src/components/pagination/PaginationPages.vue +43 -0
  180. package/src/components/pagination/PaginationPopup.vue +92 -0
  181. package/src/components/pagination/PaginationSize.vue +30 -0
  182. package/src/components/pagination/composables/usePagination.ts +76 -0
  183. package/src/components/pagination/composables/usePaginationEllipsis.ts +269 -0
  184. package/src/components/pagination/composables/usePaginationJump.ts +23 -0
  185. package/src/components/pagination/composables/usePaginationPageTooltip.ts +23 -0
  186. package/src/components/pagination/composables/usePaginationWindow.ts +116 -0
  187. package/src/components/pagination/context.ts +23 -0
  188. package/src/components/pagination/pagination.variants.ts +32 -0
  189. package/src/components/pagination/types.ts +19 -0
  190. package/src/components/pagination/utils/ranges.ts +12 -0
  191. package/src/components/popover/Popover.vue +6 -2
  192. package/src/components/range-slider/RangeSlider.vue +6 -0
  193. package/src/components/scroll-area/composables/useLayerLock.ts +8 -9
  194. package/src/components/scroll-area/composables/useWheelRedirect.ts +1 -1
  195. package/src/components/skeleton/LICENSE +24 -0
  196. package/src/components/slider/Slider.vue +6 -0
  197. package/src/components/slider/slider.variants.ts +1 -0
  198. package/src/components/tooltip/Tooltip.vue +10 -33
  199. package/src/components/tooltip/TooltipBubble.vue +36 -0
  200. package/src/components/tooltip/TooltipDirectiveRoot.ts +61 -0
  201. package/src/components/tooltip/composables/useTooltipTarget.ts +56 -0
  202. package/src/components/tooltip/directive.ts +54 -0
  203. package/src/components/tooltip/types.ts +15 -0
  204. package/src/components/tooltip/utils/directiveContext.ts +32 -0
  205. package/src/components/tree-select/TreeSelect.vue +90 -61
  206. package/src/components/tree-select/TreeSelectSearch.vue +48 -0
  207. package/src/components/tree-select/composables/useTreeSelect.ts +130 -0
  208. package/src/components/tree-select/tree-select.variants.ts +2 -2
  209. package/src/components/tree-select/utils/filterTree.ts +19 -0
  210. package/src/index.ts +10 -0
  211. package/src/lib/anchored-overlay.ts +2 -5
  212. package/src/lib/dialog-focus.ts +13 -0
  213. package/src/lib/overlay-portal.ts +8 -0
  214. package/src/lib/overlay-positioner.ts +25 -0
  215. package/src/lib/pointer-corridor.ts +26 -0
  216. package/src/lib/useDirection.ts +31 -0
  217. package/src/locale/en-US.ts +15 -1
  218. package/src/locale/index.ts +1 -0
  219. package/src/locale/types.ts +13 -0
  220. package/src/locale/zh-CN.ts +15 -1
  221. package/src/styles/interaction.css +26 -3
  222. package/src/styles/motion.css +14 -2
  223. package/src/styles/semantic.css +5 -1
@@ -0,0 +1,54 @@
1
+ import { h, render, type AppContext, type DirectiveBinding, type ObjectDirective } from 'vue'
2
+ import TooltipDirectiveRoot from './TooltipDirectiveRoot'
3
+ import { directiveContext } from './utils/directiveContext'
4
+ import type { TooltipDirectiveOptions, TooltipDirectiveValue } from './types'
5
+
6
+ interface TooltipMount {
7
+ container: HTMLElement
8
+ context: AppContext
9
+ revision: number
10
+ }
11
+
12
+ const mounts = new WeakMap<HTMLElement, TooltipMount>()
13
+ let nextId = 0
14
+
15
+ function options(value: TooltipDirectiveValue): TooltipDirectiveOptions {
16
+ return typeof value === 'string' ? { content: value } : value ? { ...value } : { content: '' }
17
+ }
18
+
19
+ function update(target: HTMLElement, binding: DirectiveBinding<TooltipDirectiveValue>) {
20
+ const mount = mounts.get(target)
21
+ if (!mount) return
22
+ const vnode = h(TooltipDirectiveRoot, {
23
+ target,
24
+ options: options(binding.value),
25
+ revision: ++mount.revision,
26
+ })
27
+ vnode.appContext = mount.context
28
+ render(vnode, mount.container)
29
+ }
30
+
31
+ export const vTooltip: ObjectDirective<HTMLElement, TooltipDirectiveValue> = {
32
+ deep: true,
33
+ mounted(target, binding) {
34
+ const owner = binding.instance?.$
35
+ if (!owner) return
36
+ const context = directiveContext(owner, target)
37
+ context.config = Object.create(context.config) as AppContext['config']
38
+ context.config.idPrefix = 'hn-tooltip-' + ++nextId
39
+ mounts.set(target, {
40
+ container: target.ownerDocument.createElement('div'),
41
+ context,
42
+ revision: 0,
43
+ })
44
+ update(target, binding)
45
+ },
46
+ updated: update,
47
+ beforeUnmount(target) {
48
+ const mount = mounts.get(target)
49
+ if (!mount) return
50
+ render(null, mount.container)
51
+ mounts.delete(target)
52
+ },
53
+ getSSRProps: () => ({}),
54
+ }
@@ -0,0 +1,15 @@
1
+ export interface TooltipProps {
2
+ content?: string
3
+ side?: 'top' | 'right' | 'bottom' | 'left'
4
+ align?: 'start' | 'center' | 'end'
5
+ sideOffset?: number
6
+ open?: boolean
7
+ disabled?: boolean
8
+ class?: string
9
+ }
10
+
11
+ export type TooltipDirectiveOptions = Omit<TooltipProps, 'open' | 'content'> & {
12
+ content: string
13
+ }
14
+
15
+ export type TooltipDirectiveValue = string | TooltipDirectiveOptions | null | undefined | false
@@ -0,0 +1,32 @@
1
+ import { isVNode, type AppContext, type ComponentInternalInstance, type VNode } from 'vue'
2
+
3
+ type ContextOwner = ComponentInternalInstance & { provides: AppContext['provides'] }
4
+
5
+ function findOwner(
6
+ node: VNode,
7
+ target: HTMLElement,
8
+ owner: ComponentInternalInstance,
9
+ ): ComponentInternalInstance | undefined {
10
+ if (node.component) {
11
+ return findOwner(node.component.subTree, target, node.component)
12
+ }
13
+ if (node.suspense) {
14
+ for (const branch of [node.suspense.activeBranch, node.suspense.pendingBranch]) {
15
+ const match = branch && findOwner(branch, target, owner)
16
+ if (match) return match
17
+ }
18
+ }
19
+ if (node.el === target) return owner
20
+ if (Array.isArray(node.children)) {
21
+ for (const child of node.children) {
22
+ if (!isVNode(child)) continue
23
+ const match = findOwner(child, target, owner)
24
+ if (match) return match
25
+ }
26
+ }
27
+ }
28
+
29
+ export function directiveContext(owner: ComponentInternalInstance, target: HTMLElement) {
30
+ const parent = (findOwner(owner.subTree, target, owner) ?? owner) as ContextOwner
31
+ return { ...parent.appContext, provides: parent.provides }
32
+ }
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, ref, watch } from 'vue'
2
+ import { useId } from 'vue'
3
3
  import { Check } from '@lucide/vue'
4
4
  import {
5
5
  PopoverContent,
@@ -29,7 +29,9 @@
29
29
  treeSelectRow,
30
30
  treeSelectToggle,
31
31
  } from './tree-select.variants'
32
- import { findNode, pathTo, type TreeSelectNode } from './types'
32
+ import type { TreeSelectNode } from './types'
33
+ import TreeSelectSearch from './TreeSelectSearch.vue'
34
+ import { useTreeSelect } from './composables/useTreeSelect'
33
35
 
34
36
  defineOptions({ name: 'HnTreeSelect', inheritAttrs: false })
35
37
 
@@ -37,6 +39,8 @@
37
39
  defineProps<{
38
40
  items: TreeSelectNode[]
39
41
  placeholder?: string
42
+ searchable?: boolean
43
+ searchPlaceholder?: string
40
44
  defaultExpanded?: Array<string | number>
41
45
  variant?: InputVariants['variant']
42
46
  size?: InputVariants['size']
@@ -49,6 +53,9 @@
49
53
 
50
54
  const model = defineModel<string | number | null>()
51
55
  const open = defineModel<boolean>('open', { default: false })
56
+ const search = defineModel<string>('search', { default: '' })
57
+ const treeId = useId()
58
+ const triggerId = useId()
52
59
 
53
60
  defineSlots<{ node(props: { node: TreeSelectNode }): unknown }>()
54
61
 
@@ -60,82 +67,102 @@
60
67
  invalid,
61
68
  disabled,
62
69
  describedBy,
70
+ labelledBy,
63
71
  } = useFieldControl({
64
72
  invalid: () => props.invalid || !!group?.invalid.value,
65
73
  disabled: () => props.disabled || !!group?.disabled.value,
66
74
  })
67
- const selected = computed(() => findNode(props.items, model.value))
68
- const expanded = ref<string[]>([])
69
-
70
- watch(
71
- open,
72
- isOpen => {
73
- if (!isOpen) return
74
- const path = pathTo(props.items, model.value) ?? []
75
- expanded.value = Array.from(
76
- new Set([...props.defaultExpanded, ...path].map(value => String(value))),
77
- )
78
- },
79
- { immediate: true },
80
- )
81
-
82
- function key(node: TreeSelectNode) {
83
- return String(node.value)
84
- }
85
-
86
- function choose(node: TreeSelectNode | undefined) {
87
- if (!node || node.disabled) return
88
- model.value = node.value
89
- open.value = false
90
- }
91
-
92
- function keepRowClick(event: CustomEvent<{ originalEvent: Event }>) {
93
- if (event.detail.originalEvent.type === 'click') event.preventDefault()
94
- }
75
+ const {
76
+ selected,
77
+ items,
78
+ expanded,
79
+ input,
80
+ tree,
81
+ getChildren,
82
+ key,
83
+ choose,
84
+ keepRowClick,
85
+ focusSearch,
86
+ clearSearch,
87
+ onEscape,
88
+ onSearchKeydown,
89
+ onTreeKeydown,
90
+ } = useTreeSelect(props, model, open, search)
95
91
  </script>
96
92
 
97
93
  <template>
98
94
  <PopoverRoot v-model:open="open" modal>
99
- <PopoverTrigger
100
- v-bind="$attrs"
101
- :id="fieldId"
102
- :aria-describedby="describedBy"
103
- data-hn-tree-select
104
- role="combobox"
105
- :aria-expanded="open"
106
- :disabled="disabled"
107
- :data-placeholder="selected ? undefined : ''"
108
- :data-invalid="invalid ? '' : undefined"
109
- :data-disabled="disabled ? '' : undefined"
110
- :aria-invalid="invalid || undefined"
111
- :class="
112
- cn(
113
- group ? inputEmbedded() : inputHost({ variant: props.variant, size: props.size }),
114
- selectTrigger(),
115
- props.class,
116
- )
117
- "
118
- >
119
- <span class="min-w-0 flex-1 truncate">
120
- {{ selected ? selected.label : (props.placeholder ?? t.select.placeholder) }}
121
- </span>
122
- <span :class="inputAdornment()">
123
- <DisclosureIcon />
124
- </span>
95
+ <PopoverTrigger as-child>
96
+ <button
97
+ type="button"
98
+ v-bind="$attrs"
99
+ :id="fieldId ?? triggerId"
100
+ :aria-describedby="describedBy"
101
+ data-hn-tree-select
102
+ role="combobox"
103
+ :aria-expanded="open"
104
+ :disabled="disabled"
105
+ :data-placeholder="selected ? undefined : ''"
106
+ :data-invalid="invalid ? '' : undefined"
107
+ :data-disabled="disabled ? '' : undefined"
108
+ :aria-invalid="invalid || undefined"
109
+ :class="
110
+ cn(
111
+ group ? inputEmbedded() : inputHost({ variant: props.variant, size: props.size }),
112
+ selectTrigger(),
113
+ props.class,
114
+ )
115
+ "
116
+ >
117
+ <span class="min-w-0 flex-1 truncate">
118
+ {{ selected ? selected.label : (props.placeholder ?? t.select.placeholder) }}
119
+ </span>
120
+ <span :class="inputAdornment()">
121
+ <DisclosureIcon />
122
+ </span>
123
+ </button>
125
124
  </PopoverTrigger>
126
125
  <PopoverPortal>
127
- <PopoverContent as-child align="start" :side-offset="8">
128
- <Card :padded="false" data-hn-tree-select-content :class="treeSelectContent()">
126
+ <PopoverContent
127
+ as-child
128
+ align="start"
129
+ :side-offset="8"
130
+ @open-auto-focus="focusSearch"
131
+ @escape-key-down="onEscape"
132
+ >
133
+ <Card
134
+ :padded="false"
135
+ data-hn-tree-select-content
136
+ :aria-label="$attrs['aria-label'] as string | undefined"
137
+ :aria-labelledby="
138
+ labelledBy ?? ($attrs['aria-label'] ? undefined : (fieldId ?? triggerId))
139
+ "
140
+ :class="treeSelectContent()"
141
+ >
142
+ <TreeSelectSearch
143
+ v-if="props.searchable"
144
+ ref="input"
145
+ v-model="search"
146
+ :placeholder="props.searchPlaceholder ?? t.treeSelect.search"
147
+ :controls="treeId"
148
+ @clear="clearSearch"
149
+ @keydown="onSearchKeydown"
150
+ />
129
151
  <ScrollArea :class="treeSelectList()">
130
152
  <TreeRoot
153
+ :id="treeId"
154
+ ref="tree"
131
155
  v-slot="{ flattenItems }"
132
156
  v-model:expanded="expanded"
133
- :items="props.items"
157
+ :items="items"
134
158
  :get-key="key"
135
- :get-children="node => node.children"
159
+ :get-children="getChildren"
136
160
  :model-value="selected"
161
+ :aria-label="$attrs['aria-label'] as string | undefined"
162
+ :aria-labelledby="labelledBy"
137
163
  class="flex flex-col p-1 outline-none"
138
164
  @update:model-value="choose"
165
+ @keydown.capture="onTreeKeydown"
139
166
  >
140
167
  <TreeItem
141
168
  v-for="item in flattenItems"
@@ -169,7 +196,9 @@
169
196
  <Check v-if="isSelected" />
170
197
  </span>
171
198
  </TreeItem>
172
- <div v-if="props.items.length === 0" :class="selectEmpty()">{{ t.select.empty }}</div>
199
+ <li v-if="items.length === 0" role="none" :class="selectEmpty()">
200
+ <span role="status">{{ t.select.empty }}</span>
201
+ </li>
173
202
  </TreeRoot>
174
203
  </ScrollArea>
175
204
  </Card>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ import { shallowRef } from 'vue'
3
+ import { Search, X } from '@lucide/vue'
4
+ import { useUiLocale } from '../../locale'
5
+ import InputAction from '../input/InputAction.vue'
6
+ import { inputAdornment, inputControl, textInputHost } from '../input/input.variants'
7
+
8
+ defineOptions({ name: 'HnTreeSelectSearch', inheritAttrs: false })
9
+
10
+ const props = defineProps<{ placeholder: string; controls: string }>()
11
+ const emit = defineEmits<{ clear: []; keydown: [event: KeyboardEvent] }>()
12
+ const model = defineModel<string>({ required: true })
13
+ const t = useUiLocale()
14
+ const input = shallowRef<HTMLInputElement | null>(null)
15
+
16
+ defineExpose({ focus: () => input.value?.focus() })
17
+ </script>
18
+
19
+ <template>
20
+ <div class="border-line shrink-0 border-b px-1">
21
+ <div :class="textInputHost({ variant: 'bare', size: 'sm' })">
22
+ <span :class="inputAdornment()"><Search /></span>
23
+ <input
24
+ ref="input"
25
+ v-model="model"
26
+ data-hn-tree-select-search
27
+ type="text"
28
+ role="searchbox"
29
+ autocomplete="off"
30
+ :aria-label="props.placeholder"
31
+ :aria-controls="props.controls"
32
+ :placeholder="props.placeholder"
33
+ :class="inputControl({ leading: true, trailing: !!model })"
34
+ @keydown="emit('keydown', $event)"
35
+ />
36
+ <Transition
37
+ enter-active-class="hn-transition-base"
38
+ enter-from-class="scale-90 opacity-0"
39
+ leave-active-class="hn-transition"
40
+ leave-to-class="scale-90 opacity-0"
41
+ >
42
+ <InputAction v-if="model" :label="t.common.clear" @click="emit('clear')">
43
+ <X />
44
+ </InputAction>
45
+ </Transition>
46
+ </div>
47
+ </div>
48
+ </template>
@@ -0,0 +1,130 @@
1
+ import { computed, ref, shallowRef, watch, type Ref } from 'vue'
2
+ import { useFilter } from 'reka-ui'
3
+ import { findNode, pathTo, type TreeSelectNode } from '../types'
4
+ import { filterTree } from '../utils/filterTree'
5
+
6
+ interface TreeSelectOptions {
7
+ items: TreeSelectNode[]
8
+ defaultExpanded: Array<string | number>
9
+ searchable?: boolean
10
+ }
11
+
12
+ export function useTreeSelect(
13
+ props: TreeSelectOptions,
14
+ model: Ref<string | number | null | undefined>,
15
+ open: Ref<boolean>,
16
+ search: Ref<string>,
17
+ ) {
18
+ const selected = computed(() => findNode(props.items, model.value))
19
+ const normalExpanded = ref<string[]>([])
20
+ const searchExpanded = ref<string[]>([])
21
+ const input = shallowRef<{ focus: () => void } | null>(null)
22
+ const tree = shallowRef<{ $el: HTMLElement } | null>(null)
23
+ const { contains } = useFilter({ sensitivity: 'base' })
24
+ const query = computed(() => (props.searchable ? search.value.trim() : ''))
25
+ const filtered = computed(() =>
26
+ query.value ? filterTree(props.items, node => contains(node.label, query.value)) : null,
27
+ )
28
+ const items = computed(() => filtered.value?.items ?? props.items)
29
+ const expanded = computed({
30
+ get: () => (filtered.value ? searchExpanded.value : normalExpanded.value),
31
+ set: value => {
32
+ if (filtered.value) searchExpanded.value = value
33
+ else normalExpanded.value = value
34
+ },
35
+ })
36
+
37
+ watch(
38
+ filtered,
39
+ value => {
40
+ searchExpanded.value = value?.expanded ?? []
41
+ },
42
+ { immediate: true },
43
+ )
44
+
45
+ watch(
46
+ open,
47
+ (isOpen, wasOpen) => {
48
+ if (!isOpen) {
49
+ if (wasOpen) search.value = ''
50
+ return
51
+ }
52
+ const path = pathTo(props.items, model.value) ?? []
53
+ normalExpanded.value = Array.from(new Set([...props.defaultExpanded, ...path].map(String)))
54
+ },
55
+ { immediate: true },
56
+ )
57
+
58
+ function getChildren(node: TreeSelectNode) {
59
+ return filtered.value ? filtered.value.children.get(node) : node.children
60
+ }
61
+
62
+ function key(node: TreeSelectNode) {
63
+ return String(node.value)
64
+ }
65
+
66
+ function choose(node: TreeSelectNode | undefined) {
67
+ if (!node || node.disabled) return
68
+ model.value = node.value
69
+ open.value = false
70
+ }
71
+
72
+ function keepRowClick(event: CustomEvent<{ originalEvent: Event }>) {
73
+ if (event.detail.originalEvent.type === 'click') event.preventDefault()
74
+ }
75
+
76
+ function focusSearch(event: Event) {
77
+ if (!props.searchable) return
78
+ event.preventDefault()
79
+ input.value?.focus()
80
+ }
81
+
82
+ function clearSearch() {
83
+ search.value = ''
84
+ input.value?.focus()
85
+ }
86
+
87
+ function onEscape(event: KeyboardEvent) {
88
+ if (!props.searchable || !search.value) return
89
+ event.preventDefault()
90
+ clearSearch()
91
+ }
92
+
93
+ function enabledRows() {
94
+ return Array.from(
95
+ tree.value?.$el.querySelectorAll<HTMLElement>('[role="treeitem"]:not([data-disabled])') ?? [],
96
+ )
97
+ }
98
+
99
+ function onSearchKeydown(event: KeyboardEvent) {
100
+ if (event.isComposing || !['ArrowDown', 'ArrowUp'].includes(event.key)) return
101
+ event.preventDefault()
102
+ const rows = enabledRows()
103
+ const target = event.key === 'ArrowDown' ? rows[0] : rows.at(-1)
104
+ target?.focus()
105
+ }
106
+
107
+ function onTreeKeydown(event: KeyboardEvent) {
108
+ if (!props.searchable || event.key !== 'ArrowUp' || event.target !== enabledRows()[0]) return
109
+ event.preventDefault()
110
+ event.stopPropagation()
111
+ input.value?.focus()
112
+ }
113
+
114
+ return {
115
+ selected,
116
+ items,
117
+ expanded,
118
+ input,
119
+ tree,
120
+ getChildren,
121
+ key,
122
+ choose,
123
+ keepRowClick,
124
+ focusSearch,
125
+ clearSearch,
126
+ onEscape,
127
+ onSearchKeydown,
128
+ onTreeKeydown,
129
+ }
130
+ }
@@ -2,12 +2,12 @@ import { tv } from '../../lib/tv'
2
2
 
3
3
  export const treeSelectContent = tv({
4
4
  base: [
5
- 'hn-anim-pop z-(--hn-z-overlay) flex w-[var(--reka-popover-trigger-width)] min-w-40 flex-col overflow-hidden shadow-md outline-none',
5
+ 'hn-anim-pop z-(--hn-z-overlay) flex w-[var(--reka-popover-trigger-width)] min-w-40 max-h-[var(--reka-popover-content-available-height)] flex-col overflow-hidden shadow-md outline-none',
6
6
  ],
7
7
  })
8
8
 
9
9
  export const treeSelectList = tv({
10
- base: 'max-h-[min(20rem,var(--reka-popover-content-available-height))]',
10
+ base: 'min-h-0 max-h-80',
11
11
  })
12
12
 
13
13
  export const treeSelectRow = tv({
@@ -0,0 +1,19 @@
1
+ import type { TreeSelectNode } from '../types'
2
+
3
+ export function filterTree(nodes: TreeSelectNode[], matches: (node: TreeSelectNode) => boolean) {
4
+ const children = new Map<TreeSelectNode, TreeSelectNode[]>()
5
+ const expanded: string[] = []
6
+
7
+ function visit(items: TreeSelectNode[]): TreeSelectNode[] {
8
+ return items.filter(node => {
9
+ const descendants = visit(node.children ?? [])
10
+ if (descendants.length) {
11
+ children.set(node, descendants)
12
+ expanded.push(String(node.value))
13
+ }
14
+ return descendants.length > 0 || matches(node)
15
+ })
16
+ }
17
+
18
+ return { items: visit(nodes), children, expanded }
19
+ }
package/src/index.ts CHANGED
@@ -25,6 +25,12 @@ export { default as Badge } from './components/badge/Badge.vue'
25
25
  export { default as Indicator } from './components/indicator/Indicator.vue'
26
26
  export { default as Callout } from './components/callout/Callout.vue'
27
27
  export { default as Alert } from './components/alert/Alert.vue'
28
+ export { default as Pagination } from './components/pagination/Pagination.vue'
29
+ export { default as PaginationContent } from './components/pagination/PaginationContent.vue'
30
+ export { default as PaginationInfo } from './components/pagination/PaginationInfo.vue'
31
+ export { default as PaginationSize } from './components/pagination/PaginationSize.vue'
32
+ export { default as PaginationJump } from './components/pagination/PaginationJump.vue'
33
+ export type { PaginationChange, PaginationState } from './components/pagination/types'
28
34
  export { default as PrevNext } from './components/prev-next/PrevNext.vue'
29
35
  export { default as PrevNextLink } from './components/prev-next/PrevNextLink.vue'
30
36
  export { default as Table } from './components/table/Table.vue'
@@ -206,6 +212,8 @@ export { default as BreadcrumbItem } from './components/breadcrumb/BreadcrumbIte
206
212
  export { default as BreadcrumbSeparator } from './components/breadcrumb/BreadcrumbSeparator.vue'
207
213
  export { default as TooltipProvider } from './components/tooltip/TooltipProvider.vue'
208
214
  export { default as Tooltip } from './components/tooltip/Tooltip.vue'
215
+ export { vTooltip } from './components/tooltip/directive'
216
+ export type { TooltipDirectiveOptions, TooltipDirectiveValue } from './components/tooltip/types'
209
217
  export { default as Popover } from './components/popover/Popover.vue'
210
218
  export { default as DropdownMenu } from './components/dropdown-menu/DropdownMenu.vue'
211
219
  export { default as DropdownMenuItem } from './components/dropdown-menu/DropdownMenuItem.vue'
@@ -227,6 +235,8 @@ export { default as Spoiler } from './components/spoiler/Spoiler.vue'
227
235
  export { default as Skeleton } from './components/skeleton/Skeleton.vue'
228
236
  export { default as Image } from './components/image/Image.vue'
229
237
  export { default as ImageGroup } from './components/image/ImageGroup.vue'
238
+ export { default as Lightbox } from './components/lightbox/Lightbox.vue'
239
+ export type { LightboxItem, LightboxSource } from './components/lightbox/types'
230
240
  export { provideImageResolver, useImageResolver } from './components/image/resolver'
231
241
  export type { ImageResolver } from './components/image/resolver'
232
242
  export type { ImageVariants } from './components/image/image.variants'
@@ -1,5 +1,5 @@
1
1
  import { computed, nextTick, shallowRef, watch, type Ref } from 'vue'
2
- import type { DropdownMenuContentEmits } from 'reka-ui'
2
+ import { useForwardExpose, type DropdownMenuContentEmits } from 'reka-ui'
3
3
 
4
4
  export function useAnchoredOverlay(
5
5
  props: { anchor?: HTMLElement | null; modal: boolean },
@@ -10,10 +10,7 @@ export function useAnchoredOverlay(
10
10
  ) => void,
11
11
  openAutoFocus?: (event: Event) => void,
12
12
  ) {
13
- const trigger = shallowRef<{ $el: HTMLElement } | null>(null)
14
- const triggerElement = computed(() =>
15
- trigger.value?.$el?.nodeType === 1 ? trigger.value.$el : undefined,
16
- )
13
+ const { forwardRef: trigger, currentElement: triggerElement } = useForwardExpose()
17
14
  const retainedAnchor = shallowRef<HTMLElement>()
18
15
  const reference = computed(
19
16
  () =>
@@ -0,0 +1,13 @@
1
+ import { injectDialogRootContext } from 'reka-ui'
2
+
3
+ export function useDialogCloseFocus(onCloseAutoFocus: (event: Event) => void) {
4
+ const context = injectDialogRootContext()
5
+
6
+ return (event: Event) => {
7
+ onCloseAutoFocus(event)
8
+ if (event.defaultPrevented) return
9
+ event.preventDefault()
10
+ const target = context.triggerElement.value
11
+ if (target?.isConnected) target.focus({ preventScroll: true })
12
+ }
13
+ }
@@ -0,0 +1,8 @@
1
+ import { computed, shallowRef, type ComponentPublicInstance, type Ref } from 'vue'
2
+
3
+ export function useOverlayPortal(open: Ref<boolean | undefined>) {
4
+ const content = shallowRef<ComponentPublicInstance | null>(null)
5
+ const present = computed(() => !!open.value || !!content.value)
6
+
7
+ return { content, present }
8
+ }
@@ -0,0 +1,25 @@
1
+ import {
2
+ toValue,
3
+ watchEffect,
4
+ type ComponentPublicInstance,
5
+ type MaybeRefOrGetter,
6
+ type Ref,
7
+ } from 'vue'
8
+
9
+ export function useOverlayPositionerClass(
10
+ content: Ref<ComponentPublicInstance | null>,
11
+ className: MaybeRefOrGetter<string | undefined>,
12
+ ) {
13
+ watchEffect(
14
+ onCleanup => {
15
+ const element = content.value?.$el as HTMLElement | undefined
16
+ const positioner = element?.parentElement
17
+ const classes = toValue(className)?.split(/\s+/).filter(Boolean) ?? []
18
+ if (!positioner?.hasAttribute('data-reka-popper-content-wrapper') || !classes.length) return
19
+ const added = classes.filter(name => !positioner.classList.contains(name))
20
+ positioner.classList.add(...added)
21
+ onCleanup(() => positioner.classList.remove(...added))
22
+ },
23
+ { flush: 'post' },
24
+ )
25
+ }
@@ -0,0 +1,26 @@
1
+ type Point = { x: number; y: number }
2
+ const cross = (a: Point, b: Point, c: Point) =>
3
+ (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
4
+
5
+ export function inPointerCorridor(point: Point, first: DOMRect, second: DOMRect) {
6
+ const points = [first, second]
7
+ .flatMap(rect => [
8
+ { x: rect.left - 6, y: rect.top - 6 },
9
+ { x: rect.right + 6, y: rect.top - 6 },
10
+ { x: rect.left - 6, y: rect.bottom + 6 },
11
+ { x: rect.right + 6, y: rect.bottom + 6 },
12
+ ])
13
+ .sort((a, b) => a.x - b.x || a.y - b.y)
14
+ const half = (values: Point[]) => {
15
+ const hull: Point[] = []
16
+ for (const value of values) {
17
+ while (hull.length > 1 && cross(hull[hull.length - 2]!, hull[hull.length - 1]!, value) <= 0)
18
+ hull.pop()
19
+ hull.push(value)
20
+ }
21
+ hull.pop()
22
+ return hull
23
+ }
24
+ const hull = [...half(points), ...half([...points].reverse())]
25
+ return hull.every((a, i) => cross(a, hull[(i + 1) % hull.length]!, point) >= 0)
26
+ }