@miakapp/cli 4.0.0-alpha.0 → 4.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +151 -35
  2. package/assets/agent-guide.md +232 -79
  3. package/assets/design/OFL-Manrope.txt +93 -0
  4. package/assets/design/components.css +139 -0
  5. package/assets/design/fonts.css +9 -0
  6. package/assets/design/gallery.html +667 -0
  7. package/assets/design/guide.md +77 -0
  8. package/assets/design/tokens.css +185 -0
  9. package/assets/starter/app/app/README.md +74 -0
  10. package/assets/starter/app/app/main.ts +124 -0
  11. package/assets/starter/app/app/miakapp.ts +156 -0
  12. package/assets/starter/app/app/theme.ts +8 -0
  13. package/assets/starter/app/tsconfig.json +23 -0
  14. package/bin/miakapp.js +5 -1
  15. package/dist/agent-pack.d.ts +11 -3
  16. package/dist/agent-pack.d.ts.map +1 -1
  17. package/dist/agent-pack.js +26 -10
  18. package/dist/agent-pack.js.map +1 -1
  19. package/dist/contexts.d.ts +59 -0
  20. package/dist/contexts.d.ts.map +1 -0
  21. package/dist/contexts.js +388 -0
  22. package/dist/contexts.js.map +1 -0
  23. package/dist/control-plane.d.ts +14 -0
  24. package/dist/control-plane.d.ts.map +1 -1
  25. package/dist/control-plane.js +33 -1
  26. package/dist/control-plane.js.map +1 -1
  27. package/dist/internal/names.d.ts +5 -0
  28. package/dist/internal/names.d.ts.map +1 -1
  29. package/dist/internal/names.js +6 -0
  30. package/dist/internal/names.js.map +1 -1
  31. package/dist/main.d.ts +22 -5
  32. package/dist/main.d.ts.map +1 -1
  33. package/dist/main.js +673 -60
  34. package/dist/main.js.map +1 -1
  35. package/dist/mcp.d.ts +7 -1
  36. package/dist/mcp.d.ts.map +1 -1
  37. package/dist/mcp.js +195 -26
  38. package/dist/mcp.js.map +1 -1
  39. package/dist/pairing.d.ts +27 -0
  40. package/dist/pairing.d.ts.map +1 -0
  41. package/dist/pairing.js +148 -0
  42. package/dist/pairing.js.map +1 -0
  43. package/dist/project.d.ts +3 -1
  44. package/dist/project.d.ts.map +1 -1
  45. package/dist/project.js +33 -6
  46. package/dist/project.js.map +1 -1
  47. package/dist/publication.d.ts +20 -5
  48. package/dist/publication.d.ts.map +1 -1
  49. package/dist/publication.js +58 -16
  50. package/dist/publication.js.map +1 -1
  51. package/dist/starter.d.ts +17 -0
  52. package/dist/starter.d.ts.map +1 -0
  53. package/dist/starter.js +72 -0
  54. package/dist/starter.js.map +1 -0
  55. package/dist/terminal.d.ts +10 -0
  56. package/dist/terminal.d.ts.map +1 -0
  57. package/dist/terminal.js +70 -0
  58. package/dist/terminal.js.map +1 -0
  59. package/dist/version.d.ts +15 -0
  60. package/dist/version.d.ts.map +1 -0
  61. package/dist/version.js +15 -0
  62. package/dist/version.js.map +1 -0
  63. package/package.json +6 -4
