@wyndlass/app-sdk 0.0.0 → 1.1.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.
Files changed (75) hide show
  1. package/README.md +212 -1
  2. package/cli/app-package.mjs +111 -0
  3. package/cli/commands.mjs +133 -0
  4. package/cli/main.mjs +73 -0
  5. package/cli/package.mjs +119 -0
  6. package/cli/preview.mjs +142 -0
  7. package/cli/zip.mjs +63 -0
  8. package/dist/contract/app-contract.d.ts +221 -0
  9. package/dist/contract/app-contract.js +6 -0
  10. package/dist/contract/app-definition.d.ts +155 -0
  11. package/dist/contract/app-definition.js +3 -0
  12. package/dist/contract/data-answer.d.ts +40 -0
  13. package/dist/contract/data-answer.js +3 -0
  14. package/dist/contract/data-calendar.d.ts +57 -0
  15. package/dist/contract/data-calendar.js +3 -0
  16. package/dist/contract/data-news.d.ts +29 -0
  17. package/dist/contract/data-news.js +3 -0
  18. package/dist/contract/data-quotes.d.ts +75 -0
  19. package/dist/contract/data-quotes.js +3 -0
  20. package/dist/contract/data-series.d.ts +51 -0
  21. package/dist/contract/data-series.js +3 -0
  22. package/dist/contract/data-snapshot.d.ts +30 -0
  23. package/dist/contract/data-snapshot.js +3 -0
  24. package/dist/contract/data-weather.d.ts +421 -0
  25. package/dist/contract/data-weather.js +3 -0
  26. package/dist/contract/player-api-event.d.ts +253 -0
  27. package/dist/contract/player-api-event.js +3 -0
  28. package/dist/contract/player-api-status.d.ts +178 -0
  29. package/dist/contract/player-api-status.js +3 -0
  30. package/dist/easing.d.ts +16 -0
  31. package/dist/easing.js +71 -0
  32. package/dist/index.d.ts +94 -0
  33. package/dist/index.js +218 -0
  34. package/dist/preview/frame/frame.js +2 -0
  35. package/dist/preview/frame/index.html +12 -0
  36. package/dist/preview/index.html +40 -0
  37. package/dist/preview/page.js +7 -0
  38. package/dist/qr.d.ts +5 -0
  39. package/dist/qr.js +14 -0
  40. package/dist/react.d.ts +52 -0
  41. package/dist/react.js +149 -0
  42. package/package.json +83 -4
  43. package/protocol/app-definition.schema.json +435 -0
  44. package/protocol/app-packages.json +172 -0
  45. package/protocol/common.schema.json +197 -0
  46. package/protocol/samples/calendar.json +63 -0
  47. package/protocol/samples/events/app.command.json +14 -0
  48. package/protocol/samples/events/item.end.json +13 -0
  49. package/protocol/samples/events/item.start.json +18 -0
  50. package/protocol/samples/events/state.changed.json +11 -0
  51. package/protocol/samples/news.json +37 -0
  52. package/protocol/samples/player-status.json +58 -0
  53. package/protocol/samples/weather.json +59 -0
  54. package/skill/wyndlass-app/SKILL.md +90 -0
  55. package/skill/wyndlass-app/references/contract.md +91 -0
  56. package/skill/wyndlass-app/references/mcp.md +79 -0
  57. package/skill/wyndlass-app/references/templates.md +38 -0
  58. package/templates/react/AGENTS.md +17 -0
  59. package/templates/react/README.md +23 -0
  60. package/templates/react/app.json +14 -0
  61. package/templates/react/gitignore +2 -0
  62. package/templates/react/package.json +22 -0
  63. package/templates/react/src/app.css +7 -0
  64. package/templates/react/src/app.tsx +39 -0
  65. package/templates/react/src/env.d.ts +13 -0
  66. package/templates/react/tsconfig.json +13 -0
  67. package/templates/vanilla/AGENTS.md +17 -0
  68. package/templates/vanilla/README.md +23 -0
  69. package/templates/vanilla/app.json +14 -0
  70. package/templates/vanilla/gitignore +2 -0
  71. package/templates/vanilla/package.json +18 -0
  72. package/templates/vanilla/src/app.css +7 -0
  73. package/templates/vanilla/src/app.ts +59 -0
  74. package/templates/vanilla/src/env.d.ts +13 -0
  75. package/templates/vanilla/tsconfig.json +12 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Device id.
