@escape-game-over/atlas 0.1.30 → 0.1.31

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": "@escape-game-over/atlas",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
@@ -62,6 +62,17 @@ export interface CarouselOptions {
62
62
  * renders, and calling would make a project write the first position twice.
63
63
  */
64
64
  readonly onChange: (index: number) => void;
65
+ /**
66
+ * Called with `autoplayMs` whenever a countdown to the next automatic
67
+ * advance begins, and with `null` when autoplay stops.
68
+ *
69
+ * For a progress bar. A countdown begins on attach, after every tick and
70
+ * after every manual move, and each one starts from zero — so the bar
71
+ * restarts on every number and stops on `null`, and never has to know why.
72
+ * A hold (hover, focus, a hidden tab, `pause`) and a detach report `null`
73
+ * once, not once per reason.
74
+ */
75
+ readonly onCountdown?: (ms: number | null) => void;
65
76
  }
66
77
 
67
78
  export interface Carousel {
@@ -107,6 +118,7 @@ export function carousel(options: CarouselOptions): Carousel {
107
118
  swipeThreshold = 10,
108
119
  lockMs = 500,
109
120
  onChange,
121
+ onCountdown,
110
122
  } = options;
111
123
 
112
124
  /**
@@ -143,9 +155,19 @@ export function carousel(options: CarouselOptions): Carousel {
143
155
  */
144
156
  const holds = new Set<string>();
145
157
 
158
+ /** Whether a countdown is running, so `null` is reported once per stop. */
159
+ let counting = false;
160
+
161
+ const stopAutoplay = (): void => {
162
+ clearInterval(autoplayTimer);
163
+ if (!counting) return;
164
+ counting = false;
165
+ onCountdown?.(null);
166
+ };
167
+
146
168
  const hold = (reason: string): void => {
147
169
  holds.add(reason);
148
- clearInterval(autoplayTimer);
170
+ stopAutoplay();
149
171
  };
150
172
 
151
173
  const release = (reason: string): void => {
@@ -171,16 +193,28 @@ export function carousel(options: CarouselOptions): Carousel {
171
193
  };
172
194
 
173
195
  const restartAutoplay = (): void => {
174
- clearInterval(autoplayTimer);
175
196
  // Nothing to rotate through, nobody watching, or no autoplay asked
176
197
  // for — in each case, nothing to schedule.
177
- if (autoplayMs === undefined || !canMove || !attached) return;
178
- if (holds.size > 0) return;
198
+ if (
199
+ autoplayMs === undefined ||
200
+ !canMove ||
201
+ !attached ||
202
+ holds.size > 0
203
+ ) {
204
+ stopAutoplay();
205
+ return;
206
+ }
207
+ clearInterval(autoplayTimer);
179
208
  autoplayTimer = setInterval(() => {
180
209
  // No `restartAutoplay` here: the interval already paces itself, and
181
210
  // resetting it from inside its own tick would only churn timers.
182
211
  if (claim()) move(index + 1);
212
+ // Reported even when the lock refused the move: the next tick is
213
+ // still `autoplayMs` away either way.
214
+ onCountdown?.(autoplayMs);
183
215
  }, autoplayMs);
216
+ counting = true;
217
+ onCountdown?.(autoplayMs);
184
218
  };
185
219
 
186
220
  /** The only place `index` changes. `at` may be out of range or negative. */
@@ -326,7 +360,7 @@ export function carousel(options: CarouselOptions): Carousel {
326
360
  return () => {
327
361
  listeners.abort();
328
362
  attached = false;
329
- clearInterval(autoplayTimer);
363
+ stopAutoplay();
330
364
 
331
365
  // The automatic holds belong to this attachment: an element
332
366
  // detached while hovered would otherwise keep "hover" forever,