@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.
- package/LICENSE +21 -0
- package/README.md +196 -2
- package/cli/app-package.mjs +111 -0
- package/cli/commands.mjs +114 -0
- package/cli/main.mjs +66 -0
- package/cli/package.mjs +119 -0
- package/cli/preview.mjs +142 -0
- package/cli/zip.mjs +63 -0
- package/dist/contract/app-contract.d.ts +221 -0
- package/dist/contract/app-contract.js +6 -0
- package/dist/contract/app-definition.d.ts +155 -0
- package/dist/contract/app-definition.js +3 -0
- package/dist/contract/data-answer.d.ts +40 -0
- package/dist/contract/data-answer.js +3 -0
- package/dist/contract/data-calendar.d.ts +57 -0
- package/dist/contract/data-calendar.js +3 -0
- package/dist/contract/data-news.d.ts +29 -0
- package/dist/contract/data-news.js +3 -0
- package/dist/contract/data-quotes.d.ts +75 -0
- package/dist/contract/data-quotes.js +3 -0
- package/dist/contract/data-series.d.ts +51 -0
- package/dist/contract/data-series.js +3 -0
- package/dist/contract/data-snapshot.d.ts +30 -0
- package/dist/contract/data-snapshot.js +3 -0
- package/dist/contract/data-weather.d.ts +421 -0
- package/dist/contract/data-weather.js +3 -0
- package/dist/contract/player-api-event.d.ts +253 -0
- package/dist/contract/player-api-event.js +3 -0
- package/dist/contract/player-api-status.d.ts +178 -0
- package/dist/contract/player-api-status.js +3 -0
- package/dist/easing.d.ts +16 -0
- package/dist/easing.js +71 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +218 -0
- package/dist/preview/frame/frame.js +2 -0
- package/dist/preview/frame/index.html +12 -0
- package/dist/preview/index.html +40 -0
- package/dist/preview/page.js +7 -0
- package/dist/qr.d.ts +5 -0
- package/dist/qr.js +14 -0
- package/dist/react.d.ts +52 -0
- package/dist/react.js +149 -0
- package/package.json +83 -3
- package/protocol/app-definition.schema.json +435 -0
- package/protocol/app-packages.json +172 -0
- package/protocol/common.schema.json +197 -0
- package/protocol/samples/calendar.json +63 -0
- package/protocol/samples/events/app.command.json +14 -0
- package/protocol/samples/events/item.end.json +13 -0
- package/protocol/samples/events/item.start.json +18 -0
- package/protocol/samples/events/state.changed.json +11 -0
- package/protocol/samples/news.json +37 -0
- package/protocol/samples/player-status.json +58 -0
- package/protocol/samples/weather.json +59 -0
- package/templates/react/AGENTS.md +13 -0
- package/templates/react/README.md +22 -0
- package/templates/react/app.json +14 -0
- package/templates/react/gitignore +2 -0
- package/templates/react/package.json +22 -0
- package/templates/react/src/app.css +7 -0
- package/templates/react/src/app.tsx +39 -0
- package/templates/react/src/env.d.ts +5 -0
- package/templates/react/tsconfig.json +13 -0
- package/templates/vanilla/AGENTS.md +13 -0
- package/templates/vanilla/README.md +22 -0
- package/templates/vanilla/app.json +14 -0
- package/templates/vanilla/gitignore +2 -0
- package/templates/vanilla/package.json +18 -0
- package/templates/vanilla/src/app.css +7 -0
- package/templates/vanilla/src/app.ts +59 -0
- package/templates/vanilla/src/env.d.ts +5 -0
- package/templates/vanilla/tsconfig.json +12 -0
package/dist/qr.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// QR codes for apps (WP-0101 decision 2: `@wyndlass/app-sdk/qr`), drawn without the network on qrcode-generator, as
|
|
2
|
+
// the pairing screen's are. A subpath of its own, so an app that draws none bundles none of the encoder.
|
|
3
|
+
import qrcode from "qrcode-generator";
|
|
4
|
+
/**
|
|
5
|
+
* The QR code for `text`: its modules row by row, true where dark. Error correction M (15%: a screen seen at an angle,
|
|
6
|
+
* a smudge), the smallest version that holds it; byte mode, so any URL fits.
|
|
7
|
+
*/
|
|
8
|
+
export function qrModules(text) {
|
|
9
|
+
const code = qrcode(0, "M");
|
|
10
|
+
code.addData(text, "Byte");
|
|
11
|
+
code.make();
|
|
12
|
+
const n = code.getModuleCount();
|
|
13
|
+
return Array.from({ length: n }, (_, r) => Array.from({ length: n }, (_, c) => code.isDark(r, c)));
|
|
14
|
+
}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React apps (WP-0101 decision 4, `@wyndlass/app-sdk/react`): a component rendered in the app's shadow root, its element
|
|
3
|
+
* a WyndlassApp, with hooks for what the app is given. React is a peer dependency, bundled into the app by `pack`.
|
|
4
|
+
*
|
|
5
|
+
* What a component reaches is the element, through React's context, and a bridge to it kept in this module's scope, so
|
|
6
|
+
* React's tree (which hangs off the shadow root's nodes) holds nothing another app could use: data, images and the
|
|
7
|
+
* rest go through the SDK's functions, as for any app.
|
|
8
|
+
*/
|
|
9
|
+
import { type ComponentType } from "react";
|
|
10
|
+
import type { DataAnswer, ScreenFacts } from "./contract/app-contract.js";
|
|
11
|
+
type Params = Record<string, string | number | boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* An element class for a component, the module's default export: `export default reactApp(Board, { css })`. `css` is
|
|
14
|
+
* the app's styles, as a constructable stylesheet in its shadow root (a CSS file the source imports is text, for it).
|
|
15
|
+
*/
|
|
16
|
+
export declare function reactApp(Component: ComponentType, options?: {
|
|
17
|
+
css?: string;
|
|
18
|
+
}): CustomElementConstructor;
|
|
19
|
+
/**
|
|
20
|
+
* The app as a component reads it: its props with their defaults, ready() (once, within 5 s of its start), its time
|
|
21
|
+
* zone and locale, the screen's place and facts, and whether it is in the CMS's preview. None of it changes with time.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useApp(): {
|
|
24
|
+
prop: <T>(name: string, fallback: T) => T;
|
|
25
|
+
ready: () => void;
|
|
26
|
+
timeZone: string;
|
|
27
|
+
locale: string;
|
|
28
|
+
place: {
|
|
29
|
+
lat: number;
|
|
30
|
+
lon: number;
|
|
31
|
+
} | undefined;
|
|
32
|
+
screen: ScreenFacts | null;
|
|
33
|
+
preview: boolean;
|
|
34
|
+
};
|
|
35
|
+
/** The layout's clock, drawn again at each of its frames: seeking and pausing show the right moment. */
|
|
36
|
+
export declare function useNow(): number;
|
|
37
|
+
/**
|
|
38
|
+
* Which of `count` pages shows now, each `ms` long from the layout's start, and the pages said to the host, so a layout
|
|
39
|
+
* can turn its enter and exit with them (ADR 0071, decision 5).
|
|
40
|
+
*/
|
|
41
|
+
export declare function usePaging(count: number, ms: number): number;
|
|
42
|
+
/**
|
|
43
|
+
* A data source's answer (one the definition declares): the one the screen kept, at once, then the service's; asked
|
|
44
|
+
* again every `everyMs` and when the params change. `error` once an ask has failed with no answer to show.
|
|
45
|
+
*/
|
|
46
|
+
export declare function useData<T = unknown>(source: string, params: Params, everyMs?: number): {
|
|
47
|
+
answer?: DataAnswer<T>;
|
|
48
|
+
error?: Error;
|
|
49
|
+
};
|
|
50
|
+
/** A data table's snapshot (null without one), read again each frame the layout draws: a bound table's rows change with time. */
|
|
51
|
+
export declare function useSnapshot(table: string): unknown | null;
|
|
52
|
+
export {};
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
+
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");
|
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
+
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");
|
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* React apps (WP-0101 decision 4, `@wyndlass/app-sdk/react`): a component rendered in the app's shadow root, its element
|
|
14
|
+
* a WyndlassApp, with hooks for what the app is given. React is a peer dependency, bundled into the app by `pack`.
|
|
15
|
+
*
|
|
16
|
+
* What a component reaches is the element, through React's context, and a bridge to it kept in this module's scope, so
|
|
17
|
+
* React's tree (which hangs off the shadow root's nodes) holds nothing another app could use: data, images and the
|
|
18
|
+
* rest go through the SDK's functions, as for any app.
|
|
19
|
+
*/
|
|
20
|
+
import { createContext, createElement, useContext, useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
21
|
+
import { createRoot } from "react-dom/client";
|
|
22
|
+
import { contextOf, data, kept, snapshot, WyndlassApp } from "./index.js";
|
|
23
|
+
const bridges = new WeakMap();
|
|
24
|
+
const App = createContext(null);
|
|
25
|
+
function useBridge() {
|
|
26
|
+
const app = useContext(App);
|
|
27
|
+
const bridge = app && bridges.get(app);
|
|
28
|
+
if (!app || !bridge)
|
|
29
|
+
throw new Error("a Wyndlass hook is used outside an app made by reactApp()");
|
|
30
|
+
return [app, bridge];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* An element class for a component, the module's default export: `export default reactApp(Board, { css })`. `css` is
|
|
34
|
+
* the app's styles, as a constructable stylesheet in its shadow root (a CSS file the source imports is text, for it).
|
|
35
|
+
*/
|
|
36
|
+
export function reactApp(Component, options = {}) {
|
|
37
|
+
var _ReactApp_root, _ReactApp_listeners, _ReactApp_paging, _ReactApp_time, _a;
|
|
38
|
+
return _a = class ReactApp extends WyndlassApp {
|
|
39
|
+
constructor() {
|
|
40
|
+
super(...arguments);
|
|
41
|
+
_ReactApp_root.set(this, void 0);
|
|
42
|
+
_ReactApp_listeners.set(this, new Set());
|
|
43
|
+
_ReactApp_paging.set(this, null);
|
|
44
|
+
_ReactApp_time.set(this, 0);
|
|
45
|
+
}
|
|
46
|
+
mount() {
|
|
47
|
+
if (options.css)
|
|
48
|
+
this.css(options.css);
|
|
49
|
+
__classPrivateFieldSet(this, _ReactApp_time, this.now(), "f");
|
|
50
|
+
bridges.set(this, {
|
|
51
|
+
prop: (name, fallback) => this.prop(name, fallback),
|
|
52
|
+
ready: () => this.ready(),
|
|
53
|
+
time: () => __classPrivateFieldGet(this, _ReactApp_time, "f"),
|
|
54
|
+
startedAt: () => this.startedAt(),
|
|
55
|
+
timeZone: () => this.timeZone,
|
|
56
|
+
locale: () => this.locale,
|
|
57
|
+
subscribe: listener => { __classPrivateFieldGet(this, _ReactApp_listeners, "f").add(listener); return () => { __classPrivateFieldGet(this, _ReactApp_listeners, "f").delete(listener); }; },
|
|
58
|
+
setPaging: paging => { __classPrivateFieldSet(this, _ReactApp_paging, paging, "f"); },
|
|
59
|
+
});
|
|
60
|
+
const host = document.createElement("div");
|
|
61
|
+
host.style.cssText = "width:100%;height:100%";
|
|
62
|
+
this.root.append(host);
|
|
63
|
+
__classPrivateFieldSet(this, _ReactApp_root, createRoot(host), "f");
|
|
64
|
+
__classPrivateFieldGet(this, _ReactApp_root, "f").render(createElement(App.Provider, { value: this }, createElement(Component)));
|
|
65
|
+
}
|
|
66
|
+
draw(epochMs) {
|
|
67
|
+
__classPrivateFieldSet(this, _ReactApp_time, epochMs, "f");
|
|
68
|
+
for (const listener of __classPrivateFieldGet(this, _ReactApp_listeners, "f"))
|
|
69
|
+
listener();
|
|
70
|
+
}
|
|
71
|
+
paging() { return __classPrivateFieldGet(this, _ReactApp_paging, "f"); }
|
|
72
|
+
disconnectedCallback() {
|
|
73
|
+
super.disconnectedCallback();
|
|
74
|
+
// A box the layout takes away for good is not connected again: React lets the component go with it.
|
|
75
|
+
const root = __classPrivateFieldGet(this, _ReactApp_root, "f");
|
|
76
|
+
queueMicrotask(() => { if (!this.isConnected) {
|
|
77
|
+
root?.unmount();
|
|
78
|
+
__classPrivateFieldSet(this, _ReactApp_root, undefined, "f");
|
|
79
|
+
} });
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
_ReactApp_root = new WeakMap(),
|
|
83
|
+
_ReactApp_listeners = new WeakMap(),
|
|
84
|
+
_ReactApp_paging = new WeakMap(),
|
|
85
|
+
_ReactApp_time = new WeakMap(),
|
|
86
|
+
_a;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The app as a component reads it: its props with their defaults, ready() (once, within 5 s of its start), its time
|
|
90
|
+
* zone and locale, the screen's place and facts, and whether it is in the CMS's preview. None of it changes with time.
|
|
91
|
+
*/
|
|
92
|
+
export function useApp() {
|
|
93
|
+
const [app, bridge] = useBridge();
|
|
94
|
+
return useMemo(() => {
|
|
95
|
+
const context = contextOf(app);
|
|
96
|
+
return {
|
|
97
|
+
prop: (name, fallback) => bridge.prop(name, fallback),
|
|
98
|
+
ready: () => bridge.ready(),
|
|
99
|
+
timeZone: bridge.timeZone(),
|
|
100
|
+
locale: bridge.locale(),
|
|
101
|
+
place: context?.place,
|
|
102
|
+
screen: (context?.screen ?? null),
|
|
103
|
+
preview: !!context?.preview,
|
|
104
|
+
};
|
|
105
|
+
}, [app, bridge]);
|
|
106
|
+
}
|
|
107
|
+
/** The layout's clock, drawn again at each of its frames: seeking and pausing show the right moment. */
|
|
108
|
+
export function useNow() {
|
|
109
|
+
const [, bridge] = useBridge();
|
|
110
|
+
return useSyncExternalStore(bridge.subscribe, bridge.time, bridge.time);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Which of `count` pages shows now, each `ms` long from the layout's start, and the pages said to the host, so a layout
|
|
114
|
+
* can turn its enter and exit with them (ADR 0071, decision 5).
|
|
115
|
+
*/
|
|
116
|
+
export function usePaging(count, ms) {
|
|
117
|
+
const [, bridge] = useBridge();
|
|
118
|
+
const now = useNow();
|
|
119
|
+
useEffect(() => { bridge.setPaging(count > 1 && ms > 0 ? { count, ms } : null); }, [bridge, count, ms]);
|
|
120
|
+
return count > 1 && ms > 0 ? Math.floor(Math.max(0, now - bridge.startedAt()) / ms) % count : 0;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A data source's answer (one the definition declares): the one the screen kept, at once, then the service's; asked
|
|
124
|
+
* again every `everyMs` and when the params change. `error` once an ask has failed with no answer to show.
|
|
125
|
+
*/
|
|
126
|
+
export function useData(source, params, everyMs = 10 * 60000) {
|
|
127
|
+
const [app] = useBridge();
|
|
128
|
+
const key = JSON.stringify([source, params]);
|
|
129
|
+
const [state, setState] = useState(() => ({ key, answer: kept(app, source, params) }));
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
let live = true;
|
|
132
|
+
const ask = () => data(app, source, params).then(answer => { if (live)
|
|
133
|
+
setState({ key, answer }); }, error => { if (live)
|
|
134
|
+
setState(s => s.key === key && s.answer ? s : { key, error: error }); });
|
|
135
|
+
if (state.key !== key)
|
|
136
|
+
setState({ key, answer: kept(app, source, params) });
|
|
137
|
+
void ask();
|
|
138
|
+
const timer = setInterval(ask, everyMs);
|
|
139
|
+
return () => { live = false; clearInterval(timer); };
|
|
140
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- the params by value
|
|
141
|
+
}, [app, key, everyMs]);
|
|
142
|
+
return state.key === key ? { answer: state.answer, error: state.error } : { answer: kept(app, source, params) };
|
|
143
|
+
}
|
|
144
|
+
/** A data table's snapshot (null without one), read again each frame the layout draws: a bound table's rows change with time. */
|
|
145
|
+
export function useSnapshot(table) {
|
|
146
|
+
const [app] = useBridge();
|
|
147
|
+
useNow();
|
|
148
|
+
return snapshot(app, table);
|
|
149
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wyndlass/app-sdk",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Build apps for Wyndlass's screens: web components on the app contract (Player API 2), with the runtime Wyndlass's own apps use.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"wyndlass",
|
|
7
|
+
"digital-signage",
|
|
8
|
+
"signage",
|
|
9
|
+
"web-components"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "Northwynd Labs LLC",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/NorthwyndLabs/wyndlass.git",
|
|
16
|
+
"directory": "app-sdk"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"bin": {
|
|
21
|
+
"wyndlass-app": "./cli/main.mjs"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./qr": {
|
|
29
|
+
"types": "./dist/qr.d.ts",
|
|
30
|
+
"import": "./dist/qr.js"
|
|
31
|
+
},
|
|
32
|
+
"./react": {
|
|
33
|
+
"types": "./dist/react.d.ts",
|
|
34
|
+
"import": "./dist/react.js"
|
|
35
|
+
},
|
|
36
|
+
"./app-definition.schema.json": "./protocol/app-definition.schema.json",
|
|
37
|
+
"./common.schema.json": "./protocol/common.schema.json"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"cli",
|
|
42
|
+
"templates",
|
|
43
|
+
"protocol",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=20"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"acorn": "8.19.0",
|
|
52
|
+
"ajv": "8.17.1",
|
|
53
|
+
"ajv-formats": "3.0.1",
|
|
54
|
+
"esbuild": "^0.28.2",
|
|
55
|
+
"qrcode-generator": "2.0.4"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"react": ">=18",
|
|
59
|
+
"react-dom": ">=18"
|
|
60
|
+
},
|
|
61
|
+
"peerDependenciesMeta": {
|
|
62
|
+
"react": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
65
|
+
"react-dom": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@types/node": "22",
|
|
71
|
+
"@types/react": "19.3.0",
|
|
72
|
+
"@types/react-dom": "19.3.0",
|
|
73
|
+
"playwright-core": "1.63.0",
|
|
74
|
+
"react": "19.3.0",
|
|
75
|
+
"react-dom": "19.3.0",
|
|
76
|
+
"tsx": "^4.23.15",
|
|
77
|
+
"typescript": "5.9.3"
|
|
78
|
+
},
|
|
79
|
+
"scripts": {
|
|
80
|
+
"copy-protocol": "node scripts/copy-protocol.mjs",
|
|
81
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p preview/tsconfig.json",
|
|
82
|
+
"build": "node scripts/copy-protocol.mjs --check && tsc -p tsconfig.build.json && node scripts/build-preview.mjs",
|
|
83
|
+
"test": "pnpm build && node --import tsx --test --test-force-exit test/*.test.ts",
|
|
84
|
+
"check": "node scripts/copy-protocol.mjs --check && pnpm typecheck"
|
|
85
|
+
}
|
|
6
86
|
}
|