@escape-game-over/atlas 0.1.102 → 0.1.104

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escape-game-over/atlas",
3
- "version": "0.1.102",
3
+ "version": "0.1.104",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
@@ -21,6 +21,7 @@
21
21
  "./astro/consent-banner": "./src/astro/ConsentBanner.astro",
22
22
  "./astro/document": "./src/astro/Document.astro",
23
23
  "./astro/element": "./src/astro/AtlasElement.astro",
24
+ "./astro/four-escape": "./src/astro/FourEscape.astro",
24
25
  "./astro/google-map": "./src/astro/GoogleMap.astro",
25
26
  "./astro/image": "./src/astro/Image.astro",
26
27
  "./astro/phone": "./src/astro/Phone.astro",
@@ -1,3 +1,4 @@
1
+ import type { BookingVendor } from "../booking/widget.ts";
1
2
  import {
2
3
  CONSENT_KEY,
3
4
  CONSENT_MONTHS,
@@ -50,7 +51,8 @@ export const STORAGE_VENDOR_NAMES = {
50
51
  axon: "AppLovin Axon",
51
52
  drip: "Drip",
52
53
  clarity: "Microsoft Clarity",
53
- } as const satisfies Record<StorageVendor, string>;
54
+ "4escape": "4escape",
55
+ } as const satisfies Record<StorageVendor | BookingVendor, string>;
54
56
 
55
57
  /** A span `Intl.NumberFormat` can word in any locale: `{ 2, "year" }`. */
