@hina-ui/vue 1.3.1 → 1.5.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 (215) hide show
  1. package/dist/index.js +4336 -2180
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/components/dialog/Dialog.vue.d.ts +3 -1
  4. package/dist/types/components/dialog/Dialog.vue.d.ts.map +1 -1
  5. package/dist/types/components/drawer/Drawer.vue.d.ts +21 -14
  6. package/dist/types/components/drawer/Drawer.vue.d.ts.map +1 -1
  7. package/dist/types/components/drawer/drawer.variants.d.ts +10 -1
  8. package/dist/types/components/drawer/drawer.variants.d.ts.map +1 -1
  9. package/dist/types/components/dropdown-menu/DropdownMenu.vue.d.ts +4 -1
  10. package/dist/types/components/dropdown-menu/DropdownMenu.vue.d.ts.map +1 -1
  11. package/dist/types/components/hover-card/HoverCard.vue.d.ts +10 -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 +29 -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 +22 -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 +3 -1
  18. package/dist/types/components/image/Image.vue.d.ts.map +1 -1
  19. package/dist/types/components/image/composables/useImageGroupState.d.ts.map +1 -1
  20. package/dist/types/components/image/image.variants.d.ts +1 -1
  21. package/dist/types/components/lightbox/Lightbox.vue.d.ts.map +1 -1
  22. package/dist/types/components/lightbox/LightboxChrome.vue.d.ts +4 -0
  23. package/dist/types/components/lightbox/LightboxChrome.vue.d.ts.map +1 -1
  24. package/dist/types/components/lightbox/LightboxToolbar.vue.d.ts +4 -0
  25. package/dist/types/components/lightbox/LightboxToolbar.vue.d.ts.map +1 -1
  26. package/dist/types/components/lightbox/composables/useLightboxFrames.d.ts +4 -1
  27. package/dist/types/components/lightbox/composables/useLightboxFrames.d.ts.map +1 -1
  28. package/dist/types/components/lightbox/composables/useLightboxLarge.d.ts +1 -0
  29. package/dist/types/components/lightbox/composables/useLightboxLarge.d.ts.map +1 -1
  30. package/dist/types/components/lightbox/composables/useLightboxLayout.d.ts +17 -0
  31. package/dist/types/components/lightbox/composables/useLightboxLayout.d.ts.map +1 -0
  32. package/dist/types/components/lightbox/composables/useLightboxPhase.d.ts +1 -1
  33. package/dist/types/components/lightbox/composables/useLightboxPhase.d.ts.map +1 -1
  34. package/dist/types/components/lightbox/composables/useLightboxPose.d.ts.map +1 -1
  35. package/dist/types/components/lightbox/composables/useLightboxRotation.d.ts +5 -1
  36. package/dist/types/components/lightbox/composables/useLightboxRotation.d.ts.map +1 -1
  37. package/dist/types/components/lightbox/composables/useLightboxZoom.d.ts +10 -1
  38. package/dist/types/components/lightbox/composables/useLightboxZoom.d.ts.map +1 -1
  39. package/dist/types/components/lightbox/lightbox.variants.d.ts +2 -0
  40. package/dist/types/components/lightbox/lightbox.variants.d.ts.map +1 -0
  41. package/dist/types/components/lightbox/types.d.ts +7 -1
  42. package/dist/types/components/lightbox/types.d.ts.map +1 -1
  43. package/dist/types/components/lightbox/utils/pose.d.ts +2 -2
  44. package/dist/types/components/lightbox/utils/pose.d.ts.map +1 -1
  45. package/dist/types/components/lightbox/utils/source.d.ts +4 -0
  46. package/dist/types/components/lightbox/utils/source.d.ts.map +1 -0
  47. package/dist/types/components/lightbox/utils/zoom.d.ts +8 -4
  48. package/dist/types/components/lightbox/utils/zoom.d.ts.map +1 -1
  49. package/dist/types/components/pagination/Pagination.vue.d.ts +59 -0
  50. package/dist/types/components/pagination/Pagination.vue.d.ts.map +1 -0
  51. package/dist/types/components/pagination/PaginationContent.vue.d.ts +23 -0
  52. package/dist/types/components/pagination/PaginationContent.vue.d.ts.map +1 -0
  53. package/dist/types/components/pagination/PaginationControl.vue.d.ts +10 -0
  54. package/dist/types/components/pagination/PaginationControl.vue.d.ts.map +1 -0
  55. package/dist/types/components/pagination/PaginationEllipsis.vue.d.ts +23 -0
  56. package/dist/types/components/pagination/PaginationEllipsis.vue.d.ts.map +1 -0
  57. package/dist/types/components/pagination/PaginationInfo.vue.d.ts +24 -0
  58. package/dist/types/components/pagination/PaginationInfo.vue.d.ts.map +1 -0
  59. package/dist/types/components/pagination/PaginationJump.vue.d.ts +7 -0
  60. package/dist/types/components/pagination/PaginationJump.vue.d.ts.map +1 -0
  61. package/dist/types/components/pagination/PaginationPage.vue.d.ts +20 -0
  62. package/dist/types/components/pagination/PaginationPage.vue.d.ts.map +1 -0
  63. package/dist/types/components/pagination/PaginationPages.vue.d.ts +27 -0
  64. package/dist/types/components/pagination/PaginationPages.vue.d.ts.map +1 -0
  65. package/dist/types/components/pagination/PaginationPopup.vue.d.ts +8 -0
  66. package/dist/types/components/pagination/PaginationPopup.vue.d.ts.map +1 -0
  67. package/dist/types/components/pagination/PaginationSize.vue.d.ts +7 -0
  68. package/dist/types/components/pagination/PaginationSize.vue.d.ts.map +1 -0
  69. package/dist/types/components/pagination/composables/usePagination.d.ts +25 -0
  70. package/dist/types/components/pagination/composables/usePagination.d.ts.map +1 -0
  71. package/dist/types/components/pagination/composables/usePaginationEllipsis.d.ts +34 -0
  72. package/dist/types/components/pagination/composables/usePaginationEllipsis.d.ts.map +1 -0
  73. package/dist/types/components/pagination/composables/usePaginationJump.d.ts +8 -0
  74. package/dist/types/components/pagination/composables/usePaginationJump.d.ts.map +1 -0
  75. package/dist/types/components/pagination/composables/usePaginationPageTooltip.d.ts +3 -0
  76. package/dist/types/components/pagination/composables/usePaginationPageTooltip.d.ts.map +1 -0
  77. package/dist/types/components/pagination/composables/usePaginationWindow.d.ts +188 -0
  78. package/dist/types/components/pagination/composables/usePaginationWindow.d.ts.map +1 -0
  79. package/dist/types/components/pagination/context.d.ts +19 -0
  80. package/dist/types/components/pagination/context.d.ts.map +1 -0
  81. package/dist/types/components/pagination/pagination.variants.d.ts +68 -0
  82. package/dist/types/components/pagination/pagination.variants.d.ts.map +1 -0
  83. package/dist/types/components/pagination/types.d.ts +23 -0
  84. package/dist/types/components/pagination/types.d.ts.map +1 -0
  85. package/dist/types/components/pagination/utils/ranges.d.ts +3 -0
  86. package/dist/types/components/pagination/utils/ranges.d.ts.map +1 -0
  87. package/dist/types/components/popover/Popover.vue.d.ts +4 -1
  88. package/dist/types/components/popover/Popover.vue.d.ts.map +1 -1
  89. package/dist/types/components/range-slider/RangeSlider.vue.d.ts +1 -0
  90. package/dist/types/components/range-slider/RangeSlider.vue.d.ts.map +1 -1
  91. package/dist/types/components/sheet/Sheet.vue.d.ts +21 -14
  92. package/dist/types/components/sheet/Sheet.vue.d.ts.map +1 -1
  93. package/dist/types/components/sheet/sheet.variants.d.ts +54 -3
  94. package/dist/types/components/sheet/sheet.variants.d.ts.map +1 -1
  95. package/dist/types/components/slider/Slider.vue.d.ts +1 -0
  96. package/dist/types/components/slider/Slider.vue.d.ts.map +1 -1
  97. package/dist/types/components/slider/slider.variants.d.ts.map +1 -1
  98. package/dist/types/components/tooltip/Tooltip.vue.d.ts +4 -15
  99. package/dist/types/components/tooltip/Tooltip.vue.d.ts.map +1 -1
  100. package/dist/types/components/tooltip/TooltipBubble.vue.d.ts +20 -0
  101. package/dist/types/components/tooltip/TooltipBubble.vue.d.ts.map +1 -0
  102. package/dist/types/components/tooltip/TooltipDirectiveRoot.d.ts +33 -0
  103. package/dist/types/components/tooltip/TooltipDirectiveRoot.d.ts.map +1 -0
  104. package/dist/types/components/tooltip/composables/useTooltipTarget.d.ts +2 -0
  105. package/dist/types/components/tooltip/composables/useTooltipTarget.d.ts.map +1 -0
  106. package/dist/types/components/tooltip/directive.d.ts +4 -0
  107. package/dist/types/components/tooltip/directive.d.ts.map +1 -0
  108. package/dist/types/components/tooltip/types.d.ts +14 -0
  109. package/dist/types/components/tooltip/types.d.ts.map +1 -0
  110. package/dist/types/components/tooltip/utils/directiveContext.d.ts +10 -0
  111. package/dist/types/components/tooltip/utils/directiveContext.d.ts.map +1 -0
  112. package/dist/types/components/tree-select/TreeSelect.vue.d.ts +6 -1
  113. package/dist/types/components/tree-select/TreeSelect.vue.d.ts.map +1 -1
  114. package/dist/types/components/tree-select/TreeSelectSearch.vue.d.ts +22 -0
  115. package/dist/types/components/tree-select/TreeSelectSearch.vue.d.ts.map +1 -0
  116. package/dist/types/components/tree-select/composables/useTreeSelect.d.ts +35 -0
  117. package/dist/types/components/tree-select/composables/useTreeSelect.d.ts.map +1 -0
  118. package/dist/types/components/tree-select/tree-select.variants.d.ts +1 -1
  119. package/dist/types/components/tree-select/tree-select.variants.d.ts.map +1 -1
  120. package/dist/types/components/tree-select/utils/filterTree.d.ts +7 -0
  121. package/dist/types/components/tree-select/utils/filterTree.d.ts.map +1 -0
  122. package/dist/types/index.d.ts +10 -1
  123. package/dist/types/index.d.ts.map +1 -1
  124. package/dist/types/lib/anchored-overlay.d.ts +12 -2
  125. package/dist/types/lib/anchored-overlay.d.ts.map +1 -1
  126. package/dist/types/lib/overlay-anchor.d.ts +16 -0
  127. package/dist/types/lib/overlay-anchor.d.ts.map +1 -0
  128. package/dist/types/lib/overlay-positioner.d.ts +3 -0
  129. package/dist/types/lib/overlay-positioner.d.ts.map +1 -0
  130. package/dist/types/lib/pointer-corridor.d.ts +7 -0
  131. package/dist/types/lib/pointer-corridor.d.ts.map +1 -0
  132. package/dist/types/lib/scroll-viewport.d.ts +9 -0
  133. package/dist/types/lib/scroll-viewport.d.ts.map +1 -0
  134. package/dist/types/lib/useDirection.d.ts +7 -0
  135. package/dist/types/lib/useDirection.d.ts.map +1 -0
  136. package/dist/types/locale/en-US.d.ts.map +1 -1
  137. package/dist/types/locale/index.d.ts.map +1 -1
  138. package/dist/types/locale/types.d.ts +13 -0
  139. package/dist/types/locale/types.d.ts.map +1 -1
  140. package/dist/types/locale/zh-CN.d.ts.map +1 -1
  141. package/package.json +1 -1
  142. package/src/components/badge/badge.variants.ts +1 -1
  143. package/src/components/dialog/Dialog.vue +5 -1
  144. package/src/components/drawer/Drawer.vue +69 -32
  145. package/src/components/drawer/drawer.variants.ts +6 -2
  146. package/src/components/dropdown-menu/DropdownMenu.vue +18 -5
  147. package/src/components/hover-card/HoverCard.vue +39 -16
  148. package/src/components/hover-card/HoverCardAnchor.vue +18 -0
  149. package/src/components/hover-card/composables/useHoverCardAnchor.ts +151 -0
  150. package/src/components/image/Image.vue +2 -0
  151. package/src/components/image/composables/useImageGroupState.ts +9 -1
  152. package/src/components/image/image.variants.ts +1 -1
  153. package/src/components/lightbox/Lightbox.vue +23 -17
  154. package/src/components/lightbox/LightboxChrome.vue +7 -1
  155. package/src/components/lightbox/LightboxToolbar.vue +16 -2
  156. package/src/components/lightbox/composables/useLightboxFrames.ts +108 -12
  157. package/src/components/lightbox/composables/useLightboxLarge.ts +4 -1
  158. package/src/components/lightbox/composables/useLightboxLayout.ts +44 -0
  159. package/src/components/lightbox/composables/useLightboxPhase.ts +12 -4
  160. package/src/components/lightbox/composables/useLightboxPose.ts +17 -19
  161. package/src/components/lightbox/composables/useLightboxRotation.ts +14 -4
  162. package/src/components/lightbox/composables/useLightboxZoom.ts +51 -9
  163. package/src/components/lightbox/lightbox.variants.ts +5 -0
  164. package/src/components/lightbox/types.ts +5 -1
  165. package/src/components/lightbox/utils/pose.ts +13 -6
  166. package/src/components/lightbox/utils/source.ts +17 -0
  167. package/src/components/lightbox/utils/zoom.ts +21 -7
  168. package/src/components/pagination/Pagination.vue +118 -0
  169. package/src/components/pagination/PaginationContent.vue +29 -0
  170. package/src/components/pagination/PaginationControl.vue +42 -0
  171. package/src/components/pagination/PaginationEllipsis.vue +67 -0
  172. package/src/components/pagination/PaginationInfo.vue +18 -0
  173. package/src/components/pagination/PaginationJump.vue +32 -0
  174. package/src/components/pagination/PaginationPage.vue +34 -0
  175. package/src/components/pagination/PaginationPages.vue +43 -0
  176. package/src/components/pagination/PaginationPopup.vue +92 -0
  177. package/src/components/pagination/PaginationSize.vue +30 -0
  178. package/src/components/pagination/composables/usePagination.ts +76 -0
  179. package/src/components/pagination/composables/usePaginationEllipsis.ts +269 -0
  180. package/src/components/pagination/composables/usePaginationJump.ts +23 -0
  181. package/src/components/pagination/composables/usePaginationPageTooltip.ts +23 -0
  182. package/src/components/pagination/composables/usePaginationWindow.ts +116 -0
  183. package/src/components/pagination/context.ts +23 -0
  184. package/src/components/pagination/pagination.variants.ts +32 -0
  185. package/src/components/pagination/types.ts +19 -0
  186. package/src/components/pagination/utils/ranges.ts +12 -0
  187. package/src/components/popover/Popover.vue +17 -7
  188. package/src/components/range-slider/RangeSlider.vue +6 -0
  189. package/src/components/sheet/Sheet.vue +67 -24
  190. package/src/components/sheet/sheet.variants.ts +17 -4
  191. package/src/components/slider/Slider.vue +6 -0
  192. package/src/components/slider/slider.variants.ts +1 -0
  193. package/src/components/tooltip/Tooltip.vue +10 -33
  194. package/src/components/tooltip/TooltipBubble.vue +36 -0
  195. package/src/components/tooltip/TooltipDirectiveRoot.ts +61 -0
  196. package/src/components/tooltip/composables/useTooltipTarget.ts +56 -0
  197. package/src/components/tooltip/directive.ts +54 -0
  198. package/src/components/tooltip/types.ts +15 -0
  199. package/src/components/tooltip/utils/directiveContext.ts +32 -0
  200. package/src/components/tree-select/TreeSelect.vue +90 -61
  201. package/src/components/tree-select/TreeSelectSearch.vue +48 -0
  202. package/src/components/tree-select/composables/useTreeSelect.ts +130 -0
  203. package/src/components/tree-select/tree-select.variants.ts +2 -2
  204. package/src/components/tree-select/utils/filterTree.ts +19 -0
  205. package/src/index.ts +10 -1
  206. package/src/lib/anchored-overlay.ts +28 -19
  207. package/src/lib/overlay-anchor.ts +65 -0
  208. package/src/lib/overlay-positioner.ts +25 -0
  209. package/src/lib/pointer-corridor.ts +26 -0
  210. package/src/lib/scroll-viewport.ts +8 -0
  211. package/src/lib/useDirection.ts +31 -0
  212. package/src/locale/en-US.ts +15 -1
  213. package/src/locale/index.ts +1 -0
  214. package/src/locale/types.ts +13 -0
  215. package/src/locale/zh-CN.ts +15 -1
