@justai/cuts 0.111.0 → 0.112.1
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 +15 -14
- package/src/Preroll.astro +316 -0
- package/src/ground.css +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justai/cuts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.112.1",
|
|
4
4
|
"description": "A persona's named parts \u2014 the page shell that holds them, and the cuts themselves.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -13,25 +13,26 @@
|
|
|
13
13
|
"src"
|
|
14
14
|
],
|
|
15
15
|
"exports": {
|
|
16
|
+
"./ActionList.astro": "./src/ActionList.astro",
|
|
17
|
+
"./ActionRow.astro": "./src/ActionRow.astro",
|
|
18
|
+
"./AuthMount.astro": "./src/AuthMount.astro",
|
|
19
|
+
"./BackToZone.astro": "./src/BackToZone.astro",
|
|
16
20
|
"./Base.astro": "./src/Base.astro",
|
|
21
|
+
"./Catalogue.astro": "./src/Catalogue.astro",
|
|
22
|
+
"./CatalogueCard.astro": "./src/CatalogueCard.astro",
|
|
23
|
+
"./ChildPersona.astro": "./src/ChildPersona.astro",
|
|
24
|
+
"./ConfirmSheet.astro": "./src/ConfirmSheet.astro",
|
|
25
|
+
"./FilePicker.astro": "./src/FilePicker.astro",
|
|
26
|
+
"./Files.astro": "./src/FilePicker.astro",
|
|
17
27
|
"./LangPicker.astro": "./src/LangPicker.astro",
|
|
18
|
-
"./BackToZone.astro": "./src/BackToZone.astro",
|
|
19
28
|
"./Posts.astro": "./src/Posts.astro",
|
|
20
|
-
"./
|
|
29
|
+
"./Preroll.astro": "./src/Preroll.astro",
|
|
21
30
|
"./Profile.astro": "./src/Profile.astro",
|
|
22
|
-
"./ConfirmSheet.astro": "./src/ConfirmSheet.astro",
|
|
23
|
-
"./Sheet.astro": "./src/Sheet.astro",
|
|
24
31
|
"./Segmented.astro": "./src/Segmented.astro",
|
|
32
|
+
"./Sheet.astro": "./src/Sheet.astro",
|
|
33
|
+
"./Toolbar.astro": "./src/Toolbar.astro",
|
|
25
34
|
"./ground.css": "./src/ground.css",
|
|
26
|
-
"./
|
|
27
|
-
"./ActionList.astro": "./src/ActionList.astro",
|
|
28
|
-
"./ActionRow.astro": "./src/ActionRow.astro",
|
|
29
|
-
"./Files.astro": "./src/FilePicker.astro",
|
|
30
|
-
"./AuthMount.astro": "./src/AuthMount.astro",
|
|
31
|
-
"./ChildPersona.astro": "./src/ChildPersona.astro",
|
|
32
|
-
"./tagline.js": "./src/tagline.js",
|
|
33
|
-
"./Catalogue.astro": "./src/Catalogue.astro",
|
|
34
|
-
"./CatalogueCard.astro": "./src/CatalogueCard.astro"
|
|
35
|
+
"./tagline.js": "./src/tagline.js"
|
|
35
36
|
},
|
|
36
37
|
"peerDependencies": {
|
|
37
38
|
"astro": ">=5"
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
---
|
|
2
|
+
// ── PREROLL — the shell holds the door until somebody knocks ────────────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// queries: preroll · the game loads on page open · why does the game not start immediately ·
|
|
5
|
+
// game persona kernel · tap to play · loading progress · game ready · atrium game ·
|
|
6
|
+
// first interaction · autoplay gesture · the frame is empty until you tap
|
|
7
|
+
//
|
|
8
|
+
// A game persona is two things at one address: the SHELL — profile, toolbar, feed, child personas —
|
|
9
|
+
// and the GAME, which runs in a frame of its own. This cut is the line between them, and it is the
|
|
10
|
+
// shell's side of the game–shell protocol (pin.games' `.justai/expert/the-game-shell-protocol.md`).
|
|
11
|
+
//
|
|
12
|
+
// ── WHY THE GAME IS NOT LOADED ON PAGE LOAD ───────────────────────────────────────────────────
|
|
13
|
+
//
|
|
14
|
+
// Three arguments, and they land on the same design, which is usually the sign it is the right one.
|
|
15
|
+
//
|
|
16
|
+
// IT COSTS THE PERSONA ITS BENCHMARKS. sudoku's game is 3.9 MB over 92 requests and reaches a
|
|
17
|
+
// playable state in about 4.7s over the network. Spent at first paint, that is LCP and blocking
|
|
18
|
+
// time on a page a search engine judges — and the persona is more than its game.
|
|
19
|
+
//
|
|
20
|
+
// THE VISITOR MAY NOT HAVE COME FOR THE GAME. They may want the feed, or to meet the character, or
|
|
21
|
+
// to see what it keeps about them. And a player who DOES tap play may want to read the feed while
|
|
22
|
+
// it loads and come back, which is only possible when the load is not the page.
|
|
23
|
+
//
|
|
24
|
+
// AND THE BROWSER ALREADY REQUIRES A GESTURE. Audio will not autoplay without one. A game with
|
|
25
|
+
// sound needs a tap before it can be itself, so the tap is not a cost this cut invents — it is one
|
|
26
|
+
// the platform imposes, turned into the moment the game introduces itself.
|
|
27
|
+
//
|
|
28
|
+
// ── THE HANDOVER, AND THE DISTINCTION IT RESTS ON ─────────────────────────────────────────────
|
|
29
|
+
//
|
|
30
|
+
// A LOADING SCREEN AND A CURTAIN ARE NOT THE SAME THING. A loading screen exists because waiting
|
|
31
|
+
// exists, and it belongs here — this cut has the gesture and it has the progress. A curtain is a
|
|
32
|
+
// title card, a logo animation, the game's own identity, and it is played even on an instant load;
|
|
33
|
+
// it belongs to the game.
|
|
34
|
+
//
|
|
35
|
+
// So a game that answers `{ curtain: true }` is handed the frame EARLY and covers the rest of its
|
|
36
|
+
// own loading. One that says nothing is held until it reports ready. What must never happen is both:
|
|
37
|
+
// a bar that fills here and a "Loading…" that starts there is a player waiting twice, with the
|
|
38
|
+
// second wait being theatre pretending to be work.
|
|
39
|
+
|
|
40
|
+
// THE GROUND rides with the cut. Law 1: a component that renders broken unless its consumer defined
|
|
41
|
+
// --accent, --surface-2 and --text-dim is a template with homework. The wait is an OPAQUE cover over
|
|
42
|
+
// a game that is drawing itself underneath, so a ground that resolves to nothing here does not mean
|
|
43
|
+
// "unstyled" — it means the player watches the game boot through the loading screen.
|
|
44
|
+
import "./ground.css";
|
|
45
|
+
|
|
46
|
+
interface Props {
|
|
47
|
+
/** The game's own document, served from this persona's origin. */
|
|
48
|
+
src: string;
|
|
49
|
+
/** The button. Keep it a verb. */
|
|
50
|
+
play: string;
|
|
51
|
+
/** One line under it — what this is, in the persona's voice. Optional. */
|
|
52
|
+
note?: string;
|
|
53
|
+
/** What the frame is called for somebody who cannot see it. */
|
|
54
|
+
title: string;
|
|
55
|
+
/** Said while the bytes are moving. */
|
|
56
|
+
loading?: string;
|
|
57
|
+
/** atrium ids are a grammar — [a-z0-9._-] — so this is sanitised rather than trusted. */
|
|
58
|
+
id?: string;
|
|
59
|
+
}
|
|
60
|
+
const { src, play, note, title, loading = "Loading", id = "game" } = Astro.props;
|
|
61
|
+
const safeId = "game-" + (id.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "x");
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
<div class="pr" id="pr" data-src={src} data-atrium={safeId}>
|
|
65
|
+
{/* ── THE STAGE IS THE FLOW; THE DOOR AND THE WAIT ARE COVERS OVER IT ─────────────────────────
|
|
66
|
+
queries: the game boots at zero size · getBoundingClientRect returns 0 in the frame ·
|
|
67
|
+
the game jumps when the preroll hands over · display none iframe · hidden iframe
|
|
68
|
+
A GAME MUST BOOT AT ITS REAL SIZE. The first version of this cut put the stage, the door and
|
|
69
|
+
the wait side by side and switched between them with `display: none` — and a game loading
|
|
70
|
+
inside a `display: none` frame has no layout at all: every rect it measures is zero, images do
|
|
71
|
+
not decode, and rAF is suspended. It would have booted blind and then been handed a viewport it
|
|
72
|
+
had already measured as nothing.
|
|
73
|
+
So the stage is always laid out and the covers sit ON TOP of it. The frame reaches its final
|
|
74
|
+
size the moment it is created, and the handover is one element leaving rather than a reflow. */}
|
|
75
|
+
<div class="pr-stage" id="prStage" data-title={title}></div>
|
|
76
|
+
|
|
77
|
+
{/* THE DOOR. No iframe exists yet — that is the entire point, and it is why this markup is a
|
|
78
|
+
button and a slot rather than a frame with a cover over it. A hidden iframe still fetches. */}
|
|
79
|
+
<div class="pr-door" id="prDoor">
|
|
80
|
+
<slot name="art" />
|
|
81
|
+
<button class="pr-play" id="prPlay" type="button">{play}</button>
|
|
82
|
+
{note && <p class="pr-note">{note}</p>}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* The wait. Indeterminate until the game says otherwise: a bar that moves without measuring
|
|
86
|
+
anything teaches a player that the number means nothing. */}
|
|
87
|
+
<div class="pr-wait" id="prWait" hidden>
|
|
88
|
+
<div class="pr-bar" id="prBar" data-indeterminate><i id="prFill"></i></div>
|
|
89
|
+
<p class="pr-note">{loading}</p>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<style>
|
|
94
|
+
/* queries: the game frame has square corners · the game's colours do not match the page ·
|
|
95
|
+
* a seam around the game · full-bleed game · preroll radius
|
|
96
|
+
*
|
|
97
|
+
* ── THE SHELL OWNS THE FRAME'S SHAPE; THE GAME OWNS WHAT IS INSIDE IT ──────────────────────
|
|
98
|
+
*
|
|
99
|
+
* A game paints its own ground and it will not match the persona's — sudoku's art direction is
|
|
100
|
+
* nothing like app.zone's grey. Rendered edge to edge with square corners, that mismatch reads as
|
|
101
|
+
* a page that failed rather than as a game that has a look; rounded and clipped, it reads as an
|
|
102
|
+
* object ON the page, which is what it is. So the seam is made deliberate instead of denied.
|
|
103
|
+
*
|
|
104
|
+
* `--preroll-radius` is the escape hatch, and it is one line: a game persona that wants the frame
|
|
105
|
+
* to BE the screen — the shape sudoku takes — sets it to 0 and cancels the gutter itself. */
|
|
106
|
+
/* queries: the play button cannot be clicked · something else intercepts pointer events ·
|
|
107
|
+
* the preroll has no height · blank page preroll · law 1 geometry
|
|
108
|
+
*
|
|
109
|
+
* ── A COMPONENT BRINGS ITS OWN HEIGHT, NOT ONLY ITS OWN COLOURS ────────────────────────────
|
|
110
|
+
*
|
|
111
|
+
* `flex: 1` gives this cut the kernel's whole height — in a kernel. On a page that is not a flex
|
|
112
|
+
* column it means nothing, the only in-flow child is an empty stage, and the box collapses to
|
|
113
|
+
* zero: the door is then an absolutely positioned overlay inside a zero-height parent, clipped by
|
|
114
|
+
* the `overflow` below and sitting under whatever the page put above it. The blank-page fixture
|
|
115
|
+
* caught it as `<p>bare page — no consumer css</p> intercepts pointer events`, which is the same
|
|
116
|
+
* law 1 the ground.css import two screens up answers for colour.
|
|
117
|
+
*
|
|
118
|
+
* The floor is small enough that the flex case always wins it and large enough that a door is a
|
|
119
|
+
* door. Nothing real is shorter: the narrowest phone leaves ~486px under the toolbar. */
|
|
120
|
+
.pr {
|
|
121
|
+
position: relative; flex: 1; min-height: 240px; display: flex; flex-direction: column;
|
|
122
|
+
border-radius: var(--preroll-radius, var(--radius-lg)); overflow: hidden;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* OPAQUE, and `--bg` rather than `--surface`: the fleet's `--surface` is a translucent overlay
|
|
126
|
+
tint, so a cover painted with it shows the booting game through itself. */
|
|
127
|
+
.pr-door, .pr-wait {
|
|
128
|
+
position: absolute; inset: 0; z-index: 1; background: var(--bg);
|
|
129
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px;
|
|
130
|
+
}
|
|
131
|
+
.pr-door[hidden], .pr-wait[hidden] { display: none; }
|
|
132
|
+
|
|
133
|
+
.pr-play {
|
|
134
|
+
font: inherit; font-weight: 600; font-size: var(--fs-md);
|
|
135
|
+
padding: 14px 40px; border: 0; border-radius: var(--radius-pill);
|
|
136
|
+
background: var(--accent); color: var(--accent-on); cursor: pointer;
|
|
137
|
+
transition: transform 0.18s var(--ease-spring);
|
|
138
|
+
}
|
|
139
|
+
.pr-play:active { transform: scale(0.94); }
|
|
140
|
+
.pr-note { margin: 0; color: var(--text-dim); font-size: var(--fs-sm); text-align: center; }
|
|
141
|
+
|
|
142
|
+
.pr-bar {
|
|
143
|
+
width: min(240px, 70%); height: 4px; border-radius: 999px;
|
|
144
|
+
background: var(--surface-2); overflow: hidden;
|
|
145
|
+
}
|
|
146
|
+
.pr-bar i { display: block; height: 100%; width: 0; border-radius: 999px; background: var(--accent); }
|
|
147
|
+
/* A game that reports nothing gets a sweep rather than a number — honest about not knowing. */
|
|
148
|
+
.pr-bar[data-indeterminate] i {
|
|
149
|
+
width: 40%; animation: pr-sweep 1.1s var(--ease-out) infinite;
|
|
150
|
+
}
|
|
151
|
+
@keyframes pr-sweep { from { transform: translateX(-100%); } to { transform: translateX(250%); } }
|
|
152
|
+
@media (prefers-reduced-motion: reduce) {
|
|
153
|
+
.pr-bar[data-indeterminate] i { animation: none; width: 100%; opacity: 0.5; }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.pr-stage { flex: 1; min-height: 0; }
|
|
157
|
+
.pr-stage :global(iframe) { display: block; width: 100%; height: 100%; border: 0; }
|
|
158
|
+
</style>
|
|
159
|
+
|
|
160
|
+
<script>
|
|
161
|
+
// queries: preroll script · the game never connects · atrium router game · boot · curtain
|
|
162
|
+
//
|
|
163
|
+
// THE SHELL IS THE ROUTER, AND IT IS ONE BEFORE THE FRAME EXISTS. atrium's rule is that a guest
|
|
164
|
+
// discovers UPWARD, so the room has to be standing before anybody can walk into it. `mount` names
|
|
165
|
+
// the guest by its WINDOW — never by what it calls itself, because a guest that announces its own
|
|
166
|
+
// name lets any frame on the page claim to be the game.
|
|
167
|
+
import atrium from "@justai/atrium";
|
|
168
|
+
|
|
169
|
+
const root = document.getElementById("pr");
|
|
170
|
+
if (root) {
|
|
171
|
+
const door = document.getElementById("prDoor") as HTMLElement;
|
|
172
|
+
const wait = document.getElementById("prWait") as HTMLElement;
|
|
173
|
+
const stage = document.getElementById("prStage") as HTMLElement;
|
|
174
|
+
const bar = document.getElementById("prBar") as HTMLElement;
|
|
175
|
+
const fill = document.getElementById("prFill") as HTMLElement;
|
|
176
|
+
const playBtn = document.getElementById("prPlay") as HTMLButtonElement;
|
|
177
|
+
|
|
178
|
+
const src = root.dataset.src as string;
|
|
179
|
+
const atriumId = root.dataset.atrium as string;
|
|
180
|
+
|
|
181
|
+
let started = false;
|
|
182
|
+
|
|
183
|
+
// The stage is never switched — it is underneath the whole time. `show(null)` is the handover:
|
|
184
|
+
// both covers leave and what was already drawing there is simply visible.
|
|
185
|
+
const show = (el: HTMLElement | null) => {
|
|
186
|
+
for (const n of [door, wait]) n.hidden = n !== el;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// ── PATIENCE IS MEASURED IN SILENCE, NOT IN SECONDS ──────────────────────────────────────
|
|
190
|
+
//
|
|
191
|
+
// queries: the game was cut off while it was still loading · the preroll gave up too early ·
|
|
192
|
+
// how long does the shell wait · slow connection · the bar vanished mid-load
|
|
193
|
+
//
|
|
194
|
+
// A total time limit is the wrong instrument and it fails in both directions: 20s abandons a
|
|
195
|
+
// real game on a real 3G connection, and 120s leaves a player staring at a dead frame. What
|
|
196
|
+
// separates the two cases is not how long it has taken — it is whether the game is still
|
|
197
|
+
// TALKING. So the clock is reset by every progress report, and only silence runs it down.
|
|
198
|
+
//
|
|
199
|
+
// A game that never speaks at all gets one window's worth from the moment the frame is created,
|
|
200
|
+
// which is the same promise made to a player whose game is simply broken.
|
|
201
|
+
const PATIENCE = 20000;
|
|
202
|
+
let watchdog: ReturnType<typeof setTimeout> | null = null;
|
|
203
|
+
// Assigned where the promise it resolves is created — a no-op until then, because a progress
|
|
204
|
+
// report cannot arrive before the frame exists and a `bump` that throws would take the bar
|
|
205
|
+
// down with it.
|
|
206
|
+
let bump = () => {};
|
|
207
|
+
|
|
208
|
+
const setProgress = (v: unknown) => {
|
|
209
|
+
bump();
|
|
210
|
+
const n = typeof v === "number" ? v : (v as any)?.value;
|
|
211
|
+
if (typeof n !== "number" || !isFinite(n)) return;
|
|
212
|
+
bar.removeAttribute("data-indeterminate");
|
|
213
|
+
fill.style.width = Math.max(0, Math.min(1, n)) * 100 + "%";
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const start = async () => {
|
|
217
|
+
if (started) return;
|
|
218
|
+
started = true;
|
|
219
|
+
show(wait);
|
|
220
|
+
|
|
221
|
+
const frame = document.createElement("iframe");
|
|
222
|
+
frame.title = stage.dataset.title || "";
|
|
223
|
+
// `allow` carries the gesture forward: the tap that opened this frame is what lets the game
|
|
224
|
+
// play sound at all, and a frame without it starts muted for reasons nobody can see.
|
|
225
|
+
frame.setAttribute("allow", "autoplay; fullscreen");
|
|
226
|
+
frame.setAttribute("referrerpolicy", "same-origin");
|
|
227
|
+
|
|
228
|
+
let bus: any = null;
|
|
229
|
+
try {
|
|
230
|
+
bus = await atrium({
|
|
231
|
+
id: atriumId,
|
|
232
|
+
router: true,
|
|
233
|
+
mount: ({ source }: any) => (source === frame.contentWindow ? "game" : null),
|
|
234
|
+
});
|
|
235
|
+
} catch {
|
|
236
|
+
// atrium failing must not cost the player the game: mount the frame and hand over blind.
|
|
237
|
+
// A game that cannot speak still runs; it simply gets no progress and no early curtain.
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const arrived = bus
|
|
241
|
+
? new Promise<void>((resolve) => bus.on("connected", (e: any) => { if (e?.name === "game") resolve(); }))
|
|
242
|
+
: Promise.resolve();
|
|
243
|
+
|
|
244
|
+
stage.appendChild(frame);
|
|
245
|
+
// ── WHAT CROSSES INTO THE FRAME: AN ALLOWLIST, AND A SHORT ONE ──────────────────────────
|
|
246
|
+
//
|
|
247
|
+
// Forwarding the persona's whole query string would be the obvious convenience and it is a
|
|
248
|
+
// privacy hole: a `?data=` envelope, a campaign's utm tags and anything else a person arrived
|
|
249
|
+
// carrying would be handed to a frame that will one day be a third party's deployment. So the
|
|
250
|
+
// shell passes exactly two flags, both of them DEBUG instruments that belong to whoever is
|
|
251
|
+
// looking at the page rather than to whoever built the game: `xray`, the framework's design
|
|
252
|
+
// grid, and `slow`, which asks a game to spread its own loading out far enough to watch.
|
|
253
|
+
// A game that does not understand either simply ignores them.
|
|
254
|
+
const here = new URLSearchParams(location.search);
|
|
255
|
+
const url = new URL(src, location.href);
|
|
256
|
+
for (const flag of ["xray", "slow"]) if (here.has(flag)) url.searchParams.set(flag, here.get(flag) ?? "");
|
|
257
|
+
frame.src = url.pathname + url.search;
|
|
258
|
+
|
|
259
|
+
// ── THE HANDOVER ─────────────────────────────────────────────────────────────────────────
|
|
260
|
+
//
|
|
261
|
+
// ONE CALL CARRIES BOTH. atrium's `proxy` takes a progress callback and resolves when the
|
|
262
|
+
// method returns, so `boot` is the whole protocol: the game reports as it loads and answers
|
|
263
|
+
// when it can draw. No second channel, no polling, and nothing to keep in sync.
|
|
264
|
+
//
|
|
265
|
+
// ** THE EXPLICIT `timeout` IS LOAD-BEARING. ** atrium's default transaction timeout is ONE
|
|
266
|
+
// SECOND — right for a `pick` or an `echo`, and catastrophic here: every game on earth takes
|
|
267
|
+
// longer than a second to load, so `boot` rejected at 1.000s, the catch below swallowed it,
|
|
268
|
+
// and the shell handed over an empty frame while the game was still fetching. It failed the
|
|
269
|
+
// same way every time and said nothing, which is the whole silent-failure genre in one line.
|
|
270
|
+
// Caught by the spec in harness/tests/behavior.spec.mjs, which asked the bar what it read.
|
|
271
|
+
//
|
|
272
|
+
// The number is deliberately far beyond any real load: the POLICY is the watchdog above, and
|
|
273
|
+
// two clocks racing to enforce one promise means the promise is whichever one you forgot.
|
|
274
|
+
const boot = (async () => {
|
|
275
|
+
if (!bus) return {};
|
|
276
|
+
await arrived;
|
|
277
|
+
return await bus.proxy({
|
|
278
|
+
method: "boot",
|
|
279
|
+
service: "game",
|
|
280
|
+
options: { timeout: 600000, callback: (e: any) => setProgress(e?.progress ?? e?.value ?? e) },
|
|
281
|
+
});
|
|
282
|
+
})();
|
|
283
|
+
|
|
284
|
+
// The watchdog is the other half of the promise made to the player: whatever is wrong in
|
|
285
|
+
// there, they are better off seeing it than seeing us wait for it.
|
|
286
|
+
const giveUp = new Promise<any>((resolve) => {
|
|
287
|
+
bump = () => {
|
|
288
|
+
if (watchdog) clearTimeout(watchdog);
|
|
289
|
+
watchdog = setTimeout(() => resolve({ lateBy: "silence" }), PATIENCE);
|
|
290
|
+
};
|
|
291
|
+
bump();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// A SWALLOWED REASON IS THE FAILURE ITSELF. The frame is handed over either way — a player must
|
|
295
|
+
// not lose the game to a broken handshake — but the catch SAYS what went wrong, because the
|
|
296
|
+
// symptom on the other side of it is "the wait ended early" and nothing else. This cut's own
|
|
297
|
+
// one-second bug lived inside an empty catch and was invisible until a spec read the bar.
|
|
298
|
+
const answer: any = await Promise.race([
|
|
299
|
+
boot.catch((e: any) => {
|
|
300
|
+
console.warn("[preroll] the game's boot did not complete:", e?.message || e);
|
|
301
|
+
return {};
|
|
302
|
+
}),
|
|
303
|
+
giveUp,
|
|
304
|
+
]);
|
|
305
|
+
if (watchdog) clearTimeout(watchdog);
|
|
306
|
+
|
|
307
|
+
show(null);
|
|
308
|
+
// `curtain: true` is the game saying "hand over early, I will cover the rest myself" — which
|
|
309
|
+
// is what a console does, where the logo animation IS the loading screen. Nothing else here
|
|
310
|
+
// changes; the difference is only how long this cut keeps the door shut.
|
|
311
|
+
if (answer?.curtain) root.dataset.curtain = "";
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
playBtn.addEventListener("click", start, { once: true });
|
|
315
|
+
}
|
|
316
|
+
</script>
|
package/src/ground.css
CHANGED
|
@@ -54,6 +54,15 @@
|
|
|
54
54
|
--surface: rgba(255, 255, 255, 0.04);
|
|
55
55
|
--surface-2: rgba(255, 255, 255, 0.07);
|
|
56
56
|
--accent: #0a84ff;
|
|
57
|
+
/* THE INK ON A FILLED ACCENT. Every persona already declares this — the template writes it from
|
|
58
|
+
the accent's own `color.on` — but no cut had rendered a filled accent button until <Preroll>'s
|
|
59
|
+
play surface, so it had never needed a ground and did not have one. It arrives with the same
|
|
60
|
+
lesson the card tokens above carry: a token is not grounded because the fleet defines it, it is
|
|
61
|
+
grounded because THIS package does. The value pairs with the `--accent` on the line above and
|
|
62
|
+
is not scheme-aware, because that accent is not either.
|
|
63
|
+
A persona whose accent is pale — a yellow, an orange — must override it, and every one of them
|
|
64
|
+
already does; white ink on a #f5a623 button is the failure this token exists to prevent. */
|
|
65
|
+
--accent-on: #ffffff;
|
|
57
66
|
/* THE ecosystem's destructive red — Apple's system red, and one of it. Written here rather than
|
|
58
67
|
in each component because a confirmation sheet and a Sign Out row saying "this ends something"
|
|
59
68
|
in two different reds is exactly the drift the ConfirmSheet's own header warns about. It is
|