castle-web-sdk 0.4.7 → 0.4.8

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/README.md CHANGED
@@ -19,6 +19,7 @@ import { setup, initCard, Storage, Leaderboard } from "castle-web-sdk";
19
19
  - [Time](#time)
20
20
  - [User](#user)
21
21
  - [Pass](#pass)
22
+ - [Portal](#portal)
22
23
  - [Lifecycle](#lifecycle)
23
24
  - [Setup](#setup)
24
25
  - [CastleError](#castleerror)
@@ -224,6 +225,50 @@ if (status === "purchased" || status === "alreadyOwned") {
224
225
  }
225
226
  ```
226
227
 
228
+ ## Portal
229
+
230
+ A portal sends the player from this deck to another Castle deck,
231
+ referred to by its deck id.
232
+
233
+ ### `Portal.open(deckId): Promise<PortalOpenResult>`
234
+
235
+ Sends the player to `deckId`: the Castle app swipes the feed to it, the
236
+ website opens its page. There's nowhere to go in the editor or dev server,
237
+ so there it resolves `unavailable`. `PortalOpenResult` has a `status`:
238
+
239
+ - `'navigating'` — the host is moving to the target deck. Treat `open` as
240
+ final: this deck is torn down as it navigates away, so don't rely on code
241
+ running afterward.
242
+ - `'unavailable'` — nowhere to navigate here (the editor or dev server).
243
+
244
+ ```js
245
+ portalButton.onclick = async () => {
246
+ const { status } = await Portal.open(nextDeckId);
247
+ if (status === "unavailable") {
248
+ // No feed to navigate here — fall back to your own affordance.
249
+ }
250
+ };
251
+ ```
252
+
253
+ ### `Portal.prefetch(deckId): Promise<PortalPrefetchResult>`
254
+
255
+ A best-effort hint that the player may soon `open(deckId)`. In the Castle app
256
+ the host warms that deck (fetching it now so a later `open` transitions without
257
+ a cold load); everywhere else it's a no-op. `PortalPrefetchResult`
258
+ has a `status`:
259
+
260
+ - `'prefetching'` — the host accepted the hint (or the deck was already warm).
261
+ - `'rejected'` — the host declined, e.g. this deck has already prefetched its
262
+ limit of upcoming decks. Prefetch a few likely destinations, not everything.
263
+ - `'unavailable'` — the host doesn't prefetch here (the website, editor, or dev
264
+ server).
265
+
266
+ ```js
267
+ link.addEventListener("pointerenter", () => {
268
+ Portal.prefetch(link.dataset.deckId);
269
+ });
270
+ ```
271
+
227
272
  ## Lifecycle
228
273
 
229
274
  `Lifecycle` tells the host when the deck has painted its first frame, so
@@ -256,8 +301,9 @@ Startup, editor-mode check, and a file-write call for editor UI.
256
301
 
257
302
  Call this once at the start of the deck, before any other SDK call.
258
303
  `setup()` initializes the SDK so the rest of the API is usable and
259
- mounts the centered 5:7 card shell around whatever the deck renders
260
- into `#root` (when the deck is being played standalone in a browser).
304
+ mounts a 5:7 Castle card around whatever the deck renders into `#root`
305
+ in play mode. The SDK preserves the card aspect ratio; Castle hosts own
306
+ the card's max size, placement, and surrounding padding.
261
307
  While running locally with `castle-web serve`, it also forwards
262
308
  `console` output to the CLI and reloads the page when `castle-web
263
309
  restart` runs.
@@ -271,9 +317,10 @@ setup();
271
317
  ### `initCard(): HTMLDivElement`
272
318
 
273
319
  Use this when the deck draws into a `<canvas>` (or anything else)
274
- rather than into the React tree at `#root`. Returns a centered,
275
- viewport-sized `<div>` with the standard Castle 5:7 card aspect ratio.
276
- The div resizes itself when the window resizes.
320
+ rather than into the React tree at `#root`. Returns a `<div>` with the
321
+ standard Castle 5:7 card aspect ratio. The div resizes itself when the
322
+ window resizes. In Castle-hosted iframes/WebViews it fills the host's
323
+ available card frame.
277
324
 
278
325
  ```js
279
326
  import { setup, initCard } from "castle-web-sdk";
@@ -289,6 +336,10 @@ card.appendChild(canvas);
289
336
  If the deck mounts a React tree into `#root` instead, you don't need
290
337
  `initCard()` — `setup()` already wraps `#root`'s children in a card.
291
338
 
339
+ Host-specific layout flags such as `CastleEmbed.feed` are deprecated.
340
+ New code should let Castle hosts provide the frame and let the SDK infer
341
+ whether it is running standalone or embedded.
342
+
292
343
  ### `CARD_RATIO`
293
344
 
294
345
  The card aspect ratio (`5 / 7`). Use this if you need to size something
package/dist/castle.d.ts CHANGED
@@ -6,6 +6,8 @@ export { Lifecycle } from "./lifecycle";
6
6
  export type { CastleLifecycleApi } from "./lifecycle";
7
7
  export { Pass } from "./passes";
8
8
  export type { CastlePassApi, PassOfferResult, PassOfferStatus, } from "./passes";
9
+ export { Portal } from "./portal";
10
+ export type { CastlePortalApi, PortalOpenResult, PortalOpenStatus, PortalPrefetchResult, PortalPrefetchStatus, } from "./portal";
9
11
  export { CARD_RATIO, initCard, onBeforeRestart, setup, writeFile } from "./runtime";
10
12
  export { SharedStorage, Storage } from "./storage";
11
13
  export { Time } from "./time";
package/dist/castle.js CHANGED
@@ -4,6 +4,7 @@ export { CastleError } from "./errors";
4
4
  export { Leaderboard } from "./leaderboard";
5
5
  export { Lifecycle } from "./lifecycle";
6
6
  export { Pass } from "./passes";
7
+ export { Portal } from "./portal";
7
8
  export { CARD_RATIO, initCard, onBeforeRestart, setup, writeFile } from "./runtime";
8
9
  export { SharedStorage, Storage } from "./storage";
9
10
  export { Time } from "./time";
@@ -24,6 +24,14 @@ export type PassOfferStatus = "purchased" | "alreadyOwned" | "cancelled" | "unav
24
24
  export interface PassOfferResult {
25
25
  status: PassOfferStatus;
26
26
  }
27
+ export type PortalOpenStatus = "navigating" | "unavailable";
28
+ export interface PortalOpenResult {
29
+ status: PortalOpenStatus;
30
+ }
31
+ export type PortalPrefetchStatus = "prefetching" | "rejected" | "unavailable";
32
+ export interface PortalPrefetchResult {
33
+ status: PortalPrefetchStatus;
34
+ }
27
35
  export interface CommandParams {
28
36
  "deckStorage.load": Record<string, never>;
29
37
  "deckStorage.update": {
@@ -57,6 +65,12 @@ export interface CommandParams {
57
65
  "pass.offer": {
58
66
  passId: string;
59
67
  };
68
+ "portal.open": {
69
+ targetDeckId: string;
70
+ };
71
+ "portal.prefetch": {
72
+ targetDeckId: string;
73
+ };
60
74
  }
61
75
  export interface CommandResult {
62
76
  "deckStorage.load": {
@@ -93,6 +107,8 @@ export interface CommandResult {
93
107
  hasPass: boolean;
94
108
  };
95
109
  "pass.offer": PassOfferResult;
110
+ "portal.open": PortalOpenResult;
111
+ "portal.prefetch": PortalPrefetchResult;
96
112
  }
97
113
  export type CommandName = keyof CommandParams;
98
114
  export interface SerializedCommandError {
package/dist/context.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  export type CastleHost = "web" | "mobile" | "dev";
2
2
  export interface CastleEmbed {
3
3
  edit?: boolean;
4
+ /**
5
+ * @deprecated Hosts should not set this for new behavior. The SDK now infers
6
+ * host-framed vs standalone presentation from its runtime context.
7
+ */
4
8
  feed?: boolean;
5
9
  host?: CastleHost;
6
10
  }
@@ -0,0 +1,7 @@
1
+ import type { PortalOpenResult, PortalPrefetchResult } from "./commands";
2
+ export type { PortalOpenResult, PortalOpenStatus, PortalPrefetchResult, PortalPrefetchStatus, } from "./commands";
3
+ export interface CastlePortalApi {
4
+ open(deckId: string): Promise<PortalOpenResult>;
5
+ prefetch(deckId: string): Promise<PortalPrefetchResult>;
6
+ }
7
+ export declare const Portal: CastlePortalApi;
package/dist/portal.js ADDED
@@ -0,0 +1,35 @@
1
+ // Portal — a deck-facing capability for sending the player to another Castle
2
+ // deck ("following a portal"). The host owns the transition: in the Castle feed
3
+ // it swipes to the target deck (which may itself be a web deck or a normal
4
+ // engine deck); elsewhere (the website, the dev server, the editor) there's no
5
+ // feed to navigate, so it resolves `unavailable` and nothing happens.
6
+ //
7
+ // Navigation is effectively fire-and-forget: once the host accepts it, the
8
+ // source deck is torn down as the feed moves to the target, so a deck should
9
+ // not rely on code running after a successful `open`.
10
+ import { CastleError } from "./errors";
11
+ import { hostRequest } from "./transport";
12
+ export const Portal = {
13
+ open,
14
+ prefetch,
15
+ };
16
+ async function open(deckId) {
17
+ if (typeof deckId !== "string" || deckId.length === 0) {
18
+ throw new CastleError({
19
+ code: "INVALID_ARGUMENT",
20
+ message: "Portal.open requires a deckId.",
21
+ operation: "Portal.open",
22
+ });
23
+ }
24
+ return hostRequest("portal.open", { targetDeckId: deckId });
25
+ }
26
+ async function prefetch(deckId) {
27
+ if (typeof deckId !== "string" || deckId.length === 0) {
28
+ throw new CastleError({
29
+ code: "INVALID_ARGUMENT",
30
+ message: "Portal.prefetch requires a deckId.",
31
+ operation: "Portal.prefetch",
32
+ });
33
+ }
34
+ return hostRequest("portal.prefetch", { targetDeckId: deckId });
35
+ }
package/dist/runtime.js CHANGED
@@ -48,11 +48,6 @@ export function initCard() {
48
48
  card.id = "castle-card";
49
49
  document.body.appendChild(card);
50
50
  function resize() {
51
- if (getCastleEmbed()?.feed === true) {
52
- card.style.width = "100vw";
53
- card.style.height = "100vh";
54
- return;
55
- }
56
51
  const { w, h } = computeCardSize();
57
52
  card.style.width = w + "px";
58
53
  card.style.height = h + "px";
@@ -61,14 +56,11 @@ export function initCard() {
61
56
  window.addEventListener("resize", resize);
62
57
  return card;
63
58
  }
64
- // Constrains whatever the deck renders into #root to a centered 5:7 card.
65
- // The mobile feed host card-sizes its WebView itself, and the editor needs the
66
- // full viewport, so the card shell applies only to standalone play.
59
+ // Constrains whatever the deck renders into #root to a 5:7 card in play mode.
60
+ // Hosts own max size and padding; the SDK only preserves the card aspect ratio.
67
61
  function initPlayCard() {
68
62
  if (isEdit())
69
63
  return;
70
- if (getCastleEmbed()?.feed === true)
71
- return;
72
64
  const style = document.createElement("style");
73
65
  style.textContent = `
74
66
  html, body { background: #000; }
@@ -94,23 +86,27 @@ function initPlayCard() {
94
86
  window.addEventListener("resize", resize);
95
87
  }
96
88
  function computeCardSize() {
97
- const maxW = 450;
98
- const maxH = 630;
99
- const pad = 20;
100
- const aw = window.innerWidth - pad * 2;
101
- const ah = window.innerHeight - pad * 2;
89
+ const pad = shouldUseStandaloneChrome() ? 20 : 0;
90
+ const aw = Math.max(1, window.innerWidth - pad * 2);
91
+ const ah = Math.max(1, window.innerHeight - pad * 2);
102
92
  let w;
103
93
  let h;
104
94
  if (aw / ah < CARD_RATIO) {
105
- w = Math.min(aw, maxW);
95
+ w = aw;
106
96
  h = w / CARD_RATIO;
107
97
  }
108
98
  else {
109
- h = Math.min(ah, maxH);
99
+ h = ah;
110
100
  w = h * CARD_RATIO;
111
101
  }
112
102
  return { w, h };
113
103
  }
104
+ function shouldUseStandaloneChrome() {
105
+ if (window.parent !== window)
106
+ return false;
107
+ const embed = getCastleEmbed();
108
+ return embed?.host === "dev";
109
+ }
114
110
  function sendMsg(msg) {
115
111
  if (ws && ws.readyState === WebSocket.OPEN) {
116
112
  ws.send(JSON.stringify(msg));
package/dist/transport.js CHANGED
@@ -17,6 +17,7 @@ const REQUEST_TIMEOUT_MS = 15000;
17
17
  // is wrong for them: they get NO timeout and resolve only when the host replies.
18
18
  const INTERACTIVE_COMMANDS = new Set([
19
19
  "pass.offer",
20
+ "portal.open",
20
21
  ]);
21
22
  let nextRequestId = 1;
22
23
  const pending = new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-sdk",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "type": "module",
5
5
  "main": "dist/castle.js",
6
6
  "types": "dist/castle.d.ts",