@at-flux/astroflare 1.0.12 → 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.12",
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,11 +14,24 @@
14
14
  * choose its `object-fit` with your own classes, exactly as you would without
15
15
  * this wrapper.
16
16
  *
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.
21
+ *
17
22
  * @example
18
23
  * <ImageFade aspectRatio="16/9" rounded="rounded-2xl">
19
24
  * <Image src={hero} alt="…" />
20
25
  * </ImageFade>
26
+ *
27
+ * @example Custom indicator
28
+ * <ImageFade aspectRatio="16/9">
29
+ * <LoadingSpinner slot="placeholder" dual size="3rem" color="var(--brand)" />
30
+ * <Image src={hero} alt="…" />
31
+ * </ImageFade>
21
32
  */
33
+ import LoadingSpinner from './LoadingSpinner.astro';
34
+
22
35
  interface Props {
23
36
  /** Extra classes merged onto the wrapper. */
24
37
  class?: string;
@@ -28,12 +41,8 @@ interface Props {
28
41
  aspectRatio?: string;
29
42
  /** Fade-in duration in ms. */
30
43
  duration?: number;
31
- /** Spinner/skeleton accent colour (defaults to `--color-brand`). */
44
+ /** Accent for the default spinner, the skeleton shimmer and the wrapper tint. */
32
45
  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
46
  /** Wrapper background shown behind the image. Pass `"transparent"` for `object-contain` images. */
38
47
  background?: string;
39
48
  /** Show a shimmer skeleton instead of a spinner. */
@@ -46,8 +55,6 @@ const {
46
55
  aspectRatio,
47
56
  duration = 500,
48
57
  spinnerColor,
49
- spinnerColorSecondary,
50
- spinnerSize,
51
58
  background,
52
59
  skeleton = false,
53
60
  } = Astro.props;
@@ -55,8 +62,6 @@ const {
55
62
  const style = [
56
63
  aspectRatio ? `aspect-ratio:${aspectRatio}` : '',
57
64
  spinnerColor ? `--if-accent:${spinnerColor}` : '',
58
- spinnerColorSecondary ? `--if-accent-2:${spinnerColorSecondary}` : '',
59
- spinnerSize ? `--if-size:${spinnerSize}` : '',
60
65
  background ? `--if-bg:${background}` : '',
61
66
  `--if-duration:${duration}ms`,
62
67
  ]
@@ -66,13 +71,9 @@ const style = [
66
71
 
67
72
  <image-fade class:list={['image-fade', skeleton && 'is-skeleton', rounded, className]} style={style}>
68
73
  <span class='image-fade__placeholder' aria-hidden='true'>
69
- {
70
- !skeleton && (
71
- <span class='image-fade__ring'>
72
- <span class='image-fade__ring-inner' />
73
- </span>
74
- )
75
- }
74
+ <slot name='placeholder'>
75
+ {!skeleton && <LoadingSpinner color={spinnerColor} />}
76
+ </slot>
76
77
  </span>
77
78
  <slot />
78
79
  </image-fade>
@@ -116,43 +117,6 @@ const style = [
116
117
  opacity: 0;
117
118
  }
118
119
 
119
- .image-fade__ring {
120
- position: relative;
121
- display: block;
122
- width: var(--if-size, 2.5rem);
123
- height: var(--if-size, 2.5rem);
124
- border: 3px solid transparent;
125
- border-top-color: var(--if-accent, var(--color-brand, #7c5cff));
126
- border-right-color: color-mix(
127
- in oklab,
128
- var(--if-accent, var(--color-brand, #7c5cff)) 50%,
129
- transparent
130
- );
131
- border-radius: 50%;
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;
154
- }
155
-
156
120
  /* Skeleton variant: shimmer sweep instead of a spinner */
157
121
  image-fade.image-fade.is-skeleton .image-fade__placeholder {
158
122
  background: linear-gradient(
@@ -165,36 +129,12 @@ const style = [
165
129
  animation: image-fade-shimmer 1.4s ease-in-out infinite;
166
130
  }
167
131
 
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. */
171
- @keyframes image-fade-spin {
172
- from {
173
- transform: rotate(0deg);
174
- }
175
- to {
176
- transform: rotate(360deg);
177
- }
178
- }
179
- @keyframes image-fade-shimmer {
180
- from {
181
- background-position: 200% 0;
182
- }
183
- to {
184
- background-position: -200% 0;
185
- }
186
- }
187
-
188
132
  @media (prefers-reduced-motion: reduce) {
189
133
  image-fade.image-fade > img,
190
134
  image-fade.image-fade > picture > img,
191
135
  .image-fade__placeholder {
192
136
  transition: none;
193
137
  }
194
- .image-fade__ring,
195
- .image-fade__ring-inner {
196
- animation: none;
197
- }
198
138
  image-fade.image-fade.is-skeleton .image-fade__placeholder {
199
139
  animation: none;
200
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>