@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
@@ -1,4 +1,4 @@
1
- import { nextTick, shallowRef, type Ref } from 'vue'
1
+ import { nextTick, onScopeDispose, shallowRef, type Ref } from 'vue'
2
2
  import type { useLightboxMotion } from './useLightboxMotion'
3
3
  import type { Pose } from '../utils/pose'
4
4
 
@@ -10,7 +10,7 @@ export function useLightboxPhase(options: {
10
10
  locked: Ref<boolean>
11
11
  motion: ReturnType<typeof useLightboxMotion>
12
12
  pose: () => Pose | null
13
- prepare: () => void
13
+ prepare: () => void | Promise<void>
14
14
  layout: () => void
15
15
  }) {
16
16
  const { motion } = options
@@ -21,10 +21,13 @@ export function useLightboxPhase(options: {
21
21
  if (phase.value !== 'closed') return
22
22
  phase.value = 'entering'
23
23
  const run = ++generation
24
- options.prepare()
24
+ const preparing = options.prepare()
25
+ if (preparing) await preparing
26
+ if (run !== generation) return
25
27
  options.mounted.value = true
26
28
  options.locked.value = true
27
29
  await nextTick()
30
+ if (run !== generation) return
28
31
  options.layout()
29
32
  await motion.enter(options.pose())
30
33
  if (run === generation) phase.value = 'open'
@@ -42,7 +45,7 @@ export function useLightboxPhase(options: {
42
45
  if (phase.value === 'closed' || phase.value === 'closing') return
43
46
  phase.value = 'closing'
44
47
  const run = ++generation
45
- await motion.leave(options.pose())
48
+ if (options.mounted.value) await motion.leave(options.pose())
46
49
  if (run !== generation) return
47
50
  phase.value = 'closed'
48
51
  options.mounted.value = false
@@ -55,5 +58,10 @@ export function useLightboxPhase(options: {
55
58
  else if (phase.value === 'closing') void reopen()
56
59
  }
57
60
 
61
+ onScopeDispose(() => {
62
+ generation++
63
+ motion.stop()
64
+ })
65
+
58
66
  return { current: () => phase.value, show, close, reopen, interrupt }
59
67
  }
@@ -1,35 +1,33 @@
1
1
  import type { useLightboxFrames } from './useLightboxFrames'
2
- import { openPose, type Pose, type Rect } from '../utils/pose'
2
+ import { openPose, type Pose } from '../utils/pose'
3
3
  import { clipOf } from '../utils/clip'
4
+ import { sourceRect } from '../utils/source'
4
5
  import type { LightboxItem } from '../types'
5
6
 
6
7
  export function useLightboxPose(
7
8
  frames: ReturnType<typeof useLightboxFrames>,
8
9
  current: () => LightboxItem | undefined,
9
10
  ) {
10
- function sourceBox(): Rect | null {
11
- const el = current()?.source?.()
12
- if (!el?.isConnected) return null
13
- const rect = el.getBoundingClientRect()
14
- if (rect.width <= 0 || rect.height <= 0) return null
11
+ function fromSource(): Pose | null {
12
+ const item = current()
13
+ const source = item?.source?.()
14
+ const box = sourceRect(source)
15
+ const el = source && 'getBoundingClientRect' in source ? source : undefined
16
+ const natural =
17
+ el?.naturalWidth && el.naturalHeight
18
+ ? { width: el.naturalWidth, height: el.naturalHeight }
19
+ : (frames.naturalOf(item) ?? frames.displayOf(item))
20
+ if (!box || !natural) return null
15
21
  const stage = frames.stage.value
16
22
  if (
17
- rect.bottom <= 0 ||
18
- rect.right <= 0 ||
19
- rect.top >= stage.height ||
20
- rect.left >= stage.width
23
+ box.y + box.height <= 0 ||
24
+ box.x + box.width <= 0 ||
25
+ box.y >= stage.height ||
26
+ box.x >= stage.width
21
27
  ) {
22
28
  return null
23
29
  }
24
- return { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
25
- }
26
-
27
- function fromSource(): Pose | null {
28
- const item = current()
29
- const box = sourceBox()
30
- const natural = frames.naturalOf(item)
31
- if (!box || !natural) return null
32
- const clip = clipOf(item?.source?.(), box)
30
+ const clip = clipOf(el, box)
33
31
  if (clip.rect.width <= 0 || clip.rect.height <= 0) return null
34
32
  return openPose(frames.frameOf(item), box, natural, item?.fit, clip.corners, clip.rect)
35
33
  }
@@ -1,7 +1,7 @@
1
1
  import { reactive } from 'vue'
2
2
  import type { useLightboxFrames } from './useLightboxFrames'
3
3
  import { baseScale } from '../utils/pose'
4
- import { rotatedSize } from '../utils/zoom'
4
+ import { rotatedSize, zoomLevels } from '../utils/zoom'
5
5
  import type { LightboxItem } from '../types'
6
6
 
7
7
  export function useLightboxRotation(
@@ -15,17 +15,27 @@ export function useLightboxRotation(
15
15
  }
16
16
 
17
17
  function baseOf(item: LightboxItem | undefined): number {
18
- return baseScale(frames.frameOf(item), frames.stage.value, rotationOf(item) % 360)
18
+ const frame = frames.frameOf(item)
19
+ return baseScale(
20
+ frame,
21
+ frames.area.value,
22
+ rotationOf(item) % 360,
23
+ frames.displayOf(item) ?? frame,
24
+ )
19
25
  }
20
26
 
21
27
  function geometry() {
22
28
  const item = current()
23
29
  const base = baseOf(item)
24
30
  const visual = rotatedSize(frames.frameOf(item), rotationOf(item) % 360)
31
+ const fit = { width: visual.width * base, height: visual.height * base }
32
+ const natural = rotatedSize(frames.displayOf(item) ?? fit, rotationOf(item) % 360)
25
33
  return {
26
- fit: { width: visual.width * base, height: visual.height * base },
27
- stage: frames.stage.value,
34
+ fit,
35
+ frame: frames.frameOf(item),
36
+ stage: frames.area.value,
28
37
  base,
38
+ ...zoomLevels(natural, fit, frames.area.value),
29
39
  }
30
40
  }
31
41
 
@@ -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
  }
@@ -35,11 +35,15 @@ function px(value: number): string {
35
35
  return `${Math.round(value * 100) / 100}px`
36
36
  }
37
37
 
38
- 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 {
39
43
  const size = fitSize(natural, stage, rotation)
40
44
  return {
41
- x: (stage.width - size.width) / 2,
42
- 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,
43
47
  width: size.width,
44
48
  height: size.height,
45
49
  }
@@ -117,11 +121,14 @@ export function openPose(
117
121
  }
118
122
  }
119
123
 
120
- export function baseScale(frame: Size, stage: Size, rotation: number): number {
124
+ export function baseScale(frame: Size, stage: Size, rotation: number, natural: Size): number {
121
125
  const visual = rotatedSize(frame, rotation)
122
126
  if (visual.width <= 0 || visual.height <= 0) return 1
123
- if (rotation % 180 === 0) return 1
124
- return Math.min(stage.width / visual.width, stage.height / visual.height)
127
+ return Math.min(
128
+ stage.width / visual.width,
129
+ stage.height / visual.height,
130
+ natural.width / frame.width,
131
+ )
125
132
  }
126
133
 
127
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>
@@ -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>