@escape-game-over/atlas 0.1.24 → 0.1.26
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/README.md +27 -44
- package/bin/use-project.mjs +18 -13
- package/docs/NOT-BUILT.md +1 -1
- package/docs/client-scripts.md +96 -193
- package/docs/rich-text.md +18 -20
- package/package.json +9 -15
- package/src/analytics/google.ts +6 -6
- package/src/analytics/index.ts +4 -3
- package/src/analytics/tags.ts +13 -58
- package/src/analytics/umami.ts +8 -8
- package/src/astro/AtlasElement.astro +26 -0
- package/src/astro/ConsentBanner.astro +25 -0
- package/src/astro/ConsentElement.astro +67 -0
- package/src/astro/Document.astro +44 -0
- package/src/astro/Image.astro +101 -0
- package/src/astro/MetaTags.astro +30 -40
- package/src/astro/RichText.astro +88 -0
- package/src/astro/Zoom.astro +61 -0
- package/src/astro/client.ts +18 -9
- package/src/astro/consent.ts +20 -0
- package/src/astro/dev-log.ts +8 -14
- package/src/astro/element.ts +76 -112
- package/src/astro/filters-view.ts +48 -64
- package/src/astro/filters.ts +42 -35
- package/src/astro/index.ts +2 -9
- package/src/astro/ref.ts +46 -0
- package/src/astro/site-routes.ts +9 -15
- package/src/config.ts +23 -36
- package/src/content/index.ts +1 -1
- package/src/content/marks.ts +13 -13
- package/src/content/rich.ts +26 -42
- package/src/hours.ts +48 -11
- package/src/i18n/define.ts +14 -74
- package/src/index.ts +40 -57
- package/src/meta/index.ts +7 -13
- package/src/meta/share-image.ts +2 -26
- package/src/meta/tag.ts +1 -45
- package/src/money.ts +161 -6
- package/src/project.ts +84 -73
- package/src/routes/define.ts +8 -44
- package/src/routes/resolve.ts +1 -1
- package/src/site/api.ts +7 -33
- package/src/site/create.ts +6 -10
- package/src/site/define.ts +120 -0
- package/src/site/index.ts +2 -5
- package/src/site/page.ts +4 -2
- package/src/sitemap.ts +2 -35
- package/src/warn.ts +16 -17
- package/src/astro/dom.ts +0 -35
- package/src/astro/markup.ts +0 -656
package/docs/client-scripts.md
CHANGED
|
@@ -32,33 +32,26 @@ the referrer policy — and everything that does, classes above all, comes from
|
|
|
32
32
|
caller through a callback. It too is handed the button and the box rather than
|
|
33
33
|
finding them.
|
|
34
34
|
|
|
35
|
-
| Module
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
the banner that is rendered. See NOT-BUILT.md on where the halves divide.
|
|
56
|
-
|
|
57
|
-
**A script imports one path: `@escape-game-over/atlas/client`**, which re-exports
|
|
58
|
-
every module above. All of it exists to touch a live document, so it belongs in
|
|
59
|
-
a `<script src>` and not in frontmatter or `astro.config.ts`. A component's
|
|
60
|
-
contract is the one thing both halves need, and it stays on `./astro/markup`,
|
|
61
|
-
which touches nothing.
|
|
35
|
+
| Module | Owns | Leaves to the project |
|
|
36
|
+
| ------------------ | -------------------------------------------------------------- | ----------------------------------------------- |
|
|
37
|
+
| `carousel` | which slide is current: modulo, swipe, autoplay, the move lock | the transform, dots, arrows, `aria` |
|
|
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 |
|
|
40
|
+
| `youtube` | the swap from poster to player, on the click and not before | the poster, the button, the iframe's classes |
|
|
41
|
+
| `background-video` | playing or paused, playable or not, and which cut is loaded | the play/pause control, its icons and its label |
|
|
42
|
+
| `consent` | remembering an answer, expiring it, handing it to Google | the banner — its wording, its buttons, its law |
|
|
43
|
+
| `element` | registering a custom element, and ending what it started | what the element does while it is on the page |
|
|
44
|
+
|
|
45
|
+
`consent` is the one module that ships its own behaviour: `<ConsentBanner
|
|
46
|
+
analytics={…}>` (`@escape-game-over/atlas/astro/consent-banner`) renders the
|
|
47
|
+
site's markup as its children, skips itself — script included — when the
|
|
48
|
+
analytics need no permission, and remembers, expires and applies the answer. The
|
|
49
|
+
markup spreads `consent.accept` and `consent.decline` on its buttons, and
|
|
50
|
+
`consent.reopen` on the control that brings it back.
|
|
51
|
+
|
|
52
|
+
**Browser code has one import path: `@escape-game-over/atlas/client`**, which
|
|
53
|
+
re-exports every module above. None of it touches the DOM at import, so an
|
|
54
|
+
element's contract can be imported in frontmatter from the same path.
|
|
62
55
|
|
|
63
56
|
## Lifetimes are the recurring bug
|
|
64
57
|
|
|
@@ -75,21 +68,17 @@ const detach = loop.attach(video); // background-video: the <video>
|
|
|
75
68
|
const detach = trailer.attach(button, box); // youtube: what is pressed, what it replaces
|
|
76
69
|
```
|
|
77
70
|
|
|
78
|
-
`
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
everything else.
|
|
71
|
+
`element` says the same thing in the shape a custom element needs: its function
|
|
72
|
+
runs when the element enters the page, `signal` ends what it registered when the
|
|
73
|
+
element leaves, and what it returns is the undo for everything else.
|
|
82
74
|
|
|
83
75
|
```ts
|
|
84
|
-
|
|
85
|
-
host.addEventListener("click", open, { signal }); // ends with the visit
|
|
86
|
-
return loop.attach(host);
|
|
76
|
+
export const thing = element("go-thing", ({ host, signal }) => {
|
|
77
|
+
ref(host, "[data-thing-button]").addEventListener("click", open, { signal }); // ends with the visit
|
|
78
|
+
return loop.attach(host); // and so does this
|
|
87
79
|
});
|
|
88
80
|
```
|
|
89
81
|
|
|
90
|
-
It takes the component rather than a tag name, so the element it registers and
|
|
91
|
-
the roles it hands the function are the same contract the template spread.
|
|
92
|
-
|
|
93
82
|
An element can enter the page more than once — moving it in the DOM runs the
|
|
94
83
|
undo and then the function again — so it has to be able to run twice. State that
|
|
95
84
|
must survive a move goes in a `WeakMap` keyed by `host`, which is what the
|
|
@@ -114,9 +103,9 @@ the URL. The caller declares fields; the item shape follows from them.
|
|
|
114
103
|
```ts
|
|
115
104
|
const list = filters({
|
|
116
105
|
fields: {
|
|
117
|
-
q: { kind: "text"
|
|
118
|
-
category: { kind: "choice"
|
|
119
|
-
featured: { kind: "flag"
|
|
106
|
+
q: { kind: "text" },
|
|
107
|
+
category: { kind: "choice" },
|
|
108
|
+
featured: { kind: "flag" },
|
|
120
109
|
},
|
|
121
110
|
items: entries.map((entry) => ({
|
|
122
111
|
key: entry.id,
|
|
@@ -186,7 +175,7 @@ pass. What is deliberately absent is one field holding several selected values;
|
|
|
186
175
|
that is a `choices` kind, and it forces a URL encoding decision (repeated
|
|
187
176
|
parameters or comma-joined) that nothing has needed yet.
|
|
188
177
|
|
|
189
|
-
`param`
|
|
178
|
+
`param` defaults to the field's name. `param: false` keeps a field state-only, out of the URL.
|
|
190
179
|
|
|
191
180
|
### Facet counts: `matchedWithout`
|
|
192
181
|
|
|
@@ -278,25 +267,23 @@ The twelve lines every one of the three wrote around its search field, byte for
|
|
|
278
267
|
byte.
|
|
279
268
|
|
|
280
269
|
```ts
|
|
281
|
-
const search = searchBox({
|
|
282
|
-
input: one<HTMLInputElement>("[data-filter-search]"),
|
|
283
|
-
clear: one("[data-filter-clear]"),
|
|
284
|
-
empty: one("[data-filter-empty]"),
|
|
285
|
-
});
|
|
286
|
-
|
|
287
270
|
const list = filters({
|
|
288
271
|
fields, items,
|
|
289
|
-
onChange({
|
|
290
|
-
search.render(state.q, matched.size);
|
|
272
|
+
onChange({ matched }) {
|
|
291
273
|
// …everything this list draws for itself
|
|
292
274
|
},
|
|
293
275
|
});
|
|
294
|
-
|
|
295
|
-
const unbind = search.bind(list, "q");
|
|
296
276
|
const detach = list.attach();
|
|
277
|
+
const unbind = searchBox(list, "q", {
|
|
278
|
+
input: one<HTMLInputElement>("[data-filter-search]"),
|
|
279
|
+
clear: one("[data-filter-clear]"),
|
|
280
|
+
empty: one("[data-filter-empty]"),
|
|
281
|
+
});
|
|
297
282
|
```
|
|
298
283
|
|
|
299
|
-
|
|
284
|
+
It paints the list's current state at once, then repaints on every change
|
|
285
|
+
through `list.subscribe`, after the page's own `onChange`. The part worth
|
|
286
|
+
centralising is this:
|
|
300
287
|
|
|
301
288
|
```ts
|
|
302
289
|
if (input != null && input.value !== query) input.value = query;
|
|
@@ -306,19 +293,13 @@ It exists because `popstate` and `reset` move state without touching the
|
|
|
306
293
|
keyboard — forget it and the back button leaves a stale query sitting in the box
|
|
307
294
|
while the list shows something else. The inequality is not an optimisation:
|
|
308
295
|
assigning `value` moves the caret to the end, so writing it unconditionally would
|
|
309
|
-
make the field unusable mid-word.
|
|
310
|
-
|
|
311
|
-
`bind` is a second call rather than part of construction because of an ordering
|
|
312
|
-
knot. `render` runs inside the list's `onChange`, so the box must exist before
|
|
313
|
-
the list; `bind` needs the list. Two `const`s in sequence is the honest shape,
|
|
314
|
-
and the alternative is a `let` the reader has to carry. Its return is the undo,
|
|
315
|
-
like `attach` — so a page-load teardown aborts both.
|
|
296
|
+
make the field unusable mid-word. Its return is the undo, like `attach`.
|
|
316
297
|
|
|
317
298
|
`TextFieldOf<F>` restricts the second argument to the list's `text` fields, so a
|
|
318
299
|
box pointed at a `flag` is a compile error rather than a filter that silently
|
|
319
300
|
never matches.
|
|
320
301
|
|
|
321
|
-
Every element is optional and `null` is accepted, because `
|
|
302
|
+
Every element is optional and `null` is accepted, because a role's `one()`
|
|
322
303
|
returns `null` and a page may have a search field with no clear button. Handing
|
|
323
304
|
over what a lookup returned, unchecked, is the point.
|
|
324
305
|
|
|
@@ -333,7 +314,7 @@ errors, and the page looks wrong in a way that does not point at the filter.
|
|
|
333
314
|
```ts
|
|
334
315
|
onChange({ matched }) {
|
|
335
316
|
for (const item of items) item.hidden = !matched.has(keyOf(item));
|
|
336
|
-
hideEmpty(groups, (group) =>
|
|
317
|
+
hideEmpty(groups, (group) => rowsIn.get(group) ?? []);
|
|
337
318
|
}
|
|
338
319
|
```
|
|
339
320
|
|
|
@@ -346,8 +327,8 @@ city in it is hidden; a region is empty when every country in it is hidden,
|
|
|
346
327
|
**Which makes the order load-bearing: innermost first.**
|
|
347
328
|
|
|
348
329
|
```ts
|
|
349
|
-
hideEmpty(countries, (c) =>
|
|
350
|
-
hideEmpty(regions, (r) =>
|
|
330
|
+
hideEmpty(countries, (c) => citiesIn.get(c) ?? []);
|
|
331
|
+
hideEmpty(regions, (r) => countriesIn.get(r) ?? []);
|
|
351
332
|
```
|
|
352
333
|
|
|
353
334
|
Run the other way round, the regions are judged against countries nothing has
|
|
@@ -380,147 +361,72 @@ filters({ … }) + searchBox + hideEmpty // the roll-up gone too
|
|
|
380
361
|
A page whose markup wants a class instead of `hidden`, or removal from the DOM,
|
|
381
362
|
skips the helper and writes it in `onChange`. Nothing degrades.
|
|
382
363
|
|
|
383
|
-
## `
|
|
364
|
+
## `element`
|
|
384
365
|
|
|
385
|
-
A
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
the facet simply never matches: the page reads as a filter with no results, not
|
|
389
|
-
as a typo. A shared constant pins one name. `markup` pins every name a script
|
|
390
|
-
and its template share, and the values in them.
|
|
366
|
+
A custom element that enhances server-rendered markup: `element` registers the
|
|
367
|
+
behaviour and runs it for as long as the element is on the page, and `ref` and
|
|
368
|
+
`refs` find what it works on by plain attribute selectors.
|
|
391
369
|
|
|
392
370
|
```ts
|
|
393
|
-
//
|
|
394
|
-
export const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
371
|
+
// faq.ts — imported by the template for the tag, and loaded by the page's script
|
|
372
|
+
export const faq = element("go-faq", ({ host, signal }) => {
|
|
373
|
+
const input = ref<HTMLInputElement>(host, "[data-faq-search]");
|
|
374
|
+
for (const row of refs<HTMLDetailsElement>(host, "[data-faq-row]")) {
|
|
375
|
+
row.dataset.key;
|
|
376
|
+
}
|
|
398
377
|
});
|
|
399
|
-
export const search = markup("network-search");
|
|
400
378
|
```
|
|
401
379
|
|
|
402
380
|
```astro
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
```ts
|
|
408
|
-
for (const { element, values } of row.all(this)) {
|
|
409
|
-
values.status; // "open" | "soon"
|
|
410
|
-
}
|
|
411
|
-
const input = search.require<HTMLInputElement>(this).element;
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
- **Names are derived, never spelled.** `data-network-row` marks the element and
|
|
415
|
-
`data-network-row-status` holds a field. A field that does not exist is a
|
|
416
|
-
compile error in the template and in the script.
|
|
417
|
-
- **Values are typed on the way in.** `attrs` takes each field's type: a missing
|
|
418
|
-
required field, or a status outside its list, fails `astro check`. It also
|
|
419
|
-
checks at run time, because this runs at build time and a value can arrive as
|
|
420
|
-
a plain `string` from an env var or a cast; a throw there fails the build
|
|
421
|
-
rather than shipping a page whose script throws on every visit.
|
|
422
|
-
- **And parsed on the way out.** `read`, `all`, `one` and `require` parse every
|
|
423
|
-
field and throw, naming the element and the attribute, when one is missing or
|
|
424
|
-
malformed. A malformed match fails the whole `all` rather than being skipped:
|
|
425
|
-
a list that quietly loses a row is the failure this exists to prevent.
|
|
426
|
-
|
|
427
|
-
The kinds are data, not builders, for the reason `filters` gives:
|
|
428
|
-
`text`, `number`, `list`, `template`, `choice` and `flag`, all but `flag`
|
|
429
|
-
optionally `optional`. A `flag` is off by being absent, never `"false"`, so a
|
|
430
|
-
stylesheet can select it with a bare attribute.
|
|
431
|
-
|
|
432
|
-
A `template` is the one that keeps a sentence out of the script. It is written
|
|
433
|
-
as the translated string with its placeholders left in, and read back as the
|
|
434
|
-
function that fills them:
|
|
381
|
+
---
|
|
382
|
+
import AtlasElement from "@escape-game-over/atlas/astro/element";
|
|
383
|
+
---
|
|
435
384
|
|
|
436
|
-
|
|
437
|
-
<p {...faq.line.attrs({ sentence: t("faq.matched", { count: "{count}" }) })}></p>
|
|
438
|
-
```
|
|
385
|
+
<script src="./faq.ts"></script>
|
|
439
386
|
|
|
440
|
-
|
|
441
|
-
|
|
387
|
+
<AtlasElement of={faq} class="…">
|
|
388
|
+
<input data-faq-search type="search">
|
|
389
|
+
<details data-faq-row data-key={key}>…</details>
|
|
390
|
+
</AtlasElement>
|
|
442
391
|
```
|
|
443
392
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
two components can pick alike, and scope, since `querySelectorAll` from an
|
|
467
|
-
element also finds everything inside a nested copy of that element. `component`
|
|
468
|
-
ties every role to a custom element and settles both.
|
|
393
|
+
It registers itself when loaded in a browser and does nothing in Node, so the
|
|
394
|
+
template imports the same file for the tag.
|
|
395
|
+
|
|
396
|
+
- **What the script asks for is what devtools shows.** A selector is a string
|
|
397
|
+
the template wrote as an attribute; there is no naming scheme to decode.
|
|
398
|
+
Prefix the names per element (`data-faq-…`) so two elements sharing a
|
|
399
|
+
subtree cannot pick up each other's.
|
|
400
|
+
- **Missing is loud in dev.** `ref` throws naming the root and the selector;
|
|
401
|
+
`refs` returns what it found, none included. A throw inside `connect` leaves
|
|
402
|
+
that element inert and lands in the dev panel, as does any uncaught error on
|
|
403
|
+
the page — a failing listener, a rejected `await`.
|
|
404
|
+
- **Values are the DOM's.** `data(row, "key")` reads `data-key` and throws, as
|
|
405
|
+
`ref` does, when it is missing; an optional one is `row.dataset.state`. Parse
|
|
406
|
+
where the script needs a number or a list. State a stylesheet reads is a plain attribute the
|
|
407
|
+
script sets — `data-blocked` — which Tailwind selects as `group-data-blocked:`.
|
|
408
|
+
- **The root says it is one.** `AtlasElement` renders the tag with
|
|
409
|
+
`data-atlas-root`, so a project gives every root a display in one stylesheet
|
|
410
|
+
rule. A root rendered by hand says so on the dev server.
|
|
411
|
+
|
|
412
|
+
A sentence the script updates keeps its words in the page. The copy wraps the
|
|
413
|
+
changing part in a variant run, `RichText`'s `attrs` gives that run an
|
|
414
|
+
attribute, and the script writes only that run:
|
|
469
415
|
|
|
470
416
|
```ts
|
|
471
|
-
|
|
472
|
-
row: { key: { kind: "text" }, text: { kind: "text" } },
|
|
473
|
-
search: {},
|
|
474
|
-
});
|
|
417
|
+
"faq.matched": { "en-US": "[v:count]{count}[/v] answers found" },
|
|
475
418
|
```
|
|
476
419
|
|
|
477
420
|
```astro
|
|
478
|
-
<faq.
|
|
479
|
-
<input {...faq.search.attrs()} type="search">
|
|
480
|
-
<details {...faq.row.attrs({ key, text })}>…</details>
|
|
481
|
-
</faq.tag>
|
|
421
|
+
<RichText spans={rich("faq.matched", { count: "" })} attrs={{ count: { "data-faq-count": "" } }} />
|
|
482
422
|
```
|
|
483
423
|
|
|
484
424
|
```ts
|
|
485
|
-
|
|
486
|
-
for (const { element, values } of row.all()) { … }
|
|
487
|
-
});
|
|
425
|
+
for (const each of refs(host, "[data-faq-count]")) each.textContent = String(hits.size);
|
|
488
426
|
```
|
|
489
427
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
cannot share an attribute. Two roles of one component that would write the
|
|
493
|
-
same attribute — `search` with a field `clear`, and a role `searchClear` — are
|
|
494
|
-
refused when the component is created.
|
|
495
|
-
- **Lookups stay in their instance.** `all`, `one` and `require` return only
|
|
496
|
-
elements whose nearest ancestor with this tag is the root's. A nested copy
|
|
497
|
-
keeps its elements, and a lookup from an element inside the instance, like a
|
|
498
|
-
dropdown's own panel, still counts as that instance. Elements are filtered
|
|
499
|
-
before they are read, so a broken nested copy cannot fail the outer lookup.
|
|
500
|
-
- **The tag is written once.** `faq.tag` is what the template renders, and the
|
|
501
|
-
component itself is what `defineElement` registers the behaviour under. A contract stays inert either
|
|
502
|
-
way: it names things, and nothing in it touches a document, which is why
|
|
503
|
-
frontmatter can import it.
|
|
504
|
-
- **The names are checked in the editor.** A tag without a hyphen, or a role
|
|
505
|
-
that is not camelCase, is a compile error where it is written — the rules are
|
|
506
|
-
types, character by character, as in `i18n/placeholders.ts`. Nothing is
|
|
507
|
-
validated at run time, because a name that reached the browser malformed would
|
|
508
|
-
already have been refused there: by `setAttribute`, by `querySelectorAll`, or
|
|
509
|
-
by `customElements.define`.
|
|
510
|
-
- **Most roles carry nothing**, and say so: `search: marker` rather than
|
|
511
|
-
`search: {}`, which reads like options somebody forgot to fill in.
|
|
512
|
-
|
|
513
|
-
- **The roles arrive bound to the instance.** `defineElement` takes the
|
|
514
|
-
component, so `all`, `one` and `require` need no root: there is no way to
|
|
515
|
-
write `faq.row.all(document)`, which compiles and returns the roles belonging
|
|
516
|
-
to no instance at all.
|
|
517
|
-
- **The root says it is one.** `{...faq.root}` writes `data-atlas-root`, so a
|
|
518
|
-
project gives every root a display in one stylesheet rule rather than a class
|
|
519
|
-
per component. Forget it and the element says so on the dev server.
|
|
520
|
-
|
|
521
|
-
`tag` and `root` are the component's own keys, so no role may take them. A role
|
|
522
|
-
that lives outside every instance, such as a footer button that reopens a
|
|
523
|
-
banner, is still plain `markup`.
|
|
428
|
+
Marks are parsed before placeholders are filled, so a run filled with `""`
|
|
429
|
+
still renders as an empty element for the script to write into.
|
|
524
430
|
|
|
525
431
|
## `youtube`
|
|
526
432
|
|
|
@@ -655,16 +561,13 @@ right properties is a faithful stand-in.
|
|
|
655
561
|
`matches` read live as a browser's is, and fakes the `<video>` down to
|
|
656
562
|
`paused`, `currentSrc`, `networkState`, `autoplay`, `play`, `pause` and `load`,
|
|
657
563
|
including what resource selection does after a reload.
|
|
658
|
-
- `tests/
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
`type-tests/component.ts` pins the role types and the reserved keys.
|
|
666
|
-
|
|
667
|
-
`carousel`, `consent`, `element` and `dom` have none. They need a real DOM and
|
|
564
|
+
- `tests/ref.test.ts` fakes a root down to its two query methods, and pins
|
|
565
|
+
that `ref` throws naming the root and the selector.
|
|
566
|
+
- `tests/element.test.ts` stubs `customElements` to upgrade on `define`, as a
|
|
567
|
+
browser does, and pins what `connect` is handed.
|
|
568
|
+
|
|
569
|
+
`carousel` and `consent` have none, and `element` has no lifecycle test — moves
|
|
570
|
+
and teardown. They need a real DOM and
|
|
668
571
|
this package carries no environment for one; adding `happy-dom` as a dev
|
|
669
572
|
dependency and setting `environment` in `vitest.config.ts` is what that would
|
|
670
573
|
take.
|
package/docs/rich-text.md
CHANGED
|
@@ -29,10 +29,10 @@ Five marks, one void mark, two escapes. That is the whole vocabulary.
|
|
|
29
29
|
| Written | Short for | Run | Argument | Notes |
|
|
30
30
|
| ---------------- | ----------- | ---------------------------------- | -------- | -------------------------------------------- |
|
|
31
31
|
| `[b]…[/b]` | **b**old | `{ kind: "bold" }` | — | `<strong>` — emphasis, not a font weight |
|
|
32
|
-
| `[v:name]…[/v]` | **v**ariant | `{ kind: "
|
|
32
|
+
| `[v:name]…[/v]` | **v**ariant | `{ kind: "variant", variant }` | required | the renderer maps `name` to classes |
|
|
33
33
|
| `[a:slot]…[/a]` | **a**nchor | `{ kind: "link", to, href, url? }` | required | the **call site** says where `slot` goes |
|
|
34
|
-
| `[mail]…[/mail]` | `mailto:` | `{ kind: "
|
|
35
|
-
| `[tel]…[/tel]` | `tel:` | `{ kind: "
|
|
34
|
+
| `[mail]…[/mail]` | `mailto:` | `{ kind: "mail", href }` | — | the wrapped text must be the address |
|
|
35
|
+
| `[tel]…[/tel]` | `tel:` | `{ kind: "tel", href }` | — | the wrapped text must be the number |
|
|
36
36
|
| `[br]` | **br**eak | `{ kind: "break" }` | — | wraps nothing, closed by nothing |
|
|
37
37
|
| `[[` and `]]` | an escape | literal `[` and `]` | — | the escapes, matching `{{` and `}}` in `t()` |
|
|
38
38
|
|
|
@@ -192,7 +192,7 @@ what to do:
|
|
|
192
192
|
|
|
193
193
|
```txt
|
|
194
194
|
Message "about.intro" (en-US) carries marks, and t() can only print them.
|
|
195
|
-
Read it with rich(), or with plain(
|
|
195
|
+
Read it with rich(), or with site.plain() for the words alone.
|
|
196
196
|
```
|
|
197
197
|
|
|
198
198
|
A malformed mark fails there too, with the same error it would give anywhere
|
|
@@ -215,10 +215,8 @@ const plainText = site.plain(locale);
|
|
|
215
215
|
plainText("about.intro", { company }) // no `venue`, no `ask`
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
-
`plain(rich(…))` is the same answer when the runs are already in hand.
|
|
219
|
-
|
|
220
218
|
Reach for `t()` when the copy is structurally plain and should stay that way — a
|
|
221
|
-
button label, an `aria-label`. Reach for `plain()` when the answer is prose: it
|
|
219
|
+
button label, an `aria-label`. Reach for `site.plain()` when the answer is prose: it
|
|
222
220
|
keeps working on the day someone adds emphasis to the sentence, where `t()` would
|
|
223
221
|
start throwing.
|
|
224
222
|
|
|
@@ -236,20 +234,20 @@ faqPage([{ question, answer: html(rich("faq.city.a", { network: "network" })) }]
|
|
|
236
234
|
|
|
237
235
|
## The renderer half
|
|
238
236
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
240
|
+
([`examples/b2c/src/components/RichText.astro`](../examples/b2c/src/components/RichText.astro)):
|
|
242
241
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
242
|
+
```astro
|
|
243
|
+
<p class="…">
|
|
244
|
+
<RichText spans={rich("about.intro", { venue: "contact" })} variants={{ accent: "…" }} link="…" />
|
|
245
|
+
</p>
|
|
246
|
+
```
|
|
247
247
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
sweep through every deployment's sentences. It falls back to no classes for a
|
|
252
|
-
variant it does not know — losing the sentence is the worse failure.
|
|
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.
|
|
253
251
|
|
|
254
252
|
## What fails, and where
|
|
255
253
|
|
|
@@ -270,4 +268,4 @@ variant it does not know — losing the sentence is the worse failure.
|
|
|
270
268
|
| `[tel]` around a local number | build error — write it with a country code |
|
|
271
269
|
| `http://` as a link target | build error — an `http://` link is a downgrade |
|
|
272
270
|
| `"contact#"` — a fragment that names nothing | build error |
|
|
273
|
-
| A marked message read with `t()` | build error, pointing at `rich()` and `plain()`
|
|
271
|
+
| A marked message read with `t()` | build error, pointing at `rich()` and `site.plain()` |
|
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.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
|
|
6
6
|
"private": false,
|
|
@@ -15,18 +15,12 @@
|
|
|
15
15
|
".": "./src/index.ts",
|
|
16
16
|
"./astro": "./src/astro/index.ts",
|
|
17
17
|
"./astro/images": "./src/astro/images.ts",
|
|
18
|
-
"./astro/background-video": "./src/astro/background-video.ts",
|
|
19
|
-
"./astro/carousel": "./src/astro/carousel.ts",
|
|
20
18
|
"./client": "./src/astro/client.ts",
|
|
21
|
-
"./astro/consent": "./src/astro/
|
|
22
|
-
"./astro/
|
|
23
|
-
"./astro/
|
|
24
|
-
"./astro/
|
|
25
|
-
"./astro/
|
|
26
|
-
"./astro/filters-view": "./src/astro/filters-view.ts",
|
|
27
|
-
"./astro/markup": "./src/astro/markup.ts",
|
|
28
|
-
"./astro/meta-tags": "./src/astro/MetaTags.astro",
|
|
29
|
-
"./astro/youtube": "./src/astro/youtube.ts"
|
|
19
|
+
"./astro/consent-banner": "./src/astro/ConsentBanner.astro",
|
|
20
|
+
"./astro/document": "./src/astro/Document.astro",
|
|
21
|
+
"./astro/element": "./src/astro/AtlasElement.astro",
|
|
22
|
+
"./astro/image": "./src/astro/Image.astro",
|
|
23
|
+
"./astro/rich-text": "./src/astro/RichText.astro"
|
|
30
24
|
},
|
|
31
25
|
"bin": {
|
|
32
26
|
"atlas": "bin/use-project.mjs"
|
|
@@ -57,10 +51,10 @@
|
|
|
57
51
|
"typescript": ">=5"
|
|
58
52
|
},
|
|
59
53
|
"devDependencies": {
|
|
60
|
-
"@biomejs/biome": "2.5.
|
|
61
|
-
"@types/node": "26.
|
|
54
|
+
"@biomejs/biome": "2.5.14",
|
|
55
|
+
"@types/node": "26.6.2",
|
|
62
56
|
"@vitest/coverage-istanbul": "5.0.1",
|
|
63
|
-
"astro": "7.3.
|
|
57
|
+
"astro": "7.3.3",
|
|
64
58
|
"typescript": "6.0.3",
|
|
65
59
|
"vitest": "5.0.1"
|
|
66
60
|
}
|
package/src/analytics/google.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { warn } from "../warn.ts";
|
|
2
|
-
import { type AnalyticsTags, literal } from "./tags.ts";
|
|
2
|
+
import { type AnalyticsTags, literal, preconnect } from "./tags.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Google's tags — Analytics and Tag Manager — and the consent state they read.
|
|
@@ -366,16 +366,16 @@ export function googleScripts(
|
|
|
366
366
|
// Ahead of the inline block, which is the only position that buys
|
|
367
367
|
// anything: for a container the loader's URL is written *by* that
|
|
368
368
|
// block, so this is the only mention of the origin the browser can
|
|
369
|
-
// act on before the script has run.
|
|
370
|
-
|
|
371
|
-
{ kind: "
|
|
369
|
+
// act on before the script has run.
|
|
370
|
+
preconnect(TAG_ORIGIN),
|
|
371
|
+
{ kind: "script", content: inline },
|
|
372
372
|
...(first === undefined
|
|
373
373
|
? []
|
|
374
374
|
: [
|
|
375
375
|
{
|
|
376
|
-
kind: "
|
|
376
|
+
kind: "externalScript" as const,
|
|
377
377
|
src: `${TAG_ORIGIN}/gtag/js?id=${encodeURIComponent(first)}`,
|
|
378
|
-
|
|
378
|
+
attrs: {},
|
|
379
379
|
},
|
|
380
380
|
]),
|
|
381
381
|
],
|
package/src/analytics/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { MetaTag } from "../meta/tag.ts";
|
|
1
2
|
import type { HttpsUrl } from "../url.ts";
|
|
2
3
|
import { type GoogleSettings, googleEmits, googleScripts } from "./google.ts";
|
|
3
|
-
import type {
|
|
4
|
+
import type { AnalyticsTags } from "./tags.ts";
|
|
4
5
|
import {
|
|
5
6
|
checkUmamiDomains,
|
|
6
7
|
type UmamiSettings,
|
|
@@ -28,7 +29,7 @@ export {
|
|
|
28
29
|
type ConsentState,
|
|
29
30
|
type GoogleSettings,
|
|
30
31
|
} from "./google.ts";
|
|
31
|
-
export type {
|
|
32
|
+
export type { AnalyticsTags } from "./tags.ts";
|
|
32
33
|
export type {
|
|
33
34
|
UmamiReplay,
|
|
34
35
|
UmamiSettings,
|
|
@@ -134,6 +135,6 @@ export const NOT_FOUND_TAG = "404";
|
|
|
134
135
|
*/
|
|
135
136
|
export function notFoundAnalytics(
|
|
136
137
|
analytics: AnalyticsSettings | undefined
|
|
137
|
-
): readonly
|
|
138
|
+
): readonly MetaTag[] {
|
|
138
139
|
return umamiScripts(analytics?.umami, NOT_FOUND_TAG);
|
|
139
140
|
}
|