@nervur-org/dock 0.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.
Files changed (285) hide show
  1. package/GETTING_STARTED.md +128 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +6 -0
  4. package/README.md +37 -0
  5. package/api/quo-api.md +196 -0
  6. package/api/route.ts +227 -0
  7. package/beings/GLOSSARY.md +118 -0
  8. package/beings/WORLDS.md +567 -0
  9. package/beings/avatar.ts +101 -0
  10. package/beings/carry.ts +159 -0
  11. package/beings/clock.ts +552 -0
  12. package/beings/courier.ts +107 -0
  13. package/beings/desk.ts +128 -0
  14. package/beings/doorbell.ts +129 -0
  15. package/beings/envoy.ts +218 -0
  16. package/beings/index.ts +16 -0
  17. package/beings/join.ts +51 -0
  18. package/beings/link.ts +104 -0
  19. package/beings/look.ts +139 -0
  20. package/beings/porter.ts +43 -0
  21. package/beings/presence.ts +39 -0
  22. package/beings/quo-app-bells.md +280 -0
  23. package/beings/quo-dock.md +1942 -0
  24. package/beings/setup.ts +45 -0
  25. package/beings/side.ts +60 -0
  26. package/beings/user.ts +459 -0
  27. package/beings/way.ts +65 -0
  28. package/cli/bundles.ts +20 -0
  29. package/cli/client.ts +73 -0
  30. package/cli/daemon.ts +581 -0
  31. package/cli/estate/Caddyfile +33 -0
  32. package/cli/estate/quo.service +35 -0
  33. package/cli/estate/systems.quo.plist +44 -0
  34. package/cli/estate.ts +48 -0
  35. package/cli/folder.ts +27 -0
  36. package/cli/oauth.ts +32 -0
  37. package/cli/pilot.ts +38 -0
  38. package/cli/quo-dock.md +1942 -0
  39. package/cli/quo.ts +203 -0
  40. package/cli/stand.ts +160 -0
  41. package/dist/api/route.d.ts +21 -0
  42. package/dist/api/route.js +193 -0
  43. package/dist/beings/avatar.d.ts +32 -0
  44. package/dist/beings/avatar.js +100 -0
  45. package/dist/beings/carry.d.ts +12 -0
  46. package/dist/beings/carry.js +174 -0
  47. package/dist/beings/clock.d.ts +161 -0
  48. package/dist/beings/clock.js +528 -0
  49. package/dist/beings/courier.d.ts +50 -0
  50. package/dist/beings/courier.js +96 -0
  51. package/dist/beings/desk.d.ts +53 -0
  52. package/dist/beings/desk.js +135 -0
  53. package/dist/beings/doorbell.d.ts +78 -0
  54. package/dist/beings/doorbell.js +133 -0
  55. package/dist/beings/envoy.d.ts +54 -0
  56. package/dist/beings/envoy.js +211 -0
  57. package/dist/beings/index.d.ts +14 -0
  58. package/dist/beings/index.js +16 -0
  59. package/dist/beings/join.d.ts +15 -0
  60. package/dist/beings/join.js +42 -0
  61. package/dist/beings/link.d.ts +14 -0
  62. package/dist/beings/link.js +91 -0
  63. package/dist/beings/look.d.ts +30 -0
  64. package/dist/beings/look.js +104 -0
  65. package/dist/beings/porter.d.ts +14 -0
  66. package/dist/beings/porter.js +41 -0
  67. package/dist/beings/presence.d.ts +8 -0
  68. package/dist/beings/presence.js +21 -0
  69. package/dist/beings/setup.d.ts +3 -0
  70. package/dist/beings/setup.js +39 -0
  71. package/dist/beings/side.d.ts +32 -0
  72. package/dist/beings/side.js +37 -0
  73. package/dist/beings/user.d.ts +315 -0
  74. package/dist/beings/user.js +462 -0
  75. package/dist/beings/way.d.ts +19 -0
  76. package/dist/beings/way.js +28 -0
  77. package/dist/cli/bundles.d.ts +5 -0
  78. package/dist/cli/bundles.js +17 -0
  79. package/dist/cli/client.d.ts +15 -0
  80. package/dist/cli/client.js +58 -0
  81. package/dist/cli/daemon.d.ts +50 -0
  82. package/dist/cli/daemon.js +584 -0
  83. package/dist/cli/estate/Caddyfile +33 -0
  84. package/dist/cli/estate/quo.service +35 -0
  85. package/dist/cli/estate/systems.quo.plist +44 -0
  86. package/dist/cli/estate.d.ts +5 -0
  87. package/dist/cli/estate.js +50 -0
  88. package/dist/cli/folder.d.ts +2 -0
  89. package/dist/cli/folder.js +25 -0
  90. package/dist/cli/oauth.d.ts +2 -0
  91. package/dist/cli/oauth.js +32 -0
  92. package/dist/cli/pilot.d.ts +5 -0
  93. package/dist/cli/pilot.js +22 -0
  94. package/dist/cli/quo.d.ts +2 -0
  95. package/dist/cli/quo.js +201 -0
  96. package/dist/cli/stand.d.ts +37 -0
  97. package/dist/cli/stand.js +110 -0
  98. package/dist/dock/agents.d.ts +102 -0
  99. package/dist/dock/agents.js +105 -0
  100. package/dist/dock/apns.d.ts +27 -0
  101. package/dist/dock/apns.js +98 -0
  102. package/dist/dock/console.d.ts +6 -0
  103. package/dist/dock/console.js +20 -0
  104. package/dist/dock/dialers.d.ts +56 -0
  105. package/dist/dock/dialers.js +75 -0
  106. package/dist/dock/faculty.d.ts +24 -0
  107. package/dist/dock/faculty.js +142 -0
  108. package/dist/dock/fcm.d.ts +13 -0
  109. package/dist/dock/fcm.js +77 -0
  110. package/dist/dock/grants.d.ts +41 -0
  111. package/dist/dock/grants.js +99 -0
  112. package/dist/dock/http2.d.ts +4 -0
  113. package/dist/dock/http2.js +68 -0
  114. package/dist/dock/index.d.ts +41 -0
  115. package/dist/dock/index.js +113 -0
  116. package/dist/dock/local.d.ts +13 -0
  117. package/dist/dock/local.js +44 -0
  118. package/dist/dock/location.d.ts +19 -0
  119. package/dist/dock/location.js +11 -0
  120. package/dist/dock/notifications.d.ts +38 -0
  121. package/dist/dock/notifications.js +60 -0
  122. package/dist/dock/push.d.ts +79 -0
  123. package/dist/dock/push.js +132 -0
  124. package/dist/dock/service.d.ts +4 -0
  125. package/dist/dock/service.js +107 -0
  126. package/dist/dock/timer.d.ts +52 -0
  127. package/dist/dock/timer.js +149 -0
  128. package/dist/dock/webpush.d.ts +12 -0
  129. package/dist/dock/webpush.js +144 -0
  130. package/dist/harbor/browser.d.ts +15 -0
  131. package/dist/harbor/browser.js +82 -0
  132. package/dist/harbor/capacitor.d.ts +28 -0
  133. package/dist/harbor/capacitor.js +218 -0
  134. package/dist/harbor/disk.d.ts +27 -0
  135. package/dist/harbor/disk.js +249 -0
  136. package/dist/harbor/edge/alarm.d.ts +10 -0
  137. package/dist/harbor/edge/alarm.js +38 -0
  138. package/dist/harbor/edge/edge.d.ts +54 -0
  139. package/dist/harbor/edge/edge.js +370 -0
  140. package/dist/harbor/edge/exercise.d.ts +7 -0
  141. package/dist/harbor/edge/exercise.js +232 -0
  142. package/dist/harbor/edge/given.d.ts +26 -0
  143. package/dist/harbor/edge/given.js +42 -0
  144. package/dist/harbor/edge/platform.d.ts +39 -0
  145. package/dist/harbor/edge/storage.d.ts +30 -0
  146. package/dist/harbor/edge/storage.js +180 -0
  147. package/dist/harbor/edge/worker.d.ts +17 -0
  148. package/dist/harbor/edge/worker.js +67 -0
  149. package/dist/harbor/files.d.ts +15 -0
  150. package/dist/harbor/files.js +220 -0
  151. package/dist/harbor/http.d.ts +19 -0
  152. package/dist/harbor/http.js +177 -0
  153. package/dist/harbor/idb.d.ts +15 -0
  154. package/dist/harbor/idb.js +0 -0
  155. package/dist/harbor/quo.d.ts +10 -0
  156. package/dist/harbor/quo.js +42 -0
  157. package/dist/harbor/resolve.d.ts +16 -0
  158. package/dist/harbor/resolve.js +17 -0
  159. package/dist/harbor/seal.d.ts +3 -0
  160. package/dist/harbor/seal.js +25 -0
  161. package/dist/harbor/sealed.d.ts +35 -0
  162. package/dist/harbor/sealed.js +162 -0
  163. package/dist/harbor/tauri.d.ts +16 -0
  164. package/dist/harbor/tauri.js +59 -0
  165. package/dist/harbor/ward.d.ts +9 -0
  166. package/dist/harbor/ward.js +15 -0
  167. package/dist/human/app.d.ts +32 -0
  168. package/dist/human/app.js +527 -0
  169. package/dist/human/apps.d.ts +10 -0
  170. package/dist/human/apps.js +29 -0
  171. package/dist/human/dom.d.ts +2 -0
  172. package/dist/human/dom.js +15 -0
  173. package/dist/human/door.d.ts +19 -0
  174. package/dist/human/door.js +92 -0
  175. package/dist/human/guest.d.ts +3 -0
  176. package/dist/human/guest.js +33 -0
  177. package/dist/human/html.d.ts +55 -0
  178. package/dist/human/html.js +451 -0
  179. package/dist/human/local.d.ts +7 -0
  180. package/dist/human/local.js +10 -0
  181. package/dist/human/mark.d.ts +8 -0
  182. package/dist/human/mark.js +47 -0
  183. package/dist/human/move.d.ts +17 -0
  184. package/dist/human/move.js +90 -0
  185. package/dist/human/screen.d.ts +19 -0
  186. package/dist/human/screen.js +119 -0
  187. package/dist/human/style.d.ts +2 -0
  188. package/dist/human/style.js +60 -0
  189. package/dist/human/tab.bundle.txt +4775 -0
  190. package/dist/human/tab.d.ts +15 -0
  191. package/dist/human/tab.js +409 -0
  192. package/dist/human/tree.d.ts +50 -0
  193. package/dist/human/tree.js +157 -0
  194. package/dist/human/wake.d.ts +34 -0
  195. package/dist/human/wake.js +179 -0
  196. package/dist/human/web.d.ts +33 -0
  197. package/dist/human/web.js +184 -0
  198. package/dist/human/worker.bundle.txt +3868 -0
  199. package/dist/human/worker.d.ts +1 -0
  200. package/dist/human/worker.js +81 -0
  201. package/dist/human/worlds.d.ts +24 -0
  202. package/dist/human/worlds.js +99 -0
  203. package/dist/mcp/agent.d.ts +10 -0
  204. package/dist/mcp/agent.js +78 -0
  205. package/dist/mcp/allow.d.ts +20 -0
  206. package/dist/mcp/allow.js +88 -0
  207. package/dist/mcp/http.d.ts +27 -0
  208. package/dist/mcp/http.js +92 -0
  209. package/dist/mcp/oauth.d.ts +91 -0
  210. package/dist/mcp/oauth.js +241 -0
  211. package/dist/mcp/route.d.ts +15 -0
  212. package/dist/mcp/route.js +42 -0
  213. package/dist/mcp/runner.d.ts +45 -0
  214. package/dist/mcp/runner.js +99 -0
  215. package/dist/mcp/server.d.ts +11 -0
  216. package/dist/mcp/server.js +102 -0
  217. package/dock/agents.ts +121 -0
  218. package/dock/apns.ts +138 -0
  219. package/dock/console.ts +23 -0
  220. package/dock/dialers.ts +85 -0
  221. package/dock/faculty.ts +148 -0
  222. package/dock/fcm.ts +103 -0
  223. package/dock/grants.ts +111 -0
  224. package/dock/http2.ts +70 -0
  225. package/dock/index.ts +133 -0
  226. package/dock/local.ts +48 -0
  227. package/dock/location.ts +22 -0
  228. package/dock/notifications.ts +67 -0
  229. package/dock/push.ts +161 -0
  230. package/dock/quo-app-bells.md +280 -0
  231. package/dock/quo-dock.md +1942 -0
  232. package/dock/service.ts +100 -0
  233. package/dock/timer.ts +174 -0
  234. package/dock/webpush.ts +182 -0
  235. package/harbor/browser.ts +97 -0
  236. package/harbor/capacitor.ts +223 -0
  237. package/harbor/disk.ts +237 -0
  238. package/harbor/edge/alarm.ts +48 -0
  239. package/harbor/edge/edge.ts +407 -0
  240. package/harbor/edge/exercise.ts +241 -0
  241. package/harbor/edge/given.ts +106 -0
  242. package/harbor/edge/platform.d.ts +39 -0
  243. package/harbor/edge/storage.ts +179 -0
  244. package/harbor/edge/text.d.ts +15 -0
  245. package/harbor/edge/worker.ts +70 -0
  246. package/harbor/edge/wrangler.toml +30 -0
  247. package/harbor/files.ts +221 -0
  248. package/harbor/http.ts +188 -0
  249. package/harbor/idb.ts +0 -0
  250. package/harbor/quo-harbor.md +850 -0
  251. package/harbor/quo.ts +44 -0
  252. package/harbor/resolve.ts +31 -0
  253. package/harbor/seal.ts +26 -0
  254. package/harbor/sealed.ts +183 -0
  255. package/harbor/tauri.ts +79 -0
  256. package/harbor/ward.ts +28 -0
  257. package/human/app.ts +580 -0
  258. package/human/apps.ts +47 -0
  259. package/human/dom.ts +21 -0
  260. package/human/door.ts +113 -0
  261. package/human/guest.ts +34 -0
  262. package/human/html.ts +490 -0
  263. package/human/local.ts +30 -0
  264. package/human/mark.ts +53 -0
  265. package/human/move.ts +122 -0
  266. package/human/quo-human.md +1042 -0
  267. package/human/screen.ts +136 -0
  268. package/human/style.ts +63 -0
  269. package/human/tab.bundle.txt +4775 -0
  270. package/human/tab.ts +446 -0
  271. package/human/tree.ts +217 -0
  272. package/human/wake.ts +191 -0
  273. package/human/web.ts +259 -0
  274. package/human/worker.bundle.txt +3868 -0
  275. package/human/worker.ts +124 -0
  276. package/human/worlds.ts +127 -0
  277. package/mcp/agent.ts +94 -0
  278. package/mcp/allow.ts +131 -0
  279. package/mcp/http.ts +92 -0
  280. package/mcp/oauth.ts +270 -0
  281. package/mcp/quo-mcp.md +343 -0
  282. package/mcp/route.ts +45 -0
  283. package/mcp/runner.ts +136 -0
  284. package/mcp/server.ts +101 -0
  285. package/package.json +121 -0