@@ -1,12 +1,16 @@
1
1
  <script setup lang="ts">
2
+ import type { OverlayAnchor, OverlayPositionStrategy } from '../../lib/overlay-anchor'
2
3
  import { HoverCardContent, HoverCardPortal, HoverCardRoot, HoverCardTrigger } from 'reka-ui'
3
4
  import { cn } from '../../lib/cn'
4
5
  import Card from '../card/Card.vue'
6
+ import HoverCardAnchor from './HoverCardAnchor.vue'
5
7
 
6
8
  defineOptions({ name: 'HnHoverCard' })
7
9
 
8
10
  const props = withDefaults(
9
11
  defineProps<{
12
+ anchor?: OverlayAnchor | null
13
+ updatePositionStrategy?: OverlayPositionStrategy
10
14
  side?: 'top' | 'right' | 'bottom' | 'left'
11
15
  align?: 'start' | 'center' | 'end'
12
16
  sideOffset?: number
@@ -14,8 +18,10 @@
14
18
  closeDelay?: number
15
19
  padded?: boolean
16
20
  class?: string
21
+ positionerClass?: string
17
22
  }>(),
18
23
  {
24
+ updatePositionStrategy: 'optimized',
19
25
  side: 'bottom',
20
26
  align: 'center',
21
27
  sideOffset: 8,
@@ -25,29 +31,46 @@
25
31
  },
26
32
  )
