@at-flux/astroflare 1.0.10 → 1.0.12

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.10",
3
+ "version": "1.0.12",
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>",
@@ -10,6 +10,10 @@
10
10
  * Reserve layout space with `aspectRatio` to avoid CLS. Styling is theme-aware
11
11
  * via `--color-brand` (override with `spinnerColor`).
12
12
  *
13
+ * The wrapper only controls the slotted image's opacity — size the image and
14
+ * choose its `object-fit` with your own classes, exactly as you would without
15
+ * this wrapper.
16
+ *
13
17
  * @example
14
18
  * <ImageFade aspectRatio="16/9" rounded="rounded-2xl">
15
19
  * <Image src={hero} alt="…" />
@@ -26,6 +30,12 @@ interface Props {
26
30
  duration?: number;
27
31
  /** Spinner/skeleton accent colour (defaults to `--color-brand`). */
28
32
  spinnerColor?: string;
33
+ /** Second accent, used by the counter-rotating inner ring (defaults to `spinnerColor`). */
34
+ spinnerColorSecondary?: string;
35
+ /** Outer ring diameter, any CSS length. */
36
+ spinnerSize?: string;
37
+ /** Wrapper background shown behind the image. Pass `"transparent"` for `object-contain` images. */
38
+ background?: string;
29
39
  /** Show a shimmer skeleton instead of a spinner. */
30
40
  skeleton?: boolean;
31
41
  }
@@ -36,12 +46,18 @@ const {
36
46
  aspectRatio,
37
47
  duration = 500,
38
48
  spinnerColor,
49
+ spinnerColorSecondary,
50
+ spinnerSize,
51
+ background,
39
52
  skeleton = false,
40
53
  } = Astro.props;
41
54
 
42
55
  const style = [
43
56
  aspectRatio ? `aspect-ratio:${aspectRatio}` : '',
44
57
  spinnerColor ? `--if-accent:${spinnerColor}` : '',
58
+ spinnerColorSecondary ? `--if-accent-2:${spinnerColorSecondary}` : '',
59
+ spinnerSize ? `--if-size:${spinnerSize}` : '',
60
+ background ? `--if-bg:${background}` : '',
45
61
  `--if-duration:${duration}ms`,
46
62
  ]
47
63
  .filter(Boolean)
@@ -50,7 +66,13 @@ const style = [
50
66
 
51
67
  <image-fade class:list={['image-fade', skeleton && 'is-skeleton', rounded, className]} style={style}>
52
68
  <span class='image-fade__placeholder' aria-hidden='true'>
53
- {!skeleton && <span class='image-fade__ring'></span>}
69
+ {
70
+ !skeleton && (
71
+ <span class='image-fade__ring'>
72
+ <span class='image-fade__ring-inner' />
73
+ </span>
74
+ )
75
+ }
54
76
  </span>
55
77
  <slot />
56
78
  </image-fade>
@@ -60,16 +82,18 @@ const style = [
60
82
  position: relative;
61
83
  display: block;
62
84
  overflow: hidden;
63
- background: color-mix(in oklab, var(--if-accent, var(--color-brand, #7c5cff)) 8%, transparent);
85
+ background: var(
86
+ --if-bg,
87
+ color-mix(in oklab, var(--if-accent, var(--color-brand, #7c5cff)) 8%, transparent)
88
+ );
64
89
  }
65
90
 
66
- /* Slotted image starts hidden, fades in once loaded */
91
+ /* Slotted image starts hidden, fades in once loaded.
92
+ Only opacity is owned here — sizing and `object-fit` stay with the caller
93
+ so consumer classes (`object-contain`, `object-fill`, …) are not overridden. */
67
94
  image-fade.image-fade > img,
68
95
  image-fade.image-fade > picture > img {
69
96
  display: block;
70
- width: 100%;
71
- height: 100%;
72
- object-fit: cover;
73
97
  opacity: 0;
74
98
  transition: opacity var(--if-duration, 500ms) ease-out;
75
99
  }
@@ -93,8 +117,10 @@ const style = [
93
117
  }
94
118
 
95
119
  .image-fade__ring {
96
- width: 2.5rem;
97
- height: 2.5rem;
120
+ position: relative;
121
+ display: block;
122
+ width: var(--if-size, 2.5rem);
123
+ height: var(--if-size, 2.5rem);
98
124
  border: 3px solid transparent;
99
125
  border-top-color: var(--if-accent, var(--color-brand, #7c5cff));
100
126
  border-right-color: color-mix(
@@ -103,7 +129,28 @@ const style = [
103
129
  transparent
104
130
  );
105
131
  border-radius: 50%;
106
- animation: image-fade-spin 0.9s linear infinite;
132
+ /* `transform` is reset explicitly: a host page may set a base transform on
133
+ every element (e.g. a `* { transform: translateZ(0) }` GPU hint), which
134
+ would otherwise make the keyframes interpolate as matrices and cancel the
135
+ full turn out to no visible rotation. */
136
+ transform: rotate(0deg);
137
+ animation: image-fade-spin 1.2s linear infinite;
138
+ }
139
+
140
+ .image-fade__ring-inner {
141
+ position: absolute;
142
+ top: 50%;
143
+ left: 50%;
144
+ display: block;
145
+ width: 60%;
146
+ height: 60%;
147
+ margin: -30% 0 0 -30%;
148
+ border: 2px solid transparent;
149
+ border-top-color: var(--if-accent-2, var(--if-accent, var(--color-brand, #7c5cff)));
150
+ border-right-color: var(--if-accent-2, var(--if-accent, var(--color-brand, #7c5cff)));
151
+ border-radius: 50%;
152
+ transform: rotate(0deg);
153
+ animation: image-fade-spin 0.8s linear infinite reverse;
107
154
  }
108
155
 
109
156
  /* Skeleton variant: shimmer sweep instead of a spinner */
@@ -118,7 +165,13 @@ const style = [
118
165
  animation: image-fade-shimmer 1.4s ease-in-out infinite;
119
166
  }
120
167
 
168
+ /* Both frames are explicit so the interpolation stays angular. An implicit
169
+ `from` would be taken from the element's computed transform, which is not
170
+ guaranteed to be `none` on every host page. */
121
171
  @keyframes image-fade-spin {
172
+ from {
173
+ transform: rotate(0deg);
174
+ }
122
175
  to {
123
176
  transform: rotate(360deg);
124
177
  }
@@ -138,7 +191,8 @@ const style = [
138
191
  .image-fade__placeholder {
139
192
  transition: none;
140
193
  }
141
- .image-fade__ring {
194
+ .image-fade__ring,
195
+ .image-fade__ring-inner {
142
196
  animation: none;
143
197
  }
144
198
  image-fade.image-fade.is-skeleton .image-fade__placeholder {