3
+ */
4
+ export type CommonDeviceId = string;
5
+ export type CommonSemver = string;
6
+ /**
7
+ * IANA time zone name.
8
+ */
9
+ export type CommonTimeZone = string;
10
+ /**
11
+ * BCP 47 language tag.
12
+ */
13
+ export type CommonLocale = string;
14
+ /**
15
+ * Whether the player is connected to Wyndlass (NATS).
16
+ */
17
+ export type PlayerApiCommonConnection = "online" | "offline";
18
+ /**
19
+ * idle: nothing to play; stopped: the standby screen.
20
+ */
21
+ export type State = "starting" | "playing" | "paused" | "stopped" | "idle" | "error";
22
+ /**
23
+ * Who caused a change: the player itself, an app on the screen, or a remote caller whose principal core relayed.
24
+ */
25
+ export type PlayerApiCommonBy = {
26
+ kind: "player";
27
+ } | {
28
+ kind: "app";
29
+ /**
30
+ * The app's id (app-definition).
31
+ */
32
+ app: string;
33
+ instance: string;
34
+ } | {
35
+ kind: "remote";
36
+ principal: CommonPrincipalRef;
37
+ };
38
+ /**
39
+ * A principal, e.g. user:u_123, agent:fleet-guardian, device:dev_...
40
+ */
41
+ export type CommonPrincipalRef = string;
42
+ /**
43
+ * RFC 3339 timestamp with offset.
44
+ */
45
+ export type CommonTimestamp = string;
46
+ /**
47
+ * Lowercase hex SHA-256 digest.
48
+ */
49
+ export type CommonSha256 = string;
50
+ /**
51
+ * RFC 3339 timestamp with offset.
52
+ */
53
+ export type CommonTimestamp1 = string;
54
+ /**
55
+ * A zone of the screen; main until compositions have zones.
56
+ */
57
+ export type PlayerApiCommonZone = string;
58
+ /**
59
+ * Media item id (asset, app, composition, playlist or live source).
60
+ */
61
+ export type CommonMediaId = string;
62
+ /**
63
+ * Everything the controller knows about the player at once: player.status's result, and what apps read through their controller (protocol/player-api.json, ADR 0050).
64
+ */
65
+ export interface PlayerAPIStatus {
66
+ apiVersion: number;
67
+ device: PlayerApiCommonDeviceFacts;
68
+ /**
69
+ * Where player-core runs; in preview and the designer, commands change nothing.
70
+ */
71
+ mode: "player" | "preview" | "designer";
72
+ timeZone: CommonTimeZone;
73
+ locale: CommonLocale;
74
+ connection: PlayerApiCommonConnection;
75
+ state: State;
76
+ held?: Held;
77
+ content?: Content;
78
+ /**
79
+ * What each zone shows.
80
+ *
81
+ * @maxItems 64
82
+ */
83
+ now: NowItem[];
84
+ audio?: PlayerApiCommonAudio;
85
+ display?: PlayerApiCommonDisplay;
86
+ capabilities: PlayerApiCommonHostCapabilities;
87
+ }
88
+ /**
89
+ * What the host says the device is; fixed while the player runs.
90
+ */
91
+ export interface PlayerApiCommonDeviceFacts {
92
+ id: CommonDeviceId;
93
+ /**
94
+ * The device type (protocol/device-types).
95
+ */
96
+ type: string;
97
+ /**
98
+ * Which shell hosts player-core.
99
+ */
100
+ shell: "android" | "universal-electron" | "universal-brightsign" | "wpf" | "webos" | "simulator";
101
+ shellVersion: CommonSemver;
102
+ coreVersion: CommonSemver;
103
+ model?: string;
104
+ resolution: PlayerApiCommonResolution;
105
+ orientation: "landscape" | "portrait";
106
+ }
107
+ export interface PlayerApiCommonResolution {
108
+ width: number;
109
+ height: number;
110
+ }
111
+ /**
112
+ * A remote pause or stop in effect: apps' playback commands are refused with HELD until a remote play or a restart (ADR 0050).
113
+ */
114
+ export interface Held {
115
+ by: PlayerApiCommonBy;
116
+ since: CommonTimestamp;
117
+ }
118
+ /**
119
+ * The manifest playing; absent when there is none.
120
+ */
121
+ export interface Content {
122
+ manifest: CommonSha256;
123
+ issuedAt: CommonTimestamp;
124
+ switchedAt: CommonTimestamp;
125
+ windowTo?: CommonTimestamp1;
126
+ /**
127
+ * The window has ended: the player plays this content until a newer manifest arrives (WP-0053).
128
+ */
129
+ stale?: boolean;
130
+ }
131
+ export interface NowItem {
132
+ zone: PlayerApiCommonZone;
133
+ item: CommonItemRef;
134
+ blob: CommonSha256;
135
+ contentType: string;
136
+ startedAt: CommonTimestamp;
137
+ durationMs?: number;
138
+ positionMs?: number;
139
+ }
140
+ /**
141
+ * A pinned reference to a media item version.
142
+ */
143
+ export interface CommonItemRef {
144
+ id: CommonMediaId;
145
+ version: number;
146
+ }
147
+ /**
148
+ * The player's own level, not the operating system's.
149
+ */
150
+ export interface PlayerApiCommonAudio {
151
+ volume: number;
152
+ muted: boolean;
153
+ }
154
+ export interface PlayerApiCommonDisplay {
155
+ power: "on" | "standby" | "unknown";
156
+ brightness?: number;
157
+ }
158
+ /**
159
+ * What the host can do beyond the controller's own; tools that need what it lacks are not listed.
160
+ */
161
+ export interface PlayerApiCommonHostCapabilities {
162
+ /**
163
+ * The player's volume and mute can be set.
164
+ */
165
+ audio: boolean;
166
+ /**
167
+ * With audio, whether the volume can be set to a level (audio.setVolume). False where only mute can: the Amazon Signage Stick, whose API sets no level on any TV (WP-0096 part g). Absent means it can.
168
+ */
169
+ volume?: boolean;
170
+ display: {
171
+ power: boolean;
172
+ brightness: boolean;
173
+ /**
174
+ * The host turns its whole output, native surfaces included, by the system (ADR 0052, decision 4): host.setDisplay's rotation. Without it, the page turns itself (WP-0056 part d).
175
+ */
176
+ rotation?: boolean;
177
+ };
178
+ }
@@ -0,0 +1,3 @@
1
+ // Copied from protocol/generated/ts/player-api-status.d.ts by app-sdk/scripts/copy-protocol.mjs: edit the source, then run `pnpm --filter @wyndlass/app-sdk copy-protocol`.
2
+ /* Generated from protocol/schemas by scripts/generate.mjs. Do not edit. */
3
+ export {};
@@ -0,0 +1,16 @@
1
+ /** A CSS easing name or cubic-bezier(a, b, c, d) as a cubic Bézier; `ease` when absent or unreadable. */
2
+ export declare function easingOf(name: string | undefined): readonly number[];
3
+ /**
4
+ * The cubic Bézier `easing` at x, as browsers solve it: x(t) inverted by Newton's method, then bisection, to 1e-7; then
5
+ * y at that t. 0 before 0 and 1 after 1.
6
+ */
7
+ export declare function cubicBezier(easing: readonly number[], x: number): number;
8
+ /**
9
+ * When each of n items animates in a staggered enter or exit (ADR 0071, decision 4): item i starts i × stagger after
10
+ * the start, and every item runs for what is left, so the last ends with the whole. The stagger is shortened, to whole
11
+ * milliseconds, where it would leave an item less than 16 ms (one frame).
12
+ */
13
+ export declare function staggerTimes(startMs: number, durationMs: number, staggerMs: number | undefined, n: number): {
14
+ startMs: number;
15
+ endMs: number;
16
+ }[];
package/dist/easing.js ADDED
@@ -0,0 +1,71 @@
1
+ // The presets' curves (WP-0101 decision 2): the SDK's own, so it imports nothing of player-core's. Both give every case
2
+ // of protocol/test-vectors/easing.json, so an app's enter and exit move as the layout's clips around it do.
3
+ const NAMED = {
4
+ linear: [0, 0, 1, 1], ease: [0.25, 0.1, 0.25, 1], "ease-in": [0.42, 0, 1, 1], "ease-out": [0, 0, 0.58, 1], "ease-in-out": [0.42, 0, 0.58, 1],
5
+ };
6
+ /** A CSS easing name or cubic-bezier(a, b, c, d) as a cubic Bézier; `ease` when absent or unreadable. */
7
+ export function easingOf(name) {
8
+ if (!name)
9
+ return NAMED.ease;
10
+ if (NAMED[name])
11
+ return NAMED[name];
12
+ const m = /^cubic-bezier\(\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^)]+)\)$/.exec(name);
13
+ const v = m?.slice(1).map(Number);
14
+ return v && v.every(Number.isFinite) ? v : NAMED.ease;
15
+ }
16
+ /**
17
+ * The cubic Bézier `easing` at x, as browsers solve it: x(t) inverted by Newton's method, then bisection, to 1e-7; then
18
+ * y at that t. 0 before 0 and 1 after 1.
19
+ */
20
+ export function cubicBezier(easing, x) {
21
+ if (x <= 0)
22
+ return 0;
23
+ if (x >= 1)
24
+ return 1;
25
+ const [x1, y1, x2, y2] = easing;
26
+ if (x1 === y1 && x2 === y2)
27
+ return x;
28
+ const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx;
29
+ const cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
30
+ const sampleX = (t) => ((ax * t + bx) * t + cx) * t;
31
+ const sampleY = (t) => ((ay * t + by) * t + cy) * t;
32
+ const slopeX = (t) => (3 * ax * t + 2 * bx) * t + cx;
33
+ let t = x;
34
+ for (let i = 0; i < 8; i++) {
35
+ const err = sampleX(t) - x;
36
+ if (Math.abs(err) < 1e-7)
37
+ return sampleY(t);
38
+ const d = slopeX(t);
39
+ if (Math.abs(d) < 1e-6)
40
+ break;
41
+ t -= err / d;
42
+ }
43
+ let lo = 0, hi = 1;
44
+ t = x;
45
+ while (lo < hi) {
46
+ const v = sampleX(t);
47
+ if (Math.abs(v - x) < 1e-7)
48
+ break;
49
+ if (x > v)
50
+ lo = t;
51
+ else
52
+ hi = t;
53
+ t = (hi - lo) / 2 + lo;
54
+ if (hi - lo < 1e-7)
55
+ break;
56
+ }
57
+ return sampleY(t);
58
+ }
59
+ /**
60
+ * When each of n items animates in a staggered enter or exit (ADR 0071, decision 4): item i starts i × stagger after
61
+ * the start, and every item runs for what is left, so the last ends with the whole. The stagger is shortened, to whole
62
+ * milliseconds, where it would leave an item less than 16 ms (one frame).
63
+ */
64
+ export function staggerTimes(startMs, durationMs, staggerMs, n) {
65
+ if (n <= 0)
66
+ return [];
67
+ const most = n > 1 ? Math.floor(Math.max(0, durationMs - 16) / (n - 1)) : 0;
68
+ const s = Math.min(staggerMs ?? 0, most);
69
+ const each = durationMs - (n - 1) * s;
70
+ return Array.from({ length: n }, (_, i) => ({ startMs: startMs + i * s, endMs: startMs + i * s + each }));
71
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The app runtime (WP-0101, ADR 0101): what every app on Wyndlass's screens shares, Wyndlass's own and an
3
+ * organization's. Its context from the host, its props with their defaults, drawing from the layout's clock (so seeking
4
+ * and pausing draw the right moment), saying it is ready once, and its enter and exit presets on that clock.
5
+ *
6
+ * Both shapes of the app contract (WP-0100, ADR 0100): a host of Player API 2 defines the bundle's default export under
7
+ * a tag of its own and hands it the context once, through ATTACH, leaving nothing on the element; an older one reads the
8
+ * tag the bundle defined (`define`) and sets the context as `element.wyndlass`.
9
+ *
10
+ * Outside a host (a unit test, the SDK's preview before it gives one) an app still runs: the time is now, each prop is
11
+ * its default, data and images are refused as unavailable, and ready() does nothing.
12
+ */
13
+ import { ATTACH, type AppContext, type AppController, type DataAnswer, type HookOptions, type ImpressionsFeed } from "./contract/app-contract.js";
14
+ export { ATTACH };
15
+ export type { AppContext, AppController, AppElement, AppModule, AppCapability, DataAnswer, HookOptions, ImpressionsFeed, ImpressionsNow, ImpressionEvent, ScreenFacts, TableBinding, TableRule, TimeNow, NotApplied, } from "./contract/app-contract.js";
16
+ export type { AppDefinition } from "./contract/app-definition.js";
17
+ /** The data service's answers, one namespace for each source (data-*.schema.json). */
18
+ export type * as calendar from "./contract/data-calendar.js";
19
+ export type * as news from "./contract/data-news.js";
20
+ export type * as quotes from "./contract/data-quotes.js";
21
+ export type * as series from "./contract/data-series.js";
22
+ export type * as weather from "./contract/data-weather.js";
23
+ export type * as tables from "./contract/data-snapshot.js";
24
+ export { cubicBezier, easingOf, staggerTimes } from "./easing.js";
25
+ /** A default's type, widened: a prop that defaults to "digital" may be any string. */
26
+ type Widen<T> = T extends string ? string : T extends number ? number : T extends boolean ? boolean : T;
27
+ type Params = Record<string, string | number | boolean>;
28
+ /** An app's context, however the host gave it; none outside a host. */
29
+ export declare function contextOf(app: WyndlassApp): AppContext | undefined;
30
+ /** The data service's answer for a source the definition declares (data-answer.schema.json); refused outside a host. */
31
+ export declare function data<T = unknown>(app: WyndlassApp, source: string, params: Params): Promise<DataAnswer<T>>;
32
+ /** The answer the screen last kept for the same request, however old, at once (WP-0111); none before the first, or here. */
33
+ export declare function kept<T = unknown>(app: WyndlassApp, source: string, params: Params): DataAnswer<T> | undefined;
34
+ /** An image a data answer names, as a blob URL the app can draw; refused outside a host. */
35
+ export declare function image(app: WyndlassApp, path: string): Promise<string>;
36
+ /** A data table's snapshot as the manifest carries it, the bound table's with only the rows the binding chooses; null without one. */
37
+ export declare function snapshot(app: WyndlassApp, table: string): unknown | null;
38
+ /** The app's view of the player's controller (ADR 0050), on screens, as its definition's `capabilities.player` allows. */
39
+ export declare function controllerOf(app: WyndlassApp): AppController | undefined;
40
+ /** The people at the screen (WP-0105), where the definition declares the impressions sensor and the site measures. */
41
+ export declare function impressionsOf(app: WyndlassApp): ImpressionsFeed | undefined;
42
+ export declare abstract class WyndlassApp extends HTMLElement {
43
+ #private;
44
+ /** The context as a host older than Player API 2 sets it. Declared only, so nothing is left on a newer host's element. */
45
+ wyndlass?: AppContext;
46
+ protected root: ShadowRoot;
47
+ /** Player API 2: the host hands the context over once, before the element joins the document; a second is ignored. */
48
+ [ATTACH](context: AppContext): void;
49
+ connectedCallback(): void;
50
+ disconnectedCallback(): void;
51
+ /** A prop, or its default when it is absent or empty. */
52
+ protected prop<T>(name: string, fallback: T): Widen<T>;
53
+ /** The font prop, a typography value (layout.schema.json#/$defs/typography, WP-0068 part e), as CSS for the app's box. */
54
+ protected font(): string;
55
+ /**
56
+ * Styles for the app's shadow root, as a constructable stylesheet (adoptedStyleSheets), not a <style>: a page whose
57
+ * style policy uses nonces still draws it (WP-0124 decision 9). Each call adds a sheet after the others. Call it from
58
+ * mount(), once the root is there.
59
+ */
60
+ protected css(text: string): CSSStyleSheet;
61
+ /** The layout's clock: now, or the moment it is held at when seeked or paused. */
62
+ protected now(): number;
63
+ /** When the layout started, on the same clock. */
64
+ protected startedAt(): number;
65
+ protected get timeZone(): string;
66
+ protected get locale(): string;
67
+ /** Says the app may be shown; once. Within 5 s of its start, or the screen reports it late. */
68
+ protected ready(): void;
69
+ /** Builds the app's shadow DOM, once. */
70
+ protected abstract mount(): void;
71
+ /** Draws the moment `epochMs` on the layout's clock. */
72
+ protected abstract draw(epochMs: number): void;
73
+ update(epochMs: number): void;
74
+ enter(o: HookOptions): void;
75
+ exit(o: HookOptions): void;
76
+ /** Forgets the enters and exits given so far (the designer gives them again when the clips change). */
77
+ reset(): void;
78
+ /** A paged app's items, for a staggered enter or exit (ADR 0071, decision 4); none animates the app as one box. */
79
+ protected staggered(): HTMLElement[];
80
+ }
81
+ /** A size that fits the app's box: the smaller of a share of its height and a share of its width (--h and --w). */
82
+ export declare function fit(ofHeight: number, ofWidth: number): string;
83
+ /** A typography value as CSS: its face, decoration, letter case, spacing, line height, shadow, and its scale as --s. */
84
+ export declare function typographyCss(value: unknown): string;
85
+ /** The colour props as CSS, with a fallback. */
86
+ export declare function color(v: unknown, fallback: string): string;
87
+ /**
88
+ * Defines an element once, under its fixed tag, and returns the tag (the bundle's `element` export): the old shape, for
89
+ * players older than Player API 2, which only Wyndlass's own apps need. A newer host defines the default export under a
90
+ * tag of its own instead.
91
+ */
92
+ export declare function define(tag: string, element: CustomElementConstructor): string;
93
+ /** Text safe to put in markup. */
94
+ export declare function escape(s: string): string;
package/dist/index.js ADDED
@@ -0,0 +1,218 @@
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
+ };
6
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
7
+ if (kind === "m") throw new TypeError("Private method is not writable");
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
10
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
11
+ };
12
+ var _WyndlassApp_instances, _WyndlassApp_said, _WyndlassApp_hooks, _WyndlassApp_mounted, _WyndlassApp_observer, _WyndlassApp_lastItems, _WyndlassApp_sized, _WyndlassApp_present;
13
+ /**
14
+ * The app runtime (WP-0101, ADR 0101): what every app on Wyndlass's screens shares, Wyndlass's own and an
15
+ * organization's. Its context from the host, its props with their defaults, drawing from the layout's clock (so seeking
16
+ * and pausing draw the right moment), saying it is ready once, and its enter and exit presets on that clock.
17
+ *
18
+ * Both shapes of the app contract (WP-0100, ADR 0100): a host of Player API 2 defines the bundle's default export under
19
+ * a tag of its own and hands it the context once, through ATTACH, leaving nothing on the element; an older one reads the
20
+ * tag the bundle defined (`define`) and sets the context as `element.wyndlass`.
21
+ *
22
+ * Outside a host (a unit test, the SDK's preview before it gives one) an app still runs: the time is now, each prop is
23
+ * its default, data and images are refused as unavailable, and ready() does nothing.
24
+ */
25
+ import { ATTACH } from "./contract/app-contract.js";
26
+ import { cubicBezier, easingOf, staggerTimes } from "./easing.js";
27
+ export { ATTACH };
28
+ export { cubicBezier, easingOf, staggerTimes } from "./easing.js";
29
+ /** The contexts handed over through ATTACH, where no other script on the page can reach them. */
30
+ const given = new WeakMap();
31
+ // What acts or reads for an app is a function of this module, never a method or getter of the element: another app on
32
+ // the page can reach the element, but not this module's scope, so it cannot use the app's context through it.
33
+ /** An app's context, however the host gave it; none outside a host. */
34
+ export function contextOf(app) { return given.get(app) ?? app.wyndlass; }
35
+ const unavailable = () => new Error("unavailable: no host gives this app data here");
36
+ /** The data service's answer for a source the definition declares (data-answer.schema.json); refused outside a host. */
37
+ export function data(app, source, params) {
38
+ const c = contextOf(app);
39
+ return c ? c.data(source, params) : Promise.reject(unavailable());
40
+ }
41
+ /** The answer the screen last kept for the same request, however old, at once (WP-0111); none before the first, or here. */
42
+ export function kept(app, source, params) {
43
+ return contextOf(app)?.kept?.(source, params);
44
+ }
45
+ /** An image a data answer names, as a blob URL the app can draw; refused outside a host. */
46
+ export function image(app, path) {
47
+ const c = contextOf(app);
48
+ return c ? c.image(path) : Promise.reject(unavailable());
49
+ }
50
+ /** A data table's snapshot as the manifest carries it, the bound table's with only the rows the binding chooses; null without one. */
51
+ export function snapshot(app, table) {
52
+ return contextOf(app)?.snapshot(table) ?? null;
53
+ }
54
+ /** The app's view of the player's controller (ADR 0050), on screens, as its definition's `capabilities.player` allows. */
55
+ export function controllerOf(app) { return contextOf(app)?.controller; }
56
+ /** The people at the screen (WP-0105), where the definition declares the impressions sensor and the site measures. */
57
+ export function impressionsOf(app) { return contextOf(app)?.impressions; }
58
+ export class WyndlassApp extends HTMLElement {
59
+ constructor() {
60
+ super(...arguments);
61
+ _WyndlassApp_instances.add(this);
62
+ _WyndlassApp_said.set(this, false);
63
+ _WyndlassApp_hooks.set(this, []);
64
+ _WyndlassApp_mounted.set(this, false);
65
+ _WyndlassApp_observer.set(this, void 0);
66
+ _WyndlassApp_lastItems.set(this, []);
67
+ }
68
+ /** Player API 2: the host hands the context over once, before the element joins the document; a second is ignored. */
69
+ [(_WyndlassApp_said = new WeakMap(), _WyndlassApp_hooks = new WeakMap(), _WyndlassApp_mounted = new WeakMap(), _WyndlassApp_observer = new WeakMap(), _WyndlassApp_lastItems = new WeakMap(), _WyndlassApp_instances = new WeakSet(), ATTACH)](context) { if (!given.has(this))
70
+ given.set(this, context); }
71
+ connectedCallback() {
72
+ // Its box's size as --w and --h, for text sized to it: container queries need Chromium 105, and the floor is 94 (ADR 0051).
73
+ __classPrivateFieldGet(this, _WyndlassApp_instances, "m", _WyndlassApp_sized).call(this);
74
+ if (typeof ResizeObserver === "function" && !__classPrivateFieldGet(this, _WyndlassApp_observer, "f")) {
75
+ __classPrivateFieldSet(this, _WyndlassApp_observer, new ResizeObserver(() => __classPrivateFieldGet(this, _WyndlassApp_instances, "m", _WyndlassApp_sized).call(this)), "f");
76
+ __classPrivateFieldGet(this, _WyndlassApp_observer, "f").observe(this);
77
+ }
78
+ if (__classPrivateFieldGet(this, _WyndlassApp_mounted, "f"))
79
+ return;
80
+ __classPrivateFieldSet(this, _WyndlassApp_mounted, true, "f");
81
+ this.root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
82
+ // Every app's background: its own prop, transparent by default, so the layout shows through.
83
+ this.style.background = color(this.prop("background", "#00000000"), "#00000000");
84
+ this.mount();
85
+ this.update(this.now());
86
+ }
87
+ disconnectedCallback() { __classPrivateFieldGet(this, _WyndlassApp_observer, "f")?.disconnect(); __classPrivateFieldSet(this, _WyndlassApp_observer, undefined, "f"); }
88
+ /** A prop, or its default when it is absent or empty. */
89
+ prop(name, fallback) {
90
+ const v = contextOf(this)?.props?.[name];
91
+ return (v === undefined || v === null || v === "" ? fallback : v);
92
+ }
93
+ /** The font prop, a typography value (layout.schema.json#/$defs/typography, WP-0068 part e), as CSS for the app's box. */
94
+ font() {
95
+ return typographyCss(this.prop("font", null));
96
+ }
97
+ /**
98
+ * Styles for the app's shadow root, as a constructable stylesheet (adoptedStyleSheets), not a <style>: a page whose
99
+ * style policy uses nonces still draws it (WP-0124 decision 9). Each call adds a sheet after the others. Call it from
100
+ * mount(), once the root is there.
101
+ */
102
+ css(text) {
103
+ const sheet = new CSSStyleSheet();
104
+ sheet.replaceSync(text);
105
+ this.root.adoptedStyleSheets = [...this.root.adoptedStyleSheets, sheet];
106
+ return sheet;
107
+ }
108
+ /** The layout's clock: now, or the moment it is held at when seeked or paused. */
109
+ now() { return contextOf(this)?.now() ?? Date.now(); }
110
+ /** When the layout started, on the same clock. */
111
+ startedAt() { return contextOf(this)?.startedAt() ?? this.now(); }
112
+ get timeZone() { return contextOf(this)?.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone; }
113
+ get locale() { return contextOf(this)?.locale ?? "en-US"; }
114
+ /** Says the app may be shown; once. Within 5 s of its start, or the screen reports it late. */
115
+ ready() {
116
+ if (__classPrivateFieldGet(this, _WyndlassApp_said, "f"))
117
+ return;
118
+ __classPrivateFieldSet(this, _WyndlassApp_said, true, "f");
119
+ contextOf(this)?.ready();
120
+ }
121
+ update(epochMs) {
122
+ if (!__classPrivateFieldGet(this, _WyndlassApp_mounted, "f"))
123
+ return;
124
+ this.draw(epochMs);
125
+ __classPrivateFieldGet(this, _WyndlassApp_instances, "m", _WyndlassApp_present).call(this, epochMs);
126
+ }
127
+ // Every call is kept (ADR 0071): a paged app is given an exit and an enter at each page turn, all at the start.
128
+ enter(o) { __classPrivateFieldSet(this, _WyndlassApp_hooks, [...__classPrivateFieldGet(this, _WyndlassApp_hooks, "f"), { ...o, kind: "enter" }].sort((a, b) => a.startAt - b.startAt), "f"); this.update(this.now()); }
129
+ exit(o) { __classPrivateFieldSet(this, _WyndlassApp_hooks, [...__classPrivateFieldGet(this, _WyndlassApp_hooks, "f"), { ...o, kind: "exit" }].sort((a, b) => a.startAt - b.startAt), "f"); this.update(this.now()); }
130
+ /** Forgets the enters and exits given so far (the designer gives them again when the clips change). */
131
+ reset() { __classPrivateFieldSet(this, _WyndlassApp_hooks, [], "f"); this.update(this.now()); }
132
+ /** A paged app's items, for a staggered enter or exit (ADR 0071, decision 4); none animates the app as one box. */
133
+ staggered() { return []; }
134
+ }
135
+ _WyndlassApp_sized = function _WyndlassApp_sized() {
136
+ this.style.setProperty("--w", `${this.clientWidth || 1}px`);
137
+ this.style.setProperty("--h", `${this.clientHeight || 1}px`);
138
+ }, _WyndlassApp_present = function _WyndlassApp_present(epochMs) {
139
+ const started = __classPrivateFieldGet(this, _WyndlassApp_hooks, "f").filter(h => h.startAt <= epochMs);
140
+ const h = started[started.length - 1];
141
+ const items = h?.staggerMs ? this.staggered() : [];
142
+ for (const item of __classPrivateFieldGet(this, _WyndlassApp_lastItems, "f"))
143
+ if (!items.includes(item))
144
+ Object.assign(item.style, { opacity: "", transform: "" });
145
+ __classPrivateFieldSet(this, _WyndlassApp_lastItems, items, "f");
146
+ if (!h) {
147
+ this.style.opacity = __classPrivateFieldGet(this, _WyndlassApp_hooks, "f").some(x => x.kind === "enter") ? "0" : "";
148
+ this.style.transform = "";
149
+ return;
150
+ }
151
+ if (items.length) {
152
+ Object.assign(this.style, { opacity: "", transform: "" });
153
+ staggerTimes(h.startAt, h.durationMs, h.staggerMs, items.length).forEach((w, i) => Object.assign(items[i].style, look(h, w.startMs, w.endMs - w.startMs, epochMs)));
154
+ return;
155
+ }
156
+ Object.assign(this.style, look(h, h.startAt, h.durationMs, epochMs));
157
+ };
158
+ /** One preset's look at a moment: from its start, over its length, by its easing (the layout schema's names). */
159
+ function look(h, startAt, durationMs, epochMs) {
160
+ const raw = durationMs > 0 ? (epochMs - startAt) / durationMs : epochMs >= startAt ? 1 : 0;
161
+ const t = Math.min(1, Math.max(0, h.reducedMotion ? (raw >= 1 ? 1 : 0) : raw));
162
+ const eased = cubicBezier(easingOf(CAMEL[h.easing ?? ""] ?? h.easing ?? "ease-out"), t);
163
+ const shown = h.kind === "enter" ? eased : 1 - eased;
164
+ const transform = h.preset === "scale" ? `scale(${0.8 + 0.2 * shown})` : h.preset === "slide-up" ? `translateY(${(1 - shown) * 20}%)`
165
+ : h.preset === "slide-left" ? `translateX(${(1 - shown) * 20}%)` : "";
166
+ return { opacity: shown >= 1 ? "" : String(shown), transform };
167
+ }
168
+ /** The names Wyndlass's apps' release 1.2.0 read, for layouts saved with them. */
169
+ const CAMEL = { easeIn: "ease-in", easeOut: "ease-out", easeInOut: "ease-in-out" };
170
+ /** A size that fits the app's box: the smaller of a share of its height and a share of its width (--h and --w). */
171
+ export function fit(ofHeight, ofWidth) {
172
+ // Times the font's scale (--s, from the font prop): the size the person asked for, relative to the one the app chose.
173
+ return `calc(min(calc(var(--h) * ${ofHeight}), calc(var(--w) * ${ofWidth})) * var(--s, 1))`;
174
+ }
175
+ /** A typography value as CSS: its face, decoration, letter case, spacing, line height, shadow, and its scale as --s. */
176
+ export function typographyCss(value) {
177
+ const f = value && typeof value === "object" ? value : {};
178
+ const num = (v, lo, hi) => typeof v === "number" && Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : undefined;
179
+ const family = typeof f.family === "string" ? f.family.replace(/["\\;{}<>]/g, "") : "";
180
+ const out = [family ? `font-family:"${family}",system-ui,sans-serif` : "font-family:system-ui,sans-serif"];
181
+ out.push(`font-weight:${num(f.weight, 100, 900) ?? 400}`, `font-style:${f.style === "italic" ? "italic" : "normal"}`);
182
+ const lines = Array.isArray(f.decoration) ? f.decoration.filter(d => d === "underline" || d === "line-through" || d === "overline") : [];
183
+ if (lines.length)
184
+ out.push(`text-decoration-line:${lines.join(" ")}`);
185
+ if (f.transform === "uppercase" || f.transform === "lowercase" || f.transform === "capitalize")
186
+ out.push(`text-transform:${f.transform}`);
187
+ const spacing = num(f.letterSpacing, -0.5, 2);
188
+ if (spacing)
189
+ out.push(`letter-spacing:${spacing}em`);
190
+ const height = num(f.lineHeight, 0.5, 4);
191
+ if (height)
192
+ out.push(`line-height:${height}`);
193
+ const shadow = f.shadow && typeof f.shadow === "object" ? f.shadow : null;
194
+ if (shadow) {
195
+ const c = shadow.color?.value;
196
+ out.push(`text-shadow:${num(shadow.x, -200, 200) ?? 0}px ${num(shadow.y, -200, 200) ?? 2}px ${num(shadow.blur, 0, 200) ?? 4}px ${color(c, "#000000")}`);
197
+ }
198
+ out.push(`--s:${num(f.scale, 0.25, 4) ?? 1}`);
199
+ return out.join(";") + ";";
200
+ }
201
+ /** The colour props as CSS, with a fallback. */
202
+ export function color(v, fallback) {
203
+ return typeof v === "string" && /^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(v) ? v : fallback;
204
+ }
205
+ /**
206
+ * Defines an element once, under its fixed tag, and returns the tag (the bundle's `element` export): the old shape, for
207
+ * players older than Player API 2, which only Wyndlass's own apps need. A newer host defines the default export under a
208
+ * tag of its own instead.
209
+ */
210
+ export function define(tag, element) {
211
+ if (!customElements.get(tag))
212
+ customElements.define(tag, element);
213
+ return tag;
214
+ }
215
+ /** Text safe to put in markup. */
216
+ export function escape(s) {
217
+ return s.replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" })[c]);
218
+ }