27
33
 
34
+ defineSlots<{ default?(): unknown; content?(): unknown }>()
35
+
28
36
  const open = defineModel<boolean>('open')
29
37
  </script>
30
38
 
31
39
  <template>
32
40
  <HoverCardRoot v-model:open="open" :open-delay="props.openDelay" :close-delay="props.closeDelay">
33
- <HoverCardTrigger as-child>
41
+ <HoverCardTrigger v-if="$slots.default" as-child>
34
42
  <slot />
35
43
  </HoverCardTrigger>
36
- <HoverCardPortal>
37
- <HoverCardContent
38
- as-child
39
- :side="props.side"
40
- :align="props.align"
41
- :side-offset="props.sideOffset"
42
- >
43
- <Card
44
- data-hn-hover-card
45
- :padded="props.padded"
46
- :class="cn('hn-anim-pop z-(--hn-z-overlay) max-w-sm shadow-md outline-none', props.class)"
44
+ <HoverCardAnchor
45
+ v-slot="{ reference, contentRef, present }"
46
+ :anchor="props.anchor"
47
+ :update-position-strategy="props.updatePositionStrategy"
48
+ :external="!$slots.default"
49
+ :close-delay="props.closeDelay"
50
+ :positioner-class="props.positionerClass"
51
+ >
52
+ <HoverCardPortal v-if="present">
53
+ <HoverCardContent
54
+ :reference="reference"
55
+ :update-position-strategy="props.updatePositionStrategy"
56
+ as-child
57
+ :side="props.side"
58
+ :align="props.align"
59
+ :side-offset="props.sideOffset"
47
60
  >
