@hina-ui/vue 1.3.1 → 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 (185) hide show
  1. package/dist/index.js +3990 -2016
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/components/hover-card/HoverCard.vue.d.ts +7 -7
  4. package/dist/types/components/hover-card/HoverCard.vue.d.ts.map +1 -1
  5. package/dist/types/components/hover-card/HoverCardAnchor.vue.d.ts +27 -0
  6. package/dist/types/components/hover-card/HoverCardAnchor.vue.d.ts.map +1 -0
  7. package/dist/types/components/hover-card/composables/useHoverCardAnchor.d.ts +17 -0
  8. package/dist/types/components/hover-card/composables/useHoverCardAnchor.d.ts.map +1 -0
  9. package/dist/types/components/image/Image.vue.d.ts +3 -1
  10. package/dist/types/components/image/Image.vue.d.ts.map +1 -1
  11. package/dist/types/components/image/composables/useImageGroupState.d.ts.map +1 -1
  12. package/dist/types/components/image/image.variants.d.ts +1 -1
  13. package/dist/types/components/lightbox/Lightbox.vue.d.ts.map +1 -1
  14. package/dist/types/components/lightbox/LightboxChrome.vue.d.ts +4 -0
  15. package/dist/types/components/lightbox/LightboxChrome.vue.d.ts.map +1 -1
  16. package/dist/types/components/lightbox/LightboxToolbar.vue.d.ts +4 -0
  17. package/dist/types/components/lightbox/LightboxToolbar.vue.d.ts.map +1 -1
  18. package/dist/types/components/lightbox/composables/useLightboxFrames.d.ts +4 -1
  19. package/dist/types/components/lightbox/composables/useLightboxFrames.d.ts.map +1 -1
  20. package/dist/types/components/lightbox/composables/useLightboxLarge.d.ts +1 -0
  21. package/dist/types/components/lightbox/composables/useLightboxLarge.d.ts.map +1 -1
  22. package/dist/types/components/lightbox/composables/useLightboxLayout.d.ts +17 -0
  23. package/dist/types/components/lightbox/composables/useLightboxLayout.d.ts.map +1 -0
  24. package/dist/types/components/lightbox/composables/useLightboxPhase.d.ts +1 -1
  25. package/dist/types/components/lightbox/composables/useLightboxPhase.d.ts.map +1 -1
  26. package/dist/types/components/lightbox/composables/useLightboxPose.d.ts.map +1 -1
  27. package/dist/types/components/lightbox/composables/useLightboxRotation.d.ts +5 -1
  28. package/dist/types/components/lightbox/composables/useLightboxRotation.d.ts.map +1 -1
  29. package/dist/types/components/lightbox/composables/useLightboxZoom.d.ts +10 -1
  30. package/dist/types/components/lightbox/composables/useLightboxZoom.d.ts.map +1 -1
  31. package/dist/types/components/lightbox/lightbox.variants.d.ts +2 -0
  32. package/dist/types/components/lightbox/lightbox.variants.d.ts.map +1 -0
  33. package/dist/types/components/lightbox/types.d.ts +7 -1
  34. package/dist/types/components/lightbox/types.d.ts.map +1 -1
  35. package/dist/types/components/lightbox/utils/pose.d.ts +2 -2
  36. package/dist/types/components/lightbox/utils/pose.d.ts.map +1 -1
  37. package/dist/types/components/lightbox/utils/source.d.ts +4 -0
  38. package/dist/types/components/lightbox/utils/source.d.ts.map +1 -0
  39. package/dist/types/components/lightbox/utils/zoom.d.ts +8 -4
  40. package/dist/types/components/lightbox/utils/zoom.d.ts.map +1 -1
  41. package/dist/types/components/pagination/Pagination.vue.d.ts +59 -0
  42. package/dist/types/components/pagination/Pagination.vue.d.ts.map +1 -0
  43. package/dist/types/components/pagination/PaginationContent.vue.d.ts +23 -0
  44. package/dist/types/components/pagination/PaginationContent.vue.d.ts.map +1 -0
  45. package/dist/types/components/pagination/PaginationControl.vue.d.ts +10 -0
  46. package/dist/types/components/pagination/PaginationControl.vue.d.ts.map +1 -0
  47. package/dist/types/components/pagination/PaginationEllipsis.vue.d.ts +23 -0
  48. package/dist/types/components/pagination/PaginationEllipsis.vue.d.ts.map +1 -0
  49. package/dist/types/components/pagination/PaginationInfo.vue.d.ts +24 -0
  50. package/dist/types/components/pagination/PaginationInfo.vue.d.ts.map +1 -0
  51. package/dist/types/components/pagination/PaginationJump.vue.d.ts +7 -0
  52. package/dist/types/components/pagination/PaginationJump.vue.d.ts.map +1 -0
  53. package/dist/types/components/pagination/PaginationPage.vue.d.ts +20 -0
  54. package/dist/types/components/pagination/PaginationPage.vue.d.ts.map +1 -0
  55. package/dist/types/components/pagination/PaginationPages.vue.d.ts +27 -0
  56. package/dist/types/components/pagination/PaginationPages.vue.d.ts.map +1 -0
  57. package/dist/types/components/pagination/PaginationPopup.vue.d.ts +8 -0
  58. package/dist/types/components/pagination/PaginationPopup.vue.d.ts.map +1 -0
  59. package/dist/types/components/pagination/PaginationSize.vue.d.ts +7 -0
  60. package/dist/types/components/pagination/PaginationSize.vue.d.ts.map +1 -0
  61. package/dist/types/components/pagination/composables/usePagination.d.ts +25 -0
  62. package/dist/types/components/pagination/composables/usePagination.d.ts.map +1 -0
  63. package/dist/types/components/pagination/composables/usePaginationEllipsis.d.ts +34 -0
  64. package/dist/types/components/pagination/composables/usePaginationEllipsis.d.ts.map +1 -0
  65. package/dist/types/components/pagination/composables/usePaginationJump.d.ts +8 -0
  66. package/dist/types/components/pagination/composables/usePaginationJump.d.ts.map +1 -0
  67. package/dist/types/components/pagination/composables/usePaginationPageTooltip.d.ts +3 -0
  68. package/dist/types/components/pagination/composables/usePaginationPageTooltip.d.ts.map +1 -0
  69. package/dist/types/components/pagination/composables/usePaginationWindow.d.ts +188 -0
  70. package/dist/types/components/pagination/composables/usePaginationWindow.d.ts.map +1 -0
  71. package/dist/types/components/pagination/context.d.ts +19 -0
  72. package/dist/types/components/pagination/context.d.ts.map +1 -0
  73. package/dist/types/components/pagination/pagination.variants.d.ts +68 -0
  74. package/dist/types/components/pagination/pagination.variants.d.ts.map +1 -0
  75. package/dist/types/components/pagination/types.d.ts +23 -0
  76. package/dist/types/components/pagination/types.d.ts.map +1 -0
  77. package/dist/types/components/pagination/utils/ranges.d.ts +3 -0
  78. package/dist/types/components/pagination/utils/ranges.d.ts.map +1 -0
  79. package/dist/types/components/range-slider/RangeSlider.vue.d.ts +1 -0
  80. package/dist/types/components/range-slider/RangeSlider.vue.d.ts.map +1 -1
  81. package/dist/types/components/slider/Slider.vue.d.ts +1 -0
  82. package/dist/types/components/slider/Slider.vue.d.ts.map +1 -1
  83. package/dist/types/components/slider/slider.variants.d.ts.map +1 -1
  84. package/dist/types/components/tooltip/Tooltip.vue.d.ts +4 -15
  85. package/dist/types/components/tooltip/Tooltip.vue.d.ts.map +1 -1
  86. package/dist/types/components/tooltip/TooltipBubble.vue.d.ts +20 -0
  87. package/dist/types/components/tooltip/TooltipBubble.vue.d.ts.map +1 -0
  88. package/dist/types/components/tooltip/TooltipDirectiveRoot.d.ts +33 -0
  89. package/dist/types/components/tooltip/TooltipDirectiveRoot.d.ts.map +1 -0
  90. package/dist/types/components/tooltip/composables/useTooltipTarget.d.ts +2 -0
  91. package/dist/types/components/tooltip/composables/useTooltipTarget.d.ts.map +1 -0
  92. package/dist/types/components/tooltip/directive.d.ts +4 -0
  93. package/dist/types/components/tooltip/directive.d.ts.map +1 -0
  94. package/dist/types/components/tooltip/types.d.ts +14 -0
  95. package/dist/types/components/tooltip/types.d.ts.map +1 -0
  96. package/dist/types/components/tooltip/utils/directiveContext.d.ts +10 -0
  97. package/dist/types/components/tooltip/utils/directiveContext.d.ts.map +1 -0
  98. package/dist/types/components/tree-select/TreeSelect.vue.d.ts +6 -1
  99. package/dist/types/components/tree-select/TreeSelect.vue.d.ts.map +1 -1
  100. package/dist/types/components/tree-select/TreeSelectSearch.vue.d.ts +22 -0
  101. package/dist/types/components/tree-select/TreeSelectSearch.vue.d.ts.map +1 -0
  102. package/dist/types/components/tree-select/composables/useTreeSelect.d.ts +35 -0
  103. package/dist/types/components/tree-select/composables/useTreeSelect.d.ts.map +1 -0
  104. package/dist/types/components/tree-select/tree-select.variants.d.ts +1 -1
  105. package/dist/types/components/tree-select/tree-select.variants.d.ts.map +1 -1
  106. package/dist/types/components/tree-select/utils/filterTree.d.ts +7 -0
  107. package/dist/types/components/tree-select/utils/filterTree.d.ts.map +1 -0
  108. package/dist/types/index.d.ts +9 -1
  109. package/dist/types/index.d.ts.map +1 -1
  110. package/dist/types/lib/overlay-positioner.d.ts +3 -0
  111. package/dist/types/lib/overlay-positioner.d.ts.map +1 -0
  112. package/dist/types/lib/pointer-corridor.d.ts +7 -0
  113. package/dist/types/lib/pointer-corridor.d.ts.map +1 -0
  114. package/dist/types/lib/useDirection.d.ts +7 -0
  115. package/dist/types/lib/useDirection.d.ts.map +1 -0
  116. package/dist/types/locale/en-US.d.ts.map +1 -1
  117. package/dist/types/locale/index.d.ts.map +1 -1
  118. package/dist/types/locale/types.d.ts +13 -0
  119. package/dist/types/locale/types.d.ts.map +1 -1
  120. package/dist/types/locale/zh-CN.d.ts.map +1 -1
  121. package/package.json +1 -1
  122. package/src/components/badge/badge.variants.ts +1 -1
  123. package/src/components/hover-card/HoverCard.vue +34 -16
  124. package/src/components/hover-card/HoverCardAnchor.vue +16 -0
  125. package/src/components/hover-card/composables/useHoverCardAnchor.ts +160 -0
  126. package/src/components/image/Image.vue +2 -0
  127. package/src/components/image/composables/useImageGroupState.ts +9 -1
  128. package/src/components/image/image.variants.ts +1 -1
  129. package/src/components/lightbox/Lightbox.vue +23 -17
  130. package/src/components/lightbox/LightboxChrome.vue +7 -1
  131. package/src/components/lightbox/LightboxToolbar.vue +16 -2
  132. package/src/components/lightbox/composables/useLightboxFrames.ts +108 -12
  133. package/src/components/lightbox/composables/useLightboxLarge.ts +4 -1
  134. package/src/components/lightbox/composables/useLightboxLayout.ts +44 -0
  135. package/src/components/lightbox/composables/useLightboxPhase.ts +12 -4
  136. package/src/components/lightbox/composables/useLightboxPose.ts +17 -19
  137. package/src/components/lightbox/composables/useLightboxRotation.ts +14 -4
  138. package/src/components/lightbox/composables/useLightboxZoom.ts +51 -9
  139. package/src/components/lightbox/lightbox.variants.ts +5 -0
  140. package/src/components/lightbox/types.ts +5 -1
  141. package/src/components/lightbox/utils/pose.ts +13 -6
  142. package/src/components/lightbox/utils/source.ts +17 -0
  143. package/src/components/lightbox/utils/zoom.ts +21 -7
  144. package/src/components/pagination/Pagination.vue +118 -0
  145. package/src/components/pagination/PaginationContent.vue +29 -0
  146. package/src/components/pagination/PaginationControl.vue +42 -0
  147. package/src/components/pagination/PaginationEllipsis.vue +67 -0
  148. package/src/components/pagination/PaginationInfo.vue +18 -0
  149. package/src/components/pagination/PaginationJump.vue +32 -0
  150. package/src/components/pagination/PaginationPage.vue +34 -0
  151. package/src/components/pagination/PaginationPages.vue +43 -0
  152. package/src/components/pagination/PaginationPopup.vue +92 -0
  153. package/src/components/pagination/PaginationSize.vue +30 -0
  154. package/src/components/pagination/composables/usePagination.ts +76 -0
  155. package/src/components/pagination/composables/usePaginationEllipsis.ts +269 -0
  156. package/src/components/pagination/composables/usePaginationJump.ts +23 -0
  157. package/src/components/pagination/composables/usePaginationPageTooltip.ts +23 -0
  158. package/src/components/pagination/composables/usePaginationWindow.ts +116 -0
  159. package/src/components/pagination/context.ts +23 -0
  160. package/src/components/pagination/pagination.variants.ts +32 -0
  161. package/src/components/pagination/types.ts +19 -0
  162. package/src/components/pagination/utils/ranges.ts +12 -0
  163. package/src/components/range-slider/RangeSlider.vue +6 -0
  164. package/src/components/slider/Slider.vue +6 -0
  165. package/src/components/slider/slider.variants.ts +1 -0
  166. package/src/components/tooltip/Tooltip.vue +10 -33
  167. package/src/components/tooltip/TooltipBubble.vue +36 -0
  168. package/src/components/tooltip/TooltipDirectiveRoot.ts +61 -0
  169. package/src/components/tooltip/composables/useTooltipTarget.ts +56 -0
  170. package/src/components/tooltip/directive.ts +54 -0
  171. package/src/components/tooltip/types.ts +15 -0
  172. package/src/components/tooltip/utils/directiveContext.ts +32 -0
  173. package/src/components/tree-select/TreeSelect.vue +90 -61
  174. package/src/components/tree-select/TreeSelectSearch.vue +48 -0
  175. package/src/components/tree-select/composables/useTreeSelect.ts +130 -0
  176. package/src/components/tree-select/tree-select.variants.ts +2 -2
  177. package/src/components/tree-select/utils/filterTree.ts +19 -0
  178. package/src/index.ts +9 -1
  179. package/src/lib/overlay-positioner.ts +25 -0
  180. package/src/lib/pointer-corridor.ts +26 -0
  181. package/src/lib/useDirection.ts +31 -0
  182. package/src/locale/en-US.ts +15 -1
  183. package/src/locale/index.ts +1 -0
  184. package/src/locale/types.ts +13 -0
  185. package/src/locale/zh-CN.ts +15 -1
