@escape-game-over/atlas 0.1.25 → 0.1.27
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 +84 -128
- package/docs/rich-text.md +9 -4
- package/package.json +5 -4
- package/src/astro/AtlasElement.astro +26 -0
- package/src/astro/ConsentElement.astro +20 -14
- package/src/astro/Image.astro +1 -2
- package/src/astro/MetaTags.astro +27 -14
- package/src/astro/RichText.astro +60 -43
- package/src/astro/client.ts +7 -8
- package/src/astro/consent.ts +10 -10
- package/src/astro/element.ts +59 -93
- package/src/astro/filters-view.ts +10 -20
- package/src/astro/filters.ts +108 -116
- package/src/astro/ref.ts +46 -0
- package/src/astro/markup.ts +0 -656
package/docs/client-scripts.md
CHANGED
|
@@ -73,15 +73,15 @@ runs when the element enters the page, `signal` ends what it registered when the
|
|
|
73
73
|
element leaves, and what it returns is the undo for everything else.
|
|
74
74
|
|
|
75
75
|
```ts
|
|
76
|
-
export const thing = element("go-thing", {
|
|
77
|
-
button.
|
|
78
|
-
return loop.attach(
|
|
76
|
+
export const thing = element("go-thing", ({ root, signal }) => {
|
|
77
|
+
ref(root, "[data-thing-button]").addEventListener("click", open, { signal }); // ends with the visit
|
|
78
|
+
return loop.attach(root); // and so does this
|
|
79
79
|
});
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
An element can enter the page more than once — moving it in the DOM runs the
|
|
83
83
|
undo and then the function again — so it has to be able to run twice. State that
|
|
84
|
-
must survive a move goes in a `WeakMap` keyed by `
|
|
84
|
+
must survive a move goes in a `WeakMap` keyed by `root`, which is what the
|
|
85
85
|
carousel example does with its index.
|
|
86
86
|
|
|
87
87
|
**The trap this exists for is view transitions.** A bundled `<script src>` is an
|
|
@@ -98,50 +98,46 @@ handled. Without a re-`attach` there is simply nothing wired.
|
|
|
98
98
|
## `filters`
|
|
99
99
|
|
|
100
100
|
A list already rendered by the server, narrowed in the browser and mirrored in
|
|
101
|
-
the URL. The caller declares fields
|
|
101
|
+
the URL. The caller declares fields and hands over its own items — elements,
|
|
102
|
+
records, whatever it holds — with how to read each one's values.
|
|
102
103
|
|
|
103
104
|
```ts
|
|
104
105
|
const list = filters({
|
|
105
|
-
fields: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
fields: { q: "text", category: "choice", featured: "flag" },
|
|
107
|
+
items: rows,
|
|
108
|
+
values: (row) => ({
|
|
109
|
+
q: data(row, "text"),
|
|
110
|
+
category: data(row, "category"),
|
|
111
|
+
featured: row.hasAttribute("data-featured"),
|
|
112
|
+
}),
|
|
113
|
+
onChange: ({ matched }) => {
|
|
114
|
+
for (const row of rows) row.hidden = !matched.has(row);
|
|
109
115
|
},
|
|
110
|
-
items: entries.map((entry) => ({
|
|
111
|
-
key: entry.id,
|
|
112
|
-
values: {
|
|
113
|
-
q: `${entry.title} ${entry.summary}`,
|
|
114
|
-
category: entry.category,
|
|
115
|
-
featured: entry.featured,
|
|
116
|
-
},
|
|
117
|
-
})),
|
|
118
|
-
onChange: ({ matched, state }) => { /* the project's DOM writes, all of them */ },
|
|
119
116
|
});
|
|
120
117
|
|
|
121
118
|
const detach = list.attach();
|
|
122
119
|
```
|
|
123
120
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
A field is its kind, and its URL parameter is its own name — to rename the
|
|
122
|
+
parameter, rename the field. `{ kind: "text", url: false }` keeps a field out of
|
|
123
|
+
the URL, for a list whose page owns the URL itself. `values` is typed *from*
|
|
127
124
|
`fields`, so a `flag` demands a boolean and a mismatch is a compile error rather
|
|
128
|
-
than an item that silently never matches.
|
|
125
|
+
than an item that silently never matches. It is read once per item, at
|
|
126
|
+
construction. `matched` is a set of the caller's own items, so there is no key
|
|
127
|
+
to invent, carry in the markup and map back.
|
|
129
128
|
|
|
130
129
|
| Kind | State | Item value | Matches when | In the URL | History |
|
|
131
130
|
| -------- | --------------- | --------------- | ------------------------------------------------ | ------------------------------- | ------- |
|
|
132
131
|
| `text` | the query | searchable text | every folded query word appears; empty keeps all | `?q=…`, dropped if empty | replace |
|
|
133
132
|
| `choice` | one value, `""` | one, or several | the item holds the chosen one; `""` keeps all | `?category=…`, dropped if empty | push |
|
|
134
|
-
| `flag` | boolean | boolean | off keeps all; on keeps only items that carry it | `?featured
|
|
133
|
+
| `flag` | boolean | boolean | off keeps all; on keeps only items that carry it | `?featured`, absent when off | push |
|
|
135
134
|
|
|
136
135
|
**A `choice` item may hold several values.** The state stays one — the tab strip
|
|
137
136
|
picks one category, the URL carries one — but an item can sit in more than one
|
|
138
137
|
and then answers to any of them:
|
|
139
138
|
|
|
140
139
|
```ts
|
|
141
|
-
|
|
142
|
-
{ key: "burgos", values: { category: "city" } },
|
|
143
|
-
{ key: "avila", values: { category: ["walk", "city"] } },
|
|
144
|
-
]
|
|
140
|
+
values: (room) => ({ category: data(room, "categories").split(" ") }),
|
|
145
141
|
```
|
|
146
142
|
|
|
147
143
|
An escape room is adventure *and* sci-fi; a film is a comedy *and* a drama. This
|
|
@@ -175,8 +171,6 @@ pass. What is deliberately absent is one field holding several selected values;
|
|
|
175
171
|
that is a `choices` kind, and it forces a URL encoding decision (repeated
|
|
176
172
|
parameters or comma-joined) that nothing has needed yet.
|
|
177
173
|
|
|
178
|
-
`param` defaults to the field's name. `param: false` keeps a field state-only, out of the URL.
|
|
179
|
-
|
|
180
174
|
### Facet counts: `matchedWithout`
|
|
181
175
|
|
|
182
176
|
A dropdown that says how many results each option would give needs a different
|
|
@@ -191,7 +185,7 @@ onChange({ matched }) {
|
|
|
191
185
|
for (const option of countryOptions) {
|
|
192
186
|
const value = option.dataset.choice;
|
|
193
187
|
const count = rows.filter(
|
|
194
|
-
(row) => rest.has(row
|
|
188
|
+
(row) => rest.has(row) && row.country === value
|
|
195
189
|
).length;
|
|
196
190
|
// …the project writes `count` wherever its design puts it
|
|
197
191
|
}
|
|
@@ -236,21 +230,12 @@ instead of flashing the whole list back. Nothing writes to the URL before
|
|
|
236
230
|
`attach`, because `attach` is what reads it — a `set` on an unattached instance
|
|
237
231
|
would overwrite state nobody had loaded.
|
|
238
232
|
|
|
239
|
-
A
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
emit, and links people have shared should keep resolving to the view they named.
|
|
244
|
-
|
|
245
|
-
### Two things it refuses at construction
|
|
246
|
-
|
|
247
|
-
Both are silent otherwise:
|
|
233
|
+
A flag is on by being there, as an HTML boolean attribute is: written as the bare
|
|
234
|
+
`?featured`, and **absent** when off. Reading asks only whether the name is
|
|
235
|
+
present, so `?featured=1` from an older link still reads as on — and so does a
|
|
236
|
+
hand-written `?featured=false`, just as `hidden="false"` still hides.
|
|
248
237
|
|
|
249
|
-
|
|
250
|
-
- **Two fields on one URL parameter.** Each write erases the other's value, which
|
|
251
|
-
reads as a filter that will not stay set.
|
|
252
|
-
|
|
253
|
-
And a `set` to the value already held announces nothing and pushes nothing, so
|
|
238
|
+
A `set` to the value already held announces nothing and pushes nothing, so
|
|
254
239
|
re-clicking the current tab does not add a history step to walk back through.
|
|
255
240
|
|
|
256
241
|
## `filters-view`
|
|
@@ -275,9 +260,9 @@ const list = filters({
|
|
|
275
260
|
});
|
|
276
261
|
const detach = list.attach();
|
|
277
262
|
const unbind = searchBox(list, "q", {
|
|
278
|
-
input:
|
|
279
|
-
clear:
|
|
280
|
-
empty:
|
|
263
|
+
input: ref<HTMLInputElement>(root, "[data-filter-search]"),
|
|
264
|
+
clear: ref(root, "[data-filter-clear]"),
|
|
265
|
+
empty: ref(root, "[data-filter-empty]"),
|
|
281
266
|
});
|
|
282
267
|
```
|
|
283
268
|
|
|
@@ -363,96 +348,70 @@ skips the helper and writes it in `onChange`. Nothing degrades.
|
|
|
363
348
|
|
|
364
349
|
## `element`
|
|
365
350
|
|
|
366
|
-
A
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
the facet simply never matches. `element` names every attribute a script and its
|
|
370
|
-
template share, types the values in them, and registers the behaviour.
|
|
351
|
+
A custom element that enhances server-rendered markup: `element` registers the
|
|
352
|
+
behaviour and runs it for as long as the element is on the page, and `ref` and
|
|
353
|
+
`refs` find what it works on by plain attribute selectors.
|
|
371
354
|
|
|
372
355
|
```ts
|
|
373
|
-
// faq.ts — imported by the template, and loaded by the page's script
|
|
374
|
-
export const faq = element("go-faq", {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
for (const { element, values } of row.all()) {
|
|
379
|
-
values.status; // "open" | "soon"
|
|
356
|
+
// faq.ts — imported by the template for the tag, and loaded by the page's script
|
|
357
|
+
export const faq = element("go-faq", ({ root, signal }) => {
|
|
358
|
+
const input = ref<HTMLInputElement>(root, "[data-faq-search]");
|
|
359
|
+
for (const row of refs<HTMLDetailsElement>(root, "[data-faq-row]")) {
|
|
360
|
+
row.dataset.key;
|
|
380
361
|
}
|
|
381
|
-
const input = search.one<HTMLInputElement>(); // a marker role is the element itself
|
|
382
362
|
});
|
|
383
363
|
```
|
|
384
364
|
|
|
385
365
|
```astro
|
|
366
|
+
---
|
|
367
|
+
import AtlasElement from "@escape-game-over/atlas/astro/element";
|
|
368
|
+
---
|
|
369
|
+
|
|
386
370
|
<script src="./faq.ts"></script>
|
|
387
371
|
|
|
388
|
-
<
|
|
389
|
-
<input
|
|
390
|
-
<details
|
|
391
|
-
</
|
|
372
|
+
<AtlasElement of={faq} class="…">
|
|
373
|
+
<input data-faq-search type="search">
|
|
374
|
+
<details data-faq-row data-key={key}>…</details>
|
|
375
|
+
</AtlasElement>
|
|
392
376
|
```
|
|
393
377
|
|
|
394
378
|
It registers itself when loaded in a browser and does nothing in Node, so the
|
|
395
|
-
template imports the same file for the
|
|
396
|
-
|
|
397
|
-
- **
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
- **
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
379
|
+
template imports the same file for the tag.
|
|
380
|
+
|
|
381
|
+
- **What the script asks for is what devtools shows.** A selector is a string
|
|
382
|
+
the template wrote as an attribute; there is no naming scheme to decode.
|
|
383
|
+
Prefix the names per element (`data-faq-…`) so two elements sharing a
|
|
384
|
+
subtree cannot pick up each other's.
|
|
385
|
+
- **Missing is loud in dev.** `ref` throws naming the root and the selector;
|
|
386
|
+
`refs` returns what it found, none included. A throw inside `connect` leaves
|
|
387
|
+
that element inert and lands in the dev panel, as does any uncaught error on
|
|
388
|
+
the page — a failing listener, a rejected `await`.
|
|
389
|
+
- **Values are the DOM's.** `data(row, "key")` reads `data-key` and throws, as
|
|
390
|
+
`ref` does, when it is missing; an optional one is `row.dataset.state`. Parse
|
|
391
|
+
where the script needs a number or a list. State a stylesheet reads is a plain attribute the
|
|
392
|
+
script sets — `data-blocked` — which Tailwind selects as `group-data-blocked:`.
|
|
393
|
+
- **The root says it is one.** `AtlasElement` renders the tag with
|
|
394
|
+
`data-atlas-root`, so a project gives every root a display in one stylesheet
|
|
395
|
+
rule. A root rendered by hand says so on the dev server.
|
|
396
|
+
|
|
397
|
+
A sentence the script updates keeps its words in the page. The copy wraps the
|
|
398
|
+
changing part in a variant run, `RichText`'s `attrs` gives that run an
|
|
399
|
+
attribute, and the script writes only that run:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
"faq.matched": { "en-US": "[v:count]{count}[/v] answers found" },
|
|
403
|
+
```
|
|
418
404
|
|
|
419
405
|
```astro
|
|
420
|
-
<
|
|
406
|
+
<RichText spans={rich("faq.matched", { count: "" })} attrs={{ count: { "data-faq-count": "" } }} />
|
|
421
407
|
```
|
|
422
408
|
|
|
423
409
|
```ts
|
|
424
|
-
|
|
410
|
+
for (const each of refs(root, "[data-faq-count]")) each.textContent = String(hits.size);
|
|
425
411
|
```
|
|
426
412
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
`write` sets fields back onto an element, for state a stylesheet reads — a
|
|
431
|
-
dropdown that is blocked — so the script uses the names the template did.
|
|
432
|
-
|
|
433
|
-
**What it still cannot reach is the stylesheet.** Tailwind generates CSS from
|
|
434
|
-
class names it finds as literals in the source, so a variant keyed on one of
|
|
435
|
-
these attributes has to spell it: `group-data-network-dropdown-blocked/dd:`.
|
|
436
|
-
That is the one place a rename is not a compile error. It is also the one place
|
|
437
|
-
a drift is visible, since the style stops applying, where a script reading the
|
|
438
|
-
wrong attribute fails silently. `attribute(field)` returns the name for the
|
|
439
|
-
consumers that can take a computed one, such as an `attributeFilter`.
|
|
440
|
-
|
|
441
|
-
It keeps the rule at the top of this page in the only reading that admits a
|
|
442
|
-
lookup: it finds only elements carrying its own marker, writes only its own
|
|
443
|
-
attributes, and sets no class, `aria` or `hidden`. The names are the project's,
|
|
444
|
-
so nothing in this package has to be matched.
|
|
445
|
-
|
|
446
|
-
- **Names come from the tag.** Every attribute is `data-<tag>-<role>` plus the
|
|
447
|
-
field, and the browser refuses to define one tag twice, so two elements cannot
|
|
448
|
-
share an attribute.
|
|
449
|
-
- **Lookups stay in their instance.** A nested copy of the element keeps its
|
|
450
|
-
own roles.
|
|
451
|
-
- **The root says it is one.** `{...faq.root}` writes `data-atlas-root`, so a
|
|
452
|
-
project gives every root a display in one stylesheet rule. Forget it and the
|
|
453
|
-
element says so on the dev server.
|
|
454
|
-
- **Names are checked in the editor.** A tag without a hyphen, or a role that is
|
|
455
|
-
not camelCase, is a compile error where it is written.
|
|
413
|
+
Marks are parsed before placeholders are filled, so a run filled with `""`
|
|
414
|
+
still renders as an empty element for the script to write into.
|
|
456
415
|
|
|
457
416
|
## `youtube`
|
|
458
417
|
|
|
@@ -587,16 +546,13 @@ right properties is a faithful stand-in.
|
|
|
587
546
|
`matches` read live as a browser's is, and fakes the `<video>` down to
|
|
588
547
|
`paused`, `currentSrc`, `networkState`, `autoplay`, `play`, `pause` and `load`,
|
|
589
548
|
including what resource selection does after a reload.
|
|
590
|
-
- `tests/
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
`type-tests/component.ts` pins the role types and the reserved keys.
|
|
598
|
-
|
|
599
|
-
`carousel`, `consent` and `element` have none. They need a real DOM and
|
|
549
|
+
- `tests/ref.test.ts` fakes a root down to its two query methods, and pins
|
|
550
|
+
that `ref` throws naming the root and the selector.
|
|
551
|
+
- `tests/element.test.ts` stubs `customElements` to upgrade on `define`, as a
|
|
552
|
+
browser does, and pins what `connect` is handed.
|
|
553
|
+
|
|
554
|
+
`carousel` and `consent` have none, and `element` has no lifecycle test — moves
|
|
555
|
+
and teardown. They need a real DOM and
|
|
600
556
|
this package carries no environment for one; adding `happy-dom` as a dev
|
|
601
557
|
dependency and setting `environment` in `vitest.config.ts` is what that would
|
|
602
558
|
take.
|
package/docs/rich-text.md
CHANGED
|
@@ -234,15 +234,20 @@ faqPage([{ question, answer: html(rich("faq.city.a", { network: "network" })) }]
|
|
|
234
234
|
|
|
235
235
|
## The renderer half
|
|
236
236
|
|
|
237
|
-
`@escape-game-over/atlas/astro/rich-text` renders the runs
|
|
238
|
-
|
|
237
|
+
`@escape-game-over/atlas/astro/rich-text` renders the runs, and only the runs:
|
|
238
|
+
the caller writes the element around them. The site supplies the look — a class
|
|
239
|
+
per `[v:name]` and one for links — usually in a small wrapper
|
|
239
240
|
([`examples/b2c/src/components/RichText.astro`](../examples/b2c/src/components/RichText.astro)):
|
|
240
241
|
|
|
241
242
|
```astro
|
|
242
|
-
<
|
|
243
|
+
<p class="…">
|
|
244
|
+
<RichText spans={rich("about.intro", { venue: "contact" })} variants={{ accent: "…" }} link="…" />
|
|
245
|
+
</p>
|
|
243
246
|
```
|
|
244
247
|
|
|
245
|
-
|
|
248
|
+
`attrs` puts attributes on a variant's run, so a script can find it and rewrite
|
|
249
|
+
its words — see [client-scripts.md](client-scripts.md). A variant with neither a
|
|
250
|
+
class nor attrs still renders its words, and warns at build.
|
|
246
251
|
|
|
247
252
|
## What fails, and where
|
|
248
253
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escape-game-over/atlas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
|
|
6
6
|
"private": false,
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"./client": "./src/astro/client.ts",
|
|
19
19
|
"./astro/consent-banner": "./src/astro/ConsentBanner.astro",
|
|
20
20
|
"./astro/document": "./src/astro/Document.astro",
|
|
21
|
+
"./astro/element": "./src/astro/AtlasElement.astro",
|
|
21
22
|
"./astro/image": "./src/astro/Image.astro",
|
|
22
23
|
"./astro/rich-text": "./src/astro/RichText.astro"
|
|
23
24
|
},
|
|
@@ -50,10 +51,10 @@
|
|
|
50
51
|
"typescript": ">=5"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
|
-
"@biomejs/biome": "2.5.
|
|
54
|
-
"@types/node": "26.
|
|
54
|
+
"@biomejs/biome": "2.5.14",
|
|
55
|
+
"@types/node": "26.6.2",
|
|
55
56
|
"@vitest/coverage-istanbul": "5.0.1",
|
|
56
|
-
"astro": "7.3.
|
|
57
|
+
"astro": "7.3.3",
|
|
57
58
|
"typescript": "6.0.3",
|
|
58
59
|
"vitest": "5.0.1"
|
|
59
60
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Renders a custom element with the root attribute: what a stylesheet gives
|
|
4
|
+
* every root its display by, and what `element` checks for in dev.
|
|
5
|
+
*
|
|
6
|
+
* ```astro
|
|
7
|
+
* <AtlasElement of={trailer} data-video={video} class="…">…</AtlasElement>
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
import type { HTMLAttributes } from "astro/types";
|
|
11
|
+
import { type ElementTag, ROOT_ATTRIBUTE } from "./element.ts";
|
|
12
|
+
|
|
13
|
+
interface Props extends HTMLAttributes<"div"> {
|
|
14
|
+
readonly of: ElementTag;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { of, ...rest } = Astro.props;
|
|
18
|
+
const Tag = of.tag;
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<Tag
|
|
22
|
+
{...rest}
|
|
23
|
+
{...{ [ROOT_ATTRIBUTE]: "" }}
|
|
24
|
+
>
|
|
25
|
+
<slot />
|
|
26
|
+
</Tag>
|
|
@@ -1,55 +1,61 @@
|
|
|
1
1
|
---
|
|
2
2
|
/** Internal to `ConsentBanner.astro`, so its script ships only where it renders. */
|
|
3
3
|
import type { HTMLAttributes } from "astro/types";
|
|
4
|
+
import AtlasElement from "./AtlasElement.astro";
|
|
4
5
|
import { consentBanner } from "./consent.ts";
|
|
5
6
|
|
|
6
7
|
type Props = HTMLAttributes<"div">;
|
|
7
8
|
---
|
|
8
9
|
|
|
9
|
-
<
|
|
10
|
+
<AtlasElement
|
|
11
|
+
of={consentBanner}
|
|
10
12
|
{...Astro.props}
|
|
11
|
-
{...consentBanner.root}
|
|
12
13
|
hidden
|
|
13
14
|
role="dialog"
|
|
14
15
|
aria-modal="false"
|
|
15
16
|
>
|
|
16
17
|
<slot />
|
|
17
|
-
</
|
|
18
|
+
</AtlasElement>
|
|
18
19
|
|
|
19
20
|
<script>
|
|
20
21
|
import {
|
|
21
22
|
applyConsent,
|
|
22
|
-
|
|
23
|
+
CONSENT_ANSWER,
|
|
24
|
+
CONSENT_REOPEN,
|
|
23
25
|
consentApplies,
|
|
24
|
-
consentReopen,
|
|
25
26
|
consentStore,
|
|
26
27
|
} from "./consent.ts";
|
|
27
28
|
import { element } from "./element.ts";
|
|
29
|
+
import { refs } from "./ref.ts";
|
|
28
30
|
|
|
29
|
-
element("atlas-consent",
|
|
31
|
+
element("atlas-consent", ({ root, signal }) => {
|
|
30
32
|
// The head script left no consent hook: nothing here sets a cookie.
|
|
31
33
|
if (!consentApplies()) return;
|
|
32
34
|
|
|
33
35
|
const store = consentStore();
|
|
34
36
|
const show = (): void => {
|
|
35
|
-
|
|
37
|
+
root.hidden = false;
|
|
36
38
|
};
|
|
37
39
|
|
|
38
|
-
for (const
|
|
39
|
-
|
|
40
|
+
for (const button of refs(root, `[${CONSENT_ANSWER}]`)) {
|
|
41
|
+
const choice = button.getAttribute(CONSENT_ANSWER);
|
|
42
|
+
if (choice !== "granted" && choice !== "denied") {
|
|
43
|
+
throw new Error(`${CONSENT_ANSWER} is "${choice}"`);
|
|
44
|
+
}
|
|
45
|
+
button.addEventListener(
|
|
40
46
|
"click",
|
|
41
47
|
() => {
|
|
42
|
-
store.record(
|
|
43
|
-
|
|
48
|
+
store.record(choice);
|
|
49
|
+
root.hidden = true;
|
|
44
50
|
},
|
|
45
51
|
{ signal }
|
|
46
52
|
);
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
// Outside the banner — withdrawing must be as easy as granting.
|
|
50
|
-
for (const
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
for (const button of refs(document, `[${CONSENT_REOPEN}]`)) {
|
|
57
|
+
button.hidden = false;
|
|
58
|
+
button.addEventListener("click", show, { signal });
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
// Applied, not re-recorded: recording stamps today's date, and an answer
|
package/src/astro/Image.astro
CHANGED
package/src/astro/MetaTags.astro
CHANGED
|
@@ -25,27 +25,40 @@ const { tags } = Astro.props;
|
|
|
25
25
|
// again as HTML would turn each quote into an entity and leave a block no
|
|
26
26
|
// parser reads.
|
|
27
27
|
if (tag.kind === "script") {
|
|
28
|
-
|
|
28
|
+
return (
|
|
29
|
+
<script
|
|
30
|
+
type={tag.type}
|
|
31
|
+
is:inline
|
|
32
|
+
set:html={tag.content}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
29
35
|
}
|
|
30
36
|
// `is:inline` so the bundler leaves a third-party URL alone, and `defer`
|
|
31
37
|
// because a script that blocks the parser costs the thing it measures.
|
|
32
38
|
if (tag.kind === "externalScript") {
|
|
33
|
-
|
|
39
|
+
return (
|
|
40
|
+
<script
|
|
41
|
+
is:inline
|
|
42
|
+
defer
|
|
43
|
+
src={tag.src}
|
|
44
|
+
{...tag.attrs}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
34
47
|
}
|
|
35
48
|
// Tag Manager's fallback, and only ever in the body list. Hidden the way
|
|
36
49
|
// Google's own snippet hides it — a zero-sized iframe nothing can see.
|
|
37
50
|
if (tag.kind === "noscriptFrame") {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
return (
|
|
52
|
+
<noscript>
|
|
53
|
+
<iframe
|
|
54
|
+
src={tag.src}
|
|
55
|
+
title='Google Tag Manager'
|
|
56
|
+
height='0'
|
|
57
|
+
width='0'
|
|
58
|
+
style='display:none;visibility:hidden'
|
|
59
|
+
/>
|
|
60
|
+
</noscript>
|
|
61
|
+
);
|
|
49
62
|
}
|
|
50
63
|
if (tag.kind === "meta") return <meta {...tag.attrs} />;
|
|
51
64
|
// Named rather than left to a trailing `return`, which used to catch
|
|
@@ -59,4 +72,4 @@ const { tags } = Astro.props;
|
|
|
59
72
|
// copied into every project.
|
|
60
73
|
const unhandled: never = tag;
|
|
61
74
|
throw new Error(`Unhandled meta tag: ${JSON.stringify(unhandled)}`);
|
|
62
|
-
|
|
75
|
+
})}
|