@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,67 @@
1
+ <script setup lang="ts">
2
+ import { computed, type ComponentPublicInstance } from 'vue'
3
+ import { ChevronLeft, ChevronRight, MoreHorizontal } from '@lucide/vue'
4
+ import Button from '../button/Button.vue'
5
+ import { useUiLocale } from '../../locale'
6
+ import { usePaginationContext } from './context'
7
+ import { paginationArrow } from './pagination.variants'
8
+ import type { PaginationRange } from './types'
9
+ import type { PaginationEllipsisController } from './composables/usePaginationEllipsis'
10
+ defineOptions({ name: 'HnPaginationEllipsis' })
11
+ const props = defineProps<{ range: PaginationRange; controller: PaginationEllipsisController }>()
12
+ const { size, blocked, direction, siblingCount } = usePaginationContext()
13
+ const t = useUiLocale()
14
+ const expanded = computed(
15
+ () => props.controller.open.value && props.controller.active.value === props.range.side,
16
+ )
17
+ const register = (element: Element | ComponentPublicInstance | null) =>
18
+ props.controller.register(props.range.side, element)
19
+ const step = computed(() => siblingCount.value * 2 + 1)
20
+ </script>
21
+
22
+ <template>
23
+ <Button
24
+ :ref="register"
25
+ variant="ghost"
26
+ tone="neutral"
27
+ icon-only
28
+ :size="size"
29
+ :disabled="blocked"
30
+ data-hn-pagination-ellipsis
31
+ data-type="ellipsis"
32
+ :data-side="props.range.side"
33
+ aria-haspopup="dialog"
34
+ :aria-expanded="expanded"
35
+ :aria-controls="expanded ? props.controller.id : undefined"
36
+ :aria-label="
37
+ props.range.side === 'prev'
38
+ ? t.pagination.previousPagesLabel(step)
39
+ : t.pagination.nextPagesLabel(step)
40
+ "
41
+ :aria-description="t.pagination.choosePageHint"
42
+ aria-keyshortcuts="ArrowDown ArrowUp"
43
+ @pointerdown="props.controller.pointerdown"
44
+ @pointerenter="props.controller.enter(props.range.side, $event)"
45
+ @pointerleave="props.controller.leave"
46
+ @focus="props.controller.focus(props.range.side)"
47
+ @keydown="props.controller.keydown(props.range.side, $event)"
48
+ @click="props.controller.click(props.range.side)"
49
+ >
50
+ <slot :side="props.range.side" :expanded="expanded">
51
+ <span class="grid" aria-hidden="true">
52
+ <MoreHorizontal
53
+ class="hn-transition-base [grid-area:1/1]"
54
+ :class="expanded ? 'scale-90 opacity-0' : 'scale-100 opacity-100'"
55
+ />
56
+ <component
57
+ :is="props.range.side === 'prev' ? ChevronLeft : ChevronRight"
58
+ :class="[
59
+ paginationArrow({ dir: direction }),
60
+ expanded ? 'scale-100 opacity-100' : 'scale-90 opacity-0',
61
+ ]"
62
+ class="hn-transition-base [grid-area:1/1]"
63
+ />
64
+ </span>
65
+ </slot>
66
+ </Button>
67
+ </template>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ import { cn } from '../../lib/cn'
3
+ import { useUiLocale } from '../../locale'
4
+ import { usePaginationContext } from './context'
5
+ defineOptions({ name: 'HnPaginationInfo' })
6
+ const props = defineProps<{ class?: string }>()
7
+ const { state } = usePaginationContext()
8
+ const t = useUiLocale()
9
+ </script>
10
+
11
+ <template>
12
+ <p data-hn-pagination-info :class="cn('text-muted max-w-full text-sm tabular-nums', props.class)">
13
+ <slot v-bind="state">
14
+ {{ t.pagination.rangeLabel(state.from, state.to, state.total) }} ·
15
+ {{ t.pagination.pageCountLabel(state.page, state.pageCount) }}
16
+ </slot>
17
+ </p>
18
+ </template>
@@ -0,0 +1,32 @@
1
+ <script setup lang="ts">
2
+ import { cn } from '../../lib/cn'
3
+ import { useUiLocale } from '../../locale'
4
+ import Input from '../input/Input.vue'
5
+ import { usePaginationJump } from './composables/usePaginationJump'
6
+ defineOptions({ name: 'HnPaginationJump' })
7
+ const props = defineProps<{ class?: string }>()
8
+ const t = useUiLocale()
9
+ const { draft, commit, reset, size, blocked } = usePaginationJump()
10
+ </script>
11
+
12
+ <template>
13
+ <label
14
+ data-hn-pagination-jump
15
+ :class="
16
+ cn('text-muted flex shrink-0 items-center gap-2 text-sm whitespace-nowrap', props.class)
17
+ "
18
+ >
19
+ {{ t.pagination.jumpLabel }}
20
+ <Input
21
+ v-model="draft"
22
+ :size="size"
23
+ :disabled="blocked"
24
+ inputmode="numeric"
25
+ :aria-label="t.pagination.jumpLabel"
26
+ class="w-16"
27
+ @blur="commit"
28
+ @keydown.enter.prevent.stop="commit"
29
+ @keydown.esc.prevent.stop="reset"
30
+ />
31
+ </label>
32
+ </template>
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ import { useTemplateRef } from 'vue'
3
+ import { PaginationListItem } from 'reka-ui'
4
+ import Button from '../button/Button.vue'
5
+ import { vTooltip } from '../tooltip/directive'
6
+ import { useUiLocale } from '../../locale'
7
+ import { usePaginationContext } from './context'
8
+ import { paginationItem } from './pagination.variants'
9
+ import { usePaginationPageTooltip } from './composables/usePaginationPageTooltip'
10
+
11
+ defineOptions({ name: 'HnPaginationPage' })
12
+ const props = defineProps<{ page: number }>()
13
+ const { state, size, blocked } = usePaginationContext()
14
+ const t = useUiLocale()
15
+ const label = useTemplateRef<HTMLElement>('label')
16
+ const tooltip = usePaginationPageTooltip(label, blocked)
17
+ </script>
18
+
19
+ <template>
20
+ <PaginationListItem :value="props.page" as-child>
21
+ <Button
22
+ v-tooltip="tooltip"
23
+ variant="ghost"
24
+ tone="neutral"
25
+ :size="size"
26
+ :aria-label="t.pagination.pageLabel(props.page)"
27
+ :class="paginationItem({ size, selected: props.page === state.page })"
28
+ >
29
+ <span ref="label" class="min-w-0 truncate">
30
+ <slot :page="props.page" :selected="props.page === state.page">{{ props.page }}</slot>
31
+ </span>
32
+ </Button>
33
+ </PaginationListItem>
34
+ </template>
@@ -0,0 +1,43 @@
1
+ <script setup lang="ts">
2
+ import { toRef } from 'vue'
3
+ import { usePaginationContext } from './context'
4
+ import PaginationControl from './PaginationControl.vue'
5
+ import PaginationPage from './PaginationPage.vue'
6
+ import PaginationEllipsis from './PaginationEllipsis.vue'
7
+ import PaginationPopup from './PaginationPopup.vue'
8
+ import { usePaginationEllipsis } from './composables/usePaginationEllipsis'
9
+ import type { PaginationEntry } from './types'
10
+ defineOptions({ name: 'HnPaginationPages' })
11
+ const props = defineProps<{ items: PaginationEntry[]; host?: HTMLElement }>()
12
+ const { size, direction, showFirstLast } = usePaginationContext()
13
+ const ellipsis = usePaginationEllipsis(toRef(props, 'host'), () => props.items)
14
+ </script>
15
+
16
+ <template>
17
+ <li v-if="showFirstLast"><PaginationControl action="first" :size="size" :dir="direction" /></li>
18
+ <li><PaginationControl action="prev" :size="size" :dir="direction" /></li>
19
+ <li
20
+ v-for="(item, index) in props.items"
21
+ :key="item.type === 'page' ? item.value : 'ellipsis-' + (index === 1 ? 'prev' : 'next')"
22
+ >
23
+ <PaginationPage v-if="item.type === 'page'" :page="item.value">
24
+ <template v-if="$slots.page" #default="slotProps">
25
+ <slot name="page" v-bind="slotProps" />
26
+ </template>
27
+ </PaginationPage>
28
+ <PaginationEllipsis
29
+ v-else-if="
30
+ ellipsis.ranges.value.find(range => range.side === (index === 1 ? 'prev' : 'next'))
31
+ "
32
+ :range="ellipsis.ranges.value.find(range => range.side === (index === 1 ? 'prev' : 'next'))!"
33
+ :controller="ellipsis"
34
+ >
35
+ <template v-if="$slots.ellipsis" #default="slotProps">
36
+ <slot name="ellipsis" v-bind="slotProps" />
37
+ </template>
38
+ </PaginationEllipsis>
39
+ </li>
40
+ <li><PaginationControl action="next" :size="size" :dir="direction" /></li>
41
+ <li v-if="showFirstLast"><PaginationControl action="last" :size="size" :dir="direction" /></li>
42
+ <PaginationPopup :controller="ellipsis" />
43
+ </template>
@@ -0,0 +1,92 @@
1
+ <script setup lang="ts">
2
+ import { type ComponentPublicInstance } from 'vue'
3
+ import { PopoverRoot, PopoverPortal } from 'reka-ui'
4
+ import PopoverContent from '../popover/PopoverContent.vue'
5
+ import Card from '../card/Card.vue'
6
+ import Button from '../button/Button.vue'
7
+ import ScrollArea from '../scroll-area/ScrollArea.vue'
8
+ import { useOverlayPortal } from '../../lib/overlay-portal'
9
+ import { useUiLocale } from '../../locale'
10
+ import { usePaginationContext } from './context'
11
+ import { usePaginationWindow } from './composables/usePaginationWindow'
12
+ import type { PaginationEllipsisController } from './composables/usePaginationEllipsis'
13
+ defineOptions({ name: 'HnPaginationPopup' })
14
+ const props = defineProps<{ controller: PaginationEllipsisController }>()
15
+ const { open, reference, id } = props.controller
16
+ const { size, blocked, direction } = usePaginationContext()
17
+ const { content, present } = useOverlayPortal(open)
18
+ const { scroll, rowsHost, activePage, extent, rows, keydown } = usePaginationWindow(
19
+ props.controller,
20
+ )
21
+ const t = useUiLocale()
22
+ function panel(value: Element | ComponentPublicInstance | null) {
23
+ content.value = value as ComponentPublicInstance | null
24
+ props.controller.panel.value = value && '$el' in value ? value.$el : undefined
25
+ }
26
+ </script>
27
+
28
+ <template>
29
+ <PopoverRoot
30
+ :open="open"
31
+ :modal="false"
32
+ @update:open="
33
+ value => {
34
+ if (!value) props.controller.close()
35
+ }
36
+ "
37
+ >
38
+ <PopoverPortal v-if="present">
39
+ <PopoverContent
40
+ :id="id"
41
+ :reference="reference"
42
+ :side-offset="8"
43
+ align="center"
44
+ :aria-label="t.pagination.choosePage"
45
+ as-child
46
+ @open-auto-focus.prevent
47
+ @close-auto-focus="props.controller.closeAutoFocus"
48
+ @interact-outside="props.controller.outside"
49
+ @escape-key-down.prevent="props.controller.close(true)"
50
+ >
51
+ <Card
52
+ :ref="panel"
53
+ :padded="false"
54
+ :dir="direction"
55
+ :inert="!open || blocked || undefined"
56
+ data-hn-pagination-popup
57
+ class="hn-anim-pop z-(--hn-z-overlay) w-24 p-1 shadow-md outline-none"
58
+ @pointerenter="props.controller.keep"
59
+ @pointerleave="props.controller.leave"
60
+ @keydown="keydown"
61
+ @keydown.tab.capture="props.controller.tab"
62
+ >
63
+ <ScrollArea ref="scroll" class="max-h-56">
64
+ <div ref="rowsHost" class="relative gap-1" :style="{ height: extent + 'px' }">
65
+ <div
66
+ v-for="row in rows"
67
+ :key="row.page"
68
+ class="absolute inset-x-0"
69
+ :style="{ top: row.top + 'px' }"
70
+ >
71
+ <Button
72
+ :data-hn-pagination-choice="row.page"
73
+ variant="ghost"
74
+ tone="neutral"
75
+ :size="size"
76
+ :disabled="blocked"
77
+ :tabindex="activePage === row.page ? 0 : -1"
78
+ :aria-label="t.pagination.pageLabel(row.page)"
79
+ class="w-full tabular-nums"
80
+ @focus="activePage = row.page"
81
+ @click="props.controller.pick(row.page)"
82
+ >
83
+ {{ row.page }}
84
+ </Button>
85
+ </div>
86
+ </div>
87
+ </ScrollArea>
88
+ </Card>
89
+ </PopoverContent>
90
+ </PopoverPortal>
91
+ </PopoverRoot>
92
+ </template>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { cn } from '../../lib/cn'
4
+ import { useUiLocale } from '../../locale'
5
+ import Select from '../select/Select.vue'
6
+ import { usePaginationContext } from './context'
7
+ defineOptions({ name: 'HnPaginationSize' })
8
+ const props = defineProps<{ class?: string }>()
9
+ const { state, options, size, blocked, resize, direction } = usePaginationContext()
10
+ const t = useUiLocale()
11
+ const model = computed({
12
+ get: () => state.value.pageSize,
13
+ set: (value: string | number | null | undefined) => {
14
+ if (value != null) resize(Number(value))
15
+ },
16
+ })
17
+ </script>
18
+
19
+ <template>
20
+ <Select
21
+ v-model="model"
22
+ :options="options"
23
+ :size="size"
24
+ :disabled="blocked"
25
+ :aria-label="t.pagination.pageSizeLabel"
26
+ :dir="direction"
27
+ data-hn-pagination-size
28
+ :class="cn('w-auto shrink-0 whitespace-nowrap', props.class)"
29
+ />
30
+ </template>
@@ -0,0 +1,76 @@
1
+ import { computed, watch, type Ref } from 'vue'
2
+ import type { PaginationChange, PaginationState } from '../types'
3
+
4
+ interface PaginationOptions {
5
+ total: number
6
+ itemCount?: number
7
+ siblingCount: number
8
+ pageSizeOptions?: number[]
9
+ disabled?: boolean
10
+ pending?: boolean
11
+ }
12
+
13
+ export function paginationInteger(value: number, fallback: number, minimum: number) {
14
+ return Number.isFinite(value) ? Math.max(minimum, Math.floor(value)) : fallback
15
+ }
16
+
17
+ export function usePagination(
18
+ props: PaginationOptions,
19
+ model: Ref<number>,
20
+ sizeModel: Ref<number>,
21
+ change: (value: PaginationChange) => void,
22
+ ) {
23
+ const total = computed(() => paginationInteger(props.total, 0, 0))
24
+ const pageSize = computed(() => paginationInteger(sizeModel.value, 10, 1))
25
+ const siblingCount = computed(() => paginationInteger(props.siblingCount, 1, 0))
26
+ const pageCount = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
27
+ const clamp = (value: number) => Math.min(pageCount.value, paginationInteger(value, 1, 1))
28
+ const page = computed(() => clamp(model.value))
29
+ const blocked = computed(() => !!(props.disabled || props.pending))
30
+ const state = computed<PaginationState>(() => {
31
+ const count =
32
+ props.itemCount === undefined ? pageSize.value : paginationInteger(props.itemCount, 0, 0)
33
+ const from = total.value && count ? (page.value - 1) * pageSize.value + 1 : 0
34
+ return {
35
+ page: page.value,
36
+ pageSize: pageSize.value,
37
+ pageCount: pageCount.value,
38
+ total: total.value,
39
+ from,
40
+ to: from ? Math.min(total.value, from + count - 1) : 0,
41
+ }
42
+ })
43
+ const sizes = computed(() =>
44
+ [...new Set([...(props.pageSizeOptions ?? []), pageSize.value])]
45
+ .filter(value => Number.isInteger(value) && value > 0)
46
+ .sort((a, b) => a - b),
47
+ )
48
+
49
+ watch(
50
+ [model, pageCount, () => props.pending],
51
+ () => {
52
+ if (props.pending || model.value === page.value) return
53
+ const next = page.value
54
+ model.value = next
55
+ change({ page: next, pageSize: pageSize.value })
56
+ },
57
+ { immediate: true },
58
+ )
59
+
60
+ function update(value: number) {
61
+ const next = clamp(value)
62
+ if (blocked.value || page.value === next) return
63
+ model.value = next
64
+ change({ page: next, pageSize: pageSize.value })
65
+ }
66
+
67
+ function resize(value: number) {
68
+ const next = paginationInteger(value, pageSize.value, 1)
69
+ if (blocked.value || pageSize.value === next) return
70
+ sizeModel.value = next
71
+ if (model.value !== 1) model.value = 1
72
+ change({ page: 1, pageSize: next })
73
+ }
74
+
75
+ return { total, pageSize, siblingCount, pageCount, page, state, blocked, sizes, update, resize }
76
+ }
@@ -0,0 +1,269 @@
1
+ import {
2
+ computed,
3
+ nextTick,
4
+ onScopeDispose,
5
+ ref,
6
+ shallowRef,
7
+ useId,
8
+ watch,
9
+ type ComponentPublicInstance,
10
+ type Ref,
11
+ } from 'vue'
12
+ import { useEventListener, useRafFn } from '@vueuse/core'
13
+ import { usePaginationContext } from '../context'
14
+ import { paginationRanges } from '../utils/ranges'
15
+ import { inPointerCorridor } from '../../../lib/pointer-corridor'
16
+ import type { PaginationEntry, PaginationRange, PaginationSide } from '../types'
17
+
18
+ export function usePaginationEllipsis(
19
+ host: Ref<HTMLElement | undefined>,
20
+ items: () => PaginationEntry[],
21
+ ) {
22
+ const context = usePaginationContext()
23
+ const id = useId()
24
+ const ranges = computed(() => paginationRanges(items()))
25
+ const open = ref(false)
26
+ const active = ref<PaginationSide>()
27
+ const displayRange = shallowRef<PaginationRange>()
28
+ const panel = shallowRef<HTMLElement>()
29
+ const source = shallowRef<HTMLElement>()
30
+ const rect = shallowRef<DOMRect>()
31
+ const focusRequest = shallowRef<{ edge: 'first' | 'last' }>()
32
+ const reference = computed(() =>
33
+ rect.value
34
+ ? { getBoundingClientRect: () => rect.value!, contextElement: host.value }
35
+ : undefined,
36
+ )
37
+ const triggers = new Map<PaginationSide, HTMLElement>()
38
+ let opening: ReturnType<typeof setTimeout> | undefined
39
+ let closing: ReturnType<typeof setTimeout> | undefined
40
+ let mode: 'mouse' | 'keyboard' | 'touch' = 'keyboard'
41
+ let lastInput = 'keyboard'
42
+ let restore = false
43
+ let restoring = false
44
+ let suppressed = false
45
+
46
+ function cancel() {
47
+ clearTimeout(opening)
48
+ clearTimeout(closing)
49
+ opening = closing = undefined
50
+ }
51
+
52
+ function measure() {
53
+ const element = source.value
54
+ if (!element?.isConnected) return
55
+ const next = element.getBoundingClientRect()
56
+ const old = rect.value
57
+ if (
58
+ !old ||
59
+ next.x !== old.x ||
60
+ next.y !== old.y ||
61
+ next.width !== old.width ||
62
+ next.height !== old.height
63
+ )
64
+ rect.value = next
65
+ }
66
+
67
+ const { pause, resume } = useRafFn(measure, { immediate: false })
68
+ watch(
69
+ () => open.value || !!panel.value,
70
+ present => (present ? resume() : pause()),
71
+ { flush: 'post' },
72
+ )
73
+
74
+ function close(returnFocus = false) {
75
+ cancel()
76
+ restore = returnFocus
77
+ open.value = false
78
+ focusRequest.value = undefined
79
+ suppressed = true
80
+ }
81
+
82
+ function show(side: PaginationSide, edge?: 'first' | 'last') {
83
+ cancel()
84
+ const range = ranges.value.find(range => range.side === side)
85
+ const trigger = triggers.get(side)
86
+ if (context.blocked.value || !range || !trigger?.isConnected) return
87
+ source.value = trigger
88
+ active.value = side
89
+ displayRange.value = { ...range }
90
+ rect.value = trigger.getBoundingClientRect()
91
+ restore = false
92
+ open.value = true
93
+ suppressed = false
94
+ if (edge) focusRequest.value = { edge }
95
+ }
96
+
97
+ function enter(side: PaginationSide, event: PointerEvent) {
98
+ if (event.pointerType !== 'mouse' || context.blocked.value) return
99
+ mode = 'mouse'
100
+ suppressed = false
101
+ cancel()
102
+ if (open.value) show(side)
103
+ else opening = setTimeout(() => show(side), 150)
104
+ }
105
+
106
+ function focus(side: PaginationSide) {
107
+ if (restoring || suppressed || lastInput !== 'keyboard' || context.blocked.value) return
108
+ mode = 'keyboard'
109
+ show(side)
110
+ }
111
+
112
+ function leave(event: PointerEvent) {
113
+ if (event.pointerType !== 'mouse' || mode !== 'mouse') return
114
+ clearTimeout(opening)
115
+ opening = undefined
116
+ if (!open.value) return
117
+ const a = source.value?.getBoundingClientRect()
118
+ const b = panel.value?.getBoundingClientRect()
119
+ if (a && b && inPointerCorridor({ x: event.clientX, y: event.clientY }, a, b)) return
120
+ clearTimeout(closing)
121
+ closing = setTimeout(() => close(), 100)
122
+ }
123
+
124
+ function keep() {
125
+ clearTimeout(closing)
126
+ }
127
+
128
+ function pointerdown(event: PointerEvent) {
129
+ lastInput = event.pointerType === 'touch' ? 'touch' : 'mouse'
130
+ }
131
+
132
+ function click(side: PaginationSide) {
133
+ if (context.blocked.value) return
134
+ if (lastInput === 'touch') {
135
+ mode = 'touch'
136
+ show(side)
137
+ return
138
+ }
139
+ const step = context.siblingCount.value * 2 + 1
140
+ context.update(context.state.value.page + (side === 'prev' ? -step : step))
141
+ }
142
+
143
+ function keydown(side: PaginationSide, event: KeyboardEvent) {
144
+ lastInput = 'keyboard'
145
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return
146
+ event.preventDefault()
147
+ mode = 'keyboard'
148
+ show(side, event.key === 'ArrowDown' ? 'first' : 'last')
149
+ }
150
+
151
+ function register(side: PaginationSide, value: Element | ComponentPublicInstance | null) {
152
+ const element = value && ('$el' in value ? value.$el : value)
153
+ if (element instanceof HTMLElement) triggers.set(side, element)
154
+ else {
155
+ triggers.delete(side)
156
+ if (active.value === side && open.value) close(mode === 'keyboard')
157
+ }
158
+ }
159
+
160
+ function pick(page: number) {
161
+ if (context.blocked.value) return
162
+ close(true)
163
+ context.update(page)
164
+ }
165
+
166
+ function closeAutoFocus(event: Event) {
167
+ event.preventDefault()
168
+ const previousPanel = event.target as HTMLElement
169
+ nextTick(() => {
170
+ if (open.value || !restore) return
171
+ const document = previousPanel.ownerDocument
172
+ if (
173
+ document.activeElement !== document.body &&
174
+ !previousPanel.contains(document.activeElement)
175
+ )
176
+ return
177
+ const target = source.value?.isConnected
178
+ ? source.value
179
+ : host.value?.querySelector<HTMLElement>('[aria-current="page"]')
180
+ restoring = true
181
+ target?.focus({ preventScroll: true })
182
+ nextTick(() => {
183
+ restoring = false
184
+ })
185
+ })
186
+ }
187
+
188
+ function tab(event: KeyboardEvent) {
189
+ event.stopPropagation()
190
+ close()
191
+ const target = source.value?.isConnected
192
+ ? source.value
193
+ : host.value?.querySelector<HTMLElement>('[aria-current="page"]')
194
+ target?.focus({ preventScroll: true })
195
+ }
196
+
197
+ function outside(event: Event) {
198
+ const target = event.target as Node | null
199
+ if ([...triggers.values()].some(trigger => target && trigger.contains(target)))
200
+ event.preventDefault()
201
+ }
202
+
203
+ watch(ranges, next => {
204
+ if (!open.value) return
205
+ const range = next.find(range => range.side === active.value)
206
+ if (!range) close(mode === 'keyboard')
207
+ else displayRange.value = { ...range }
208
+ })
209
+ watch(context.blocked, blocked => {
210
+ if (blocked) close()
211
+ })
212
+ useEventListener(
213
+ () => host.value?.ownerDocument,
214
+ 'pointerdown',
215
+ event => {
216
+ pointerdown(event as PointerEvent)
217
+ },
218
+ { capture: true },
219
+ )
220
+ useEventListener(
221
+ () => host.value?.ownerDocument,
222
+ 'keydown',
223
+ () => {
224
+ lastInput = 'keyboard'
225
+ },
226
+ { capture: true },
227
+ )
228
+ useEventListener(
229
+ () => host.value?.ownerDocument,
230
+ 'pointermove',
231
+ event => {
232
+ const pointer = event as PointerEvent
233
+ if (!open.value || mode !== 'mouse' || pointer.pointerType !== 'mouse') return
234
+ const target = event.target as Node
235
+ if (source.value?.contains(target) || panel.value?.contains(target)) return keep()
236
+ const a = source.value?.getBoundingClientRect()
237
+ const b = panel.value?.getBoundingClientRect()
238
+ if (a && b && inPointerCorridor({ x: pointer.clientX, y: pointer.clientY }, a, b))
239
+ return keep()
240
+ leave(pointer)
241
+ },
242
+ )
243
+ onScopeDispose(cancel)
244
+ return {
245
+ id,
246
+ open,
247
+ active,
248
+ displayRange,
249
+ panel,
250
+ reference,
251
+ focusRequest,
252
+ ranges,
253
+ enter,
254
+ focus,
255
+ leave,
256
+ keep,
257
+ click,
258
+ pointerdown,
259
+ keydown,
260
+ register,
261
+ pick,
262
+ close,
263
+ closeAutoFocus,
264
+ outside,
265
+ tab,
266
+ }
267
+ }
268
+
269
+ export type PaginationEllipsisController = ReturnType<typeof usePaginationEllipsis>
@@ -0,0 +1,23 @@
1
+ import { ref, watch } from 'vue'
2
+ import { usePaginationContext } from '../context'
3
+
4
+ export function usePaginationJump() {
5
+ const context = usePaginationContext()
6
+ const draft = ref(String(context.state.value.page))
7
+ const reset = () => {
8
+ draft.value = String(context.state.value.page)
9
+ }
10
+ watch(() => context.state.value.page, reset)
11
+ watch(context.blocked, blocked => {
12
+ if (blocked) reset()
13
+ })
14
+ function commit() {
15
+ if (context.blocked.value) return reset()
16
+ const value = Number(draft.value.trim())
17
+ if (!draft.value.trim() || !Number.isFinite(value)) return reset()
18
+ const next = Math.min(context.state.value.pageCount, Math.max(1, Math.trunc(value)))
19
+ draft.value = String(next)
20
+ context.update(next)
21
+ }
22
+ return { draft, commit, reset, size: context.size, blocked: context.blocked }
23
+ }