56
58
  export interface Lifetime {
@@ -64,15 +66,18 @@ type OwnRow = `${VendorWithOwn}-own`;
64
66
  /** The keys Atlas itself sets. */
65
67
  export type AtlasStoredId = "atlas-locale" | "atlas-campaign" | "atlas-consent";
66
68
 
69
+ /** A booking widget's rows: what it keeps, and what it keeps for the tab. */
70
+ type BookingRow = BookingVendor | `${BookingVendor}-tab`;
71
+
67
72
  /** One row's id: what a project keys the row's wording by. */
68
- export type StoredId = AtlasStoredId | StorageVendor | OwnRow;
73
+ export type StoredId = AtlasStoredId | StorageVendor | OwnRow | BookingRow;
69
74
 
70
75
  export interface StoredItem {
71
76
  readonly id: StoredId;
72
77
  /** As the browser shows them; a pattern like `_ga_<id>` where the vendor appends one. */
73
78
  readonly names: readonly string[];
74
79
  /** Who sets it: this site, or the vendor. */
75
- readonly provider: "site" | StorageVendor;
80
+ readonly provider: "site" | StorageVendor | BookingVendor;
76
81
  /** Set on the vendor's own domains rather than the site's; absent for the site's. */
77
82
  readonly domains?: readonly string[];
78
83
  /** A span; `tab` for session storage; `kept` until the visitor clears it. */
@@ -86,6 +91,12 @@ export interface StoredItemsOptions {
86
91
  readonly rememberLocale?: boolean;
87
92
  /** As passed to `Document`, and with its default: `false`. */
88
93
  readonly rememberCampaign?: boolean;
94
+ /**
95
+ * The vendors of the booking widgets the site embeds anywhere: their
96
+ * storage is on the site's domain, where the analytics settings cannot
97
+ * see it.
98
+ */
99
+ readonly booking?: readonly BookingVendor[];
89
100
  /**
90
101
  * Rows the site sets first, then by what has to be granted — nothing,
91
102
  * analytics, marketing — and otherwise in catalog order. Off, the rows
@@ -256,6 +267,24 @@ const VENDOR_ORDER: readonly StorageVendor[] = [
256
267
  ];
257
268
 
258
269
  /** The vendors a deployment's settings load, in catalog order. */
270
+ /**
271
+ * Each booking widget's storage, all on the site's domain and none of it
272
+ * cookies: what its script left after rendering, seen live 2026-11-11, since
273
+ * the vendor documents none. Functional — the language, the cart — so nothing
274
+ * has to be granted first.
275
+ */
276
+ const BOOKING = {
277
+ // `forescapeAnalytics` stayed empty, with no tracker loaded; listed because
278
+ // it is set.
279
+ "4escape": {
280
+ kept: ["forescapeLayout", "forescapeStoreOrder", "forescapeAnalytics"],
281
+ tab: ["forescape"],
282
+ },
283
+ } as const satisfies Record<
284
+ BookingVendor,
285
+ { readonly kept: readonly string[]; readonly tab: readonly string[] }
286
+ >;
287
+
259
288
  function loadedVendors(
260
289
  analytics: AnalyticsSettings | undefined
261
290
  ): readonly StorageVendor[] {
@@ -327,6 +356,7 @@ export function storedItems(
327
356
  const {
328
357
  rememberLocale = true,
329
358
  rememberCampaign = false,
359
+ booking = [],
330
360
  sort = false,
331
361
  } = options;
332
362
  const asked = consentVendors(analytics).length > 0;
@@ -382,5 +412,23 @@ export function storedItems(
382
412
  }
383
413
  }
384
414
 
415
+ for (const widget of new Set(booking)) {
416
+ const entry = BOOKING[widget];
417
+ rows.push({
418
+ id: widget,
419
+ names: entry.kept,
420
+ provider: widget,
421
+ lasts: "kept",
422
+ consent: "none",
423
+ });
424
+ rows.push({
425
+ id: `${widget}-tab`,
426
+ names: entry.tab,
427
+ provider: widget,
428
+ lasts: "tab",
429
+ consent: "none",
430
+ });
431
+ }
432
+
385
433
  return sort ? rows.toSorted(compareStoredItems) : rows;
386
434
  }
@@ -0,0 +1,67 @@
1
+ ---
2
+ /**
3
+ * A 4escape catalog, and its cart below it unless `cart={false}`: the store's
4
+ * games and free slots, booked without leaving the page. See `four-escape.ts`
5
+ * for what the widgets are and where their contract comes from.
6
+ *
7
+ * `locale` is the page's; the widgets speak it when they can, and otherwise
8
+ * the account's language as 4escape sets it. The cart is one per page: give
9
+ * the second catalog on a page `cart={false}`.
10
+ *
11
+ * The widgets draw themselves, in 4escape's style. Attributes go on the
12
+ * element around the catalog, which is the place to give it room; the cart
13
+ * follows it.
14
+ *
15
+ * ```astro
16
+ * <FourEscape widgetId={id} domain="acme.4escape.io" locale={locale} class="my-8" />
17
+ * ```
18
+ */
19
+ import type { HTMLAttributes } from "astro/types";
20
+ import {
21
+ checkFourEscape,
22
+ FOUR_ESCAPE_SCRIPT,
23
+ FOUR_ESCAPE_STYLESHEET,
24
+ type FourEscapeWidget,
25
+ fourEscapeLanguage,
26
+ fourEscapeSettings,
27
+ } from "../booking/four-escape.ts";
28
+
29
+ type Props = HTMLAttributes<"div"> &
30
+ FourEscapeWidget & {
31
+ readonly locale: string;
32
+ readonly cart?: boolean;
33
+ };
34
+
35
+ const { widgetId, domain, locale, cart = true, ...attrs } = Astro.props;
36
+
37
+ checkFourEscape({ widgetId, domain }, Astro.url.pathname);
38
+
39
+ const settings = fourEscapeSettings(domain);
40
+ const language = fourEscapeLanguage(locale);
41
+ ---
42
+
43
+ <link
44
+ rel="stylesheet"
45
+ href={FOUR_ESCAPE_STYLESHEET}
46
+ crossorigin="anonymous"
47
+ >
48
+ <div {...attrs}>
49
+ <div
50
+ class="forescape-catalog"
51
+ data-widget-id={widgetId}
52
+ data-settings={settings}
53
+ data-language={language}
54
+ ></div>
55
+ </div>
56
+ {cart && (
57
+ <div
58
+ class='forescape-cart'
59
+ data-settings={settings}
60
+ data-language={language}
61
+ />
62
+ )}
63
+ <script
64
+ is:inline
65
+ type="module"
66
+ src={FOUR_ESCAPE_SCRIPT}
67
+ ></script>
@@ -0,0 +1,81 @@
1
+ import { invalidId, UUID, type Uuid } from "../analytics/ids.ts";
2
+
3
+ /**
4
+ * 4escape's booking widgets: a catalog of a store's games and slots, and the
5
+ * cart a booking goes through. Not 4escape's older iframe, which cannot share
6
+ * a page with them; the widgets are what 4escape builds for an account, so
7
+ * the catalog's id comes from 4escape.
8
+ *
9
+ * Plain markup and one module script, which the browser runs once however
10
+ * many widgets a page has: there is nothing to load on demand. They set no
11
+ * cookies and need no consent; what they keep is in `storedItems`.
12
+ *
13
+ * Their help shows the embed only as screenshots, so the contract here is the
14
+ * widget script's own, read 2026-11-11: it looks for `.forescape-<type>`
15
+ * elements and reads `data-settings` as `b64.` plus base64 JSON with the
16
+ * `domain`. The language is `data-language`, as the help gives it for the
17
+ * iframe; the widgets read it the same way.
18
+ *
19
+ * @see https://help.4escape.io/en/articles/259019-how-to-integrate-the-reservation-widget-on-your-website
20
+ */
21
+ export interface FourEscapeWidget {
22
+ /** The catalog's id, from 4escape. */
23
+ readonly widgetId: Uuid;
24
+ /** The account's 4escape address: `acme.4escape.io`. */
25
+ readonly domain: string;
26
+ }
27
+
28
+ /** The languages the widgets are translated into, as `data-language` takes them. */
29
+ export const FOUR_ESCAPE_LANGUAGES = [
30
+ "fr",
31
+ "en",
32
+ "es",
33
+ "nl",
34
+ "de",
35
+ "it",
36
+ "pt",
37
+ ] as const;
38
+
39
+ export type FourEscapeLanguage = (typeof FOUR_ESCAPE_LANGUAGES)[number];
40
+
41
+ /** The stylesheet and script the widgets need, as 4escape serves them. */
42
+ export const FOUR_ESCAPE_STYLESHEET = "https://widgets.4escape.app/main.css";
43
+ export const FOUR_ESCAPE_SCRIPT = "https://widgets-mirror.4escape.app/main.js";
44
+
45
+ /** An account's address on 4escape. */
46
+ const FOUR_ESCAPE_DOMAIN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.4escape\.io$/;
47
+
48
+ /**
49
+ * Fails the build on a widget that would render nothing: a catalog id that is
50
+ * not a UUID, or a domain that is not an account's on 4escape.
51
+ */
52
+ export function checkFourEscape(widget: FourEscapeWidget, at: string): void {
53
+ if (!UUID.test(widget.widgetId)) {
54
+ invalidId(
55
+ at,
56
+ `4escape widget id "${widget.widgetId}" is not a UUID, so the catalog would render nothing. 4escape gives it with the widget's embed code.`
57
+ );
58
+ }
59
+ if (!FOUR_ESCAPE_DOMAIN.test(widget.domain)) {
60
+ invalidId(
61
+ at,
62
+ `4escape domain "${widget.domain}" is not an account's address on 4escape, like "acme.4escape.io".`
63
+ );
64
+ }
65
+ }
66
+
67
+ /** `data-settings` for an account: `b64.` and the settings as base64 JSON. */
68
+ export function fourEscapeSettings(domain: string): string {
69
+ return `b64.${btoa(JSON.stringify({ domain }))}`;
70
+ }
71
+
72
+ /**
73
+ * The widgets' language for a page's locale, or `undefined` for one they are
74
+ * not translated into: the account's own language then, as 4escape sets it.
75
+ */
76
+ export function fourEscapeLanguage(
77
+ locale: string
78
+ ): FourEscapeLanguage | undefined {
79
+ const { language } = new Intl.Locale(locale);
80
+ return FOUR_ESCAPE_LANGUAGES.find((supported) => supported === language);
81
+ }
@@ -0,0 +1,10 @@
1
+ import type { FourEscapeWidget } from "./four-escape.ts";
2
+
3
+ /**
4
+ * A booking widget a site embeds, by its vendor: what a site's config names,
5
+ * and what picks the component that draws it.
6
+ */
7
+ export type BookingWidget = { readonly vendor: "4escape" } & FourEscapeWidget;
8
+
9
+ /** Who makes a booking widget: what `storedItems` takes for their rows. */
10
+ export type BookingVendor = BookingWidget["vendor"];
package/src/index.ts CHANGED
@@ -55,6 +55,17 @@ export {
55
55
  storedItems,
56
56
  withPurposes,
57
57
  } from "./analytics/storage.ts";
58
+ export {
59
+ checkFourEscape,
60
+ FOUR_ESCAPE_LANGUAGES,
61
+ FOUR_ESCAPE_SCRIPT,
62
+ FOUR_ESCAPE_STYLESHEET,
63
+ type FourEscapeLanguage,
64
+ type FourEscapeWidget,
65
+ fourEscapeLanguage,
66
+ fourEscapeSettings,
67
+ } from "./booking/four-escape.ts";
68
+ export type { BookingVendor, BookingWidget } from "./booking/widget.ts";
58
69
  export type {
59
70
  LanguageTag,
60
71
  LocaleMeta,