castle-web-sdk 0.4.9 → 0.4.11

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
@@ -20,6 +20,7 @@ import { setup, initCard, Storage, Leaderboard } from "castle-web-sdk";
20
20
  - [User](#user)
21
21
  - [Pass](#pass)
22
22
  - [Portal](#portal)
23
+ - [Haptics](#haptics)
23
24
  - [Lifecycle](#lifecycle)
24
25
  - [Setup](#setup)
25
26
  - [CastleError](#castleerror)
@@ -269,6 +270,41 @@ link.addEventListener("pointerenter", () => {
269
270
  });
270
271
  ```
271
272
 
273
+ ## Haptics
274
+
275
+ `Haptics` plays a short device vibration ("buzz") for tactile feedback —
276
+ a tap confirmation, a success chime, an error shake. The host owns the
277
+ effect: the Castle mobile app plays a native haptic, the website uses the
278
+ browser's vibration API where available, and the dev server or an
279
+ unsupported device does nothing.
280
+
281
+ ### `Haptics.play(style): Promise<HapticsResult>`
282
+
283
+ Plays a haptic in one of seven styles:
284
+
285
+ - `'light'`, `'medium'`, `'heavy'` — impact taps of increasing strength.
286
+ - `'selection'` — a light tick, e.g. moving through options.
287
+ - `'success'`, `'warning'`, `'error'` — notification patterns.
288
+
289
+ Usually you don't await it — fire it and move on:
290
+
291
+ ```js
292
+ button.onclick = () => {
293
+ Haptics.play("light");
294
+ doTheThing();
295
+ };
296
+ ```
297
+
298
+ The returned `HapticsResult` has a `status`, if you want to branch on it:
299
+
300
+ - `'triggered'` — the host played (or accepted) the haptic.
301
+ - `'unavailable'` — this host or device can't play haptics (e.g. the dev
302
+ server, or a browser with no vibration support). Nothing was played.
303
+
304
+ Haptics respect the player's settings — if a player has muted haptics,
305
+ `play` does nothing and resolves `'unavailable'`. Rapid repeated calls may
306
+ be coalesced, so it's safe to call on frequent events.
307
+
272
308
  ## Lifecycle
273
309
 
274
310
  `Lifecycle` tells the host when the deck has painted its first frame, so
package/dist/castle.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export { isEdit } from "./context";
2
2
  export { CastleError } from "./errors";
3
+ export { deckPrefixOf, IMPORTS_DIR, IMPORTS_PREFIX, isImportedFile, resolveDeckFile, } from "./files";
4
+ export { Haptics } from "./haptics";
5
+ export type { CastleHapticsApi, HapticsResult, HapticsStatus, HapticStyle } from "./haptics";
3
6
  export { Leaderboard } from "./leaderboard";
4
7
  export type { LeaderboardData, LeaderboardEntry, LeaderboardOptions, LeaderboardScope, LeaderboardSort, } from "./leaderboard";
5
8
  export { Lifecycle } from "./lifecycle";
package/dist/castle.js CHANGED
@@ -1,6 +1,8 @@
1
1
  // Castle Web SDK
2
2
  export { isEdit } from "./context";
3
3
  export { CastleError } from "./errors";
4
+ export { deckPrefixOf, IMPORTS_DIR, IMPORTS_PREFIX, isImportedFile, resolveDeckFile, } from "./files";
5
+ export { Haptics } from "./haptics";
4
6
  export { Leaderboard } from "./leaderboard";
5
7
  export { Lifecycle } from "./lifecycle";
6
8
  export { Pass } from "./passes";
@@ -32,6 +32,11 @@ export type PortalPrefetchStatus = "prefetching" | "rejected" | "unavailable";
32
32
  export interface PortalPrefetchResult {
33
33
  status: PortalPrefetchStatus;
34
34
  }
35
+ export type HapticStyle = "light" | "medium" | "heavy" | "selection" | "success" | "warning" | "error";
36
+ export type HapticsStatus = "triggered" | "unavailable";
37
+ export interface HapticsResult {
38
+ status: HapticsStatus;
39
+ }
35
40
  export interface CommandParams {
36
41
  "deckStorage.load": Record<string, never>;
37
42
  "deckStorage.update": {
@@ -71,6 +76,9 @@ export interface CommandParams {
71
76
  "portal.prefetch": {
72
77
  targetDeckId: string;
73
78
  };
79
+ "haptics.play": {
80
+ style: HapticStyle;
81
+ };
74
82
  }
75
83
  export interface CommandResult {
76
84
  "deckStorage.load": {
@@ -109,6 +117,7 @@ export interface CommandResult {
109
117
  "pass.offer": PassOfferResult;
110
118
  "portal.open": PortalOpenResult;
111
119
  "portal.prefetch": PortalPrefetchResult;
120
+ "haptics.play": HapticsResult;
112
121
  }
113
122
  export type CommandName = keyof CommandParams;
114
123
  export interface SerializedCommandError {
@@ -0,0 +1,15 @@
1
+ export declare const IMPORTS_PREFIX = "@imports/";
2
+ export declare const IMPORTS_DIR = "imports";
3
+ export declare function deckPrefixOf(filePath: string): string;
4
+ /**
5
+ * Resolve a reference to the key it names in the deck's file map.
6
+ *
7
+ * @param ref the reference as written -- `drawings/rock.pxart` or
8
+ * `@imports/someone.pack/drawings/rock.pxart`
9
+ * @param from the path of the file the reference was written in; a reference in
10
+ * a deck's own file resolves against the deck root, one in an
11
+ * import's file against that import.
12
+ */
13
+ export declare function resolveDeckFile(ref: string, from?: string): string;
14
+ /** Whether a path names a file some import owns (rather than the deck's own). */
15
+ export declare function isImportedFile(filePath: string): boolean;
package/dist/files.js ADDED
@@ -0,0 +1,52 @@
1
+ // How a deck names a file -- one rule, everywhere a path appears: in JS module
2
+ // specifiers, in a scene's `blueprint`, in a Sprite's `file`, in anything a kit
3
+ // invents later.
4
+ //
5
+ // drawings/rock.pxart a file of the deck the REFERENCE lives in
6
+ // @imports/<alias>/drawings/rock.pxart a file of that import
7
+ //
8
+ // The unprefixed form is deck-relative, and "the deck" means whichever deck the
9
+ // referencing file belongs to. So a kit's blueprint saying `drawings/cauldron.pxart`
10
+ // means the kit's own drawing whether the kit is the deck being played or an
11
+ // import of it -- the same text keeps meaning the same file after the kit is
12
+ // imported by somebody else. That is what makes a deck publishable as a
13
+ // dependency without rewriting its insides.
14
+ //
15
+ // `@imports/` is the only way to name someone else's file, so a cross-deck
16
+ // reference is visible as one at a glance, and it means the same thing from any
17
+ // depth: imports are flattened into one `imports/` at the deck root, so the
18
+ // prefix is positional in nothing.
19
+ export const IMPORTS_PREFIX = '@imports/';
20
+ // Where imports live on disk, relative to the deck root. `@imports/x/y` is
21
+ // exactly `imports/x/y` -- the alias exists so the reference reads as a
22
+ // cross-deck one and does not depend on where the referencing file sits.
23
+ export const IMPORTS_DIR = 'imports';
24
+ function stripDotSlash(path) {
25
+ return path.replace(/^\.\//, '');
26
+ }
27
+ // The deck a file belongs to, as a path prefix: `imports/<alias>/` for a file of
28
+ // an import, `''` for one of the deck's own.
29
+ export function deckPrefixOf(filePath) {
30
+ const match = new RegExp(`^(${IMPORTS_DIR}/[^/]+/)`).exec(filePath ?? '');
31
+ return match ? match[1] : '';
32
+ }
33
+ /**
34
+ * Resolve a reference to the key it names in the deck's file map.
35
+ *
36
+ * @param ref the reference as written -- `drawings/rock.pxart` or
37
+ * `@imports/someone.pack/drawings/rock.pxart`
38
+ * @param from the path of the file the reference was written in; a reference in
39
+ * a deck's own file resolves against the deck root, one in an
40
+ * import's file against that import.
41
+ */
42
+ export function resolveDeckFile(ref, from = '') {
43
+ const path = stripDotSlash(String(ref ?? ''));
44
+ if (path.startsWith(IMPORTS_PREFIX)) {
45
+ return `${IMPORTS_DIR}/${path.slice(IMPORTS_PREFIX.length)}`;
46
+ }
47
+ return deckPrefixOf(stripDotSlash(from)) + path;
48
+ }
49
+ /** Whether a path names a file some import owns (rather than the deck's own). */
50
+ export function isImportedFile(filePath) {
51
+ return deckPrefixOf(filePath) !== '';
52
+ }
@@ -0,0 +1,6 @@
1
+ import type { HapticStyle, HapticsResult } from "./commands";
2
+ export type { HapticStyle, HapticsResult, HapticsStatus } from "./commands";
3
+ export interface CastleHapticsApi {
4
+ play(style: HapticStyle): Promise<HapticsResult>;
5
+ }
6
+ export declare const Haptics: CastleHapticsApi;
@@ -0,0 +1,36 @@
1
+ // Haptics — a deck-facing capability for playing a device haptic (a "buzz").
2
+ // Like pass and portal, the deck stays capability-AGNOSTIC: it asks for a
3
+ // haptic and gets back one normalized outcome regardless of platform. The host
4
+ // owns the effect:
5
+ // - mobile app : plays a native haptic (Taptic Engine / VibrationEffect),
6
+ // subject to the host's own rate-limiting and device support
7
+ // - web player : best-effort navigator.vibrate; `unavailable` where the
8
+ // browser has no vibration API (e.g. iOS Safari)
9
+ // - dev CLI : no host handler, so `unavailable`
10
+ // A haptic is a fleeting device effect, not deck-scoped state, so unlike
11
+ // pass/portal the host requires no deckId. Decks usually fire-and-forget (don't
12
+ // await); the returned status just says whether the host could play it.
13
+ import { CastleError } from "./errors";
14
+ import { hostRequest } from "./transport";
15
+ const HAPTIC_STYLES = [
16
+ "light",
17
+ "medium",
18
+ "heavy",
19
+ "selection",
20
+ "success",
21
+ "warning",
22
+ "error",
23
+ ];
24
+ export const Haptics = {
25
+ play,
26
+ };
27
+ async function play(style) {
28
+ if (!HAPTIC_STYLES.includes(style)) {
29
+ throw new CastleError({
30
+ code: "INVALID_ARGUMENT",
31
+ message: `Haptics.play style must be one of: ${HAPTIC_STYLES.join(", ")}.`,
32
+ operation: "Haptics.play",
33
+ });
34
+ }
35
+ return hostRequest("haptics.play", { style });
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-sdk",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "type": "module",
5
5
  "main": "dist/castle.js",
6
6
  "types": "dist/castle.d.ts",