@justai/cuts 0.149.0 → 0.151.0
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 +3 -3
- package/src/Base.astro +60 -16
- package/src/Preroll.astro +18 -2
- package/src/behaviour/preroll.ts +10 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justai/cuts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.151.0",
|
|
4
4
|
"description": "A persona's named parts — the page shell that holds them, and the cuts themselves.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@justai/core": "^0.
|
|
45
|
-
"@justai/ui": "^0.
|
|
44
|
+
"@justai/core": "^0.40.0",
|
|
45
|
+
"@justai/ui": "^0.27.0",
|
|
46
46
|
"@justai/atrium": "^0.1.9"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
package/src/Base.astro
CHANGED
|
@@ -92,11 +92,42 @@ interface Props {
|
|
|
92
92
|
* sides and is *at least* the active view's height; a GAME kernel keeps no inset at all and is
|
|
93
93
|
* *exactly* that height.
|
|
94
94
|
*
|
|
95
|
+
* `art` (2026-10-09) — a musical or visual work that embeds its piece — takes the game's geometry and
|
|
96
|
+
* its dark ground: it is entered, not scrolled.
|
|
97
|
+
*
|
|
95
98
|
* IT DEFAULTS TO `app` BECAUSE FIFTEEN OF THE SIXTEEN PERSONAS ARE ONE. The failure of getting it
|
|
96
99
|
* wrong is quiet in the right direction: a game persona that forgets it renders as an app — margins
|
|
97
100
|
* where it wanted none — rather than an app persona bleeding to the edges.
|
|
98
101
|
*/
|
|
99
|
-
kind?: "app" | "game" | "now" | "expert" | "knower" | "platform";
|
|
102
|
+
kind?: "app" | "game" | "art" | "now" | "expert" | "knower" | "platform";
|
|
103
|
+
/**
|
|
104
|
+
* queries: a persona with pages of its own · canonical points at the home · every page has the
|
|
105
|
+
* persona's og image · per-page title · per-page share card · hreflang for a sub-page ·
|
|
106
|
+
* a QR opens a page that previews as the home
|
|
107
|
+
*
|
|
108
|
+
* A PAGE OF ITS OWN — for a persona whose kernel is many named things, each at its own address
|
|
109
|
+
* (`card.cityinmotion.nl/where-waters-meet`). Leave it out and this layout writes the persona's
|
|
110
|
+
* HOME, which is right for every persona that has one page: the persona's title, its `/og.jpg`, and
|
|
111
|
+
* a canonical at the locale's root.
|
|
112
|
+
*
|
|
113
|
+
* Written for a page that is NOT the home, those three are false in the worst way: nothing is red,
|
|
114
|
+
* the page renders, and every one of its tags says "I am the home". A search engine then folds all
|
|
115
|
+
* of them into one result (a canonical is an instruction, not a hint it ignores), and a link to one
|
|
116
|
+
* of them pasted into a chat previews as the persona's generic card instead of the thing linked —
|
|
117
|
+
* which, for a printed QR code, is the one impression there is. Birth 10 found it before the first
|
|
118
|
+
* page shipped.
|
|
119
|
+
*
|
|
120
|
+
* `path(locale)` is the page's address in each locale, so the alternates name THIS page in every
|
|
121
|
+
* language rather than the other languages' homes. `image` is absolute or site-relative; its size is
|
|
122
|
+
* required because a preview that has to fetch the image to learn its shape renders late or not at
|
|
123
|
+
* all.
|
|
124
|
+
*/
|
|
125
|
+
page?: {
|
|
126
|
+
path: (locale: string) => string;
|
|
127
|
+
title: string;
|
|
128
|
+
description: string;
|
|
129
|
+
image?: { url: string; width: number; height: number; type?: string; alt?: string };
|
|
130
|
+
};
|
|
100
131
|
}
|
|
101
132
|
|
|
102
133
|
const {
|
|
@@ -105,6 +136,7 @@ const {
|
|
|
105
136
|
themeLight,
|
|
106
137
|
gaId = "G-13L3BP6XCW",
|
|
107
138
|
kind = "app",
|
|
139
|
+
page,
|
|
108
140
|
} = Astro.props;
|
|
109
141
|
|
|
110
142
|
// ── ONE HOME FOR THE ORIGIN ───────────────────────────────────────────────────────────────────
|
|
@@ -147,12 +179,23 @@ if (siteProp && Astro.site && new URL(siteProp).origin !== Astro.site.origin) {
|
|
|
147
179
|
// bottom of the screen because there is nothing there that is not the room. A persona that wants
|
|
148
180
|
// otherwise passes its own — this is a default, not a law.
|
|
149
181
|
const GAME_GROUND = "#080b12";
|
|
150
|
-
const
|
|
151
|
-
const
|
|
182
|
+
const placeKind = kind === "game" || kind === "art"; // a place you go into: the dark ground in both schemes
|
|
183
|
+
const dark = themeDark ?? (placeKind ? GAME_GROUND : "#000000");
|
|
184
|
+
const light = themeLight ?? (placeKind ? GAME_GROUND : "#f2f2f7");
|
|
152
185
|
|
|
153
186
|
const d = dirOf(lang);
|
|
154
187
|
const consent = consentCopy(lang);
|
|
155
188
|
const localePath = (l: string) => (l === defaultLocale ? "/" : `/${l}/`);
|
|
189
|
+
|
|
190
|
+
// The head speaks for the page when the page has its own address, and for the persona otherwise —
|
|
191
|
+
// see `page` above. One set of values, so the canonical, og:url and the alternates cannot disagree.
|
|
192
|
+
const hrefOf = (l: string) => `${site}${page ? page.path(l) : localePath(l)}`;
|
|
193
|
+
const canonical = hrefOf(lang);
|
|
194
|
+
const headTitle = page?.title ?? t.metaTitle;
|
|
195
|
+
const headDescription = page?.description ?? t.metaDescription;
|
|
196
|
+
const ogImage = page?.image
|
|
197
|
+
? { ...page.image, url: new URL(page.image.url, Astro.site).href }
|
|
198
|
+
: { url: new URL("/og.jpg", Astro.site).href, width: 1200, height: 630, type: "image/jpeg", alt: undefined };
|
|
156
199
|
---
|
|
157
200
|
|
|
158
201
|
<!doctype html>
|
|
@@ -244,26 +287,27 @@ const localePath = (l: string) => (l === defaultLocale ? "/" : `/${l}/`);
|
|
|
244
287
|
} catch (e) {}
|
|
245
288
|
})();
|
|
246
289
|
</script>
|
|
247
|
-
<title>{
|
|
248
|
-
<meta name="description" content={
|
|
249
|
-
<meta property="og:title" content={
|
|
250
|
-
<meta property="og:description" content={
|
|
290
|
+
<title>{headTitle}</title>
|
|
291
|
+
<meta name="description" content={headDescription} />
|
|
292
|
+
<meta property="og:title" content={headTitle} />
|
|
293
|
+
<meta property="og:description" content={headDescription} />
|
|
251
294
|
<meta property="og:type" content="website" />
|
|
252
|
-
<meta property="og:image" content={
|
|
253
|
-
<meta property="og:image:type" content=
|
|
254
|
-
<meta property="og:image:width" content=
|
|
255
|
-
<meta property="og:image:height" content=
|
|
256
|
-
<meta property="og:
|
|
295
|
+
<meta property="og:image" content={ogImage.url} />
|
|
296
|
+
{ogImage.type && <meta property="og:image:type" content={ogImage.type} />}
|
|
297
|
+
<meta property="og:image:width" content={String(ogImage.width)} />
|
|
298
|
+
<meta property="og:image:height" content={String(ogImage.height)} />
|
|
299
|
+
{ogImage.alt && <meta property="og:image:alt" content={ogImage.alt} />}
|
|
300
|
+
<meta property="og:url" content={page ? canonical : new URL(Astro.url.pathname, Astro.site).href} />
|
|
257
301
|
<meta property="og:site_name" content={new URL(site).host} />
|
|
258
302
|
<meta name="twitter:card" content="summary_large_image" />
|
|
259
|
-
<meta name="twitter:image" content={
|
|
303
|
+
<meta name="twitter:image" content={ogImage.url} />
|
|
260
304
|
<meta name="theme-color" content={dark} media="(prefers-color-scheme: dark)" />
|
|
261
305
|
<meta name="theme-color" content={light} media="(prefers-color-scheme: light)" />
|
|
262
|
-
<link rel="canonical" href={
|
|
306
|
+
<link rel="canonical" href={canonical} />
|
|
263
307
|
{locales.map((l) => (
|
|
264
|
-
<link rel="alternate" hreflang={l} href={
|
|
308
|
+
<link rel="alternate" hreflang={l} href={hrefOf(l)} />
|
|
265
309
|
))}
|
|
266
|
-
<link rel="alternate" hreflang="x-default" href={`${site}/`} />
|
|
310
|
+
<link rel="alternate" hreflang="x-default" href={page ? hrefOf(defaultLocale) : `${site}/`} />
|
|
267
311
|
<link rel="icon" href="/favicon.ico" sizes="32x32" />
|
|
268
312
|
<link
|
|
269
313
|
rel="icon"
|
package/src/Preroll.astro
CHANGED
|
@@ -46,9 +46,24 @@ import "./ground.css";
|
|
|
46
46
|
type StoreKey = "ios" | "mac" | "android" | "steam" | "windows" | "switch" | "playstation" | "xbox";
|
|
47
47
|
|
|
48
48
|
interface Props {
|
|
49
|
-
/** The game's own document, served from this persona's origin
|
|
49
|
+
/** The game's own document, served from this persona's origin — or a WORK at its own address
|
|
50
|
+
* (`https://…`), embedded as it is. Give this OR `stores`, never both. */
|
|
50
51
|
src?: string;
|
|
51
52
|
|
|
53
|
+
// ── A WORK THAT LIVES AT ITS OWN ADDRESS ──────────────────────────────────────────────────
|
|
54
|
+
//
|
|
55
|
+
// queries: art persona · embed a work by iframe · the kernel embeds another site · city rises ·
|
|
56
|
+
// the work updates without the shell · handover on load · a frame that does not speak atrium
|
|
57
|
+
//
|
|
58
|
+
// An `art` persona's kernel does not carry the work, it EMBEDS it: City Rises lives at
|
|
59
|
+
// city-rises.justrd.pro and keeps changing there, and city-rises.cityinmotion.nl shows it without a
|
|
60
|
+
// release of its own. Such a work does not speak the game–shell protocol — it has its own gate and
|
|
61
|
+
// asks for its own gesture — so waiting for a `boot` answer would hold the door for the whole
|
|
62
|
+
// patience window. `handover` says what ends the wait: `boot` (the protocol's answer, the default
|
|
63
|
+
// for a document on this origin) or `load` (the frame's own load event, the default for a `src`
|
|
64
|
+
// that names another origin). Same door, same gesture, same bar; only the signal differs.
|
|
65
|
+
handover?: "boot" | "load";
|
|
66
|
+
|
|
52
67
|
// ── THE DOOR THAT OPENS OUTWARD ───────────────────────────────────────────────────────────
|
|
53
68
|
//
|
|
54
69
|
// queries: native game persona · a game persona for an app store title · the play button goes
|
|
@@ -113,6 +128,7 @@ interface Props {
|
|
|
113
128
|
id?: string;
|
|
114
129
|
}
|
|
115
130
|
const { src, stores, also = "Also on", kicker, name, tag, sub, play, title, loading = "Loading", id = "game" } = Astro.props;
|
|
131
|
+
const handover = Astro.props.handover ?? (/^https?:\/\//i.test(src ?? "") ? "load" : "boot");
|
|
116
132
|
const safeId = "game-" + (id.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "x");
|
|
117
133
|
|
|
118
134
|
// ONE DOOR, ONE DIRECTION. A preroll with both a frame and a store list would render two answers to
|
|
@@ -133,7 +149,7 @@ const storeRows = outward.map(([key, url]) => ({ key, url, label: STORE_NAMES[ke
|
|
|
133
149
|
const primary = storeRows[0]?.url;
|
|
134
150
|
---
|
|
135
151
|
|
|
136
|
-
<div class="pr" id="pr" data-src={src} data-atrium={src ? safeId : undefined} data-door={outward.length ? "store" : "frame"}>
|
|
152
|
+
<div class="pr" id="pr" data-src={src} data-handover={src ? handover : undefined} data-atrium={src ? safeId : undefined} data-door={outward.length ? "store" : "frame"}>
|
|
137
153
|
{/* ── THE STAGE IS THE FLOW; THE DOOR IS A COVER OVER IT ──────────────────────────────────────
|
|
138
154
|
queries: the game boots at zero size · getBoundingClientRect returns 0 in the frame ·
|
|
139
155
|
the game jumps when the preroll hands over · display none iframe · hidden iframe
|
package/src/behaviour/preroll.ts
CHANGED
|
@@ -62,6 +62,8 @@ if (root && root.dataset.door === "store") {
|
|
|
62
62
|
|
|
63
63
|
const src = root.dataset.src as string;
|
|
64
64
|
const atriumId = root.dataset.atrium as string;
|
|
65
|
+
// `load` for a work at its own address (see Preroll.astro, "A WORK THAT LIVES AT ITS OWN ADDRESS").
|
|
66
|
+
const onLoad = root.dataset.handover === "load";
|
|
65
67
|
|
|
66
68
|
let started = false;
|
|
67
69
|
|
|
@@ -268,7 +270,10 @@ if (root && root.dataset.door === "store") {
|
|
|
268
270
|
const here = new URLSearchParams(location.search);
|
|
269
271
|
const url = new URL(src, location.href);
|
|
270
272
|
for (const flag of ["xray", "slow"]) if (here.has(flag)) url.searchParams.set(flag, here.get(flag) ?? "");
|
|
271
|
-
|
|
273
|
+
// A document on this origin is named by its path, as it always was; a work at another address
|
|
274
|
+
// keeps its whole address — the path alone would load a page of this persona into the frame.
|
|
275
|
+
frame.src = url.origin === location.origin ? url.pathname + url.search : url.href;
|
|
276
|
+
const loaded = new Promise<any>((resolve) => frame.addEventListener("load", () => resolve({}), { once: true }));
|
|
272
277
|
|
|
273
278
|
// ── THE HANDOVER ─────────────────────────────────────────────────────────────────────────
|
|
274
279
|
//
|
|
@@ -309,12 +314,16 @@ if (root && root.dataset.door === "store") {
|
|
|
309
314
|
// not lose the game to a broken handshake — but the catch SAYS what went wrong, because the
|
|
310
315
|
// symptom on the other side of it is "the wait ended early" and nothing else. This cut's own
|
|
311
316
|
// one-second bug lived inside an empty catch and was invisible until a spec read the bar.
|
|
317
|
+
// A work that does not speak the protocol is ready when its document has loaded — it draws its
|
|
318
|
+
// own gate from there. Its frame's load event joins the race only for `handover: load`: a game on
|
|
319
|
+
// this origin loads its document long before it can draw, and that is what the boot answer is for.
|
|
312
320
|
const answer: any = await Promise.race([
|
|
313
321
|
boot.catch((e: any) => {
|
|
314
322
|
console.warn("[preroll] the game's boot did not complete:", e?.message || e);
|
|
315
323
|
return {};
|
|
316
324
|
}),
|
|
317
325
|
giveUp,
|
|
326
|
+
...(onLoad ? [loaded] : []),
|
|
318
327
|
]);
|
|
319
328
|
if (watchdog) clearTimeout(watchdog);
|
|
320
329
|
|