@@ -0,0 +1,10 @@
1
+ export function local(ward, key) {
2
+ // A named ask names it; the empty ask says nothing at all, which is how
3
+ // the owner's ask says the empty ask too.
4
+ const one = (method, args = {}) => ward.ask('ask', method === undefined ? { being: key } : { being: key, method, args });
5
+ return {
6
+ tools: async () => (await one()),
7
+ call: (name, args = {}) => one(name, args),
8
+ ears: new Set(),
9
+ };
10
+ }
@@ -0,0 +1,8 @@
1
+ export declare const RING = "<circle cx=\"32\" cy=\"32\" r=\"22\" stroke-dasharray=\"110 30\" transform=\"rotate(-60 32 32)\"/><circle cx=\"32\" cy=\"32\" r=\"5\" fill=\"currentColor\" stroke=\"none\"/>";
2
+ export declare const MARK_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"4\" stroke-linecap=\"round\"><circle cx=\"32\" cy=\"32\" r=\"22\" stroke-dasharray=\"110 30\" transform=\"rotate(-60 32 32)\"/><circle cx=\"32\" cy=\"32\" r=\"5\" fill=\"currentColor\" stroke=\"none\"/></svg>";
3
+ export declare const MARK: string;
4
+ export declare const ICON_GROUND = "#3b6ef5";
5
+ export declare const ICON_MARK = "#fbfaf7";
6
+ export declare function markLayer(fraction?: number): string;
7
+ export declare function groundLayer(): string;
8
+ export declare function iconSVG(radius?: number): string;
@@ -0,0 +1,47 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The mark: a ring with a gap and a point at the centre, one being's voice
3
+ // reaching another's door. It is the one mark every Quo app wears, on the
4
+ // door page, on the desk app's dock icon and on the phone's home screen,
5
+ // and it is drawn once here so those can never drift apart.
6
+ //
7
+ // The glyph is strokes on nothing, coloured by whoever draws it: the door
8
+ // page inherits `currentColor` and the app icons are given the two colours
9
+ // below. `app/icon/build.ts` is what makes the icons of it.
10
+ export const RING = '<circle cx="32" cy="32" r="22" stroke-dasharray="110 30" transform="rotate(-60 32 32)"/><circle cx="32" cy="32" r="5" fill="currentColor" stroke="none"/>';
11
+ // The mark as a page uses it: strokes in the colour of the text around it.
12
+ export const MARK_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round">${RING}</svg>`;
13
+ // And as a data URI, since a look carries a logo that way and no request
14
+ // for an image ever leaves a page.
15
+ export const MARK = 'data:image/svg+xml;base64,' + btoa(MARK_SVG);
16
+ // The app icon's two colours, the dock's own accent and paper: an icon is
17
+ // the product's palette or it is somebody else's product.
18
+ export const ICON_GROUND = '#3b6ef5';
19
+ export const ICON_MARK = '#fbfaf7';
20
+ // The mark alone on nothing, at the size a layer wants it. Android draws an
21
+ // icon in two layers and crops both to whatever shape the phone likes, a
22
+ // circle among them, so its foreground is the mark small enough to survive
23
+ // the crop, roughly the middle half, and its background is the ground.
24
+ export function markLayer(fraction = 0.5) {
25
+ const scale = fraction;
26
+ const shift = (64 - 64 * scale) / 2;
27
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><g transform="translate(${shift} ${shift}) scale(${scale})" fill="none" stroke="${ICON_MARK}" stroke-width="4" stroke-linecap="round" color="${ICON_MARK}">${RING}</g></svg>`;
28
+ }
29
+ export function groundLayer() {
30
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" fill="${ICON_GROUND}"/></svg>`;
31
+ }
32
+ // The mark as an app icon, on its ground. `radius` is the corner in the
33
+ // viewBox's own units: 0 for a full square, which is what iOS and Android
34
+ // want since the platform masks it, and a rounded one for macOS, which
35
+ // masks nothing and where every icon in the dock is a rounded square with
36
+ // air around it.
37
+ export function iconSVG(radius = 0) {
38
+ const inset = radius === 0 ? 0 : 5;
39
+ const size = 64 - inset * 2;
40
+ const glyph = radius === 0 ? '' : ` transform="translate(${inset} ${inset}) scale(${size / 64})"`;
41
+ return [
42
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">',
43
+ `<rect x="${inset}" y="${inset}" width="${size}" height="${size}" rx="${radius}" fill="${ICON_GROUND}"/>`,
44
+ `<g${glyph} fill="none" stroke="${ICON_MARK}" stroke-width="4" stroke-linecap="round" color="${ICON_MARK}">${RING}</g>`,
45
+ '</svg>',
46
+ ].join('');
47
+ }
@@ -0,0 +1,17 @@
1
+ import type { Harbor } from '@nervur-org/nervur/harbor';
2
+ import type { Invitation } from '@nervur-org/nervur';
3
+ import { type World } from './worlds.ts';
4
+ export declare const COURIER = "courier";
5
+ export type Giving = {
6
+ offer: string;
7
+ invitation: Invitation;
8
+ moved: Promise<string>;
9
+ close(): Promise<void>;
10
+ };
11
+ export declare function giveWorld(harbor: Harbor, world: World, at: string): Promise<Giving>;
12
+ export declare function takeWorld(harbor: Harbor, invitation: Invitation): Promise<{
13
+ took: string;
14
+ world: World;
15
+ } | {
16
+ error: string;
17
+ }>;
@@ -0,0 +1,90 @@
1
+ import { arithmetic } from '@nervur-org/nervur/ward';
2
+ import { Courier, MOVER } from '../beings/courier.js';
3
+ import { link } from '../beings/link.js';
4
+ import { localName } from './worlds.js';
5
+ const { hex, unhex } = arithmetic;
6
+ // The ward a device's courier lives in, minted when a move starts and gone
7
+ // when it ends: nothing of a move outlives it.
8
+ export const COURIER = 'courier';
9
+ const KEY = 'courier';
10
+ async function courier(harbor) {
11
+ const ward = harbor.wards.get(COURIER) ?? (await harbor.create(COURIER, 'me'));
12
+ if (!ward.being(KEY)) {
13
+ const out = (await ward.ask('boot', { key: KEY, class: 'Courier' }));
14
+ if (out.error)
15
+ throw new Error(`no courier on this device: ${out.error}`);
16
+ }
17
+ return ward.being(KEY);
18
+ }
19
+ const away = async (harbor) => {
20
+ const c = harbor.wards.get(COURIER)?.being(KEY);
21
+ if (c) {
22
+ c.giving = undefined;
23
+ c.gone = undefined;
24
+ }
25
+ await harbor.drop(COURIER);
26
+ };
27
+ // This device, offering one world to whichever device takes the offer.
28
+ // `at` is the rendezvous this device dials, which is the world's own `quo.`
29
+ // route: the other device has no reach to anything yet and this is what it
30
+ // dials to answer.
31
+ export async function giveWorld(harbor, world, at) {
32
+ const name = localName(world.pk);
33
+ if (!harbor.wards.has(name))
34
+ throw new Error('this device does not hold that world');
35
+ const c = await courier(harbor);
36
+ const invitation = await c.invite(MOVER);
37
+ if (!invitation) {
38
+ await away(harbor);
39
+ throw new Error('this device is already offering a world');
40
+ }
41
+ let landed = () => { };
42
+ const moved = new Promise((ok) => (landed = ok));
43
+ c.giving = async () => {
44
+ const ward = harbor.wards.get(name);
45
+ if (!ward)
46
+ return { error: 'this device no longer holds that world' };
47
+ await ward.save();
48
+ const kept = await harbor.store.load(name);
49
+ if (!kept)
50
+ return { error: 'this device no longer holds that world' };
51
+ // The partition crosses as the values it is: a ward's partition is
52
+ // I-JSON already, since that is all a ward ever writes into one.
53
+ return { world: world.pk, at: world.at, name: world.name, seed: hex(kept.seed), partition: kept.partition, record: kept.record };
54
+ };
55
+ c.gone = async () => {
56
+ const gone = await harbor.drop(name);
57
+ if (!gone)
58
+ return { error: 'this device no longer holds that world' };
59
+ landed(world.pk);
60
+ return { deleted: world.pk };
61
+ };
62
+ await harbor.wards.get(COURIER).save();
63
+ return { offer: link(at, invitation), invitation, moved, close: () => away(harbor) };
64
+ }
65
+ // This device, taking the world the other one offers. The harbor must
66
+ // already be dialing the reach note in the offer: dialing is the shell's,
67
+ // since only a shell knows which harbor its terrain gave it.
68
+ export async function takeWorld(harbor, invitation) {
69
+ const c = await courier(harbor);
70
+ try {
71
+ const moving = await c.fetch(invitation);
72
+ if ('error' in moving)
73
+ return moving;
74
+ const name = localName(moving.world);
75
+ if (harbor.wards.has(name))
76
+ return { error: 'this device is already in that world' };
77
+ const hosted = await harbor.adopt(name, { seed: unhex(moving.seed), partition: moving.partition, record: moving.record });
78
+ // it is here: the other device may let go, and if that word never
79
+ // arrives the human is told, since two copies is what the next move
80
+ // has to be told about
81
+ const let_go = await c.landed();
82
+ return 'error' in let_go ? { error: `it is here, but the other device still has it: ${let_go.error}` } : { took: hosted.pk, world: { pk: moving.world, at: moving.at, name: moving.name } };
83
+ }
84
+ catch (e) {
85
+ return { error: e instanceof Error ? e.message : String(e) };
86
+ }
87
+ finally {
88
+ await away(harbor);
89
+ }
90
+ }
@@ -0,0 +1,19 @@
1
+ import { type Serving, type Subject } from '../beings/side.ts';
2
+ import type { Invitation } from '@nervur-org/nervur';
3
+ import { type Model, type Raw } from './html.ts';
4
+ export type Surface = {
5
+ show(html: string): void;
6
+ onSubmit: ((ask: string, raw: Raw) => void) | undefined;
7
+ };
8
+ export type Options = {
9
+ after?: () => Promise<void>;
10
+ notice?: string;
11
+ admit?: (invitation: Invitation) => Promise<void>;
12
+ focus?: string;
13
+ at?: string;
14
+ ended?: () => Promise<void>;
15
+ };
16
+ export declare function screenSide(avatar: Subject, surface: Surface, options?: Options): Promise<Serving & {
17
+ model: Model;
18
+ refresh(): Promise<void>;
19
+ }>;
@@ -0,0 +1,119 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The human side: one avatar, one surface. The surface is the least a
3
+ // screen can be: it shows a page and hands back a submitted form, and that
4
+ // is all this side asks of a terrain. A DOM is one surface; a test is
5
+ // another. The side keeps the model, speaks it as a page after every change
6
+ // through `html.ts`, calls the avatar when a form comes back, and re-asks
7
+ // her describe after each call, so a digest that moved is a page that
8
+ // moved. A push is appended and shown the moment it lands.
9
+ import { digest } from '@nervur-org/nervur';
10
+ import { word } from '../beings/side.js';
11
+ import { isInvitation } from '@nervur-org/nervur';
12
+ import { page, values, hintFor, mine, asks, standing, stands } from './html.js';
13
+ import { sanitiseTree, answersIn } from './tree.js';
14
+ import { groups, sanitise } from '../beings/look.js';
15
+ import { isSilence, isWord } from '@nervur-org/nervur';
16
+ export async function screenSide(avatar, surface, options = {}) {
17
+ const after = options.after ?? (async () => { });
18
+ const notice = options.notice ?? '';
19
+ const model = { blueprint: null, look: {}, tree: null, pages: {}, notice, standing: 'in', answers: {}, pushes: [], ...(options.focus !== undefined ? { focus: options.focus } : {}), ...(options.at !== undefined ? { at: options.at } : {}) };
20
+ let seen = null;
21
+ const show = () => surface.show(page(model));
22
+ // Her describe, again: the page follows the digest.
23
+ const refresh = async () => {
24
+ const bp = await avatar.tools();
25
+ if ('error' in bp && !('asks' in bp)) {
26
+ // the last page she gave stays, with the answers under it, and the
27
+ // notice says where the human stands now: her relation ended, the
28
+ // world away for the moment, or her own error, told apart because
29
+ // only one of the three is over
30
+ model.standing = standing(bp.error);
31
+ model.notice = stands(bp.error);
32
+ if (model.standing === 'ended')
33
+ await options.ended?.();
34
+ }
35
+ else {
36
+ model.standing = 'in';
37
+ const d = await digest(bp);
38
+ const moved = d !== seen;
39
+ if (moved) {
40
+ seen = d;
41
+ model.blueprint = bp;
42
+ for (const k of Object.keys(model.answers))
43
+ if (!bp.asks.some((a) => a.name === k))
44
+ delete model.answers[k];
45
+ // her look, once per digest: how she wants her page painted, and what each ask is
46
+ if (bp.asks.some((a) => a.name === 'look')) {
47
+ const l = await avatar.call('look');
48
+ model.look = isSilence(l) || isWord(l) ? {} : sanitise(l);
49
+ }
50
+ else
51
+ model.look = {};
52
+ }
53
+ // her page, after every ask: a tree of values from what she holds now, or nothing and her asks
54
+ // are forms. A cell moved is a page moved, and her describe need not have; so the answers the
55
+ // page shows are run again with it, and the page never stands more than one ask behind her
56
+ const trees = () => [model.tree ? answersIn(model.tree) : [], ...Object.entries(model.pages).map(([id, t]) => (t ? answersIn(t).map((n) => `${id}-${n}`) : []))].flat();
57
+ const shows = new Set(trees());
58
+ const tree = async (name) => {
59
+ const t = await avatar.call(name);
60
+ return isSilence(t) || isWord(t) ? null : sanitiseTree(t);
61
+ };
62
+ model.tree = bp.asks.some((a) => a.name === 'page') ? await tree('page') : null;
63
+ // and the page of every standing she carries, the same way, painted in that standing's section;
64
+ // a name a carried page uses is one of that standing's asks, carried, and nothing else
65
+ model.pages = {};
66
+ for (const [id, g] of Object.entries(groups(bp)))
67
+ if (typeof g.page === 'string' && bp.asks.some((a) => a.name === g.page))
68
+ model.pages[id] = await tree(g.page);
69
+ const wanted = new Set(trees());
70
+ for (const n of shows)
71
+ if (wanted.has(n))
72
+ delete model.answers[n];
73
+ // a read-only ask that needs nothing typed is run on her behalf once per digest, and one her page
74
+ // shows the answer of after every ask, so the page opens and stays with what it shows
75
+ for (const a of bp.asks) {
76
+ // What the side fills for itself is not something a human has to
77
+ // type, so an ask whose only required argument is the moment has
78
+ // nothing left to ask and runs like one with no arguments at all.
79
+ const needs = asks(a).length > 0;
80
+ const run = wanted.has(a.name) || (moved && hintFor(bp, model.look, a.name).readOnly);
81
+ if (run && !needs && !(a.name in model.answers))
82
+ model.answers[a.name] = word(await avatar.call(a.name, mine(a)));
83
+ }
84
+ }
85
+ await after();
86
+ show();
87
+ };
88
+ surface.onSubmit = (name, raw) => void submit(name, raw);
89
+ const submit = async (name, raw) => {
90
+ const ask = model.blueprint?.asks.find((a) => a.name === name);
91
+ if (!ask)
92
+ return;
93
+ const v = values(ask, raw);
94
+ if ('error' in v) {
95
+ model.answers[name] = { word: 'error', value: { error: v.error } };
96
+ return show();
97
+ }
98
+ const out = await avatar.call(name, v.args);
99
+ await after();
100
+ if (options.admit && isInvitation(out))
101
+ return options.admit(out);
102
+ model.answers[name] = word(out);
103
+ await refresh();
104
+ };
105
+ const ear = (object) => {
106
+ model.pushes.push(object);
107
+ show();
108
+ };
109
+ avatar.ears.add(ear);
110
+ await refresh();
111
+ return {
112
+ model,
113
+ refresh,
114
+ close: async () => {
115
+ avatar.ears.delete(ear);
116
+ surface.onSubmit = undefined;
117
+ },
118
+ };
119
+ }
@@ -0,0 +1,2 @@
1
+ export declare const CSS: string;
2
+ export declare const BARE: string;
@@ -0,0 +1,60 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The stylesheet: the kit, and then what the screen alone has.
3
+ //
4
+ // One stylesheet for every screen there is, whatever carries it: the web
5
+ // route serves it beside the tab's bundle, and the desk app's window
6
+ // carries it in its own. A screen that looked like another product on
7
+ // another terrain would be two designs to keep and one of them always
8
+ // behind. It is one string and not four files because a screen builds its
9
+ // own page and has nowhere to put a link.
10
+ //
11
+ // The kit brings the tokens, its own light and dark face, the reset, the
12
+ // baseline, the bare controls and every piece a page is made of. The
13
+ // painter emits those pieces and nothing else, so what follows is small:
14
+ // the page's one column, and the few things the shells alone have, the
15
+ // door, a local being's section, moving a world, the daemon's state line.
16
+ // Nothing below mints a colour or a size; every one is a slot or a scale,
17
+ // so a face swapped for another swaps the screen with it. A far being's
18
+ // look sets the kit's slots on her own section, and the kit does the rest.
19
+ import { CSS as KIT, TOKENS, BASE, COMPONENTS } from '@nervur-org/ui/css';
20
+ const SCREEN = [
21
+ // One column that is a phone first and a desk after: the same page, more
22
+ // air. The insets are the phone's: an app fills the screen, corners and
23
+ // camera and all, so the first line of the page would otherwise sit under
24
+ // the clock. `env()` is zero everywhere else, so a tab and a window are
25
+ // laid out by the gutter alone.
26
+ 'body{max-width:var(--nv-page-w-md);margin-inline:auto;padding-inline:calc(var(--nv-page-gutter) + env(safe-area-inset-left)) calc(var(--nv-page-gutter) + env(safe-area-inset-right));padding-block:env(safe-area-inset-top) calc(var(--nv-space-8) + env(safe-area-inset-bottom))}',
27
+ 'main{display:flex;flex-direction:column;gap:var(--nv-space-4)}',
28
+ // a subsection holding a form and the answer under it, and the sections
29
+ // of one being's page after another
30
+ 'main>.nv-stack>section+section,main>.nv-stack>section+.nv-standing,main>.nv-stack>.nv-standing+section{margin-block-start:var(--nv-space-3)}',
31
+ // a far being's logo beside her name, and the way to her own page
32
+ 'img.logo{height:var(--nv-icon-lg)}a.open,a.back{text-decoration:none;font-weight:var(--nv-weight-medium)}a.back::before{content:"\\2190 "}',
33
+ // A place the side vouches for reads as a place to go. One it will not
34
+ // vouch for reads as what it is, an address the human may copy and weigh,
35
+ // and it is deliberately not underlined and not the accent: it must never
36
+ // look like something the page is willing to stand behind. A link that
37
+ // leaves the world wears the origin it goes to, out of the anchor's own
38
+ // data, so a human reads where she is going on the page rather than in a
39
+ // status bar she may never look at.
40
+ 'a.link.out::after{content:" \\2197 " attr(data-origin);font-size:var(--nv-text-sm);color:var(--nv-color-fg-muted)}span.link.away{border:1px dashed var(--nv-color-border-strong)}',
41
+ // a being of the world running in the tab: her own section under the far page
42
+ 'section.local{border-block-start:1px solid var(--nv-color-border);margin-block:var(--nv-space-5) 0;padding-block-start:var(--nv-space-3)}',
43
+ // moving a world to another device, and leaving one: quiet, at the foot
44
+ // of the page, and the offer wrapping rather than running off the side,
45
+ // since it is one long word
46
+ '.move,section.held{display:flex;flex-direction:column;gap:var(--nv-space-2);align-items:flex-start;border-block-start:1px solid var(--nv-color-border);margin-block:var(--nv-space-7) 0;padding-block-start:var(--nv-space-3);font-size:var(--nv-text-sm);color:var(--nv-color-fg-muted)}.move .invitation,section.held .invitation{font-family:var(--nv-font-mono);font-size:var(--nv-text-xs);overflow-wrap:anywhere}',
47
+ 'section.held .worlds{display:flex;flex-wrap:wrap;gap:var(--nv-space-2)}',
48
+ // a foot the shell hid stays hidden: a class that lays it out would otherwise beat the attribute
49
+ '.move[hidden],section.held[hidden]{display:none}',
50
+ // the door page: one thing on air, vertically centred in the window
51
+ 'main[data-door]{min-height:70vh;justify-content:center}main[data-door] .nv-picture{width:var(--nv-space-7);height:var(--nv-space-7);color:var(--nv-color-accent)}',
52
+ // the desk window: the state of the daemon behind the page, a line, and nothing when all is well
53
+ 'p.state:empty{display:none}',
54
+ ].join('');
55
+ export const CSS = `${KIT}\n${SCREEN}`;
56
+ // The same screen for a world that wears a design of its own: the kit
57
+ // without its own face, since the kit's contract says a surface loads the
58
+ // default face or a design's and never both, and the design's stylesheet
59
+ // is linked after it by the route that serves it.
60
+ export const BARE = `${TOKENS}\n${BASE}\n${COMPONENTS}\n${SCREEN}`;