@at-flux/astroflare 1.0.9 → 1.0.10
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/package.json +7 -7
- package/src/components/ImageFade.astro +172 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@at-flux/astroflare",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Reusable headless components, styles, and utilities for Astro + Tailwind v4 + Cloudflare projects",
|
|
6
6
|
"author": "atflux <dev@atflux.uk>",
|
|
@@ -62,14 +62,14 @@
|
|
|
62
62
|
"README.md"
|
|
63
63
|
],
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@astrojs/node": "
|
|
66
|
-
"jsdom": "
|
|
67
|
-
"tsdown": "
|
|
68
|
-
"typescript": "
|
|
69
|
-
"vitest": "
|
|
65
|
+
"@astrojs/node": "9.5.5",
|
|
66
|
+
"jsdom": "25.0.1",
|
|
67
|
+
"tsdown": "0.21.8",
|
|
68
|
+
"typescript": "5.9.3",
|
|
69
|
+
"vitest": "3.2.4"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"resend": "
|
|
72
|
+
"resend": "4.8.0"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
75
|
"astro": ">=5.0.0"
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ImageFade — generic image loading wrapper.
|
|
4
|
+
*
|
|
5
|
+
* Wraps any slotted `<img>`, `<picture>`, or Astro `<Image />` (which renders
|
|
6
|
+
* an `<img>`). Shows a spinner (or shimmer skeleton) placeholder until the
|
|
7
|
+
* image finishes loading, then fades the image in and the placeholder out.
|
|
8
|
+
* Cached/already-complete images resolve instantly (no flash).
|
|
9
|
+
*
|
|
10
|
+
* Reserve layout space with `aspectRatio` to avoid CLS. Styling is theme-aware
|
|
11
|
+
* via `--color-brand` (override with `spinnerColor`).
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* <ImageFade aspectRatio="16/9" rounded="rounded-2xl">
|
|
15
|
+
* <Image src={hero} alt="…" />
|
|
16
|
+
* </ImageFade>
|
|
17
|
+
*/
|
|
18
|
+
interface Props {
|
|
19
|
+
/** Extra classes merged onto the wrapper. */
|
|
20
|
+
class?: string;
|
|
21
|
+
/** Radius class/utility applied to the wrapper (clips the image), e.g. `rounded-2xl`. */
|
|
22
|
+
rounded?: string;
|
|
23
|
+
/** CSS `aspect-ratio` to reserve space and prevent layout shift, e.g. `"16/9"`, `"1"`. */
|
|
24
|
+
aspectRatio?: string;
|
|
25
|
+
/** Fade-in duration in ms. */
|
|
26
|
+
duration?: number;
|
|
27
|
+
/** Spinner/skeleton accent colour (defaults to `--color-brand`). */
|
|
28
|
+
spinnerColor?: string;
|
|
29
|
+
/** Show a shimmer skeleton instead of a spinner. */
|
|
30
|
+
skeleton?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
class: className = '',
|
|
35
|
+
rounded = '',
|
|
36
|
+
aspectRatio,
|
|
37
|
+
duration = 500,
|
|
38
|
+
spinnerColor,
|
|
39
|
+
skeleton = false,
|
|
40
|
+
} = Astro.props;
|
|
41
|
+
|
|
42
|
+
const style = [
|
|
43
|
+
aspectRatio ? `aspect-ratio:${aspectRatio}` : '',
|
|
44
|
+
spinnerColor ? `--if-accent:${spinnerColor}` : '',
|
|
45
|
+
`--if-duration:${duration}ms`,
|
|
46
|
+
]
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.join(';');
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
<image-fade class:list={['image-fade', skeleton && 'is-skeleton', rounded, className]} style={style}>
|
|
52
|
+
<span class='image-fade__placeholder' aria-hidden='true'>
|
|
53
|
+
{!skeleton && <span class='image-fade__ring'></span>}
|
|
54
|
+
</span>
|
|
55
|
+
<slot />
|
|
56
|
+
</image-fade>
|
|
57
|
+
|
|
58
|
+
<style is:global>
|
|
59
|
+
image-fade.image-fade {
|
|
60
|
+
position: relative;
|
|
61
|
+
display: block;
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
background: color-mix(in oklab, var(--if-accent, var(--color-brand, #7c5cff)) 8%, transparent);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Slotted image starts hidden, fades in once loaded */
|
|
67
|
+
image-fade.image-fade > img,
|
|
68
|
+
image-fade.image-fade > picture > img {
|
|
69
|
+
display: block;
|
|
70
|
+
width: 100%;
|
|
71
|
+
height: 100%;
|
|
72
|
+
object-fit: cover;
|
|
73
|
+
opacity: 0;
|
|
74
|
+
transition: opacity var(--if-duration, 500ms) ease-out;
|
|
75
|
+
}
|
|
76
|
+
image-fade.image-fade.is-loaded > img,
|
|
77
|
+
image-fade.image-fade.is-loaded > picture > img {
|
|
78
|
+
opacity: 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.image-fade__placeholder {
|
|
82
|
+
position: absolute;
|
|
83
|
+
inset: 0;
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
opacity: 1;
|
|
88
|
+
transition: opacity var(--if-duration, 500ms) ease-out;
|
|
89
|
+
pointer-events: none;
|
|
90
|
+
}
|
|
91
|
+
image-fade.image-fade.is-loaded .image-fade__placeholder {
|
|
92
|
+
opacity: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.image-fade__ring {
|
|
96
|
+
width: 2.5rem;
|
|
97
|
+
height: 2.5rem;
|
|
98
|
+
border: 3px solid transparent;
|
|
99
|
+
border-top-color: var(--if-accent, var(--color-brand, #7c5cff));
|
|
100
|
+
border-right-color: color-mix(
|
|
101
|
+
in oklab,
|
|
102
|
+
var(--if-accent, var(--color-brand, #7c5cff)) 50%,
|
|
103
|
+
transparent
|
|
104
|
+
);
|
|
105
|
+
border-radius: 50%;
|
|
106
|
+
animation: image-fade-spin 0.9s linear infinite;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Skeleton variant: shimmer sweep instead of a spinner */
|
|
110
|
+
image-fade.image-fade.is-skeleton .image-fade__placeholder {
|
|
111
|
+
background: linear-gradient(
|
|
112
|
+
100deg,
|
|
113
|
+
color-mix(in oklab, var(--if-accent, var(--color-brand, #7c5cff)) 6%, transparent) 30%,
|
|
114
|
+
color-mix(in oklab, var(--if-accent, var(--color-brand, #7c5cff)) 16%, transparent) 50%,
|
|
115
|
+
color-mix(in oklab, var(--if-accent, var(--color-brand, #7c5cff)) 6%, transparent) 70%
|
|
116
|
+
);
|
|
117
|
+
background-size: 200% 100%;
|
|
118
|
+
animation: image-fade-shimmer 1.4s ease-in-out infinite;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@keyframes image-fade-spin {
|
|
122
|
+
to {
|
|
123
|
+
transform: rotate(360deg);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
@keyframes image-fade-shimmer {
|
|
127
|
+
from {
|
|
128
|
+
background-position: 200% 0;
|
|
129
|
+
}
|
|
130
|
+
to {
|
|
131
|
+
background-position: -200% 0;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@media (prefers-reduced-motion: reduce) {
|
|
136
|
+
image-fade.image-fade > img,
|
|
137
|
+
image-fade.image-fade > picture > img,
|
|
138
|
+
.image-fade__placeholder {
|
|
139
|
+
transition: none;
|
|
140
|
+
}
|
|
141
|
+
.image-fade__ring {
|
|
142
|
+
animation: none;
|
|
143
|
+
}
|
|
144
|
+
image-fade.image-fade.is-skeleton .image-fade__placeholder {
|
|
145
|
+
animation: none;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
</style>
|
|
149
|
+
|
|
150
|
+
<script>
|
|
151
|
+
class ImageFade extends HTMLElement {
|
|
152
|
+
connectedCallback() {
|
|
153
|
+
const img = this.querySelector('img');
|
|
154
|
+
if (!img) {
|
|
155
|
+
this.classList.add('is-loaded');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const done = () => this.classList.add('is-loaded');
|
|
159
|
+
// Already cached / decoded
|
|
160
|
+
if (img.complete && img.naturalWidth > 0) {
|
|
161
|
+
done();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
img.addEventListener('load', done, { once: true });
|
|
165
|
+
// On error, still reveal (broken-image icon beats a permanent spinner)
|
|
166
|
+
img.addEventListener('error', done, { once: true });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!customElements.get('image-fade')) {
|
|
170
|
+
customElements.define('image-fade', ImageFade);
|
|
171
|
+
}
|
|
172
|
+
</script>
|