@escape-game-over/atlas 0.1.75 → 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.
- package/docs/client-scripts.md +54 -5
- package/package.json +1 -1
- package/src/astro/filters-view.ts +77 -6
- package/src/astro/google-event.ts +16 -14
package/docs/client-scripts.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
@@ -581,7 +625,9 @@ googleEvent("form_submissions_contact"); // does not compile
|
|
|
581
625
|
A name nobody declared does not compile, so a typo cannot quietly become a
|
|
582
626
|
second event in a report — which is why this is not a `string` with the standard
|
|
583
627
|
names offered for autocomplete. Data declared with a required field is required
|
|
584
|
-
at every call.
|
|
628
|
+
at every call. A Google declaration may reuse a recommended name —
|
|
629
|
+
`generate_lead` with an agency's own parameters, say — and then takes what was
|
|
630
|
+
declared instead of GA4's. The interfaces are `GoogleCustomEvents`, `MetaCustomEvents`,
|
|
585
631
|
`TikTokCustomEvents`, `UmamiEvents`, `DripEvents` and `ClarityEvents`; Clarity
|
|
586
632
|
carries a name only, so its entries are all `undefined`. Snap and Axon take no
|
|
587
633
|
declarations because they accept no names but their own.
|
|
@@ -630,6 +676,9 @@ right properties is a faithful stand-in.
|
|
|
630
676
|
pins the write-back guard by counting *writes* — so deleting the guard fails
|
|
631
677
|
rather than passing on an identical value — and pins the nesting order in
|
|
632
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.
|
|
633
682
|
- `tests/youtube.test.ts` fakes the trigger, and a container whose
|
|
634
683
|
`ownerDocument` makes the iframe — which is why the module reaches the
|
|
635
684
|
document through the container rather than the global.
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
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
|
|
6
|
-
* silent.
|
|
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`,
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
+
}
|
|
@@ -78,22 +78,24 @@ export interface GoogleEventData {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
* A
|
|
82
|
-
*
|
|
83
|
-
*
|
|
81
|
+
* A declared event takes what the project declared — checked first, so a
|
|
82
|
+
* project can declare a recommended name too, when its Tag Manager wants that
|
|
83
|
+
* name with its own parameters (an agency's `generate_lead`, say). Otherwise a
|
|
84
|
+
* recommended event takes GA4's parameters; `purchase` without its three
|
|
85
|
+
* reports a sale of nothing, possibly twice.
|
|
84
86
|
*/
|
|
85
87
|
type GoogleEventArgs<E extends GoogleEventName> =
|
|
86
|
-
E extends
|
|
87
|
-
? E
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
E extends DeclaredEvent<GoogleCustomEvents>
|
|
89
|
+
? EventDataArgs<GoogleCustomEvents[E]>
|
|
90
|
+
: E extends "purchase"
|
|
91
|
+
? [
|
|
92
|
+
data: GoogleEventData & {
|
|
93
|
+
value: number;
|
|
94
|
+
currency: CurrencyCode;
|
|
95
|
+
transaction_id: string;
|
|
96
|
+
},
|
|
97
|
+
]
|
|
98
|
+
: [data?: GoogleEventData];
|
|
97
99
|
|
|
98
100
|
/**
|
|
99
101
|
* Records an event with Google — a lead sent, a booking made — through
|