@at-flux/astroflare 1.0.11 → 1.0.13

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@at-flux/astroflare",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
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>",
@@ -14,10 +14,20 @@
14
14
  * choose its `object-fit` with your own classes, exactly as you would without
15
15
  * this wrapper.
16
16
  *
17
+ * The built-in placeholder is a spinner (or a shimmer skeleton). To use your own
18
+ * loading indicator instead, fill the `placeholder` slot — it is centred in the
19
+ * frame and faded out on load exactly like the built-in one.
20
+ *
17
21
  * @example
18
22
  * <ImageFade aspectRatio="16/9" rounded="rounded-2xl">
19
23
  * <Image src={hero} alt="…" />
20
24
  * </ImageFade>
25
+ *
26
+ * @example Custom indicator
27
+ * <ImageFade aspectRatio="16/9">
28
+ * <MySpinner slot="placeholder" />
29
+ * <Image src={hero} alt="…" />
30
+ * </ImageFade>
21
31
  */
22
32
  interface Props {
23
33
  /** Extra classes merged onto the wrapper. */
@@ -30,6 +40,12 @@ interface Props {
30
40
  duration?: number;
31
41
  /** Spinner/skeleton accent colour (defaults to `--color-brand`). */
32
42
  spinnerColor?: string;
43
+ /** Second accent, used by the counter-rotating inner ring (defaults to `spinnerColor`). */
44
+ spinnerColorSecondary?: string;
45
+ /** Outer ring diameter, any CSS length. */
46
+ spinnerSize?: string;
47
+ /** Wrapper background shown behind the image. Pass `"transparent"` for `object-contain` images. */
48
+ background?: string;
33
49
  /** Show a shimmer skeleton instead of a spinner. */
34
50
  skeleton?: boolean;
35
51
  }
@@ -40,12 +56,18 @@ const {
40
56
  aspectRatio,
41
57
  duration = 500,
42
58
  spinnerColor,
59
+ spinnerColorSecondary,
60
+ spinnerSize,
61
+ background,
43
62
  skeleton = false,
44
63
  } = Astro.props;
45
64
 
46
65
  const style = [
47
66
  aspectRatio ? `aspect-ratio:${aspectRatio}` : '',
48
67
  spinnerColor ? `--if-accent:${spinnerColor}` : '',
68
+ spinnerColorSecondary ? `--if-accent-2:${spinnerColorSecondary}` : '',
69
+ spinnerSize ? `--if-size:${spinnerSize}` : '',
70
+ background ? `--if-bg:${background}` : '',
49
71
  `--if-duration:${duration}ms`,
50
72
  ]
51
73
  .filter(Boolean)
@@ -54,7 +76,15 @@ const style = [
54
76
 
55
77
  <image-fade class:list={['image-fade', skeleton && 'is-skeleton', rounded, className]} style={style}>
56
78
  <span class='image-fade__placeholder' aria-hidden='true'>
57
- {!skeleton && <span class='image-fade__ring'></span>}
79
+ <slot name='placeholder'>
80
+ {
81
+ !skeleton && (
82
+ <span class='image-fade__ring'>
83
+ <span class='image-fade__ring-inner' />
84
+ </span>
85
+ )
86
+ }
87
+ </slot>
58
88
  </span>
59
89
  <slot />
60
90
  </image-fade>
@@ -64,7 +94,10 @@ const style = [
64
94
  position: relative;
65
95
  display: block;
66
96
  overflow: hidden;
67
- background: color-mix(in oklab, var(--if-accent, var(--color-brand, #7c5cff)) 8%, transparent);
97
+ background: var(
98
+ --if-bg,
99
+ color-mix(in oklab, var(--if-accent, var(--color-brand, #7c5cff)) 8%, transparent)
100
+ );
68
101
  }
69
102
 
70
103
  /* Slotted image starts hidden, fades in once loaded.
@@ -96,8 +129,10 @@ const style = [
96
129
  }
97
130
 
98
131
  .image-fade__ring {
99
- width: 2.5rem;
100
- height: 2.5rem;
132
+ position: relative;
133
+ display: block;
134
+ width: var(--if-size, 2.5rem);
135
+ height: var(--if-size, 2.5rem);
101
136
  border: 3px solid transparent;
102
137
  border-top-color: var(--if-accent, var(--color-brand, #7c5cff));
103
138
  border-right-color: color-mix(
@@ -106,7 +141,28 @@ const style = [
106
141
  transparent
107
142
  );
108
143
  border-radius: 50%;
109
- animation: image-fade-spin 0.9s linear infinite;
144
+ /* `transform` is reset explicitly: a host page may set a base transform on
145
+ every element (e.g. a `* { transform: translateZ(0) }` GPU hint), which
146
+ would otherwise make the keyframes interpolate as matrices and cancel the
147
+ full turn out to no visible rotation. */
148
+ transform: rotate(0deg);
149
+ animation: image-fade-spin 1.2s linear infinite;
150
+ }
151
+
152
+ .image-fade__ring-inner {
153
+ position: absolute;
154
+ top: 50%;
155
+ left: 50%;
156
+ display: block;
157
+ width: 60%;
158
+ height: 60%;
159
+ margin: -30% 0 0 -30%;
160
+ border: 2px solid transparent;
161
+ border-top-color: var(--if-accent-2, var(--if-accent, var(--color-brand, #7c5cff)));
162
+ border-right-color: var(--if-accent-2, var(--if-accent, var(--color-brand, #7c5cff)));
163
+ border-radius: 50%;
164
+ transform: rotate(0deg);
165
+ animation: image-fade-spin 0.8s linear infinite reverse;
110
166
  }
111
167
 
112
168
  /* Skeleton variant: shimmer sweep instead of a spinner */
@@ -121,7 +177,13 @@ const style = [
121
177
  animation: image-fade-shimmer 1.4s ease-in-out infinite;
122
178
  }
123
179
 
180
+ /* Both frames are explicit so the interpolation stays angular. An implicit
181
+ `from` would be taken from the element's computed transform, which is not
182
+ guaranteed to be `none` on every host page. */
124
183
  @keyframes image-fade-spin {
184
+ from {
185
+ transform: rotate(0deg);
186
+ }
125
187
  to {
126
188
  transform: rotate(360deg);
127
189
  }
@@ -141,7 +203,8 @@ const style = [
141
203
  .image-fade__placeholder {
142
204
  transition: none;
143
205
  }
144
- .image-fade__ring {
206
+ .image-fade__ring,
207
+ .image-fade__ring-inner {
145
208
  animation: none;
146
209
  }
147
210
  image-fade.image-fade.is-skeleton .image-fade__placeholder {