@hina-ui/vue 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/dist/index.js +3990 -2016
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/components/hover-card/HoverCard.vue.d.ts +7 -7
  4. package/dist/types/components/hover-card/HoverCard.vue.d.ts.map +1 -1
  5. package/dist/types/components/hover-card/HoverCardAnchor.vue.d.ts +27 -0
  6. package/dist/types/components/hover-card/HoverCardAnchor.vue.d.ts.map +1 -0
  7. package/dist/types/components/hover-card/composables/useHoverCardAnchor.d.ts +17 -0
  8. package/dist/types/components/hover-card/composables/useHoverCardAnchor.d.ts.map +1 -0
  9. package/dist/types/components/image/Image.vue.d.ts +3 -1
  10. package/dist/types/components/image/Image.vue.d.ts.map +1 -1
  11. package/dist/types/components/image/composables/useImageGroupState.d.ts.map +1 -1
  12. package/dist/types/components/image/image.variants.d.ts +1 -1
  13. package/dist/types/components/lightbox/Lightbox.vue.d.ts.map +1 -1
  14. package/dist/types/components/lightbox/LightboxChrome.vue.d.ts +4 -0
  15. package/dist/types/components/lightbox/LightboxChrome.vue.d.ts.map +1 -1
  16. package/dist/types/components/lightbox/LightboxToolbar.vue.d.ts +4 -0
  17. package/dist/types/components/lightbox/LightboxToolbar.vue.d.ts.map +1 -1
  18. package/dist/types/components/lightbox/composables/useLightboxFrames.d.ts +4 -1
  19. package/dist/types/components/lightbox/composables/useLightboxFrames.d.ts.map +1 -1
  20. package/dist/types/components/lightbox/composables/useLightboxLarge.d.ts +1 -0
  21. package/dist/types/components/lightbox/composables/useLightboxLarge.d.ts.map +1 -1
  22. package/dist/types/components/lightbox/composables/useLightboxLayout.d.ts +17 -0
  23. package/dist/types/components/lightbox/composables/useLightboxLayout.d.ts.map +1 -0
  24. package/dist/types/components/lightbox/composables/useLightboxPhase.d.ts +1 -1
  25. package/dist/types/components/lightbox/composables/useLightboxPhase.d.ts.map +1 -1
  26. package/dist/types/components/lightbox/composables/useLightboxPose.d.ts.map +1 -1
  27. package/dist/types/components/lightbox/composables/useLightboxRotation.d.ts +5 -1
  28. package/dist/types/components/lightbox/composables/useLightboxRotation.d.ts.map +1 -1
  29. package/dist/types/components/lightbox/composables/useLightboxZoom.d.ts +10 -1
  30. package/dist/types/components/lightbox/composables/useLightboxZoom.d.ts.map +1 -1
  31. package/dist/types/components/lightbox/lightbox.variants.d.ts +2 -0
  32. package/dist/types/components/lightbox/lightbox.variants.d.ts.map +1 -0
  33. package/dist/types/components/lightbox/types.d.ts +7 -1
  34. package/dist/types/components/lightbox/types.d.ts.map +1 -1
  35. package/dist/types/components/lightbox/utils/pose.d.ts +2 -2
  36. package/dist/types/components/lightbox/utils/pose.d.ts.map +1 -1
  37. package/dist/types/components/lightbox/utils/source.d.ts +4 -0
  38. package/dist/types/components/lightbox/utils/source.d.ts.map +1 -0
  39. package/dist/types/components/lightbox/utils/zoom.d.ts +8 -4
  40. package/dist/types/components/lightbox/utils/zoom.d.ts.map +1 -1
  41. package/dist/types/components/pagination/Pagination.vue.d.ts +59 -0
  42. package/dist/types/components/pagination/Pagination.vue.d.ts.map +1 -0
  43. package/dist/types/components/pagination/PaginationContent.vue.d.ts +23 -0
  44. package/dist/types/components/pagination/PaginationContent.vue.d.ts.map +1 -0
  45. package/dist/types/components/pagination/PaginationControl.vue.d.ts +10 -0
  46. package/dist/types/components/pagination/PaginationControl.vue.d.ts.map +1 -0
  47. package/dist/types/components/pagination/PaginationEllipsis.vue.d.ts +23 -0
  48. package/dist/types/components/pagination/PaginationEllipsis.vue.d.ts.map +1 -0
  49. package/dist/types/components/pagination/PaginationInfo.vue.d.ts +24 -0
  50. package/dist/types/components/pagination/PaginationInfo.vue.d.ts.map +1 -0
  51. package/dist/types/components/pagination/PaginationJump.vue.d.ts +7 -0
  52. package/dist/types/components/pagination/PaginationJump.vue.d.ts.map +1 -0
  53. package/dist/types/components/pagination/PaginationPage.vue.d.ts +20 -0
  54. package/dist/types/components/pagination/PaginationPage.vue.d.ts.map +1 -0
  55. package/dist/types/components/pagination/PaginationPages.vue.d.ts +27 -0
  56. package/dist/types/components/pagination/PaginationPages.vue.d.ts.map +1 -0
  57. package/dist/types/components/pagination/PaginationPopup.vue.d.ts +8 -0
  58. package/dist/types/components/pagination/PaginationPopup.vue.d.ts.map +1 -0
  59. package/dist/types/components/pagination/PaginationSize.vue.d.ts +7 -0
  60. package/dist/types/components/pagination/PaginationSize.vue.d.ts.map +1 -0
  61. package/dist/types/components/pagination/composables/usePagination.d.ts +25 -0
  62. package/dist/types/components/pagination/composables/usePagination.d.ts.map +1 -0
  63. package/dist/types/components/pagination/composables/usePaginationEllipsis.d.ts +34 -0
  64. package/dist/types/components/pagination/composables/usePaginationEllipsis.d.ts.map +1 -0
  65. package/dist/types/components/pagination/composables/usePaginationJump.d.ts +8 -0
  66. package/dist/types/components/pagination/composables/usePaginationJump.d.ts.map +1 -0
  67. package/dist/types/components/pagination/composables/usePaginationPageTooltip.d.ts +3 -0
  68. package/dist/types/components/pagination/composables/usePaginationPageTooltip.d.ts.map +1 -0
  69. package/dist/types/components/pagination/composables/usePaginationWindow.d.ts +188 -0
  70. package/dist/types/components/pagination/composables/usePaginationWindow.d.ts.map +1 -0
  71. package/dist/types/components/pagination/context.d.ts +19 -0
  72. package/dist/types/components/pagination/context.d.ts.map +1 -0
  73. package/dist/types/components/pagination/pagination.variants.d.ts +68 -0
  74. package/dist/types/components/pagination/pagination.variants.d.ts.map +1 -0
  75. package/dist/types/components/pagination/types.d.ts +23 -0
  76. package/dist/types/components/pagination/types.d.ts.map +1 -0
  77. package/dist/types/components/pagination/utils/ranges.d.ts +3 -0
  78. package/dist/types/components/pagination/utils/ranges.d.ts.map +1 -0
  79. package/dist/types/components/range-slider/RangeSlider.vue.d.ts +1 -0
  80. package/dist/types/components/range-slider/RangeSlider.vue.d.ts.map +1 -1
  81. package/dist/types/components/slider/Slider.vue.d.ts +1 -0
  82. package/dist/types/components/slider/Slider.vue.d.ts.map +1 -1
  83. package/dist/types/components/slider/slider.variants.d.ts.map +1 -1
  84. package/dist/types/components/tooltip/Tooltip.vue.d.ts +4 -15
  85. package/dist/types/components/tooltip/Tooltip.vue.d.ts.map +1 -1
  86. package/dist/types/components/tooltip/TooltipBubble.vue.d.ts +20 -0
  87. package/dist/types/components/tooltip/TooltipBubble.vue.d.ts.map +1 -0
  88. package/dist/types/components/tooltip/TooltipDirectiveRoot.d.ts +33 -0
  89. package/dist/types/components/tooltip/TooltipDirectiveRoot.d.ts.map +1 -0
  90. package/dist/types/components/tooltip/composables/useTooltipTarget.d.ts +2 -0
  91. package/dist/types/components/tooltip/composables/useTooltipTarget.d.ts.map +1 -0
  92. package/dist/types/components/tooltip/directive.d.ts +4 -0
  93. package/dist/types/components/tooltip/directive.d.ts.map +1 -0
  94. package/dist/types/components/tooltip/types.d.ts +14 -0
  95. package/dist/types/components/tooltip/types.d.ts.map +1 -0
  96. package/dist/types/components/tooltip/utils/directiveContext.d.ts +10 -0
  97. package/dist/types/components/tooltip/utils/directiveContext.d.ts.map +1 -0
  98. package/dist/types/components/tree-select/TreeSelect.vue.d.ts +6 -1
  99. package/dist/types/components/tree-select/TreeSelect.vue.d.ts.map +1 -1
  100. package/dist/types/components/tree-select/TreeSelectSearch.vue.d.ts +22 -0
  101. package/dist/types/components/tree-select/TreeSelectSearch.vue.d.ts.map +1 -0
  102. package/dist/types/components/tree-select/composables/useTreeSelect.d.ts +35 -0
  103. package/dist/types/components/tree-select/composables/useTreeSelect.d.ts.map +1 -0
  104. package/dist/types/components/tree-select/tree-select.variants.d.ts +1 -1
  105. package/dist/types/components/tree-select/tree-select.variants.d.ts.map +1 -1
  106. package/dist/types/components/tree-select/utils/filterTree.d.ts +7 -0
  107. package/dist/types/components/tree-select/utils/filterTree.d.ts.map +1 -0
  108. package/dist/types/index.d.ts +9 -1
  109. package/dist/types/index.d.ts.map +1 -1
  110. package/dist/types/lib/overlay-positioner.d.ts +3 -0
  111. package/dist/types/lib/overlay-positioner.d.ts.map +1 -0
  112. package/dist/types/lib/pointer-corridor.d.ts +7 -0
  113. package/dist/types/lib/pointer-corridor.d.ts.map +1 -0
  114. package/dist/types/lib/useDirection.d.ts +7 -0
  115. package/dist/types/lib/useDirection.d.ts.map +1 -0
  116. package/dist/types/locale/en-US.d.ts.map +1 -1
  117. package/dist/types/locale/index.d.ts.map +1 -1
  118. package/dist/types/locale/types.d.ts +13 -0
  119. package/dist/types/locale/types.d.ts.map +1 -1
  120. package/dist/types/locale/zh-CN.d.ts.map +1 -1
  121. package/package.json +1 -1
  122. package/src/components/badge/badge.variants.ts +1 -1
  123. package/src/components/hover-card/HoverCard.vue +34 -16
  124. package/src/components/hover-card/HoverCardAnchor.vue +16 -0
  125. package/src/components/hover-card/composables/useHoverCardAnchor.ts +160 -0
  126. package/src/components/image/Image.vue +2 -0
  127. package/src/components/image/composables/useImageGroupState.ts +9 -1
  128. package/src/components/image/image.variants.ts +1 -1
  129. package/src/components/lightbox/Lightbox.vue +23 -17
  130. package/src/components/lightbox/LightboxChrome.vue +7 -1
  131. package/src/components/lightbox/LightboxToolbar.vue +16 -2
  132. package/src/components/lightbox/composables/useLightboxFrames.ts +108 -12
  133. package/src/components/lightbox/composables/useLightboxLarge.ts +4 -1
  134. package/src/components/lightbox/composables/useLightboxLayout.ts +44 -0
  135. package/src/components/lightbox/composables/useLightboxPhase.ts +12 -4
  136. package/src/components/lightbox/composables/useLightboxPose.ts +17 -19
  137. package/src/components/lightbox/composables/useLightboxRotation.ts +14 -4
  138. package/src/components/lightbox/composables/useLightboxZoom.ts +51 -9
  139. package/src/components/lightbox/lightbox.variants.ts +5 -0
  140. package/src/components/lightbox/types.ts +5 -1
  141. package/src/components/lightbox/utils/pose.ts +13 -6
  142. package/src/components/lightbox/utils/source.ts +17 -0
  143. package/src/components/lightbox/utils/zoom.ts +21 -7
  144. package/src/components/pagination/Pagination.vue +118 -0
  145. package/src/components/pagination/PaginationContent.vue +29 -0
  146. package/src/components/pagination/PaginationControl.vue +42 -0
  147. package/src/components/pagination/PaginationEllipsis.vue +67 -0
  148. package/src/components/pagination/PaginationInfo.vue +18 -0
  149. package/src/components/pagination/PaginationJump.vue +32 -0
  150. package/src/components/pagination/PaginationPage.vue +34 -0
  151. package/src/components/pagination/PaginationPages.vue +43 -0
  152. package/src/components/pagination/PaginationPopup.vue +92 -0
  153. package/src/components/pagination/PaginationSize.vue +30 -0
  154. package/src/components/pagination/composables/usePagination.ts +76 -0
  155. package/src/components/pagination/composables/usePaginationEllipsis.ts +269 -0
  156. package/src/components/pagination/composables/usePaginationJump.ts +23 -0
  157. package/src/components/pagination/composables/usePaginationPageTooltip.ts +23 -0
  158. package/src/components/pagination/composables/usePaginationWindow.ts +116 -0
  159. package/src/components/pagination/context.ts +23 -0
  160. package/src/components/pagination/pagination.variants.ts +32 -0
  161. package/src/components/pagination/types.ts +19 -0
  162. package/src/components/pagination/utils/ranges.ts +12 -0
  163. package/src/components/range-slider/RangeSlider.vue +6 -0
  164. package/src/components/slider/Slider.vue +6 -0
  165. package/src/components/slider/slider.variants.ts +1 -0
  166. package/src/components/tooltip/Tooltip.vue +10 -33
  167. package/src/components/tooltip/TooltipBubble.vue +36 -0
  168. package/src/components/tooltip/TooltipDirectiveRoot.ts +61 -0
  169. package/src/components/tooltip/composables/useTooltipTarget.ts +56 -0
  170. package/src/components/tooltip/directive.ts +54 -0
  171. package/src/components/tooltip/types.ts +15 -0
  172. package/src/components/tooltip/utils/directiveContext.ts +32 -0
  173. package/src/components/tree-select/TreeSelect.vue +90 -61
  174. package/src/components/tree-select/TreeSelectSearch.vue +48 -0
  175. package/src/components/tree-select/composables/useTreeSelect.ts +130 -0
  176. package/src/components/tree-select/tree-select.variants.ts +2 -2
  177. package/src/components/tree-select/utils/filterTree.ts +19 -0
  178. package/src/index.ts +9 -1
  179. package/src/lib/overlay-positioner.ts +25 -0
  180. package/src/lib/pointer-corridor.ts +26 -0
  181. package/src/lib/useDirection.ts +31 -0
  182. package/src/locale/en-US.ts +15 -1
  183. package/src/locale/index.ts +1 -0
  184. package/src/locale/types.ts +13 -0
  185. package/src/locale/zh-CN.ts +15 -1
