@hina-ui/vue 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/dist/index.js +4326 -2130
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/components/alert-dialog/AlertDialog.vue.d.ts +2 -2
  4. package/dist/types/components/alert-dialog/AlertDialog.vue.d.ts.map +1 -1
  5. package/dist/types/components/dialog/Dialog.vue.d.ts.map +1 -1
  6. package/dist/types/components/dialog/ModalContent.vue.d.ts +21 -0
  7. package/dist/types/components/dialog/ModalContent.vue.d.ts.map +1 -0
  8. package/dist/types/components/dropdown-menu/DropdownMenu.vue.d.ts.map +1 -1
  9. package/dist/types/components/file-upload/FileUpload.vue.d.ts +1 -1
  10. package/dist/types/components/file-upload/FileUpload.vue.d.ts.map +1 -1
  11. package/dist/types/components/hover-card/HoverCard.vue.d.ts +7 -7
  12. package/dist/types/components/hover-card/HoverCard.vue.d.ts.map +1 -1
  13. package/dist/types/components/hover-card/HoverCardAnchor.vue.d.ts +27 -0
  14. package/dist/types/components/hover-card/HoverCardAnchor.vue.d.ts.map +1 -0
  15. package/dist/types/components/hover-card/composables/useHoverCardAnchor.d.ts +17 -0
  16. package/dist/types/components/hover-card/composables/useHoverCardAnchor.d.ts.map +1 -0
  17. package/dist/types/components/image/Image.vue.d.ts +8 -3
  18. package/dist/types/components/image/Image.vue.d.ts.map +1 -1
  19. package/dist/types/components/image/composables/useImage.d.ts.map +1 -1
  20. package/dist/types/components/image/composables/useImageGroupState.d.ts.map +1 -1
  21. package/dist/types/components/image/image.variants.d.ts +23 -1
  22. package/dist/types/components/image/image.variants.d.ts.map +1 -1
  23. package/dist/types/components/lightbox/Lightbox.vue.d.ts.map +1 -1
  24. package/dist/types/components/lightbox/LightboxChrome.vue.d.ts +4 -0
  25. package/dist/types/components/lightbox/LightboxChrome.vue.d.ts.map +1 -1
  26. package/dist/types/components/lightbox/LightboxToolbar.vue.d.ts +4 -0
  27. package/dist/types/components/lightbox/LightboxToolbar.vue.d.ts.map +1 -1
  28. package/dist/types/components/lightbox/composables/useLightboxFrames.d.ts +4 -1
  29. package/dist/types/components/lightbox/composables/useLightboxFrames.d.ts.map +1 -1
  30. package/dist/types/components/lightbox/composables/useLightboxLarge.d.ts +1 -0
  31. package/dist/types/components/lightbox/composables/useLightboxLarge.d.ts.map +1 -1
  32. package/dist/types/components/lightbox/composables/useLightboxLayout.d.ts +17 -0
  33. package/dist/types/components/lightbox/composables/useLightboxLayout.d.ts.map +1 -0
  34. package/dist/types/components/lightbox/composables/useLightboxMotion.d.ts.map +1 -1
  35. package/dist/types/components/lightbox/composables/useLightboxPhase.d.ts +1 -1
  36. package/dist/types/components/lightbox/composables/useLightboxPhase.d.ts.map +1 -1
  37. package/dist/types/components/lightbox/composables/useLightboxPose.d.ts.map +1 -1
  38. package/dist/types/components/lightbox/composables/useLightboxRotation.d.ts +5 -1
  39. package/dist/types/components/lightbox/composables/useLightboxRotation.d.ts.map +1 -1
  40. package/dist/types/components/lightbox/composables/useLightboxZoom.d.ts +10 -1
  41. package/dist/types/components/lightbox/composables/useLightboxZoom.d.ts.map +1 -1
  42. package/dist/types/components/lightbox/lightbox.variants.d.ts +2 -0
  43. package/dist/types/components/lightbox/lightbox.variants.d.ts.map +1 -0
  44. package/dist/types/components/lightbox/types.d.ts +7 -1
  45. package/dist/types/components/lightbox/types.d.ts.map +1 -1
  46. package/dist/types/components/lightbox/utils/clip.d.ts +16 -0
  47. package/dist/types/components/lightbox/utils/clip.d.ts.map +1 -0
  48. package/dist/types/components/lightbox/utils/pose.d.ts +4 -4
  49. package/dist/types/components/lightbox/utils/pose.d.ts.map +1 -1
  50. package/dist/types/components/lightbox/utils/source.d.ts +4 -0
  51. package/dist/types/components/lightbox/utils/source.d.ts.map +1 -0
  52. package/dist/types/components/lightbox/utils/zoom.d.ts +8 -4
  53. package/dist/types/components/lightbox/utils/zoom.d.ts.map +1 -1
  54. package/dist/types/components/pagination/Pagination.vue.d.ts +59 -0
  55. package/dist/types/components/pagination/Pagination.vue.d.ts.map +1 -0
  56. package/dist/types/components/pagination/PaginationContent.vue.d.ts +23 -0
  57. package/dist/types/components/pagination/PaginationContent.vue.d.ts.map +1 -0
  58. package/dist/types/components/pagination/PaginationControl.vue.d.ts +10 -0
  59. package/dist/types/components/pagination/PaginationControl.vue.d.ts.map +1 -0
  60. package/dist/types/components/pagination/PaginationEllipsis.vue.d.ts +23 -0
  61. package/dist/types/components/pagination/PaginationEllipsis.vue.d.ts.map +1 -0
  62. package/dist/types/components/pagination/PaginationInfo.vue.d.ts +24 -0
  63. package/dist/types/components/pagination/PaginationInfo.vue.d.ts.map +1 -0
  64. package/dist/types/components/pagination/PaginationJump.vue.d.ts +7 -0
  65. package/dist/types/components/pagination/PaginationJump.vue.d.ts.map +1 -0
  66. package/dist/types/components/pagination/PaginationPage.vue.d.ts +20 -0
  67. package/dist/types/components/pagination/PaginationPage.vue.d.ts.map +1 -0
  68. package/dist/types/components/pagination/PaginationPages.vue.d.ts +27 -0
  69. package/dist/types/components/pagination/PaginationPages.vue.d.ts.map +1 -0
  70. package/dist/types/components/pagination/PaginationPopup.vue.d.ts +8 -0
  71. package/dist/types/components/pagination/PaginationPopup.vue.d.ts.map +1 -0
  72. package/dist/types/components/pagination/PaginationSize.vue.d.ts +7 -0
  73. package/dist/types/components/pagination/PaginationSize.vue.d.ts.map +1 -0
  74. package/dist/types/components/pagination/composables/usePagination.d.ts +25 -0
  75. package/dist/types/components/pagination/composables/usePagination.d.ts.map +1 -0
  76. package/dist/types/components/pagination/composables/usePaginationEllipsis.d.ts +34 -0
  77. package/dist/types/components/pagination/composables/usePaginationEllipsis.d.ts.map +1 -0
  78. package/dist/types/components/pagination/composables/usePaginationJump.d.ts +8 -0
  79. package/dist/types/components/pagination/composables/usePaginationJump.d.ts.map +1 -0
  80. package/dist/types/components/pagination/composables/usePaginationPageTooltip.d.ts +3 -0
  81. package/dist/types/components/pagination/composables/usePaginationPageTooltip.d.ts.map +1 -0
  82. package/dist/types/components/pagination/composables/usePaginationWindow.d.ts +188 -0
  83. package/dist/types/components/pagination/composables/usePaginationWindow.d.ts.map +1 -0
  84. package/dist/types/components/pagination/context.d.ts +19 -0
  85. package/dist/types/components/pagination/context.d.ts.map +1 -0
  86. package/dist/types/components/pagination/pagination.variants.d.ts +68 -0
  87. package/dist/types/components/pagination/pagination.variants.d.ts.map +1 -0
  88. package/dist/types/components/pagination/types.d.ts +23 -0
  89. package/dist/types/components/pagination/types.d.ts.map +1 -0
  90. package/dist/types/components/pagination/utils/ranges.d.ts +3 -0
  91. package/dist/types/components/pagination/utils/ranges.d.ts.map +1 -0
  92. package/dist/types/components/popover/Popover.vue.d.ts.map +1 -1
  93. package/dist/types/components/range-slider/RangeSlider.vue.d.ts +1 -0
  94. package/dist/types/components/range-slider/RangeSlider.vue.d.ts.map +1 -1
  95. package/dist/types/components/scroll-area/composables/useLayerLock.d.ts.map +1 -1
  96. package/dist/types/components/slider/Slider.vue.d.ts +1 -0
  97. package/dist/types/components/slider/Slider.vue.d.ts.map +1 -1
  98. package/dist/types/components/slider/slider.variants.d.ts.map +1 -1
  99. package/dist/types/components/tooltip/Tooltip.vue.d.ts +4 -15
  100. package/dist/types/components/tooltip/Tooltip.vue.d.ts.map +1 -1
  101. package/dist/types/components/tooltip/TooltipBubble.vue.d.ts +20 -0
  102. package/dist/types/components/tooltip/TooltipBubble.vue.d.ts.map +1 -0
  103. package/dist/types/components/tooltip/TooltipDirectiveRoot.d.ts +33 -0
  104. package/dist/types/components/tooltip/TooltipDirectiveRoot.d.ts.map +1 -0
  105. package/dist/types/components/tooltip/composables/useTooltipTarget.d.ts +2 -0
  106. package/dist/types/components/tooltip/composables/useTooltipTarget.d.ts.map +1 -0
  107. package/dist/types/components/tooltip/directive.d.ts +4 -0
  108. package/dist/types/components/tooltip/directive.d.ts.map +1 -0
  109. package/dist/types/components/tooltip/types.d.ts +14 -0
  110. package/dist/types/components/tooltip/types.d.ts.map +1 -0
  111. package/dist/types/components/tooltip/utils/directiveContext.d.ts +10 -0
  112. package/dist/types/components/tooltip/utils/directiveContext.d.ts.map +1 -0
  113. package/dist/types/components/tree-select/TreeSelect.vue.d.ts +6 -1
  114. package/dist/types/components/tree-select/TreeSelect.vue.d.ts.map +1 -1
  115. package/dist/types/components/tree-select/TreeSelectSearch.vue.d.ts +22 -0
  116. package/dist/types/components/tree-select/TreeSelectSearch.vue.d.ts.map +1 -0
  117. package/dist/types/components/tree-select/composables/useTreeSelect.d.ts +35 -0
  118. package/dist/types/components/tree-select/composables/useTreeSelect.d.ts.map +1 -0
  119. package/dist/types/components/tree-select/tree-select.variants.d.ts +1 -1
  120. package/dist/types/components/tree-select/tree-select.variants.d.ts.map +1 -1
  121. package/dist/types/components/tree-select/utils/filterTree.d.ts +7 -0
  122. package/dist/types/components/tree-select/utils/filterTree.d.ts.map +1 -0
  123. package/dist/types/index.d.ts +10 -0
  124. package/dist/types/index.d.ts.map +1 -1
  125. package/dist/types/lib/anchored-overlay.d.ts +3 -7
  126. package/dist/types/lib/anchored-overlay.d.ts.map +1 -1
  127. package/dist/types/lib/dialog-focus.d.ts +2 -0
  128. package/dist/types/lib/dialog-focus.d.ts.map +1 -0
  129. package/dist/types/lib/overlay-portal.d.ts +6 -0
  130. package/dist/types/lib/overlay-portal.d.ts.map +1 -0
  131. package/dist/types/lib/overlay-positioner.d.ts +3 -0
  132. package/dist/types/lib/overlay-positioner.d.ts.map +1 -0
  133. package/dist/types/lib/pointer-corridor.d.ts +7 -0
  134. package/dist/types/lib/pointer-corridor.d.ts.map +1 -0
  135. package/dist/types/lib/useDirection.d.ts +7 -0
  136. package/dist/types/lib/useDirection.d.ts.map +1 -0
  137. package/dist/types/locale/en-US.d.ts.map +1 -1
  138. package/dist/types/locale/index.d.ts.map +1 -1
  139. package/dist/types/locale/types.d.ts +13 -0
  140. package/dist/types/locale/types.d.ts.map +1 -1
  141. package/dist/types/locale/zh-CN.d.ts.map +1 -1
  142. package/package.json +1 -1
  143. package/src/components/alert-dialog/AlertDialog.vue +7 -4
  144. package/src/components/badge/badge.variants.ts +1 -1
  145. package/src/components/dialog/Dialog.vue +7 -4
  146. package/src/components/dialog/ModalContent.vue +19 -0
  147. package/src/components/dropdown-menu/DropdownMenu.vue +1 -1
  148. package/src/components/hover-card/HoverCard.vue +34 -16
  149. package/src/components/hover-card/HoverCardAnchor.vue +16 -0
  150. package/src/components/hover-card/composables/useHoverCardAnchor.ts +160 -0
  151. package/src/components/image/Image.vue +11 -12
  152. package/src/components/image/composables/useImage.ts +6 -3
  153. package/src/components/image/composables/useImageGroupState.ts +9 -1
  154. package/src/components/image/image.variants.ts +13 -1
  155. package/src/components/lightbox/Lightbox.vue +23 -17
  156. package/src/components/lightbox/LightboxChrome.vue +7 -1
  157. package/src/components/lightbox/LightboxToolbar.vue +16 -2
  158. package/src/components/lightbox/composables/useLightboxFrames.ts +108 -12
  159. package/src/components/lightbox/composables/useLightboxLarge.ts +4 -1
  160. package/src/components/lightbox/composables/useLightboxLayout.ts +44 -0
  161. package/src/components/lightbox/composables/useLightboxMotion.ts +3 -1
  162. package/src/components/lightbox/composables/useLightboxPhase.ts +12 -4
  163. package/src/components/lightbox/composables/useLightboxPose.ts +20 -19
  164. package/src/components/lightbox/composables/useLightboxRotation.ts +14 -4
  165. package/src/components/lightbox/composables/useLightboxZoom.ts +51 -9
  166. package/src/components/lightbox/lightbox.variants.ts +5 -0
  167. package/src/components/lightbox/types.ts +5 -1
  168. package/src/components/lightbox/utils/clip.ts +117 -0
  169. package/src/components/lightbox/utils/pose.ts +36 -19
  170. package/src/components/lightbox/utils/source.ts +17 -0
  171. package/src/components/lightbox/utils/zoom.ts +21 -7
  172. package/src/components/pagination/Pagination.vue +118 -0
  173. package/src/components/pagination/PaginationContent.vue +29 -0
  174. package/src/components/pagination/PaginationControl.vue +42 -0
  175. package/src/components/pagination/PaginationEllipsis.vue +67 -0
  176. package/src/components/pagination/PaginationInfo.vue +18 -0
  177. package/src/components/pagination/PaginationJump.vue +32 -0
  178. package/src/components/pagination/PaginationPage.vue +34 -0
  179. package/src/components/pagination/PaginationPages.vue +43 -0
  180. package/src/components/pagination/PaginationPopup.vue +92 -0
  181. package/src/components/pagination/PaginationSize.vue +30 -0
  182. package/src/components/pagination/composables/usePagination.ts +76 -0
  183. package/src/components/pagination/composables/usePaginationEllipsis.ts +269 -0
  184. package/src/components/pagination/composables/usePaginationJump.ts +23 -0
  185. package/src/components/pagination/composables/usePaginationPageTooltip.ts +23 -0
  186. package/src/components/pagination/composables/usePaginationWindow.ts +116 -0
  187. package/src/components/pagination/context.ts +23 -0
  188. package/src/components/pagination/pagination.variants.ts +32 -0
  189. package/src/components/pagination/types.ts +19 -0
  190. package/src/components/pagination/utils/ranges.ts +12 -0
  191. package/src/components/popover/Popover.vue +6 -2
  192. package/src/components/range-slider/RangeSlider.vue +6 -0
  193. package/src/components/scroll-area/composables/useLayerLock.ts +8 -9
  194. package/src/components/scroll-area/composables/useWheelRedirect.ts +1 -1
  195. package/src/components/skeleton/LICENSE +24 -0
  196. package/src/components/slider/Slider.vue +6 -0
  197. package/src/components/slider/slider.variants.ts +1 -0
  198. package/src/components/tooltip/Tooltip.vue +10 -33
  199. package/src/components/tooltip/TooltipBubble.vue +36 -0
  200. package/src/components/tooltip/TooltipDirectiveRoot.ts +61 -0
  201. package/src/components/tooltip/composables/useTooltipTarget.ts +56 -0
  202. package/src/components/tooltip/directive.ts +54 -0
  203. package/src/components/tooltip/types.ts +15 -0
  204. package/src/components/tooltip/utils/directiveContext.ts +32 -0
  205. package/src/components/tree-select/TreeSelect.vue +90 -61
  206. package/src/components/tree-select/TreeSelectSearch.vue +48 -0
  207. package/src/components/tree-select/composables/useTreeSelect.ts +130 -0
  208. package/src/components/tree-select/tree-select.variants.ts +2 -2
  209. package/src/components/tree-select/utils/filterTree.ts +19 -0
  210. package/src/index.ts +10 -0
  211. package/src/lib/anchored-overlay.ts +2 -5
  212. package/src/lib/dialog-focus.ts +13 -0
  213. package/src/lib/overlay-portal.ts +8 -0
  214. package/src/lib/overlay-positioner.ts +25 -0
  215. package/src/lib/pointer-corridor.ts +26 -0
  216. package/src/lib/useDirection.ts +31 -0
  217. package/src/locale/en-US.ts +15 -1
  218. package/src/locale/index.ts +1 -0
  219. package/src/locale/types.ts +13 -0
  220. package/src/locale/zh-CN.ts +15 -1
  221. package/src/styles/interaction.css +26 -3
  222. package/src/styles/motion.css +14 -2
  223. package/src/styles/semantic.css +5 -1