48
- <slot name="content" />
49
- </Card>
50
- </HoverCardContent>
51
- </HoverCardPortal>
61
+ <Card
62
+ :ref="contentRef"
63
+ data-hn-hover-card
64
+ :inert="!open"
65
+ :padded="props.padded"
66
+ :class="
67
+ cn('hn-anim-pop z-(--hn-z-overlay) max-w-sm shadow-md outline-none', props.class)
68
+ "
69
+ >
70
+ <slot name="content" />
71
+ </Card>
72
+ </HoverCardContent>
73
+ </HoverCardPortal>
74
+ </HoverCardAnchor>
52
75
  </HoverCardRoot>
53
76
  </template>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ import type { OverlayAnchor, OverlayPositionStrategy } from '../../lib/overlay-anchor'
3
+ import { useHoverCardAnchor } from './composables/useHoverCardAnchor'
4
+
5
+ defineOptions({ name: 'HnHoverCardAnchor' })
6
+ const props = defineProps<{
7
+ anchor?: OverlayAnchor | null
8
+ updatePositionStrategy: OverlayPositionStrategy
9
+ external: boolean
10
+ closeDelay: number
11
+ positionerClass?: string
12
+ }>()
13
+ const { reference, contentRef, present } = useHoverCardAnchor(props)
14
+ </script>
15
+
16
+ <template>
17
+ <slot :reference="reference" :content-ref="contentRef" :present="present" />
18
+ </template>
@@ -0,0 +1,151 @@
1
+ import { computed, onScopeDispose, watch, type ComponentPublicInstance } from 'vue'
2
+ import { useEventListener, useRafFn } from '@vueuse/core'
3
+ import { injectHoverCardRootContext } from 'reka-ui'
4
+ import {
5
+ overlayAnchorElement,
6
+ useOverlayAnchor,
7
+ type OverlayAnchor,
8
+ type OverlayPositionStrategy,
9
+ } from '../../../lib/overlay-anchor'
10
+ import { useOverlayPortal } from '../../../lib/overlay-portal'
11
+ import { useOverlayPositionerClass } from '../../../lib/overlay-positioner'
12
+ import { inPointerCorridor } from '../../../lib/pointer-corridor'
13
+
14
+ interface HoverCardAnchorOptions {
15
+ anchor?: OverlayAnchor | null
16
+ updatePositionStrategy: OverlayPositionStrategy
17
+ external: boolean
18
+ closeDelay: number
19
+ positionerClass?: string
20
+ }
21
+
22
+ export function useHoverCardAnchor(props: HoverCardAnchorOptions) {
23
+ const root = injectHoverCardRootContext()
24
+ const original = { onOpen: root.onOpen, onClose: root.onClose, onDismiss: root.onDismiss }
25
+ const anchor = computed(() => (props.external ? (props.anchor ?? undefined) : undefined))
26
+ const element = computed(() => overlayAnchorElement(anchor.value))
27
+ const virtual = computed(() => !!anchor.value && !element.value)
28
+ const { content, present } = useOverlayPortal(root.open)
29
+ useOverlayPositionerClass(content, () => props.positionerClass)
30
+ const panel = computed(() =>
31
+ props.external ? (content.value?.$el as HTMLElement | undefined) : undefined,
32
+ )
33
+ const reference = useOverlayAnchor(
34
+ () => (props.external ? anchor.value : root.triggerElement.value),
35
+ root.open,
36
+ present,
37
+ () => props.updatePositionStrategy,
38
+ )
39
+ let timer: ReturnType<typeof setTimeout> | undefined
40
+ let tracking = false
41
+
42
+ function cancel() {
43
+ clearTimeout(timer)
44
+ timer = undefined
45
+ }
46
+
47
+ function dismiss() {
48
+ cancel()
49
+ tracking = false
50
+ root.onOpenChange(false)
51
+ }
52
+
53
+ function close() {
54
+ if (
55
+ virtual.value ||
56
+ timer !== undefined ||
57
+ !root.open.value ||
58
+ root.hasSelectionRef.value ||
59
+ root.isPointerDownOnContentRef.value
60
+ )
61
+ return
62
+ timer = setTimeout(dismiss, props.closeDelay)
63
+ }
64
+
65
+ function keep() {
66
+ tracking = false
67
+ cancel()
68
+ }
69
+
70
+ function track(event: Event) {
71
+ const pointer = event as PointerEvent
72
+ if (!root.open.value || !tracking || pointer.pointerType === 'touch') return
73
+ const target = pointer.target as Node | null
74
+ if (target && (element.value?.contains(target) || panel.value?.contains(target))) return keep()
75
+ const a = element.value?.getBoundingClientRect()
76
+ const b = panel.value?.getBoundingClientRect()
77
+ if (a && b && inPointerCorridor({ x: pointer.clientX, y: pointer.clientY }, a, b)) cancel()
78
+ else close()
79
+ }
80
+
81
+ function leave(event: Event) {
82
+ const pointer = event as PointerEvent
83
+ if (virtual.value || pointer.pointerType === 'touch' || !root.open.value) return
84
+ tracking = true
85
+ const a = element.value?.getBoundingClientRect()
86
+ const b = panel.value?.getBoundingClientRect()
87
+ if (a && b && inPointerCorridor({ x: pointer.clientX, y: pointer.clientY }, a, b)) cancel()
88
+ else close()
89
+ }
90
+
91
+ function measure() {
92
+ if (!anchor.value || (element.value && !element.value.isConnected)) {
93
+ if (root.open.value) dismiss()
94
+ return
95
+ }
96
+ reference.value?.getBoundingClientRect()
97
+ }
98
+
99
+ const { pause, resume } = useRafFn(measure, { immediate: false })
100
+ root.onOpen = () => (props.external ? keep() : original.onOpen())
101
+ root.onClose = () => (props.external ? close() : original.onClose())
102
+ root.onDismiss = () => (props.external ? dismiss() : original.onDismiss())
103
+
104
+ watch(
105
+ [anchor, root.open, present],
106
+ ([element, , mounted], previous) => {
107
+ cancel()
108
+ tracking = false
109
+ pause()
110
+ if (!props.external || !mounted) return
111
+ if (element !== previous?.[0]) {
112
+ root.hasSelectionRef.value = false
113
+ root.isPointerDownOnContentRef.value = false
114
+ }
115
+ measure()
116
+ if (!virtual.value) resume()
117
+ },
118
+ { immediate: true, flush: 'post' },
119
+ )
120
+
121
+ useEventListener(element, 'pointerenter', keep)
122
+ useEventListener(element, 'pointerleave', leave)
123
+ useEventListener(element, 'focusin', keep)
124
+ useEventListener(element, 'focusout', event => {
125
+ const target = (event as FocusEvent).relatedTarget as Node | null
126
+ if (!target || (!element.value?.contains(target) && !panel.value?.contains(target))) close()
127
+ })
128
+ useEventListener(panel, 'pointerenter', keep)
129
+ useEventListener(panel, 'pointerleave', leave)
130
+ useEventListener(() => element.value?.ownerDocument, 'pointermove', track)
131
+ useEventListener(
132
+ () => element.value?.ownerDocument,
133
+ 'scroll',
134
+ event => {
135
+ if (root.open.value && (event.target as Node | null)?.contains(element.value ?? null))
136
+ dismiss()
137
+ },
138
+ { capture: true, passive: true },
139
+ )
140
+
141
+ onScopeDispose(() => {
142
+ cancel()
143
+ Object.assign(root, original)
144
+ })
145
+
146
+ function contentRef(value: Element | ComponentPublicInstance | null) {
147
+ content.value = value as ComponentPublicInstance | null
148
+ }
149
+
150
+ return { reference, contentRef, present }
151
+ }
@@ -25,6 +25,7 @@
25
25
  skeleton?: boolean
