@hina-ui/vue 1.2.0 → 1.3.1
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.
- package/dist/index.js +880 -347
- package/dist/index.js.map +1 -1
- package/dist/types/components/alert-dialog/AlertDialog.vue.d.ts +6 -2
- package/dist/types/components/alert-dialog/AlertDialog.vue.d.ts.map +1 -1
- package/dist/types/components/alert-dialog/alert-dialog.variants.d.ts +5 -0
- package/dist/types/components/alert-dialog/alert-dialog.variants.d.ts.map +1 -0
- package/dist/types/components/alert-dialog/composables/useAlertDialogConfirm.d.ts +9 -0
- package/dist/types/components/alert-dialog/composables/useAlertDialogConfirm.d.ts.map +1 -0
- package/dist/types/components/alert-dialog/composables/useConfirmDelay.d.ts +3 -0
- package/dist/types/components/alert-dialog/composables/useConfirmDelay.d.ts.map +1 -0
- package/dist/types/components/anchor/Anchor.vue.d.ts.map +1 -1
- package/dist/types/components/code-block/CodeBlock.vue.d.ts.map +1 -1
- package/dist/types/components/code-block/code-block.variants.d.ts +6 -0
- package/dist/types/components/code-block/code-block.variants.d.ts.map +1 -0
- package/dist/types/components/code-block/composables/useCodeHighlight.d.ts +7 -0
- package/dist/types/components/code-block/composables/useCodeHighlight.d.ts.map +1 -0
- package/dist/types/components/dialog/Dialog.vue.d.ts.map +1 -1
- package/dist/types/components/dialog/ModalContent.vue.d.ts +21 -0
- package/dist/types/components/dialog/ModalContent.vue.d.ts.map +1 -0
- package/dist/types/components/dropdown-menu/DropdownMenu.vue.d.ts +18 -6
- package/dist/types/components/dropdown-menu/DropdownMenu.vue.d.ts.map +1 -1
- package/dist/types/components/file-upload/FileUpload.vue.d.ts +1 -1
- package/dist/types/components/file-upload/FileUpload.vue.d.ts.map +1 -1
- package/dist/types/components/highlight/Highlight.vue.d.ts +2 -0
- package/dist/types/components/highlight/Highlight.vue.d.ts.map +1 -1
- package/dist/types/components/highlight/composables/useHighlightMotion.d.ts +10 -0
- package/dist/types/components/highlight/composables/useHighlightMotion.d.ts.map +1 -0
- package/dist/types/components/image/Image.vue.d.ts +5 -2
- package/dist/types/components/image/Image.vue.d.ts.map +1 -1
- package/dist/types/components/image/composables/useImage.d.ts.map +1 -1
- package/dist/types/components/image/image.variants.d.ts +23 -1
- package/dist/types/components/image/image.variants.d.ts.map +1 -1
- package/dist/types/components/lightbox/LightboxThumbs.vue.d.ts.map +1 -1
- package/dist/types/components/lightbox/composables/useLightboxMotion.d.ts.map +1 -1
- package/dist/types/components/lightbox/composables/useLightboxPose.d.ts.map +1 -1
- package/dist/types/components/lightbox/utils/clip.d.ts +16 -0
- package/dist/types/components/lightbox/utils/clip.d.ts.map +1 -0
- package/dist/types/components/lightbox/utils/pose.d.ts +2 -2
- package/dist/types/components/lightbox/utils/pose.d.ts.map +1 -1
- package/dist/types/components/popover/Popover.vue.d.ts +19 -6
- package/dist/types/components/popover/Popover.vue.d.ts.map +1 -1
- package/dist/types/components/popover/PopoverContent.vue.d.ts +29 -0
- package/dist/types/components/popover/PopoverContent.vue.d.ts.map +1 -0
- package/dist/types/components/popover/composables/usePopoverCloseFocus.d.ts +2 -0
- package/dist/types/components/popover/composables/usePopoverCloseFocus.d.ts.map +1 -0
- package/dist/types/components/scroll-area/ScrollArea.vue.d.ts.map +1 -1
- package/dist/types/components/scroll-area/composables/useLayerLock.d.ts.map +1 -1
- package/dist/types/components/scroll-area/composables/useScrollFocus.d.ts +17 -0
- package/dist/types/components/scroll-area/composables/useScrollFocus.d.ts.map +1 -0
- package/dist/types/components/segmented-control/SegmentedControl.vue.d.ts.map +1 -1
- package/dist/types/components/tabs/TabsTrigger.vue.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lib/anchored-overlay.d.ts +19 -0
- package/dist/types/lib/anchored-overlay.d.ts.map +1 -0
- package/dist/types/lib/dialog-focus.d.ts +2 -0
- package/dist/types/lib/dialog-focus.d.ts.map +1 -0
- package/dist/types/lib/overlay-portal.d.ts +6 -0
- package/dist/types/lib/overlay-portal.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/alert-dialog/AlertDialog.vue +31 -40
- package/src/components/alert-dialog/alert-dialog.variants.ts +17 -0
- package/src/components/alert-dialog/composables/useAlertDialogConfirm.ts +29 -0
- package/src/components/alert-dialog/composables/useConfirmDelay.ts +37 -0
- package/src/components/anchor/Anchor.vue +1 -0
- package/src/components/code-block/CodeBlock.vue +15 -30
- package/src/components/code-block/code-block.variants.ts +21 -0
- package/src/components/code-block/composables/useCodeHighlight.ts +30 -0
- package/src/components/dialog/Dialog.vue +7 -4
- package/src/components/dialog/ModalContent.vue +19 -0
- package/src/components/dropdown-menu/DropdownMenu.vue +16 -4
- package/src/components/highlight/Highlight.vue +5 -4
- package/src/components/highlight/composables/useHighlightMotion.ts +22 -0
- package/src/components/image/Image.vue +9 -12
- package/src/components/image/composables/useImage.ts +6 -3
- package/src/components/image/image.variants.ts +13 -1
- package/src/components/lightbox/LightboxThumbs.vue +1 -0
- package/src/components/lightbox/composables/useLightboxMotion.ts +3 -1
- package/src/components/lightbox/composables/useLightboxPose.ts +5 -2
- package/src/components/lightbox/utils/clip.ts +117 -0
- package/src/components/lightbox/utils/pose.ts +23 -13
- package/src/components/popover/Popover.vue +26 -6
- package/src/components/popover/PopoverContent.vue +25 -0
- package/src/components/popover/composables/usePopoverCloseFocus.ts +17 -0
- package/src/components/scroll-area/ScrollArea.vue +7 -5
- package/src/components/scroll-area/composables/useLayerLock.ts +8 -9
- package/src/components/scroll-area/composables/useScrollFocus.ts +39 -0
- package/src/components/scroll-area/composables/useWheelRedirect.ts +1 -1
- package/src/components/segmented-control/SegmentedControl.vue +6 -1
- package/src/components/skeleton/LICENSE +24 -0
- package/src/components/tabs/TabsTrigger.vue +6 -1
- package/src/index.ts +2 -0
- package/src/lib/anchored-overlay.ts +107 -0
- package/src/lib/dialog-focus.ts +13 -0
- package/src/lib/overlay-portal.ts +8 -0
- package/src/styles/interaction.css +26 -3
- package/src/styles/motion.css +14 -2
- package/src/styles/semantic.css +5 -1
- package/src/styles/typography.css +9 -4
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
import type { MotionProps } from 'motion-v'
|
|
3
|
+
import { TRANSITION, prefersReducedMotion } from '../../../motion'
|
|
4
|
+
|
|
5
|
+
export function useHighlightMotion(axis: () => 'x' | 'y' | 'both') {
|
|
6
|
+
const transition = computed(() => (prefersReducedMotion() ? { duration: 0 } : TRANSITION.layout))
|
|
7
|
+
const transformTemplate = computed<MotionProps['transformTemplate']>(() => {
|
|
8
|
+
const direction = axis()
|
|
9
|
+
if (direction === 'both') return undefined
|
|
10
|
+
|
|
11
|
+
return (_, generated) => {
|
|
12
|
+
if (!generated || generated === 'none' || typeof DOMMatrix === 'undefined') return generated
|
|
13
|
+
|
|
14
|
+
const matrix = new DOMMatrix(generated)
|
|
15
|
+
if (direction === 'x') matrix.m42 = 0
|
|
16
|
+
else matrix.m41 = 0
|
|
17
|
+
return matrix.toString()
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return { transition, transformTemplate }
|
|
22
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onBeforeUnmount, shallowRef, useId, watch } from 'vue'
|
|
2
|
+
import { computed, onBeforeUnmount, shallowRef, useId, watch, type StyleValue } from 'vue'
|
|
3
3
|
import { Primitive } from 'reka-ui'
|
|
4
4
|
import { cn } from '../../lib/cn'
|
|
5
5
|
import { useRequiredLabel } from '../../lib/a11y'
|
|
6
6
|
import Skeleton from '../skeleton/Skeleton.vue'
|
|
7
7
|
import Lightbox from '../lightbox/Lightbox.vue'
|
|
8
8
|
import type { LightboxItem } from '../lightbox/types'
|
|
9
|
-
import { image, type ImageVariants } from './image.variants'
|
|
9
|
+
import { image, imageRoot, type ImageVariants } from './image.variants'
|
|
10
10
|
import { useImage } from './composables/useImage'
|
|
11
11
|
import { useImageGroup } from './context'
|
|
12
12
|
import { useImageResolver } from './resolver'
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
preview?: boolean | string
|
|
28
28
|
draggable?: boolean
|
|
29
29
|
class?: string
|
|
30
|
+
style?: StyleValue
|
|
30
31
|
imageClass?: string
|
|
32
|
+
imageStyle?: StyleValue
|
|
31
33
|
}>(),
|
|
32
34
|
{
|
|
33
35
|
alt: '',
|
|
@@ -93,20 +95,15 @@
|
|
|
93
95
|
ref="rootEl"
|
|
94
96
|
:as="props.preview ? 'button' : 'span'"
|
|
95
97
|
:type="props.preview ? 'button' : undefined"
|
|
96
|
-
:class="
|
|
97
|
-
|
|
98
|
-
'relative block overflow-hidden',
|
|
99
|
-
props.preview && 'hn-focus-ring cursor-zoom-in',
|
|
100
|
-
props.class,
|
|
101
|
-
)
|
|
102
|
-
"
|
|
103
|
-
:style="props.ratio ? { aspectRatio: String(props.ratio) } : undefined"
|
|
98
|
+
:class="cn(imageRoot({ preview: !!props.preview }), props.class)"
|
|
99
|
+
:style="[props.ratio ? { aspectRatio: String(props.ratio) } : undefined, props.style]"
|
|
104
100
|
@click="openPreview"
|
|
105
101
|
>
|
|
106
102
|
<img
|
|
107
103
|
v-if="showImage"
|
|
108
104
|
ref="imageEl"
|
|
109
105
|
v-bind="$attrs"
|
|
106
|
+
:style="props.imageStyle"
|
|
110
107
|
:src="src"
|
|
111
108
|
:alt="props.alt"
|
|
112
109
|
:decoding="props.eager ? 'sync' : 'async'"
|
|
@@ -114,8 +111,8 @@
|
|
|
114
111
|
:draggable="props.draggable"
|
|
115
112
|
:class="
|
|
116
113
|
cn(
|
|
117
|
-
image({ fit: props.fit }),
|
|
118
|
-
props.lazy
|
|
114
|
+
image({ fit: props.fit, lazy: props.lazy }),
|
|
115
|
+
props.lazy && !revealed && 'opacity-0',
|
|
119
116
|
props.imageClass,
|
|
120
117
|
)
|
|
121
118
|
"
|
|
@@ -75,7 +75,7 @@ export function useImage(
|
|
|
75
75
|
const style = getComputedStyle(el)
|
|
76
76
|
try {
|
|
77
77
|
return el.animate(frames, {
|
|
78
|
-
duration: Number.parseFloat(style.getPropertyValue('--hn-duration-
|
|
78
|
+
duration: Number.parseFloat(style.getPropertyValue('--hn-duration-fast')) || 0,
|
|
79
79
|
easing: style.getPropertyValue('--hn-ease-enter').trim() || 'linear',
|
|
80
80
|
fill: 'forwards',
|
|
81
81
|
})
|
|
@@ -89,16 +89,19 @@ export function useImage(
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
async function reveal() {
|
|
92
|
-
revealed.value = true
|
|
93
|
-
|
|
94
92
|
if (!props.lazy) {
|
|
93
|
+
revealed.value = true
|
|
95
94
|
skeletonMounted.value = false
|
|
96
95
|
return
|
|
97
96
|
}
|
|
98
97
|
|
|
98
|
+
const img = imageEl.value
|
|
99
|
+
const at = src.value
|
|
99
100
|
await nextTick()
|
|
100
101
|
await afterPaint()
|
|
102
|
+
if (imageEl.value !== img || src.value !== at) return
|
|
101
103
|
|
|
104
|
+
revealed.value = true
|
|
102
105
|
const layer = skeletonEl.value
|
|
103
106
|
const anim = layer && play(layer, [{ opacity: 1 }, { opacity: 0 }])
|
|
104
107
|
if (!anim) {
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { tv, type VariantProps } from '../../lib/tv'
|
|
2
2
|
|
|
3
|
+
export const imageRoot = tv({
|
|
4
|
+
base: 'relative block w-full overflow-hidden',
|
|
5
|
+
variants: {
|
|
6
|
+
preview: {
|
|
7
|
+
true: 'hn-focus-ring cursor-zoom-in',
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
})
|
|
11
|
+
|
|
3
12
|
export const image = tv({
|
|
4
|
-
base: 'block size-full',
|
|
13
|
+
base: 'relative z-10 block size-full',
|
|
5
14
|
variants: {
|
|
15
|
+
lazy: {
|
|
16
|
+
true: 'transition-opacity duration-(--hn-duration-base) ease-enter motion-reduce:transition-none',
|
|
17
|
+
},
|
|
6
18
|
fit: {
|
|
7
19
|
cover: 'object-cover',
|
|
8
20
|
contain: 'object-contain',
|
|
@@ -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
|
|
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,5 +1,6 @@
|
|
|
1
1
|
import type { useLightboxFrames } from './useLightboxFrames'
|
|
2
|
-
import { openPose,
|
|
2
|
+
import { openPose, type Pose, type Rect } from '../utils/pose'
|
|
3
|
+
import { clipOf } from '../utils/clip'
|
|
3
4
|
import type { LightboxItem } from '../types'
|
|
4
5
|
|
|
5
6
|
export function useLightboxPose(
|
|
@@ -28,7 +29,9 @@ export function useLightboxPose(
|
|
|
28
29
|
const box = sourceBox()
|
|
29
30
|
const natural = frames.naturalOf(item)
|
|
30
31
|
if (!box || !natural) return null
|
|
31
|
-
|
|
32
|
+
const clip = clipOf(item?.source?.(), box)
|
|
33
|
+
if (clip.rect.width <= 0 || clip.rect.height <= 0) return null
|
|
34
|
+
return openPose(frames.frameOf(item), box, natural, item?.fit, clip.corners, clip.rect)
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
return { fromSource }
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Rect } from './pose'
|
|
2
|
+
|
|
3
|
+
export interface Corner {
|
|
4
|
+
x: number
|
|
5
|
+
y: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type Corners = [Corner, Corner, Corner, Corner]
|
|
9
|
+
|
|
10
|
+
function emptyCorners(): Corners {
|
|
11
|
+
return [
|
|
12
|
+
{ x: 0, y: 0 },
|
|
13
|
+
{ x: 0, y: 0 },
|
|
14
|
+
{ x: 0, y: 0 },
|
|
15
|
+
{ x: 0, y: 0 },
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function length(value: string, size: number): number {
|
|
20
|
+
const number = Number.parseFloat(value) || 0
|
|
21
|
+
return value.endsWith('%') ? (number * size) / 100 : number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function cornersOf(style: CSSStyleDeclaration, width: number, height: number): Corners {
|
|
25
|
+
const corners = [
|
|
26
|
+
style.borderTopLeftRadius,
|
|
27
|
+
style.borderTopRightRadius,
|
|
28
|
+
style.borderBottomRightRadius,
|
|
29
|
+
style.borderBottomLeftRadius,
|
|
30
|
+
].map(value => {
|
|
31
|
+
const [x = '0', y = x] = value.split(/\s+/)
|
|
32
|
+
return { x: length(x, width), y: length(y, height) }
|
|
33
|
+
}) as Corners
|
|
34
|
+
const [tl, tr, br, bl] = corners
|
|
35
|
+
const factor = Math.min(
|
|
36
|
+
1,
|
|
37
|
+
width / (tl.x + tr.x || 1),
|
|
38
|
+
width / (bl.x + br.x || 1),
|
|
39
|
+
height / (tl.y + bl.y || 1),
|
|
40
|
+
height / (tr.y + br.y || 1),
|
|
41
|
+
)
|
|
42
|
+
return corners.map(corner => ({ x: corner.x * factor, y: corner.y * factor })) as Corners
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function paddingBox(node: Element, style: CSSStyleDeclaration) {
|
|
46
|
+
const box = node.getBoundingClientRect()
|
|
47
|
+
const width = node instanceof HTMLElement ? node.offsetWidth : box.width
|
|
48
|
+
const height = node instanceof HTMLElement ? node.offsetHeight : box.height
|
|
49
|
+
const sx = width ? box.width / width : 1
|
|
50
|
+
const sy = height ? box.height / height : 1
|
|
51
|
+
const left = Number.parseFloat(style.borderLeftWidth) || 0
|
|
52
|
+
const right = Number.parseFloat(style.borderRightWidth) || 0
|
|
53
|
+
const top = Number.parseFloat(style.borderTopWidth) || 0
|
|
54
|
+
const bottom = Number.parseFloat(style.borderBottomWidth) || 0
|
|
55
|
+
const corners = cornersOf(style, width, height)
|
|
56
|
+
const borders = [
|
|
57
|
+
[left, top],
|
|
58
|
+
[right, top],
|
|
59
|
+
[right, bottom],
|
|
60
|
+
[left, bottom],
|
|
61
|
+
]
|
|
62
|
+
return {
|
|
63
|
+
rect: {
|
|
64
|
+
x: box.x + left * sx,
|
|
65
|
+
y: box.y + top * sy,
|
|
66
|
+
width: Math.max(0, box.width - (left + right) * sx),
|
|
67
|
+
height: Math.max(0, box.height - (top + bottom) * sy),
|
|
68
|
+
},
|
|
69
|
+
corners: corners.map((corner, index) => ({
|
|
70
|
+
x: Math.max(0, corner.x - borders[index]![0]!) * sx,
|
|
71
|
+
y: Math.max(0, corner.y - borders[index]![1]!) * sy,
|
|
72
|
+
})) as Corners,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function points(rect: Rect) {
|
|
77
|
+
return [
|
|
78
|
+
[rect.x, rect.y],
|
|
79
|
+
[rect.x + rect.width, rect.y],
|
|
80
|
+
[rect.x + rect.width, rect.y + rect.height],
|
|
81
|
+
[rect.x, rect.y + rect.height],
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function clipOf(el: Element | null | undefined, box: Rect) {
|
|
86
|
+
let rect = { x: box.x, y: box.y, width: box.width, height: box.height }
|
|
87
|
+
const clips: ReturnType<typeof paddingBox>[] = []
|
|
88
|
+
for (let node = el; node; node = node.parentElement) {
|
|
89
|
+
const style = getComputedStyle(node)
|
|
90
|
+
const clipX = node === el || /^(hidden|clip|scroll|auto)$/.test(style.overflowX)
|
|
91
|
+
const clipY = node === el || /^(hidden|clip|scroll|auto)$/.test(style.overflowY)
|
|
92
|
+
if (!clipX && !clipY) continue
|
|
93
|
+
const clip = paddingBox(node, style)
|
|
94
|
+
const x = clipX ? Math.max(rect.x, clip.rect.x) : rect.x
|
|
95
|
+
const y = clipY ? Math.max(rect.y, clip.rect.y) : rect.y
|
|
96
|
+
const right = clipX
|
|
97
|
+
? Math.min(rect.x + rect.width, clip.rect.x + clip.rect.width)
|
|
98
|
+
: rect.x + rect.width
|
|
99
|
+
const bottom = clipY
|
|
100
|
+
? Math.min(rect.y + rect.height, clip.rect.y + clip.rect.height)
|
|
101
|
+
: rect.y + rect.height
|
|
102
|
+
rect = { x, y, width: Math.max(0, right - x), height: Math.max(0, bottom - y) }
|
|
103
|
+
if (clipX && clipY) clips.push(clip)
|
|
104
|
+
}
|
|
105
|
+
const corners = emptyCorners()
|
|
106
|
+
const visible = points(rect)
|
|
107
|
+
for (const clip of clips) {
|
|
108
|
+
const edges = points(clip.rect)
|
|
109
|
+
for (let index = 0; index < 4; index += 1) {
|
|
110
|
+
if (visible[index]!.some((value, axis) => Math.abs(value - edges[index]![axis]!) > 0.5))
|
|
111
|
+
continue
|
|
112
|
+
corners[index]!.x = Math.max(corners[index]!.x, clip.corners[index]!.x)
|
|
113
|
+
corners[index]!.y = Math.max(corners[index]!.y, clip.corners[index]!.y)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { rect, corners }
|
|
117
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ImageVariants } from '../../image/image.variants'
|
|
2
|
+
import type { Corners } from './clip'
|
|
2
3
|
import { fitSize, rotatedSize, type Size } from './zoom'
|
|
3
4
|
|
|
4
5
|
export interface Rect {
|
|
@@ -21,7 +22,7 @@ export const REST_POSE: Pose = {
|
|
|
21
22
|
y: 0,
|
|
22
23
|
scale: 1,
|
|
23
24
|
rotate: 0,
|
|
24
|
-
clipPath: 'inset(0px 0px 0px 0px round 0px)',
|
|
25
|
+
clipPath: 'inset(0px 0px 0px 0px round 0px 0px 0px 0px / 0px 0px 0px 0px)',
|
|
25
26
|
}
|
|
26
27
|
export const TRAVEL_SPRING = { type: 'spring', visualDuration: 0.3, bounce: 0 } as const
|
|
27
28
|
export const RETURN_SPRING = { ...TRAVEL_SPRING, visualDuration: 0.22 } as const
|
|
@@ -79,23 +80,40 @@ export function openPose(
|
|
|
79
80
|
box: Rect,
|
|
80
81
|
natural: Size,
|
|
81
82
|
fit?: ImageVariants['fit'],
|
|
82
|
-
radius = 0,
|
|
83
|
+
radius: number | Corners = 0,
|
|
84
|
+
clipBox = box,
|
|
83
85
|
): Pose {
|
|
84
86
|
const rendered = renderedRect(box, natural, fit)
|
|
85
87
|
if (frame.width <= 0 || frame.height <= 0 || rendered.width <= 0) return REST_POSE
|
|
86
88
|
const scale = rendered.width / frame.width
|
|
87
|
-
const visible = intersect(box, rendered)
|
|
89
|
+
const visible = intersect(intersect(box, clipBox), rendered)
|
|
88
90
|
const top = (visible.y - rendered.y) / scale
|
|
89
91
|
const left = (visible.x - rendered.x) / scale
|
|
90
92
|
const right = (rendered.x + rendered.width - visible.x - visible.width) / scale
|
|
91
93
|
const bottom = (rendered.y + rendered.height - visible.y - visible.height) / scale
|
|
92
|
-
const
|
|
94
|
+
const corners =
|
|
95
|
+
typeof radius === 'number'
|
|
96
|
+
? Array.from({ length: 4 }, () => ({ x: radius, y: radius }))
|
|
97
|
+
: radius
|
|
98
|
+
const leftEdge = Math.abs(visible.x - clipBox.x) <= 0.5
|
|
99
|
+
const topEdge = Math.abs(visible.y - clipBox.y) <= 0.5
|
|
100
|
+
const rightEdge = Math.abs(visible.x + visible.width - clipBox.x - clipBox.width) <= 0.5
|
|
101
|
+
const bottomEdge = Math.abs(visible.y + visible.height - clipBox.y - clipBox.height) <= 0.5
|
|
102
|
+
const touches = [
|
|
103
|
+
leftEdge && topEdge,
|
|
104
|
+
rightEdge && topEdge,
|
|
105
|
+
rightEdge && bottomEdge,
|
|
106
|
+
leftEdge && bottomEdge,
|
|
107
|
+
]
|
|
108
|
+
const clipped = corners.map((corner, index) => (touches[index] ? corner : { x: 0, y: 0 }))
|
|
109
|
+
const horizontal = clipped.map(corner => px(Math.max(0, corner.x) / scale)).join(' ')
|
|
110
|
+
const vertical = clipped.map(corner => px(Math.max(0, corner.y) / scale)).join(' ')
|
|
93
111
|
return {
|
|
94
112
|
x: rendered.x + rendered.width / 2 - (frame.x + frame.width / 2),
|
|
95
113
|
y: rendered.y + rendered.height / 2 - (frame.y + frame.height / 2),
|
|
96
114
|
scale,
|
|
97
115
|
rotate: 0,
|
|
98
|
-
clipPath: `inset(${px(top)} ${px(right)} ${px(bottom)} ${px(left)} round ${
|
|
116
|
+
clipPath: `inset(${px(top)} ${px(right)} ${px(bottom)} ${px(left)} round ${horizontal} / ${vertical})`,
|
|
99
117
|
}
|
|
100
118
|
}
|
|
101
119
|
|
|
@@ -106,14 +124,6 @@ export function baseScale(frame: Size, stage: Size, rotation: number): number {
|
|
|
106
124
|
return Math.min(stage.width / visual.width, stage.height / visual.height)
|
|
107
125
|
}
|
|
108
126
|
|
|
109
|
-
export function radiusOf(el: Element | null | undefined): number {
|
|
110
|
-
for (let node = el, depth = 0; node && depth < 2; node = node.parentElement, depth += 1) {
|
|
111
|
-
const value = Number.parseFloat(getComputedStyle(node).borderTopLeftRadius)
|
|
112
|
-
if (value > 0) return value
|
|
113
|
-
}
|
|
114
|
-
return 0
|
|
115
|
-
}
|
|
116
|
-
|
|
117
127
|
export function dismissProgress(offsetY: number, stageHeight: number): number {
|
|
118
128
|
if (stageHeight <= 0) return 0
|
|
119
129
|
return Math.min(1, Math.max(0, offsetY) / (stageHeight * DISMISS_TRAVEL))
|
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
|
-
import { PopoverRoot, PopoverTrigger, PopoverPortal,
|
|
3
|
+
import { PopoverRoot, PopoverTrigger, PopoverPortal, type PopoverContentEmits } from 'reka-ui'
|
|
4
|
+
import PopoverContent from './PopoverContent.vue'
|
|
5
|
+
import { useAnchoredOverlay } from '../../lib/anchored-overlay'
|
|
4
6
|
import Card from '../card/Card.vue'
|
|
7
|
+
import { useOverlayPortal } from '../../lib/overlay-portal'
|
|
5
8
|
import { cn } from '../../lib/cn'
|
|
6
9
|
|
|
7
|
-
defineOptions({ name: 'HnPopover' })
|
|
10
|
+
defineOptions({ name: 'HnPopover', inheritAttrs: false })
|
|
8
11
|
|
|
9
12
|
const props = withDefaults(
|
|
10
13
|
defineProps<{
|
|
14
|
+
anchor?: HTMLElement | null
|
|
15
|
+
modal?: boolean
|
|
11
16
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
12
17
|
align?: 'start' | 'center' | 'end'
|
|
13
18
|
sideOffset?: number
|
|
14
19
|
padded?: boolean
|
|
15
20
|
class?: string
|
|
16
21
|
}>(),
|
|
17
|
-
{ side: 'bottom', align: 'center', sideOffset: 8, padded: true },
|
|
22
|
+
{ side: 'bottom', align: 'center', sideOffset: 8, padded: true, modal: true },
|
|
18
23
|
)
|
|
19
24
|
|
|
20
25
|
const open = defineModel<boolean>('open')
|
|
26
|
+
const emit = defineEmits<PopoverContentEmits>()
|
|
27
|
+
defineSlots<{ default?(): unknown; content?(): unknown }>()
|
|
28
|
+
const { trigger, reference, visible, events } = useAnchoredOverlay(props, open, emit, event =>
|
|
29
|
+
emit('openAutoFocus', event),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const { content, present } = useOverlayPortal(visible)
|
|
21
33
|
|
|
22
34
|
const pressOrigin = computed(() => {
|
|
23
35
|
if (props.side === 'left') return 'right'
|
|
@@ -29,18 +41,26 @@
|
|
|
29
41
|
</script>
|
|
30
42
|
|
|
31
43
|
<template>
|
|
32
|
-
<PopoverRoot v-model:open="
|
|
33
|
-
<PopoverTrigger
|
|
44
|
+
<PopoverRoot v-model:open="visible" :modal="props.modal">
|
|
45
|
+
<PopoverTrigger
|
|
46
|
+
v-if="$slots.default"
|
|
47
|
+
:ref="trigger"
|
|
48
|
+
as-child
|
|
49
|
+
:style="{ transformOrigin: pressOrigin }"
|
|
50
|
+
>
|
|
34
51
|
<slot />
|
|
35
52
|
</PopoverTrigger>
|
|
36
|
-
<PopoverPortal>
|
|
53
|
+
<PopoverPortal v-if="present">
|
|
37
54
|
<PopoverContent
|
|
55
|
+
v-bind="{ ...$attrs, ...events }"
|
|
56
|
+
:reference="reference"
|
|
38
57
|
as-child
|
|
39
58
|
:side="props.side"
|
|
40
59
|
:align="props.align"
|
|
41
60
|
:side-offset="props.sideOffset"
|
|
42
61
|
>
|
|
43
62
|
<Card
|
|
63
|
+
ref="content"
|
|
44
64
|
:padded="props.padded"
|
|
45
65
|
:class="cn('hn-anim-pop z-(--hn-z-overlay) max-w-sm shadow-md outline-none', props.class)"
|
|
46
66
|
>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
PopoverContent,
|
|
4
|
+
useForwardPropsEmits,
|
|
5
|
+
type PopoverContentProps,
|
|
6
|
+
type PopoverContentEmits,
|
|
7
|
+
} from 'reka-ui'
|
|
8
|
+
import { usePopoverCloseFocus } from './composables/usePopoverCloseFocus'
|
|
9
|
+
|
|
10
|
+
defineOptions({ name: 'HnPopoverContent' })
|
|
11
|
+
|
|
12
|
+
const props = defineProps<PopoverContentProps>()
|
|
13
|
+
const emit = defineEmits<PopoverContentEmits>()
|
|
14
|
+
const forwarded = useForwardPropsEmits(props, emit)
|
|
15
|
+
const closeAutoFocus = usePopoverCloseFocus(emit)
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<PopoverContent
|
|
20
|
+
v-bind="{ ...forwarded, onCloseAutoFocus: closeAutoFocus }"
|
|
21
|
+
:reference="props.reference"
|
|
22
|
+
>
|
|
23
|
+
<slot />
|
|
24
|
+
</PopoverContent>
|
|
25
|
+
</template>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { nextTick } from 'vue'
|
|
2
|
+
import { injectPopoverRootContext } from 'reka-ui'
|
|
3
|
+
|
|
4
|
+
export function usePopoverCloseFocus(emit: (event: 'closeAutoFocus', value: Event) => void) {
|
|
5
|
+
const root = injectPopoverRootContext()
|
|
6
|
+
|
|
7
|
+
return (event: Event) => {
|
|
8
|
+
emit('closeAutoFocus', event)
|
|
9
|
+
if (!event.defaultPrevented || !root.triggerElement.value) return
|
|
10
|
+
|
|
11
|
+
const trigger = root.triggerElement.value
|
|
12
|
+
root.triggerElement.value = undefined
|
|
13
|
+
nextTick(() => {
|
|
14
|
+
root.triggerElement.value ??= trigger
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { useEdgeShadow } from './composables/useEdgeShadow'
|
|
7
7
|
import { useWheelRedirect } from './composables/useWheelRedirect'
|
|
8
8
|
import { useLayerLock } from './composables/useLayerLock'
|
|
9
|
+
import { useScrollFocus } from './composables/useScrollFocus'
|
|
9
10
|
|
|
10
11
|
defineOptions({ name: 'HnScrollArea', inheritAttrs: false })
|
|
11
12
|
|
|
@@ -59,6 +60,10 @@
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
const { viewport, instance, onEvent } = useOverlayScrollbars(host, content, options)
|
|
63
|
+
const { hostFocus, viewportFocus } = useScrollFocus(host, viewport, () => ({
|
|
64
|
+
focusable: props.focusable,
|
|
65
|
+
label: props.label ?? t.value.scroll.regionLabel,
|
|
66
|
+
}))
|
|
62
67
|
const { showXStart, showXEnd, showYStart, showYEnd, updateEdges } = useEdgeShadow(
|
|
63
68
|
viewport,
|
|
64
69
|
() => ({ direction: props.direction, shadow: props.shadow }),
|
|
@@ -83,14 +88,11 @@
|
|
|
83
88
|
<div :class="cn('hn-scroll-area relative flex flex-col overflow-hidden', props.class)">
|
|
84
89
|
<div
|
|
85
90
|
ref="host"
|
|
86
|
-
v-bind="
|
|
91
|
+
v-bind="{ ...$attrs, ...hostFocus }"
|
|
87
92
|
data-overlayscrollbars-initialize
|
|
88
|
-
:tabindex="props.focusable ? 0 : undefined"
|
|
89
|
-
:role="props.focusable ? 'region' : undefined"
|
|
90
|
-
:aria-label="props.focusable ? (props.label ?? t.scroll.regionLabel) : undefined"
|
|
91
93
|
class="w-full min-h-0 grow"
|
|
92
94
|
>
|
|
93
|
-
<div ref="content" data-overlayscrollbars-contents>
|
|
95
|
+
<div ref="content" v-bind="viewportFocus" data-overlayscrollbars-contents>
|
|
94
96
|
<slot />
|
|
95
97
|
</div>
|
|
96
98
|
</div>
|
|
@@ -13,10 +13,6 @@ const useBodyPointerLock = createSharedComposable(() => {
|
|
|
13
13
|
return locked
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
function block(event: Event) {
|
|
17
|
-
event.preventDefault()
|
|
18
|
-
}
|
|
19
|
-
|
|
20
16
|
export function useLayerLock(viewport: ShallowRef<HTMLElement | undefined>) {
|
|
21
17
|
const locked = useBodyPointerLock()
|
|
22
18
|
let release: (() => void) | undefined
|
|
@@ -25,12 +21,15 @@ export function useLayerLock(viewport: ShallowRef<HTMLElement | undefined>) {
|
|
|
25
21
|
release?.()
|
|
26
22
|
release = undefined
|
|
27
23
|
const el = viewport.value
|
|
28
|
-
if (!el || !locked.value
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
if (!el || !locked.value) return
|
|
25
|
+
const block = (event: Event) => {
|
|
26
|
+
if (getComputedStyle(el).pointerEvents === 'none') event.preventDefault()
|
|
27
|
+
}
|
|
28
|
+
el.addEventListener('wheel', block, { passive: false, capture: true })
|
|
29
|
+
el.addEventListener('touchmove', block, { passive: false, capture: true })
|
|
31
30
|
release = () => {
|
|
32
|
-
el.removeEventListener('wheel', block)
|
|
33
|
-
el.removeEventListener('touchmove', block)
|
|
31
|
+
el.removeEventListener('wheel', block, true)
|
|
32
|
+
el.removeEventListener('touchmove', block, true)
|
|
34
33
|
}
|
|
35
34
|
}
|
|
36
35
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { computed, nextTick, watch, type ShallowRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
export function useScrollFocus(
|
|
4
|
+
host: ShallowRef<HTMLElement | undefined>,
|
|
5
|
+
viewport: ShallowRef<HTMLElement | undefined>,
|
|
6
|
+
options: () => { focusable: boolean; label: string },
|
|
7
|
+
) {
|
|
8
|
+
function attributes(enabled: boolean) {
|
|
9
|
+
const { focusable, label } = options()
|
|
10
|
+
return {
|
|
11
|
+
tabindex: enabled && focusable ? 0 : undefined,
|
|
12
|
+
role: enabled && focusable ? 'region' : undefined,
|
|
13
|
+
'aria-label': enabled && focusable ? label : undefined,
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const hostFocus = computed(() => attributes(!viewport.value))
|
|
18
|
+
const viewportFocus = computed(() => attributes(!!viewport.value))
|
|
19
|
+
|
|
20
|
+
watch(
|
|
21
|
+
viewport,
|
|
22
|
+
async element => {
|
|
23
|
+
const target = host.value
|
|
24
|
+
if (!element || !target || !options().focusable) return
|
|
25
|
+
if (target.ownerDocument.activeElement !== target) return
|
|
26
|
+
await nextTick()
|
|
27
|
+
const active = target.ownerDocument.activeElement
|
|
28
|
+
if (
|
|
29
|
+
options().focusable &&
|
|
30
|
+
element.isConnected &&
|
|
31
|
+
(active === target || active === target.ownerDocument.body)
|
|
32
|
+
)
|
|
33
|
+
element.focus({ preventScroll: true })
|
|
34
|
+
},
|
|
35
|
+
{ flush: 'sync' },
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return { hostFocus, viewportFocus }
|
|
39
|
+
}
|
|
@@ -5,7 +5,7 @@ export function useWheelRedirect(
|
|
|
5
5
|
enabled: () => boolean,
|
|
6
6
|
) {
|
|
7
7
|
function onWheel(event: WheelEvent) {
|
|
8
|
-
if (!enabled()) return
|
|
8
|
+
if (event.defaultPrevented || !enabled()) return
|
|
9
9
|
const el = viewport.value
|
|
10
10
|
if (!el) return
|
|
11
11
|
if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return
|
|
@@ -71,7 +71,12 @@
|
|
|
71
71
|
:aria-label="slots.option ? option.label : undefined"
|
|
72
72
|
:class="segmentedItem({ size: props.size, block: props.block })"
|
|
73
73
|
>
|
|
74
|
-
<Highlight
|
|
74
|
+
<Highlight
|
|
75
|
+
v-if="option.value === current"
|
|
76
|
+
:id="highlightId"
|
|
77
|
+
:axis="props.orientation === 'vertical' ? 'y' : 'x'"
|
|
78
|
+
:class="segmentedThumb()"
|
|
79
|
+
/>
|
|
75
80
|
<Ripple />
|
|
76
81
|
<slot name="option" :option="option">{{ option.label }}</slot>
|
|
77
82
|
</ToggleGroupItem>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
The skeleton shimmer styles and light/dark opacity values are adapted from PrimeVue / PrimeUIX
|
|
2
|
+
(https://github.com/primefaces/primeuix), using Hina design tokens.
|
|
3
|
+
|
|
4
|
+
MIT License
|
|
5
|
+
|
|
6
|
+
Copyright (c) 2025 PrimeTek
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
in the Software without restriction, including without limitation the rights
|
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
furnished to do so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
SOFTWARE.
|
|
@@ -26,7 +26,12 @@
|
|
|
26
26
|
:disabled="props.disabled"
|
|
27
27
|
:class="cn(tabsTrigger({ variant, size, orientation }), props.class)"
|
|
28
28
|
>
|
|
29
|
-
<Highlight
|
|
29
|
+
<Highlight
|
|
30
|
+
v-if="active"
|
|
31
|
+
:id="highlightId"
|
|
32
|
+
:axis="orientation === 'vertical' ? 'y' : 'x'"
|
|
33
|
+
:class="tabsHighlight({ variant, orientation })"
|
|
34
|
+
/>
|
|
30
35
|
<Ripple />
|
|
31
36
|
<slot />
|
|
32
37
|
</TabsTrigger>
|
package/src/index.ts
CHANGED
|
@@ -227,6 +227,8 @@ export { default as Spoiler } from './components/spoiler/Spoiler.vue'
|
|
|
227
227
|
export { default as Skeleton } from './components/skeleton/Skeleton.vue'
|
|
228
228
|
export { default as Image } from './components/image/Image.vue'
|
|
229
229
|
export { default as ImageGroup } from './components/image/ImageGroup.vue'
|
|
230
|
+
export { default as Lightbox } from './components/lightbox/Lightbox.vue'
|
|
231
|
+
export type { LightboxItem } from './components/lightbox/types'
|
|
230
232
|
export { provideImageResolver, useImageResolver } from './components/image/resolver'
|
|
231
233
|
export type { ImageResolver } from './components/image/resolver'
|
|
232
234
|
export type { ImageVariants } from './components/image/image.variants'
|