@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,10 @@
|
|
|
1
|
+
export function local(ward, key) {
|
|
2
|
+
// A named ask names it; the empty ask says nothing at all, which is how
|
|
3
|
+
// the owner's ask says the empty ask too.
|
|
4
|
+
const one = (method, args = {}) => ward.ask('ask', method === undefined ? { being: key } : { being: key, method, args });
|
|
5
|
+
return {
|
|
6
|
+
tools: async () => (await one()),
|
|
7
|
+
call: (name, args = {}) => one(name, args),
|
|
8
|
+
ears: new Set(),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const RING = "<circle cx=\"32\" cy=\"32\" r=\"22\" stroke-dasharray=\"110 30\" transform=\"rotate(-60 32 32)\"/><circle cx=\"32\" cy=\"32\" r=\"5\" fill=\"currentColor\" stroke=\"none\"/>";
|
|
2
|
+
export declare const MARK_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"4\" stroke-linecap=\"round\"><circle cx=\"32\" cy=\"32\" r=\"22\" stroke-dasharray=\"110 30\" transform=\"rotate(-60 32 32)\"/><circle cx=\"32\" cy=\"32\" r=\"5\" fill=\"currentColor\" stroke=\"none\"/></svg>";
|
|
3
|
+
export declare const MARK: string;
|
|
4
|
+
export declare const ICON_GROUND = "#3b6ef5";
|
|
5
|
+
export declare const ICON_MARK = "#fbfaf7";
|
|
6
|
+
export declare function markLayer(fraction?: number): string;
|
|
7
|
+
export declare function groundLayer(): string;
|
|
8
|
+
export declare function iconSVG(radius?: number): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The mark: a ring with a gap and a point at the centre, one being's voice
|
|
3
|
+
// reaching another's door. It is the one mark every Quo app wears, on the
|
|
4
|
+
// door page, on the desk app's dock icon and on the phone's home screen,
|
|
5
|
+
// and it is drawn once here so those can never drift apart.
|
|
6
|
+
//
|
|
7
|
+
// The glyph is strokes on nothing, coloured by whoever draws it: the door
|
|
8
|
+
// page inherits `currentColor` and the app icons are given the two colours
|
|
9
|
+
// below. `app/icon/build.ts` is what makes the icons of it.
|
|
10
|
+
export const RING = '<circle cx="32" cy="32" r="22" stroke-dasharray="110 30" transform="rotate(-60 32 32)"/><circle cx="32" cy="32" r="5" fill="currentColor" stroke="none"/>';
|
|
11
|
+
// The mark as a page uses it: strokes in the colour of the text around it.
|
|
12
|
+
export const MARK_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round">${RING}</svg>`;
|
|
13
|
+
// And as a data URI, since a look carries a logo that way and no request
|
|
14
|
+
// for an image ever leaves a page.
|
|
15
|
+
export const MARK = 'data:image/svg+xml;base64,' + btoa(MARK_SVG);
|
|
16
|
+
// The app icon's two colours, the dock's own accent and paper: an icon is
|
|
17
|
+
// the product's palette or it is somebody else's product.
|
|
18
|
+
export const ICON_GROUND = '#3b6ef5';
|
|
19
|
+
export const ICON_MARK = '#fbfaf7';
|
|
20
|
+
// The mark alone on nothing, at the size a layer wants it. Android draws an
|
|
21
|
+
// icon in two layers and crops both to whatever shape the phone likes, a
|
|
22
|
+
// circle among them, so its foreground is the mark small enough to survive
|
|
23
|
+
// the crop, roughly the middle half, and its background is the ground.
|
|
24
|
+
export function markLayer(fraction = 0.5) {
|
|
25
|
+
const scale = fraction;
|
|
26
|
+
const shift = (64 - 64 * scale) / 2;
|
|
27
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><g transform="translate(${shift} ${shift}) scale(${scale})" fill="none" stroke="${ICON_MARK}" stroke-width="4" stroke-linecap="round" color="${ICON_MARK}">${RING}</g></svg>`;
|
|
28
|
+
}
|
|
29
|
+
export function groundLayer() {
|
|
30
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" fill="${ICON_GROUND}"/></svg>`;
|
|
31
|
+
}
|
|
32
|
+
// The mark as an app icon, on its ground. `radius` is the corner in the
|
|
33
|
+
// viewBox's own units: 0 for a full square, which is what iOS and Android
|
|
34
|
+
// want since the platform masks it, and a rounded one for macOS, which
|
|
35
|
+
// masks nothing and where every icon in the dock is a rounded square with
|
|
36
|
+
// air around it.
|
|
37
|
+
export function iconSVG(radius = 0) {
|
|
38
|
+
const inset = radius === 0 ? 0 : 5;
|
|
39
|
+
const size = 64 - inset * 2;
|
|
40
|
+
const glyph = radius === 0 ? '' : ` transform="translate(${inset} ${inset}) scale(${size / 64})"`;
|
|
41
|
+
return [
|
|
42
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">',
|
|
43
|
+
`<rect x="${inset}" y="${inset}" width="${size}" height="${size}" rx="${radius}" fill="${ICON_GROUND}"/>`,
|
|
44
|
+
`<g${glyph} fill="none" stroke="${ICON_MARK}" stroke-width="4" stroke-linecap="round" color="${ICON_MARK}">${RING}</g>`,
|
|
45
|
+
'</svg>',
|
|
46
|
+
].join('');
|
|
47
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Harbor } from '@nervur-org/nervur/harbor';
|
|
2
|
+
import type { Invitation } from '@nervur-org/nervur';
|
|
3
|
+
import { type World } from './worlds.ts';
|
|
4
|
+
export declare const COURIER = "courier";
|
|
5
|
+
export type Giving = {
|
|
6
|
+
offer: string;
|
|
7
|
+
invitation: Invitation;
|
|
8
|
+
moved: Promise<string>;
|
|
9
|
+
close(): Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
export declare function giveWorld(harbor: Harbor, world: World, at: string): Promise<Giving>;
|
|
12
|
+
export declare function takeWorld(harbor: Harbor, invitation: Invitation): Promise<{
|
|
13
|
+
took: string;
|
|
14
|
+
world: World;
|
|
15
|
+
} | {
|
|
16
|
+
error: string;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { arithmetic } from '@nervur-org/nervur/ward';
|
|
2
|
+
import { Courier, MOVER } from '../beings/courier.js';
|
|
3
|
+
import { link } from '../beings/link.js';
|
|
4
|
+
import { localName } from './worlds.js';
|
|
5
|
+
const { hex, unhex } = arithmetic;
|
|
6
|
+
// The ward a device's courier lives in, minted when a move starts and gone
|
|
7
|
+
// when it ends: nothing of a move outlives it.
|
|
8
|
+
export const COURIER = 'courier';
|
|
9
|
+
const KEY = 'courier';
|
|
10
|
+
async function courier(harbor) {
|
|
11
|
+
const ward = harbor.wards.get(COURIER) ?? (await harbor.create(COURIER, 'me'));
|
|
12
|
+
if (!ward.being(KEY)) {
|
|
13
|
+
const out = (await ward.ask('boot', { key: KEY, class: 'Courier' }));
|
|
14
|
+
if (out.error)
|
|
15
|
+
throw new Error(`no courier on this device: ${out.error}`);
|
|
16
|
+
}
|
|
17
|
+
return ward.being(KEY);
|
|
18
|
+
}
|
|
19
|
+
const away = async (harbor) => {
|
|
20
|
+
const c = harbor.wards.get(COURIER)?.being(KEY);
|
|
21
|
+
if (c) {
|
|
22
|
+
c.giving = undefined;
|
|
23
|
+
c.gone = undefined;
|
|
24
|
+
}
|
|
25
|
+
await harbor.drop(COURIER);
|
|
26
|
+
};
|
|
27
|
+
// This device, offering one world to whichever device takes the offer.
|
|
28
|
+
// `at` is the rendezvous this device dials, which is the world's own `quo.`
|
|
29
|
+
// route: the other device has no reach to anything yet and this is what it
|
|
30
|
+
// dials to answer.
|
|
31
|
+
export async function giveWorld(harbor, world, at) {
|
|
32
|
+
const name = localName(world.pk);
|
|
33
|
+
if (!harbor.wards.has(name))
|
|
34
|
+
throw new Error('this device does not hold that world');
|
|
35
|
+
const c = await courier(harbor);
|
|
36
|
+
const invitation = await c.invite(MOVER);
|
|
37
|
+
if (!invitation) {
|
|
38
|
+
await away(harbor);
|
|
39
|
+
throw new Error('this device is already offering a world');
|
|
40
|
+
}
|
|
41
|
+
let landed = () => { };
|
|
42
|
+
const moved = new Promise((ok) => (landed = ok));
|
|
43
|
+
c.giving = async () => {
|
|
44
|
+
const ward = harbor.wards.get(name);
|
|
45
|
+
if (!ward)
|
|
46
|
+
return { error: 'this device no longer holds that world' };
|
|
47
|
+
await ward.save();
|
|
48
|
+
const kept = await harbor.store.load(name);
|
|
49
|
+
if (!kept)
|
|
50
|
+
return { error: 'this device no longer holds that world' };
|
|
51
|
+
// The partition crosses as the values it is: a ward's partition is
|
|
52
|
+
// I-JSON already, since that is all a ward ever writes into one.
|
|
53
|
+
return { world: world.pk, at: world.at, name: world.name, seed: hex(kept.seed), partition: kept.partition, record: kept.record };
|
|
54
|
+
};
|
|
55
|
+
c.gone = async () => {
|
|
56
|
+
const gone = await harbor.drop(name);
|
|
57
|
+
if (!gone)
|
|
58
|
+
return { error: 'this device no longer holds that world' };
|
|
59
|
+
landed(world.pk);
|
|
60
|
+
return { deleted: world.pk };
|
|
61
|
+
};
|
|
62
|
+
await harbor.wards.get(COURIER).save();
|
|
63
|
+
return { offer: link(at, invitation), invitation, moved, close: () => away(harbor) };
|
|
64
|
+
}
|
|
65
|
+
// This device, taking the world the other one offers. The harbor must
|
|
66
|
+
// already be dialing the reach note in the offer: dialing is the shell's,
|
|
67
|
+
// since only a shell knows which harbor its terrain gave it.
|
|
68
|
+
export async function takeWorld(harbor, invitation) {
|
|
69
|
+
const c = await courier(harbor);
|
|
70
|
+
try {
|
|
71
|
+
const moving = await c.fetch(invitation);
|
|
72
|
+
if ('error' in moving)
|
|
73
|
+
return moving;
|
|
74
|
+
const name = localName(moving.world);
|
|
75
|
+
if (harbor.wards.has(name))
|
|
76
|
+
return { error: 'this device is already in that world' };
|
|
77
|
+
const hosted = await harbor.adopt(name, { seed: unhex(moving.seed), partition: moving.partition, record: moving.record });
|
|
78
|
+
// it is here: the other device may let go, and if that word never
|
|
79
|
+
// arrives the human is told, since two copies is what the next move
|
|
80
|
+
// has to be told about
|
|
81
|
+
const let_go = await c.landed();
|
|
82
|
+
return 'error' in let_go ? { error: `it is here, but the other device still has it: ${let_go.error}` } : { took: hosted.pk, world: { pk: moving.world, at: moving.at, name: moving.name } };
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await away(harbor);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Serving, type Subject } from '../beings/side.ts';
|
|
2
|
+
import type { Invitation } from '@nervur-org/nervur';
|
|
3
|
+
import { type Model, type Raw } from './html.ts';
|
|
4
|
+
export type Surface = {
|
|
5
|
+
show(html: string): void;
|
|
6
|
+
onSubmit: ((ask: string, raw: Raw) => void) | undefined;
|
|
7
|
+
};
|
|
8
|
+
export type Options = {
|
|
9
|
+
after?: () => Promise<void>;
|
|
10
|
+
notice?: string;
|
|
11
|
+
admit?: (invitation: Invitation) => Promise<void>;
|
|
12
|
+
focus?: string;
|
|
13
|
+
at?: string;
|
|
14
|
+
ended?: () => Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
export declare function screenSide(avatar: Subject, surface: Surface, options?: Options): Promise<Serving & {
|
|
17
|
+
model: Model;
|
|
18
|
+
refresh(): Promise<void>;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The human side: one avatar, one surface. The surface is the least a
|
|
3
|
+
// screen can be: it shows a page and hands back a submitted form, and that
|
|
4
|
+
// is all this side asks of a terrain. A DOM is one surface; a test is
|
|
5
|
+
// another. The side keeps the model, speaks it as a page after every change
|
|
6
|
+
// through `html.ts`, calls the avatar when a form comes back, and re-asks
|
|
7
|
+
// her describe after each call, so a digest that moved is a page that
|
|
8
|
+
// moved. A push is appended and shown the moment it lands.
|
|
9
|
+
import { digest } from '@nervur-org/nervur';
|
|
10
|
+
import { word } from '../beings/side.js';
|
|
11
|
+
import { isInvitation } from '@nervur-org/nervur';
|
|
12
|
+
import { page, values, hintFor, mine, asks, standing, stands } from './html.js';
|
|
13
|
+
import { sanitiseTree, answersIn } from './tree.js';
|
|
14
|
+
import { groups, sanitise } from '../beings/look.js';
|
|
15
|
+
import { isSilence, isWord } from '@nervur-org/nervur';
|
|
16
|
+
export async function screenSide(avatar, surface, options = {}) {
|
|
17
|
+
const after = options.after ?? (async () => { });
|
|
18
|
+
const notice = options.notice ?? '';
|
|
19
|
+
const model = { blueprint: null, look: {}, tree: null, pages: {}, notice, standing: 'in', answers: {}, pushes: [], ...(options.focus !== undefined ? { focus: options.focus } : {}), ...(options.at !== undefined ? { at: options.at } : {}) };
|
|
20
|
+
let seen = null;
|
|
21
|
+
const show = () => surface.show(page(model));
|
|
22
|
+
// Her describe, again: the page follows the digest.
|
|
23
|
+
const refresh = async () => {
|
|
24
|
+
const bp = await avatar.tools();
|
|
25
|
+
if ('error' in bp && !('asks' in bp)) {
|
|
26
|
+
// the last page she gave stays, with the answers under it, and the
|
|
27
|
+
// notice says where the human stands now: her relation ended, the
|
|
28
|
+
// world away for the moment, or her own error, told apart because
|
|
29
|
+
// only one of the three is over
|
|
30
|
+
model.standing = standing(bp.error);
|
|
31
|
+
model.notice = stands(bp.error);
|
|
32
|
+
if (model.standing === 'ended')
|
|
33
|
+
await options.ended?.();
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
model.standing = 'in';
|
|
37
|
+
const d = await digest(bp);
|
|
38
|
+
const moved = d !== seen;
|
|
39
|
+
if (moved) {
|
|
40
|
+
seen = d;
|
|
41
|
+
model.blueprint = bp;
|
|
42
|
+
for (const k of Object.keys(model.answers))
|
|
43
|
+
if (!bp.asks.some((a) => a.name === k))
|
|
44
|
+
delete model.answers[k];
|
|
45
|
+
// her look, once per digest: how she wants her page painted, and what each ask is
|
|
46
|
+
if (bp.asks.some((a) => a.name === 'look')) {
|
|
47
|
+
const l = await avatar.call('look');
|
|
48
|
+
model.look = isSilence(l) || isWord(l) ? {} : sanitise(l);
|
|
49
|
+
}
|
|
50
|
+
else
|
|
51
|
+
model.look = {};
|
|
52
|
+
}
|
|
53
|
+
// her page, after every ask: a tree of values from what she holds now, or nothing and her asks
|
|
54
|
+
// are forms. A cell moved is a page moved, and her describe need not have; so the answers the
|
|
55
|
+
// page shows are run again with it, and the page never stands more than one ask behind her
|
|
56
|
+
const trees = () => [model.tree ? answersIn(model.tree) : [], ...Object.entries(model.pages).map(([id, t]) => (t ? answersIn(t).map((n) => `${id}-${n}`) : []))].flat();
|
|
57
|
+
const shows = new Set(trees());
|
|
58
|
+
const tree = async (name) => {
|
|
59
|
+
const t = await avatar.call(name);
|
|
60
|
+
return isSilence(t) || isWord(t) ? null : sanitiseTree(t);
|
|
61
|
+
};
|
|
62
|
+
model.tree = bp.asks.some((a) => a.name === 'page') ? await tree('page') : null;
|
|
63
|
+
// and the page of every standing she carries, the same way, painted in that standing's section;
|
|
64
|
+
// a name a carried page uses is one of that standing's asks, carried, and nothing else
|
|
65
|
+
model.pages = {};
|
|
66
|
+
for (const [id, g] of Object.entries(groups(bp)))
|
|
67
|
+
if (typeof g.page === 'string' && bp.asks.some((a) => a.name === g.page))
|
|
68
|
+
model.pages[id] = await tree(g.page);
|
|
69
|
+
const wanted = new Set(trees());
|
|
70
|
+
for (const n of shows)
|
|
71
|
+
if (wanted.has(n))
|
|
72
|
+
delete model.answers[n];
|
|
73
|
+
// a read-only ask that needs nothing typed is run on her behalf once per digest, and one her page
|
|
74
|
+
// shows the answer of after every ask, so the page opens and stays with what it shows
|
|
75
|
+
for (const a of bp.asks) {
|
|
76
|
+
// What the side fills for itself is not something a human has to
|
|
77
|
+
// type, so an ask whose only required argument is the moment has
|
|
78
|
+
// nothing left to ask and runs like one with no arguments at all.
|
|
79
|
+
const needs = asks(a).length > 0;
|
|
80
|
+
const run = wanted.has(a.name) || (moved && hintFor(bp, model.look, a.name).readOnly);
|
|
81
|
+
if (run && !needs && !(a.name in model.answers))
|
|
82
|
+
model.answers[a.name] = word(await avatar.call(a.name, mine(a)));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
await after();
|
|
86
|
+
show();
|
|
87
|
+
};
|
|
88
|
+
surface.onSubmit = (name, raw) => void submit(name, raw);
|
|
89
|
+
const submit = async (name, raw) => {
|
|
90
|
+
const ask = model.blueprint?.asks.find((a) => a.name === name);
|
|
91
|
+
if (!ask)
|
|
92
|
+
return;
|
|
93
|
+
const v = values(ask, raw);
|
|
94
|
+
if ('error' in v) {
|
|
95
|
+
model.answers[name] = { word: 'error', value: { error: v.error } };
|
|
96
|
+
return show();
|
|
97
|
+
}
|
|
98
|
+
const out = await avatar.call(name, v.args);
|
|
99
|
+
await after();
|
|
100
|
+
if (options.admit && isInvitation(out))
|
|
101
|
+
return options.admit(out);
|
|
102
|
+
model.answers[name] = word(out);
|
|
103
|
+
await refresh();
|
|
104
|
+
};
|
|
105
|
+
const ear = (object) => {
|
|
106
|
+
model.pushes.push(object);
|
|
107
|
+
show();
|
|
108
|
+
};
|
|
109
|
+
avatar.ears.add(ear);
|
|
110
|
+
await refresh();
|
|
111
|
+
return {
|
|
112
|
+
model,
|
|
113
|
+
refresh,
|
|
114
|
+
close: async () => {
|
|
115
|
+
avatar.ears.delete(ear);
|
|
116
|
+
surface.onSubmit = undefined;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The stylesheet: the kit, and then what the screen alone has.
|
|
3
|
+
//
|
|
4
|
+
// One stylesheet for every screen there is, whatever carries it: the web
|
|
5
|
+
// route serves it beside the tab's bundle, and the desk app's window
|
|
6
|
+
// carries it in its own. A screen that looked like another product on
|
|
7
|
+
// another terrain would be two designs to keep and one of them always
|
|
8
|
+
// behind. It is one string and not four files because a screen builds its
|
|
9
|
+
// own page and has nowhere to put a link.
|
|
10
|
+
//
|
|
11
|
+
// The kit brings the tokens, its own light and dark face, the reset, the
|
|
12
|
+
// baseline, the bare controls and every piece a page is made of. The
|
|
13
|
+
// painter emits those pieces and nothing else, so what follows is small:
|
|
14
|
+
// the page's one column, and the few things the shells alone have, the
|
|
15
|
+
// door, a local being's section, moving a world, the daemon's state line.
|
|
16
|
+
// Nothing below mints a colour or a size; every one is a slot or a scale,
|
|
17
|
+
// so a face swapped for another swaps the screen with it. A far being's
|
|
18
|
+
// look sets the kit's slots on her own section, and the kit does the rest.
|
|
19
|
+
import { CSS as KIT, TOKENS, BASE, COMPONENTS } from '@nervur-org/ui/css';
|
|
20
|
+
const SCREEN = [
|
|
21
|
+
// One column that is a phone first and a desk after: the same page, more
|
|
22
|
+
// air. The insets are the phone's: an app fills the screen, corners and
|
|
23
|
+
// camera and all, so the first line of the page would otherwise sit under
|
|
24
|
+
// the clock. `env()` is zero everywhere else, so a tab and a window are
|
|
25
|
+
// laid out by the gutter alone.
|
|
26
|
+
'body{max-width:var(--nv-page-w-md);margin-inline:auto;padding-inline:calc(var(--nv-page-gutter) + env(safe-area-inset-left)) calc(var(--nv-page-gutter) + env(safe-area-inset-right));padding-block:env(safe-area-inset-top) calc(var(--nv-space-8) + env(safe-area-inset-bottom))}',
|
|
27
|
+
'main{display:flex;flex-direction:column;gap:var(--nv-space-4)}',
|
|
28
|
+
// a subsection holding a form and the answer under it, and the sections
|
|
29
|
+
// of one being's page after another
|
|
30
|
+
'main>.nv-stack>section+section,main>.nv-stack>section+.nv-standing,main>.nv-stack>.nv-standing+section{margin-block-start:var(--nv-space-3)}',
|
|
31
|
+
// a far being's logo beside her name, and the way to her own page
|
|
32
|
+
'img.logo{height:var(--nv-icon-lg)}a.open,a.back{text-decoration:none;font-weight:var(--nv-weight-medium)}a.back::before{content:"\\2190 "}',
|
|
33
|
+
// A place the side vouches for reads as a place to go. One it will not
|
|
34
|
+
// vouch for reads as what it is, an address the human may copy and weigh,
|
|
35
|
+
// and it is deliberately not underlined and not the accent: it must never
|
|
36
|
+
// look like something the page is willing to stand behind. A link that
|
|
37
|
+
// leaves the world wears the origin it goes to, out of the anchor's own
|
|
38
|
+
// data, so a human reads where she is going on the page rather than in a
|
|
39
|
+
// status bar she may never look at.
|
|
40
|
+
'a.link.out::after{content:" \\2197 " attr(data-origin);font-size:var(--nv-text-sm);color:var(--nv-color-fg-muted)}span.link.away{border:1px dashed var(--nv-color-border-strong)}',
|
|
41
|
+
// a being of the world running in the tab: her own section under the far page
|
|
42
|
+
'section.local{border-block-start:1px solid var(--nv-color-border);margin-block:var(--nv-space-5) 0;padding-block-start:var(--nv-space-3)}',
|
|
43
|
+
// moving a world to another device, and leaving one: quiet, at the foot
|
|
44
|
+
// of the page, and the offer wrapping rather than running off the side,
|
|
45
|
+
// since it is one long word
|
|
46
|
+
'.move,section.held{display:flex;flex-direction:column;gap:var(--nv-space-2);align-items:flex-start;border-block-start:1px solid var(--nv-color-border);margin-block:var(--nv-space-7) 0;padding-block-start:var(--nv-space-3);font-size:var(--nv-text-sm);color:var(--nv-color-fg-muted)}.move .invitation,section.held .invitation{font-family:var(--nv-font-mono);font-size:var(--nv-text-xs);overflow-wrap:anywhere}',
|
|
47
|
+
'section.held .worlds{display:flex;flex-wrap:wrap;gap:var(--nv-space-2)}',
|
|
48
|
+
// a foot the shell hid stays hidden: a class that lays it out would otherwise beat the attribute
|
|
49
|
+
'.move[hidden],section.held[hidden]{display:none}',
|
|
50
|
+
// the door page: one thing on air, vertically centred in the window
|
|
51
|
+
'main[data-door]{min-height:70vh;justify-content:center}main[data-door] .nv-picture{width:var(--nv-space-7);height:var(--nv-space-7);color:var(--nv-color-accent)}',
|
|
52
|
+
// the desk window: the state of the daemon behind the page, a line, and nothing when all is well
|
|
53
|
+
'p.state:empty{display:none}',
|
|
54
|
+
].join('');
|
|
55
|
+
export const CSS = `${KIT}\n${SCREEN}`;
|
|
56
|
+
// The same screen for a world that wears a design of its own: the kit
|
|
57
|
+
// without its own face, since the kit's contract says a surface loads the
|
|
58
|
+
// default face or a design's and never both, and the design's stylesheet
|
|
59
|
+
// is linked after it by the route that serves it.
|
|
60
|
+
export const BARE = `${TOKENS}\n${BASE}\n${COMPONENTS}\n${SCREEN}`;
|