26
26
  eager?: boolean
27
27
  preview?: boolean | string
28
+ previewSize?: LightboxItem['previewSize']
28
29
  draggable?: boolean
29
30
  class?: string
30
31
  style?: StyleValue
@@ -64,6 +65,7 @@
64
65
  typeof props.preview === 'string' ? props.preview : (props.src ?? ''),
65
66
  'preview',
66
67
  ),
68
+ previewSize: props.previewSize,
67
69
  alt: props.alt,
68
70
  fit: props.fit,
69
71
  source: () => imageEl.value,
@@ -10,7 +10,15 @@ export function useImageGroupState() {
10
10
  function byDom(a: LightboxItem, b: LightboxItem): number {
11
11
  const first = a.source?.()
12
12
  const second = b.source?.()
13
- if (!first || !second || first === second) return 0
13
+ if (
14
+ !first ||
15
+ !second ||
16
+ first === second ||
17
+ !('compareDocumentPosition' in first) ||
18
+ !('compareDocumentPosition' in second)
19
+ ) {
20
+ return 0
21
+ }
14
22
  return first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
15
23
  }
16
24
 
@@ -1,7 +1,7 @@
1
1
  import { tv, type VariantProps } from '../../lib/tv'
2
2
 
3
3
  export const imageRoot = tv({
4
- base: 'relative block w-full overflow-hidden',
4
+ base: 'relative isolate block w-full overflow-hidden',
5
5
  variants: {
6
6
  preview: {
7
7
  true: 'hn-focus-ring cursor-zoom-in',
@@ -2,7 +2,6 @@
2
2
  import { computed, onMounted, shallowRef, watch } from 'vue'
3
3
  import { DialogRoot, DialogPortal, DialogContent, DialogTitle, useBodyScrollLock } from 'reka-ui'
4
4
  import { Motion } from 'motion-v'
5
- import { useEventListener } from '@vueuse/core'
6
5
  import { cn } from '../../lib/cn'
7
6
  import VisuallyHidden from '../visually-hidden/VisuallyHidden.vue'
8
7
  import LightboxStrip from './LightboxStrip.vue'
@@ -17,6 +16,8 @@
17
16
  import { useLightboxPhase } from './composables/useLightboxPhase'
18
17
  import { useLightboxHint } from './composables/useLightboxHint'
19
18
  import { useLightboxSlides } from './composables/useLightboxSlides'
19
+ import { lightboxStage } from './lightbox.variants'
20
+ import { useLightboxLayout } from './composables/useLightboxLayout'
20
21
  import { useLightboxRotation } from './composables/useLightboxRotation'
21
22
  import { clampIndex } from './utils/paging'
22
23
  import { saveImage } from './utils/download'
@@ -57,9 +58,10 @@
57
58
  loop: () => !!props.loop,
58
59
  })
59
60
 
60
- function measure() {
61
+ function measure(force = false) {
62
+ const width = frames.stage.value.width
61
63
  frames.measure(stageEl.value)
62
- paging.sync()
64
+ if (force || width !== frames.stage.value.width) paging.sync()
63
65
  }
64
66
 
65
67
  const phase = useLightboxPhase({
@@ -70,9 +72,9 @@
70
72
  pose: pose.fromSource,
71
73
  prepare: () => {
72
74
  rotation.clear()
73
- frames.read()
75
+ return frames.read(() => current.value)
74
76
  },
75
- layout: measure,
77
+ layout: () => measure(true),
76
78
  })
77
79
 
78
80
  const close = phase.close
@@ -94,6 +96,16 @@
94
96
  () => mounted.value,
95
97
  )
96
98
 
99
+ const layout = useLightboxLayout({
100
+ stage: () => stageEl.value,
101
+ current: () => current.value,
102
+ frames,
103
+ large,
104
+ phase,
105
+ zoom,
106
+ measure,
107
+ })
108
+
97
109
  const hint = useLightboxHint({ motion, frames, paging, current: () => current.value })
98
110
 
99
111
  const looping = computed(() => !!props.loop && count.value > 1)
@@ -137,10 +149,6 @@
137
149
  if (open.value) void phase.show()
138
150
  })
139
151
 
140
- useEventListener('resize', () => {
141
- if (mounted.value) measure()
142
- })
143
-
144
152
  function onEscape(event: Event) {
145
153
  event.preventDefault()
146
154
  void close()
@@ -173,7 +181,7 @@
173
181
  const currentClass = computed(() =>
174
182
  cn(
175
183
  'absolute will-change-transform',
176
- !zoom.zoomed.value && 'cursor-zoom-in',
184
+ !zoom.zoomed.value && (zoom.canToggle.value ? 'cursor-zoom-in' : 'cursor-default'),
177
185
  zoom.zoomed.value && (gesture.mode.value === 'pan' ? 'cursor-grabbing' : 'cursor-grab'),
178
186
  ),
179
187
  )
@@ -191,12 +199,7 @@
191
199
  <div
192
200
  ref="stageEl"
193
201
  :data-hn-phase="phase.current()"
194
- :class="
195
- cn(
196
- 'dark fixed inset-0 z-(--hn-z-overlay) touch-none outline-none select-none',
197
- props.class,
198
- )
199
- "
202
+ :class="cn(lightboxStage(), props.class)"
200
203
  @pointerdown="gesture.onPointerdown"
201
204
  @pointermove="gesture.onPointermove"
202
205
  @pointerup="gesture.onPointerup"
@@ -220,7 +223,7 @@
220
223
  :transition="motion.transition('base')"
221
224
  :large-src="large.src.value"
222
225
  :large-ready="large.ready.value"
223
- @learn="frames.learn"
226
+ @learn="layout.learn"
224
227
  />
225
228
  <DialogTitle as-child>
226
229
  <VisuallyHidden>{{ current?.alt }}</VisuallyHidden>
@@ -229,6 +232,8 @@
229
232
  :items="props.items"
230
233
  :index="index"
231
234
  :zoomed="zoom.zoomed.value"
235
+ :can-zoom-in="zoom.canZoomIn.value"
236
+ :at-original="zoom.atOriginal.value"
232
237
  :waiting="large.waiting.value"
233
238
  :loop="looping"
234
239
  :hint="hint"
@@ -239,6 +244,7 @@
239
244
  @zoom-in="zoom.step(1)"
240
245
  @zoom-out="zoom.step(-1)"
241
246
  @reset="zoom.reset()"
247
+ @original="zoom.original()"
242
248
  @rotate="rotate"
243
249
  @download="download"
244
250
  />
@@ -16,6 +16,8 @@
16
16
  items: LightboxItem[]
17
17
  index: number
18
18
  zoomed: boolean
19
+ canZoomIn: boolean
20
+ atOriginal: boolean
19
21
  waiting: boolean
20
22
  loop: boolean
21
23
  presence: MotionValue<number>
@@ -29,6 +31,7 @@
29
31
  zoomIn: []
30
32
  zoomOut: []
31
33
  reset: []
34
+ original: []
32
35
  rotate: []
33
36
  download: []
34
37
  }>()
@@ -88,7 +91,7 @@
88
91
  <ChevronRight />
89
92
  </IconButton>
90
93
  </Motion>
91
- <Motion class="absolute end-4 top-4" :style="{ opacity: props.presence }">
94
+ <Motion data-hn-close class="absolute end-4 top-4" :style="{ opacity: props.presence }">
92
95
  <DialogClose as-child>
93
96
  <CloseButton size="md" />
94
97
  </DialogClose>
@@ -102,10 +105,13 @@
102
105
  :items="props.items"
103
106
  :index="props.index"
104
107
  :zoomed="props.zoomed"
108
+ :can-zoom-in="props.canZoomIn"
109
+ :at-original="props.atOriginal"
105
110
  @select="emit('select', $event)"
106
111
  @zoom-in="emit('zoomIn')"
107
112
  @zoom-out="emit('zoomOut')"
108
113
  @reset="emit('reset')"
114
+ @original="emit('original')"
109
115
  @rotate="emit('rotate')"
110
116
  @download="emit('download')"
111
117
  />
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { Download, RotateCw, Shrink, ZoomIn, ZoomOut } from '@lucide/vue'
2
+ import { Download, RotateCw, Scan, Shrink, ZoomIn, ZoomOut } from '@lucide/vue'
3
3
  import { useUiLocale } from '../../locale'
4
4
  import IconButton from '../icon-button/IconButton.vue'
5
5
  import LightboxThumbs from './LightboxThumbs.vue'
@@ -11,6 +11,8 @@
11
11
  items: LightboxItem[]
12
12
  index: number
13
13
  zoomed: boolean
14
+ canZoomIn: boolean
15
+ atOriginal: boolean
14
16
  }>()