@@ -0,0 +1,116 @@
1
+ import { computed, nextTick, ref, shallowRef, watch } from 'vue'
2
+ import { useEventListener, useResizeObserver } from '@vueuse/core'
3
+ import type ScrollArea from '../../scroll-area/ScrollArea.vue'
4
+ import type { PaginationEllipsisController } from './usePaginationEllipsis'
5
+
6
+ export function usePaginationWindow(controller: PaginationEllipsisController) {
7
+ const scroll = shallowRef<InstanceType<typeof ScrollArea>>()
8
+ const rowsHost = shallowRef<HTMLElement>()
9
+ const viewport = computed(() => scroll.value?.viewport)
10
+ const top = ref(0)
11
+ const height = ref(224)
12
+ const rowHeight = ref(36)
13
+ const rowGap = ref(0)
14
+ const rowStep = computed(() => rowHeight.value + rowGap.value)
15
+ const activePage = ref(1)
16
+ const range = controller.displayRange
17
+ const count = computed(() => (range.value ? range.value.to - range.value.from + 1 : 0))
18
+ const extent = computed(() => Math.max(0, count.value * rowStep.value - rowGap.value))
19
+ const rows = computed(() => {
20
+ if (!range.value) return []
21
+ const start = Math.max(0, Math.floor(top.value / rowStep.value) - 3)
22
+ const end = Math.min(count.value, start + Math.ceil(height.value / rowStep.value) + 7)
23
+ const values = Array.from(
24
+ { length: Math.max(0, end - start) },
25
+ (_, i) => start + i + range.value!.from,
26
+ )
27
+ if (
28
+ activePage.value >= range.value.from &&
29
+ activePage.value <= range.value.to &&
30
+ !values.includes(activePage.value)
31
+ )
32
+ values.push(activePage.value)
33
+ return values.map(page => ({ page, top: (page - range.value!.from) * rowStep.value }))
34
+ })
35
+
36
+ function measure() {
37
+ if (viewport.value) height.value = viewport.value.clientHeight || 224
38
+ const button = rowsHost.value?.querySelector<HTMLElement>('[data-hn-pagination-choice]')
39
+ if (button) {
40
+ rowHeight.value = button.offsetHeight
41
+ rowGap.value = Number.parseFloat(getComputedStyle(rowsHost.value!).rowGap) || 0
42
+ }
43
+ }
44
+
45
+ async function focusPage(page: number) {
46
+ if (!range.value || !controller.open.value) return
47
+ activePage.value = Math.min(range.value.to, Math.max(range.value.from, page))
48
+ const element = viewport.value
49
+ if (element) {
50
+ const offset = (activePage.value - range.value.from) * rowStep.value
51
+ if (offset < element.scrollTop) element.scrollTop = offset
52
+ else if (offset + rowHeight.value > element.scrollTop + element.clientHeight)
53
+ element.scrollTop = offset + rowHeight.value - element.clientHeight
54
+ top.value = element.scrollTop
55
+ }
56
+ await nextTick()
57
+ rowsHost.value
58
+ ?.querySelector<HTMLElement>('[data-hn-pagination-choice="' + activePage.value + '"]')
59
+ ?.focus({ preventScroll: true })
60
+ }
61
+
62
+ function keydown(event: KeyboardEvent) {
63
+ if (!range.value) return
64
+ const step = Math.max(1, Math.floor(height.value / rowStep.value))
65
+ const targets: Record<string, number> = {
66
+ ArrowDown: activePage.value + 1,
67
+ ArrowUp: activePage.value - 1,
68
+ Home: range.value.from,
69
+ End: range.value.to,
70
+ PageDown: activePage.value + step,
71
+ PageUp: activePage.value - step,
72
+ }
73
+ const target = targets[event.key]
74
+ if (target === undefined) return
75
+ event.preventDefault()
76
+ event.stopPropagation()
77
+ void focusPage(target)
78
+ }
79
+
80
+ watch(
81
+ [range, controller.open],
82
+ () => {
83
+ if (!controller.open.value || !range.value) return
84
+ activePage.value = range.value.from
85
+ top.value = 0
86
+ if (viewport.value) viewport.value.scrollTop = 0
87
+ },
88
+ { immediate: true },
89
+ )
90
+ watch(
91
+ [controller.focusRequest, viewport],
92
+ async ([request, element]) => {
93
+ if (!request || !element || !range.value) return
94
+ await nextTick()
95
+ measure()
96
+ await focusPage(request.edge === 'first' ? range.value.from : range.value.to)
97
+ controller.focusRequest.value = undefined
98
+ },
99
+ { flush: 'post' },
100
+ )
101
+ useResizeObserver(
102
+ () => rowsHost.value?.querySelector<HTMLElement>('[data-hn-pagination-choice]'),
103
+ measure,
104
+ )
105
+ useResizeObserver(rowsHost, measure)
106
+ useResizeObserver(viewport, measure)
107
+ useEventListener(
108
+ viewport,
109
+ 'scroll',
110
+ () => {
111
+ top.value = viewport.value?.scrollTop ?? 0
112
+ },
113
+ { passive: true },
114
+ )
115
+ return { scroll, rowsHost, activePage, extent, rows, keydown }
116
+ }
@@ -0,0 +1,23 @@
1
+ import { inject, provide, type InjectionKey, type Ref } from 'vue'
2
+ import type { PaginationState } from './types'
3
+
4
+ export interface PaginationContext {
5
+ state: Readonly<Ref<PaginationState>>
6
+ blocked: Readonly<Ref<boolean>>
7
+ size: Readonly<Ref<'sm' | 'md' | 'lg'>>
8
+ direction: Readonly<Ref<'ltr' | 'rtl'>>
9
+ siblingCount: Readonly<Ref<number>>
10
+ showFirstLast: Readonly<Ref<boolean>>
11
+ options: Readonly<Ref<Array<{ value: number; label: string }>>>
12
+ update: (page: number) => void
13
+ resize: (pageSize: number) => void
14
+ }
15
+
16
+ const key: InjectionKey<PaginationContext> = Symbol('HnPagination')
17
+
18
+ export const providePagination = (context: PaginationContext) => provide(key, context)
19
+ export function usePaginationContext() {
20
+ const context = inject(key)
21
+ if (!context) throw new Error('Pagination parts require a Pagination parent')
22
+ return context
23
+ }
@@ -0,0 +1,32 @@
1
+ import { tv } from '../../lib/tv'
2
+
3
+ export const pagination = tv({
4
+ base: 'flex w-full shrink-0 flex-wrap items-center gap-x-4 gap-y-3',
5
+ variants: {
6
+ align: {
7
+ start: 'justify-start',
8
+ center: 'justify-center',
9
+ end: 'justify-end',
10
+ between: 'justify-between',
11
+ },
12
+ },
13
+ })
14
+
15
+ export const paginationList = tv({
16
+ base: 'relative isolate flex max-w-full flex-wrap items-center gap-1',
17
+ })
18
+
19
+ export const paginationItem = tv({
20
+ base: 'min-w-0 px-0 tabular-nums [&>span]:min-w-0',
21
+ variants: {
22
+ size: {
23
+ sm: 'size-[var(--hn-control-h-sm)]',
24
+ md: 'size-[var(--hn-control-h-md)]',
25
+ lg: 'size-[var(--hn-control-h-lg)]',
26
+ },
27
+ selected: { true: 'bg-accent text-accent-on' },
28
+ },
29
+ defaultVariants: { size: 'md' },
30
+ })
31
+
32
+ export const paginationArrow = tv({ variants: { dir: { ltr: '', rtl: 'rotate-180' } } })
@@ -0,0 +1,19 @@
1
+ export interface PaginationChange {
2
+ page: number
3
+ pageSize: number
4
+ }
5
+
6
+ export interface PaginationState extends PaginationChange {
7
+ total: number
8
+ pageCount: number
9
+ from: number
10
+ to: number
11
+ }
12
+
13
+ export type PaginationEntry = { type: 'page'; value: number } | { type: 'ellipsis' }
14
+ export type PaginationSide = 'prev' | 'next'
15
+ export interface PaginationRange {
16
+ side: PaginationSide
17
+ from: number
18
+ to: number
19
+ }
@@ -0,0 +1,12 @@
1
+ import type { PaginationEntry, PaginationRange } from '../types'
2
+
3
+ export function paginationRanges(items: PaginationEntry[]): PaginationRange[] {
4
+ return items.flatMap((item, index) => {
5
+ if (item.type !== 'ellipsis') return []
6
+ const previous = items[index - 1]
7
+ const next = items[index + 1]
8
+ if (previous?.type !== 'page' || next?.type !== 'page' || next.value <= previous.value + 1)
9
+ return []
10
+ return [{ side: index === 1 ? 'prev' : 'next', from: previous.value + 1, to: next.value - 1 }]
11
+ })
12
+ }
@@ -5,6 +5,7 @@
5
5
  import { useFieldControl } from '../form-field/context'
