@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
@@ -1,4 +1,4 @@
1
- import { onBeforeUnmount, shallowRef } from 'vue'
1
+ import { computed, onBeforeUnmount, shallowRef } from 'vue'
2
2
  import { animate } from 'motion-v'
3
3
  import { prefersReducedMotion, type TransitionName } from '../../../motion'
4
4
  import type { useLightboxMotion } from './useLightboxMotion'
@@ -6,6 +6,7 @@ import { FLING, TRACKPAD_INTENSITY } from '../utils/gesture'
6
6
  import {
7
7
  WHEEL_INTENSITY,
8
8
  ZOOM_MIN,
9
+ ZOOM_EPSILON,
9
10
  clampOffset,
10
11
  clampZoom,
11
12
  doubleTapZoom,
@@ -30,16 +31,28 @@ export interface PinchStart {
30
31
 
31
32
  export function useLightboxZoom(
32
33
  motion: ReturnType<typeof useLightboxMotion>,
33
- geometry: () => { fit: Size; stage: Size; base: number },
34
+ geometry: () => {
35
+ fit: Size
36
+ frame: Size
37
+ stage: Size
38
+ base: number
39
+ original: number
40
+ secondary: number
41
+ max: number
42
+ },
34
43
  ) {
35
44
  const { x, y, scale } = motion
36
- const zoomed = shallowRef(false)
45
+ const raw = shallowRef(scale.get())
37
46
  const unsubscribe = scale.on('change', value => {
38
- zoomed.value = isZoomed(value / geometry().base)
47
+ raw.value = value
39
48
  })
40
49
  onBeforeUnmount(unsubscribe)
41
50
 
42
- const zoom = () => scale.get() / geometry().base
51
+ const zoom = () => raw.value / geometry().base
52
+ const zoomed = computed(() => isZoomed(zoom()))
53
+ const canZoomIn = computed(() => zoom() < geometry().max - ZOOM_EPSILON)
54
+ const canToggle = computed(() => geometry().secondary > ZOOM_MIN + ZOOM_EPSILON)
55
+ const atOriginal = computed(() => Math.abs(zoom() - geometry().original) < ZOOM_EPSILON)
43
56
  const offset = (): Point => ({ x: x.get(), y: y.get() })
44
57
 
45
58
  function bounds(level = zoom()): Bounds {
@@ -57,7 +70,7 @@ export function useLightboxZoom(
57
70
  }
58
71
 
59
72
  function target(level: number, focal: Point): { level: number; offset: Point } {
60
- const clamped = clampZoom(level)
73
+ const clamped = clampZoom(level, ZOOM_MIN, geometry().max)
61
74
  if (!isZoomed(clamped)) return { level: ZOOM_MIN, offset: { x: 0, y: 0 } }
62
75
  return {
63
76
  level: clamped,
@@ -74,7 +87,7 @@ export function useLightboxZoom(
74
87
  }
75
88
 
76
89
  function step(direction: 1 | -1) {
77
- zoomTo(stepZoom(zoom(), direction), { x: 0, y: 0 })
90
+ zoomTo(stepZoom(zoom(), direction, geometry().max), { x: 0, y: 0 })
78
91
  }
79
92
 
80
93
  function reset() {
@@ -88,11 +101,11 @@ export function useLightboxZoom(
88
101
  }
89
102
 
90
103
  function doubleTap(focal: Point) {
91
- zoomTo(doubleTapZoom(zoom()), focal)
104
+ zoomTo(doubleTapZoom(zoom(), geometry().secondary), focal)
92
105
  }
93
106
 
94
107
  function pinch(start: PinchStart, mid: Point, ratio: number) {
95
- const level = elasticZoom(start.zoom * ratio)
108
+ const level = elasticZoom(start.zoom * ratio, ZOOM_MIN, geometry().max)
96
109
  const about = zoomAbout(start.offset, start.zoom, level, start.mid)
97
110
  place(level, { x: about.x + mid.x - start.mid.x, y: about.y + mid.y - start.mid.y })
98
111
  }
@@ -101,6 +114,30 @@ export function useLightboxZoom(
101
114
  zoomTo(zoom(), focal, 'press')
102
115
  }
103
116
 
117
+ function reflow(update: () => void) {
118
+ const before = geometry()
119
+ const wasZoomed = isZoomed(zoom())
120
+ const currentScale = scale.get()
121
+ update()
122
+ const after = geometry()
123
+ if (before.frame.width <= 0 || after.frame.width <= 0) return
124
+ if (
125
+ before.frame.width === after.frame.width &&
126
+ before.frame.height === after.frame.height &&
127
+ before.base === after.base &&
128
+ before.stage.width === after.stage.width &&
129
+ before.stage.height === after.stage.height
130
+ )
131
+ return
132
+ scale.stop()
133
+ scale.set((currentScale * before.frame.width) / after.frame.width)
134
+ zoomTo(wasZoomed ? zoom() : ZOOM_MIN, { x: 0, y: 0 })
135
+ }
136
+
137
+ function original() {
138
+ zoomTo(geometry().original, { x: 0, y: 0 })
139
+ }
140
+
104
141
  function pan(origin: Point, delta: Point) {
105
142
  const next = elasticOffset(
106
143
  { x: origin.x + delta.x, y: origin.y + delta.y },
@@ -133,6 +170,11 @@ export function useLightboxZoom(
133
170
 
134
171
  return {
135
172
  zoomed,
173
+ canZoomIn,
174
+ canToggle,
175
+ atOriginal,
176
+ original,
177
+ reflow,
136
178
  zoom,
137
179
  isZoomed: () => isZoomed(zoom()),
138
180
  zoomTo,
@@ -0,0 +1,5 @@
1
+ import { tv } from '../../lib/tv'
2
+
3
+ export const lightboxStage = tv({
4
+ base: 'dark fixed inset-0 z-(--hn-z-overlay) touch-none p-4 outline-none select-none sm:p-6',
5
+ })
@@ -1,10 +1,14 @@
1
1
  import type { ImageVariants } from '../image/image.variants'
2
+ import type { Rect } from './utils/pose'
3
+
4
+ export type LightboxSource = HTMLImageElement | Rect
2
5
 
3
6
  export interface LightboxItem {
4
7
  id: string
5
8
  src: string
6
9
  preview?: string
10
+ previewSize?: { width: number; height: number }
7
11
  alt: string
8
12
  fit?: ImageVariants['fit']
9
- source?: () => HTMLImageElement | null | undefined
13
+ source?: () => LightboxSource | null | undefined
10
14
  }
@@ -0,0 +1,117 @@
1
+ import type { Rect } from './pose'
2
+
3
+ export interface Corner {
4
+ x: number
5
+ y: number
6
+ }
7
+
8
+ export type Corners = [Corner, Corner, Corner, Corner]
9
+
10
+ function emptyCorners(): Corners {
11
+ return [
12
+ { x: 0, y: 0 },
13
+ { x: 0, y: 0 },
14
+ { x: 0, y: 0 },
15
+ { x: 0, y: 0 },
16
+ ]
17
+ }
18
+
19
+ function length(value: string, size: number): number {
20
+ const number = Number.parseFloat(value) || 0
21
+ return value.endsWith('%') ? (number * size) / 100 : number
22
+ }
23
+
24
+ function cornersOf(style: CSSStyleDeclaration, width: number, height: number): Corners {
25
+ const corners = [
26
+ style.borderTopLeftRadius,
27
+ style.borderTopRightRadius,
28
+ style.borderBottomRightRadius,
29
+ style.borderBottomLeftRadius,
30
+ ].map(value => {
31
+ const [x = '0', y = x] = value.split(/\s+/)
32
+ return { x: length(x, width), y: length(y, height) }
33
+ }) as Corners
34
+ const [tl, tr, br, bl] = corners
35
+ const factor = Math.min(
36
+ 1,
37
+ width / (tl.x + tr.x || 1),
38
+ width / (bl.x + br.x || 1),
39
+ height / (tl.y + bl.y || 1),
40
+ height / (tr.y + br.y || 1),
41
+ )
42
+ return corners.map(corner => ({ x: corner.x * factor, y: corner.y * factor })) as Corners
43
+ }
44
+
45
+ function paddingBox(node: Element, style: CSSStyleDeclaration) {
46
+ const box = node.getBoundingClientRect()
47
+ const width = node instanceof HTMLElement ? node.offsetWidth : box.width
48
+ const height = node instanceof HTMLElement ? node.offsetHeight : box.height
49
+ const sx = width ? box.width / width : 1
50
+ const sy = height ? box.height / height : 1
51
+ const left = Number.parseFloat(style.borderLeftWidth) || 0
52
+ const right = Number.parseFloat(style.borderRightWidth) || 0
53
+ const top = Number.parseFloat(style.borderTopWidth) || 0
54
+ const bottom = Number.parseFloat(style.borderBottomWidth) || 0
55
+ const corners = cornersOf(style, width, height)
56
+ const borders = [
57
+ [left, top],
58
+ [right, top],
59
+ [right, bottom],
60
+ [left, bottom],
61
+ ]
62
+ return {
63
+ rect: {
64
+ x: box.x + left * sx,
65
+ y: box.y + top * sy,
66
+ width: Math.max(0, box.width - (left + right) * sx),
67
+ height: Math.max(0, box.height - (top + bottom) * sy),
68
+ },
69
+ corners: corners.map((corner, index) => ({
70
+ x: Math.max(0, corner.x - borders[index]![0]!) * sx,
71
+ y: Math.max(0, corner.y - borders[index]![1]!) * sy,
72
+ })) as Corners,
73
+ }
74
+ }
75
+
76
+ function points(rect: Rect) {
77
+ return [
78
+ [rect.x, rect.y],
79
+ [rect.x + rect.width, rect.y],
80
+ [rect.x + rect.width, rect.y + rect.height],
81
+ [rect.x, rect.y + rect.height],
82
+ ]
83
+ }
84
+
85
+ export function clipOf(el: Element | null | undefined, box: Rect) {
86
+ let rect = { x: box.x, y: box.y, width: box.width, height: box.height }
87
+ const clips: ReturnType<typeof paddingBox>[] = []
88
+ for (let node = el; node; node = node.parentElement) {
89
+ const style = getComputedStyle(node)
90
+ const clipX = node === el || /^(hidden|clip|scroll|auto)$/.test(style.overflowX)
91
+ const clipY = node === el || /^(hidden|clip|scroll|auto)$/.test(style.overflowY)
92
+ if (!clipX && !clipY) continue
93
+ const clip = paddingBox(node, style)
94
+ const x = clipX ? Math.max(rect.x, clip.rect.x) : rect.x
95
+ const y = clipY ? Math.max(rect.y, clip.rect.y) : rect.y
96
+ const right = clipX
97
+ ? Math.min(rect.x + rect.width, clip.rect.x + clip.rect.width)
98
+ : rect.x + rect.width
99
+ const bottom = clipY
100
+ ? Math.min(rect.y + rect.height, clip.rect.y + clip.rect.height)
101
+ : rect.y + rect.height
102
+ rect = { x, y, width: Math.max(0, right - x), height: Math.max(0, bottom - y) }
103
+ if (clipX && clipY) clips.push(clip)
104
+ }
105
+ const corners = emptyCorners()
106
+ const visible = points(rect)
107
+ for (const clip of clips) {
108
+ const edges = points(clip.rect)
109
+ for (let index = 0; index < 4; index += 1) {
110
+ if (visible[index]!.some((value, axis) => Math.abs(value - edges[index]![axis]!) > 0.5))
111
+ continue
112
+ corners[index]!.x = Math.max(corners[index]!.x, clip.corners[index]!.x)
113
+ corners[index]!.y = Math.max(corners[index]!.y, clip.corners[index]!.y)
114
+ }
115
+ }
116
+ return { rect, corners }
117
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ImageVariants } from '../../image/image.variants'
2
+ import type { Corners } from './clip'
2
3
  import { fitSize, rotatedSize, type Size } from './zoom'
3
4
 
4
5
  export interface Rect {
@@ -21,7 +22,7 @@ export const REST_POSE: Pose = {
21
22
  y: 0,
22
23
  scale: 1,
23
24
  rotate: 0,
24
- clipPath: 'inset(0px 0px 0px 0px round 0px)',
25
+ clipPath: 'inset(0px 0px 0px 0px round 0px 0px 0px 0px / 0px 0px 0px 0px)',
25
26
  }
26
27
  export const TRAVEL_SPRING = { type: 'spring', visualDuration: 0.3, bounce: 0 } as const
27
28
  export const RETURN_SPRING = { ...TRAVEL_SPRING, visualDuration: 0.22 } as const
@@ -34,11 +35,15 @@ function px(value: number): string {
34
35
  return `${Math.round(value * 100) / 100}px`
35
36
  }
36
37
 
37
- export function fitRect(natural: Size, stage: Size, rotation = 0): Rect {
38
+ export function fitRect(
39
+ natural: Size,
40
+ stage: Size & Partial<Pick<Rect, 'x' | 'y'>>,
41
+ rotation = 0,
42
+ ): Rect {
38
43
  const size = fitSize(natural, stage, rotation)
39
44
  return {
40
- x: (stage.width - size.width) / 2,
41
- y: (stage.height - size.height) / 2,
45
+ x: (stage.x ?? 0) + (stage.width - size.width) / 2,
46
+ y: (stage.y ?? 0) + (stage.height - size.height) / 2,
42
47
  width: size.width,
43
48
  height: size.height,
44
49
  }
@@ -79,39 +84,51 @@ export function openPose(
79
84
  box: Rect,
80
85
  natural: Size,
81
86
  fit?: ImageVariants['fit'],
82
- radius = 0,
87
+ radius: number | Corners = 0,
88
+ clipBox = box,
83
89
  ): Pose {
84
90
  const rendered = renderedRect(box, natural, fit)
85
91
  if (frame.width <= 0 || frame.height <= 0 || rendered.width <= 0) return REST_POSE
86
92
  const scale = rendered.width / frame.width
87
- const visible = intersect(box, rendered)
93
+ const visible = intersect(intersect(box, clipBox), rendered)
88
94
  const top = (visible.y - rendered.y) / scale
89
95
  const left = (visible.x - rendered.x) / scale
90
96
  const right = (rendered.x + rendered.width - visible.x - visible.width) / scale
91
97
  const bottom = (rendered.y + rendered.height - visible.y - visible.height) / scale
92
- const corner = Math.max(0, radius) / scale
98
+ const corners =
99
+ typeof radius === 'number'
100
+ ? Array.from({ length: 4 }, () => ({ x: radius, y: radius }))
101
+ : radius
102
+ const leftEdge = Math.abs(visible.x - clipBox.x) <= 0.5
103
+ const topEdge = Math.abs(visible.y - clipBox.y) <= 0.5
104
+ const rightEdge = Math.abs(visible.x + visible.width - clipBox.x - clipBox.width) <= 0.5
105
+ const bottomEdge = Math.abs(visible.y + visible.height - clipBox.y - clipBox.height) <= 0.5
106
+ const touches = [
107
+ leftEdge && topEdge,
108
+ rightEdge && topEdge,
109
+ rightEdge && bottomEdge,
110
+ leftEdge && bottomEdge,
111
+ ]
112
+ const clipped = corners.map((corner, index) => (touches[index] ? corner : { x: 0, y: 0 }))
113
+ const horizontal = clipped.map(corner => px(Math.max(0, corner.x) / scale)).join(' ')
114
+ const vertical = clipped.map(corner => px(Math.max(0, corner.y) / scale)).join(' ')
93
115
  return {
94
116
  x: rendered.x + rendered.width / 2 - (frame.x + frame.width / 2),
95
117
  y: rendered.y + rendered.height / 2 - (frame.y + frame.height / 2),
96
118
  scale,
97
119
  rotate: 0,
98
- clipPath: `inset(${px(top)} ${px(right)} ${px(bottom)} ${px(left)} round ${px(corner)})`,
120
+ clipPath: `inset(${px(top)} ${px(right)} ${px(bottom)} ${px(left)} round ${horizontal} / ${vertical})`,
99
121
  }
100
122
  }
101
123
 
102
- export function baseScale(frame: Size, stage: Size, rotation: number): number {
124
+ export function baseScale(frame: Size, stage: Size, rotation: number, natural: Size): number {
103
125
  const visual = rotatedSize(frame, rotation)
104
126
  if (visual.width <= 0 || visual.height <= 0) return 1
105
- if (rotation % 180 === 0) return 1
106
- return Math.min(stage.width / visual.width, stage.height / visual.height)
107
- }
108
-
109
- export function radiusOf(el: Element | null | undefined): number {
110
- for (let node = el, depth = 0; node && depth < 2; node = node.parentElement, depth += 1) {
111
- const value = Number.parseFloat(getComputedStyle(node).borderTopLeftRadius)
112
- if (value > 0) return value
113
- }
114
- return 0
127
+ return Math.min(
128
+ stage.width / visual.width,
129
+ stage.height / visual.height,
130
+ natural.width / frame.width,
131
+ )
115
132
  }
116
133
 
117
134
  export function dismissProgress(offsetY: number, stageHeight: number): number {
@@ -0,0 +1,17 @@
1
+ import type { LightboxSource } from '../types'
2
+ import type { Rect } from './pose'
3
+
4
+ export function sourceRect(source: LightboxSource | null | undefined): Rect | null {
5
+ if (!source) return null
6
+ const isElement = 'getBoundingClientRect' in source
7
+ if (isElement && !source.isConnected) return null
8
+ const rect = isElement ? source.getBoundingClientRect() : source
9
+ if (
10
+ ![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite) ||
11
+ rect.width <= 0 ||
12
+ rect.height <= 0
13
+ ) {
14
+ return null
15
+ }
16
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
17
+ }
@@ -16,8 +16,7 @@ export interface Bounds {
16
16
  }
17
17
 
18
18
  export const ZOOM_MIN = 1
19
- export const ZOOM_MAX = 6
20
- export const ZOOM_DOUBLE_TAP = 2.5
19
+ export const ZOOM_MAX = 2
21
20
  export const ZOOM_STEP = 1.5
22
21
  export const OVERSHOOT = 0.55
23
22
  export const WHEEL_INTENSITY = 0.002
@@ -36,7 +35,7 @@ export function rotatedSize(size: Size, rotation: number): Size {
36
35
  export function fitSize(natural: Size, stage: Size, rotation = 0): Size {
37
36
  const upright = rotatedSize(natural, rotation)
38
37
  if (upright.width <= 0 || upright.height <= 0) return { width: 0, height: 0 }
39
- const scale = Math.min(stage.width / upright.width, stage.height / upright.height)
38
+ const scale = Math.min(1, stage.width / upright.width, stage.height / upright.height)
40
39
  return { width: upright.width * scale, height: upright.height * scale }
41
40
  }
42
41
 
@@ -104,10 +103,25 @@ export function wheelZoom(zoom: number, deltaY: number, intensity = WHEEL_INTENS
104
103
  return zoom * Math.exp(-deltaY * intensity)
105
104
  }
106
105
 
107
- export function doubleTapZoom(zoom: number): number {
108
- return isZoomed(zoom) ? ZOOM_MIN : ZOOM_DOUBLE_TAP
106
+ export function zoomLevels(natural: Size, fit: Size, stage: Size) {
107
+ const original = fit.width > 0 ? Math.max(ZOOM_MIN, natural.width / fit.width) : ZOOM_MIN
108
+ const secondary =
109
+ natural.height > natural.width * 2
110
+ ? Math.min(original, stage.width / fit.width)
111
+ : natural.width > natural.height * 2
112
+ ? Math.min(original, stage.height / fit.height)
113
+ : original
114
+ return {
115
+ original,
116
+ secondary: Number.isFinite(secondary) ? Math.max(ZOOM_MIN, secondary) : ZOOM_MIN,
117
+ max: original * ZOOM_MAX,
118
+ }
119
+ }
120
+
121
+ export function doubleTapZoom(zoom: number, secondary: number): number {
122
+ return isZoomed(zoom) ? ZOOM_MIN : secondary
109
123
  }
110
124
 
111
- export function stepZoom(zoom: number, direction: 1 | -1): number {
112
- return clampZoom(zoom * ZOOM_STEP ** direction)
125
+ export function stepZoom(zoom: number, direction: 1 | -1, max = ZOOM_MAX): number {
126
+ return clampZoom(zoom * ZOOM_STEP ** direction, ZOOM_MIN, max)
113
127
  }
@@ -0,0 +1,118 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { PaginationRoot } from 'reka-ui'
4
+ import { cn } from '../../lib/cn'
5
+ import { useDirection } from '../../lib/useDirection'
6
+ import { useUiLocale } from '../../locale'
7
+ import LoadingOverlay from '../loading-overlay/LoadingOverlay.vue'
8
+ import PaginationContent from './PaginationContent.vue'
9
+ import PaginationInfo from './PaginationInfo.vue'
10
+ import PaginationSize from './PaginationSize.vue'
11
+ import PaginationJump from './PaginationJump.vue'
12
+ import { providePagination } from './context'
13
+ import { usePagination } from './composables/usePagination'
14
+ import { pagination } from './pagination.variants'
15
+ import type { PaginationChange, PaginationState } from './types'
16
+
17
+ defineOptions({ name: 'HnPagination', inheritAttrs: false })
18
+ const props = withDefaults(
19
+ defineProps<{
20
+ total: number
21
+ itemCount?: number
22
+ siblingCount?: number
23
+ showEdges?: boolean
24
+ showFirstLast?: boolean
25
+ showInfo?: boolean
26
+ showJump?: boolean
27
+ hideSinglePage?: boolean
28
+ pageSizeOptions?: number[]
29
+ pending?: boolean
30
+ align?: 'start' | 'center' | 'end' | 'between'
31
+ size?: 'sm' | 'md' | 'lg'
32
+ disabled?: boolean
33
+ dir?: 'ltr' | 'rtl'
34
+ label?: string
35
+ class?: string
36
+ }>(),
37
+ { siblingCount: 1, showEdges: true, size: 'md', align: 'start' },
38
+ )
39
+ const model = defineModel<number>({ default: 1 })
40
+ const sizeModel = defineModel<number>('pageSize', { default: 10 })
41
+ const emit = defineEmits<{ change: [value: PaginationChange] }>()
42
+ defineSlots<{
43
+ default?(state: PaginationState): unknown
44
+ list?(state: PaginationState): unknown
45
+ page?(props: { page: number; selected: boolean }): unknown
46
+ ellipsis?(props: { side: 'prev' | 'next'; expanded: boolean }): unknown
47
+ }>()
48
+ const t = useUiLocale()
49
+ const { root, direction, rootDirection } = useDirection(() => props.dir)
50
+ const { total, pageSize, siblingCount, page, state, blocked, sizes, update, resize } =
51
+ usePagination(props, model, sizeModel, value => emit('change', value))
52
+ providePagination({
53
+ state,
54
+ blocked,
55
+ siblingCount,
56
+ update,
57
+ resize,
58
+ direction,
59
+ size: computed(() => props.size),
60
+ showFirstLast: computed(() => props.showFirstLast),
61
+ options: computed(() =>
62
+ sizes.value.map(value => ({ value, label: t.value.pagination.pageSizeOption(value) })),
63
+ ),
64
+ })
65
+ </script>
66
+
67
+ <template>
68
+ <div
69
+ ref="root"
70
+ v-bind="$attrs"
71
+ data-hn-pagination
72
+ :dir="rootDirection"
73
+ :aria-busy="props.pending || undefined"
74
+ :class="cn('max-w-full', $slots.list && 'flex min-h-0 flex-col gap-3', props.class)"
75
+ >
76
+ <div v-if="$slots.list" class="relative min-h-0 flex-auto">
77
+ <div :inert="props.pending || undefined"><slot name="list" v-bind="state" /></div>
78
+ <LoadingOverlay :visible="props.pending" />
79
+ </div>
80
+ <PaginationRoot
81
+ v-if="!props.hideSinglePage || state.pageCount > 1"
82
+ :page="page"
83
+ :total="total"
84
+ :items-per-page="pageSize"
85
+ :sibling-count="siblingCount"
86
+ :show-edges="props.showEdges"
87
+ :disabled="blocked"
88
+ as-child
89
+ @update:page="update"
90
+ >
91
+ <nav
92
+ :aria-label="props.label ?? t.pagination.navLabel"
93
+ :inert="props.pending || undefined"
94
+ :data-disabled="blocked ? '' : undefined"
95
+ :class="pagination({ align: props.align })"
96
+ >
97
+ <slot v-bind="state">
98
+ <PaginationInfo v-if="props.showInfo" />
99
+ <PaginationContent>
100
+ <template v-if="$slots.page" #page="slotProps">
101
+ <slot name="page" v-bind="slotProps" />
102
+ </template>
103
+ <template v-if="$slots.ellipsis" #ellipsis="slotProps">
104
+ <slot name="ellipsis" v-bind="slotProps" />
105
+ </template>
106
+ </PaginationContent>
107
+ <div
108
+ v-if="props.pageSizeOptions?.length || props.showJump"
109
+ class="flex max-w-full flex-wrap items-center gap-3"
110
+ >
111
+ <PaginationSize v-if="props.pageSizeOptions?.length" />
112
+ <PaginationJump v-if="props.showJump" />
113
+ </div>
114
+ </slot>
115
+ </nav>
116
+ </PaginationRoot>
117
+ </div>
118
+ </template>
@@ -0,0 +1,29 @@
1
+ <script setup lang="ts">
2
+ import { shallowRef } from 'vue'
3
+ import { PaginationList } from 'reka-ui'
4
+ import { cn } from '../../lib/cn'
5
+ import PaginationPages from './PaginationPages.vue'
6
+ import { paginationList } from './pagination.variants'
7
+ defineOptions({ name: 'HnPaginationContent' })
8
+ const props = defineProps<{ class?: string }>()
9
+ defineSlots<{
10
+ page?(props: { page: number; selected: boolean }): unknown
11
+ ellipsis?(props: { side: 'prev' | 'next'; expanded: boolean }): unknown
12
+ }>()
13
+ const host = shallowRef<HTMLElement>()
14
+ </script>
15
+
16
+ <template>
17
+ <PaginationList v-slot="{ items }" as-child>
18
+ <ul ref="host" data-hn-pagination-content :class="cn(paginationList(), props.class)">
19
+ <PaginationPages :items="items" :host="host">
20
+ <template v-if="$slots.page" #page="slotProps">
21
+ <slot name="page" v-bind="slotProps" />
22
+ </template>
23
+ <template v-if="$slots.ellipsis" #ellipsis="slotProps">
24
+ <slot name="ellipsis" v-bind="slotProps" />
25
+ </template>
26
+ </PaginationPages>
27
+ </ul>
28
+ </PaginationList>
29
+ </template>
@@ -0,0 +1,42 @@
1
+ <script setup lang="ts">
2
+ import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from '@lucide/vue'
3
+ import { PaginationFirst, PaginationPrev, PaginationNext, PaginationLast } from 'reka-ui'
4
+ import { useUiLocale } from '../../locale'
5
+ import Button from '../button/Button.vue'
6
+ import type { ButtonVariants } from '../button/button.variants'
7
+ import { paginationArrow } from './pagination.variants'
8
+
9
+ defineOptions({ name: 'HnPaginationControl' })
10
+
11
+ const props = defineProps<{
12
+ action: 'first' | 'prev' | 'next' | 'last'
13
+ size?: ButtonVariants['size']
14
+ dir: 'ltr' | 'rtl'
15
+ }>()
16
+ const t = useUiLocale()
17
+ const controls = {
18
+ first: { component: PaginationFirst, icon: ChevronsLeft },
19
+ prev: { component: PaginationPrev, icon: ChevronLeft },
20
+ next: { component: PaginationNext, icon: ChevronRight },
21
+ last: { component: PaginationLast, icon: ChevronsRight },
22
+ }
23
+ </script>
24
+
25
+ <template>
26
+ <component :is="controls[props.action].component" as-child>
27
+ <Button
28
+ variant="ghost"
29
+ tone="neutral"
30
+ :size="props.size"
31
+ icon-only
32
+ :aria-label="t.pagination[props.action]"
33
+ :data-hn-pagination-action="props.action"
34
+ >
35
+ <component
36
+ :is="controls[props.action].icon"
37
+ aria-hidden="true"
38
+ :class="paginationArrow({ dir: props.dir })"
39
+ />
40
+ </Button>
41
+ </component>
42
+ </template>