15
17
 
16
18
  const emit = defineEmits<{
@@ -18,6 +20,7 @@
18
20
  zoomIn: []
19
21
  zoomOut: []
20
22
  reset: []
23
+ original: []
21
24
  rotate: []
22
25
  download: []
23
26
  }>()
@@ -26,7 +29,7 @@
26
29
  </script>
27
30
 
28
31
  <template>
29
- <div class="flex flex-col items-center gap-1 pb-3">
32
+ <div class="flex flex-col items-center gap-1 pb-[max(--spacing(3),env(safe-area-inset-bottom))]">
30
33
  <LightboxThumbs
31
34
  v-if="props.items.length > 1"
32
35
  :items="props.items"
@@ -36,6 +39,7 @@
36
39
  <div class="flex items-center gap-1">
37
40
  <IconButton
38
41
  :label="t.lightbox.zoomOut"
42
+ :disabled="!props.zoomed"
39
43
  variant="soft"
40
44
  pill
41
45
  class="hidden pointer-fine:inline-flex"
@@ -45,6 +49,7 @@
45
49
  </IconButton>
46
50
  <IconButton
47
51
  :label="t.lightbox.zoomIn"
52
+ :disabled="!props.canZoomIn"
48
53
  variant="soft"
49
54
  pill
50
55
  class="hidden pointer-fine:inline-flex"
