@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,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
+ }
@@ -0,0 +1,23 @@
1
+ import { computed, ref, type Ref } from 'vue'
2
+ import { useEventListener, useMutationObserver, useResizeObserver } from '@vueuse/core'
3
+
4
+ export function usePaginationPageTooltip(
5
+ label: Readonly<Ref<HTMLElement | null>>,
6
+ blocked: Readonly<Ref<boolean>>,
7
+ ) {
8
+ const content = ref('')
9
+
10
+ function measure() {
11
+ const element = label.value
12
+ content.value =
13
+ element && element.scrollWidth > element.clientWidth
14
+ ? (element.textContent?.trim() ?? '')
15
+ : ''
16
+ }
17
+
18
+ useResizeObserver(label, measure)
19
+ useMutationObserver(label, measure, { childList: true, characterData: true, subtree: true })
20
+ useEventListener(() => label.value?.ownerDocument.fonts, 'loadingdone', measure)
21
+
22
+ return computed(() => (blocked.value ? false : content.value))
23
+ }