@escape-game-over/atlas 0.1.76 → 0.1.77

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.
@@ -36,7 +36,7 @@ finding them.
36
36
  | ------------------ | -------------------------------------------------------------- | ----------------------------------------------- |
37
37
  | `carousel` | which slide is current: modulo, swipe, autoplay, the move lock | the transform, dots, arrows, `aria` |
38
38
  | `filters` | which items match, and what the address bar says | every DOM read and write |
39
- | `filters-view` | the two DOM writes every filtered list turned out to share | finding the elements, and everything else drawn |
39
+ | `filters-view` | the DOM writes every filtered list turned out to share | finding the elements, and everything else drawn |
40
40
  | `youtube` | the swap from poster to player, on the click and not before | the poster, the button, the iframe's classes |
41
41
  | `background-video` | playing or paused, playable or not, and which cut is loaded | the play/pause control, its icons and its label |
42
42
  | `consent` | remembering an answer, expiring it, handing it to Google | the banner — its wording, its buttons, its law |
@@ -249,11 +249,12 @@ re-clicking the current tab does not add a history step to walk back through.
249
249
 
250
250
  ## `filters-view`
251
251
 
252
- Two helpers, and the bar for being here is not "a list might want this" but
252
+ Three helpers, and the bar for being here is not "a list might want this" but
253
253
  "every list we have already written did, character for character, and getting it
254
254
  wrong was silent". Three filtered lists in one consuming project — a catalogue, a
255
- location directory and an FAQ — agree on exactly these and disagree about
256
- everything else they draw.
255
+ location directory and an FAQ — agree on `searchBox` and `hideEmpty` and
256
+ disagree about everything else they draw; `filtering` was copied between two
257
+ sites before it came here.
257
258
 
258
259
  ### `searchBox`
259
260
 
@@ -334,6 +335,49 @@ recorded as behaviour rather than only as prose.
334
335
  A group with no children at all is hidden, by the same rule: there is nothing
335
336
  visible in it.
336
337
 
338
+ ### `filtering`
339
+
340
+ A chip that narrows a grid in place, animated: what stays slides to its new
341
+ place, what leaves or arrives fades. It wraps the `set` or `reset` the chip does
342
+ in a view transition, and runs it plainly where the API is missing.
343
+
344
+ ```ts
345
+ const items = refs(root, "[data-filter-item]");
346
+ chip.addEventListener(
347
+ "click",
348
+ () => filtering(items, () => list.set("category", data(chip, "category"))),
349
+ { signal }
350
+ );
351
+ ```
352
+
353
+ Every item needs its own view-transition name for the move to animate, and
354
+ `match-element` gives each one without the page minting ids. **Only while it
355
+ runs**: named for good, every card would also pair up across page navigations,
356
+ and one with no partner on the next page would linger over it. So the helper
357
+ writes the name inline, and puts back whatever inline name the item had when
358
+ the last transition ends.
359
+
360
+ **The last, not each.** A second click skips the first transition, and the
361
+ first one's end would otherwise strip the names out from under the one still
362
+ running, which then animates nothing. The helper counts. `tests/filtering.test.ts`
363
+ pins the overlap, including an item that had a name of its own.
364
+
365
+ **One rule stays the project's.** A site that stops unpaired names animating
366
+ across pages also stops filtering's fades, since a card hidden or shown has no
367
+ partner either. The helper marks `<html>` with `data-atlas-filtering`
368
+ (`FILTERING_ATTRIBUTE`) while it runs, for the site to scope that rule out:
369
+
370
+ ```css
371
+ :root:not([data-atlas-filtering])::view-transition-new(*):only-child {
372
+ animation: none;
373
+ }
374
+ :root:not([data-atlas-filtering])::view-transition-old(*):only-child {
375
+ display: none;
376
+ }
377
+ ```
378
+
379
+ A site without such a rule needs no CSS at all.
380
+
337
381
  ### Why these write instead of calling back
338
382
 
339
383
  `filters.onChange` is already the callback that hands a consumer the answer and
@@ -632,6 +676,9 @@ right properties is a faithful stand-in.
632
676
  pins the write-back guard by counting *writes* — so deleting the guard fails
633
677
  rather than passing on an identical value — and pins the nesting order in
634
678
  both directions.
679
+ - `tests/filtering.test.ts` stubs `document` with a `startViewTransition` whose
680
+ transitions finish when the test says, so two overlapping runs can be ended
681
+ in order.
635
682
  - `tests/youtube.test.ts` fakes the trigger, and a container whose
