@escape-game-over/atlas 0.1.25 → 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.
@@ -73,9 +73,9 @@ 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", { button: marker }, (host, { button }, signal) => {
77
- button.one()?.addEventListener("click", open, { signal }); // ends with the visit
78
- return loop.attach(host); // and so does this
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
79
79
  });
80
80
  ```
81
81
 
@@ -363,96 +363,70 @@ skips the helper and writes it in `onChange`. Nothing degrades.
363
363
 
364
364
  ## `element`
365
365
 
366
- A script that enhances server-rendered markup has to agree with the template on
367
- attribute names, and nothing checks that agreement. `data-filter-regoin` in the
368
- template and `dataset.filterRegion` in the script compile, build and ship, and
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.
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.
371
369
 
372
370
  ```ts
373
- // faq.ts — imported by the template, and loaded by the page's script
374
- export const faq = element("go-faq", {
375
- row: { key: { kind: "text" }, status: { kind: "choice", of: ["open", "soon"] } },
376
- search: marker,
377
- }, (host, { row, search }, signal) => {
378
- for (const { element, values } of row.all()) {
379
- values.status; // "open" | "soon"
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;
380
376
  }
381
- const input = search.one<HTMLInputElement>(); // a marker role is the element itself
382
377
  });
383
378
  ```
384
379
 
385
380
  ```astro
381
+ ---
382
+ import AtlasElement from "@escape-game-over/atlas/astro/element";
383
+ ---
384
+
386
385
  <script src="./faq.ts"></script>
387
386
 
388
- <go-faq {...faq.root}>
389
- <input {...faq.search.attrs()} type="search">
390
- <details {...faq.row.attrs({ key, status: "open" })}>…</details>
391
- </go-faq>
387
+ <AtlasElement of={faq} class="…">
388
+ <input data-faq-search type="search">
389
+ <details data-faq-row data-key={key}>…</details>
390
+ </AtlasElement>
392
391
  ```
393
392
 
394
393
  It registers itself when loaded in a browser and does nothing in Node, so the
395
- template imports the same file for the contract.
396
-
397
- - **Names are derived, never spelled.** `data-network-row` marks the element and
398
- `data-network-row-status` holds a field. A field that does not exist is a
399
- compile error in the template and in the script.
400
- - **Values are typed on the way in.** `attrs` takes each field's type: a missing
401
- required field, or a status outside its list, fails `astro check`. It also
402
- checks at run time, because this runs at build time and a value can arrive as
403
- a plain `string` from an env var or a cast; a throw there fails the build
404
- rather than shipping a page whose script throws on every visit.
405
- - **And parsed on the way out.** `read`, `all`, `one` and `require` parse every
406
- field and throw, naming the element and the attribute, when one is missing or
407
- malformed. A malformed match fails the whole `all` rather than being skipped:
408
- a list that quietly loses a row is the failure this exists to prevent.
409
-
410
- The kinds are data, not builders, for the reason `filters` gives:
411
- `text`, `number`, `list`, `template`, `choice` and `flag`, all but `flag`
412
- optionally `optional`. A `flag` is off by being absent, never `"false"`, so a
413
- stylesheet can select it with a bare attribute.
414
-
415
- A `template` is the one that keeps a sentence out of the script. It is written
416
- as the translated string with its placeholders left in, and read back as the
417
- function that fills them:
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:
415
+
416
+ ```ts
417
+ "faq.matched": { "en-US": "[v:count]{count}[/v] answers found" },
418
+ ```
418
419
 
419
420
  ```astro
420
- <p {...faq.line.attrs({ sentence: t("faq.matched", { count: "{count}" }) })}></p>
421
+ <RichText spans={rich("faq.matched", { count: "" })} attrs={{ count: { "data-faq-count": "" } }} />
421
422
  ```
422
423
 
423
424
  ```ts
424
- line.element.textContent = line.values.sentence({ count: String(hits.size) });
425
+ for (const each of refs(host, "[data-faq-count]")) each.textContent = String(hits.size);
425
426
  ```
426
427
 
427
- So no script spells `"{count}"`, and a sentence that never says one of its own
428
- placeholders fails the build rather than shipping a brace to a reader.
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.
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.
456
430
 
457
431
  ## `youtube`
458
432
 
@@ -587,16 +561,13 @@ right properties is a faithful stand-in.
587
561
  `matches` read live as a browser's is, and fakes the `<video>` down to
588
562
  `paused`, `currentSrc`, `networkState`, `autoplay`, `play`, `pause` and `load`,
589
563
  including what resource selection does after a reload.
590
- - `tests/markup.test.ts` fakes elements down to four attribute methods and a
591
- root that answers bare attribute selectors, and round-trips every field kind
592
- through `attrs` and `read`. `type-tests/markup.ts` pins the half that is a
593
- compile error.
594
- - `tests/component.test.ts` builds a small element tree with parents and
595
- `closest`, and pins the scoping: a nested instance, two side by side, a root
596
- inside the instance, and a broken nested element that must not be read.
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
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
600
571
  this package carries no environment for one; adding `happy-dom` as a dev
601
572
  dependency and setting `environment` in `vitest.config.ts` is what that would
602
573
  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. The site supplies the
238
- look — a class per `[v:name]` and one for links — usually in a small wrapper
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
- <RichText spans={rich("about.intro", { venue: "contact" })} variants={{ accent: "…" }} link="…" />
243
+ <p class="…">
244
+ <RichText spans={rich("about.intro", { venue: "contact" })} variants={{ accent: "…" }} link="…" />
245
+ </p>
243
246
  ```