6
6
  import { useUiLocale } from '../../locale'
7
7
  import { useSliderChrome } from '../slider/composables/useSliderChrome'
8
+ import { useDirection } from '../../lib/useDirection'
8
9
  import SliderHandle from '../slider/SliderHandle.vue'
9
10
  import SliderMarks from '../slider/SliderMarks.vue'
10
11
  import {
@@ -22,6 +23,7 @@
22
23
  min?: number
23
24
  max?: number
24
25
  step?: number
26
+ dir?: 'ltr' | 'rtl'
25
27
  minSteps?: number
26
28
  marks?: Array<{ value: number; label?: string }>
27
29
  label?: 'auto' | 'always' | 'none'
@@ -37,6 +39,7 @@
37
39
 
38
40
  const model = defineModel<[number, number]>()
39
41
 
42
+ const { root, direction, rootDirection } = useDirection(() => props.dir)
40
43
  const t = useUiLocale()
41
44
  const { labelledBy, invalid, disabled, describedBy } = useFieldControl({
42
45
  disabled: () => props.disabled,
@@ -66,6 +69,7 @@
66
69
 
67
70
  <template>
68
71
  <span
72
+ ref="root"
69
73
  data-hn-range-slider
70
74
  data-hn-state-group
71
75
  role="group"
@@ -73,6 +77,7 @@
73
77
  :aria-labelledby="labelledBy"
74
78
  :aria-describedby="describedBy"
75
79
  :aria-invalid="invalid || undefined"
80
+ :dir="rootDirection"
76
81
  :data-disabled="disabled ? '' : undefined"
77
82
  :data-dragging="dragging ? '' : undefined"
78
83
  :style="position"
@@ -81,6 +86,7 @@
81
86
  v-on="listeners"
82
87
  >
83
88
  <SliderRoot
89
+ :dir="direction"
84
90
  :model-value="values"
85
91
  :min="props.min"
86
92
  :max="props.max"
@@ -5,6 +5,7 @@
5
5
  import { useFieldControl } from '../form-field/context'
6
6
  import { useUiLocale } from '../../locale'
7
7
  import { useSliderChrome } from './composables/useSliderChrome'
8
+ import { useDirection } from '../../lib/useDirection'
8
9
  import SliderHandle from './SliderHandle.vue'
9
10
  import SliderMarks from './SliderMarks.vue'
10
11
  import {
@@ -22,6 +23,7 @@
22
23
  min?: number
23
24
  max?: number
24
25
  step?: number
26
+ dir?: 'ltr' | 'rtl'
25
27
  marks?: Array<{ value: number; label?: string }>
26
28
  label?: 'auto' | 'always' | 'none'
27
29
  format?: (value: number) => string
@@ -36,6 +38,7 @@
36
38
 
37
39
  const model = defineModel<number>()
38
40
 
41
+ const { root, direction, rootDirection } = useDirection(() => props.dir)
39
42
  const t = useUiLocale()
40
43
  const { labelledBy, invalid, disabled, describedBy } = useFieldControl({
41
44
  disabled: () => props.disabled,
@@ -60,8 +63,10 @@
60
63
 
61
64
  <template>
62
65
  <span
66
+ ref="root"
63
67
  data-hn-slider
64
68
  data-hn-state-group
69
+ :dir="rootDirection"
65
70
  :data-disabled="disabled ? '' : undefined"
66
71
  :data-dragging="dragging ? '' : undefined"
67
72
  :style="position"
@@ -70,6 +75,7 @@
70
75
  v-on="listeners"
71
76
  >
72
77
  <SliderRoot
78
+ :dir="direction"
73
79
  :model-value="values"
74
80
  :min="props.min"
75
81
  :max="props.max"
@@ -18,6 +18,7 @@ export const sliderRoot = tv({
18
18
  base: [
19
19
  'bg-line-strong relative flex h-[var(--hn-slider-h)] w-full cursor-pointer touch-none rounded-full select-none',
20
20
  'data-disabled:pointer-events-none',
21
+ '[&[dir=rtl]>[role=slider]]:[--reka-slider-thumb-transform:translateX(50%)]',
21
22
  ],
22
23
  })
23
24
 
@@ -1,21 +1,14 @@
1
1
  <script setup lang="ts">
2
- import { TooltipRoot, TooltipTrigger, TooltipPortal, TooltipContent, TooltipArrow } from 'reka-ui'
3
- import { cn } from '../../lib/cn'
2
+ import { TooltipRoot, TooltipTrigger } from 'reka-ui'
3
+ import TooltipBubble from './TooltipBubble.vue'
4
+ import type { TooltipProps } from './types'
4
5
 
5
6
  defineOptions({ name: 'HnTooltip' })
6
7
 
7
- const props = withDefaults(
8
- defineProps<{
9
- content?: string
10
- side?: 'top' | 'right' | 'bottom' | 'left'
11
- align?: 'start' | 'center' | 'end'
12
- sideOffset?: number
13
- open?: boolean
14
- disabled?: boolean
15
- class?: string
16
- }>(),
17
- { side: 'top', align: 'center', sideOffset: 8, open: undefined, disabled: false },
18
- )
8
+ const props = withDefaults(defineProps<TooltipProps>(), {
9
+ open: undefined,
10
+ disabled: false,
11
+ })
19
12
  </script>
20
13
 
21
14
  <template>
@@ -23,24 +16,8 @@
23
16
  <TooltipTrigger as-child>
24
17
  <slot />
25
18
  </TooltipTrigger>
26
- <TooltipPortal>
27
- <TooltipContent
28
- :side="props.side"
29
- :align="props.align"
30
- :side-offset="props.sideOffset"
31
- :aria-label="props.content"
32
- :update-position-strategy="props.open === undefined ? 'optimized' : 'always'"
33
- :class="
34
- cn(
35
- 'hn-anim-pop [--hn-pop-in:var(--hn-duration-fast)] bg-neutral-solid text-neutral-solid-on z-(--hn-z-overlay)',
36
- 'max-w-xs rounded-md px-2.5 py-1 text-xs shadow-md',
37
- props.class,
38
- )
39
- "
40
- >
41
- <slot name="content">{{ props.content }}</slot>
42
- <TooltipArrow :width="10" :height="5" class="fill-neutral-solid" />
43
- </TooltipContent>
44
- </TooltipPortal>
19
+ <TooltipBubble v-bind="props">
20
+ <slot name="content">{{ props.content }}</slot>
21
+ </TooltipBubble>
45
22
  </TooltipRoot>
46
23
  </template>
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ import { TooltipPortal, TooltipContent, TooltipArrow } from 'reka-ui'
3
+ import { cn } from '../../lib/cn'
4
+ import type { TooltipProps } from './types'
5
+
6
+ defineOptions({ name: 'HnTooltipBubble' })
7
+
8
+ const props = withDefaults(defineProps<TooltipProps>(), {
9
+ side: 'top',
10
+ align: 'center',
11
+ sideOffset: 8,
12
+ open: undefined,
13
+ })
14
+ </script>
15
+
16
+ <template>
17
+ <TooltipPortal>
18
+ <TooltipContent
19
+ :side="props.side"
20
+ :align="props.align"
21
+ :side-offset="props.sideOffset"
22
+ :aria-label="props.content"
23
+ :update-position-strategy="props.open === undefined ? 'optimized' : 'always'"
24
+ :class="
25
+ cn(
26
+ 'hn-anim-pop [--hn-pop-in:var(--hn-duration-fast)] bg-neutral-solid text-neutral-solid-on z-(--hn-z-overlay)',
27
+ 'max-w-xs rounded-md px-2.5 py-1 text-xs shadow-md',
28
+ props.class,
29
+ )
30
+ "
31
+ >
32
+ <slot>{{ props.content }}</slot>
33
+ <TooltipArrow :width="10" :height="5" class="fill-neutral-solid" />
34
+ </TooltipContent>
35
+ </TooltipPortal>
36
+ </template>
@@ -0,0 +1,61 @@
1
+ import { computed, defineComponent, h, type PropType } from 'vue'
2
+ import {
3
+ TooltipTrigger,
4
+ TooltipRoot,
5
+ injectTooltipProviderContext,
6
+ injectTooltipRootContext,
7
+ } from 'reka-ui'
8
+ import TooltipBubble from './TooltipBubble.vue'
9
+ import { useTooltipTarget } from './composables/useTooltipTarget'
10
+ import type { TooltipDirectiveOptions } from './types'
11
+
12
+ const TooltipTarget = defineComponent({
13
+ name: 'HnTooltipTarget',
14
+ props: {
15
+ target: { type: Object as PropType<HTMLElement>, required: true },
16
+ options: { type: Object as PropType<TooltipDirectiveOptions>, required: true },
17
+ revision: { type: Number, required: true },
18
+ },
19
+ setup(props) {
20
+ const root = injectTooltipRootContext()
21
+ const ExternalTarget = defineComponent({
22
+ name: 'HnTooltipElement',
23
+ inheritAttrs: false,
24
+ setup(_, { attrs }) {
25
+ useTooltipTarget(props.target, attrs, () => props.revision)
26
+ return () => null
27
+ },
28
+ })
29
+ return () => [
30
+ h(TooltipTrigger, {
31
+ key: root.disabled.value ? 'disabled' : 'enabled',
32
+ reference: props.target,
33
+ as: ExternalTarget,
34
+ }),
35
+ h(TooltipBubble, props.options),
36
+ ]
37
+ },
38
+ })
39
+
40
+ export default defineComponent({
41
+ name: 'HnTooltipDirectiveRoot',
42
+ props: {
43
+ target: { type: Object as PropType<HTMLElement>, required: true },
44
+ options: { type: Object as PropType<TooltipDirectiveOptions>, required: true },
45
+ revision: { type: Number, required: true },
46
+ },
47
+ setup(props) {
48
+ const provider = injectTooltipProviderContext(null)
49
+ const disabled = computed(() => !!props.options.disabled || !props.options.content.trim())
50
+ return () =>
51
+ provider
52
+ ? h(
53
+ TooltipRoot,
54
+ { disabled: disabled.value, ignoreNonKeyboardFocus: true },
55
+ {
56
+ default: () => h(TooltipTarget, props),
57
+ },
58
+ )
59
+ : null
60
+ },
61
+ })
@@ -0,0 +1,56 @@
1
+ import { onScopeDispose, watch, watchPostEffect } from 'vue'
2
+ import { useEventListener } from '@vueuse/core'
3
+ import { injectTooltipProviderContext, injectTooltipRootContext } from 'reka-ui'
4
+
5
+ type Handler = (event: Event) => void
6
+
7
+ export function useTooltipTarget(
8
+ target: HTMLElement,
9
+ attrs: Record<string, unknown>,
10
+ revision: () => number,
11
+ ) {
12
+ const root = injectTooltipRootContext()
13
+ const provider = injectTooltipProviderContext()
14
+
15
+ watchPostEffect(() => {
16
+ if (root.trigger.value !== target) root.onTriggerChange(target)
17
+ })
18
+
19
+ const contentId = root.contentId
20
+ const graceAttribute = 'data-grace-area-trigger'
21
+ const previousGrace = target.getAttribute(graceAttribute)
22
+ target.setAttribute(graceAttribute, '')
23
+
24
+ function describe(open: boolean) {
25
+ const ids = new Set(
26
+ (target.getAttribute('aria-describedby') ?? '').split(/\s+/).filter(Boolean),
27
+ )
28
+ ids.delete(contentId)
29
+ if (open) ids.add(contentId)
30
+ if (ids.size) target.setAttribute('aria-describedby', [...ids].join(' '))
31
+ else target.removeAttribute('aria-describedby')
32
+ }
33
+
34
+ for (const event of ['pointermove', 'pointerleave', 'pointerdown', 'focus', 'blur', 'click']) {
35
+ const prop = 'on' + event[0]!.toUpperCase() + event.slice(1)
36
+ useEventListener(target, event, (event: Event) => {
37
+ const handlers = attrs[prop] as Handler | Handler[] | undefined
38
+ for (const handler of Array.isArray(handlers) ? handlers : handlers ? [handlers] : []) {
39
+ handler(event)
40
+ }
41
+ })
42
+ }
43
+
44
+ watch([root.open, revision], ([open]) => describe(open), { immediate: true, flush: 'post' })
45
+ watch(root.disabled, disabled => {
46
+ if (disabled) root.onClose()
47
+ })
48
+
49
+ onScopeDispose(() => {
50
+ if (root.open.value) provider.onClose()
51
+ root.onClose()
52
+ describe(false)
53
+ if (previousGrace === null) target.removeAttribute(graceAttribute)
54
+ else target.setAttribute(graceAttribute, previousGrace)
55
+ })
56
+ }
@@ -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
+ }