@agentshouse/app-bridge 0.1.0-alpha.5 → 0.1.0-alpha.6
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/README.md +9 -2
- package/client/src/bridge.d.ts +2 -1
- package/client/src/bridge.js +3 -0
- package/client/src/test-host.d.ts +2 -1
- package/client/src/test-host.js +8 -0
- package/client/src/wire.d.ts +2 -0
- package/package.json +4 -2
- package/src/apps/tab.d.ts +6 -0
- package/src/apps/tab.js +27 -0
package/README.md
CHANGED
|
@@ -6,7 +6,12 @@ port's one listener. It carries four families:
|
|
|
6
6
|
|
|
7
7
|
- **Navigation** - the part of the shell's URL below the App's path, with its
|
|
8
8
|
query and fragment. The shell delivers it when the App starts and whenever the
|
|
9
|
-
User navigates; the App asks the shell to push or replace it.
|
|
9
|
+
User navigates; the App asks the shell to push or replace it. The App also
|
|
10
|
+
sets its tab's title and icon with `setTab({ title, icon })`, either or both:
|
|
11
|
+
a title of at most 200 characters and an icon given as a base64 `data:` URL
|
|
12
|
+
of an SVG, PNG, GIF or WebP image of at most 1,048,576 characters. The shell
|
|
13
|
+
shows them over the App's display name, or a Site's title, and its tab icon
|
|
14
|
+
until the next load, and ignores a request outside those bounds.
|
|
10
15
|
- **Ceremony** - the App asks the shell to leave for one of House's own
|
|
11
16
|
purpose-bound authenticated pages. The shell navigates only to a same-origin
|
|
12
17
|
absolute path, carries its current address as the return target, and the page
|
|
@@ -40,6 +45,7 @@ const md = connectMdRuntime(await bridge.runtime);
|
|
|
40
45
|
|
|
41
46
|
bridge.onAddress((address) => render(address));
|
|
42
47
|
bridge.onStatus((status) => showSync(status.sync));
|
|
48
|
+
bridge.setTab({ title: 'Atlas (3)' });
|
|
43
49
|
|
|
44
50
|
open('/rooms/atlas');
|
|
45
51
|
function open(address) {
|
|
@@ -81,4 +87,5 @@ admitted ceremony, refuses a push, replace or ceremony that is not a
|
|
|
81
87
|
same-origin absolute path, publishes a status, its `service_mode` included,
|
|
82
88
|
records in `consents` each
|
|
83
89
|
`gaConsent` it takes while that status measures and publishes the status the
|
|
84
|
-
choice leaves,
|
|
90
|
+
choice leaves, records in `tabs` each `setTab` the shell would take and ignores
|
|
91
|
+
any other, and attaches a Runtime port.
|
package/client/src/bridge.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HouseBridgePort, HouseGaChoice, HouseMessagePort, HouseStatus } from './wire.js';
|
|
1
|
+
import type { HouseBridgePort, HouseGaChoice, HouseMessagePort, HouseStatus, HouseTab } from './wire.js';
|
|
2
2
|
export declare class HouseCeremonyRefused extends Error {
|
|
3
3
|
readonly path: string | null;
|
|
4
4
|
constructor(path: string | null);
|
|
@@ -20,6 +20,7 @@ export interface HouseBridge {
|
|
|
20
20
|
push(address: string): Promise<string>;
|
|
21
21
|
replace(address: string): Promise<string>;
|
|
22
22
|
ceremony(path: string): Promise<never>;
|
|
23
|
+
setTab(tab: HouseTab): void;
|
|
23
24
|
readAttachment(room: string, attachment: string): Promise<HouseImageCopy | null>;
|
|
24
25
|
status(): HouseStatus | null;
|
|
25
26
|
onStatus(listener: (status: HouseStatus) => void): () => void;
|
package/client/src/bridge.js
CHANGED
|
@@ -134,6 +134,9 @@ export function connectHouseBridge(source = heldBridge()) {
|
|
|
134
134
|
send({ house: 'ceremony', path });
|
|
135
135
|
return pending.promise;
|
|
136
136
|
},
|
|
137
|
+
setTab(tab) {
|
|
138
|
+
send({ house: 'tab', title: tab.title, icon: tab.icon });
|
|
139
|
+
},
|
|
137
140
|
readAttachment(room, attachment) {
|
|
138
141
|
const pending = settled();
|
|
139
142
|
const id = ++nextAttachment;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HouseImageCopy } from './bridge.js';
|
|
2
|
-
import type { HouseBridgePort, HouseGaChoice, HouseMessagePort, HouseNavigationMode, HouseStatus } from './wire.js';
|
|
2
|
+
import type { HouseBridgePort, HouseGaChoice, HouseMessagePort, HouseNavigationMode, HouseStatus, HouseTab } from './wire.js';
|
|
3
3
|
export interface HouseBridgeNavigation {
|
|
4
4
|
readonly mode: HouseNavigationMode;
|
|
5
5
|
readonly address: string;
|
|
@@ -9,6 +9,7 @@ export interface HouseBridgeTestHost {
|
|
|
9
9
|
readonly navigations: readonly HouseBridgeNavigation[];
|
|
10
10
|
readonly ceremonies: readonly string[];
|
|
11
11
|
readonly consents: readonly HouseGaChoice[];
|
|
12
|
+
readonly tabs: readonly HouseTab[];
|
|
12
13
|
address(): string;
|
|
13
14
|
open(address: string): void;
|
|
14
15
|
publish(status: HouseStatus): void;
|
package/client/src/test-host.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { appAddressOf } from '../../src/apps/remainder.js';
|
|
2
|
+
import { tabChange } from '../../src/apps/tab.js';
|
|
2
3
|
import { HOUSE_ADDRESS_MAX_CHARS } from './wire.js';
|
|
3
4
|
const HOST_ORIGIN = 'https://house.invalid';
|
|
4
5
|
function sameOriginPath(value) {
|
|
@@ -15,6 +16,7 @@ export function createHouseBridgeTestHost(options = {}) {
|
|
|
15
16
|
const navigations = [];
|
|
16
17
|
const ceremonies = [];
|
|
17
18
|
const consents = [];
|
|
19
|
+
const tabs = [];
|
|
18
20
|
const listeners = new Set();
|
|
19
21
|
const queued = [];
|
|
20
22
|
let started = false;
|
|
@@ -77,6 +79,11 @@ export function createHouseBridgeTestHost(options = {}) {
|
|
|
77
79
|
else if (data.house === 'ceremony') {
|
|
78
80
|
ceremony(data.path);
|
|
79
81
|
}
|
|
82
|
+
else if (data.house === 'tab') {
|
|
83
|
+
const change = tabChange(data);
|
|
84
|
+
if (change !== null)
|
|
85
|
+
tabs.push(change);
|
|
86
|
+
}
|
|
80
87
|
else if (data.house === 'ga/consent') {
|
|
81
88
|
consent(data.choice);
|
|
82
89
|
}
|
|
@@ -123,6 +130,7 @@ export function createHouseBridgeTestHost(options = {}) {
|
|
|
123
130
|
navigations,
|
|
124
131
|
ceremonies,
|
|
125
132
|
consents,
|
|
133
|
+
tabs,
|
|
126
134
|
address() {
|
|
127
135
|
return address;
|
|
128
136
|
},
|
package/client/src/wire.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TabChange } from '../../src/apps/tab.js';
|
|
1
2
|
export interface HouseBridgeEvent {
|
|
2
3
|
readonly data: unknown;
|
|
3
4
|
readonly ports: readonly HouseMessagePort[];
|
|
@@ -35,4 +36,5 @@ export interface HouseStatus {
|
|
|
35
36
|
readonly service_mode: 'open' | 'write-halted' | null;
|
|
36
37
|
}
|
|
37
38
|
export type HouseNavigationMode = 'push' | 'replace';
|
|
39
|
+
export type HouseTab = TabChange;
|
|
38
40
|
export declare const HOUSE_ADDRESS_MAX_CHARS = 1000;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentshouse/app-bridge",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
4
|
-
"description": "Renderer-side client of the agents.house App bridge: navigation, ceremonies, status and Site image reads, with an in-page test host.",
|
|
3
|
+
"version": "0.1.0-alpha.6",
|
|
4
|
+
"description": "Renderer-side client of the agents.house App bridge: navigation, the tab title and icon, ceremonies, status and Site image reads, with an in-page test host.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
"client/src/*.d.ts",
|
|
24
24
|
"src/apps/remainder.js",
|
|
25
25
|
"src/apps/remainder.d.ts",
|
|
26
|
+
"src/apps/tab.js",
|
|
27
|
+
"src/apps/tab.d.ts",
|
|
26
28
|
"README.md"
|
|
27
29
|
]
|
|
28
30
|
}
|
package/src/apps/tab.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const TAB_TITLE_MAX_CHARS = 200;
|
|
2
|
+
const TAB_ICON_MAX_CHARS = 1_048_576;
|
|
3
|
+
function caseless(text) {
|
|
4
|
+
return text.replace(/[a-z]/gu, (letter) => `[${letter}${letter.toUpperCase()}]`);
|
|
5
|
+
}
|
|
6
|
+
export const TAB_IMAGE = new RegExp(`^${caseless('data:image/')}(?:svg\\+xml|${['png', 'gif', 'webp'].map(caseless).join('|')});${caseless('base64')},[A-Za-z0-9+/]+={0,2}$`, 'u');
|
|
7
|
+
function admittedTitle(title) {
|
|
8
|
+
return typeof title === 'string' && title.length <= TAB_TITLE_MAX_CHARS;
|
|
9
|
+
}
|
|
10
|
+
function admittedIcon(icon) {
|
|
11
|
+
return typeof icon === 'string' && icon.length <= TAB_ICON_MAX_CHARS && TAB_IMAGE.test(icon);
|
|
12
|
+
}
|
|
13
|
+
export function tabChange(asked) {
|
|
14
|
+
if (asked === null || typeof asked !== 'object')
|
|
15
|
+
return null;
|
|
16
|
+
const { title, icon } = asked;
|
|
17
|
+
if (title === undefined && icon === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
if (title !== undefined && !admittedTitle(title))
|
|
20
|
+
return null;
|
|
21
|
+
if (icon !== undefined && !admittedIcon(icon))
|
|
22
|
+
return null;
|
|
23
|
+
return {
|
|
24
|
+
...(title === undefined ? {} : { title: title }),
|
|
25
|
+
...(icon === undefined ? {} : { icon: icon }),
|
|
26
|
+
};
|
|
27
|
+
}
|