@hina-ui/vue 1.3.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 +276 -54
- package/dist/index.js.map +1 -1
- package/dist/types/components/alert-dialog/AlertDialog.vue.d.ts +2 -2
- package/dist/types/components/alert-dialog/AlertDialog.vue.d.ts.map +1 -1
- 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.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/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/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.map +1 -1
- package/dist/types/components/scroll-area/composables/useLayerLock.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 +3 -7
- package/dist/types/lib/anchored-overlay.d.ts.map +1 -1
- 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 +7 -4
- package/src/components/dialog/Dialog.vue +7 -4
- package/src/components/dialog/ModalContent.vue +19 -0
- package/src/components/dropdown-menu/DropdownMenu.vue +1 -1
- 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/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 +6 -2
- package/src/components/scroll-area/composables/useLayerLock.ts +8 -9
- package/src/components/scroll-area/composables/useWheelRedirect.ts +1 -1
- package/src/components/skeleton/LICENSE +24 -0
- package/src/index.ts +2 -0
- package/src/lib/anchored-overlay.ts +2 -5
- 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
|
@@ -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))
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import PopoverContent from './PopoverContent.vue'
|
|
5
5
|
import { useAnchoredOverlay } from '../../lib/anchored-overlay'
|
|
6
6
|
import Card from '../card/Card.vue'
|
|
7
|
+
import { useOverlayPortal } from '../../lib/overlay-portal'
|
|
7
8
|
import { cn } from '../../lib/cn'
|
|
8
9
|
|
|
9
10
|
defineOptions({ name: 'HnPopover', inheritAttrs: false })
|
|
@@ -28,6 +29,8 @@
|
|
|
28
29
|
emit('openAutoFocus', event),
|
|
29
30
|
)
|
|
30
31
|
|
|
32
|
+
const { content, present } = useOverlayPortal(visible)
|
|
33
|
+
|
|
31
34
|
const pressOrigin = computed(() => {
|
|
32
35
|
if (props.side === 'left') return 'right'
|
|
33
36
|
if (props.side === 'right') return 'left'
|
|
@@ -41,13 +44,13 @@
|
|
|
41
44
|
<PopoverRoot v-model:open="visible" :modal="props.modal">
|
|
42
45
|
<PopoverTrigger
|
|
43
46
|
v-if="$slots.default"
|
|
44
|
-
ref="trigger"
|
|
47
|
+
:ref="trigger"
|
|
45
48
|
as-child
|
|
46
49
|
:style="{ transformOrigin: pressOrigin }"
|
|
47
50
|
>
|
|
48
51
|
<slot />
|
|
49
52
|
</PopoverTrigger>
|
|
50
|
-
<PopoverPortal>
|
|
53
|
+
<PopoverPortal v-if="present">
|
|
51
54
|
<PopoverContent
|
|
52
55
|
v-bind="{ ...$attrs, ...events }"
|
|
53
56
|
:reference="reference"
|
|
@@ -57,6 +60,7 @@
|
|
|
57
60
|
:side-offset="props.sideOffset"
|
|
58
61
|
>
|
|
59
62
|
<Card
|
|
63
|
+
ref="content"
|
|
60
64
|
:padded="props.padded"
|
|
61
65
|
:class="cn('hn-anim-pop z-(--hn-z-overlay) max-w-sm shadow-md outline-none', props.class)"
|
|
62
66
|
>
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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.
|
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'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { computed, nextTick, shallowRef, watch, type Ref } from 'vue'
|
|
2
|
-
import type
|
|
2
|
+
import { useForwardExpose, type DropdownMenuContentEmits } from 'reka-ui'
|
|
3
3
|
|
|
4
4
|
export function useAnchoredOverlay(
|
|
5
5
|
props: { anchor?: HTMLElement | null; modal: boolean },
|
|
@@ -10,10 +10,7 @@ export function useAnchoredOverlay(
|
|
|
10
10
|
) => void,
|
|
11
11
|
openAutoFocus?: (event: Event) => void,
|
|
12
12
|
) {
|
|
13
|
-
const
|
|
14
|
-
const triggerElement = computed(() =>
|
|
15
|
-
trigger.value?.$el?.nodeType === 1 ? trigger.value.$el : undefined,
|
|
16
|
-
)
|
|
13
|
+
const { forwardRef: trigger, currentElement: triggerElement } = useForwardExpose()
|
|
17
14
|
const retainedAnchor = shallowRef<HTMLElement>()
|
|
18
15
|
const reference = computed(
|
|
19
16
|
() =>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { injectDialogRootContext } from 'reka-ui'
|
|
2
|
+
|
|
3
|
+
export function useDialogCloseFocus(onCloseAutoFocus: (event: Event) => void) {
|
|
4
|
+
const context = injectDialogRootContext()
|
|
5
|
+
|
|
6
|
+
return (event: Event) => {
|
|
7
|
+
onCloseAutoFocus(event)
|
|
8
|
+
if (event.defaultPrevented) return
|
|
9
|
+
event.preventDefault()
|
|
10
|
+
const target = context.triggerElement.value
|
|
11
|
+
if (target?.isConnected) target.focus({ preventScroll: true })
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { computed, shallowRef, type ComponentPublicInstance, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export function useOverlayPortal(open: Ref<boolean | undefined>) {
|
|
4
|
+
const content = shallowRef<ComponentPublicInstance | null>(null)
|
|
5
|
+
const present = computed(() => !!open.value || !!content.value)
|
|
6
|
+
|
|
7
|
+
return { content, present }
|
|
8
|
+
}
|
|
@@ -266,20 +266,43 @@
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
@utility hn-skeleton {
|
|
269
|
-
|
|
269
|
+
position: relative;
|
|
270
|
+
overflow: hidden;
|
|
271
|
+
background-color: var(--hn-skeleton-bg);
|
|
270
272
|
border-radius: var(--hn-radius-md);
|
|
271
273
|
color: transparent;
|
|
272
274
|
cursor: default;
|
|
273
275
|
pointer-events: none;
|
|
274
276
|
user-select: none;
|
|
275
|
-
|
|
277
|
+
|
|
278
|
+
&::after {
|
|
279
|
+
content: '';
|
|
280
|
+
position: absolute;
|
|
281
|
+
inset: 0;
|
|
282
|
+
z-index: 1;
|
|
283
|
+
transform: translateX(-100%);
|
|
284
|
+
background: linear-gradient(
|
|
285
|
+
90deg,
|
|
286
|
+
rgb(255 255 255 / 0),
|
|
287
|
+
var(--hn-skeleton-wave),
|
|
288
|
+
rgb(255 255 255 / 0)
|
|
289
|
+
);
|
|
290
|
+
animation: hn-skeleton var(--hn-skeleton-duration) var(--hn-ease-move) infinite;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
&:dir(rtl)::after {
|
|
294
|
+
animation-name: hn-skeleton-rtl;
|
|
295
|
+
}
|
|
276
296
|
|
|
277
297
|
& * {
|
|
278
298
|
visibility: hidden;
|
|
279
299
|
}
|
|
280
300
|
|
|
281
301
|
@media (prefers-reduced-motion: reduce) {
|
|
282
|
-
|
|
302
|
+
&::after,
|
|
303
|
+
&:dir(rtl)::after {
|
|
304
|
+
animation: none;
|
|
305
|
+
}
|
|
283
306
|
}
|
|
284
307
|
}
|
|
285
308
|
|
package/src/styles/motion.css
CHANGED
|
@@ -5,8 +5,20 @@
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
@keyframes hn-skeleton {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
from {
|
|
9
|
+
transform: translateX(-100%);
|
|
10
|
+
}
|
|
11
|
+
to {
|
|
12
|
+
transform: translateX(100%);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@keyframes hn-skeleton-rtl {
|
|
17
|
+
from {
|
|
18
|
+
transform: translateX(100%);
|
|
19
|
+
}
|
|
20
|
+
to {
|
|
21
|
+
transform: translateX(-100%);
|
|
10
22
|
}
|
|
11
23
|
}
|
|
12
24
|
|
package/src/styles/semantic.css
CHANGED
|
@@ -72,6 +72,8 @@
|
|
|
72
72
|
0 40px 80px -24px rgb(0 0 0 / 0.16);
|
|
73
73
|
|
|
74
74
|
--hn-scroll-shadow: rgb(0 0 0 / 0.1);
|
|
75
|
+
--hn-skeleton-bg: var(--color-neutral-200);
|
|
76
|
+
--hn-skeleton-wave: rgb(255 255 255 / 0.4);
|
|
75
77
|
|
|
76
78
|
--hn-z-overlay: 100;
|
|
77
79
|
--hn-z-toast: 110;
|
|
@@ -146,6 +148,8 @@
|
|
|
146
148
|
0 32px 64px -16px rgb(0 0 0 / 0.55);
|
|
147
149
|
|
|
148
150
|
--hn-scroll-shadow: rgb(0 0 0 / 0.5);
|
|
151
|
+
--hn-skeleton-bg: rgb(255 255 255 / 0.06);
|
|
152
|
+
--hn-skeleton-wave: rgb(255 255 255 / 0.04);
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
:root {
|
|
@@ -202,7 +206,7 @@
|
|
|
202
206
|
--hn-travel-md: 10px;
|
|
203
207
|
--hn-stagger: 24ms;
|
|
204
208
|
--hn-spin-duration: 700ms;
|
|
205
|
-
--hn-skeleton-duration:
|
|
209
|
+
--hn-skeleton-duration: 1200ms;
|
|
206
210
|
--hn-ripple-in: 75ms;
|
|
207
211
|
--hn-ripple-out: 375ms;
|
|
208
212
|
--hn-pulse-duration: 1500ms;
|