@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.
Files changed (54) hide show
  1. package/dist/index.js +276 -54
  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/image/Image.vue.d.ts +5 -2
  12. package/dist/types/components/image/Image.vue.d.ts.map +1 -1
  13. package/dist/types/components/image/composables/useImage.d.ts.map +1 -1
  14. package/dist/types/components/image/image.variants.d.ts +23 -1
  15. package/dist/types/components/image/image.variants.d.ts.map +1 -1
  16. package/dist/types/components/lightbox/composables/useLightboxMotion.d.ts.map +1 -1
  17. package/dist/types/components/lightbox/composables/useLightboxPose.d.ts.map +1 -1
  18. package/dist/types/components/lightbox/utils/clip.d.ts +16 -0
  19. package/dist/types/components/lightbox/utils/clip.d.ts.map +1 -0
  20. package/dist/types/components/lightbox/utils/pose.d.ts +2 -2
  21. package/dist/types/components/lightbox/utils/pose.d.ts.map +1 -1
  22. package/dist/types/components/popover/Popover.vue.d.ts.map +1 -1
  23. package/dist/types/components/scroll-area/composables/useLayerLock.d.ts.map +1 -1
  24. package/dist/types/index.d.ts +2 -0
  25. package/dist/types/index.d.ts.map +1 -1
  26. package/dist/types/lib/anchored-overlay.d.ts +3 -7
  27. package/dist/types/lib/anchored-overlay.d.ts.map +1 -1
  28. package/dist/types/lib/dialog-focus.d.ts +2 -0
  29. package/dist/types/lib/dialog-focus.d.ts.map +1 -0
  30. package/dist/types/lib/overlay-portal.d.ts +6 -0
  31. package/dist/types/lib/overlay-portal.d.ts.map +1 -0
  32. package/package.json +1 -1
  33. package/src/components/alert-dialog/AlertDialog.vue +7 -4
  34. package/src/components/dialog/Dialog.vue +7 -4
  35. package/src/components/dialog/ModalContent.vue +19 -0
  36. package/src/components/dropdown-menu/DropdownMenu.vue +1 -1
  37. package/src/components/image/Image.vue +9 -12
  38. package/src/components/image/composables/useImage.ts +6 -3
  39. package/src/components/image/image.variants.ts +13 -1
  40. package/src/components/lightbox/composables/useLightboxMotion.ts +3 -1
  41. package/src/components/lightbox/composables/useLightboxPose.ts +5 -2
  42. package/src/components/lightbox/utils/clip.ts +117 -0
  43. package/src/components/lightbox/utils/pose.ts +23 -13
  44. package/src/components/popover/Popover.vue +6 -2
  45. package/src/components/scroll-area/composables/useLayerLock.ts +8 -9
  46. package/src/components/scroll-area/composables/useWheelRedirect.ts +1 -1
  47. package/src/components/skeleton/LICENSE +24 -0
  48. package/src/index.ts +2 -0
  49. package/src/lib/anchored-overlay.ts +2 -5
  50. package/src/lib/dialog-focus.ts +13 -0
  51. package/src/lib/overlay-portal.ts +8 -0
  52. package/src/styles/interaction.css +26 -3
  53. package/src/styles/motion.css +14 -2
  54. 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 ? 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,5 +1,6 @@
1
1
  import type { useLightboxFrames } from './useLightboxFrames'
2
- import { openPose, radiusOf, type Pose, type Rect } from '../utils/pose'
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
- return openPose(frames.frameOf(item), box, natural, item?.fit, radiusOf(item?.source?.()))
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 corner = Math.max(0, radius) / scale
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 ${px(corner)})`,
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 || getComputedStyle(el).pointerEvents !== 'none') return
29
- el.addEventListener('wheel', block, { passive: false })
30
- el.addEventListener('touchmove', block, { passive: false })
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 { DropdownMenuContentEmits } from 'reka-ui'
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 trigger = shallowRef<{ $el: HTMLElement } | null>(null)
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
- background-color: var(--hn-bg-subtle);
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
- animation: hn-skeleton var(--hn-skeleton-duration) var(--hn-ease-move) infinite;
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
- animation: none;
302
+ &::after,
303
+ &:dir(rtl)::after {
304
+ animation: none;
305
+ }
283
306
  }
284
307
  }
285
308
 
@@ -5,8 +5,20 @@
5
5
  }
6
6
 
7
7
  @keyframes hn-skeleton {
8
- 50% {
9
- background-color: var(--hn-bg-inset);
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
 
@@ -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: 2200ms;
209
+ --hn-skeleton-duration: 1200ms;
206
210
  --hn-ripple-in: 75ms;
207
211
  --hn-ripple-out: 375ms;
208
212
  --hn-pulse-duration: 1500ms;