636
683
  `ownerDocument` makes the iframe — which is why the module reaches the
637
684
  document through the container rather than the global.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escape-game-over/atlas",
3
- "version": "0.1.76",
3
+ "version": "0.1.77",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
@@ -1,14 +1,15 @@
1
1
  /**
2
- * The two DOM writes every filtered list turned out to share.
2
+ * The DOM writes every filtered list turned out to share.
3
3
  *
4
4
  * `filters` decides which items match; what a page does about that is its own.
5
- * These are the parts three lists wrote identically, and getting wrong was
6
- * silent. Both take elements, never selectors, so no attribute name in this
5
+ * These are the parts the lists wrote identically, and getting wrong was
6
+ * silent. All take elements, never selectors, so no attribute name in this
7
7
  * package has to be matched by a consumer.
8
8
  *
9
- * They write only what is binary and derived — an input's `value`, and `hidden`
10
- * on a clear button, an empty message or a section with nothing left. No class,
11
- * style or `aria`: that is where the choices live.
9
+ * They write only what is binary and derived — an input's `value`, `hidden` on
10
+ * a clear button, an empty message or a section with nothing left, and a
11
+ * view-transition name while a filter animates. No class or `aria`: that is
12
+ * where the choices live.
12
13
  *
13
14
  * **`hidden` needs help in a grid.** Any author rule setting `display` beats it.
14
15
  * Tailwind v4's preflight ships `[hidden] { display: none !important }`; without
@@ -124,3 +125,73 @@ export function hideEmpty<T extends HTMLElement>(
124
125
  group.hidden = !visible;
125
126
  }
126
127
  }
128
+
129
+ /**
130
+ * On `<html>` while a `filtering` transition runs: what a stylesheet that
131
+ * treats unpaired view-transition names one way across pages tells the two
132
+ * cases apart by.
133
+ */
134
+ export const FILTERING_ATTRIBUTE = "data-atlas-filtering";
135
+
136
+ /** Each named item, and the inline name it had before, to put back after. */
137
+ const named = new Map<HTMLElement, string>();
138
+ let running = 0;
139
+
140
+ /**
141
+ * Runs `update` — the `set` or `reset` a chip or tab does — as a view
142
+ * transition, so what stays slides into place and what leaves or arrives
143
+ * fades. Without the API, `update` just runs.
144
+ *
145
+ * ```ts
146
+ * chip.addEventListener("click", () =>
147
+ * filtering(items, () => list.set("category", data(chip, "category")))
148
+ * );
149
+ * ```
150
+ *
151
+ * **`items` are named only while it runs**, `match-element` each, and put
152
+ * back after. Named for good, every one of them would be a pair across pages
153
+ * too, and linger over the next page on navigation.
154
+ *
155
+ * **Counted, not just started**: a second click skips the first transition,
156
+ * and that one's end would otherwise strip the names from the one still
157
+ * running.
158
+ *
159
+ * **One rule is the project's**, because only the project knows it wrote the
160
+ * other half. A site that stops unpaired names animating across pages — so a
161
+ * card on one page does not fade over the next — stops filtering's fades with
162
+ * it, since an item hidden or shown is unpaired too. Scope it out:
163
+ *
164
+ * ```css
165
+ * :root:not([data-atlas-filtering])::view-transition-new(*):only-child {
166
+ * animation: none;
167
+ * }
168
+ * ```
169
+ */
170
+ export function filtering(
171
+ items: Iterable<HTMLElement>,
172
+ update: () => void
173
+ ): void {
174
+ const doc = globalThis.document;
175
+ if (typeof doc?.startViewTransition !== "function") {
176
+ update();
177
+ return;
178
+ }
179
+
180
+ for (const item of items) {
181
+ if (!named.has(item)) named.set(item, item.style.viewTransitionName);
182
+ item.style.viewTransitionName = "match-element";
183
+ }
184
+
185
+ const root = doc.documentElement;
186
+ running += 1;
187
+ root.setAttribute(FILTERING_ATTRIBUTE, "");
188
+ doc.startViewTransition(update).finished.finally(() => {
189
+ running -= 1;
190
+ if (running > 0) return;
191
+ root.removeAttribute(FILTERING_ATTRIBUTE);
192
+ for (const [item, before] of named) {
193
+ item.style.viewTransitionName = before;
194
+ }
195
+ named.clear();
196
+ });
197
+ }