@wyndlass/app-sdk 0.0.0-stage → 1.0.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 (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +196 -2
  3. package/cli/app-package.mjs +111 -0
  4. package/cli/commands.mjs +114 -0
  5. package/cli/main.mjs +66 -0
  6. package/cli/package.mjs +119 -0
  7. package/cli/preview.mjs +142 -0
  8. package/cli/zip.mjs +63 -0
  9. package/dist/contract/app-contract.d.ts +221 -0
  10. package/dist/contract/app-contract.js +6 -0
  11. package/dist/contract/app-definition.d.ts +155 -0
  12. package/dist/contract/app-definition.js +3 -0
  13. package/dist/contract/data-answer.d.ts +40 -0
  14. package/dist/contract/data-answer.js +3 -0
  15. package/dist/contract/data-calendar.d.ts +57 -0
  16. package/dist/contract/data-calendar.js +3 -0
  17. package/dist/contract/data-news.d.ts +29 -0
  18. package/dist/contract/data-news.js +3 -0
  19. package/dist/contract/data-quotes.d.ts +75 -0
  20. package/dist/contract/data-quotes.js +3 -0
  21. package/dist/contract/data-series.d.ts +51 -0
  22. package/dist/contract/data-series.js +3 -0
  23. package/dist/contract/data-snapshot.d.ts +30 -0
  24. package/dist/contract/data-snapshot.js +3 -0
  25. package/dist/contract/data-weather.d.ts +421 -0
  26. package/dist/contract/data-weather.js +3 -0
  27. package/dist/contract/player-api-event.d.ts +253 -0
  28. package/dist/contract/player-api-event.js +3 -0
  29. package/dist/contract/player-api-status.d.ts +178 -0
  30. package/dist/contract/player-api-status.js +3 -0
  31. package/dist/easing.d.ts +16 -0
  32. package/dist/easing.js +71 -0
  33. package/dist/index.d.ts +94 -0
  34. package/dist/index.js +218 -0
  35. package/dist/preview/frame/frame.js +2 -0
  36. package/dist/preview/frame/index.html +12 -0
  37. package/dist/preview/index.html +40 -0
  38. package/dist/preview/page.js +7 -0
  39. package/dist/qr.d.ts +5 -0
  40. package/dist/qr.js +14 -0
  41. package/dist/react.d.ts +52 -0
  42. package/dist/react.js +149 -0
  43. package/package.json +83 -3
  44. package/protocol/app-definition.schema.json +435 -0
  45. package/protocol/app-packages.json +172 -0
  46. package/protocol/common.schema.json +197 -0
  47. package/protocol/samples/calendar.json +63 -0
  48. package/protocol/samples/events/app.command.json +14 -0
  49. package/protocol/samples/events/item.end.json +13 -0
  50. package/protocol/samples/events/item.start.json +18 -0
  51. package/protocol/samples/events/state.changed.json +11 -0
  52. package/protocol/samples/news.json +37 -0
  53. package/protocol/samples/player-status.json +58 -0
  54. package/protocol/samples/weather.json +59 -0
  55. package/templates/react/AGENTS.md +13 -0
  56. package/templates/react/README.md +22 -0
  57. package/templates/react/app.json +14 -0
  58. package/templates/react/gitignore +2 -0
  59. package/templates/react/package.json +22 -0
  60. package/templates/react/src/app.css +7 -0
  61. package/templates/react/src/app.tsx +39 -0
  62. package/templates/react/src/env.d.ts +5 -0
  63. package/templates/react/tsconfig.json +13 -0
  64. package/templates/vanilla/AGENTS.md +13 -0
  65. package/templates/vanilla/README.md +22 -0
  66. package/templates/vanilla/app.json +14 -0
  67. package/templates/vanilla/gitignore +2 -0
  68. package/templates/vanilla/package.json +18 -0
  69. package/templates/vanilla/src/app.css +7 -0
  70. package/templates/vanilla/src/app.ts +59 -0
  71. package/templates/vanilla/src/env.d.ts +5 -0
  72. package/templates/vanilla/tsconfig.json +12 -0
@@ -0,0 +1,142 @@
1
+ // preview (WP-0101 decision 7): the app run as screens run it, on 127.0.0.1 only. The page is player-core's host, compiled
2
+ // into the package when the SDK is built (dist/preview/); this server gives it the project's module, rebuilt with the
3
+ // SDK's own esbuild whenever a source or app.json changes, the definition, and sample answers for the data the app
4
+ // declares: the protocol's fixtures, or a folder of the developer's own. The page reloads the app on each rebuild.
5
+ // With --frame (part c2), the app runs in WP-0100's frame on an origin of its own, as in the CMS: a second server, on
6
+ // 127.0.0.1 too, serves the frame's page and runtime under the frames' own policy.
7
+ import { existsSync, readdirSync, readFileSync, watch } from "node:fs";
8
+ import { createServer } from "node:http";
9
+ import { dirname, join, resolve } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { context } from "esbuild";
12
+ import { CliError, definitionOf, entryOf } from "./package.mjs";
13
+
14
+ const sdk = join(dirname(fileURLToPath(import.meta.url)), "..");
15
+ const page = join(sdk, "dist", "preview");
16
+ const SOURCES = ["weather", "news", "calendar"];
17
+ const TYPES = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8" };
18
+
19
+ /** A sample answer's data for a source: the folder's <source>.json (its data, or a whole answer), else the protocol's. */
20
+ function sample(dataDir, source) {
21
+ const own = dataDir && join(dataDir, `${source}.json`);
22
+ const file = own && existsSync(own) ? own : join(sdk, "protocol", "samples", `${source}.json`);
23
+ if (!existsSync(file)) return null;
24
+ const read = JSON.parse(readFileSync(file, "utf8"));
25
+ return read && typeof read === "object" && "source" in read && "data" in read ? read.data : read;
26
+ }
27
+
28
+ /** The tables in the data folder's tables/: each .json or .csv file's text, by its name; the page reads them. */
29
+ function tables(dataDir) {
30
+ const dir = dataDir && join(dataDir, "tables");
31
+ if (!dir || !existsSync(dir)) return {};
32
+ return Object.fromEntries(readdirSync(dir).filter(n => /.(json|csv)$/i.test(n)).map(n => [n, readFileSync(join(dir, n), "utf8")]));
33
+ }
34
+
35
+ /** The player's status and the events the controller can be sent, as the Player API's fixtures give them. */
36
+ function controllerSamples() {
37
+ const samples = join(sdk, "protocol", "samples");
38
+ const events = join(samples, "events");
39
+ return {
40
+ status: JSON.parse(readFileSync(join(samples, "player-status.json"), "utf8")),
41
+ events: existsSync(events) ? Object.fromEntries(readdirSync(events).map(n => [n.replace(/.json$/, ""), JSON.parse(readFileSync(join(events, n), "utf8"))])) : {},
42
+ };
43
+ }
44
+
45
+ /** The frames' own policy (WP-0124's frames row), with the preview's page the one that may frame them. */
46
+ const framePolicy = (frames, page) => [
47
+ "default-src 'none'", `script-src ${frames} blob:`, "style-src 'unsafe-inline'", "img-src blob: data:", "font-src blob: data:", "media-src blob:",
48
+ "connect-src https:", `frame-ancestors ${page}`, "base-uri 'none'", "form-action 'none'",
49
+ ].join("; ");
50
+
51
+ /** A small picture for any image a data answer names: the preview fetches nothing from the network. */
52
+ const PICTURE = Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="320" height="180" viewBox="0 0 320 180"><rect width="320" height="180" fill="#5a6b7d"/><path d="M0 180 L110 80 L180 140 L230 100 L320 180Z" fill="#8fa3b8"/><circle cx="250" cy="50" r="22" fill="#d9e2ec"/></svg>`);
53
+
54
+ /**
55
+ * Serves the preview of the project at `dir` on 127.0.0.1:`port` (0: any free port), and with `frame`, the frames on
56
+ * the next port (any free one where `port` is 0). Resolves once it listens, with its address and a way to stop it.
57
+ */
58
+ export async function preview(dir = ".", { port = 5199, data: dataDir, frame = false } = {}) {
59
+ const root = resolve(dir);
60
+ definitionOf(root);
61
+ const entry = entryOf(root);
62
+ if (!existsSync(join(page, "index.html"))) throw new CliError("this copy of @wyndlass/app-sdk has no preview page (dist/preview): build the SDK first");
63
+ let frames = null, framesOrigin = "", pageOrigin = "";
64
+ const clients = new Set();
65
+ const tell = () => { for (const c of clients) c.write("event: reload\ndata: {}\n\n"); };
66
+ let built = { code: "", error: "the module has not been built yet" }, version = 0, first;
67
+ const firstBuilt = new Promise(r => { first = r; });
68
+ const builder = await context({
69
+ entryPoints: [entry], absWorkingDir: root, bundle: true, format: "esm", target: "es2020", platform: "browser", write: false, sourcemap: "inline",
70
+ jsx: "automatic", logLevel: "silent", define: { "process.env.NODE_ENV": "\"development\"" }, outfile: join(root, "dist", "preview.js"),
71
+ loader: { ".css": "text", ".png": "dataurl", ".jpg": "dataurl", ".jpeg": "dataurl", ".gif": "dataurl", ".webp": "dataurl", ".svg": "dataurl", ".woff2": "dataurl", ".woff": "dataurl" },
72
+ plugins: [{ name: "wyndlass-preview", setup(b) {
73
+ b.onEnd(r => {
74
+ built = r.errors.length ? { code: "", error: r.errors.map(e => `${e.location ? `${e.location.file}:${e.location.line}: ` : ""}${e.text}`).join("\n") }
75
+ : { code: r.outputFiles.find(f => f.path.endsWith(".js")).text, error: null };
76
+ version++;
77
+ tell();
78
+ first();
79
+ });
80
+ } }],
81
+ });
82
+ await builder.watch();
83
+ // The first build done before anyone is told where the preview is: a page loaded sooner would find no module.
84
+ await firstBuilt;
85
+ // The definition is not a source esbuild follows.
86
+ const definitionWatch = watch(join(root, "app.json"), () => { version++; tell(); });
87
+
88
+ const server = createServer((req, res) => {
89
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
90
+ const send = (status, type, body, more = {}) => res.writeHead(status, { "content-type": type, "cache-control": "no-store", ...more }).end(body);
91
+ switch (url.pathname) {
92
+ case "/": return send(200, TYPES[".html"], readFileSync(join(page, "index.html")));
93
+ case "/page.js": return send(200, TYPES[".js"], readFileSync(join(page, "page.js")));
94
+ // Its version with it, so a page that connects to /events after a change knows it loaded an older one.
95
+ case "/app.js": return built.error ? send(500, "text/plain; charset=utf-8", built.error, { "x-preview-version": String(version) })
96
+ : send(200, TYPES[".js"], built.code, { "x-preview-version": String(version) });
97
+ case "/app.json": return send(200, TYPES[".json"], readFileSync(join(root, "app.json")));
98
+ case "/samples.json": {
99
+ const declared = (definitionOf(root).read?.data ?? []).filter(s => SOURCES.includes(s));
100
+ return send(200, TYPES[".json"], JSON.stringify(Object.fromEntries(declared.map(s => [s, sample(dataDir && resolve(dataDir), s)]))));
101
+ }
102
+ case "/config.json": return send(200, TYPES[".json"], JSON.stringify({ frames: framesOrigin, tables: tables(dataDir && resolve(dataDir)), controller: controllerSamples() }));
103
+ case "/picture.svg": return send(200, "image/svg+xml", PICTURE);
104
+ case "/favicon.ico": return res.writeHead(204).end();
105
+ case "/events": {
106
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive" });
107
+ res.write(`event: hello\ndata: ${JSON.stringify({ version })}\n\n`);
108
+ clients.add(res);
109
+ req.on("close", () => clients.delete(res));
110
+ return;
111
+ }
112
+ default: return send(404, "text/plain; charset=utf-8", "not found");
113
+ }
114
+ });
115
+ await new Promise((ok, fail) => { server.once("error", fail); server.listen(port, "127.0.0.1", ok); })
116
+ .catch(e => { void builder.dispose(); definitionWatch.close(); throw new CliError(e.code === "EADDRINUSE" ? `port ${port} is taken: give another with --port` : e.message); });
117
+ const { address, port: bound } = server.address();
118
+ pageOrigin = `http://${address}:${bound}`;
119
+ if (frame) {
120
+ // The frame's page and runtime only, nothing else, under the frames' policy: the app's document there is opaque.
121
+ frames = createServer((req, res) => {
122
+ const path = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
123
+ const file = path === "/" ? "index.html" : path === "/frame.js" ? "frame.js" : null;
124
+ if (!file) return res.writeHead(404).end();
125
+ res.writeHead(200, { "content-type": TYPES[file.endsWith(".js") ? ".js" : ".html"], "cache-control": "no-store", "content-security-policy": framePolicy(framesOrigin, pageOrigin) })
126
+ .end(readFileSync(join(page, "frame", file)));
127
+ });
128
+ await new Promise((ok, fail) => { frames.once("error", fail); frames.listen(port === 0 ? 0 : port + 1, "127.0.0.1", ok); })
129
+ .catch(async e => { await builder.dispose(); definitionWatch.close(); server.close(); throw new CliError(e.code === "EADDRINUSE" ? `port ${port + 1}, for the frames, is taken: give another with --port` : e.message); });
130
+ framesOrigin = `http://127.0.0.1:${frames.address().port}`;
131
+ }
132
+ return {
133
+ url: `http://${address}:${bound}/`, address, port: bound, frames: framesOrigin || null,
134
+ async close() {
135
+ definitionWatch.close();
136
+ if (frames) await new Promise(r => frames.close(r));
137
+ for (const c of clients) c.end();
138
+ await builder.dispose();
139
+ await new Promise(r => server.close(r));
140
+ },
141
+ };
142
+ }
package/cli/zip.mjs ADDED
@@ -0,0 +1,63 @@
1
+ // The package's zip (WP-0101 decision 8): the same entries give the same bytes, so importing a version again is a no-op,
2
+ // not a refusal (WP-0100 decision 4). The entries in the order given, each deflated at level 9, dated 1 January 1980
3
+ // (the DOS epoch), named in UTF-8, with no extra fields or comments.
4
+ import { deflateRawSync } from "node:zlib";
5
+
6
+ const TABLE = Array.from({ length: 256 }, (_, n) => {
7
+ let c = n;
8
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
9
+ return c >>> 0;
10
+ });
11
+
12
+ /** CRC-32 (ISO 3309), as zip records it; node:zlib's own is not in every Node 20. */
13
+ export function crc32(bytes) {
14
+ let c = 0xffffffff;
15
+ for (const b of bytes) c = TABLE[(c ^ b) & 0xff] ^ (c >>> 8);
16
+ return (c ^ 0xffffffff) >>> 0;
17
+ }
18
+
19
+ const DOS_TIME = 0, DOS_DATE = (0 << 9) | (1 << 5) | 1;
20
+
21
+ /** A zip of `entries`, each [name, bytes], in that order. */
22
+ export function zip(entries) {
23
+ const locals = [], central = [];
24
+ let offset = 0;
25
+ for (const [entryName, data] of entries) {
26
+ const raw = Buffer.from(data), body = deflateRawSync(raw, { level: 9 }), name = Buffer.from(entryName, "utf8"), crc = crc32(raw);
27
+ const local = Buffer.alloc(30);
28
+ local.writeUInt32LE(0x04034b50, 0);
29
+ local.writeUInt16LE(20, 4);
30
+ local.writeUInt16LE(0x0800, 6);
31
+ local.writeUInt16LE(8, 8);
32
+ local.writeUInt16LE(DOS_TIME, 10);
33
+ local.writeUInt16LE(DOS_DATE, 12);
34
+ local.writeUInt32LE(crc, 14);
35
+ local.writeUInt32LE(body.length, 18);
36
+ local.writeUInt32LE(raw.length, 22);
37
+ local.writeUInt16LE(name.length, 26);
38
+ const head = Buffer.alloc(46);
39
+ head.writeUInt32LE(0x02014b50, 0);
40
+ head.writeUInt16LE(20, 4);
41
+ head.writeUInt16LE(20, 6);
42
+ head.writeUInt16LE(0x0800, 8);
43
+ head.writeUInt16LE(8, 10);
44
+ head.writeUInt16LE(DOS_TIME, 12);
45
+ head.writeUInt16LE(DOS_DATE, 14);
46
+ head.writeUInt32LE(crc, 16);
47
+ head.writeUInt32LE(body.length, 20);
48
+ head.writeUInt32LE(raw.length, 24);
49
+ head.writeUInt16LE(name.length, 28);
50
+ head.writeUInt32LE(offset, 42);
51
+ locals.push(local, name, body);
52
+ central.push(head, name);
53
+ offset += local.length + name.length + body.length;
54
+ }
55
+ const directory = Buffer.concat(central);
56
+ const end = Buffer.alloc(22);
57
+ end.writeUInt32LE(0x06054b50, 0);
58
+ end.writeUInt16LE(entries.length, 8);
59
+ end.writeUInt16LE(entries.length, 10);
60
+ end.writeUInt32LE(directory.length, 12);
61
+ end.writeUInt32LE(offset, 16);
62
+ return Buffer.concat([...locals, directory, end]);
63
+ }
@@ -0,0 +1,221 @@
1
+ import type { PlayerAPIEvent } from "./player-api-event.js";
2
+ import type { PlayerAPIStatus } from "./player-api-status.js";
3
+ /**
4
+ * How the host hands an element its context (Player API 2): `element[ATTACH](context)`, once, after it makes the element
5
+ * and before the element joins the document. The element keeps the context where nothing else can reach it, and ignores a
6
+ * second call.
7
+ */
8
+ export declare const ATTACH: unique symbol;
9
+ /**
10
+ * What an app's enter and exit hooks are given (ADR 0053, decision 8; ADR 0071): when on the player's clock (the clip's
11
+ * delay is in startAt, so delayMs is 0), how, and how long; the stagger between its items, and for a paged app the page it
12
+ * is for.
13
+ */
14
+ export interface HookOptions {
15
+ startAt: number;
16
+ preset: string;
17
+ durationMs: number;
18
+ easing?: string;
19
+ delayMs?: number;
20
+ staggerMs?: number;
21
+ page?: number;
22
+ reducedMotion: boolean;
23
+ }
24
+ /** The data service's answer (data-answer.schema.json), with whether it came from this screen's own store, and its age. */
25
+ export interface DataAnswer<T = unknown> {
26
+ source: string;
27
+ params: Record<string, unknown>;
28
+ fetchedAt: string;
29
+ expiresAt: string;
30
+ stale: boolean;
31
+ attribution?: {
32
+ text: string;
33
+ url?: string;
34
+ };
35
+ data: T;
36
+ /** True when the service could not be reached and this is the last answer the screen kept. */
37
+ offline: boolean;
38
+ /** How old the data is, from fetchedAt to now. */
39
+ ageMs: number;
40
+ }
41
+ /** The screen's facts (WP-0068 part g): its name, type, tags and node path, for what an app draws itself. */
42
+ export interface ScreenFacts {
43
+ name: string;
44
+ type?: string;
45
+ tags?: string[];
46
+ /** Its node and the nodes above it, from the organization down. */
47
+ path: {
48
+ name: string;
49
+ kind: string;
50
+ }[];
51
+ /** The workspace's own fields (WP-0068 part h): its value, else the nearest node's. */
52
+ fields?: Record<string, string>;
53
+ }
54
+ /** A rule a table's row must meet to be shown (WP-0137, ADR 0125): against a value, or the screen's own. */
55
+ export interface TableRule {
56
+ column: string;
57
+ op: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "contains";
58
+ value: string | number | boolean | null | {
59
+ screen: "name" | "location" | "site" | "fields";
60
+ key?: string;
61
+ };
62
+ }
63
+ /** An app's table binding (DES-DB-12): its table, each slot's column, and which rows it shows, in what order, how many. */
64
+ export interface TableBinding {
65
+ table: string;
66
+ fields?: Record<string, string>;
67
+ filter?: TableRule[];
68
+ sort?: {
69
+ column: string;
70
+ order: "asc" | "desc";
71
+ };
72
+ limit?: number;
73
+ }
74
+ /** An anonymous event at the screen (WP-0105): someone came into view, looked, or left after a while. */
75
+ export type ImpressionEvent = {
76
+ type: "enter";
77
+ source: string;
78
+ } | {
79
+ type: "look";
80
+ source: string;
81
+ } | {
82
+ type: "leave";
83
+ source: string;
84
+ dwellS: number;
85
+ attentionS?: number;
86
+ };
87
+ /** Each kind of sensor's people in view now, the longest any has stayed and, where faces are seen, looked. */
88
+ export interface ImpressionsNow {
89
+ source: string;
90
+ present: number;
91
+ longestDwellS: number;
92
+ longestAttentionS?: number;
93
+ }
94
+ /** What an app that declares the `impressions` sensor gets as context.impressions. */
95
+ export interface ImpressionsFeed {
96
+ now(): ImpressionsNow[];
97
+ /** Hears batches of events, at most four times a second; returns how to stop. */
98
+ on(listener: (events: ImpressionEvent[]) => void): () => void;
99
+ }
100
+ /** What an app may do with the player's controller: its definition's capabilities.player (ADR 0050, decision 3). */
101
+ export type AppCapability = "read" | "control";
102
+ /** In the designer or a preview, commands change nothing. */
103
+ export interface NotApplied {
104
+ applied: false;
105
+ reason: "preview";
106
+ }
107
+ /** The player's time (time.now). */
108
+ export interface TimeNow {
109
+ iso: string;
110
+ epochMs: number;
111
+ timeZone: string;
112
+ utcOffsetMinutes: number;
113
+ synced: boolean;
114
+ }
115
+ /**
116
+ * An app's own view of the player's controller (ADR 0050, decision 3): properties and events with capabilities.player
117
+ * "read" (the default), commands as well with "control". Its listeners go when the app does.
118
+ */
119
+ export interface AppController {
120
+ readonly app: string;
121
+ readonly instance: string;
122
+ readonly capability: AppCapability;
123
+ /** Everything the controller knows now (the same as player.status). */
124
+ status(): PlayerAPIStatus;
125
+ get<K extends keyof PlayerAPIStatus>(property: K): PlayerAPIStatus[K];
126
+ /** Listens for an event, or every event with "*"; returns how to stop. */
127
+ on(event: PlayerAPIEvent["type"] | "*", listener: (event: PlayerAPIEvent) => void): () => void;
128
+ time(): Promise<TimeNow>;
129
+ /** A command by its Player API name, e.g. call("player.pause"); throws when refused, with the Player API's code. */
130
+ call(command: string, args?: Record<string, unknown>): Promise<unknown | NotApplied>;
131
+ play(): Promise<unknown | NotApplied>;
132
+ pause(): Promise<unknown | NotApplied>;
133
+ stop(): Promise<unknown | NotApplied>;
134
+ next(): Promise<unknown | NotApplied>;
135
+ previous(): Promise<unknown | NotApplied>;
136
+ /** Ends this app's slot now: its item ends as finished and the loop moves on. */
137
+ finish(): Promise<unknown | NotApplied>;
138
+ /** Removes every listener; the host calls it when the app goes. */
139
+ dispose(): void;
140
+ }
141
+ /** What an app is given: through ATTACH (Player API 2), or as `element.wyndlass` by a player older than that. */
142
+ export interface AppContext {
143
+ /** The app's id (app-definition), or its item's id where the layout does not say. */
144
+ app: string;
145
+ /** The tag the host defined its element under. */
146
+ element: string;
147
+ props: Record<string, unknown>;
148
+ /** The screen's place (the manifest's), for "the screen's place" defaults. */
149
+ place?: {
150
+ lat: number;
151
+ lon: number;
152
+ };
153
+ /** The screen's facts (WP-0068 part g), for what an app draws itself. */
154
+ screen?: ScreenFacts | null;
155
+ timeZone: string;
156
+ locale: string;
157
+ /** True in the CMS's previews. */
158
+ preview: boolean;
159
+ /** The layout's clock: now, or the moment it is held at when seeked or paused. */
160
+ now(): number;
161
+ /** When the layout started, on the same clock. */
162
+ startedAt(): number;
163
+ /** Says the app is drawn and may be shown. One that has not by the deadline is reported late. */
164
+ ready(): void;
165
+ data<T = unknown>(source: string, params: Record<string, string | number | boolean>): Promise<DataAnswer<T>>;
166
+ /**
167
+ * The answer this screen last kept for a request, however old, at once; nothing before the first (WP-0111). An app draws
168
+ * it while `data` asks again, so it is ready without waiting for the network. Absent on players older than WP-0111 part
169
+ * b, so apps check for it.
170
+ */
171
+ kept?<T = unknown>(source: string, params: Record<string, string | number | boolean>): DataAnswer<T> | undefined;
172
+ /** An image a data answer names (a data service path), as a blob URL the app can draw. */
173
+ image(path: string): Promise<string>;
174
+ /**
175
+ * The layout's table binding for this app (DES-DB-12): its table, each slot's column, and which of its rows it shows,
176
+ * in what order and how many (WP-0137, ADR 0125).
177
+ */
178
+ binding?: TableBinding;
179
+ /**
180
+ * A data table's snapshot, as the manifest carries it; null without one. The bound table's comes with only the rows the
181
+ * binding chooses, chosen at each read on the layout's clock with the screen's facts (WP-0137).
182
+ */
183
+ snapshot(table: string): unknown | null;
184
+ /** The app's view of the player's controller, on screens (WP-0100); absent in the designer and previews. */
185
+ controller?: AppController;
186
+ /**
187
+ * The people at the screen (WP-0105), for an app whose definition declares `capabilities.sensors: ["impressions"]`,
188
+ * where the site measures them. Absent otherwise.
189
+ */
190
+ impressions?: ImpressionsFeed;
191
+ }
192
+ /**
193
+ * An app's element: an HTMLElement (a host types it as both), with the hooks the host calls, each optional, and how it
194
+ * takes its context. Written without the DOM's types, so the contract checks wherever it is read, a Worker included.
195
+ */
196
+ export interface AppElement {
197
+ /** Player API 2: takes the context, once. */
198
+ [ATTACH]?(context: AppContext): void;
199
+ /** The context as players older than Player API 2 give it, and Wyndlass's apps of the old shape read it. */
200
+ wyndlass?: AppContext;
201
+ /** Draws the app at a moment on the layout's clock; called each frame while it plays, and once when held. */
202
+ update?(epochMs: number): void;
203
+ enter?(o: HookOptions): Promise<void> | void;
204
+ exit?(o: HookOptions): Promise<void> | void;
205
+ /** A paged app's pages (ADR 0071, decision 5): how many, and how long each shows, from the data it started with. */
206
+ paging?(): {
207
+ count: number;
208
+ ms: number;
209
+ } | null;
210
+ /** Forgets every enter and exit given so far: the designer's canvas gives them again after the clips change (WP-0069). */
211
+ reset?(): void;
212
+ }
213
+ /**
214
+ * An app's module (ADR 0099 decision 2; Player API 2): its default export is its element's class, which the host defines
215
+ * under a tag of its choosing; its class extends HTMLElement. The old shape, for Wyndlass's own apps on older players
216
+ * only, defines its element itself and exports its tag as `element`; a module of Wyndlass's may have both.
217
+ */
218
+ export interface AppModule {
219
+ default?: new () => AppElement;
220
+ element?: string;
221
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * How the host hands an element its context (Player API 2): `element[ATTACH](context)`, once, after it makes the element
3
+ * and before the element joins the document. The element keeps the context where nothing else can reach it, and ignores a
4
+ * second call.
5
+ */
6
+ export const ATTACH = Symbol.for("wyndlass.app.attach");
@@ -0,0 +1,155 @@
1
+ export type CommonSemver = string;
2
+ /**
3
+ * BCP 47 language tag.
4
+ */
5
+ export type CommonLocale = string;
6
+ /**
7
+ * The YAML definition of a web component app (scoping doc 7.8). Drives the designer's property panel, the catalog and the MCP tools.
8
+ */
9
+ export interface AppDefinition {
10
+ /**
11
+ * Reverse-domain id, e.g. com.wyndlass.weather. com.wyndlass. is Wyndlass's own: an app an organization imports may not use it (WP-0100).
12
+ */
13
+ id: string;
14
+ version: CommonSemver;
15
+ name: string;
16
+ description?: string;
17
+ /**
18
+ * The element's name. From Player API 2 (WP-0100) the host defines the module's default export under this name, a hyphen and a suffix of its own, so it is the readable part of the tag. Wyndlass's apps also define it themselves, for players older than that.
19
+ */
20
+ element: string;
21
+ params?: {
22
+ [k: string]: Param | undefined;
23
+ };
24
+ properties?: {
25
+ resizable?: boolean;
26
+ minDuration?: string;
27
+ offline?: boolean;
28
+ transparent?: boolean;
29
+ aspectRatio?: string;
30
+ };
31
+ capabilities?: {
32
+ network?: string[];
33
+ /**
34
+ * Sensors the app reads on the screen. `impressions`: the live, anonymous feed of the people at the screen, where its site measures them (WP-0105): context.impressions.
35
+ */
36
+ sensors?: string[];
37
+ audio?: boolean;
38
+ peers?: boolean;
39
+ /**
40
+ * What the app may do with the player's controller (ADR 0050): read its properties and events (read, the default), or also command playback, audio and display (control). The CMS shows it when the app is added.
41
+ */
42
+ player?: "read" | "control";
43
+ };
44
+ bindings?: string[];
45
+ /**
46
+ * Named binding slots (ADR 0053, decision 8; DES-AP-01): each a field the app shows, mapped to a table column in a layout's binding. A layout's binding.fields names these.
47
+ */
48
+ slots?: {
49
+ [k: string]: {
50
+ label: string;
51
+ /**
52
+ * What the column must hold.
53
+ */
54
+ type: "text" | "number" | "currency" | "date" | "datetime" | "image" | "url" | "boolean";
55
+ required?: boolean;
56
+ help?: string;
57
+ } | undefined;
58
+ };
59
+ /**
60
+ * The presets the app's enter and exit hooks take (ADR 0053, decision 8; DES-AH-01 to 07). An app without them is animated as one box.
61
+ */
62
+ animation?: {
63
+ /**
64
+ * Presets its enter hook plays (fade, slide, typewriter...).
65
+ */
66
+ enter?: string[];
67
+ /**
68
+ * Presets its exit hook plays.
69
+ */
70
+ exit?: string[];
71
+ /**
72
+ * Its items can enter one after another (DES-AN-06).
73
+ */
74
+ stagger?: boolean;
75
+ /**
76
+ * It pages through its items, playing exit and enter at each page (DES-DB-13): its page count comes from the snapshot the layout started with.
77
+ */
78
+ pages?: boolean;
79
+ };
80
+ /**
81
+ * The data service's sources it reads (ADR 0069): weather, news.
82
+ */
83
+ data?: string[];
84
+ events?: {
85
+ emits?: string[];
86
+ consumes?: string[];
87
+ };
88
+ mobile?: {
89
+ mode?: "render" | "snapshot" | "hidden";
90
+ };
91
+ locales?: CommonLocale[];
92
+ }
93
+ export interface Param {
94
+ /**
95
+ * A param's kind. A font param's value is a typography object (layout.schema.json#/$defs/typography): a family with its weight, style, decoration, letter case and spacing, line height, shadow and scale.
96
+ */
97
+ type: "string" | "text" | "number" | "boolean" | "enum" | "color" | "font" | "media" | "location" | "datetime" | "duration" | "url" | "datatable" | "column" | "secret" | "json";
98
+ label?: string;
99
+ default?: unknown;
100
+ /**
101
+ * On an enum, its choices. On any other type, suggestions the designer offers, while any valid value is still allowed (WP-0098). Each is a plain value, or labeled: shown by its label, under its group.
102
+ *
103
+ * @minItems 1
104
+ */
105
+ values?: [ParamValue, ...ParamValue[]];
106
+ group?: string;
107
+ required?: boolean;
108
+ min?: number;
109
+ max?: number;
110
+ step?: number;
111
+ help?: string;
112
+ advanced?: boolean;
113
+ /**
114
+ * When the param applies (WP-0068 part f, after gadget.yaml's depends): each key names another param of this app, and its test must hold. A param that does not apply is not shown in the designer and is not required; its value is kept.
115
+ */
116
+ when?: {
117
+ [k: string]: ParamTest | undefined;
118
+ };
119
+ }
120
+ /**
121
+ * A value as a param's values list it (WP-0098): a string, number or boolean, or labeled ({ value, label, group }) for the designer to show by its label, under its group.
122
+ */
123
+ export interface ParamValue {
124
+ [k: string]: unknown | undefined;
125
+ }
126
+ /**
127
+ * A test of another param's value (WP-0068 part f): every one given must hold. Values compare as text.
128
+ */
129
+ export interface ParamTest {
130
+ /**
131
+ * Its value is one of these.
132
+ *
133
+ * @minItems 1
134
+ * @maxItems 50
135
+ */
136
+ anyOf?: [string | number | boolean, ...(string | number | boolean)[]];
137
+ /**
138
+ * Its value, a list, holds every one of these.
139
+ *
140
+ * @minItems 1
141
+ * @maxItems 50
142
+ */
143
+ allOf?: [string | number | boolean, ...(string | number | boolean)[]];
144
+ /**
145
+ * Its value is none of these.
146
+ *
147
+ * @minItems 1
148
+ * @maxItems 50
149
+ */
150
+ noneOf?: [string | number | boolean, ...(string | number | boolean)[]];
151
+ /**
152
+ * true: it has a value; false: it has none (absent, empty text, or an empty list).
153
+ */
154
+ set?: boolean;
155
+ }
@@ -0,0 +1,3 @@
1
+ // Copied from protocol/generated/ts/app-definition.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,40 @@
1
+ /**
2
+ * What the data service answers for any source (ADR 0069): GET /v0/{source}. data is the source's normalized form (data-weather, data-news, data-quotes, data-series, data-calendar). Players keep the last answer and show it offline with its age.
3
+ */
4
+ export interface DataServiceAnswer {
5
+ /**
6
+ * The source asked: weather, news, quotes, series, calendar.
7
+ */
8
+ source: string;
9
+ /**
10
+ * The parameters the answer is for, as the service normalized them (a place rounded to 0.1°, a feed's URL). A calendar's address is left out: it often works as a password (WP-0111).
11
+ */
12
+ params: {
13
+ [k: string]: (string | number | boolean) | undefined;
14
+ };
15
+ /**
16
+ * When the provider answered.
17
+ */
18
+ fetchedAt: string;
19
+ /**
20
+ * When to ask again.
21
+ */
22
+ expiresAt: string;
23
+ /**
24
+ * True when the provider failed and this is the last answer it gave, kept for up to a day past expiresAt.
25
+ */
26
+ stale: boolean;
27
+ /**
28
+ * Credit the source requires wherever its data is shown (Open-Meteo: CC BY 4.0).
29
+ */
30
+ attribution?: {
31
+ text: string;
32
+ url?: string;
33
+ };
34
+ /**
35
+ * The source's normalized data.
36
+ */
37
+ data: {
38
+ [k: string]: unknown | undefined;
39
+ };
40
+ }
@@ -0,0 +1,3 @@
1
+ // Copied from protocol/generated/ts/data-answer.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 {};