@escape-game-over/atlas 0.1.102 → 0.1.103

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.103",
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",
@@ -35,6 +35,13 @@ export type StorageVendor =
35
35
  | "drip"
36
36
  | "clarity";
37
37
 
38
+ /**
39
+ * A booking widget a site embeds, which keeps its own storage on the site's
40
+ * domain: rows the analytics settings cannot know about, so the site names
41
+ * the widgets it embeds.
42
+ */
43
+ export type BookingWidget = "4escape";
44
+
38
45
  /**
39
46
  * Each vendor's product as it calls itself, for a table's "set by" column.
40
47
  * Names, not copy: the same in every language, so a project translates only
@@ -50,7 +57,8 @@ export const STORAGE_VENDOR_NAMES = {
50
57
  axon: "AppLovin Axon",
51
58
  drip: "Drip",
52
59
  clarity: "Microsoft Clarity",
53
- } as const satisfies Record<StorageVendor, string>;
60
+ "4escape": "4escape",
61
+ } as const satisfies Record<StorageVendor | BookingWidget, string>;
54
62
 
55
63
  /** A span `Intl.NumberFormat` can word in any locale: `{ 2, "year" }`. */
56
64
  export interface Lifetime {
@@ -64,15 +72,18 @@ type OwnRow = `${VendorWithOwn}-own`;
64
72
  /** The keys Atlas itself sets. */
65
73
  export type AtlasStoredId = "atlas-locale" | "atlas-campaign" | "atlas-consent";
66
74
 
75
+ /** A booking widget's rows: what it keeps, and what it keeps for the tab. */
76
+ type BookingRow = BookingWidget | `${BookingWidget}-tab`;
77
+
67
78
  /** One row's id: what a project keys the row's wording by. */
68
- export type StoredId = AtlasStoredId | StorageVendor | OwnRow;
79
+ export type StoredId = AtlasStoredId | StorageVendor | OwnRow | BookingRow;
69
80
 
70
81
  export interface StoredItem {
71
82
  readonly id: StoredId;
72
83
  /** As the browser shows them; a pattern like `_ga_<id>` where the vendor appends one. */
73
84
  readonly names: readonly string[];
74
85
  /** Who sets it: this site, or the vendor. */
75
- readonly provider: "site" | StorageVendor;
86
+ readonly provider: "site" | StorageVendor | BookingWidget;
76
87
  /** Set on the vendor's own domains rather than the site's; absent for the site's. */
77
88
  readonly domains?: readonly string[];
78
89
  /** A span; `tab` for session storage; `kept` until the visitor clears it. */
@@ -86,6 +97,8 @@ export interface StoredItemsOptions {
86
97
  readonly rememberLocale?: boolean;
87
98
  /** As passed to `Document`, and with its default: `false`. */
88
99
  readonly rememberCampaign?: boolean;
100
+ /** The booking widgets the site embeds anywhere. */
101
+ readonly booking?: readonly BookingWidget[];
89
102
  /**
90
103
  * Rows the site sets first, then by what has to be granted — nothing,
91
104
  * analytics, marketing — and otherwise in catalog order. Off, the rows
@@ -256,6 +269,24 @@ const VENDOR_ORDER: readonly StorageVendor[] = [
256
269
  ];
257
270
 
258
271
  /** The vendors a deployment's settings load, in catalog order. */
272
+ /**
273
+ * Each booking widget's storage, all on the site's domain and none of it
274
+ * cookies: what its script left after rendering, seen live 2026-11-11, since
275
+ * the vendor documents none. Functional — the language, the cart — so nothing
276
+ * has to be granted first.
277
+ */
278
+ const BOOKING = {
279
+ // `forescapeAnalytics` stayed empty, with no tracker loaded; listed because
280
+ // it is set.
281
+ "4escape": {
282
+ kept: ["forescapeLayout", "forescapeStoreOrder", "forescapeAnalytics"],
283
+ tab: ["forescape"],
284
+ },
285
+ } as const satisfies Record<
286
+ BookingWidget,
287
+ { readonly kept: readonly string[]; readonly tab: readonly string[] }
288
+ >;
289
+
259
290
  function loadedVendors(
260
291
  analytics: AnalyticsSettings | undefined
261
292
  ): readonly StorageVendor[] {
@@ -327,6 +358,7 @@ export function storedItems(
327
358
  const {
328
359
  rememberLocale = true,
329
360
  rememberCampaign = false,
361
+ booking = [],
330
362
  sort = false,
331
363
  } = options;
332
364
  const asked = consentVendors(analytics).length > 0;
@@ -382,5 +414,23 @@ export function storedItems(
382
414
  }
383
415
  }
384
416
 
417
+ for (const widget of new Set(booking)) {
418
+ const entry = BOOKING[widget];
419
+ rows.push({
420
+ id: widget,
421
+ names: entry.kept,
422
+ provider: widget,
423
+ lasts: "kept",
424
+ consent: "none",
425
+ });
426
+ rows.push({
427
+ id: `${widget}-tab`,
428
+ names: entry.tab,
429
+ provider: widget,
430
+ lasts: "tab",
431
+ consent: "none",
432
+ });
433
+ }
434
+
385
435
  return sort ? rows.toSorted(compareStoredItems) : rows;
386
436
  }
@@ -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
+ }
package/src/index.ts CHANGED
@@ -45,6 +45,7 @@ export {
45
45
  } from "./analytics/index.ts";
46
46
  export {
47
47
  type AtlasStoredId,
48
+ type BookingWidget,
48
49
  compareStoredItems,
49
50
  type Lifetime,
50
51
  STORAGE_VENDOR_NAMES,
@@ -55,6 +56,16 @@ export {
55
56
  storedItems,
56
57
  withPurposes,
57
58
  } from "./analytics/storage.ts";
59
+ export {
60
+ checkFourEscape,
61
+ FOUR_ESCAPE_LANGUAGES,
62
+ FOUR_ESCAPE_SCRIPT,
63
+ FOUR_ESCAPE_STYLESHEET,
64
+ type FourEscapeLanguage,
65
+ type FourEscapeWidget,
66
+ fourEscapeLanguage,
67
+ fourEscapeSettings,
68
+ } from "./booking/four-escape.ts";
58
69
  export type {
59
70
  LanguageTag,
60
71
  LocaleMeta,