@wyndlass/app-sdk 1.1.3 → 1.3.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/README.md +33 -5
- package/cli/preview.mjs +8 -4
- package/dist/contract/app-contract.d.ts +9 -1
- package/dist/frames.d.ts +4 -0
- package/dist/frames.js +18 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/preview/frame/frame.js +1 -1
- package/dist/preview/frame/index.html +1 -0
- package/dist/preview/frame/made.js +2 -0
- package/dist/preview/index.html +5 -4
- package/dist/preview/page.js +5 -5
- package/dist/react.d.ts +13 -6
- package/dist/react.js +30 -12
- package/dist/signals.d.ts +17 -0
- package/dist/signals.js +30 -0
- package/package.json +6 -1
- package/skill/wyndlass-app/SKILL.md +3 -0
- package/skill/wyndlass-app/references/contract.md +12 -4
- package/skill/wyndlass-app/references/templates.md +7 -6
- package/templates/react/AGENTS.md +3 -2
- package/templates/react/src/app.tsx +3 -2
- package/templates/vanilla/AGENTS.md +2 -1
package/dist/react.js
CHANGED
|
@@ -17,7 +17,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
17
17
|
* React's tree (which hangs off the shadow root's nodes) holds nothing another app could use: data, images and the
|
|
18
18
|
* rest go through the SDK's functions, as for any app.
|
|
19
19
|
*/
|
|
20
|
-
import { createContext, createElement, useContext, useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
20
|
+
import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
21
21
|
import { createRoot } from "react-dom/client";
|
|
22
22
|
import { contextOf, data, kept, snapshot, WyndlassApp } from "./index.js";
|
|
23
23
|
const bridges = new WeakMap();
|
|
@@ -105,23 +105,27 @@ export function useApp() {
|
|
|
105
105
|
}, [app, bridge]);
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
|
-
* The layout's clock
|
|
109
|
-
*
|
|
110
|
-
*
|
|
108
|
+
* The layout's clock: seeking and pausing show the right moment. Without `everyMs`, the component renders again at each
|
|
109
|
+
* of the layout's frames, many times a second. With it, the time is a multiple of `everyMs` (`useNow(1000)`: whole
|
|
110
|
+
* seconds), and the component renders only when that changes, as a seek to another moment does (WP-0147 decision 2).
|
|
111
|
+
* Take the coarsest the app shows. Either way, make formatters and other objects with `useMemo`, never in the render: one
|
|
112
|
+
* made per frame holds memory a screen does not free in time.
|
|
111
113
|
*/
|
|
112
|
-
export function useNow() {
|
|
114
|
+
export function useNow(everyMs) {
|
|
113
115
|
const [, bridge] = useBridge();
|
|
114
|
-
|
|
116
|
+
const step = everyMs !== undefined && everyMs > 0 ? everyMs : 0;
|
|
117
|
+
const read = useCallback(() => step ? Math.floor(bridge.time() / step) * step : bridge.time(), [bridge, step]);
|
|
118
|
+
return useSyncExternalStore(bridge.subscribe, read, read);
|
|
115
119
|
}
|
|
116
120
|
/**
|
|
117
121
|
* Which of `count` pages shows now, each `ms` long from the layout's start, and the pages said to the host, so a layout
|
|
118
|
-
* can turn its enter and exit with them (ADR 0071, decision 5).
|
|
122
|
+
* can turn its enter and exit with them (ADR 0071, decision 5). The component renders when the page turns (WP-0147).
|
|
119
123
|
*/
|
|
120
124
|
export function usePaging(count, ms) {
|
|
121
125
|
const [, bridge] = useBridge();
|
|
122
|
-
const
|
|
126
|
+
const read = useCallback(() => count > 1 && ms > 0 ? Math.floor(Math.max(0, bridge.time() - bridge.startedAt()) / ms) % count : 0, [bridge, count, ms]);
|
|
123
127
|
useEffect(() => { bridge.setPaging(count > 1 && ms > 0 ? { count, ms } : null); }, [bridge, count, ms]);
|
|
124
|
-
return
|
|
128
|
+
return useSyncExternalStore(bridge.subscribe, read, read);
|
|
125
129
|
}
|
|
126
130
|
/**
|
|
127
131
|
* A data source's answer (one the definition declares): the one the screen kept, at once, then the service's; asked
|
|
@@ -145,9 +149,23 @@ export function useData(source, params, everyMs = 10 * 60000) {
|
|
|
145
149
|
}, [app, key, everyMs]);
|
|
146
150
|
return state.key === key ? { answer: state.answer, error: state.error } : { answer: kept(app, source, params) };
|
|
147
151
|
}
|
|
148
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* A data table's snapshot, as snapshot() gives it (null when the screen has none). On a player that says when a table
|
|
154
|
+
* changes (onSnapshot, WP-0149), it renders when the rows do: when the screen says so, or when a binding's rows, chosen
|
|
155
|
+
* on the layout's clock, change. On an older player, which makes the app again when a table changes, it reads them at
|
|
156
|
+
* every frame, as before.
|
|
157
|
+
*/
|
|
149
158
|
export function useSnapshot(table) {
|
|
150
|
-
const [app] = useBridge();
|
|
151
|
-
|
|
159
|
+
const [app, bridge] = useBridge();
|
|
160
|
+
const live = typeof contextOf(app)?.onSnapshot === "function";
|
|
161
|
+
// Each frame, and each change the screen says (it may come while the layout is held).
|
|
162
|
+
const subscribe = useCallback((listener) => {
|
|
163
|
+
const off = bridge.subscribe(listener);
|
|
164
|
+
const unheard = contextOf(app)?.onSnapshot?.(() => listener());
|
|
165
|
+
return () => { off(); unheard?.(); };
|
|
166
|
+
}, [app, bridge]);
|
|
167
|
+
// Live, the rows are the same object until they change; an older player's are a new object at each read, so its frames decide.
|
|
168
|
+
const read = useCallback(() => live ? snapshot(app, table) : bridge.time(), [app, bridge, live, table]);
|
|
169
|
+
useSyncExternalStore(subscribe, read, read);
|
|
152
170
|
return snapshot(app, table);
|
|
153
171
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table rows as Preact signals (WP-0149 decision 5, `@wyndlass/app-sdk/signals`): for an app that updates its DOM when its
|
|
3
|
+
* rows change, in an effect, rather than at every frame. An entry of its own, so an app that does not import it does not
|
|
4
|
+
* bundle it. Its signals are this copy of `@preact/signals-core`'s: take effect, computed and the rest from here too, as
|
|
5
|
+
* an effect from another copy does not see them.
|
|
6
|
+
*/
|
|
7
|
+
import { type ReadonlySignal } from "@preact/signals-core";
|
|
8
|
+
import { type WyndlassApp } from "./index.js";
|
|
9
|
+
export { batch, computed, effect, signal, untracked, type ReadonlySignal, type Signal } from "@preact/signals-core";
|
|
10
|
+
/**
|
|
11
|
+
* A data table's snapshot, as snapshot() gives it, as a read-only signal: its value changes when the rows do, and is the
|
|
12
|
+
* same object until then. Read again when the screen says the table changed, and at each frame, since a binding's rows
|
|
13
|
+
* are chosen on the layout's clock (WP-0137). Make it in mount(), where the app has its context.
|
|
14
|
+
*
|
|
15
|
+
* On a player before WP-0149, which makes the app again when a table changes, it keeps the rows the app was made with.
|
|
16
|
+
*/
|
|
17
|
+
export declare function tableSignal(app: WyndlassApp, table: string): ReadonlySignal<unknown | null>;
|
package/dist/signals.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table rows as Preact signals (WP-0149 decision 5, `@wyndlass/app-sdk/signals`): for an app that updates its DOM when its
|
|
3
|
+
* rows change, in an effect, rather than at every frame. An entry of its own, so an app that does not import it does not
|
|
4
|
+
* bundle it. Its signals are this copy of `@preact/signals-core`'s: take effect, computed and the rest from here too, as
|
|
5
|
+
* an effect from another copy does not see them.
|
|
6
|
+
*/
|
|
7
|
+
import { computed, signal } from "@preact/signals-core";
|
|
8
|
+
import { eachFrame } from "./frames.js";
|
|
9
|
+
import { contextOf, snapshot } from "./index.js";
|
|
10
|
+
export { batch, computed, effect, signal, untracked } from "@preact/signals-core";
|
|
11
|
+
/**
|
|
12
|
+
* A data table's snapshot, as snapshot() gives it, as a read-only signal: its value changes when the rows do, and is the
|
|
13
|
+
* same object until then. Read again when the screen says the table changed, and at each frame, since a binding's rows
|
|
14
|
+
* are chosen on the layout's clock (WP-0137). Make it in mount(), where the app has its context.
|
|
15
|
+
*
|
|
16
|
+
* On a player before WP-0149, which makes the app again when a table changes, it keeps the rows the app was made with.
|
|
17
|
+
*/
|
|
18
|
+
export function tableSignal(app, table) {
|
|
19
|
+
const rows = signal(snapshot(app, table));
|
|
20
|
+
const context = contextOf(app);
|
|
21
|
+
if (context?.onSnapshot) {
|
|
22
|
+
// The same object while the rows are the same: set only when another comes.
|
|
23
|
+
const read = () => { const next = snapshot(app, table); if (next !== rows.peek())
|
|
24
|
+
rows.value = next; };
|
|
25
|
+
context.onSnapshot(changed => { if (changed === table)
|
|
26
|
+
read(); });
|
|
27
|
+
eachFrame(app, read);
|
|
28
|
+
}
|
|
29
|
+
return computed(() => rows.value);
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wyndlass/app-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"wyndlass",
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"types": "./dist/react.d.ts",
|
|
34
34
|
"import": "./dist/react.js"
|
|
35
35
|
},
|
|
36
|
+
"./signals": {
|
|
37
|
+
"types": "./dist/signals.d.ts",
|
|
38
|
+
"import": "./dist/signals.js"
|
|
39
|
+
},
|
|
36
40
|
"./app-definition.schema.json": "./protocol/app-definition.schema.json",
|
|
37
41
|
"./common.schema.json": "./protocol/common.schema.json"
|
|
38
42
|
},
|
|
@@ -49,6 +53,7 @@
|
|
|
49
53
|
"node": ">=20"
|
|
50
54
|
},
|
|
51
55
|
"dependencies": {
|
|
56
|
+
"@preact/signals-core": "1.14.5",
|
|
52
57
|
"acorn": "8.19.0",
|
|
53
58
|
"ajv": "8.17.1",
|
|
54
59
|
"ajv-formats": "3.0.1",
|
|
@@ -33,6 +33,7 @@ Use the `react` template unless the person asks for plain TypeScript (`vanilla`)
|
|
|
33
33
|
- Run `npm run typecheck` and `npm run check` after each change, and fix what they say.
|
|
34
34
|
- Start `npm run preview` in the background. It serves `http://127.0.0.1:5199/` on this computer only, and draws the app again on every save.
|
|
35
35
|
- **Ask the person to look,** at the screen size they gave and at the other orientation: under **Screen**, set **Size** to 1920 × 1080, then 1080 × 1920. Ask them to play the clock and to pause it. Change what they ask for.
|
|
36
|
+
- **Read what it makes while it plays,** the line below the report. If it says the app makes something at every frame (a formatter, a canvas, an object URL, a listener), fix it as it says before you pack: one made at every frame can run a screen out of memory.
|
|
36
37
|
- **Before importing,** run `npm run preview -- --frame` once and ask them to look again. In the CMS an app runs in a sandboxed frame with no storage, cookies or parent page. An app that fails there works on screens but not in the designer. The frame is served from a second port: use a regular browser for this check, since a browser built into an agent's app may block that port (`net::ERR_BLOCKED_BY_CLIENT`), and the app then reports `app.failed: load`.
|
|
37
38
|
- **Tables in the preview** are keyed as Wyndlass keys a table's columns: from their labels, lowercased, with underscores (a CSV header `Now serving` is the column `now_serving`). A `column` param holds that key, as it will on screens.
|
|
38
39
|
|
|
@@ -90,6 +91,8 @@ Screens or the CMS enforce each of these:
|
|
|
90
91
|
|
|
91
92
|
One more, which nothing enforces: **make nothing in a draw.** `draw(epochMs)` and a React component run again at every frame, many times a second. Make formatters (`Intl.DateTimeFormat`, `Intl.NumberFormat`) and other objects once: in `mount()`, or with `useMemo` on what they depend on. One made in each draw holds memory a screen does not free in time: an app that did so ran a 4 GB screen out of memory within an hour.
|
|
92
93
|
|
|
94
|
+
And **read a table's rows where you draw,** never once in `mount()`: a screen gives the app a table's new rows while it plays, within about a second of the save. Use `snapshot(this, table)` in `draw()`, `useSnapshot(table)` in a component, or `tableSignal(this, table)` with an `effect` (`@wyndlass/app-sdk/signals`). The rows are frozen: copy them before you sort or change them.
|
|
95
|
+
|
|
93
96
|
## When something goes wrong
|
|
94
97
|
|
|
95
98
|
- **A tool you need is missing:** call `whoami`. The import tools are listed only while the organization has imported apps turned on at its top place. An Owner or Admin turns them on in the CMS, under **Settings**, **Imported apps**.
|
|
@@ -27,7 +27,7 @@ A paged app also defines `paging()`, returning `{count, ms}`, so the layout can
|
|
|
27
27
|
- `data(this, source, params)`: the data service's answer, a promise. Only for sources `app.json`'s `data` lists;
|
|
28
28
|
- `kept(this, source, params)`: the answer the screen last kept for the same request, at once, or `undefined`. Draw it while `data` asks again, so the app is ready without the network;
|
|
29
29
|
- `image(this, path)`: a picture a data answer names, as a URL the app can draw;
|
|
30
|
-
- `snapshot(this, table)`: the rows of a table the layout names, in a `datatable` param or in the element's `binding`. Where a binding chooses rows (a filter, a sort, a limit), only those. `null` when the screen has no snapshot of that table. A `datatable` param needs no binding;
|
|
30
|
+
- `snapshot(this, table)`: the rows of a table the layout names, in a `datatable` param or in the element's `binding`. Where a binding chooses rows (a filter, a sort, a limit), only those. `null` when the screen has no snapshot of that table. A `datatable` param needs no binding. A screen gives the app a table's new rows while it plays: read them where you draw (in `draw()`, `useSnapshot` or `tableSignal`), never once in `mount()`. They are the same object until they change, and frozen: copy rows before you sort or change them;
|
|
31
31
|
- `controllerOf(this)`: the player's controller, as `capabilities.player` allows; absent in the designer and previews;
|
|
32
32
|
- `impressionsOf(this)`: the people at the screen, for an app whose definition declares the `impressions` sensor;
|
|
33
33
|
- `contextOf(this)`: the rest of the context (the screen's `place` and facts, `preview`).
|
|
@@ -40,10 +40,18 @@ A paged app also defines `paging()`, returning `{count, ms}`, so the layout can
|
|
|
40
40
|
|
|
41
41
|
`export default reactApp(Component, { css })`: the element's class. The component renders in the app's shadow root, with `css` as its styles. Hooks:
|
|
42
42
|
- `useApp()`: `prop(name, default)`, `ready()`, `timeZone`, `locale`, `place`, `screen`, `preview`;
|
|
43
|
-
- `useNow()`: the layout's clock
|
|
44
|
-
- `usePaging(count, ms)`: the page showing now. It also tells the host the pages;
|
|
43
|
+
- `useNow(everyMs?)`: the layout's clock. Without `everyMs`, drawn again at each of its frames, so the component renders many times a second. With it, the time is a multiple of `everyMs` (`useNow(1000)`: whole seconds), and the component renders only when that changes: take the coarsest the app shows. Either way, make formatters and other objects with `useMemo`, never in the render;
|
|
44
|
+
- `usePaging(count, ms)`: the page showing now, rendering when it turns. It also tells the host the pages;
|
|
45
45
|
- `useData(source, params)`: `{answer, error}`. The kept answer at once, then the service's, asked again every 10 minutes;
|
|
46
|
-
- `useSnapshot(table)`: a data table's rows, as `snapshot` gives them
|
|
46
|
+
- `useSnapshot(table)`: a data table's rows, as `snapshot` gives them. The component renders when they change.
|
|
47
|
+
|
|
48
|
+
## Signals (`@wyndlass/app-sdk/signals`)
|
|
49
|
+
|
|
50
|
+
For a vanilla app that updates its DOM when its rows change, rather than at every frame:
|
|
51
|
+
- `tableSignal(this, table)`: a read-only Preact signal of the table's snapshot, made in `mount()`. Its value changes when the rows do;
|
|
52
|
+
- `effect`, `computed`, `batch`, `signal`, `untracked`: Preact's, from the same copy. Import them from here, never from `@preact/signals-core`, whose effects would not see these signals.
|
|
53
|
+
|
|
54
|
+
Draw in an `effect(() => { … rows.value … })` made in `mount()`, and leave `draw()` empty.
|
|
47
55
|
|
|
48
56
|
## The definition, `app.json`
|
|
49
57
|
|
|
@@ -28,11 +28,12 @@ Two pages, eight seconds each: a heading and the time, then the weather for the
|
|
|
28
28
|
|
|
29
29
|
1. **Change `app.json` first:** the `params` people set in the designer (each with a `label` and a `default`), the `data` sources it reads, and `capabilities` if it asks for more.
|
|
30
30
|
2. **Read each param** with `prop(name, default)` (vanilla) or `useApp().prop(name, default)` (react), with the same default as `app.json`.
|
|
31
|
-
3. **Draw from the layout's clock:** vanilla's `draw(epochMs)` gets the moment; react's `useNow()` gives it. Work out what changes with time (a page, a countdown, a clock) from that number, never from a timer of your own. Both run at every frame: make a formatter once, as the templates do (`#format` in `mount()`, `useMemo` in react), never in the draw.
|
|
32
|
-
4. **
|
|
33
|
-
5. **
|
|
34
|
-
6. **
|
|
35
|
-
7. **
|
|
36
|
-
8. **
|
|
31
|
+
3. **Draw from the layout's clock:** vanilla's `draw(epochMs)` gets the moment; react's `useNow()` gives it, and `useNow(1000)` gives whole seconds, rendering once a second. Work out what changes with time (a page, a countdown, a clock) from that number, never from a timer of your own. Both run at every frame: make a formatter once, as the templates do (`#format` in `mount()`, `useMemo` in react), never in the draw.
|
|
32
|
+
4. **Read a table's rows where you draw,** for an app with a `datatable` param: `snapshot(this, table)` in `draw()`, `tableSignal(this, table)` with an `effect` (vanilla, `@wyndlass/app-sdk/signals`), or `useSnapshot(table)` (react). A screen gives the app a table's new rows while it plays, so a copy kept from `mount()` goes stale. The rows are frozen: copy before you sort them. In the preview, edit a table's rows and check the app follows without being made again.
|
|
33
|
+
5. **Say `ready()`** as soon as the app has something to show, within 5 s: the kept answer, a default, or the failure.
|
|
34
|
+
6. **Styles** go in `src/app.css`. Size text to the box with `fit()` or the `--w` and `--h` properties ([contract.md](contract.md)), since screens run from 1080p to 4K.
|
|
35
|
+
7. **Images:** import them from `src/` (`import logo from "./logo.png"`: PNG, JPEG, GIF, WebP or SVG), and `pack` inlines each as a data URL, for an `<img>`'s `src` or a style set from code. A `url()` inside `src/app.css` is not bundled.
|
|
36
|
+
8. **Fonts:** give the app a param named `font`, of the kind `font`, so the person picks a font in the designer. Apply it as CSS with `font()` (vanilla), or `typographyCss(app.prop("font", null))` (react) ([contract.md](contract.md)).
|
|
37
|
+
9. **Delete what the template drew** that the person did not ask for, its weather included, and its `data` entry with it.
|
|
37
38
|
|
|
38
39
|
Each source change redraws the preview. `npm run typecheck` and `npm run check` find mistakes before the person sees them.
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
An app for Wyndlass's screens, on `@wyndlass/app-sdk` and React. Its README says what it is; these are the rules.
|
|
4
4
|
|
|
5
5
|
- **The module is `src/app.tsx`;** its default export is `reactApp(Component, { css })`, the element's class. The component renders in the app's shadow root.
|
|
6
|
-
- **Time:** `useNow()` gives the layout's clock, drawn again at each of its frames; never `Date.now()`, so a seek or a pause shows the right moment.
|
|
6
|
+
- **Time:** `useNow()` gives the layout's clock, drawn again at each of its frames, and `useNow(1000)` whole seconds, rendering once a second: take the coarsest the app shows; never `Date.now()`, so a seek or a pause shows the right moment.
|
|
7
7
|
- **Nothing made in a render:** the component renders at every frame, many times a second. Make formatters (`Intl.DateTimeFormat`, `Intl.NumberFormat`) and other objects with `useMemo` on what they depend on, never in the render. One made in each draw holds memory a screen does not free in time: an app that did so ran a 4 GB screen out of memory within an hour.
|
|
8
|
+
- **A table's rows from `useSnapshot(table)`:** a screen gives the app a table's new rows while it plays, and the component renders when they change. Never keep a copy made once. They are frozen: copy before you sort or change them.
|
|
8
9
|
- **Ready:** call `useApp().ready()` once drawn, within 5 s, or the screen reports the app late. `useData()` gives the answer the screen kept at once, then the service's.
|
|
9
10
|
- **Data:** only through `useData(source, params)`, and only the sources `app.json`'s `data` lists. Pages: `usePaging(count, ms)`.
|
|
10
11
|
- **Styles:** `src/app.css`, passed as `css`, never a `<style>`.
|
|
11
12
|
- **No `localStorage`** for anything that matters, **no secrets** in params or the module, and **network hosts declared** in `capabilities.network`.
|
|
12
|
-
- **Look at it:** `npm run preview` (http://127.0.0.1:5199/), in both orientations, with the clock played and held, and once with `npm run preview -- --frame`, as the CMS runs it.
|
|
13
|
+
- **Look at it:** `npm run preview` (http://127.0.0.1:5199/), in both orientations, with the clock played and held, and once with `npm run preview -- --frame`, as the CMS runs it. Below the report, it says what the app makes at every frame: fix that before packing.
|
|
13
14
|
- **Before importing:** `npm run check`, then `npm run pack`. Raise `version` in `app.json` for each new version.
|
|
14
15
|
- **Importing and scheduling** go through the Wyndlass MCP, with the person's approval: `app.prepareImport`, `npx wyndlass-app upload <url> dist/<zip> --header "name: value"`, `app.import`, then `app.list` until the version is ready or held. A held version waits for a person who may approve media.
|
|
15
16
|
- **Never approve a version:** a person who may approve media does, in the CMS. Give them the link `app.import` answers, and say why it is held.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// __APP_NAME__: a heading and the time on one page, the weather on the next, a React component on Wyndlass's app SDK.
|
|
2
2
|
// Everything it draws comes from the layout's clock (useNow()), so the designer's seek and pause show the right moment.
|
|
3
|
+
// It shows whole seconds at most, so it renders once a second (useNow(1000)), not at every frame.
|
|
3
4
|
import { useEffect, useMemo } from "react";
|
|
4
5
|
import { color, type weather } from "@wyndlass/app-sdk";
|
|
5
6
|
import { reactApp, useApp, useData, useNow, usePaging } from "@wyndlass/app-sdk/react";
|
|
@@ -10,12 +11,12 @@ const PAGE_MS = 8_000;
|
|
|
10
11
|
|
|
11
12
|
function Board() {
|
|
12
13
|
const app = useApp();
|
|
13
|
-
const now = useNow();
|
|
14
|
+
const now = useNow(1000);
|
|
14
15
|
const page = usePaging(2, PAGE_MS);
|
|
15
16
|
// The weather for the screen's place, or Winnipeg's where there is none (the designer's sample).
|
|
16
17
|
const place = app.place ?? { lat: 49.9, lon: -97.14 };
|
|
17
18
|
const { answer, error } = useData<weather.Weather>("weather", { lat: place.lat, lon: place.lon, units: app.prop("units", "metric") });
|
|
18
|
-
// Made once
|
|
19
|
+
// Made once, not in each render: a formatter made in each render holds memory (AGENTS.md).
|
|
19
20
|
const time = useMemo(() => new Intl.DateTimeFormat(app.locale, { timeZone: app.timeZone, hour: "numeric", minute: "2-digit",
|
|
20
21
|
...(app.prop("seconds", false) ? { second: "2-digit" } : {}) }), [app]);
|
|
21
22
|
// Ready once there is something to show: the kept answer at once, else the service's answer or its failure.
|
|
@@ -5,11 +5,12 @@ An app for Wyndlass's screens, on `@wyndlass/app-sdk`. Its README says what it i
|
|
|
5
5
|
- **The module is `src/app.ts`;** its default export is the element's class, extending `WyndlassApp`. `mount()` builds the shadow DOM once; `draw(epochMs)` draws that moment of the layout's clock.
|
|
6
6
|
- **Time:** from `now()` or `draw`'s `epochMs`, never `Date.now()`, so a seek or a pause shows the right moment.
|
|
7
7
|
- **Nothing made in `draw`:** it runs at every frame, many times a second. Make formatters (`Intl.DateTimeFormat`, `Intl.NumberFormat`) and other objects in `mount()`, never in `draw`. One made in each draw holds memory a screen does not free in time: an app that did so ran a 4 GB screen out of memory within an hour.
|
|
8
|
+
- **A table's rows where you draw:** a screen gives the app a table's new rows while it plays. Read them with `snapshot(this, table)` in `draw`, or with `tableSignal(this, table)` and an `effect` from `@wyndlass/app-sdk/signals`, made in `mount()`. Never read them once in `mount()` and keep them. They are frozen: copy before you sort or change them.
|
|
8
9
|
- **Ready:** call `ready()` once drawn, within 5 s, or the screen reports the app late. Draw the answer `kept()` gives at once, then ask `data()`.
|
|
9
10
|
- **Data:** only through `data(this, source, params)`, and only the sources `app.json`'s `data` lists. Images a data answer names through `image(this, path)`.
|
|
10
11
|
- **Styles:** `this.css(text)`, never a `<style>`. A CSS file the source imports is its text.
|
|
11
12
|
- **No `localStorage`** for anything that matters, **no secrets** in params or the module, and **network hosts declared** in `capabilities.network`.
|
|
12
|
-
- **Look at it:** `npm run preview` (http://127.0.0.1:5199/), in both orientations, with the clock played and held, and once with `npm run preview -- --frame`, as the CMS runs it.
|
|
13
|
+
- **Look at it:** `npm run preview` (http://127.0.0.1:5199/), in both orientations, with the clock played and held, and once with `npm run preview -- --frame`, as the CMS runs it. Below the report, it says what the app makes at every frame: fix that before packing.
|
|
13
14
|
- **Before importing:** `npm run check`, then `npm run pack`. Raise `version` in `app.json` for each new version.
|
|
14
15
|
- **Importing and scheduling** go through the Wyndlass MCP, with the person's approval: `app.prepareImport`, `npx wyndlass-app upload <url> dist/<zip> --header "name: value"`, `app.import`, then `app.list` until the version is ready or held. A held version waits for a person who may approve media.
|
|
15
16
|
- **Never approve a version:** a person who may approve media does, in the CMS. Give them the link `app.import` answers, and say why it is held.
|