@escape-game-over/atlas 0.1.111 → 0.1.112

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/docs/checks.md CHANGED
@@ -132,7 +132,7 @@ dependencies at all. Vitest type-checks them itself when it runs them.
132
132
  | `money.test.ts` | a bare count widened to a band, the span of a table, the gaps and overlaps that throw, and an audience priced twice |
133
133
  | `url.test.ts` | joining an origin to a path exactly once, and normalising the origin an `@id` is built from |
134
134
  | `warn.test.ts` | the shared prefix, and reducing a URL to its path |
135
- | `filters.test.ts` | folding both sides of a query, how each kind narrows, the four URL decisions, and attach/detach |
135
+ | `filters.test.ts` | folding both sides of a query, how each kind narrows, the four URL decisions, values no item holds, and attach/detach |
136
136
  | `filters-view.test.ts` | the write-back into the input and that it is skipped when unchanged, clear taking focus, and the nesting order |
137
137
  | `build-cache.test.ts` | that an unset `CI_ASTRO_CACHE` yields nothing, so Astro's own `cacheDir` default survives |
138
138
 
@@ -241,6 +241,12 @@ instead of flashing the whole list back. Nothing writes to the URL before
241
241
  `attach`, because `attach` is what reads it — a `set` on an unattached instance
242
242
  would overwrite state nobody had loaded.
243
243
 
244
+ A choice no item holds, or a flag no item carries, is a stale or mistyped link:
245
+ reading it — on `attach` and on the back button — takes it as no choice, and
246
+ replaces the URL without it. Otherwise `?category=castle` showed an empty list
247
+ with nothing pressed, not even "All". A `text` query is left alone: it matches
248
+ words, not values, and finding nothing is a fair answer to it.
249
+
244
250
  A flag is on by being there, as an HTML boolean attribute is: written as the bare
245
251
  `?featured`, and **absent** when off. Reading asks only whether the name is
246
252
  present, so `?featured=1` from an older link still reads as on — and so does a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escape-game-over/atlas",
3
- "version": "0.1.111",
3
+ "version": "0.1.112",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
@@ -199,6 +199,18 @@ export function filters<const F extends FieldMap, T>(
199
199
  return { item, values, haystacks };
200
200
  });
201
201
 
202
+ // What the items hold, per choice and flag: a link asking for anything
203
+ // else is stale or mistyped, and is read as no choice rather than an empty
204
+ // list with nothing pressed.
205
+ const held = new Map<string, ReadonlySet<unknown>>();
206
+ for (const name of names) {
207
+ if (kindOf(name) === "text") continue;
208
+ held.set(
209
+ name,
210
+ new Set(entries.flatMap((entry) => entry.values[name] as unknown))
211
+ );
212
+ }
213
+
202
214
  const empty = (name: keyof F & string): StateValue<F[typeof name]> =>
203
215
  (kindOf(name) === "flag" ? false : "") as StateValue<F[typeof name]>;
204
216
 
@@ -287,9 +299,11 @@ export function filters<const F extends FieldMap, T>(
287
299
  matched = next;
288
300
  }
289
301
 
290
- function readUrl(): void {
302
+ /** Applies the URL; says whether it asked for anything no item holds. */
303
+ function readUrl(): boolean {
291
304
  const search = new URLSearchParams(window.location.search);
292
305
  const next: Record<string, string | boolean> = {};
306
+ let dropped = false;
293
307
  for (const name of names) {
294
308
  if (!inUrl.includes(name)) {
295
309
  // A state-only field is not in the URL, so the URL has nothing
@@ -300,12 +314,17 @@ export function filters<const F extends FieldMap, T>(
300
314
  }
301
315
  // A flag is on by being there, as an HTML boolean attribute is:
302
316
  // `?featured`, and `?featured=1` from an older link, alike.
303
- next[name] =
317
+ const value =
304
318
  kindOf(name) === "flag"
305
319
  ? search.has(name)
306
320
  : (search.get(name) ?? "");
321
+ const unheld =
322
+ value !== empty(name) && held.get(name)?.has(value) === false;
323
+ next[name] = unheld ? empty(name) : value;
324
+ if (unheld) dropped = true;
307
325
  }
308
326
  state = next as FilterState<F>;
327
+ return dropped;
309
328
  }
310
329
 
311
330
  function writeUrl(history: "push" | "replace"): void {
@@ -424,8 +443,9 @@ export function filters<const F extends FieldMap, T>(
424
443
  window.addEventListener(
425
444
  "popstate",
426
445
  () => {
427
- readUrl();
446
+ const dropped = readUrl();
428
447
  recompute();
448
+ if (dropped) writeUrl("replace");
429
449
  announce();
430
450
  },
431
451
  { signal: events.signal }
@@ -435,8 +455,11 @@ export function filters<const F extends FieldMap, T>(
435
455
  // Read before the first announcement: the URL may already carry a
436
456
  // state, and announcing the empty one first flashes the unfiltered
437
457
  // list over it.
438
- readUrl();
458
+ // A value no item holds leaves the address bar as well, replaced
459
+ // rather than pushed: it is a correction, not a step to go back to.
460
+ const dropped = readUrl();
439
461
  recompute();
462
+ if (dropped) writeUrl("replace");
440
463
  announce();
441
464
 
442
465
  return () => {