@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.
- package/GETTING_STARTED.md +128 -0
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +37 -0
- package/api/quo-api.md +196 -0
- package/api/route.ts +227 -0
- package/beings/GLOSSARY.md +118 -0
- package/beings/WORLDS.md +567 -0
- package/beings/avatar.ts +101 -0
- package/beings/carry.ts +159 -0
- package/beings/clock.ts +552 -0
- package/beings/courier.ts +107 -0
- package/beings/desk.ts +128 -0
- package/beings/doorbell.ts +129 -0
- package/beings/envoy.ts +218 -0
- package/beings/index.ts +16 -0
- package/beings/join.ts +51 -0
- package/beings/link.ts +104 -0
- package/beings/look.ts +139 -0
- package/beings/porter.ts +43 -0
- package/beings/presence.ts +39 -0
- package/beings/quo-app-bells.md +280 -0
- package/beings/quo-dock.md +1942 -0
- package/beings/setup.ts +45 -0
- package/beings/side.ts +60 -0
- package/beings/user.ts +459 -0
- package/beings/way.ts +65 -0
- package/cli/bundles.ts +20 -0
- package/cli/client.ts +73 -0
- package/cli/daemon.ts +581 -0
- package/cli/estate/Caddyfile +33 -0
- package/cli/estate/quo.service +35 -0
- package/cli/estate/systems.quo.plist +44 -0
- package/cli/estate.ts +48 -0
- package/cli/folder.ts +27 -0
- package/cli/oauth.ts +32 -0
- package/cli/pilot.ts +38 -0
- package/cli/quo-dock.md +1942 -0
- package/cli/quo.ts +203 -0
- package/cli/stand.ts +160 -0
- package/dist/api/route.d.ts +21 -0
- package/dist/api/route.js +193 -0
- package/dist/beings/avatar.d.ts +32 -0
- package/dist/beings/avatar.js +100 -0
- package/dist/beings/carry.d.ts +12 -0
- package/dist/beings/carry.js +174 -0
- package/dist/beings/clock.d.ts +161 -0
- package/dist/beings/clock.js +528 -0
- package/dist/beings/courier.d.ts +50 -0
- package/dist/beings/courier.js +96 -0
- package/dist/beings/desk.d.ts +53 -0
- package/dist/beings/desk.js +135 -0
- package/dist/beings/doorbell.d.ts +78 -0
- package/dist/beings/doorbell.js +133 -0
- package/dist/beings/envoy.d.ts +54 -0
- package/dist/beings/envoy.js +211 -0
- package/dist/beings/index.d.ts +14 -0
- package/dist/beings/index.js +16 -0
- package/dist/beings/join.d.ts +15 -0
- package/dist/beings/join.js +42 -0
- package/dist/beings/link.d.ts +14 -0
- package/dist/beings/link.js +91 -0
- package/dist/beings/look.d.ts +30 -0
- package/dist/beings/look.js +104 -0
- package/dist/beings/porter.d.ts +14 -0
- package/dist/beings/porter.js +41 -0
- package/dist/beings/presence.d.ts +8 -0
- package/dist/beings/presence.js +21 -0
- package/dist/beings/setup.d.ts +3 -0
- package/dist/beings/setup.js +39 -0
- package/dist/beings/side.d.ts +32 -0
- package/dist/beings/side.js +37 -0
- package/dist/beings/user.d.ts +315 -0
- package/dist/beings/user.js +462 -0
- package/dist/beings/way.d.ts +19 -0
- package/dist/beings/way.js +28 -0
- package/dist/cli/bundles.d.ts +5 -0
- package/dist/cli/bundles.js +17 -0
- package/dist/cli/client.d.ts +15 -0
- package/dist/cli/client.js +58 -0
- package/dist/cli/daemon.d.ts +50 -0
- package/dist/cli/daemon.js +584 -0
- package/dist/cli/estate/Caddyfile +33 -0
- package/dist/cli/estate/quo.service +35 -0
- package/dist/cli/estate/systems.quo.plist +44 -0
- package/dist/cli/estate.d.ts +5 -0
- package/dist/cli/estate.js +50 -0
- package/dist/cli/folder.d.ts +2 -0
- package/dist/cli/folder.js +25 -0
- package/dist/cli/oauth.d.ts +2 -0
- package/dist/cli/oauth.js +32 -0
- package/dist/cli/pilot.d.ts +5 -0
- package/dist/cli/pilot.js +22 -0
- package/dist/cli/quo.d.ts +2 -0
- package/dist/cli/quo.js +201 -0
- package/dist/cli/stand.d.ts +37 -0
- package/dist/cli/stand.js +110 -0
- package/dist/dock/agents.d.ts +102 -0
- package/dist/dock/agents.js +105 -0
- package/dist/dock/apns.d.ts +27 -0
- package/dist/dock/apns.js +98 -0
- package/dist/dock/console.d.ts +6 -0
- package/dist/dock/console.js +20 -0
- package/dist/dock/dialers.d.ts +56 -0
- package/dist/dock/dialers.js +75 -0
- package/dist/dock/faculty.d.ts +24 -0
- package/dist/dock/faculty.js +142 -0
- package/dist/dock/fcm.d.ts +13 -0
- package/dist/dock/fcm.js +77 -0
- package/dist/dock/grants.d.ts +41 -0
- package/dist/dock/grants.js +99 -0
- package/dist/dock/http2.d.ts +4 -0
- package/dist/dock/http2.js +68 -0
- package/dist/dock/index.d.ts +41 -0
- package/dist/dock/index.js +113 -0
- package/dist/dock/local.d.ts +13 -0
- package/dist/dock/local.js +44 -0
- package/dist/dock/location.d.ts +19 -0
- package/dist/dock/location.js +11 -0
- package/dist/dock/notifications.d.ts +38 -0
- package/dist/dock/notifications.js +60 -0
- package/dist/dock/push.d.ts +79 -0
- package/dist/dock/push.js +132 -0
- package/dist/dock/service.d.ts +4 -0
- package/dist/dock/service.js +107 -0
- package/dist/dock/timer.d.ts +52 -0
- package/dist/dock/timer.js +149 -0
- package/dist/dock/webpush.d.ts +12 -0
- package/dist/dock/webpush.js +144 -0
- package/dist/harbor/browser.d.ts +15 -0
- package/dist/harbor/browser.js +82 -0
- package/dist/harbor/capacitor.d.ts +28 -0
- package/dist/harbor/capacitor.js +218 -0
- package/dist/harbor/disk.d.ts +27 -0
- package/dist/harbor/disk.js +249 -0
- package/dist/harbor/edge/alarm.d.ts +10 -0
- package/dist/harbor/edge/alarm.js +38 -0
- package/dist/harbor/edge/edge.d.ts +54 -0
- package/dist/harbor/edge/edge.js +370 -0
- package/dist/harbor/edge/exercise.d.ts +7 -0
- package/dist/harbor/edge/exercise.js +232 -0
- package/dist/harbor/edge/given.d.ts +26 -0
- package/dist/harbor/edge/given.js +42 -0
- package/dist/harbor/edge/platform.d.ts +39 -0
- package/dist/harbor/edge/storage.d.ts +30 -0
- package/dist/harbor/edge/storage.js +180 -0
- package/dist/harbor/edge/worker.d.ts +17 -0
- package/dist/harbor/edge/worker.js +67 -0
- package/dist/harbor/files.d.ts +15 -0
- package/dist/harbor/files.js +220 -0
- package/dist/harbor/http.d.ts +19 -0
- package/dist/harbor/http.js +177 -0
- package/dist/harbor/idb.d.ts +15 -0
- package/dist/harbor/idb.js +0 -0
- package/dist/harbor/quo.d.ts +10 -0
- package/dist/harbor/quo.js +42 -0
- package/dist/harbor/resolve.d.ts +16 -0
- package/dist/harbor/resolve.js +17 -0
- package/dist/harbor/seal.d.ts +3 -0
- package/dist/harbor/seal.js +25 -0
- package/dist/harbor/sealed.d.ts +35 -0
- package/dist/harbor/sealed.js +162 -0
- package/dist/harbor/tauri.d.ts +16 -0
- package/dist/harbor/tauri.js +59 -0
- package/dist/harbor/ward.d.ts +9 -0
- package/dist/harbor/ward.js +15 -0
- package/dist/human/app.d.ts +32 -0
- package/dist/human/app.js +527 -0
- package/dist/human/apps.d.ts +10 -0
- package/dist/human/apps.js +29 -0
- package/dist/human/dom.d.ts +2 -0
- package/dist/human/dom.js +15 -0
- package/dist/human/door.d.ts +19 -0
- package/dist/human/door.js +92 -0
- package/dist/human/guest.d.ts +3 -0
- package/dist/human/guest.js +33 -0
- package/dist/human/html.d.ts +55 -0
- package/dist/human/html.js +451 -0
- package/dist/human/local.d.ts +7 -0
- package/dist/human/local.js +10 -0
- package/dist/human/mark.d.ts +8 -0
- package/dist/human/mark.js +47 -0
- package/dist/human/move.d.ts +17 -0
- package/dist/human/move.js +90 -0
- package/dist/human/screen.d.ts +19 -0
- package/dist/human/screen.js +119 -0
- package/dist/human/style.d.ts +2 -0
- package/dist/human/style.js +60 -0
- package/dist/human/tab.bundle.txt +4775 -0
- package/dist/human/tab.d.ts +15 -0
- package/dist/human/tab.js +409 -0
- package/dist/human/tree.d.ts +50 -0
- package/dist/human/tree.js +157 -0
- package/dist/human/wake.d.ts +34 -0
- package/dist/human/wake.js +179 -0
- package/dist/human/web.d.ts +33 -0
- package/dist/human/web.js +184 -0
- package/dist/human/worker.bundle.txt +3868 -0
- package/dist/human/worker.d.ts +1 -0
- package/dist/human/worker.js +81 -0
- package/dist/human/worlds.d.ts +24 -0
- package/dist/human/worlds.js +99 -0
- package/dist/mcp/agent.d.ts +10 -0
- package/dist/mcp/agent.js +78 -0
- package/dist/mcp/allow.d.ts +20 -0
- package/dist/mcp/allow.js +88 -0
- package/dist/mcp/http.d.ts +27 -0
- package/dist/mcp/http.js +92 -0
- package/dist/mcp/oauth.d.ts +91 -0
- package/dist/mcp/oauth.js +241 -0
- package/dist/mcp/route.d.ts +15 -0
- package/dist/mcp/route.js +42 -0
- package/dist/mcp/runner.d.ts +45 -0
- package/dist/mcp/runner.js +99 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +102 -0
- package/dock/agents.ts +121 -0
- package/dock/apns.ts +138 -0
- package/dock/console.ts +23 -0
- package/dock/dialers.ts +85 -0
- package/dock/faculty.ts +148 -0
- package/dock/fcm.ts +103 -0
- package/dock/grants.ts +111 -0
- package/dock/http2.ts +70 -0
- package/dock/index.ts +133 -0
- package/dock/local.ts +48 -0
- package/dock/location.ts +22 -0
- package/dock/notifications.ts +67 -0
- package/dock/push.ts +161 -0
- package/dock/quo-app-bells.md +280 -0
- package/dock/quo-dock.md +1942 -0
- package/dock/service.ts +100 -0
- package/dock/timer.ts +174 -0
- package/dock/webpush.ts +182 -0
- package/harbor/browser.ts +97 -0
- package/harbor/capacitor.ts +223 -0
- package/harbor/disk.ts +237 -0
- package/harbor/edge/alarm.ts +48 -0
- package/harbor/edge/edge.ts +407 -0
- package/harbor/edge/exercise.ts +241 -0
- package/harbor/edge/given.ts +106 -0
- package/harbor/edge/platform.d.ts +39 -0
- package/harbor/edge/storage.ts +179 -0
- package/harbor/edge/text.d.ts +15 -0
- package/harbor/edge/worker.ts +70 -0
- package/harbor/edge/wrangler.toml +30 -0
- package/harbor/files.ts +221 -0
- package/harbor/http.ts +188 -0
- package/harbor/idb.ts +0 -0
- package/harbor/quo-harbor.md +850 -0
- package/harbor/quo.ts +44 -0
- package/harbor/resolve.ts +31 -0
- package/harbor/seal.ts +26 -0
- package/harbor/sealed.ts +183 -0
- package/harbor/tauri.ts +79 -0
- package/harbor/ward.ts +28 -0
- package/human/app.ts +580 -0
- package/human/apps.ts +47 -0
- package/human/dom.ts +21 -0
- package/human/door.ts +113 -0
- package/human/guest.ts +34 -0
- package/human/html.ts +490 -0
- package/human/local.ts +30 -0
- package/human/mark.ts +53 -0
- package/human/move.ts +122 -0
- package/human/quo-human.md +1042 -0
- package/human/screen.ts +136 -0
- package/human/style.ts +63 -0
- package/human/tab.bundle.txt +4775 -0
- package/human/tab.ts +446 -0
- package/human/tree.ts +217 -0
- package/human/wake.ts +191 -0
- package/human/web.ts +259 -0
- package/human/worker.bundle.txt +3868 -0
- package/human/worker.ts +124 -0
- package/human/worlds.ts +127 -0
- package/mcp/agent.ts +94 -0
- package/mcp/allow.ts +131 -0
- package/mcp/http.ts +92 -0
- package/mcp/oauth.ts +270 -0
- package/mcp/quo-mcp.md +343 -0
- package/mcp/route.ts +45 -0
- package/mcp/runner.ts +136 -0
- package/mcp/server.ts +101 -0
- package/package.json +121 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { USER } from '../beings/avatar.ts';
|
|
2
|
+
export type Config = {
|
|
3
|
+
quo: string;
|
|
4
|
+
web: string;
|
|
5
|
+
wards: Record<string, {
|
|
6
|
+
pk: string;
|
|
7
|
+
public: boolean;
|
|
8
|
+
}>;
|
|
9
|
+
ward?: string;
|
|
10
|
+
being?: string;
|
|
11
|
+
beings?: boolean;
|
|
12
|
+
bell?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function start(cfg: Config, root?: HTMLElement): Promise<void>;
|
|
15
|
+
export { USER };
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { BrowserHarbor } from '../harbor/browser.js';
|
|
10
|
+
import { USER } from '../beings/avatar.js';
|
|
11
|
+
import { isInvitation } from '@nervur-org/nervur';
|
|
12
|
+
import { parse, parseProof, strip } from '../beings/link.js';
|
|
13
|
+
import { giveWorld } from './move.js';
|
|
14
|
+
import { drained, subscribe } from './wake.js';
|
|
15
|
+
import { screenSide } from './screen.js';
|
|
16
|
+
import { refused } from './html.js';
|
|
17
|
+
import { domSurface } from './dom.js';
|
|
18
|
+
import { guest } from './guest.js';
|
|
19
|
+
import { door, held } from './door.js';
|
|
20
|
+
import { world, relations, fresh, localName } from './worlds.js';
|
|
21
|
+
import { local } from './local.js';
|
|
22
|
+
const kept = (key, fallback) => {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(localStorage.getItem(key) ?? 'null') ?? fallback;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return fallback;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const keep = (key, value) => {
|
|
31
|
+
try {
|
|
32
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* forgets */
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const WORLDS = 'quo-worlds';
|
|
39
|
+
const worlds = () => kept(WORLDS, {});
|
|
40
|
+
const remember = (pk, url, name) => keep(WORLDS, { ...worlds(), [pk]: { url, name } });
|
|
41
|
+
const AT = (pk) => `quo-at:${pk}`;
|
|
42
|
+
const NAMES = (pk) => `quo-names:${pk}`;
|
|
43
|
+
const el = (tag, text = '', attrs = {}) => {
|
|
44
|
+
const e = document.createElement(tag);
|
|
45
|
+
if (text)
|
|
46
|
+
e.textContent = text;
|
|
47
|
+
for (const [k, v] of Object.entries(attrs))
|
|
48
|
+
e.setAttribute(k, v);
|
|
49
|
+
return e;
|
|
50
|
+
};
|
|
51
|
+
export async function start(cfg, root = document.body) {
|
|
52
|
+
// The link, first and once: the invitation, or the proof a world sent
|
|
53
|
+
// out, leaves the address bar before any other code sees it.
|
|
54
|
+
const linked = parse(location.hash);
|
|
55
|
+
const proved = parseProof(location.hash);
|
|
56
|
+
if (linked || proved)
|
|
57
|
+
history.replaceState(null, '', location.pathname + location.search + strip(location.hash));
|
|
58
|
+
const nav = el('nav', '', { class: 'worlds nv-strip' });
|
|
59
|
+
const who = el('nav', '', { class: 'relations nv-row' });
|
|
60
|
+
const status = el('p', 'booting', { class: 'state nv-t-quiet' });
|
|
61
|
+
const screen = el('div');
|
|
62
|
+
root.append(nav, who, status, screen);
|
|
63
|
+
const say = (s) => (status.textContent = s);
|
|
64
|
+
// The harbor in the tab: one database per origin, one ward per world.
|
|
65
|
+
// The world's code first, when its origin serves any: the classes for
|
|
66
|
+
// the tab, handed to the harbor beside the built-in ones before any ward
|
|
67
|
+
// boots, since a being kept from the last visit is booted by class name.
|
|
68
|
+
// A world that serves none has no beings in the tab, and the page is the
|
|
69
|
+
// far being's alone.
|
|
70
|
+
const harbor = new BrowserHarbor('quo');
|
|
71
|
+
const classes = cfg.beings
|
|
72
|
+
? await import(__rewriteRelativeImportExtension(`${cfg.web}/beings.js`)).then((mod) => {
|
|
73
|
+
const out = {};
|
|
74
|
+
for (const [name, v] of Object.entries(mod))
|
|
75
|
+
if (typeof v === 'function' && 'prototype' in v)
|
|
76
|
+
out[name] = v;
|
|
77
|
+
return out;
|
|
78
|
+
})
|
|
79
|
+
: {};
|
|
80
|
+
Object.assign(harbor.classes, classes);
|
|
81
|
+
// A device is one harbor, and this browser may already be holding it in
|
|
82
|
+
// another tab. That tab is the device; this one is a page, and it says
|
|
83
|
+
// so here rather than sitting on the word `booting` with the refusal in a
|
|
84
|
+
// console nobody has open.
|
|
85
|
+
try {
|
|
86
|
+
await harbor.boot();
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
status.remove();
|
|
90
|
+
screen.innerHTML = held({ world: cfg.ward, host: new URL(cfg.web).host });
|
|
91
|
+
throw e; // the terrain still hears it: a page that says so is not a page that succeeded
|
|
92
|
+
}
|
|
93
|
+
harbor.dial(cfg.quo);
|
|
94
|
+
// The doorbell. A world with a bell key is a world that can wake this
|
|
95
|
+
// device when it is closed, and the ear is the service worker on this
|
|
96
|
+
// origin; a device already subscribed says so again silently, and one
|
|
97
|
+
// that has never been asked is offered a button rather than a prompt on
|
|
98
|
+
// arrival. The ring arrives at the worker: with this page open the worker
|
|
99
|
+
// hands it a port, and the page wakes its dialers, drains and answers
|
|
100
|
+
// what it found, since the harbor is held here and nowhere else.
|
|
101
|
+
const pks = Object.values(cfg.wards).map((w) => w.pk);
|
|
102
|
+
let bell = cfg.bell ? await subscribe(cfg.web, cfg.bell) : null;
|
|
103
|
+
globalThis.navigator?.serviceWorker?.addEventListener('message', (e) => {
|
|
104
|
+
if (e.data?.quo !== 'wake')
|
|
105
|
+
return;
|
|
106
|
+
const port = e.ports[0];
|
|
107
|
+
void (async () => {
|
|
108
|
+
harbor.wake();
|
|
109
|
+
port?.postMessage({ missed: await drained(harbor, pks) });
|
|
110
|
+
})();
|
|
111
|
+
});
|
|
112
|
+
// The world this page is about: the link's, since an invitation names
|
|
113
|
+
// its ward, or the page's own. A link for a world not on this harbor is
|
|
114
|
+
// a link to the wrong page, and says so.
|
|
115
|
+
const nameOf = (pk) => Object.keys(cfg.wards).find((n) => cfg.wards[n].pk === pk);
|
|
116
|
+
const pk = linked?.ward ?? (cfg.ward ? cfg.wards[cfg.ward]?.pk : undefined);
|
|
117
|
+
const wardName = pk ? nameOf(pk) : undefined;
|
|
118
|
+
const pageOf = (name) => `${new URL(cfg.web).pathname.replace(/\/$/, '')}/${encodeURIComponent(name)}`;
|
|
119
|
+
// The same page, whole. What leaves this tab, a remembered world or a link
|
|
120
|
+
// for another device, carries the address and not the path.
|
|
121
|
+
const addressOf = (name) => location.origin + pageOf(name);
|
|
122
|
+
const switcher = () => {
|
|
123
|
+
nav.replaceChildren();
|
|
124
|
+
const known = worlds();
|
|
125
|
+
for (const [k, w] of Object.entries(known)) {
|
|
126
|
+
const a = el('a', w.name || k.slice(0, 8), { href: w.url, class: 'nv-chip' });
|
|
127
|
+
if (k === pk)
|
|
128
|
+
a.setAttribute('aria-current', 'page');
|
|
129
|
+
nav.append(a);
|
|
130
|
+
}
|
|
131
|
+
// a world this device is not in yet is named by its own page, not by the strip
|
|
132
|
+
if (!nav.childElementCount)
|
|
133
|
+
nav.remove();
|
|
134
|
+
else if (!nav.isConnected)
|
|
135
|
+
root.insertBefore(nav, root.firstChild);
|
|
136
|
+
};
|
|
137
|
+
switcher();
|
|
138
|
+
if (!pk || !wardName) {
|
|
139
|
+
say(linked ? 'this link is for a world that does not live here' : 'choose a world');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (linked && location.pathname !== pageOf(wardName))
|
|
143
|
+
history.replaceState(null, '', pageOf(wardName) + location.search + location.hash);
|
|
144
|
+
const ward = await world(harbor, pk);
|
|
145
|
+
const at = { name: wardName, pk, public: cfg.wards[wardName].public };
|
|
146
|
+
// The relations in this world, and which one is on screen.
|
|
147
|
+
let side = null;
|
|
148
|
+
let current = null;
|
|
149
|
+
const names = () => kept(NAMES(pk), {});
|
|
150
|
+
const people = () => {
|
|
151
|
+
who.replaceChildren();
|
|
152
|
+
const rels = relations(ward);
|
|
153
|
+
const called = names();
|
|
154
|
+
for (const r of rels) {
|
|
155
|
+
const b = el('button', called[r.key] ?? r.key, { type: 'button', class: 'nv-chip' });
|
|
156
|
+
if (r.key === current?.key)
|
|
157
|
+
b.setAttribute('aria-current', 'true');
|
|
158
|
+
b.onclick = () => void inside(r, `in, as ${called[r.key] ?? r.key}`, called[r.key] ?? r.key);
|
|
159
|
+
who.append(b);
|
|
160
|
+
}
|
|
161
|
+
if (rels.length) {
|
|
162
|
+
const more = el('button', 'another way in', { type: 'button', class: 'another nv-chip' });
|
|
163
|
+
more.onclick = () => void atDoor();
|
|
164
|
+
who.append(more);
|
|
165
|
+
}
|
|
166
|
+
moving.hidden = rels.length === 0; // there is nothing to move out of a world this device is not in
|
|
167
|
+
};
|
|
168
|
+
// The world's beings in the tab, for one relation: one of each class,
|
|
169
|
+
// booted under her key the first time and found there after, since their
|
|
170
|
+
// cells are in the ward and come back with it. Each is a subject of her
|
|
171
|
+
// own with her own side in her own section, so a note she keeps and a
|
|
172
|
+
// form she shows are hers, painted by the same painter as the far page.
|
|
173
|
+
const locals = [];
|
|
174
|
+
const boot = async (rel) => {
|
|
175
|
+
for (const name of Object.keys(classes)) {
|
|
176
|
+
const key = `${rel.key}-${name}`;
|
|
177
|
+
// Booted the first time and found after. Boot refuses a key that has a
|
|
178
|
+
// row, so asking is how a caller learns she is already there, and a
|
|
179
|
+
// refusal is the same nothing-to-do as finding her.
|
|
180
|
+
if (!ward.being(key)) {
|
|
181
|
+
const out = (await ward.ask('boot', { key, class: name }));
|
|
182
|
+
if (out.error)
|
|
183
|
+
continue;
|
|
184
|
+
await ward.save();
|
|
185
|
+
}
|
|
186
|
+
if (!ward.being(key))
|
|
187
|
+
continue;
|
|
188
|
+
const section = el('section', '', { class: 'local', 'data-being': key });
|
|
189
|
+
screen.append(section);
|
|
190
|
+
// The ask pointer saves before it returns, so the side has nothing to do after one.
|
|
191
|
+
locals.push(await screenSide(local(ward, key), domSurface(section), { at: addressOf(at.name) }));
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
// In, as one relation: her page, then the world's beings for her. Every
|
|
195
|
+
// call rotates her keys and a same-ward ask never crosses the harbor, so
|
|
196
|
+
// the side saves after each.
|
|
197
|
+
const inside = async (rel, notice, called) => {
|
|
198
|
+
await side?.close();
|
|
199
|
+
for (const l of locals.splice(0))
|
|
200
|
+
await l.close();
|
|
201
|
+
status.remove();
|
|
202
|
+
root.querySelector('form.password')?.remove();
|
|
203
|
+
screen.replaceChildren();
|
|
204
|
+
current = rel;
|
|
205
|
+
keep(AT(pk), rel.key);
|
|
206
|
+
const mine = el('div', '', { class: 'far' });
|
|
207
|
+
screen.append(mine);
|
|
208
|
+
// the page's address: the world's page, or one being she carries as the whole page
|
|
209
|
+
const s = await screenSide(rel.avatar, domSurface(mine), {
|
|
210
|
+
after: () => ward.save(),
|
|
211
|
+
notice,
|
|
212
|
+
at: addressOf(at.name),
|
|
213
|
+
// A world that ended the relation is over, and the one act left is
|
|
214
|
+
// leaving. The tab says so and puts the way out where she is looking;
|
|
215
|
+
// it drops nothing, since one answer at one moment is never enough to
|
|
216
|
+
// delete a ward and the far world can never be asked to confirm it.
|
|
217
|
+
ended: async () => {
|
|
218
|
+
leave.textContent = 'leave this world';
|
|
219
|
+
leave.classList.add('only');
|
|
220
|
+
moving.scrollIntoView({ block: 'nearest' });
|
|
221
|
+
},
|
|
222
|
+
...(cfg.being ? { focus: cfg.being } : {}),
|
|
223
|
+
});
|
|
224
|
+
side = s;
|
|
225
|
+
const bp = s.model.blueprint;
|
|
226
|
+
const notesName = typeof bp?.notes?.name === 'string' ? bp.notes.name : '';
|
|
227
|
+
remember(pk, addressOf(at.name), s.model.look.name || notesName || at.name);
|
|
228
|
+
if (!names()[rel.key])
|
|
229
|
+
keep(NAMES(pk), { ...names(), [rel.key]: called });
|
|
230
|
+
// How this world wakes this device, said again whenever a relation comes
|
|
231
|
+
// on screen: a push service may hand a browser a new endpoint at any
|
|
232
|
+
// time, and a stale one rings an empty house.
|
|
233
|
+
if (bell)
|
|
234
|
+
await rel.avatar.call('bell', { token: bell }).then(() => ward.save());
|
|
235
|
+
// The device is back, so it drains: whatever was pushed while this tab
|
|
236
|
+
// was closed is on her under this relation, and it lands on the avatar
|
|
237
|
+
// where a push would have landed. A tab with nothing waiting spends one
|
|
238
|
+
// ask and shows the same page.
|
|
239
|
+
await drained(harbor, [pk]);
|
|
240
|
+
switcher();
|
|
241
|
+
people();
|
|
242
|
+
if (!cfg.being)
|
|
243
|
+
await boot(rel); // a being's page is hers alone: the world's tab beings stay on the world's page
|
|
244
|
+
};
|
|
245
|
+
// The way in, from any of the three: a fresh avatar joins, the ward is
|
|
246
|
+
// saved, and she is the one on screen.
|
|
247
|
+
const admit = async (inv, notice, called) => {
|
|
248
|
+
say('knocking');
|
|
249
|
+
const rel = await fresh(ward);
|
|
250
|
+
const got = await rel.avatar.join(inv);
|
|
251
|
+
await ward.save(); // the knock went through her own door, which the harbor never sees
|
|
252
|
+
if (!('asks' in got)) {
|
|
253
|
+
say(`not in: ${got.error}`);
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
await inside(rel, notice, called);
|
|
257
|
+
return true;
|
|
258
|
+
};
|
|
259
|
+
// At the door. With a public being there, her describe as a page, and a
|
|
260
|
+
// form whose answer is an invitation lets the guest in. With nobody
|
|
261
|
+
// there, the door page: a link is the only way in, and nothing to type.
|
|
262
|
+
const atDoor = async () => {
|
|
263
|
+
await side?.close();
|
|
264
|
+
for (const l of locals.splice(0))
|
|
265
|
+
await l.close();
|
|
266
|
+
side = null;
|
|
267
|
+
current = null;
|
|
268
|
+
screen.replaceChildren();
|
|
269
|
+
if (!status.isConnected)
|
|
270
|
+
root.insertBefore(status, screen);
|
|
271
|
+
people();
|
|
272
|
+
if (!at.public) {
|
|
273
|
+
status.remove();
|
|
274
|
+
screen.innerHTML = door({ world: at.name, host: new URL(cfg.web).host });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
say(`at the door of ${at.name}`);
|
|
278
|
+
const rel = await fresh(ward);
|
|
279
|
+
const gate = guest(rel.avatar, pk);
|
|
280
|
+
const s = await screenSide(gate, domSurface(screen), {
|
|
281
|
+
after: () => ward.save(),
|
|
282
|
+
notice: `a guest at ${at.name}: what she shows strangers`,
|
|
283
|
+
at: addressOf(at.name),
|
|
284
|
+
admit: async (inv) => {
|
|
285
|
+
await s.close();
|
|
286
|
+
await admit(inv, `in, as a guest of ${at.name}`, 'guest');
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
side = s;
|
|
290
|
+
status.remove(); // her page is up and says where the human stands; the status was for the wait
|
|
291
|
+
};
|
|
292
|
+
// Moving this world to another device. What the tab holds of a world is
|
|
293
|
+
// one local ward, and every standing won here is in it, so the move is
|
|
294
|
+
// that ward and nothing else: the world is never asked and never told.
|
|
295
|
+
// The other device shows an invitation, the human carries it here, and
|
|
296
|
+
// this tab is out of the world the moment the other one has it.
|
|
297
|
+
// Leaving. What this tab holds of a world is one local ward, and every
|
|
298
|
+
// standing won here is in it, so leaving is that ward dropped whole, seed
|
|
299
|
+
// and all, and the world forgotten from the list. The far world is never
|
|
300
|
+
// asked and never told, as WORLDS has it of a lost phone: it sees one
|
|
301
|
+
// device gone and nothing else. Nothing here is recoverable afterwards, so
|
|
302
|
+
// it is one press and never a consequence of an answer: a world that says
|
|
303
|
+
// the relation ended offers this and does not take it.
|
|
304
|
+
const forget = async () => {
|
|
305
|
+
await side?.close();
|
|
306
|
+
for (const l of locals.splice(0))
|
|
307
|
+
await l.close();
|
|
308
|
+
side = null;
|
|
309
|
+
keep(WORLDS, Object.fromEntries(Object.entries(worlds()).filter(([k]) => k !== pk)));
|
|
310
|
+
localStorage.removeItem(AT(pk));
|
|
311
|
+
localStorage.removeItem(NAMES(pk));
|
|
312
|
+
};
|
|
313
|
+
const leave = el('button', 'leave this world', { type: 'button', class: 'leave nv-button', 'data-variant': 'danger' });
|
|
314
|
+
leave.onclick = () => void (async () => {
|
|
315
|
+
await forget();
|
|
316
|
+
await harbor.drop(localName(pk));
|
|
317
|
+
location.reload(); // at the door of a world this device is out of
|
|
318
|
+
})();
|
|
319
|
+
const moving = el('div', '', { class: 'move' });
|
|
320
|
+
const offer = el('p', '', { class: 'invitation' });
|
|
321
|
+
const said = el('p', '', { class: 'quiet' });
|
|
322
|
+
const give = el('button', 'move this world to another device', { type: 'button', class: 'nv-button', 'data-variant': 'quiet' });
|
|
323
|
+
// Being woken is asked for and never sprung: a page that asks a browser
|
|
324
|
+
// for notifications the moment it opens is a page every browser punishes,
|
|
325
|
+
// and a human who has not said yet is offered a button instead.
|
|
326
|
+
const doorbell = el('button', 'let this world wake me', { type: 'button', class: 'doorbell nv-button', 'data-variant': 'quiet' });
|
|
327
|
+
doorbell.hidden = !cfg.bell || bell !== null;
|
|
328
|
+
doorbell.onclick = () => void (async () => {
|
|
329
|
+
bell = cfg.bell ? await subscribe(cfg.web, cfg.bell, true) : null;
|
|
330
|
+
doorbell.hidden = bell !== null;
|
|
331
|
+
if (bell && current)
|
|
332
|
+
await current.avatar.call('bell', { token: bell }).then(() => ward.save());
|
|
333
|
+
})();
|
|
334
|
+
moving.append(give, leave, offer, said, doorbell);
|
|
335
|
+
let giving = null;
|
|
336
|
+
const stopGiving = async () => {
|
|
337
|
+
const g = giving;
|
|
338
|
+
giving = null;
|
|
339
|
+
offer.textContent = '';
|
|
340
|
+
give.textContent = 'move this world to another device';
|
|
341
|
+
await g?.close();
|
|
342
|
+
};
|
|
343
|
+
give.onclick = () => void (async () => {
|
|
344
|
+
if (giving)
|
|
345
|
+
return stopGiving();
|
|
346
|
+
said.textContent = '';
|
|
347
|
+
giving = await giveWorld(harbor, { pk, at: addressOf(at.name), name: at.name }, cfg.quo);
|
|
348
|
+
offer.textContent = giving.offer;
|
|
349
|
+
give.textContent = 'stop offering it';
|
|
350
|
+
void giving.moved.then(async () => {
|
|
351
|
+
// it is on the other device now, and this one is out of the world:
|
|
352
|
+
// the standings went with the ward and nothing here is in it
|
|
353
|
+
await forget(); // the ward went with the move, so nothing is dropped here
|
|
354
|
+
location.reload(); // the page reloads at the door of a world this device is out of
|
|
355
|
+
});
|
|
356
|
+
})().catch((e) => (said.textContent = String(e)));
|
|
357
|
+
root.append(moving);
|
|
358
|
+
// A proof: the world's own way in, spent at her door. A proof names no
|
|
359
|
+
// ward, so it is for the world this page is, and what she trades it for
|
|
360
|
+
// is an invitation, joined like any other.
|
|
361
|
+
//
|
|
362
|
+
// `device` is the ask, and that name is a convention of the dock's rather
|
|
363
|
+
// than the desk's alone: a proof arrives in a fragment and not as a form,
|
|
364
|
+
// so there is nothing to draw and a name is the only way to hand it over.
|
|
365
|
+
// A public being of any class takes proofs by writing that ask; one that
|
|
366
|
+
// writes none answers this as an unknown ask, which is a world with no
|
|
367
|
+
// proof way in and is refused like any other refusal. What a proof is worth is the
|
|
368
|
+
// way in's word and never the desk's: the desk routes it by kind, and the
|
|
369
|
+
// being who verifies it says who it is for, how far she may go and
|
|
370
|
+
// whether it may be spent again. A world with nobody at her door trades
|
|
371
|
+
// nothing, and a refusal of any of those kinds leaves the human at the
|
|
372
|
+
// door with nothing taken and one line saying so.
|
|
373
|
+
if (proved && at.public) {
|
|
374
|
+
say('at the door');
|
|
375
|
+
const rel = await fresh(ward);
|
|
376
|
+
const got = await rel.avatar.knock({ ward: pk }, 'device', { proof: { kind: proved.kind, token: proved.token } });
|
|
377
|
+
await ward.save();
|
|
378
|
+
if (isInvitation(got) && (await admit(got, 'in, by the link', 'link')))
|
|
379
|
+
return;
|
|
380
|
+
// What came back and not a guess at it. A world refuses a way in for
|
|
381
|
+
// several different reasons and only one of them is that it was spent;
|
|
382
|
+
// `id taken` in particular is a device of that name already in this
|
|
383
|
+
// world, which is what a human meets when she has lost the device that
|
|
384
|
+
// held it, and telling her it was spent sends her looking for the wrong
|
|
385
|
+
// thing.
|
|
386
|
+
say(refused(got));
|
|
387
|
+
}
|
|
388
|
+
// A link: one more relation, whoever else is in. Then whoever was on
|
|
389
|
+
// screen last, or the door.
|
|
390
|
+
if (linked && (await admit(linked, 'in, by the link', 'link')))
|
|
391
|
+
return;
|
|
392
|
+
const rels = relations(ward);
|
|
393
|
+
const last = kept(AT(pk), null);
|
|
394
|
+
const back = rels.find((r) => r.key === last) ?? rels[rels.length - 1];
|
|
395
|
+
if (back) {
|
|
396
|
+
const had = await back.avatar.tools();
|
|
397
|
+
await ward.save(); // her keys rotated on that ask, whatever it answered
|
|
398
|
+
if ('asks' in had)
|
|
399
|
+
return inside(back, linked ? `not in by the link; in, as before` : 'in, as before', names()[back.key] ?? back.key);
|
|
400
|
+
say(`not in (${had.error})`);
|
|
401
|
+
}
|
|
402
|
+
await atDoor();
|
|
403
|
+
}
|
|
404
|
+
// The page hands the config in a JSON script, which a content security
|
|
405
|
+
// policy allows where an inline script is not.
|
|
406
|
+
const config = document.getElementById('quo');
|
|
407
|
+
if (config?.textContent)
|
|
408
|
+
void start(JSON.parse(config.textContent));
|
|
409
|
+
export { USER };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Json } from '@nervur-org/nervur';
|
|
2
|
+
export type Role = 'title' | 'lead' | 'body' | 'quiet' | 'label';
|
|
3
|
+
export type As = 'auto' | 'text' | 'table' | 'cards' | 'list' | 'people' | 'feed';
|
|
4
|
+
export type Layout = 'list' | 'grid' | 'hero' | 'feed';
|
|
5
|
+
export type Node = {
|
|
6
|
+
kind: 'stack';
|
|
7
|
+
of: Node[];
|
|
8
|
+
as?: Layout;
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'row';
|
|
11
|
+
of: Node[];
|
|
12
|
+
as?: 'person';
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'text';
|
|
15
|
+
text: string;
|
|
16
|
+
role?: Role;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'image';
|
|
19
|
+
src: string;
|
|
20
|
+
alt?: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'link';
|
|
23
|
+
href: string;
|
|
24
|
+
text?: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'form';
|
|
27
|
+
ask: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'answer';
|
|
30
|
+
ask: string;
|
|
31
|
+
as?: As;
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'standing';
|
|
34
|
+
id: string;
|
|
35
|
+
as?: 'door';
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'standings';
|
|
38
|
+
};
|
|
39
|
+
export declare function sanitiseTree(v: Json, depth?: number): Node | null;
|
|
40
|
+
export declare function answersIn(n: Node, out?: string[]): string[];
|
|
41
|
+
export type Painter = {
|
|
42
|
+
link(href: string, text: string): string;
|
|
43
|
+
form(ask: string): string;
|
|
44
|
+
answer(ask: string, as: As): string;
|
|
45
|
+
standing(id: string, as?: 'door'): string;
|
|
46
|
+
standings(): string;
|
|
47
|
+
};
|
|
48
|
+
export declare function titled(n: Node | null, depth?: number): boolean;
|
|
49
|
+
export declare function paint(n: Node, p: Painter): string;
|
|
50
|
+
export declare const nobody: Painter;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { escape } from './html.js';
|
|
2
|
+
const ROLES = new Set(['title', 'lead', 'body', 'quiet', 'label']);
|
|
3
|
+
const AS = new Set(['auto', 'text', 'table', 'cards', 'list', 'people', 'feed']);
|
|
4
|
+
const LAYOUTS = new Set(['list', 'grid', 'hero', 'feed']);
|
|
5
|
+
const TEXT = 4000; // characters a text node may carry
|
|
6
|
+
const DEPTH = 12; // how deep a tree may nest
|
|
7
|
+
const WIDE = 200; // children a container may have
|
|
8
|
+
// A name in the grammar is a name in her describe, spelled as she spells
|
|
9
|
+
// it: an ask of her own, a standing's id, or a carried ask, `<id>-<name>`.
|
|
10
|
+
// A standing's id is the dock's to choose and carries a colon where the
|
|
11
|
+
// dock's own convention puts one, a way back to a device being `to:<id>`,
|
|
12
|
+
// so a shape that refused a colon would drop the section a device's own
|
|
13
|
+
// faculties are painted in. It stays a name and never a path: no slash, no
|
|
14
|
+
// space, and it is looked up in her describe before anything is painted.
|
|
15
|
+
const NAME = /^[\w.:-]{1,80}$/;
|
|
16
|
+
const HREF = 2000; // characters an address may be
|
|
17
|
+
// A value held to the grammar, or null. Children that fail are dropped and
|
|
18
|
+
// the container kept; a container with nothing left is kept empty, since an
|
|
19
|
+
// empty region is a design and not a fault.
|
|
20
|
+
export function sanitiseTree(v, depth = 0) {
|
|
21
|
+
if (depth > DEPTH || v === null || typeof v !== 'object' || Array.isArray(v))
|
|
22
|
+
return null;
|
|
23
|
+
const o = v;
|
|
24
|
+
switch (o.kind) {
|
|
25
|
+
case 'stack':
|
|
26
|
+
case 'row': {
|
|
27
|
+
const of = Array.isArray(o.of) ? o.of.slice(0, WIDE).map((c) => sanitiseTree(c, depth + 1)).filter((c) => c !== null) : [];
|
|
28
|
+
if (o.kind === 'row')
|
|
29
|
+
return typeof o.as === 'string' && o.as === 'person' ? { kind: 'row', of, as: 'person' } : { kind: 'row', of };
|
|
30
|
+
return typeof o.as === 'string' && LAYOUTS.has(o.as) ? { kind: 'stack', of, as: o.as } : { kind: 'stack', of };
|
|
31
|
+
}
|
|
32
|
+
case 'text': {
|
|
33
|
+
if (typeof o.text !== 'string')
|
|
34
|
+
return null;
|
|
35
|
+
const n = { kind: 'text', text: o.text.slice(0, TEXT) };
|
|
36
|
+
if (typeof o.role === 'string' && ROLES.has(o.role))
|
|
37
|
+
n.role = o.role;
|
|
38
|
+
return n;
|
|
39
|
+
}
|
|
40
|
+
case 'image': {
|
|
41
|
+
if (typeof o.src !== 'string' || !/^data:image\/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]+$/i.test(o.src))
|
|
42
|
+
return null;
|
|
43
|
+
const n = { kind: 'image', src: o.src };
|
|
44
|
+
if (typeof o.alt === 'string')
|
|
45
|
+
n.alt = o.alt.slice(0, 200);
|
|
46
|
+
return n;
|
|
47
|
+
}
|
|
48
|
+
case 'link': {
|
|
49
|
+
// The shape only: an absolute address on the web, parsed rather than
|
|
50
|
+
// matched, so nothing that merely looks like one gets through. Where
|
|
51
|
+
// it may point is the painter's, not this file's.
|
|
52
|
+
if (typeof o.href !== 'string' || o.href.length > HREF)
|
|
53
|
+
return null;
|
|
54
|
+
let url;
|
|
55
|
+
try {
|
|
56
|
+
url = new URL(o.href);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
62
|
+
return null;
|
|
63
|
+
const n = { kind: 'link', href: url.href };
|
|
64
|
+
if (typeof o.text === 'string' && o.text.trim())
|
|
65
|
+
n.text = o.text.slice(0, 200);
|
|
66
|
+
return n;
|
|
67
|
+
}
|
|
68
|
+
case 'form':
|
|
69
|
+
return typeof o.ask === 'string' && NAME.test(o.ask) ? { kind: 'form', ask: o.ask } : null;
|
|
70
|
+
case 'answer': {
|
|
71
|
+
if (typeof o.ask !== 'string' || !NAME.test(o.ask))
|
|
72
|
+
return null;
|
|
73
|
+
const n = { kind: 'answer', ask: o.ask };
|
|
74
|
+
if (typeof o.as === 'string' && AS.has(o.as))
|
|
75
|
+
n.as = o.as;
|
|
76
|
+
return n;
|
|
77
|
+
}
|
|
78
|
+
case 'standing':
|
|
79
|
+
if (typeof o.id !== 'string' || !NAME.test(o.id))
|
|
80
|
+
return null;
|
|
81
|
+
return o.as === 'door' ? { kind: 'standing', id: o.id, as: 'door' } : { kind: 'standing', id: o.id };
|
|
82
|
+
case 'standings':
|
|
83
|
+
return { kind: 'standings' };
|
|
84
|
+
default:
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// The names a tree asks the answers of: what a side runs on her behalf so
|
|
89
|
+
// the page opens with what it shows, when the ask needs nothing typed.
|
|
90
|
+
export function answersIn(n, out = []) {
|
|
91
|
+
if (n.kind === 'answer')
|
|
92
|
+
out.push(n.ask);
|
|
93
|
+
if (n.kind === 'stack' || n.kind === 'row')
|
|
94
|
+
for (const c of n.of)
|
|
95
|
+
answersIn(c, out);
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
const ROLE_TAG = { title: 'h1', lead: 'p', body: 'p', quiet: 'p', label: 'p' };
|
|
99
|
+
// Whether a tree opens with a title of its own, at its top or one level
|
|
100
|
+
// in: then the page's bar says no name, since the page says it once.
|
|
101
|
+
export function titled(n, depth = 0) {
|
|
102
|
+
if (!n || depth > 2)
|
|
103
|
+
return false;
|
|
104
|
+
if (n.kind === 'text')
|
|
105
|
+
return n.role === 'title';
|
|
106
|
+
if (n.kind === 'stack' || n.kind === 'row')
|
|
107
|
+
return n.of.some((c) => titled(c, depth + 1));
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
// Each kind on the kit's pieces, and the painter holds the opinion: a list
|
|
111
|
+
// wraps each child as a row, a grid as a card, a person puts her picture in
|
|
112
|
+
// the round place a face has.
|
|
113
|
+
export function paint(n, p) {
|
|
114
|
+
switch (n.kind) {
|
|
115
|
+
case 'stack': {
|
|
116
|
+
const inner = (wrap) => n.of.map((c) => `<div class="${wrap}">${paint(c, p)}</div>`).join('');
|
|
117
|
+
if (n.as === 'list')
|
|
118
|
+
return `<div class="nv-list">${inner('nv-item')}</div>`;
|
|
119
|
+
if (n.as === 'grid')
|
|
120
|
+
return `<div class="nv-cards">${inner('nv-card')}</div>`;
|
|
121
|
+
if (n.as === 'hero')
|
|
122
|
+
return `<div class="nv-hero">${n.of.map((c) => paint(c, p)).join('')}</div>`;
|
|
123
|
+
if (n.as === 'feed')
|
|
124
|
+
return `<div class="nv-feed">${n.of.map((c) => paint(c, p)).join('')}</div>`;
|
|
125
|
+
return `<div class="nv-stack">${n.of.map((c) => paint(c, p)).join('')}</div>`;
|
|
126
|
+
}
|
|
127
|
+
case 'row': {
|
|
128
|
+
if (n.as === 'person') {
|
|
129
|
+
const face = n.of.find((c) => c.kind === 'image');
|
|
130
|
+
const rest = n.of.filter((c) => c !== face).map((c) => paint(c, p)).join('');
|
|
131
|
+
const words = n.of.find((c) => c.kind === 'text');
|
|
132
|
+
const initial = words && words.kind === 'text' ? escape(words.text.trim().charAt(0).toUpperCase()) : '';
|
|
133
|
+
return `<div class="nv-person"><span class="nv-avatar">${face ? paint(face, p) : initial}</span><div class="nv-stack">${rest}</div></div>`;
|
|
134
|
+
}
|
|
135
|
+
return `<div class="nv-row" data-align="center">${n.of.map((c) => paint(c, p)).join('')}</div>`;
|
|
136
|
+
}
|
|
137
|
+
case 'text': {
|
|
138
|
+
const role = n.role ?? 'body';
|
|
139
|
+
return `<${ROLE_TAG[role]} class="nv-t-${role}">${escape(n.text)}</${ROLE_TAG[role]}>`;
|
|
140
|
+
}
|
|
141
|
+
case 'image':
|
|
142
|
+
return `<img class="nv-picture" alt="${escape(n.alt ?? '')}" src="${n.src}">`;
|
|
143
|
+
case 'link':
|
|
144
|
+
return p.link(n.href, n.text ?? n.href);
|
|
145
|
+
case 'form':
|
|
146
|
+
return p.form(n.ask);
|
|
147
|
+
case 'answer':
|
|
148
|
+
return p.answer(n.ask, n.as ?? 'auto');
|
|
149
|
+
case 'standing':
|
|
150
|
+
return p.standing(n.id, n.as);
|
|
151
|
+
case 'standings':
|
|
152
|
+
return p.standings();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// A painter with nothing behind it: for a page that names no ask, the door
|
|
156
|
+
// page and any page painted before anyone is in.
|
|
157
|
+
export const nobody = { link: () => '', form: () => '', answer: () => '', standing: () => '', standings: () => '' };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Invitation, JsonObject } from '@nervur-org/nervur';
|
|
2
|
+
import type { Harbor } from '@nervur-org/nervur/harbor';
|
|
3
|
+
import { type Token } from '../dock/index.ts';
|
|
4
|
+
export declare const webToken: (endpoint: string, p256dh?: string, auth?: string) => {
|
|
5
|
+
kind: string;
|
|
6
|
+
endpoint: string;
|
|
7
|
+
p256dh?: string | undefined;
|
|
8
|
+
auth?: string | undefined;
|
|
9
|
+
};
|
|
10
|
+
export type Bell = {
|
|
11
|
+
pk: string;
|
|
12
|
+
at: string;
|
|
13
|
+
};
|
|
14
|
+
export declare const DOORBELL = "doorbell";
|
|
15
|
+
export type Held = {
|
|
16
|
+
keep(token: Token): Promise<boolean>;
|
|
17
|
+
admit(): Promise<{
|
|
18
|
+
invitation: Invitation;
|
|
19
|
+
banner: string;
|
|
20
|
+
} | null>;
|
|
21
|
+
};
|
|
22
|
+
export declare function doorbell(harbor: Harbor, bell?: Bell): Promise<Held | null>;
|
|
23
|
+
export declare function told(rel: {
|
|
24
|
+
call(name: string, args?: JsonObject): Promise<unknown>;
|
|
25
|
+
}, at: Held | null, token?: Token | null): Promise<string | null>;
|
|
26
|
+
export declare function subscribe(web: string, key: string, ask?: boolean): Promise<{
|
|
27
|
+
kind: string;
|
|
28
|
+
endpoint: string;
|
|
29
|
+
} | null>;
|
|
30
|
+
export declare function drained(harbor: Harbor, pks: string[]): Promise<JsonObject[]>;
|
|
31
|
+
export declare function notice(objects: JsonObject[]): {
|
|
32
|
+
title: string;
|
|
33
|
+
body: string;
|
|
34
|
+
};
|