@@ -0,0 +1,77 @@
1
+ # Designing a Miakapp home
2
+
3
+ Miakapp gives you a starting point, not a framework: design tokens, four
4
+ themes, a handful of CSS recipes and example screens. Take what helps, change
5
+ anything, or make the home's own look with its residents. Nothing here is
6
+ required by `miakapp check` or by the frame.
7
+
8
+ ## What ships with this CLI
9
+
10
+ The files sit next to this guide, in the design directory printed at the end:
11
+
12
+ | File | What it is |
13
+ |---|---|
14
+ | `tokens.css` | `--mk-*` variables (color, glass, radius, type, space, motion) for four themes |
15
+ | `components.css` | Recipes on top of the tokens: glass, device tile, scene, switch, level slider, stale banner, tab bar |
16
+ | `fonts.css` | Manrope as a `data:` URL, ready for the frame, which has no network (OFL, see `OFL-Manrope.txt`) |
17
+ | `gallery.html` | Example screens built only with the files above: home, room, energy, wall tablet. Open it in a browser, switch themes, read its source |
18
+
19
+ `miakapp init --starter app` writes all three stylesheets into `app/theme.ts`
20
+ (`THEME_CSS`) and already uses them, so the first build looks like Miakapp.
21
+
22
+ ## Themes
23
+
24
+ Set `data-theme` on `<html>` (or on any element, to theme one part):
25
+
26
+ | Theme | Use |
27
+ |---|---|
28
+ | `minuit` | Night blue and gold from the Miakapp logo. Dark mode, wall tablets after dark. The house favourite. |
29
+ | `lumiere` | Day. White glass over peach, mint and lilac. Light mode and the default. |
30
+ | `miel` | Warm honey. Cosy, evening homes. |
31
+ | `craie` | Flat, no blur, high contrast. Old or slow tablets, low vision. |
32
+
33
+ Follow the resident: `home.theme()` is `light`, `dark` or `system`, and
34
+ `home.onThemeChange()` tells you when it changes. The starter maps dark to
35
+ Minuit and light to Lumière; a wall tablet may stay on Minuit.
36
+
37
+ A home's own theme is one more `[data-theme="…"]` block redefining the same
38
+ variables. Keep text contrast at 4.5:1 or more, including secondary text on
39
+ glass; check dark themes twice (dark blue on darker blue is easy to miss).
40
+
41
+ ## Principles
42
+
43
+ 1. **The home first, not the devices.** Organize by room and by moment of the
44
+ day (evening, leaving, bedtime). Use the words residents use; never show a
45
+ state path or a device identifier.
46
+ 2. **What is on glows.** One strong signal per screen: the golden `--mk-on`
47
+ glow of a light, plug or scene that is on. Everything else stays calm.
48
+ 3. **One gradient per screen.** `--mk-accent-gradient` is a signature for one
49
+ title or one key number, not for every heading.
50
+ 4. **Say when the home goes quiet.** A stale value is dimmed (`is-stale`), a
51
+ banner says since when, controls are disabled. A command in flight shows
52
+ `is-pending` until the next state confirms it; an `outcome_unknown` call is
53
+ never retried.
54
+ 5. **Big fingers, little text.** 44 px targets at least, tabular digits, three
55
+ text sizes per screen. Phones, tablets on a wall and desktops all open the
56
+ same app: test at 360 px wide and at 1280 px.
57
+ 6. **Glass, but only two levels.** A glass panel may hold tiles; tiles do not
58
+ hold more glass. `craie` drops the blur entirely for weak devices.
59
+
60
+ ## The Miakapp bar is not yours
61
+
62
+ Miakapp draws its own bar outside your frame: the home, the resident's other
63
+ homes, their account and sign-in methods. Do not draw a global navigation that
64
+ duplicates it (home switcher, profile, sign out). In-app navigation between
65
+ your own screens (rooms, energy…) is yours; `gallery.html` shows where a
66
+ home's own one or two entries would sit next to Miakapp's.
67
+
68
+ ## In the frame
69
+
70
+ - No network: no web fonts, CDN stylesheets or remote images. Inline them as
71
+ `data:`/`blob:` URLs, like `fonts.css` does. The bundle limit is 2 MiB;
72
+ Manrope costs about 33 KB.
73
+ - Respect `prefers-reduced-motion`; the tokens zero their durations for it.
74
+ - `corner-shape: squircle` gives continuous corners where the browser supports
75
+ it; elsewhere the plain radius applies.
76
+ - The bundle is public to signed-in users who know its digest: the gallery's
77
+ names are examples, real names come from state.
@@ -0,0 +1,185 @@
1
+ /*
2
+ * Miakapp design tokens — a starting palette for home interfaces.
3
+ *
4
+ * Copy it, rename it, change any value: it is yours. Four themes share the
5
+ * same variable names, so a screen written once works in all of them.
6
+ * <html data-theme="lumiere"> day, glass on peach, mint and lilac (default)
7
+ * <html data-theme="miel"> warm honey, cosy evenings
8
+ * <html data-theme="minuit"> night blue and gold, wall tablets after dark
9
+ * <html data-theme="craie"> flat and high-contrast, no blur: old tablets, accessibility
10
+ *
11
+ * The frame has no network: no web font is fetched. "Manrope" is used when the
12
+ * app bundles it as a data: URL; otherwise the system stack takes over.
13
+ */
14
+
15
+ :root,
16
+ [data-theme="lumiere"] {
17
+ color-scheme: light;
18
+
19
+ /* Canvas */
20
+ --mk-bg: #f6f3ee;
21
+ --mk-bg-art:
22
+ radial-gradient(60% 45% at 0% 0%, #ffd2b0 0%, transparent 70%),
23
+ radial-gradient(55% 45% at 100% 10%, #e3dcff 0%, transparent 70%),
24
+ radial-gradient(60% 50% at 80% 100%, #d6f5e3 0%, transparent 70%),
25
+ radial-gradient(40% 35% at 10% 90%, #ffe9a8 0%, transparent 70%);
26
+
27
+ /* Ink */
28
+ --mk-ink: #0a1024;
29
+ --mk-ink-2: rgb(10 16 36 / .62);
30
+ --mk-ink-3: rgb(10 16 36 / .40);
31
+ --mk-hairline: rgb(10 16 36 / .08);
32
+
33
+ /* Glass surfaces */
34
+ --mk-surface: linear-gradient(160deg, rgb(255 255 255 / .74), rgb(255 255 255 / .36));
35
+ --mk-surface-strong: rgb(255 255 255 / .86);
36
+ --mk-surface-sunken: rgb(10 16 36 / .05);
37
+ --mk-selected: rgb(255 255 255 / .9);
38
+ --mk-rim: rgb(255 255 255 / .8);
39
+ --mk-blur: 24px;
40
+ --mk-saturate: 1.8;
41
+ --mk-shadow: 0 1px 0 rgb(255 255 255 / .9) inset, 0 18px 40px -18px rgb(60 40 80 / .28);
42
+
43
+ /* Accent: titles, focus, the one thing that matters on a screen */
44
+ --mk-accent: #e0569b;
45
+ --mk-accent-gradient: linear-gradient(110deg, #ff7a45, #e0569b 55%, #7a5cff);
46
+
47
+ /* Something is on (a light, a plug, a scene running) */
48
+ --mk-on: linear-gradient(150deg, #ffe48a, #ffb547 70%, #ffa63b);
49
+ --mk-on-ink: #3a2300;
50
+ --mk-on-glow: 0 10px 30px -8px rgb(255 166 59 / .65);
51
+
52
+ /* Buttons */
53
+ --mk-button: #0a1024;
54
+ --mk-button-ink: #ffffff;
55
+
56
+ /* Meaning */
57
+ --mk-heat: #ff7a45;
58
+ --mk-cool: #5b8cff;
59
+ --mk-ok: #23a26d;
60
+ --mk-warn: #e58a12;
61
+ --mk-alert: #e5484d;
62
+ }
63
+
64
+ [data-theme="miel"] {
65
+ color-scheme: light;
66
+ --mk-bg: #fff6e8;
67
+ --mk-bg-art:
68
+ radial-gradient(60% 45% at 0% 0%, #ffe08a 0%, transparent 70%),
69
+ radial-gradient(50% 40% at 100% 20%, #ffc59a 0%, transparent 70%),
70
+ radial-gradient(60% 50% at 70% 100%, #ffd9c2 0%, transparent 70%);
71
+ --mk-ink: #1a1433;
72
+ --mk-ink-2: rgb(26 20 51 / .64);
73
+ --mk-ink-3: rgb(26 20 51 / .42);
74
+ --mk-hairline: rgb(26 20 51 / .09);
75
+ --mk-surface: linear-gradient(160deg, rgb(255 252 244 / .78), rgb(255 248 232 / .42));
76
+ --mk-surface-strong: rgb(255 252 244 / .9);
77
+ --mk-surface-sunken: rgb(26 20 51 / .06);
78
+ --mk-rim: rgb(255 255 255 / .75);
79
+ --mk-shadow: 0 1px 0 rgb(255 255 255 / .9) inset, 0 18px 40px -18px rgb(140 70 10 / .3);
80
+ --mk-accent: #ff7448;
81
+ --mk-accent-gradient: linear-gradient(110deg, #ffb41f, #ff9f2e 45%, #ff7448);
82
+ --mk-on: linear-gradient(150deg, #ffd45c, #ff9f2e 75%, #ff7448);
83
+ --mk-on-ink: #2a1300;
84
+ --mk-on-glow: 0 10px 30px -8px rgb(255 116 72 / .55);
85
+ --mk-button: #1a1433;
86
+ --mk-button-ink: #fff6e8;
87
+ }
88
+
89
+ [data-theme="minuit"] {
90
+ color-scheme: dark;
91
+ --mk-bg: #0a1024;
92
+ --mk-bg-art:
93
+ radial-gradient(70% 50% at 50% -10%, rgb(76 107 214 / .55) 0%, transparent 70%),
94
+ radial-gradient(50% 40% at 100% 100%, rgb(29 43 87 / .9) 0%, transparent 70%),
95
+ radial-gradient(40% 35% at 0% 80%, rgb(255 166 59 / .10) 0%, transparent 70%);
96
+ --mk-ink: #f4f1e8;
97
+ --mk-ink-2: rgb(244 241 232 / .76);
98
+ --mk-ink-3: rgb(244 241 232 / .58);
99
+ --mk-hairline: rgb(255 255 255 / .14);
100
+ --mk-surface: linear-gradient(160deg, rgb(255 255 255 / .16), rgb(255 255 255 / .07));
101
+ --mk-surface-strong: rgb(255 255 255 / .16);
102
+ --mk-surface-sunken: rgb(255 255 255 / .11);
103
+ --mk-selected: rgb(255 255 255 / .2);
104
+ --mk-rim: rgb(255 255 255 / .22);
105
+ --mk-saturate: 1.4;
106
+ --mk-shadow: 0 1px 0 rgb(255 255 255 / .12) inset, 0 20px 50px -20px rgb(0 0 0 / .7);
107
+ --mk-accent: #ffdd6e;
108
+ --mk-accent-gradient: linear-gradient(110deg, #fff1b8, #ffdd6e 40%, #ffa63b);
109
+ --mk-on: linear-gradient(150deg, #ffe48a, #ffa63b);
110
+ --mk-on-ink: #0a1024;
111
+ --mk-on-glow: 0 0 34px -6px rgb(255 190 80 / .6);
112
+ --mk-button: linear-gradient(150deg, #ffe48a, #ffa63b);
113
+ --mk-button-ink: #0a1024;
114
+ --mk-heat: #ff9a5c;
115
+ --mk-cool: #7ea2ff;
116
+ --mk-ok: #4fd39a;
117
+ --mk-warn: #ffbf47;
118
+ --mk-alert: #ff6b6f;
119
+ }
120
+
121
+ [data-theme="craie"] {
122
+ color-scheme: light;
123
+ --mk-bg: #eef0f3;
124
+ --mk-bg-art: none;
125
+ --mk-ink: #0d1117;
126
+ --mk-ink-2: #46505c;
127
+ --mk-ink-3: #6b7480;
128
+ --mk-hairline: #d6dbe1;
129
+ --mk-surface: #ffffff;
130
+ --mk-surface-strong: #ffffff;
131
+ --mk-surface-sunken: #e3e7ec;
132
+ --mk-selected: #e3e7ec;
133
+ --mk-rim: #d6dbe1;
134
+ --mk-blur: 0px;
135
+ --mk-saturate: 1;
136
+ --mk-shadow: 0 1px 2px rgb(13 17 23 / .08);
137
+ --mk-accent: #2457e6;
138
+ --mk-accent-gradient: linear-gradient(#0d1117, #0d1117);
139
+ --mk-on: #ffc533;
140
+ --mk-on-ink: #0d1117;
141
+ --mk-on-glow: none;
142
+ --mk-button: #0d1117;
143
+ --mk-button-ink: #ffffff;
144
+ --mk-ok: #12804f;
145
+ --mk-warn: #a85d00;
146
+ --mk-alert: #c4282d;
147
+ }
148
+
149
+ :root {
150
+ /* Type */
151
+ --mk-font: "Manrope", ui-rounded, "SF Pro Rounded", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
152
+ --mk-text-xs: 12px;
153
+ --mk-text-sm: 13px;
154
+ --mk-text: 15px;
155
+ --mk-text-lg: 18px;
156
+ --mk-title: 24px;
157
+ --mk-display: 40px;
158
+ --mk-hero: 64px;
159
+ --mk-tight: -0.035em;
160
+
161
+ /* Shape: continuous corners where the browser supports them */
162
+ --mk-radius-sm: 12px;
163
+ --mk-radius: 22px;
164
+ --mk-radius-lg: 30px;
165
+ --mk-pill: 999px;
166
+
167
+ /* Space: 4 px steps */
168
+ --mk-1: 4px;
169
+ --mk-2: 8px;
170
+ --mk-3: 12px;
171
+ --mk-4: 16px;
172
+ --mk-5: 20px;
173
+ --mk-6: 24px;
174
+ --mk-8: 32px;
175
+
176
+ /* Motion */
177
+ --mk-ease: cubic-bezier(.2, .8, .2, 1);
178
+ --mk-spring: cubic-bezier(.34, 1.56, .64, 1);
179
+ --mk-fast: 140ms;
180
+ --mk-slow: 320ms;
181
+ }
182
+
183
+ @media (prefers-reduced-motion: reduce) {
184
+ :root { --mk-fast: 0ms; --mk-slow: 0ms; }
185
+ }
@@ -0,0 +1,74 @@
1
+ # This home's interface
2
+
3
+ Written by `miakapp init --starter app`. It is a Miakapp **whole-house
4
+ application** (`miakapp.app/1`): your own document, styles and navigation,
5
+ bundled into one classic-script file that Miakapp runs in an isolated frame for
6
+ the home's residents.
7
+
8
+ ```
9
+ miakapp.yaml home, control plane, artifact, and what the app may read and call
10
+ package.json build, typecheck and check scripts
11
+ tsconfig.json strict TypeScript for app/
12
+ app/main.ts the interface — edit this
13
+ app/miakapp.ts the bridge to the home (window.miakapp), copied from this CLI release
14
+ app/theme.ts Miakapp design tokens, themes and recipes (`miakapp docs design`): yours to change
15
+ app/README.md this file
16
+ ```
17
+
18
+ Nothing here depends on an unpublished package or on the MiakAPI source
19
+ repository.
20
+
21
+ ## Prerequisites
22
+
23
+ - **Node.js ≥ 22.9** to run the `miakapp` CLI.
24
+ - **Bun ≥ 1.2.23** to bundle `app/main.ts` (`bun build … --format=iife`). Any
25
+ bundler that emits one classic-script IIFE with no `import()` works too.
26
+ - TypeScript only for `bun run typecheck` (`bun install` fetches it).
27
+
28
+ ## The loop
29
+
30
+ ```bash
31
+ bun install # the pinned @miakapp/cli and TypeScript, for the scripts below
32
+ bun run build # app/main.ts → the artifact named in miakapp.yaml
33
+ bun run check # build, then `miakapp check` validates it offline
34
+ npx miakapp publish # only once the interface is real (see below)
35
+ npx miakapp status # verify the digest you built is the one live
36
+ ```
37
+
38
+ `npx miakapp` runs the CLI that `bun install` pinned in this project; a global
39
+ `miakapp` works the same way.
40
+
41
+ `publish` prints `home_url`: the address residents open. Open it as a member
42
+ would and check it before handing it to anyone. The printed `artifact_url` is
43
+ the raw bundle, never a link to give.
44
+
45
+ ## Before the first publication
46
+
47
+ As generated, the app shows every value the home shares with it — grouped by
48
+ the first segment of its path — and an honest empty state while nothing is
49
+ shared. `app.requires` in `miakapp.yaml` starts empty, so the first build
50
+ shows only that empty state. It invents no value and offers no control. That
51
+ is a starting point, not a deliverable:
52
+
53
+ 1. **Inventory the house first** (`miakapp docs start`, §3): every source, what
54
+ each value means, how fresh it is, who may see it, which actions exist.
55
+ 2. **Declare what the app needs** under `app.requires.state_read` (exact paths
56
+ or `prefix.*`) and `app.requires.call`. The coordinator still decides per
57
+ resident; a path it does not share never reaches the app.
58
+ 3. **Make it a home, not a path list.** Fill `LABELS` with resident-facing
59
+ names, then replace the generic layout with one organized by room or use.
60
+ No technical vocabulary on screen.
61
+ 4. **Add controls only for actions the owner authorized.** Disable them while
62
+ `state.stale` is true. A call that fails with `outcome_unknown` may have
63
+ acted: never retry it, wait for the next state.
64
+
65
+ ## Rules the frame enforces
66
+
67
+ - No network, no cookies or storage, no popups. Bundle images and fonts as
68
+ `data:`/`blob:` URLs. At most 2 MiB, no `import()`, no source map.
69
+ - The bundle is public to signed-in users who know its digest: put no household
70
+ data in it. Read everything from state.
71
+ - Never present stale values as current; never show a reading the home did not
72
+ send.
73
+ - Do not edit `app/miakapp.ts`. It is the canonical bridge source shipped with
74
+ the CLI that generated it, so a newer CLI starter carries any change.
@@ -0,0 +1,124 @@
1
+ /**
2
+ * This home's interface: a Miakapp whole-house application (miakapp.app/1).
3
+ *
4
+ * A starting point, not a finished home. As written it shows exactly what the
5
+ * home shares with this interface — every granted state path, grouped by its
6
+ * first segment, with an honest notice when the view is stale — and nothing
7
+ * else: no invented value, no control. Before publishing it for residents:
8
+ *
9
+ * 1. inventory the house (sources, values, freshness, possible actions);
10
+ * 2. declare the paths and calls you need in miakapp.yaml (app.requires);
11
+ * 3. give every path a resident-facing name in LABELS, and replace this layout
12
+ * with one organized the way people live in the house;
13
+ * 4. add controls only for calls the owner authorized, and never retry one
14
+ * that failed with `outcome_unknown`.
15
+ *
16
+ * You own the whole document: any DOM, CSS, router or library a bundler can
17
+ * inline. `bun run build` emits one classic-script IIFE. The only way to the
18
+ * home is `./miakapp.ts`, the bridge copied from this CLI release.
19
+ *
20
+ * `./theme.ts` is the Miakapp design starting point (`miakapp docs design`):
21
+ * tokens, four themes and component recipes. Minuit follows the resident's
22
+ * dark mode, Lumière the light one. Keep it, restyle it or drop it.
23
+ */
24
+ import { connect, type HomeState, type Theme } from './miakapp.js';
25
+ import { THEME_CSS } from './theme.js';
26
+
27
+ /** Resident-facing names, from your inventory. A path without one shows as the path. */
28
+ const LABELS: Readonly<Record<string, string>> = {};
29
+
30
+ const home = connect();
31
+ const french = home.locale.toLowerCase().startsWith('fr');
32
+ const TEXT = french
33
+ ? {
34
+ empty: 'Rien de cette maison n’est encore partagé avec cette interface.',
35
+ stale: 'Dernières valeurs connues ; en attente de la maison.',
36
+ yes: 'Oui',
37
+ no: 'Non',
38
+ missing: '—',
39
+ }
40
+ : {
41
+ empty: 'Nothing from this home is shared with this interface yet.',
42
+ stale: 'Showing the last known values; waiting for the home.',
43
+ yes: 'Yes',
44
+ no: 'No',
45
+ missing: '—',
46
+ };
47
+
48
+ function element(tag: string, text?: string, className?: string): HTMLElement {
49
+ const node = document.createElement(tag);
50
+ if (text !== undefined) node.textContent = text;
51
+ if (className !== undefined) node.className = className;
52
+ return node;
53
+ }
54
+
55
+ const style = document.createElement('style');
56
+ style.textContent = `${THEME_CSS}
57
+ body { margin: 0; min-height: 100vh; }
58
+ h1 { margin: 0; padding: 28px 20px 4px; }
59
+ main { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); padding: 16px; }
60
+ section { padding: 18px; }
61
+ h2 { margin: 0 0 10px; font-size: var(--mk-text-lg); font-weight: 800; text-transform: capitalize; }
62
+ dl { margin: 0; display: grid; grid-template-columns: 1fr auto; gap: 8px 12px; }
63
+ dt { color: var(--mk-ink-2); }
64
+ dd { margin: 0; font-weight: 700; text-align: end; }
65
+ .mk-stale-banner { margin: 8px 16px 0; }
66
+ .empty { padding: 0 20px; color: var(--mk-ink-2); }
67
+ .is-stale dd { opacity: .55; }
68
+ `;
69
+ document.head.append(style);
70
+
71
+ /** The resident's light or dark preference picks the theme; "system" asks the browser. */
72
+ function applyTheme(preference: Theme): void {
73
+ const dark = preference === 'dark' || (preference === 'system'
74
+ && typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches);
75
+ document.documentElement.dataset['theme'] = dark ? 'minuit' : 'lumiere';
76
+ }
77
+ applyTheme(home.theme());
78
+ home.onThemeChange(applyTheme);
79
+ if (typeof matchMedia === 'function') {
80
+ matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => applyTheme(home.theme()));
81
+ }
82
+ document.body.className = 'mk-app';
83
+
84
+ const title = element('h1', undefined, 'mk-display');
85
+ const notice = document.createElement('p');
86
+ const main = document.createElement('main');
87
+ document.body.append(title, notice, main);
88
+
89
+ function format(value: unknown): string {
90
+ if (value === undefined || value === null) return TEXT.missing;
91
+ if (typeof value === 'boolean') return value ? TEXT.yes : TEXT.no;
92
+ if (typeof value === 'number') return Number.isFinite(value) ? value.toLocaleString(home.locale) : TEXT.missing;
93
+ if (typeof value === 'string') return value;
94
+ return JSON.stringify(value);
95
+ }
96
+
97
+ function render(state: HomeState): void {
98
+ title.textContent = home.name;
99
+ notice.className = state.stale ? 'mk-stale-banner' : '';
100
+ notice.textContent = state.stale ? TEXT.stale : '';
101
+
102
+ const paths = home.paths();
103
+ if (paths.length === 0) {
104
+ main.replaceChildren(element('p', TEXT.empty, 'empty'));
105
+ return;
106
+ }
107
+ const groups = new Map<string, string[]>();
108
+ for (const path of paths) {
109
+ const group = path.split('.')[0] ?? path;
110
+ groups.set(group, [...groups.get(group) ?? [], path]);
111
+ }
112
+ main.replaceChildren(...[...groups].map(([group, members]) => {
113
+ const section = element('section', undefined, state.stale ? 'mk-glass is-stale' : 'mk-glass');
114
+ const list = element('dl');
115
+ for (const path of members) {
116
+ list.append(element('dt', LABELS[path] ?? path), element('dd', format(state.values[path])));
117
+ }
118
+ section.append(element('h2', group), list);
119
+ return section;
120
+ }));
121
+ }
122
+
123
+ home.subscribe(render);
124
+ home.ready();
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Typed access to a Miakapp whole-house application's one window on its home.
3
+ *
4
+ * A `miakapp.app/1` release is a single classic-script bundle that owns its
5
+ * whole document: any DOM, CSS, router or UI library. It runs in an isolated
6
+ * opaque-origin frame with no network and no storage, and the trusted shell
7
+ * installs exactly one object before the bundle runs: `window.miakapp`. This
8
+ * package types that object and adds the few helpers every house needs, so a
9
+ * house reads state and calls functions without reaching for `any`.
10
+ *
11
+ * Nothing here grants anything. What the house may read and call is its
12
+ * release's `requires`, narrowed by the coordinator per resident; a path the
13
+ * resident may not see never reaches the frame.
14
+ */
15
+
16
+ export const APP_ABI = 'miakapp.app/1' as const;
17
+
18
+ export const CALL_ERROR_CODES = [
19
+ 'denied',
20
+ 'unavailable',
21
+ 'failed',
22
+ 'outcome_unknown',
23
+ 'timeout',
24
+ 'busy',
25
+ ] as const;
26
+
27
+ /**
28
+ * Why a call did not complete. `outcome_unknown` means the home may have acted:
29
+ * never retry it, wait for the next state instead.
30
+ */
31
+ export type CallErrorCode = (typeof CALL_ERROR_CODES)[number];
32
+
33
+ export type Theme = 'system' | 'light' | 'dark';
34
+
35
+ export interface HomeState {
36
+ readonly values: Readonly<Record<string, unknown>>;
37
+ readonly revision: number;
38
+ /** True when Miakapp no longer has a current view: show it, never hide it. */
39
+ readonly stale: boolean;
40
+ }
41
+
42
+ /** The object the trusted shell installs as `window.miakapp`. */
43
+ export interface MiakappHost {
44
+ readonly abi: typeof APP_ABI;
45
+ readonly release: string;
46
+ readonly home: { readonly id: string; readonly name: string };
47
+ readonly locale: string;
48
+ readonly theme: string;
49
+ onThemeChange(listener: (theme: string) => void): () => void;
50
+ readonly state: {
51
+ get(path: string): unknown;
52
+ values(): Readonly<Record<string, unknown>>;
53
+ readonly revision: number;
54
+ readonly stale: boolean;
55
+ subscribe(listener: (state: HomeState) => void): () => void;
56
+ };
57
+ readonly can: { read(path: string): boolean; call(name: string): boolean };
58
+ call(name: string, args?: unknown, options?: { timeoutMs?: number }): Promise<unknown>;
59
+ ready(): void;
60
+ }
61
+
62
+ export class MiakappUnavailableError extends Error {
63
+ constructor(message: string) {
64
+ super(message);
65
+ this.name = 'MiakappUnavailableError';
66
+ }
67
+ }
68
+
69
+ export interface Home {
70
+ readonly id: string;
71
+ readonly name: string;
72
+ readonly release: string;
73
+ /** BCP 47 language of the resident, e.g. `fr`. */
74
+ readonly locale: string;
75
+ theme(): Theme;
76
+ onThemeChange(listener: (theme: Theme) => void): () => void;
77
+ /** The current granted view, immutable. */
78
+ state(): HomeState;
79
+ get<T = unknown>(path: string): T | undefined;
80
+ /** Granted paths, optionally under one dotted prefix, sorted. */
81
+ paths(prefix?: string): string[];
82
+ /**
83
+ * Calls `listener` with every new view. With `immediate` (the default) it is
84
+ * also called once now, so a render function can be the only listener.
85
+ */
86
+ subscribe(listener: (state: HomeState) => void, options?: { immediate?: boolean }): () => void;
87
+ canRead(path: string): boolean;
88
+ canCall(name: string): boolean;
89
+ /** Rejects with an error whose `callErrorCode()` is one of `CALL_ERROR_CODES`. */
90
+ call<T = unknown>(name: string, args?: unknown, options?: { timeoutMs?: number }): Promise<T>;
91
+ /** Lifts the shell's loading screen before the script finishes, if you want. */
92
+ ready(): void;
93
+ }
94
+
95
+ function theme(value: string): Theme {
96
+ return value === 'light' || value === 'dark' ? value : 'system';
97
+ }
98
+
99
+ function isHost(value: unknown): value is MiakappHost {
100
+ if (value === null || typeof value !== 'object') return false;
101
+ const candidate = value as Partial<MiakappHost>;
102
+ return candidate.abi === APP_ABI
103
+ && typeof candidate.call === 'function'
104
+ && typeof candidate.state === 'object'
105
+ && typeof candidate.state?.subscribe === 'function';
106
+ }
107
+
108
+ /**
109
+ * Connects to the home this frame was opened for. Throws when not running
110
+ * inside a Miakapp house frame — for example in a unit test or a plain browser
111
+ * tab — so a mistake fails loudly instead of rendering an empty home.
112
+ */
113
+ export function connect(scope: { readonly miakapp?: unknown } = globalThis as { miakapp?: unknown }): Home {
114
+ const host = scope.miakapp;
115
+ if (!isHost(host)) {
116
+ throw new MiakappUnavailableError(
117
+ 'window.miakapp is missing: this bundle runs only inside a Miakapp house frame',
118
+ );
119
+ }
120
+ const snapshot = (): HomeState => ({
121
+ values: host.state.values(),
122
+ revision: host.state.revision,
123
+ stale: host.state.stale,
124
+ });
125
+ return Object.freeze({
126
+ id: host.home.id,
127
+ name: host.home.name,
128
+ release: host.release,
129
+ locale: host.locale,
130
+ theme: () => theme(host.theme),
131
+ onThemeChange: (listener: (value: Theme) => void) => host.onThemeChange((value) => listener(theme(value))),
132
+ state: snapshot,
133
+ get: <T>(path: string) => host.state.get(path) as T | undefined,
134
+ paths: (prefix?: string) => Object.keys(host.state.values())
135
+ .filter((path) => prefix === undefined || path === prefix || path.startsWith(`${prefix}.`))
136
+ .sort(),
137
+ subscribe: (listener: (state: HomeState) => void, options: { immediate?: boolean } = {}) => {
138
+ const unsubscribe = host.state.subscribe(listener);
139
+ if (options.immediate ?? true) listener(snapshot());
140
+ return unsubscribe;
141
+ },
142
+ canRead: (path: string) => host.can.read(path),
143
+ canCall: (name: string) => host.can.call(name),
144
+ call: async <T>(name: string, args?: unknown, options?: { timeoutMs?: number }) => (
145
+ await host.call(name, args ?? null, options ?? {}) as T
146
+ ),
147
+ ready: () => host.ready(),
148
+ });
149
+ }
150
+
151
+ /** The closed reason a call failed, or undefined for anything else. */
152
+ export function callErrorCode(error: unknown): CallErrorCode | undefined {
153
+ if (error === null || typeof error !== 'object' || !('code' in error)) return undefined;
154
+ const code = (error as { code: unknown }).code;
155
+ return (CALL_ERROR_CODES as readonly unknown[]).includes(code) ? code as CallErrorCode : undefined;
156
+ }