@@ -0,0 +1,160 @@
1
+ import { computed, onScopeDispose, shallowRef, watch, type ComponentPublicInstance } from 'vue'
2
+ import { useEventListener, useRafFn } from '@vueuse/core'
3
+ import { injectHoverCardRootContext } from 'reka-ui'
4
+ import { useOverlayPortal } from '../../../lib/overlay-portal'
5
+ import { useOverlayPositionerClass } from '../../../lib/overlay-positioner'
6
+ import { inPointerCorridor } from '../../../lib/pointer-corridor'
7
+
8
+ interface HoverCardAnchorOptions {
9
+ anchor?: HTMLElement | null
10
+ external: boolean
11
+ closeDelay: number
12
+ positionerClass?: string
13
+ }
14
+
15
+ export function useHoverCardAnchor(props: HoverCardAnchorOptions) {
16
+ const root = injectHoverCardRootContext()
17
+ const original = { onOpen: root.onOpen, onClose: root.onClose, onDismiss: root.onDismiss }
18
+ const anchor = computed(() => (props.external ? (props.anchor ?? undefined) : undefined))
19
+ const { content, present } = useOverlayPortal(root.open)
20
+ useOverlayPositionerClass(content, () => props.positionerClass)
21
+ const panel = computed(() =>
22
+ props.external ? (content.value?.$el as HTMLElement | undefined) : undefined,
23
+ )
24
+ const liveAnchor = shallowRef<HTMLElement>()
25
+ const retained = shallowRef<{
26
+ getBoundingClientRect: () => DOMRect
27
+ contextElement: HTMLElement
28
+ }>()
29
+ const reference = computed(() =>
30
+ props.external ? (liveAnchor.value ?? retained.value) : undefined,
31
+ )
32
+ let timer: ReturnType<typeof setTimeout> | undefined
33
+ let tracking = false
34
+
35
+ function cancel() {
36
+ clearTimeout(timer)
37
+ timer = undefined
38
+ }
39
+
40
+ function dismiss() {
41
+ cancel()
42
+ tracking = false
43
+ root.onOpenChange(false)
44
+ }
45
+
46
+ function close() {
47
+ if (
48
+ timer !== undefined ||
49
+ !root.open.value ||
50
+ root.hasSelectionRef.value ||
51
+ root.isPointerDownOnContentRef.value
52
+ )
53
+ return
54
+ timer = setTimeout(dismiss, props.closeDelay)
55
+ }
56
+
57
+ function keep() {
58
+ tracking = false
59
+ cancel()
60
+ }
61
+
62
+ function track(event: Event) {
63
+ const pointer = event as PointerEvent
64
+ if (!root.open.value || !tracking || pointer.pointerType === 'touch') return
65
+ const target = pointer.target as Node | null
66
+ if (target && (anchor.value?.contains(target) || panel.value?.contains(target))) return keep()
67
+ const a = anchor.value?.getBoundingClientRect()
68
+ const b = panel.value?.getBoundingClientRect()
69
+ if (a && b && inPointerCorridor({ x: pointer.clientX, y: pointer.clientY }, a, b)) cancel()
70
+ else close()
71
+ }
72
+
73
+ function leave(event: Event) {
74
+ const pointer = event as PointerEvent
75
+ if (pointer.pointerType === 'touch' || !root.open.value) return
76
+ tracking = true
77
+ const a = anchor.value?.getBoundingClientRect()
78
+ const b = panel.value?.getBoundingClientRect()
79
+ if (a && b && inPointerCorridor({ x: pointer.clientX, y: pointer.clientY }, a, b)) cancel()
80
+ else close()
81
+ }
82
+
83
+ function measure() {
84
+ const element = anchor.value
85
+ if (!element?.isConnected) {
86
+ liveAnchor.value = undefined
87
+ if (root.open.value) dismiss()
88
+ return
89
+ }
90
+ liveAnchor.value = element
91
+ const rect = element.getBoundingClientRect()
92
+ const previous = retained.value?.getBoundingClientRect()
93
+ if (
94
+ !previous ||
95
+ retained.value?.contextElement !== element ||
96
+ rect.x !== previous.x ||
97
+ rect.y !== previous.y ||
98
+ rect.width !== previous.width ||
99
+ rect.height !== previous.height
100
+ )
101
+ retained.value = { getBoundingClientRect: () => rect, contextElement: element }
102
+ }
103
+
104
+ const { pause, resume } = useRafFn(measure, { immediate: false })
105
+ root.onOpen = () => (props.external ? keep() : original.onOpen())
106
+ root.onClose = () => (props.external ? close() : original.onClose())
107
+ root.onDismiss = () => (props.external ? dismiss() : original.onDismiss())
108
+
109
+ watch(
110
+ [anchor, root.open, present],
111
+ ([element, , mounted], previous) => {
112
+ cancel()
113
+ tracking = false
114
+ pause()
115
+ if (!props.external || !mounted) {
116
+ liveAnchor.value = undefined
117
+ return
118
+ }
119
+ if (element !== previous?.[0]) {
120
+ root.hasSelectionRef.value = false
121
+ root.isPointerDownOnContentRef.value = false
122
+ }
123
+ measure()
124
+ resume()
125
+ },
126
+ { immediate: true, flush: 'post' },
127
+ )
128
+
129
+ useEventListener(anchor, 'pointerenter', keep)
130
+ useEventListener(anchor, 'pointerleave', leave)
131
+ useEventListener(anchor, 'focusin', keep)
132
+ useEventListener(anchor, 'focusout', event => {
133
+ const target = (event as FocusEvent).relatedTarget as Node | null
134
+ if (!target || (!anchor.value?.contains(target) && !panel.value?.contains(target))) close()
135
+ })
136
+ useEventListener(panel, 'pointerenter', keep)
137
+ useEventListener(panel, 'pointerleave', leave)
138
+ useEventListener(() => anchor.value?.ownerDocument, 'pointermove', track)
139
+ useEventListener(
140
+ () => anchor.value?.ownerDocument,
141
+ 'scroll',
142
+ event => {
143
+ if (root.open.value && (event.target as Node | null)?.contains(anchor.value ?? null))
144
+ dismiss()
145
+ },
146
+ { capture: true, passive: true },
147
+ )
148
+
149
+ onScopeDispose(() => {
150
+ cancel()
151
+ Object.assign(root, original)
152
+ })
153
+
154
+ function contentRef(value: Element | ComponentPublicInstance | null) {
155
+ content.value = value as ComponentPublicInstance | null
156
+ if (!value && !root.open.value) retained.value = undefined
157
+ }
158
+
159
+ return { reference, contentRef, present }
160
+ }
@@ -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
  }
