@at-flux/astroflare 1.0.13 → 1.0.14

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.13",
3
+ "version": "1.0.14",
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>",
@@ -3,7 +3,10 @@
3
3
  * Loading spinner for Astro view transitions.
4
4
  * Uses native <dialog> shown during page transitions.
5
5
  * Override styles via CSS custom properties or by targeting #loading.
6
+ *
7
+ * The indicator itself is `LoadingSpinner`; fill the default slot to replace it.
6
8
  */
9
+ import LoadingSpinner from './LoadingSpinner.astro';
7
10
  ---
8
11
 
9
12
  <dialog
@@ -13,7 +16,7 @@
13
16
  <div class='flex items-center justify-center min-h-full p-0'>
14
17
  <div class='loading-spinner'>
15
18
  <slot>
16
- <div class='loading-ring' />
19
+ <LoadingSpinner size='3rem' />
17
20
  </slot>
18
21
  </div>
19
22
  </div>
@@ -37,23 +40,8 @@
37
40
  transition: opacity 0.15s ease-out, transform 0.15s ease-out;
38
41
  }
39
42
 
40
- .loading-ring {
41
- width: 3rem;
42
- height: 3rem;
43
- border: 3px solid transparent;
44
- border-top-color: var(--color-brand, #7c5cff);
45
- border-right-color: color-mix(in oklab, var(--color-brand, #7c5cff) 50%, transparent);
46
- border-radius: 50%;
47
- animation: spin 1s linear infinite;
48
- }
49
-
50
- @keyframes spin {
51
- to { transform: rotate(360deg); }
52
- }
53
-
54
43
  @media (prefers-reduced-motion: reduce) {
55
44
  .loading-spinner { transition: none; }
56
- .loading-ring { animation: none; border-top-color: var(--color-brand, #7c5cff); }
57
45
  }
58
46
  </style>
59
47
 
@@ -14,9 +14,10 @@
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.
17
+ * The default placeholder is a plain `LoadingSpinner` (or a shimmer skeleton).
18
+ * Anything fancier belongs in the `placeholder` slot rather than in props
19
+ * whatever you put there is centred in the frame and faded out on load exactly
20
+ * like the default.
20
21
  *
21
22
  * @example
22
23
  * <ImageFade aspectRatio="16/9" rounded="rounded-2xl">
@@ -25,10 +26,12 @@
25
26
  *
26
27
  * @example Custom indicator
27
28
  * <ImageFade aspectRatio="16/9">
28
- * <MySpinner slot="placeholder" />
29
+ * <LoadingSpinner slot="placeholder" dual size="3rem" color="var(--brand)" />
29
30
  * <Image src={hero} alt="…" />
30
31
  * </ImageFade>
31
32
  */
33
+ import LoadingSpinner from './LoadingSpinner.astro';
34
+
32
35
  interface Props {
33
36
  /** Extra classes merged onto the wrapper. */
34
37
  class?: string;
@@ -38,12 +41,8 @@ interface Props {
38
41
  aspectRatio?: string;
39
42
  /** Fade-in duration in ms. */
40
43
  duration?: number;
41
- /** Spinner/skeleton accent colour (defaults to `--color-brand`). */
44
+ /** Accent for the default spinner, the skeleton shimmer and the wrapper tint. */
42
45
  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
46
  /** Wrapper background shown behind the image. Pass `"transparent"` for `object-contain` images. */
48
47
  background?: string;
49
48
  /** Show a shimmer skeleton instead of a spinner. */
@@ -56,8 +55,6 @@ const {
56
55
  aspectRatio,
57
56
  duration = 500,
58
57
  spinnerColor,
59
- spinnerColorSecondary,
60
- spinnerSize,
61
58
  background,
62
59
  skeleton = false,
63
60
  } = Astro.props;
@@ -65,8 +62,6 @@ const {
65
62
  const style = [
66
63
  aspectRatio ? `aspect-ratio:${aspectRatio}` : '',
67
64
  spinnerColor ? `--if-accent:${spinnerColor}` : '',
68
- spinnerColorSecondary ? `--if-accent-2:${spinnerColorSecondary}` : '',
69
- spinnerSize ? `--if-size:${spinnerSize}` : '',
70
65
  background ? `--if-bg:${background}` : '',
71
66
  `--if-duration:${duration}ms`,
72
67
  ]
@@ -77,13 +72,7 @@ const style = [
77
72
  <image-fade class:list={['image-fade', skeleton && 'is-skeleton', rounded, className]} style={style}>
78
73
  <span class='image-fade__placeholder' aria-hidden='true'>
79
74
  <slot name='placeholder'>
80
- {
81
- !skeleton && (
82
- <span class='image-fade__ring'>
83
- <span class='image-fade__ring-inner' />
84
- </span>
85
- )
86
- }
75
+ {!skeleton && <LoadingSpinner color={spinnerColor} />}
87
76
  </slot>
88
77
  </span>
89
78
  <slot />
@@ -128,43 +117,6 @@ const style = [
128
117
  opacity: 0;
129
118
  }
130
119
 
131
- .image-fade__ring {
132
- position: relative;
133
- display: block;
134
- width: var(--if-size, 2.5rem);
135
- height: var(--if-size, 2.5rem);
136
- border: 3px solid transparent;
137
- border-top-color: var(--if-accent, var(--color-brand, #7c5cff));
138
- border-right-color: color-mix(
139
- in oklab,
140
- var(--if-accent, var(--color-brand, #7c5cff)) 50%,
141
- transparent
142
- );
143
- border-radius: 50%;
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;
166
- }
167
-
168
120
  /* Skeleton variant: shimmer sweep instead of a spinner */
169
121
  image-fade.image-fade.is-skeleton .image-fade__placeholder {
170
122
  background: linear-gradient(
@@ -177,36 +129,12 @@ const style = [
177
129
  animation: image-fade-shimmer 1.4s ease-in-out infinite;
178
130
  }
179
131
 
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. */
183
- @keyframes image-fade-spin {
184
- from {
185
- transform: rotate(0deg);
186
- }
187
- to {
188
- transform: rotate(360deg);
189
- }
190
- }
191
- @keyframes image-fade-shimmer {
192
- from {
193
- background-position: 200% 0;
194
- }
195
- to {
196
- background-position: -200% 0;
197
- }
198
- }
199
-
200
132
  @media (prefers-reduced-motion: reduce) {
201
133
  image-fade.image-fade > img,
202
134
  image-fade.image-fade > picture > img,
203
135
  .image-fade__placeholder {
204
136
  transition: none;
205
137
  }
206
- .image-fade__ring,
207
- .image-fade__ring-inner {
208
- animation: none;
209
- }
210
138
  image-fade.image-fade.is-skeleton .image-fade__placeholder {
211
139
  animation: none;
212
140
  }
@@ -0,0 +1,109 @@
1
+ ---
2
+ /**
3
+ * LoadingSpinner — a plain rotating ring.
4
+ *
5
+ * Deliberately unopinionated: one ring, brand colour, no flourish. Use it
6
+ * anywhere something is pending. `ClientRouterLoadingSpinner` and `ImageFade`
7
+ * both fall back to it, and both let you replace it via a slot.
8
+ *
9
+ * Set `dual` for a second, counter-rotating inner ring.
10
+ *
11
+ * @example
12
+ * <LoadingSpinner size="3rem" />
13
+ * <LoadingSpinner dual color="var(--brand)" colorSecondary="var(--brand-light)" />
14
+ */
15
+ interface Props {
16
+ /** Outer ring diameter, any CSS length. */
17
+ size?: string;
18
+ /** Ring colour (defaults to `--color-brand`). */
19
+ color?: string;
20
+ /** Inner ring colour when `dual` is set (defaults to `color`). */
21
+ colorSecondary?: string;
22
+ /** Seconds for one full outer rotation. */
23
+ speed?: number;
24
+ /** Add a counter-rotating inner ring. */
25
+ dual?: boolean;
26
+ /** Extra classes on the ring element. */
27
+ class?: string;
28
+ }
29
+
30
+ const {
31
+ size,
32
+ color,
33
+ colorSecondary,
34
+ speed = 1,
35
+ dual = false,
36
+ class: className = '',
37
+ } = Astro.props;
38
+
39
+ const style = [
40
+ size ? `--ls-size:${size}` : '',
41
+ color ? `--ls-color:${color}` : '',
42
+ colorSecondary ? `--ls-color-2:${colorSecondary}` : '',
43
+ `--ls-speed:${speed}s`,
44
+ ]
45
+ .filter(Boolean)
46
+ .join(';');
47
+ ---
48
+
49
+ <span class:list={['loading-spinner-ring', className]} style={style} aria-hidden='true'>
50
+ {dual && <span class='loading-spinner-ring__inner' />}
51
+ </span>
52
+
53
+ <style is:global>
54
+ .loading-spinner-ring {
55
+ position: relative;
56
+ display: block;
57
+ width: var(--ls-size, 2.5rem);
58
+ height: var(--ls-size, 2.5rem);
59
+ border: 3px solid transparent;
60
+ border-top-color: var(--ls-color, var(--color-brand, #7c5cff));
61
+ border-right-color: color-mix(
62
+ in oklab,
63
+ var(--ls-color, var(--color-brand, #7c5cff)) 50%,
64
+ transparent
65
+ );
66
+ border-radius: 50%;
67
+ /* Reset explicitly: a host page may set a base transform on every element
68
+ (a `* { transform: translateZ(0) }` GPU hint is common), which would make
69
+ the keyframes interpolate as matrices and cancel the full turn out to no
70
+ visible rotation. */
71
+ transform: rotate(0deg);
72
+ animation: loading-spinner-spin var(--ls-speed, 1s) linear infinite;
73
+ }
74
+
75
+ .loading-spinner-ring__inner {
76
+ position: absolute;
77
+ top: 50%;
78
+ left: 50%;
79
+ display: block;
80
+ width: 60%;
81
+ height: 60%;
82
+ margin: -30% 0 0 -30%;
83
+ border: 2px solid transparent;
84
+ border-top-color: var(--ls-color-2, var(--ls-color, var(--color-brand, #7c5cff)));
85
+ border-right-color: var(--ls-color-2, var(--ls-color, var(--color-brand, #7c5cff)));
86
+ border-radius: 50%;
87
+ transform: rotate(0deg);
88
+ animation: loading-spinner-spin calc(var(--ls-speed, 1s) * 0.7) linear infinite reverse;
89
+ }
90
+
91
+ /* Both frames are explicit so the interpolation stays angular. An implicit
92
+ `from` would be taken from the element's computed transform, which is not
93
+ guaranteed to be `none` on every host page. */
94
+ @keyframes loading-spinner-spin {
95
+ from {
96
+ transform: rotate(0deg);
97
+ }
98
+ to {
99
+ transform: rotate(360deg);
100
+ }
101
+ }
102
+
103
+ @media (prefers-reduced-motion: reduce) {
104
+ .loading-spinner-ring,
105
+ .loading-spinner-ring__inner {
106
+ animation: none;
107
+ }
108
+ }
109
+ </style>