@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.
- package/README.md +212 -1
- package/cli/app-package.mjs +111 -0
- package/cli/commands.mjs +133 -0
- package/cli/main.mjs +73 -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 -4
- 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/skill/wyndlass-app/SKILL.md +90 -0
- package/skill/wyndlass-app/references/contract.md +91 -0
- package/skill/wyndlass-app/references/mcp.md +79 -0
- package/skill/wyndlass-app/references/templates.md +38 -0
- package/templates/react/AGENTS.md +17 -0
- package/templates/react/README.md +23 -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 +13 -0
- package/templates/react/tsconfig.json +13 -0
- package/templates/vanilla/AGENTS.md +17 -0
- package/templates/vanilla/README.md +23 -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 +13 -0
- package/templates/vanilla/tsconfig.json +12 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"place": {
|
|
3
|
+
"lat": 49.9,
|
|
4
|
+
"lon": -97.1
|
|
5
|
+
},
|
|
6
|
+
"units": "metric",
|
|
7
|
+
"timeZone": "America/Winnipeg",
|
|
8
|
+
"current": {
|
|
9
|
+
"at": "2026-10-03T22:30:00-05:00",
|
|
10
|
+
"temperature": 9.9,
|
|
11
|
+
"apparentTemperature": 7.8,
|
|
12
|
+
"condition": "clear",
|
|
13
|
+
"code": 0,
|
|
14
|
+
"isDay": false,
|
|
15
|
+
"humidity": 79,
|
|
16
|
+
"windSpeed": 8.2,
|
|
17
|
+
"windDirection": 308,
|
|
18
|
+
"precipitationProbability": 0
|
|
19
|
+
},
|
|
20
|
+
"daily": [
|
|
21
|
+
{
|
|
22
|
+
"date": "2026-10-03",
|
|
23
|
+
"high": 17.1,
|
|
24
|
+
"low": 9.5,
|
|
25
|
+
"condition": "drizzle",
|
|
26
|
+
"code": 51,
|
|
27
|
+
"precipitationProbability": 34,
|
|
28
|
+
"sunrise": "2026-10-03T07:31:00-05:00",
|
|
29
|
+
"sunset": "2026-10-03T19:01:00-05:00"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"date": "2026-10-04",
|
|
33
|
+
"high": 16.5,
|
|
34
|
+
"low": 5.1,
|
|
35
|
+
"condition": "mostly-clear",
|
|
36
|
+
"code": 1,
|
|
37
|
+
"precipitationProbability": 0,
|
|
38
|
+
"sunrise": "2026-10-04T07:33:00-05:00",
|
|
39
|
+
"sunset": "2026-10-04T18:59:00-05:00"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"date": "2026-10-05",
|
|
43
|
+
"high": 17.9,
|
|
44
|
+
"low": 3.9,
|
|
45
|
+
"condition": "cloudy",
|
|
46
|
+
"code": 3,
|
|
47
|
+
"precipitationProbability": 1,
|
|
48
|
+
"sunrise": "2026-10-05T07:34:00-05:00",
|
|
49
|
+
"sunset": "2026-10-05T18:57:00-05:00"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"facts": {
|
|
53
|
+
"temperatureC": 9.9,
|
|
54
|
+
"condition": "clear",
|
|
55
|
+
"precipitationProbability": 0,
|
|
56
|
+
"windKph": 8.2,
|
|
57
|
+
"alertActive": false
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wyndlass-app
|
|
3
|
+
description: Build an app for Wyndlass's digital signage screens with @wyndlass/app-sdk, preview it, check and pack it, then import, place and schedule it through the Wyndlass MCP. Use when the person wants their own app, widget or live element on their Wyndlass screens, or wants to change one built with @wyndlass/app-sdk.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build a Wyndlass app
|
|
7
|
+
|
|
8
|
+
An app is a web component that Wyndlass's screens run inside a layout: one ES2020 module and its definition, `app.json`. You build it on this computer with `@wyndlass/app-sdk`, the person's organization imports it, and a person reviews it before screens play it.
|
|
9
|
+
|
|
10
|
+
Work through the steps in order. Ask before anything that changes Wyndlass: importing, placing in a layout, scheduling.
|
|
11
|
+
|
|
12
|
+
## 1. Ask
|
|
13
|
+
|
|
14
|
+
Before writing code, ask the person, in one message:
|
|
15
|
+
- **What it shows, and to whom:** the content, and who reads it from how far away (a queue board read from across a room needs large type).
|
|
16
|
+
- **The screens' orientation:** landscape (1920 × 1080), portrait (1080 × 1920), or both.
|
|
17
|
+
- **The data it needs, and where from:** the weather, news feeds, pictures or calendars (data sources Wyndlass gives apps); a data table in Wyndlass; or the organization's own HTTPS service.
|
|
18
|
+
- **Its name, and an id in their own domain,** such as `com.example.queue-board`. Ids under `com.wyndlass.` are Wyndlass's own, and are refused.
|
|
19
|
+
|
|
20
|
+
## 2. Scaffold
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npx @wyndlass/app-sdk create <folder> --template react --id <id> --name "<name>"
|
|
24
|
+
cd <folder>
|
|
25
|
+
npm install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Use the `react` template unless the person asks for plain TypeScript (`vanilla`). Read [references/templates.md](references/templates.md) for what each file is, and [references/contract.md](references/contract.md) before you change the code.
|
|
29
|
+
|
|
30
|
+
## 3. Build and preview
|
|
31
|
+
|
|
32
|
+
- Change `app.json` (its params, the data it reads, what it asks for) and `src/` to do what the person asked. Keep the rules below.
|
|
33
|
+
- Run `npm run typecheck` and `npm run check` after each change, and fix what they say.
|
|
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
|
+
- **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
|
+
- **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.
|
|
37
|
+
|
|
38
|
+
## 4. Pack and check
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm run pack
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
It runs `check` first and writes `dist/<id>-<version>.zip` only when Wyndlass would take it. Its first line gives the zip's size in bytes, which the import asks for. Fix every refusal; heed the warnings (a module over 1 MiB, no `ready()`).
|
|
45
|
+
|
|
46
|
+
## 5. Import, with the Wyndlass MCP
|
|
47
|
+
|
|
48
|
+
Follow [references/mcp.md](references/mcp.md):
|
|
49
|
+
1. `whoami`: who you act as, and in which organization. Ask the person **where** to import it (a place, from `node.list`).
|
|
50
|
+
2. `app.prepareImport` with the place, the zip's file name and its size. It answers an address and headers, open for an hour.
|
|
51
|
+
3. `npx wyndlass-app upload "<address>" dist/<zip> --header "<name>: <value>"`, once per header it gave, `Content-Type` included. Never pass the file's bytes through the conversation.
|
|
52
|
+
4. `app.import` with the same place and the upload's id.
|
|
53
|
+
5. `app.list` until the version is checked: approved, held for review, refused or rejected.
|
|
54
|
+
|
|
55
|
+
- **Refused:** fix the cause in the source, raise `version` in `app.json`, pack and import again.
|
|
56
|
+
- **Held:** give the person the CMS link `app.import` answered, say why it is held, and wait. A person who may approve media approves it in the CMS. You never approve.
|
|
57
|
+
- **The same bytes again** are answered with the version the organization has; other bytes need a new `version`.
|
|
58
|
+
|
|
59
|
+
## 6. Place it
|
|
60
|
+
|
|
61
|
+
Read `guide.read` with topic `layouts`, sections `elements` and `apps`. Then, at a place the person names:
|
|
62
|
+
- `layout.save` with the app's item from `app.list` (`{"type": "app", "id": …, "frame": …, "app": {"id": "med_…"}, "props": …}`), in a new layout or one they choose (`layout.get` first);
|
|
63
|
+
- `layout.check`, and tell them what it says.
|
|
64
|
+
|
|
65
|
+
A held version can be placed and previewed. Screens play it only once it is approved and checked.
|
|
66
|
+
|
|
67
|
+
## 7. Schedule, only with the person's yes
|
|
68
|
+
|
|
69
|
+
Ask what should play where, and when. Then `campaign.save` at the place and times they gave, and show them the draft. `campaign.publish` previews first: tell them what goes live where, and confirm only once they agree. Never schedule at the organization's top place unless they say so.
|
|
70
|
+
|
|
71
|
+
## 8. A new version
|
|
72
|
+
|
|
73
|
+
Raise `version` in `app.json`, then pack and import again (steps 4 and 5). Layouts that name no version take the new one once it is approved and checked. A layout whose app element pins a `version` (the item's) moves only when you save it with the new one: show the person first.
|
|
74
|
+
|
|
75
|
+
## Rules every app keeps
|
|
76
|
+
|
|
77
|
+
Screens or the CMS enforce each of these:
|
|
78
|
+
- **Time from the layout's clock:** `now()`, `draw(epochMs)` or `useNow()`, never `Date.now()`, so a seek or a pause draws the right moment.
|
|
79
|
+
- **`ready()` within 5 s** of the app's start, or the screen reports it late.
|
|
80
|
+
- **ES2020, one module:** `pack` takes care of it. Don't import from a CDN at run time.
|
|
81
|
+
- **No `localStorage`** for anything that matters: in the CMS the app has none.
|
|
82
|
+
- **No secrets** in params or the bundle: anyone who can read the layout or the app can read both.
|
|
83
|
+
- **Data only from the sources `app.json` declares,** and only those apps may read: `weather`, `news`, `image`, `calendar`.
|
|
84
|
+
- **Network hosts declared truthfully** in `capabilities.network`: a person approves them.
|
|
85
|
+
|
|
86
|
+
## When something goes wrong
|
|
87
|
+
|
|
88
|
+
- **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**.
|
|
89
|
+
- **A screen doesn't show the app:** `device.explainPlay` for that screen. A screen whose player is too old for imported apps leaves it out, and plays the rest of the layout.
|
|
90
|
+
- **More detail:** [references/contract.md](references/contract.md) (the runtime, the definition, the limits, what screens do) and [references/mcp.md](references/mcp.md) (each tool's answer and what to do next).
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# The contract: the module, the definition, the limits
|
|
2
|
+
|
|
3
|
+
## The module
|
|
4
|
+
|
|
5
|
+
- **One ES2020 module,** whose default export is the element's class. `pack` bundles and minifies it into `app.js`.
|
|
6
|
+
- **The host defines the class under a tag of its own** (the definition's `element`, a hyphen and a suffix) and hands it its context once, before it joins the page. Never call `customElements.define` or the SDK's `define()`: the host does it.
|
|
7
|
+
- **It imports nothing at run time.** `pack` bundles everything the source imports.
|
|
8
|
+
|
|
9
|
+
## The runtime (`@wyndlass/app-sdk`)
|
|
10
|
+
|
|
11
|
+
`class X extends WyndlassApp` (vanilla). Write two methods:
|
|
12
|
+
- `mount()`: build the shadow DOM in `this.root`, once.
|
|
13
|
+
- `draw(epochMs)`: show that moment of the layout's clock. The host calls it on every frame it needs, seeking and pausing included.
|
|
14
|
+
|
|
15
|
+
It gives the element:
|
|
16
|
+
- `prop(name, default)`: a param's value, or the default when it is absent or empty;
|
|
17
|
+
- `now()`, `startedAt()`, `timeZone`, `locale`;
|
|
18
|
+
- `css(text)`: styles for the shadow root, as a constructable stylesheet. Call it from `mount()`;
|
|
19
|
+
- `font()`: the `font` param as CSS;
|
|
20
|
+
- `ready()`: the app may be shown. Once, within 5 s of its start;
|
|
21
|
+
- `--w` and `--h`: CSS properties with the box's size, for `fit()`;
|
|
22
|
+
- the enter and exit presets the layout gives (fade, scale, slide-up, slide-left). Return the items to stagger from `staggered()`.
|
|
23
|
+
|
|
24
|
+
A paged app also defines `paging()`, returning `{count, ms}`, so the layout can turn its enter and exit with the pages.
|
|
25
|
+
|
|
26
|
+
**What reads or acts for the app is a function that takes it,** so another app on the page can't use it:
|
|
27
|
+
- `data(this, source, params)`: the data service's answer, a promise. Only for sources `app.json`'s `data` lists;
|
|
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
|
+
- `image(this, path)`: a picture a data answer names, as a URL the app can draw;
|
|
30
|
+
- `snapshot(this, table)`: a data table's rows, as the layout's binding chooses them, or `null`;
|
|
31
|
+
- `controllerOf(this)`: the player's controller, as `capabilities.player` allows; absent in the designer and previews;
|
|
32
|
+
- `impressionsOf(this)`: the people at the screen, for an app whose definition declares the `impressions` sensor;
|
|
33
|
+
- `contextOf(this)`: the rest of the context (the screen's `place` and facts, `preview`).
|
|
34
|
+
|
|
35
|
+
**Helpers:** `fit(ofHeight, ofWidth)` (a CSS size fitted to the box), `typographyCss(value)`, `color(value, fallback)`, `escape(text)`, and the presets' curves `easingOf`, `cubicBezier`, `staggerTimes`. `@wyndlass/app-sdk/qr` has `qrModules(text)`, a QR code drawn without the network.
|
|
36
|
+
|
|
37
|
+
**Types:** `AppContext`, `DataAnswer<T>`, `HookOptions`, `AppDefinition`, and each data source's answer as a namespace: `weather.Weather`, `news`, `calendar`, `tables`.
|
|
38
|
+
|
|
39
|
+
## React (`@wyndlass/app-sdk/react`)
|
|
40
|
+
|
|
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
|
+
- `useApp()`: `prop(name, default)`, `ready()`, `timeZone`, `locale`, `place`, `screen`, `preview`;
|
|
43
|
+
- `useNow()`: the layout's clock, drawn again at each of its frames;
|
|
44
|
+
- `usePaging(count, ms)`: the page showing now. It also tells the host the pages;
|
|
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.
|
|
47
|
+
|
|
48
|
+
## The definition, `app.json`
|
|
49
|
+
|
|
50
|
+
Valid against `node_modules/@wyndlass/app-sdk/protocol/app-definition.schema.json`.
|
|
51
|
+
- **Required:** `id` (reverse-domain, in the organization's own domain, never `com.wyndlass.`), `version` (semver), `name`, `element` (lowercase, with a hyphen).
|
|
52
|
+
- `description`.
|
|
53
|
+
- **`params`:** what a person sets in the designer. Each has a `type` (`string`, `text`, `number`, `boolean`, `enum`, `color`, `font`, `media`, `location`, `datetime`, `duration`, `url`, `datatable`, `column`, `json`), and may have a `label`, `default`, `values` (an enum's choices, or suggestions), `group`, `required`, `min`, `max`, `step`, `help`, `advanced` and `when` (the params it depends on). A `url` param's values and default must be `https`.
|
|
54
|
+
- **`data`:** the data sources it reads: `weather`, `news`, `image`, `calendar`. Imported apps may read no others.
|
|
55
|
+
- **`capabilities`:**
|
|
56
|
+
- `player`: `read` (the default) or `control` (playback, audio and display commands);
|
|
57
|
+
- `sensors`: `["impressions"]` for the people at the screen;
|
|
58
|
+
- `audio`: true if it plays sound;
|
|
59
|
+
- `network`: every host it fetches from, such as `api.example.com`. A person sees them when approving it.
|
|
60
|
+
|
|
61
|
+
Asking for more than the approved version (control, a new sensor, audio, a new host) holds the new version for review.
|
|
62
|
+
- **`slots`** (the fields a table's row fills, bound by the layout) and **`animation`** (the presets its enter and exit take).
|
|
63
|
+
|
|
64
|
+
## Limits
|
|
65
|
+
|
|
66
|
+
- The zip: at most 6 MiB, holding exactly `app.json` (at most 256 KiB) and `app.js` (at most 5 MiB).
|
|
67
|
+
- `check` warns, refusing nothing, of a module over 1 MiB (screens load it at every start) and of no `ready()` in the sources.
|
|
68
|
+
|
|
69
|
+
`check` and `pack` refuse with a code, the same as Wyndlass's import:
|
|
70
|
+
|
|
71
|
+
| Code | Fix |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `too-large` | Shrink the module: drop unused dependencies and large inlined images. |
|
|
74
|
+
| `not-a-zip`, `encrypted`, `folder`, `path`, `entries`, `size-mismatch` | The zip wasn't made by `pack`: make it with `npm run pack`. |
|
|
75
|
+
| `definition-json`, `definition-schema` | Fix `app.json`; the schema errors say where. |
|
|
76
|
+
| `definition-id` | Use the organization's own domain, not `com.wyndlass.`. |
|
|
77
|
+
| `definition-when` | A param's `when` names itself, or a param that doesn't exist. |
|
|
78
|
+
| `definition-url` | A `url` param's values and default must be `https` addresses. |
|
|
79
|
+
| `definition-peers` | Remove `capabilities.peers`: apps can't have it yet. |
|
|
80
|
+
| `definition-data` | `data` lists a source apps may not read. |
|
|
81
|
+
| `module-syntax`, `module-import`, `module-default` | The source doesn't build to one ES2020 module with a default export: check the default export and remove any run-time `import()`. |
|
|
82
|
+
|
|
83
|
+
## What screens do
|
|
84
|
+
|
|
85
|
+
- **They draw on the layout's clock:** a seek or a pause draws that moment.
|
|
86
|
+
- **An app not ready within 5 s** is shown as it is, and reported late.
|
|
87
|
+
- **They catch every hook.** An app that throws three times in a minute is stopped and its box emptied. The screen reports why (`load`, `shape`, `unsupported`, `error`, `late` or `overran`) with the name of what was thrown, never its message.
|
|
88
|
+
- **Each app paints only in its box.** What it adds outside is removed; a dialog opens inside the box; `alert`, `confirm`, `prompt`, `print` and `window.open` do nothing.
|
|
89
|
+
- **A version that hangs the page twice in an hour** is held back on that screen until its content next changes.
|
|
90
|
+
- **Offline,** the app gets the answers the screen kept.
|
|
91
|
+
- **In the CMS,** the app runs in a sandboxed frame of its own, with no storage, cookies or parent page, and no controller or people. `npm run preview -- --frame` shows it so.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Importing, placing and scheduling through the Wyndlass MCP
|
|
2
|
+
|
|
3
|
+
Everything that touches Wyndlass goes through the person's own MCP connection, as them, with their permissions, approvals and audit. The `wyndlass-app` command signs in to nothing: `upload` only PUTs to an address the MCP gives.
|
|
4
|
+
|
|
5
|
+
Tool names below are the MCP's. A client may show them with a prefix of its own (such as `mcp__wyndlass__app_import`).
|
|
6
|
+
|
|
7
|
+
## Before importing
|
|
8
|
+
|
|
9
|
+
- **`whoami`:** who you act as, the organization, and which toolsets this connection has. If `app.prepareImport` is missing: the import tools are listed only while imported apps are on at the organization's top place. An Owner or Admin turns them on in the CMS, under **Settings**, **Imported apps**. Tell the person; don't look for another way in.
|
|
10
|
+
- **The place:** ask the person where the app goes. `node.list` with `search` finds a place's id (`node_…`). The app's item is made there on its first import, and layouts at and below it can place it. Importing needs a role that may add media at the place (Editor, Publisher, Admin or Owner). A later version goes to the app's item where it already is, and needs that role there.
|
|
11
|
+
|
|
12
|
+
## 1. `app.prepareImport`
|
|
13
|
+
|
|
14
|
+
`{node, fileName, bytes}`: the place, the zip's file name (`com.example.queue-board-1.0.0.zip`), and its size in bytes, which `npm run pack` printed (at most 6 MiB).
|
|
15
|
+
|
|
16
|
+
It answers an upload id (`upl_…`), an HTTPS address open for an hour, and the headers to send: `Content-Type: application/zip` and the signed ones.
|
|
17
|
+
|
|
18
|
+
## 2. `npx wyndlass-app upload`
|
|
19
|
+
|
|
20
|
+
From the project's folder, with the address and every header the answer gave:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npx wyndlass-app upload "<address>" dist/<id>-<version>.zip --header "Content-Type: application/zip" --header "<name>: <value>"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
It answers `<zip>: uploaded (<n> bytes). Import it with app.import.` Never read the zip into the conversation or pass its bytes to a tool. A refused PUT (an expired address, a header left out): call `app.prepareImport` again.
|
|
27
|
+
|
|
28
|
+
## 3. `app.import`
|
|
29
|
+
|
|
30
|
+
`{node, upload}`: the same place, and the upload's id. It answers one of:
|
|
31
|
+
- **"Imported <id> <version> as med_…, version <n>."** `med_…` is the app's item, and `<n>` the item's version. Then what is next:
|
|
32
|
+
- "It is approved with its import": a later version that asks for nothing more than the approved one before it. Screens play it once its checks pass;
|
|
33
|
+
- "It is held for review": an app's first version, a version that asks for more (player control, a new sensor, audio, a new network host), or one the place's media review holds. It gives the CMS link to the app.
|
|
34
|
+
- **"The workspace has <id> <version> already, with these bytes":** nothing new was made. It says where that version stands.
|
|
35
|
+
- **A refusal:**
|
|
36
|
+
- "… was imported already, with other bytes. A version never changes": raise `version` in `app.json`, pack, and start again at step 1;
|
|
37
|
+
- "… is under com.wyndlass., which is Wyndlass's own": change the id;
|
|
38
|
+
- "… a new version of it needs media_item.edit on it": the person lacks the role where the app is. Tell them;
|
|
39
|
+
- "The file has not finished uploading": the PUT did not complete. Upload again.
|
|
40
|
+
|
|
41
|
+
## 4. `app.list`
|
|
42
|
+
|
|
43
|
+
Lists Wyndlass's apps, then the organization's own: each with its item (`med_…`), and its newest versions, each:
|
|
44
|
+
- **"being checked, then approved with its import"** or **"…, then held for review":** Wyndlass is scanning and checking it. Call `app.list` again in a little while;
|
|
45
|
+
- **"refused by its checks: <reason>":** fix the cause ([contract.md](contract.md), Limits), raise `version`, pack and import again;
|
|
46
|
+
- **"held for review: a person approves it in the CMS":** give the person the link `app.import` gave, and say why it is held. Wait for them; never approve it yourself;
|
|
47
|
+
- **"approved":** screens may play it;
|
|
48
|
+
- **"rejected in review: <reason>":** tell the person the reason. Change what it says, raise `version`, and import again.
|
|
49
|
+
|
|
50
|
+
## 5. Place it: `layout.save` and `layout.check`
|
|
51
|
+
|
|
52
|
+
First `guide.read` with topic `layouts`, sections `elements` and `apps`: the layout document, the app element, and its props.
|
|
53
|
+
|
|
54
|
+
- **An app element:** `{"type": "app", "id": "queue", "frame": {…}, "app": {"id": "med_…"}, "props": {…}}`, with the item from `app.list`. `props` are the definition's params; leave one out for its default. An imported app needs no `app.use`.
|
|
55
|
+
- **A new layout:** `layout.save` with `node` (a place the person names), `name` and `document`.
|
|
56
|
+
- **A change to a layout:** `layout.get`, then `layout.save` with `media`, the whole document and `expectedVersion`.
|
|
57
|
+
- **`dryRun: true`** checks a document without saving it.
|
|
58
|
+
- **`layout.check`** gives its load against each screen type's budget and its timeline. Tell the person what it says.
|
|
59
|
+
|
|
60
|
+
A held version can be placed, and previewed in the designer, which runs it in its frame. Screens play it once it is approved and checked; until then they play the newest version before it that can play, or leave the app out.
|
|
61
|
+
|
|
62
|
+
## 6. Schedule it, only with the person's yes: `campaign.save` and `campaign.publish`
|
|
63
|
+
|
|
64
|
+
Ask what should play where, and when. Never at the organization's top place unless the person says so.
|
|
65
|
+
|
|
66
|
+
- **`campaign.save`:** `node`, `name`, `tier` (`STANDARD` for most content), `items` (the layout's `med_…`), and the person's times: `flightStart`, `flightEnd`, and a `when`. Screens keep playing what they had until it is published.
|
|
67
|
+
- **`campaign.publish`:** the first call only previews: how many screens it reaches, and a `previewHash`. Tell the person what goes live where. Call it again with `confirm: true` and that `previewHash` only once they agree. An approval policy may hold it for a person.
|
|
68
|
+
- **To stop it:** `campaign.pause`.
|
|
69
|
+
|
|
70
|
+
## A new version
|
|
71
|
+
|
|
72
|
+
1. Raise `version` in `app.json` (semver: 1.0.0 to 1.1.0), then `npm run pack`.
|
|
73
|
+
2. Import it: steps 1 to 4.
|
|
74
|
+
3. **Layouts that name no version** (`app: {"id": "med_…"}`) take the new one once it is approved and checked.
|
|
75
|
+
4. **A layout that pins one** (`app: {"id": "med_…", "version": <n>}`, the item's version) keeps it. To move it, `layout.save` with the new item version, after showing the person the change.
|
|
76
|
+
|
|
77
|
+
## When a screen doesn't show it
|
|
78
|
+
|
|
79
|
+
`device.explainPlay` for the screen says what it plays and why, with notes such as a held version and the one playing for it, or "this screen's player is too old for apps you imported". Such a screen leaves imported apps out, and plays the rest of the layout, until its player is updated.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# The templates and the project
|
|
2
|
+
|
|
3
|
+
`npx @wyndlass/app-sdk create <folder> --template react|vanilla --id <id> --name "<name>"` makes a working app. Without `--template` it is `vanilla`; without `--id`, `com.example.<folder>`; without `--name`, a name made from the id. The element's name is the id's last part, with `-app` added when that part has no hyphen.
|
|
4
|
+
|
|
5
|
+
## The project
|
|
6
|
+
|
|
7
|
+
| File | What it is |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `app.json` | The definition: `id`, `version` (starts at 0.1.0), `name`, `element`, `description`, `params`, `data`, and, when it asks for more, `capabilities`. [contract.md](contract.md) lists every field. |
|
|
10
|
+
| `src/app.tsx` (react) or `src/app.ts` (vanilla) | The module. Its default export is the element's class. |
|
|
11
|
+
| `src/app.css` | The app's styles. They apply only inside the app's shadow root. |
|
|
12
|
+
| `src/env.d.ts` | Types for what the source imports: CSS as text, images as data URLs. |
|
|
13
|
+
| `package.json` | The scripts `preview`, `check`, `pack` and `typecheck`, and `@wyndlass/app-sdk`. |
|
|
14
|
+
| `tsconfig.json` | TypeScript's options: ES2020, strict. |
|
|
15
|
+
| `README.md`, `AGENTS.md` | The commands, and the rules screens hold the app to. |
|
|
16
|
+
| `.gitignore` | Leaves out `node_modules/` and `dist/`. |
|
|
17
|
+
|
|
18
|
+
`npm run pack` writes `dist/<id>-<version>.zip` and its source map, `dist/<id>-<version>.js.map`, beside it. Only the zip is imported.
|
|
19
|
+
|
|
20
|
+
## What the template app does
|
|
21
|
+
|
|
22
|
+
Two pages, eight seconds each: a heading and the time, then the weather for the screen's place. Its params are one of each of four kinds: `heading` (string), `accent` (color), `units` (enum: metric or imperial) and `seconds` (boolean). It reads one data source, `weather`.
|
|
23
|
+
|
|
24
|
+
- **react:** `function Board()` renders both pages and hides the one not showing. `useNow()` gives the time, `usePaging(2, 8000)` the page, `useData("weather", …)` the weather (the kept answer at once, then the service's), and `useApp()` the props, the locale and the time zone. `ready()` is called once there is an answer or an error.
|
|
25
|
+
- **vanilla:** `class App extends WyndlassApp`. `mount()` builds the shadow DOM once and calls `ready()` when the screen kept a weather answer. `draw(epochMs)` shows the page for that moment, asks for the weather every ten minutes, then calls `ready()`. `paging()` says the two pages.
|
|
26
|
+
|
|
27
|
+
## Making it the person's app
|
|
28
|
+
|
|
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
|
+
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.
|
|
32
|
+
4. **Say `ready()`** as soon as the app has something to show, within 5 s: the kept answer, a default, or the failure.
|
|
33
|
+
5. **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.
|
|
34
|
+
6. **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.
|
|
35
|
+
7. **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)).
|
|
36
|
+
8. **Delete what the template drew** that the person did not ask for, its weather included, and its `data` entry with it.
|
|
37
|
+
|
|
38
|
+
Each source change redraws the preview. `npm run typecheck` and `npm run check` find mistakes before the person sees them.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Agent instructions: __APP_NAME__
|
|
2
|
+
|
|
3
|
+
An app for Wyndlass's screens, on `@wyndlass/app-sdk` and React. Its README says what it is; these are the rules.
|
|
4
|
+
|
|
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.
|
|
7
|
+
- **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.
|
|
8
|
+
- **Data:** only through `useData(source, params)`, and only the sources `app.json`'s `data` lists. Pages: `usePaging(count, ms)`.
|
|
9
|
+
- **Styles:** `src/app.css`, passed as `css`, never a `<style>`.
|
|
10
|
+
- **No `localStorage`** for anything that matters, **no secrets** in params or the module, and **network hosts declared** in `capabilities.network`.
|
|
11
|
+
- **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.
|
|
12
|
+
- **Before importing:** `npm run check`, then `npm run pack`. Raise `version` in `app.json` for each new version.
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
- **Placing:** read `guide.read` (topic `layouts`, sections `elements` and `apps`), then `layout.save` with the app's item from `app.list`, and `layout.check`, at a place the person names.
|
|
16
|
+
- **Scheduling, only with the person's yes:** `campaign.save` at the place and times they give, then `campaign.publish`, which previews first: confirm only once they agree. Never at the organization's top place unless they say so.
|
|
17
|
+
- **Every step in detail** is in the skill: `npx wyndlass-app skill install` puts it in `.claude/skills/wyndlass-app/`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# __APP_NAME__
|
|
2
|
+
|
|
3
|
+
An app for Wyndlass's screens (`__APP_ID__`), built on [`@wyndlass/app-sdk`](https://www.npmjs.com/package/@wyndlass/app-sdk).
|
|
4
|
+
|
|
5
|
+
- `app.json` is its definition: its id, version, name, element, params and the data it reads.
|
|
6
|
+
- `src/app.tsx` is its module: a React component, made the element's class by `reactApp()`, the default export. `src/app.css` is its styles.
|
|
7
|
+
|
|
8
|
+
## Commands
|
|
9
|
+
|
|
10
|
+
- `npm run preview`: the app as screens run it, at http://127.0.0.1:5199/, drawn again as you change it. Its controls set the screen's size, the params, the clock, enter and exit, the time zone, the screen's facts and each data source's answer, and give the app tables from CSV or JSON, the people at the screen and the player's events where it uses them. `npm run preview -- --frame` runs it in the frame the CMS gives apps, where it has no storage.
|
|
11
|
+
- `npm run check`: the checks Wyndlass makes when you import it.
|
|
12
|
+
- `npm run pack`: `dist/__APP_ID__-<version>.zip`, the package to import, after the checks.
|
|
13
|
+
- `npm run typecheck`: TypeScript's check.
|
|
14
|
+
- `npx wyndlass-app skill install`: the skill for a coding agent such as Claude Code, in `.claude/skills/`. It builds, previews, packs, imports and schedules the app with you.
|
|
15
|
+
|
|
16
|
+
Import the package through the Wyndlass MCP (`app.prepareImport`, `npx wyndlass-app upload`, then `app.import`) or in the CMS (Content, Library, Import an app). A person who may approve media there reviews the app's first version, and any that asks for more than the one before it, before screens play it. Raise `version` in `app.json` for each new one.
|
|
17
|
+
|
|
18
|
+
## What screens enforce
|
|
19
|
+
|
|
20
|
+
- The time comes from `useNow()`, the layout's clock, never `Date.now()`.
|
|
21
|
+
- `ready()` within 5 s of the app's start.
|
|
22
|
+
- No `localStorage` for anything that matters, and no secrets in params or the module: both are readable.
|
|
23
|
+
- Data only from the sources `app.json` declares (`weather`, `news`, `image` or `calendar`), and the network hosts declared truthfully in `capabilities.network`: a person approves them.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "__APP_ID__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"name": "__APP_NAME__",
|
|
5
|
+
"element": "__ELEMENT__",
|
|
6
|
+
"description": "A heading and the time on one page, the weather on the next.",
|
|
7
|
+
"params": {
|
|
8
|
+
"heading": { "type": "string", "default": "Good morning", "label": "Heading" },
|
|
9
|
+
"accent": { "type": "color", "default": "#4f8cff", "label": "Accent" },
|
|
10
|
+
"units": { "type": "enum", "values": ["metric", "imperial"], "default": "metric", "label": "Units" },
|
|
11
|
+
"seconds": { "type": "boolean", "default": false, "label": "Show seconds" }
|
|
12
|
+
},
|
|
13
|
+
"data": ["weather"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PACKAGE__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"preview": "wyndlass-app preview",
|
|
8
|
+
"check": "wyndlass-app check",
|
|
9
|
+
"pack": "wyndlass-app pack",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@wyndlass/app-sdk": "__SDK__",
|
|
14
|
+
"react": "^19.0.0",
|
|
15
|
+
"react-dom": "^19.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/react": "^19.0.0",
|
|
19
|
+
"@types/react-dom": "^19.0.0",
|
|
20
|
+
"typescript": "^5.9.3"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
:host { display: block; width: 100%; height: 100%; color: #ffffff; font-family: system-ui, sans-serif; }
|
|
2
|
+
.page { width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: calc(var(--h) * .04); }
|
|
3
|
+
.page[hidden] { display: none; }
|
|
4
|
+
h1 { margin: 0; font-size: min(calc(var(--h) * .14), calc(var(--w) * .08)); font-weight: 600; color: var(--accent); }
|
|
5
|
+
time { font-size: min(calc(var(--h) * .3), calc(var(--w) * .16)); font-variant-numeric: tabular-nums; }
|
|
6
|
+
.temperature { font-size: min(calc(var(--h) * .3), calc(var(--w) * .16)); }
|
|
7
|
+
.condition, .none { font-size: min(calc(var(--h) * .1), calc(var(--w) * .05)); opacity: .85; }
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
// Everything it draws comes from the layout's clock (useNow()), so the designer's seek and pause show the right moment.
|
|
3
|
+
import { useEffect, useMemo } from "react";
|
|
4
|
+
import { color, type weather } from "@wyndlass/app-sdk";
|
|
5
|
+
import { reactApp, useApp, useData, useNow, usePaging } from "@wyndlass/app-sdk/react";
|
|
6
|
+
import css from "./app.css";
|
|
7
|
+
|
|
8
|
+
/** How long each of the two pages shows. */
|
|
9
|
+
const PAGE_MS = 8_000;
|
|
10
|
+
|
|
11
|
+
function Board() {
|
|
12
|
+
const app = useApp();
|
|
13
|
+
const now = useNow();
|
|
14
|
+
const page = usePaging(2, PAGE_MS);
|
|
15
|
+
// The weather for the screen's place, or Winnipeg's where there is none (the designer's sample).
|
|
16
|
+
const place = app.place ?? { lat: 49.9, lon: -97.14 };
|
|
17
|
+
const { answer, error } = useData<weather.Weather>("weather", { lat: place.lat, lon: place.lon, units: app.prop("units", "metric") });
|
|
18
|
+
const time = useMemo(() => new Intl.DateTimeFormat(app.locale, { timeZone: app.timeZone, hour: "numeric", minute: "2-digit",
|
|
19
|
+
...(app.prop("seconds", false) ? { second: "2-digit" } : {}) }), [app]);
|
|
20
|
+
// Ready once there is something to show: the kept answer at once, else the service's answer or its failure.
|
|
21
|
+
useEffect(() => { if (answer || error) app.ready(); }, [app, answer, error]);
|
|
22
|
+
const w = answer?.data;
|
|
23
|
+
return (
|
|
24
|
+
<div style={{ width: "100%", height: "100%", ["--accent" as string]: color(app.prop("accent", "#4f8cff"), "#4f8cff") }}>
|
|
25
|
+
<section className="page" hidden={page !== 0}>
|
|
26
|
+
<h1>{app.prop("heading", "Good morning")}</h1>
|
|
27
|
+
<time>{time.format(now)}</time>
|
|
28
|
+
</section>
|
|
29
|
+
<section className="page" hidden={page !== 1}>
|
|
30
|
+
{w ? <>
|
|
31
|
+
<div className="temperature">{Math.round(w.current.temperature)}°{w.units === "imperial" ? "F" : "C"}</div>
|
|
32
|
+
<div className="condition">{w.current.condition.replace(/-/g, " ")}</div>
|
|
33
|
+
</> : <div className="none">The weather is on its way.</div>}
|
|
34
|
+
</section>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default reactApp(Board, { css });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// A CSS file the source imports is its text, for css(): pack bundles it so.
|
|
2
|
+
declare module "*.css" {
|
|
3
|
+
const css: string;
|
|
4
|
+
export default css;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// An image the source imports is a data URL, inlined by pack: for an <img>'s src or a style set from code.
|
|
8
|
+
declare module "*.png" { const url: string; export default url; }
|
|
9
|
+
declare module "*.jpg" { const url: string; export default url; }
|
|
10
|
+
declare module "*.jpeg" { const url: string; export default url; }
|
|
11
|
+
declare module "*.gif" { const url: string; export default url; }
|
|
12
|
+
declare module "*.webp" { const url: string; export default url; }
|
|
13
|
+
declare module "*.svg" { const url: string; export default url; }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Agent instructions: __APP_NAME__
|
|
2
|
+
|
|
3
|
+
An app for Wyndlass's screens, on `@wyndlass/app-sdk`. Its README says what it is; these are the rules.
|
|
4
|
+
|
|
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
|
+
- **Time:** from `now()` or `draw`'s `epochMs`, never `Date.now()`, so a seek or a pause shows the right moment.
|
|
7
|
+
- **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()`.
|
|
8
|
+
- **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)`.
|
|
9
|
+
- **Styles:** `this.css(text)`, never a `<style>`. A CSS file the source imports is its text.
|
|
10
|
+
- **No `localStorage`** for anything that matters, **no secrets** in params or the module, and **network hosts declared** in `capabilities.network`.
|
|
11
|
+
- **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.
|
|
12
|
+
- **Before importing:** `npm run check`, then `npm run pack`. Raise `version` in `app.json` for each new version.
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
- **Placing:** read `guide.read` (topic `layouts`, sections `elements` and `apps`), then `layout.save` with the app's item from `app.list`, and `layout.check`, at a place the person names.
|
|
16
|
+
- **Scheduling, only with the person's yes:** `campaign.save` at the place and times they give, then `campaign.publish`, which previews first: confirm only once they agree. Never at the organization's top place unless they say so.
|
|
17
|
+
- **Every step in detail** is in the skill: `npx wyndlass-app skill install` puts it in `.claude/skills/wyndlass-app/`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# __APP_NAME__
|
|
2
|
+
|
|
3
|
+
An app for Wyndlass's screens (`__APP_ID__`), built on [`@wyndlass/app-sdk`](https://www.npmjs.com/package/@wyndlass/app-sdk).
|
|
4
|
+
|
|
5
|
+
- `app.json` is its definition: its id, version, name, element, params and the data it reads.
|
|
6
|
+
- `src/app.ts` is its module: the element's class, the default export. `src/app.css` is its styles.
|
|
7
|
+
|
|
8
|
+
## Commands
|
|
9
|
+
|
|
10
|
+
- `npm run preview`: the app as screens run it, at http://127.0.0.1:5199/, drawn again as you change it. Its controls set the screen's size, the params, the clock, enter and exit, the time zone, the screen's facts and each data source's answer, and give the app tables from CSV or JSON, the people at the screen and the player's events where it uses them. `npm run preview -- --frame` runs it in the frame the CMS gives apps, where it has no storage.
|
|
11
|
+
- `npm run check`: the checks Wyndlass makes when you import it.
|
|
12
|
+
- `npm run pack`: `dist/__APP_ID__-<version>.zip`, the package to import, after the checks.
|
|
13
|
+
- `npm run typecheck`: TypeScript's check.
|
|
14
|
+
- `npx wyndlass-app skill install`: the skill for a coding agent such as Claude Code, in `.claude/skills/`. It builds, previews, packs, imports and schedules the app with you.
|
|
15
|
+
|
|
16
|
+
Import the package through the Wyndlass MCP (`app.prepareImport`, `npx wyndlass-app upload`, then `app.import`) or in the CMS (Content, Library, Import an app). A person who may approve media there reviews the app's first version, and any that asks for more than the one before it, before screens play it. Raise `version` in `app.json` for each new one.
|
|
17
|
+
|
|
18
|
+
## What screens enforce
|
|
19
|
+
|
|
20
|
+
- The time comes from `now()` (the `epochMs` `draw` is given), never `Date.now()`.
|
|
21
|
+
- `ready()` within 5 s of the app's start.
|
|
22
|
+
- No `localStorage` for anything that matters, and no secrets in params or the module: both are readable.
|
|
23
|
+
- Data only from the sources `app.json` declares (`weather`, `news`, `image` or `calendar`), and the network hosts declared truthfully in `capabilities.network`: a person approves them.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "__APP_ID__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"name": "__APP_NAME__",
|
|
5
|
+
"element": "__ELEMENT__",
|
|
6
|
+
"description": "A heading and the time on one page, the weather on the next.",
|
|
7
|
+
"params": {
|
|
8
|
+
"heading": { "type": "string", "default": "Good morning", "label": "Heading" },
|
|
9
|
+
"accent": { "type": "color", "default": "#4f8cff", "label": "Accent" },
|
|
10
|
+
"units": { "type": "enum", "values": ["metric", "imperial"], "default": "metric", "label": "Units" },
|
|
11
|
+
"seconds": { "type": "boolean", "default": false, "label": "Show seconds" }
|
|
12
|
+
},
|
|
13
|
+
"data": ["weather"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PACKAGE__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"preview": "wyndlass-app preview",
|
|
8
|
+
"check": "wyndlass-app check",
|
|
9
|
+
"pack": "wyndlass-app pack",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@wyndlass/app-sdk": "__SDK__"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.9.3"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
:host { display: block; width: 100%; height: 100%; color: #ffffff; font-family: system-ui, sans-serif; }
|
|
2
|
+
.page { width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: calc(var(--h) * .04); }
|
|
3
|
+
.page[hidden] { display: none; }
|
|
4
|
+
h1 { margin: 0; font-size: min(calc(var(--h) * .14), calc(var(--w) * .08)); font-weight: 600; color: var(--accent); }
|
|
5
|
+
time { font-size: min(calc(var(--h) * .3), calc(var(--w) * .16)); font-variant-numeric: tabular-nums; }
|
|
6
|
+
.temperature { font-size: min(calc(var(--h) * .3), calc(var(--w) * .16)); }
|
|
7
|
+
.condition, .none { font-size: min(calc(var(--h) * .1), calc(var(--w) * .05)); opacity: .85; }
|