@@ -6,6 +6,7 @@ export const LARGE_HINT_DELAY_MS = 800
6
6
  export function useLightboxLarge(item: () => LightboxItem | undefined, active: () => boolean) {
7
7
  const src = shallowRef<string>()
8
8
  const ready = shallowRef(false)
9
+ const image = shallowRef<HTMLImageElement>()
9
10
  const waiting = shallowRef(false)
10
11
  let loader: HTMLImageElement | null = null
11
12
  let timer: ReturnType<typeof setTimeout> | null = null
@@ -17,6 +18,7 @@ export function useLightboxLarge(item: () => LightboxItem | undefined, active: (
17
18
  timer = null
18
19
  src.value = undefined
19
20
  ready.value = false
21
+ image.value = undefined
20
22
  waiting.value = false
21
23
  }
22
24
 
@@ -36,6 +38,7 @@ export function useLightboxLarge(item: () => LightboxItem | undefined, active: (
36
38
  if (timer) clearTimeout(timer)
37
39
  timer = null
38
40
  waiting.value = false
41
+ image.value = img
39
42
  ready.value = true
40
43
  }
41
44
  const decode = typeof img.decode === 'function' ? img.decode() : Promise.resolve()
@@ -58,5 +61,5 @@ export function useLightboxLarge(item: () => LightboxItem | undefined, active: (
58
61
 
59
62
  onBeforeUnmount(reset)
60
63
 
61
- return { src, ready, waiting }
64
+ return { src, ready, waiting, image }
62
65
  }
@@ -0,0 +1,44 @@
1
+ import { watch } from 'vue'
2
+ import { useResizeObserver } from '@vueuse/core'
3
+ import type { LightboxItem } from '../types'
4
+ import type { useLightboxFrames } from './useLightboxFrames'
5
+ import type { useLightboxLarge } from './useLightboxLarge'
6
+ import type { useLightboxPhase } from './useLightboxPhase'
7
+ import type { useLightboxZoom } from './useLightboxZoom'
8
+
9
+ export function useLightboxLayout(options: {
10
+ stage: () => HTMLElement | undefined
11
+ current: () => LightboxItem | undefined
12
+ frames: ReturnType<typeof useLightboxFrames>
13
+ large: ReturnType<typeof useLightboxLarge>
14
+ phase: ReturnType<typeof useLightboxPhase>
15
+ zoom: ReturnType<typeof useLightboxZoom>
16
+ measure: () => void
17
+ }) {
18
+ function sync() {
19
+ if (options.phase.current() !== 'open') return
20
+ options.zoom.reflow(() => {
21
+ options.measure()
22
+ options.frames.learnPreview(options.current(), options.large.image.value)
23
+ })
24
+ }
25
+
26
+ function learn(id: string, img: HTMLImageElement) {
27
+ const update = () => options.frames.learn(id, img)
28
+ if (options.phase.current() === 'open' && options.current()?.id === id)
29
+ options.zoom.reflow(update)
30
+ else update()
31
+ }
32
+
33
+ watch(() => [options.phase.current(), options.current(), options.large.image.value], sync, {
34
+ flush: 'post',
35
+ })
36
+ useResizeObserver(() => {
37
+ const stage = options.stage()
38
+ if (!stage) return []
39
+ const chrome = stage.querySelector<HTMLElement>('[data-hn-chrome]')
40
+ return chrome ? [stage, chrome] : [stage]
41
+ }, sync)
42
+
43
+ return { learn }
44
+ }