@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,92 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The door page: what a stranger sees where nothing lets them in. No
3
+ // invitation in the fragment, no relation in this tab's harbor, and nobody
4
+ // at the ward's door; or a harbor's root with no world to list. It is
5
+ // Quo's page and not the estate's: it says what this is and that a world
6
+ // here is entered by a link someone sends you, and it asks for nothing,
7
+ // since there is nothing a stranger could type that would let them in. A
8
+ // world that wants a face of its own has a public being with a page and a
9
+ // look, or its own page on its own origin; this is the default and nothing
10
+ // more. It is the first page written in the grammar, `tree.ts`, painted
11
+ // with nobody behind it: no ask, no answer, no standing.
12
+ import { paint, nobody } from './tree.js';
13
+ import { escape } from './html.js';
14
+ import { MARK } from './mark.js';
15
+ export function doorTree(at) {
16
+ return {
17
+ kind: 'stack',
18
+ as: 'hero',
19
+ of: [
20
+ { kind: 'image', src: MARK, alt: '' },
21
+ { kind: 'text', text: 'quo', role: 'title' },
22
+ { kind: 'text', text: at.world ? `${at.world} at ${at.host}` : at.host, role: 'label' },
23
+ { kind: 'text', text: at.world ? 'This world is entered by invitation.' : 'The worlds here are entered by invitation.', role: 'lead' },
24
+ { kind: 'text', text: 'An invitation is a link someone sends you. Open it here, and you are in: no account, no password, nothing to type. What you can do inside is what the world shows you, and it is yours to keep on this device.' },
25
+ { kind: 'text', text: 'Nothing on this page asks anything of you. If you were sent here without a link, ask the person who sent you for one.', role: 'quiet' },
26
+ ],
27
+ };
28
+ }
29
+ export function door(at) {
30
+ return `<main data-door>${paint(doorTree(at), nobody)}</main>`;
31
+ }
32
+ // The other page with nobody behind it: what a human sees where this
33
+ // browser already holds the harbor in another tab. A device is one harbor
34
+ // and the lock is what says so, since two harbors over one seed would be
35
+ // two devices claiming to be the same one, each writing the other's wards.
36
+ // So the second tab is a page and not a device, and the page says which of
37
+ // the two it is rather than saying `booting` until she gives up. Nothing
38
+ // here offers to take the lock: the tab that holds it may be the one she
39
+ // is working in.
40
+ export function heldTree(at) {
41
+ return {
42
+ kind: 'stack',
43
+ as: 'hero',
44
+ of: [
45
+ { kind: 'image', src: MARK, alt: '' },
46
+ { kind: 'text', text: 'Already open', role: 'title' },
47
+ { kind: 'text', text: at.world ? `${at.world} at ${at.host}` : at.host, role: 'label' },
48
+ { kind: 'text', text: 'This world is open in another tab of this browser, and one browser is one device here. That tab holds the device; this one is only a page.', role: 'lead' },
49
+ { kind: 'text', text: 'Go back to the tab that has it, or close that tab and reload this one. Nothing is lost either way: what this device is in every world is kept in the browser, not in a tab.' },
50
+ ],
51
+ };
52
+ }
53
+ export function held(at) {
54
+ return `<main data-door>${paint(heldTree(at), nobody)}</main>`;
55
+ }
56
+ // The policy every page Quo writes itself is served under, wherever it is
57
+ // served from. These pages run no script and want none, so nothing but
58
+ // their own form is allowed anywhere, an image is the mark and therefore
59
+ // data, and no referer carries what brought a human here off the page.
60
+ export const POLICY = "default-src 'none'; style-src 'unsafe-inline'; img-src data:; form-action 'self'; base-uri 'none'; frame-ancestors 'none'";
61
+ // A painter for Quo's own pages. It is `nobody` with links painted, and
62
+ // that is sound where the other painter's caution is not: the vouching rule
63
+ // exists because a far being names a place and the side cannot know where
64
+ // she is sending a human. On these pages the dock wrote the address itself,
65
+ // out of what the daemon was configured with, so there is nobody to vouch
66
+ // for. No ask, no answer and no standing, as before.
67
+ export const ours = {
68
+ ...nobody,
69
+ link: (href, text) => `<a class="link" href="${escape(href)}">${escape(text)}</a>`,
70
+ };
71
+ export function landedTree(l) {
72
+ const of = [
73
+ { kind: 'image', src: MARK, alt: '' },
74
+ { kind: 'text', text: l.ok ? 'That is done' : 'That did not finish', role: 'title' },
75
+ { kind: 'text', text: l.world ? `${l.world} at ${l.host}` : l.host, role: 'label' },
76
+ {
77
+ kind: 'text',
78
+ text: l.ok
79
+ ? 'You came back from somewhere this world sent you, and it heard you. Nothing else is needed here.'
80
+ : 'You came back from somewhere this world sent you, and it could not finish. Nothing was taken from you here, and you can try again from the world itself.',
81
+ role: 'lead',
82
+ },
83
+ ];
84
+ if (l.at)
85
+ of.push({ kind: 'link', href: l.at, text: l.world ? `back to ${l.world}` : 'back to this world' });
86
+ else
87
+ of.push({ kind: 'text', text: 'You can close this page.', role: 'quiet' });
88
+ return { kind: 'stack', as: 'hero', of };
89
+ }
90
+ export function landed(l) {
91
+ return `<main data-door>${paint(landedTree(l), ours)}</main>`;
92
+ }
@@ -0,0 +1,3 @@
1
+ import type { Avatar } from '../beings/avatar.ts';
2
+ import type { Subject } from '../beings/side.ts';
3
+ export declare function guest(avatar: Avatar, ward: string): Subject;
@@ -0,0 +1,33 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // A guest at a world's door: the world's public being, whatever class she
3
+ // is, as a subject the screen can render. Her describe for a stranger is the
4
+ // guest page; a form on it is a knock on the public invitation; and an
5
+ // answer that is an invitation is the way in, which the tab takes by
6
+ // joining. Nothing here knows the desk, and nothing needs to: the forms are
7
+ // drawn from whatever asks she has, and what makes one of them the way in
8
+ // is that its answer is an invitation. She may call it anything.
9
+ //
10
+ // The proof way in is the one place a name is needed, since a proof arrives
11
+ // in a link's fragment rather than typed, so there is no form to answer and
12
+ // the shell must knock a named ask. That name is `device`, and it is a
13
+ // convention of the dock's like `hello`, `look` and `page`: a public being
14
+ // of any class takes proofs by writing an ask under that name, and one that
15
+ // writes none has no proof way in and loses nothing else.
16
+ import { isSilence, isWord, wordOf } from '@nervur-org/nervur';
17
+ export function guest(avatar, ward) {
18
+ const at = { ward };
19
+ return {
20
+ tools: async () => {
21
+ const bp = await avatar.knock(at);
22
+ if (isSilence(bp))
23
+ return { error: 'silence' };
24
+ if (isWord(bp))
25
+ return { error: wordOf(bp) };
26
+ if (bp === null || typeof bp !== 'object' || Array.isArray(bp) || !Array.isArray(bp.asks))
27
+ return { error: 'nobody is home' };
28
+ return bp;
29
+ },
30
+ call: (name, args = {}, wanted) => avatar.knock(at, name, args, wanted),
31
+ ears: new Set(),
32
+ };
33
+ }
@@ -0,0 +1,55 @@
1
+ import type { Answer, Ask, Blueprint, Json, JsonObject } from '@nervur-org/nervur';
2
+ import { type Word } from '../beings/side.ts';
3
+ import { hintFor, type Look, type Hint } from '../beings/look.ts';
4
+ import { type Node } from './tree.ts';
5
+ export type Property = {
6
+ type?: string;
7
+ description?: string;
8
+ enum?: Json[];
9
+ format?: string;
10
+ default?: Json;
11
+ };
12
+ export type Schema = {
13
+ properties?: Record<string, Property>;
14
+ required?: string[];
15
+ };
16
+ export declare const escape: (s: string) => string;
17
+ export declare const MOMENT = "moment";
18
+ export declare const mine: (ask: Ask, at?: number) => JsonObject;
19
+ export declare const asks: (ask: Ask) => string[];
20
+ export declare function field(name: string, p: Property, must: boolean): string;
21
+ export declare function form(ask: Ask, h?: Hint): string;
22
+ export type Raw = Record<string, string>;
23
+ export declare function values(ask: Ask, raw: Raw): {
24
+ args: JsonObject;
25
+ } | {
26
+ error: string;
27
+ };
28
+ export declare function view(value: Json, schema?: JsonObject): string;
29
+ export declare function face(w: Word, schema?: JsonObject): string;
30
+ export declare function people(value: Json): string;
31
+ export declare function feed(value: Json): string;
32
+ export type Model = {
33
+ blueprint: Blueprint | null;
34
+ look: Look;
35
+ tree: Node | null;
36
+ pages: Record<string, Node | null>;
37
+ focus?: string;
38
+ at?: string;
39
+ notice: string;
40
+ standing: Standing;
41
+ answers: Record<string, Word>;
42
+ pushes: JsonObject[];
43
+ };
44
+ export type Standing = 'in' | 'ended' | 'away' | 'error';
45
+ export declare function standing(error: string): Standing;
46
+ export declare function stands(error: string): string;
47
+ export declare function refused(answer: Answer): string;
48
+ export declare function title(bp: Blueprint | null, l?: Look): string;
49
+ export declare function look(l: Look | undefined): {
50
+ style: string;
51
+ head: string;
52
+ };
53
+ export declare function ordered(all: Ask[], l: Look | undefined): Ask[];
54
+ export { hintFor };
55
+ export declare function page(m: Model): string;
@@ -0,0 +1,451 @@
1
+ import { SILENCE_TEXT, UNREACHED_TEXT, word, wordText } from '../beings/side.js';
2
+ import { hint, hintFor, sanitise, groups as grouped, presentation } from '../beings/look.js';
3
+ import { paint, titled } from './tree.js';
4
+ import { isInvitation } from '@nervur-org/nervur';
5
+ import { link } from '../beings/link.js';
6
+ export const escape = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c] ?? c);
7
+ const props = (schema) => Object.entries((schema?.properties ?? {}));
8
+ const required = (schema) => new Set((schema?.required ?? []));
9
+ // ---- what a side fills for itself
10
+ //
11
+ // An ask says what its arguments are, and one of them a side can answer
12
+ // better than the human can: the moment. A being may not read a clock, since
13
+ // a being who could would be a being who could not be moved, so she takes the
14
+ // moment as an argument and every side hands her its own. A screen is not a
15
+ // being; it runs on a device, and a device has a clock.
16
+ //
17
+ // So a property whose schema says `format: "moment"` is never drawn as a
18
+ // field and never typed by anybody. The screen fills it, and a read-only ask
19
+ // with nothing left to ask a human then runs on its own and shows a table
20
+ // where a form would have been. Nothing about this is presentation, which is
21
+ // why it is read off the input schema and not off a look: the ask says what
22
+ // the argument is, and each side decides for itself what to do with that.
23
+ export const MOMENT = 'moment';
24
+ export const mine = (ask, at = Date.now()) => Object.fromEntries(props(ask.input).filter(([, p]) => p.format === MOMENT).map(([name]) => [name, at]));
25
+ // What is left for a human once the side has filled its own.
26
+ export const asks = (ask) => [...required(ask.input)].filter((name) => !(name in mine(ask)));
27
+ // ---- a schema as a form
28
+ // A property is one field by its type. A string is a text input, and a
29
+ // format the browser knows, `password`, `date`, `email`, is the input's
30
+ // type. A number is a number input. A boolean is a checkbox. An enum is a
31
+ // select. An object or an array is a box for JSON, because a form has no
32
+ // better face for a value the schema left open. A description is the label.
33
+ export function field(name, p, must) {
34
+ const id = escape(name);
35
+ const label = `<label for="${id}">${id}${must ? ' <b>*</b>' : ''}</label>`;
36
+ const help = p.description ? `<p class="nv-hint">${escape(p.description)}</p>` : '';
37
+ const one = (control) => `<div class="nv-field">${label}${control}${help}</div>`;
38
+ const req = must ? ' required' : '';
39
+ if (Array.isArray(p.enum)) {
40
+ // An enum value is a scalar in every schema a being writes, but the
41
+ // schema comes from her and not from here: an object among the options
42
+ // is shown as what it is rather than as `[object Object]`.
43
+ const text = (v) => (v !== null && typeof v === 'object' ? JSON.stringify(v) : String(v));
44
+ const options = p.enum.map((v) => `<option value="${escape(text(v))}">${escape(text(v))}</option>`).join('');
45
+ return one(`<select id="${id}" name="${id}"${req}>${must ? '' : '<option value=""></option>'}${options}</select>`);
46
+ }
47
+ if (p.type === 'boolean')
48
+ return `<div class="nv-field" data-inline><input type="checkbox" id="${id}" name="${id}" value="true">${label}${help}</div>`;
49
+ if (p.type === 'number' || p.type === 'integer')
50
+ return one(`<input type="number" id="${id}" name="${id}"${p.type === 'integer' ? ' step="1"' : ' step="any"'}${req}>`);
51
+ if (p.type === 'string' || p.type === undefined) {
52
+ const kind = p.format === 'password' || p.format === 'date' || p.format === 'email' || p.format === 'time' ? p.format : 'text';
53
+ return one(`<input type="${kind}" id="${id}" name="${id}"${req}>`);
54
+ }
55
+ return one(`<textarea id="${id}" name="${id}" placeholder="JSON"${req}></textarea>`);
56
+ }
57
+ // The ask as a form: its name is the button, its description the legend's
58
+ // small print, and `data-ask` is how the surface says which ask was sent.
59
+ export function form(ask, h = {}) {
60
+ const must = required(ask.input);
61
+ const own = mine(ask);
62
+ const fields = props(ask.input)
63
+ .filter(([name]) => !(name in own)) // the side fills it; nobody types a moment
64
+ .map(([name, p]) => field(name, p, must.has(name)))
65
+ .join('');
66
+ const label = `${h.icon ? `${escape(h.icon)} ` : ''}${escape(h.title ?? ask.name)}`;
67
+ const care = h.destructive ? ' data-confirm="true"' : '';
68
+ const said = ask.description ? `<p class="nv-hint">${escape(ask.description)}</p>` : '';
69
+ return `<form class="nv-stack" data-ask="${escape(ask.name)}"${care}><fieldset><legend>${label}</legend>${said}${fields}<div class="nv-row" data-align="center"><button class="nv-button"${h.destructive ? ' data-variant="danger"' : ''}>${label}</button></div></fieldset></form>`;
70
+ }
71
+ // The strings typed by the schema. An empty field that is not required is
72
+ // left out, so the being sees what the human said and nothing else; an
73
+ // empty required field goes as the empty string, and the being answers.
74
+ // A JSON box that does not parse is an error the side shows before asking.
75
+ export function values(ask, raw) {
76
+ // The side's own first, so a form the human submits carries the moment it
77
+ // was submitted at rather than the one the page was drawn at.
78
+ const args = mine(ask);
79
+ const must = required(ask.input);
80
+ for (const [name, p] of props(ask.input)) {
81
+ if (name in args)
82
+ continue;
83
+ const s = raw[name];
84
+ if (p.type === 'boolean') {
85
+ if (s === 'true')
86
+ args[name] = true;
87
+ else if (must.has(name))
88
+ args[name] = false;
89
+ continue;
90
+ }
91
+ if (s === undefined || (s === '' && !must.has(name)))
92
+ continue;
93
+ if (p.type === 'number' || p.type === 'integer') {
94
+ const n = Number(s);
95
+ if (s === '' || Number.isNaN(n))
96
+ return { error: `${name} is a number` };
97
+ args[name] = n;
98
+ }
99
+ else if (p.type === 'string' || p.type === undefined || Array.isArray(p.enum)) {
100
+ args[name] = s;
101
+ }
102
+ else {
103
+ try {
104
+ args[name] = JSON.parse(s);
105
+ }
106
+ catch {
107
+ return { error: `${name} is not JSON` };
108
+ }
109
+ }
110
+ }
111
+ return { args };
112
+ }
113
+ // ---- an answer as a view
114
+ // A value has one face by its shape. A primitive is text. An object is a
115
+ // table of its keys, in the output schema's order when she declared one and
116
+ // her own order otherwise, each value viewed in turn. A list of objects is
117
+ // one table with a column per key seen; any other list is a list. Null is
118
+ // the word nothing, so that an answer of null is never a blank.
119
+ export function view(value, schema) {
120
+ if (value === null)
121
+ return '<i>nothing</i>';
122
+ if (typeof value !== 'object')
123
+ return escape(String(value));
124
+ if (Array.isArray(value)) {
125
+ if (value.length && value.every((v) => v !== null && typeof v === 'object' && !Array.isArray(v))) {
126
+ const rows = value;
127
+ const keys = [...new Set(rows.flatMap((r) => Object.keys(r)))];
128
+ const head = keys.map((k) => `<th>${escape(k)}</th>`).join('');
129
+ const body = rows.map((r) => `<tr>${keys.map((k) => `<td>${k in r ? view(r[k]) : ''}</td>`).join('')}</tr>`).join('');
130
+ return `<div class="nv-table-scroll"><table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
131
+ }
132
+ return value.length ? `<ul class="nv-list" role="list">${value.map((v) => `<li class="nv-item">${view(v)}</li>`).join('')}</ul>` : '<p class="nv-empty">none</p>';
133
+ }
134
+ const declared = props(schema).map(([k]) => k);
135
+ const keys = [...declared.filter((k) => k in value), ...Object.keys(value).filter((k) => !declared.includes(k))];
136
+ if (!keys.length)
137
+ return '<i>empty</i>';
138
+ return `<div class="nv-table-scroll"><table>${keys.map((k) => `<tr><th>${escape(k)}</th><td>${view(value[k], props(schema).find(([n]) => n === k)?.[1])}</td></tr>`).join('')}</table></div>`;
139
+ }
140
+ // The three words for "no object", and the object, each with a face. An
141
+ // error object is the answer she gave, marked; silence and unreached are
142
+ // what the side says about them, since they carry nothing themselves. The
143
+ // word is on the answer for a design and a test to read, and the tone is
144
+ // what a reader sees: hers in red, the wire's in amber.
145
+ export function face(w, schema) {
146
+ if (w.word === 'object')
147
+ return `<div class="nv-answer" data-word="object">${view(w.value, schema)}</div>`;
148
+ if (w.word === 'error')
149
+ return `<div class="nv-answer" data-word="error" data-tone="danger">${view(w.value)}</div>`;
150
+ if (w.word === 'silence')
151
+ return `<div class="nv-answer" data-word="silence" data-tone="warning">${escape(SILENCE_TEXT)}</div>`;
152
+ if (w.word === 'quo')
153
+ return `<div class="nv-answer" data-word="quo" data-tone="warning">${escape(wordText(w.name))}</div>`;
154
+ return `<div class="nv-answer" data-word="unreached" data-tone="warning">${escape(UNREACHED_TEXT)}</div>`;
155
+ }
156
+ // ---- an answer of a named kind
157
+ // Rows of people: a name, a line under it, and a face when she has one, on
158
+ // one surface. A row that is not a person is shown by its own shape, so a
159
+ // page that said `people` of a list of strings still shows the strings.
160
+ const person = (v) => {
161
+ if (v === null || typeof v !== 'object' || Array.isArray(v) || typeof v.name !== 'string')
162
+ return view(v);
163
+ const name = v.name;
164
+ const line = typeof v.line === 'string' ? `<p class="nv-t-quiet">${escape(v.line)}</p>` : '';
165
+ const pic = typeof v.picture === 'string' && /^data:image\//.test(v.picture) ? `<img alt="" src="${v.picture}">` : escape(name.trim().charAt(0).toUpperCase());
166
+ return `<div class="nv-person"><span class="nv-avatar">${pic}</span><div class="nv-stack"><p class="nv-t-body">${escape(name)}</p>${line}</div></div>`;
167
+ };
168
+ export function people(value) {
169
+ if (!Array.isArray(value))
170
+ return view(value);
171
+ if (!value.length)
172
+ return '<p class="nv-empty">nobody yet</p>';
173
+ return `<div class="nv-list">${value.map((v) => `<div class="nv-item">${person(v)}</div>`).join('')}</div>`;
174
+ }
175
+ // What happened, in order: a moment and the words, down a line.
176
+ const happened = (v) => {
177
+ if (v === null || typeof v !== 'object' || Array.isArray(v) || typeof v.text !== 'string')
178
+ return view(v);
179
+ const at = typeof v.at === 'string' || typeof v.at === 'number' ? `<p class="nv-t-label">${escape(String(v.at))}</p>` : '';
180
+ return `<div class="nv-stack">${at}<p class="nv-t-body">${escape(v.text)}</p></div>`;
181
+ };
182
+ export function feed(value) {
183
+ if (!Array.isArray(value))
184
+ return view(value);
185
+ if (!value.length)
186
+ return '<p class="nv-empty">nothing yet</p>';
187
+ return `<div class="nv-feed">${value.map(happened).join('')}</div>`;
188
+ }
189
+ const ENDED = new Set(['removed', 'dropped', 'not joined']);
190
+ const AWAY = new Set(['unreached', 'silence', 'late', 'absent']);
191
+ export function standing(error) {
192
+ return ENDED.has(error) ? 'ended' : AWAY.has(error) ? 'away' : 'error';
193
+ }
194
+ // What a human reads for one, in her own terms rather than the ward's word.
195
+ export function stands(error) {
196
+ switch (standing(error)) {
197
+ case 'ended':
198
+ return error === 'not joined' ? 'you are not in this world' : 'this world ended it: you are out, and nothing here reaches it';
199
+ case 'away':
200
+ return `this world cannot be reached right now, and nothing was delivered (${error})`;
201
+ default:
202
+ return `not in: ${error}`;
203
+ }
204
+ }
205
+ // What a human reads when a way in did not let her in. A world refuses for
206
+ // several different reasons and a shell that said one sentence for all of
207
+ // them sent her looking for the wrong thing. One of them has an answer and
208
+ // is the reason this exists: `id taken` is a device of that name already in
209
+ // this world, which is exactly what a human meets on a new device after
210
+ // losing the one that held it, since a presence is a seed and the seed went
211
+ // with the device. The standing it left is still there, and hers to take
212
+ // out from any device she still holds.
213
+ //
214
+ // The three words it knows are the dock's own beings', the desk's and the
215
+ // user being's, and knowing them is allowed because they ship in this
216
+ // package beside this file: what the screen may not do is invent a word a
217
+ // being never said, and it invents none. Any other public being falls
218
+ // through and is quoted in her own words, so a world that answers something
219
+ // of its own is passed on rather than guessed at. That is the whole rule,
220
+ // and it is why there is a default branch and not a table.
221
+ export function refused(answer) {
222
+ const w = word(answer);
223
+ if (w.word === 'silence')
224
+ return SILENCE_TEXT;
225
+ if (w.word === 'unreached')
226
+ return UNREACHED_TEXT;
227
+ if (w.word === 'quo')
228
+ return wordText(w.name);
229
+ const said = w.word === 'error' && typeof w.value.error === 'string' ? w.value.error : '';
230
+ switch (said) {
231
+ case 'id taken':
232
+ return 'a device of that name is already in this world. If it is one you have lost, take it out of the world from a device you still hold, or come back under another name.';
233
+ case 'no such user':
234
+ return 'this world knows nobody by that way in.';
235
+ case 'refused':
236
+ return 'that way in was refused: it may be spent, or it may never have been one.';
237
+ default:
238
+ return said ? `that way in was refused: ${said}` : 'that way in was refused.';
239
+ }
240
+ }
241
+ // The title is the one hint the notes may carry: a string named `name`.
242
+ // The rest of the notes is shown as a view and read as nothing else.
243
+ export function title(bp, l = {}) {
244
+ if (l.name)
245
+ return l.name;
246
+ const n = bp?.notes;
247
+ return n !== null && typeof n === 'object' && !Array.isArray(n) && typeof n.name === 'string' && n.name ? n.name : 'quo';
248
+ }
249
+ // ---- a look
250
+ // A look as the page paints it: the tokens become CSS variables on one
251
+ // section, and the name and the logo its heading. The shape of every token
252
+ // is the dock's, `beings/look.ts`; this only writes what survived it.
253
+ // Her tokens become the kit's slots on her section and nowhere else: her
254
+ // accent is the accent there, her ground the raised ground, her font the
255
+ // sans, her radius the large corner. The kit's derived slots follow from
256
+ // the accent in the stylesheet, so one colour of hers recolours her whole
257
+ // section and nothing outside it.
258
+ export function look(l) {
259
+ if (!l)
260
+ return { style: '', head: '' };
261
+ const vars = [];
262
+ if (l.accent)
263
+ vars.push(`--nv-color-accent:${l.accent}`, `--nv-color-accent-hover:${l.accent}`, `--nv-color-link:${l.accent}`);
264
+ if (l.background)
265
+ vars.push(`--nv-color-bg-raised:${l.background}`);
266
+ if (l.foreground)
267
+ vars.push(`--nv-color-fg:${l.foreground}`, `--nv-color-fg-body:${l.foreground}`);
268
+ if (l.font)
269
+ vars.push(`--nv-font-sans:${l.font}`, `--nv-font-display:${l.font}`);
270
+ if (l.radius !== undefined)
271
+ vars.push(`--nv-radius-lg:${l.radius}px`, `--nv-radius-xl:${l.radius}px`);
272
+ const logo = l.logo ? `<img class="logo" alt="" src="${l.logo}">` : '';
273
+ const name = l.name ? escape(l.name) : '';
274
+ return { style: vars.length ? ` style="${vars.join(';')}"` : '', head: logo || name ? `<h2>${logo}${name}</h2>` : '' };
275
+ }
276
+ // Her asks in the order her look asks for, the rest after in her own order.
277
+ export function ordered(all, l) {
278
+ const want = l?.order ?? [];
279
+ return [...want.map((n) => all.find((a) => a.name === n)).filter((a) => a !== undefined), ...all.filter((a) => !want.includes(a.name))];
280
+ }
281
+ export { hintFor };
282
+ export function page(m) {
283
+ const bp = m.blueprint;
284
+ const pres = presentation(bp);
285
+ // An answer that is an invitation is a way in, and the only thing to do
286
+ // with one is to open it: painted as the link it is, on this world's page
287
+ // address. A being does not know where her page lives and the side does,
288
+ // which is why the grammar carries no URL and this is not a node.
289
+ const shows = (w, schema) => {
290
+ if (w.word !== 'object' || m.at === undefined || !isInvitation(w.value))
291
+ return face(w, schema);
292
+ const url = link(m.at, w.value);
293
+ return `<div class="nv-answer" data-word="object" data-way><a class="nv-key" href="${escape(url)}">${escape(url)}</a></div>`;
294
+ };
295
+ const one = (l, prefix = '') => (a) => {
296
+ const w = m.answers[a.name];
297
+ const h = hint(l, a.name.startsWith(prefix) ? a.name.slice(prefix.length) : a.name);
298
+ return `<section class="nv-stack">${form(a, h)}${w ? shows(w, a.output) : ''}</section>`;
299
+ };
300
+ // an answer shown as the page asks: as text, as cards, as a list, as
301
+ // people, as a feed, or by its shape
302
+ const answer = (name, as) => {
303
+ const a = bp?.asks.find((x) => x.name === name);
304
+ const w = a ? m.answers[name] : undefined;
305
+ if (!a || !w)
306
+ return '';
307
+ if (w.word === 'object' && as === 'text')
308
+ return `<div class="nv-answer" data-word="object">${view(w.value === null || typeof w.value !== 'object' ? w.value : JSON.stringify(w.value))}</div>`;
309
+ if (w.word === 'object' && as === 'cards' && Array.isArray(w.value))
310
+ return `<div class="nv-cards">${w.value.map((v) => `<div class="nv-card">${view(v)}</div>`).join('')}</div>`;
311
+ if (w.word === 'object' && as === 'list' && Array.isArray(w.value))
312
+ return `<ul class="nv-list" role="list">${w.value.map((v) => `<li class="nv-item">${view(v)}</li>`).join('')}</ul>`;
313
+ if (w.word === 'object' && as === 'people')
314
+ return people(w.value);
315
+ if (w.word === 'object' && as === 'feed')
316
+ return feed(w.value);
317
+ return shows(w, a.output);
318
+ };
319
+ // ---- where a being may send a human
320
+ //
321
+ // A being names a place and the side decides whether it becomes an anchor.
322
+ // Two origins are vouched for and no third. The world's own, which is the
323
+ // origin serving this very page, so a being may move a human around inside
324
+ // the world she already chose. And the origins she declared in her look as
325
+ // the ones she stands in for, which is how an envoy speaking for a payment
326
+ // or a booking vendor sends a human to that vendor's own checkout.
327
+ //
328
+ // The declaration is what makes the second safe to allow. A look is asked
329
+ // once per digest and is the same for every answer she gives, so the
330
+ // places she stands in for are a standing claim rather than a destination
331
+ // that changes under one reply. That is what bounds the attack this rule
332
+ // is for: a vendor's response relayed through an envoy, carrying an
333
+ // address nobody here wrote. It does not bound a being who is hostile
334
+ // herself, and nothing on a page could; what bounds her is that a human
335
+ // chose to hold a standing at her.
336
+ //
337
+ // An address the side will not vouch for is painted as its own words. The
338
+ // human sees exactly where she was being sent, in full, and goes there by
339
+ // her own hand or not at all. Nothing is dropped and nothing is hidden,
340
+ // because a link silently deleted is a page that lies about what the being
341
+ // said.
342
+ const originOf = (s) => {
343
+ if (s === undefined)
344
+ return null;
345
+ try {
346
+ return new URL(s).origin;
347
+ }
348
+ catch {
349
+ return null;
350
+ }
351
+ };
352
+ const here = originOf(m.at);
353
+ const linkedBy = (l) => (href, text) => {
354
+ const origin = originOf(href);
355
+ if (origin === null)
356
+ return escape(href);
357
+ const vouched = (here !== null && origin === here) || (l?.stands?.includes(origin) ?? false);
358
+ if (!vouched)
359
+ return `<span class="nv-key link away" title="${escape(href)}">${escape(href)}</span>`;
360
+ // `noopener` and `noreferrer` because a page opened from here is a page
361
+ // the world wrote, and it is handed no window and no referer of ours.
362
+ // The origin is on the anchor whether or not a design shows it, so what
363
+ // a human is about to leave for is in the page and not only in a
364
+ // status bar she may never see.
365
+ const out = origin === here ? '' : ' out';
366
+ return `<a class="link${out}" href="${escape(href)}" rel="noopener noreferrer" data-origin="${escape(origin)}">${escape(text)}</a>`;
367
+ };
368
+ const groups = grouped(bp);
369
+ const taken = new Set(Object.values(groups).flatMap((g) => g.asks ?? []));
370
+ // A standing's section. With her page, the far tree is painted inside it
371
+ // through a painter of her own: a name it uses is one of her asks, carried,
372
+ // and never one of the carrier's or another standing's, so a far page
373
+ // reaches nothing outside its section; her title is the page's, and the
374
+ // section keeps her look. Without one, her asks are forms under her name.
375
+ const section = (id, as) => {
376
+ const g = groups[id];
377
+ const inGroup = bp && g ? bp.asks.filter((a) => g.asks?.includes(a.name)) : [];
378
+ if (!inGroup.length)
379
+ return '';
380
+ const kept = sanitise(g.look);
381
+ const l = look(kept);
382
+ const prefix = `${id}-`;
383
+ const tree = m.pages[id];
384
+ // where her own page is, when the world's page is known and this is not it already
385
+ const own = m.at !== undefined && m.focus !== id ? `<p class="nv-t-quiet"><a class="open" href="${escape(`${m.at}/${encodeURIComponent(id)}`)}">open</a></p>` : '';
386
+ const whole = m.focus === id ? ' data-whole' : '';
387
+ // A door: her name and the way to her page, and nothing of hers here.
388
+ // Painted whole on her own page, as any standing is.
389
+ if (as === 'door' && m.focus !== id) {
390
+ const name = kept.name ?? id;
391
+ const count = inGroup.length;
392
+ return `<section class="nv-standing" data-standing="${escape(id)}" data-door${l.style}><div class="nv-person"><span class="nv-avatar">${escape(name.charAt(0).toUpperCase())}</span><div class="nv-stack"><p class="nv-t-body">${escape(name)}</p><p class="nv-t-quiet">${count} ${count === 1 ? 'thing' : 'things'} to do here, on its own page</p></div></div>${own}</section>`;
393
+ }
394
+ if (tree) {
395
+ const hers = {
396
+ form: (name) => {
397
+ const a = inGroup.find((x) => x.name === prefix + name);
398
+ return a ? one(kept, prefix)(a) : '';
399
+ },
400
+ answer: (name, as) => (inGroup.some((x) => x.name === prefix + name) ? answer(prefix + name, as) : ''),
401
+ link: linkedBy(kept), // a carried standing vouches by her own look and never her carrier's
402
+ standing: () => '',
403
+ standings: () => '',
404
+ };
405
+ return `<section class="nv-standing" data-standing="${escape(id)}"${whole}${l.style}>${paint(tree, hers)}${own}</section>`;
406
+ }
407
+ const want = (kept.order ?? []).map((n) => prefix + n);
408
+ const inOrder = ordered(inGroup, { order: want });
409
+ return `<section class="nv-standing" data-standing="${escape(id)}"${whole}${l.style}>${l.head || `<h2>${escape(id)}</h2>`}${inOrder.map(one(kept, prefix)).join('')}${own}</section>`;
410
+ };
411
+ const far = Object.keys(groups).map((id) => section(id)).join('');
412
+ // Her page, when she answered one: painted from her tree, each name it
413
+ // uses looked up in her describe, so a page shows nothing her gate hid.
414
+ const painter = {
415
+ form: (name) => {
416
+ const a = bp?.asks.find((x) => x.name === name && !taken.has(x.name) && !pres.has(x.name));
417
+ return a ? one(m.look)(a) : '';
418
+ },
419
+ answer: (name, as) => (pres.has(name) ? '' : answer(name, as)),
420
+ link: linkedBy(m.look),
421
+ standing: section,
422
+ standings: () => far,
423
+ };
424
+ const mineAsks = bp ? ordered(bp.asks.filter((a) => !taken.has(a.name) && !pres.has(a.name)), m.look).map(one(m.look)).join('') : '';
425
+ // A being's page: one standing she carries as the whole page, and nothing of hers around it. A key her
426
+ // describe does not carry paints as nothing, the way a name on a page does: the address adds no right.
427
+ const focused = m.focus !== undefined;
428
+ const body = focused ? section(m.focus) : m.tree ? paint(m.tree, painter) : `<div class="nv-stack">${mineAsks}${far}</div>`;
429
+ const shown = bp && bp.notes !== null && typeof bp.notes === 'object' && !Array.isArray(bp.notes) ? Object.fromEntries(Object.entries(bp.notes).filter(([k]) => k !== 'standings' && k !== 'name')) : bp?.notes;
430
+ // her notes are shown beside her forms when she has no page: a being with
431
+ // a page says everything she wants said, and a note is wiring
432
+ const notes = bp && !m.tree && shown !== null && shown !== undefined && !(typeof shown === 'object' && !Array.isArray(shown) && !Object.keys(shown).length) ? `<aside class="nv-answer notes">${view(shown)}</aside>` : '';
433
+ const pushes = m.pushes.length ? `<section class="nv-stack pushes"><p class="nv-t-label">pushes</p><div class="nv-feed">${m.pushes.map((p) => `<div>${view(p)}</div>`).join('')}</div></section>` : '';
434
+ const styled = look(m.look);
435
+ // A name is said once. A page that opens with a title of its own is the
436
+ // name, and the bar keeps only where the human stands; a being painted
437
+ // as forms has no page to say it, so the bar says her name; a focused
438
+ // page says the way back and the standing's own tree says the rest.
439
+ const back = focused && m.at !== undefined ? `<a class="back" href="${escape(m.at)}">${escape(title(bp, m.look))}</a>` : '';
440
+ const said = focused ? (m.pages[m.focus] ? titled(m.pages[m.focus]) : false) : titled(m.tree);
441
+ const head = focused ? back : m.tree && said ? '' : `${m.look.logo ? `<img class="logo" alt="" src="${m.look.logo}">` : ''}<h1>${escape(title(bp, m.look))}</h1>`;
442
+ // The notice carries where she stands, so a design and a reader can tell
443
+ // a relation that ended from a world that is merely away without parsing
444
+ // a sentence; the chip's tone is that word.
445
+ const tone = m.standing === 'ended' || m.standing === 'error' ? 'danger' : m.standing === 'away' ? 'warning' : 'success';
446
+ // `data-page` is the one hook that is not the design's: a class here would
447
+ // be a livery's name, and a livery is worn and swapped. The door marks
448
+ // itself `data-door` for the same reason, and between them anything
449
+ // outside can say which of the two it is looking at.
450
+ return `<header class="nv-bar"${styled.style}>${head}<p class="nv-chip notice" data-standing="${m.standing}" data-tone="${tone}">${escape(m.notice)}</p></header>${focused ? '' : notes}<main data-page${focused ? ' class="focus"' : ''}${styled.style}>${body}</main>${pushes}`;
451
+ }
@@ -0,0 +1,7 @@
1
+ import type { JsonObject } from '@nervur-org/nervur';
2
+ import type { Subject } from '../beings/side.ts';
3
+ type Owner = {
4
+ ask(method?: string, args?: JsonObject): Promise<unknown>;
5
+ };
6
+ export declare function local(ward: Owner, key: string): Subject;
7
+ export {};