@@ -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
+ }
@@ -84,7 +84,9 @@ export function useLightboxMotion(stageHeight: () => number, restScale: () => nu
84
84
  function leave(to: Pose | null): Promise<void> {
85
85
  stop()
86
86
  const t = to ? timing(RETURN_SPRING) : transition('exit')
87
- const travel = to ? poseTo(to, t) : [animate(scale, FADE_POSE.scale, t), animate(opacity, 0, t)]
87
+ const travel = to
88
+ ? poseTo(to, t)
89
+ : [animate(scale, Math.min(scale.get(), FADE_POSE.scale), t), animate(opacity, 0, t)]
88
90
  return settle([...travel, animate(presence, 0, t)])
89
91
  }
90
92
 
@@ -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,34 +1,35 @@
1
1
  import type { useLightboxFrames } from './useLightboxFrames'
2
- import { openPose, radiusOf, type Pose, type Rect } from '../utils/pose'
2
+ import { openPose, type Pose } from '../utils/pose'
3
+ import { clipOf } from '../utils/clip'
4
+ import { sourceRect } from '../utils/source'
3
5
  import type { LightboxItem } from '../types'
4
6
 
5
7
  export function useLightboxPose(
6
8
  frames: ReturnType<typeof useLightboxFrames>,
7
9
  current: () => LightboxItem | undefined,
8
10
  ) {
9
- function sourceBox(): Rect | null {
10
- const el = current()?.source?.()
11
- if (!el?.isConnected) return null
12
- const rect = el.getBoundingClientRect()
13
- 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
14
21
  const stage = frames.stage.value
15
22
  if (
16
- rect.bottom <= 0 ||
17
- rect.right <= 0 ||
18
- rect.top >= stage.height ||
19
- 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
20
27
  ) {
21
28
  return null
22
29
  }
23
- return { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
24
- }
25
-
26
- function fromSource(): Pose | null {
27
- const item = current()
28
- const box = sourceBox()
29
- const natural = frames.naturalOf(item)
30
- if (!box || !natural) return null
31
- return openPose(frames.frameOf(item), box, natural, item?.fit, radiusOf(item?.source?.()))
30
+ const clip = clipOf(el, box)
31
+ if (clip.rect.width <= 0 || clip.rect.height <= 0) return null
32
+ return openPose(frames.frameOf(item), box, natural, item?.fit, clip.corners, clip.rect)
32
33
  }
33
34
 
34
35
  return { fromSource }
@@ -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