@@ -52,6 +57,15 @@
52
57
  >
53
58
  <ZoomIn />
54
59
  </IconButton>
60
+ <IconButton
61
+ :label="t.lightbox.actualSize"
62
+ variant="soft"
63
+ pill
64
+ :disabled="props.atOriginal"
65
+ @click="emit('original')"
66
+ >
67
+ <Scan />
68
+ </IconButton>
55
69
  <IconButton
56
70
  :label="t.lightbox.resetZoom"
57
71
  variant="soft"
@@ -1,38 +1,134 @@
1
- import { reactive, shallowRef } from 'vue'
1
+ import { onScopeDispose, reactive, shallowRef } from 'vue'
2
2
  import type { LightboxItem } from '../types'
3
3
  import { fitRect, type Rect } from '../utils/pose'
4
4
  import type { Size } from '../utils/zoom'
5
5
 
6
+ function previewSizeOf(item: LightboxItem | undefined): Size | undefined {
7
+ const size = item?.previewSize
8
+ if (
9
+ size &&
10
+ Number.isFinite(size.width) &&
11
+ size.width > 0 &&
12
+ Number.isFinite(size.height) &&
13
+ size.height > 0
14
+ )
15
+ return size
16
+ }
17
+
6
18
  export function useLightboxFrames(items: () => LightboxItem[]) {
7
19
  const stage = shallowRef<Size>({ width: 0, height: 0 })
8
- const naturals = reactive(new Map<string, Size>())
20
+ const naturals = reactive(new Map<string, Size & { src: string }>())
21
+ const previews = reactive(new Map<string, Size & { src: string; preview: string }>())
22
+ const area = shallowRef<Rect>({ x: 0, y: 0, width: 0, height: 0 })
23
+ let loader: HTMLImageElement | undefined
9
24
 
10
25
  function measure(el: HTMLElement | undefined) {
11
- stage.value = { width: el?.clientWidth ?? 0, height: el?.clientHeight ?? 0 }
26
+ if (!el) return
27
+ const width = el.clientWidth
28
+ const height = el.clientHeight
29
+ const style = getComputedStyle(el)
30
+ const rect = el.getBoundingClientRect()
31
+ const chrome = el.querySelector('[data-hn-chrome]')?.getBoundingClientRect()
32
+ const close = el.querySelector('[data-hn-close]')?.getBoundingClientRect()
33
+ const x = Math.min(
34
+ width / 2,
35
+ Math.max(parseFloat(style.paddingLeft), parseFloat(style.paddingRight)),
36
+ )
37
+ const y = Math.min(
38
+ height / 2,
39
+ Math.max(
40
+ parseFloat(style.paddingTop) + (close ? close.bottom - rect.top : 0),
41
+ parseFloat(style.paddingBottom) + (chrome ? rect.bottom - chrome.top : 0),
42
+ ),
43
+ )
44
+ stage.value = { width, height }
45
+ area.value = { x, y, width: width - x * 2, height: height - y * 2 }
12
46
  }
13
47
 
14
- function read() {
48
+ function read(current?: () => LightboxItem | undefined): Promise<void> | undefined {
49
+ loader?.removeAttribute('src')
50
+ loader = undefined
15
51
  for (const item of items()) {
16
52
  const el = item.source?.()
17
- if (el?.naturalWidth && el.naturalHeight) {
18
- naturals.set(item.id, { width: el.naturalWidth, height: el.naturalHeight })
53
+ if (
54
+ el &&
55
+ 'naturalWidth' in el &&
56
+ el.naturalWidth &&
57
+ el.naturalHeight &&
58
+ !el.srcset &&
59
+ (el.currentSrc || el.src) === new URL(item.src, el.ownerDocument.baseURI).href
60
+ ) {
61
+ naturals.set(item.id, { src: item.src, width: el.naturalWidth, height: el.naturalHeight })
19
62
  }
20
63
  }
64
+ const item = current?.()
65
+ if (!item || previewSizeOf(item) || naturalOf(item)) return
66
+ const img = new Image()
67
+ loader = img
68
+ const { id, src } = item
69
+ img.src = src
70
+ return img
71
+ .decode()
72
+ .catch(() => {})
73
+ .then(() => {
74
+ if (loader !== img) return
75
+ learn(id, img)
76
+ loader = undefined
77
+ const latest = current?.()
78
+ if (latest?.id !== id || latest?.src !== src) return read(current)
79
+ })
21
80
  }
22
81
 
23
82
  function learn(id: string, img: HTMLImageElement) {
24
- if (naturals.has(id) || !img.naturalWidth || !img.naturalHeight) return
25
- naturals.set(id, { width: img.naturalWidth, height: img.naturalHeight })
83
+ const item = items().find(item => item.id === id)
84
+ if (
85
+ !item ||
86
+ naturalOf(item) ||
87
+ !img.naturalWidth ||
88
+ !img.naturalHeight ||
89
+ img.src !== new URL(item.src, img.ownerDocument.baseURI).href
90
+ ) {
91
+ return
92
+ }
93
+ naturals.set(id, { src: item.src, width: img.naturalWidth, height: img.naturalHeight })
26
94
  }
27
95
 
28
96
  function naturalOf(item: LightboxItem | undefined): Size | undefined {
29
- return item ? naturals.get(item.id) : undefined
97
+ if (!item) return
98
+ const natural = naturals.get(item.id)
99
+ return natural?.src === item.src ? natural : undefined
100
+ }
101
+
102
+ function learnPreview(item: LightboxItem | undefined, img: HTMLImageElement | undefined) {
103
+ if (!item?.preview || !img?.naturalWidth || !img.naturalHeight) return
104
+ if (img.src !== new URL(item.preview, img.ownerDocument.baseURI).href) return
105
+ const previous = previews.get(item.id)
106
+ if (previous?.src === item.src && previous.preview === item.preview) return
107
+ previews.set(item.id, {
108
+ src: item.src,
109
+ preview: item.preview,
110
+ width: img.naturalWidth,
111
+ height: img.naturalHeight,
112
+ })
113
+ }
114
+
115
+ function displayOf(item: LightboxItem | undefined): Size | undefined {
116
+ if (!item) return
117
+ const size = previewSizeOf(item)
118
+ if (size) return size
119
+ const preview = previews.get(item.id)
120
+ return preview?.src === item.src && preview.preview === item.preview ? preview : naturalOf(item)
30
121
  }
31
122
 
32
123
  function frameOf(item: LightboxItem | undefined): Rect {
33
- const natural = naturalOf(item)
34
- return natural ? fitRect(natural, stage.value) : { x: 0, y: 0, ...stage.value }
124
+ const natural = displayOf(item)
125
+ return natural ? fitRect(natural, area.value) : area.value
35
126
  }
36
127
 
37
- return { stage, measure, read, learn, naturalOf, frameOf }
128
+ onScopeDispose(() => {
129
+ loader?.removeAttribute('src')
130
+ loader = undefined
131
+ })
132
+
133
+ return { stage, area, measure, read, learn, learnPreview, naturalOf, displayOf, frameOf }
38
134
  }