244
247
 
245
- A variant with no class still renders its words, and warns at build.
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.25",
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,
@@ -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.13",
54
- "@types/node": "26.5.1",
54
+ "@biomejs/biome": "2.5.14",
55
+ "@types/node": "26.6.2",
55
56
  "@vitest/coverage-istanbul": "5.0.1",
56
- "astro": "7.3.2",
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,32 +1,34 @@
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
- <atlas-consent
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
- </atlas-consent>
18
+ </AtlasElement>
18
19
 
19
20
  <script>
20
21
  import {
21
22
  applyConsent,
22
- CONSENT_ROLES,
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", CONSENT_ROLES, (host, { answer }, signal) => {
31
+ element("atlas-consent", ({ host, signal }) => {
30
32
  // The head script left no consent hook: nothing here sets a cookie.
31
33
  if (!consentApplies()) return;
32
34
 
@@ -35,11 +37,15 @@ element("atlas-consent", CONSENT_ROLES, (host, { answer }, signal) => {
35
37
  host.hidden = false;
36
38
  };
37
39
 
38
- for (const { element, values } of answer.all()) {
39
- element.addEventListener(
40
+ for (const button of refs(host, `[${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(values.choice);
48
+ store.record(choice);
43
49
  host.hidden = true;
44
50
  },
45
51
  { signal }
@@ -47,9 +53,9 @@ element("atlas-consent", CONSENT_ROLES, (host, { answer }, signal) => {
47
53
  }
48
54
 
49
55
  // Outside the banner — withdrawing must be as easy as granting.
50
- for (const { element } of consentReopen.all(document)) {
51
- element.hidden = false;
52
- element.addEventListener("click", show, { signal });
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
@@ -95,8 +95,7 @@ const picture: ComponentProps<typeof Picture> = {
95
95
  src={src}
96
96
  alt={alt}
97
97
  closeLabel={zoom.closeLabel}
98
- class={zoom.class}
99
- >
98
+ class={zoom.class}>
100
99
  <Picture {...picture} />
101
100
  </Zoom>
102
101
  )}
@@ -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
- return <script type={tag.type} is:inline set:html={tag.content} />;
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
- return <script is:inline defer src={tag.src} {...tag.attrs} />;
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
- return (
39
- <noscript>
40
- <iframe
41
- src={tag.src}
42
- title="Google Tag Manager"
43
- height="0"
44
- width="0"
45
- style="display:none;visibility:hidden"
46
- />
47
- </noscript>
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
+ })}
@@ -1,71 +1,88 @@
1
1
  ---
2
2
  /**
3
3
  * Renders what `site.rich()` returns. The site supplies the look: a class per
4
- * `[v:name]` variant and one for links; everything else is decided here.
4
+ * `[v:name]` variant and one for links; everything else is decided here. Only
5
+ * the runs — the caller writes the element they sit in.
5
6
  *
6
7
  * ```astro
7
- * <RichText spans={rich("about.intro", { venue: "contact" })} variants={{ accent: "…" }} />
8
+ * <p class="…">
9
+ * <RichText spans={rich("about.intro", { venue: "contact" })} variants={{ accent: "…" }} />
10
+ * </p>
8
11
  * ```
9
12
  */
10
- import type { HTMLTag } from "astro/types";
11
13
  import type { RichText } from "../content/index.ts";
12
14
  import { warn } from "../warn.ts";
13
15
 
14
16
  interface Props {
15
17
  readonly spans: RichText;
16
- /** The element wrapping the runs. Defaults to `p`. */
17
- readonly as?: HTMLTag;
18
- readonly class?: string;
19
18
  /** `[v:name]` → classes. */
20
19
  readonly variants?: Readonly<Record<string, string>>;
20
+ /** `[v:name]` → attributes, so a script can find that run. */
21
+ readonly attrs?: Readonly<Record<string, Readonly<Record<string, string>>>>;
21
22
  /** Classes on every link: `[a:]`, `[mail]` and `[tel]`. */
22
23
  readonly link?: string;
23
24
  }
24
25
 
25
- const {
26
- spans,
27
- as: Tag = "p",
28
- class: className,
29
- variants = {},
30
- link,
31
- }: Props = Astro.props;
26
+ const { spans, variants = {}, attrs = {}, link }: Props = Astro.props;
32
27
 
33
28
  // The words still render: losing the sentence is worse than losing its style.
29
+ // A run with attributes and no class is a script's hook, not a missing style.
34
30
  const variantClass = (variant: string): string | undefined => {
35
- if (!Object.hasOwn(variants, variant)) {
31
+ if (!Object.hasOwn(variants, variant) && !Object.hasOwn(attrs, variant)) {
36
32
  warn(
37
33
  Astro.url.pathname,
38
- `[v:${variant}] has no class in RichText's variants.`
34
+ `[v:${variant}] has no class in RichText's variants and no attrs.`
39
35
  );
40
36
  }
41
- return variants[variant];
37
+ return Object.hasOwn(variants, variant) ? variants[variant] : undefined;
42
38
  };
39
+
40
+ const variantAttrs = (variant: string) =>
41
+ Object.hasOwn(attrs, variant) ? attrs[variant] : undefined;
43
42
  ---
44
43
 
45
- <Tag class={className}>
46
- {spans.map((span) => {
47
- switch (span.kind) {
48
- case "text":
49
- return span.text;
50
- case "bold":
51
- return <strong>{span.text}</strong>;
52
- case "variant":
53
- return <span class={variantClass(span.variant)}>{span.text}</span>;
54
- case "break":
55
- return <br />;
56
- case "link":
57
- return span.to === "external" ? (
58
- <a href={span.href} class={link} target="_blank" rel="noopener noreferrer">
59
- {span.text}
60
- </a>
61
- ) : (
62
- <a href={span.href} class={link}>{span.text}</a>
63
- );
64
- case "mail":
65
- case "tel":
66
- return <a href={span.href} class={link}>{span.text}</a>;
67
- default:
68
- return span satisfies never;
69
- }
70
- })}
71
- </Tag>
44
+ {spans.map((span) => {
45
+ switch (span.kind) {
46
+ case "text":
47
+ return span.text;
48
+ case "bold":
49
+ return <strong>{span.text}</strong>;
50
+ case "variant":
51
+ return (
52
+ <span
53
+ {...variantAttrs(span.variant)}
54
+ class={variantClass(span.variant)}>
55
+ {span.text}
56
+ </span>
57
+ );
58
+ case "break":
59
+ return <br />;
60
+ case "link":
61
+ return span.to === "external" ? (
62
+ <a
63
+ href={span.href}
64
+ class={link}
65
+ target='_blank'
66
+ rel='noopener noreferrer'>
67
+ {span.text}
68
+ </a>
69
+ ) : (
70
+ <a
71
+ href={span.href}
72
+ class={link}>
73
+ {span.text}
74
+ </a>
75
+ );
76
+ case "mail":
77
+ case "tel":
78
+ return (
79
+ <a
80
+ href={span.href}
81
+ class={link}>
82
+ {span.text}
83
+ </a>
84
+ );
85
+ default:
86
+ return span satisfies never;
87
+ }
88
+ })}
@@ -20,14 +20,13 @@ export {
20
20
  consentApplies,
21
21
  consentStore,
22
22
  } from "./consent.ts";
23
- export { type Connect, type ElementRoles, element } from "./element.ts";
23
+ export {
24
+ type Connect,
25
+ type ElementContext,
26
+ type ElementTag,
27
+ element,
28
+ } from "./element.ts";
24
29
  export * from "./filters.ts";
25
30
  export * from "./filters-view.ts";
26
- export {
27
- type Component,
28
- type Marked,
29
- type Markup,
30
- marker,
31
- markup,
32
- } from "./markup.ts";
31
+ export { data, ref, refs } from "./ref.ts";
33
32
  export * from "./youtube.ts";
@@ -1,12 +1,12 @@
1
1
  import { CONSENT_UPDATE_GLOBAL } from "../analytics/google.ts";
2
- import { component, markup } from "./markup.ts";
2
+ import type { ElementTag } from "./element.ts";
3
3
 
4
- /** Internal to `ConsentBanner.astro`: the element and the footer control. */
5
- export const CONSENT_ROLES = {
6
- answer: { choice: { kind: "choice", of: ["granted", "denied"] } },
7
- } as const;
8
- export const consentBanner = component("atlas-consent", CONSENT_ROLES);
9
- export const consentReopen = markup("atlas-consent-reopen");
4
+ /** Internal to `ConsentBanner.astro`: the element, and what its script finds. */
5
+ export const consentBanner: ElementTag<"atlas-consent"> = {
6
+ tag: "atlas-consent",
7
+ };
8
+ export const CONSENT_ANSWER = "data-atlas-consent-answer";
9
+ export const CONSENT_REOPEN = "data-atlas-consent-reopen";
10
10
 
11
11
  /**
12
12
  * What a site's consent markup spreads: the two answer buttons inside
@@ -15,9 +15,9 @@ export const consentReopen = markup("atlas-consent-reopen");
15
15
  * withdraw.
16
16
  */
17
17
  export const consent = {
18
- accept: consentBanner.answer.attrs({ choice: "granted" }),
19
- decline: consentBanner.answer.attrs({ choice: "denied" }),
20
- reopen: { ...consentReopen.attrs(), hidden: "" },
18
+ accept: { [CONSENT_ANSWER]: "granted" },
19
+ decline: { [CONSENT_ANSWER]: "denied" },
20
+ reopen: { [CONSENT_REOPEN]: "", hidden: "" },
21
21
  };
22
22
 
23
23
  /**