@web-applets/sdk 0.0.7 → 0.0.9
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 +100 -17
- package/dist/client.d.ts +12 -4
- package/dist/client.js +103 -57
- package/dist/context.d.ts +5 -2
- package/dist/context.js +44 -11
- package/dist/types.d.ts +25 -12
- package/dist/web-component/index.d.ts +1 -0
- package/dist/web-component/index.js +52 -0
- package/dist/web-components/applet-frame.d.ts +1 -0
- package/dist/web-components/applet-frame.js +52 -0
- package/package.json +2 -1
package/README.md
CHANGED
@@ -1,14 +1,96 @@
|
|
1
1
|
# Web Applets
|
2
2
|
|
3
|
-
> An open
|
3
|
+
> An open spec & SDK for creating apps that agents can use.
|
4
4
|
|
5
|
-
🔗 [
|
5
|
+
🔗 [Applets Repo](https://github.com/unternet-co/community-applets) | 🔗 [Mailing List](https://groups.google.com/a/unternet.co/g/community) | 🔗 [Applets Chat Demo](https://github.com/unternet-co/applets-chat)
|
6
6
|
|
7
7
|
## What is it?
|
8
8
|
|
9
|
-
Web Applets is
|
9
|
+
Web Applets is an open specification for building software that both humans and AI can understand and use together. Instead of forcing AI to operate traditional point-and-click apps built for humans, Web Applets creates a new kind of software designed for human-AI collaboration. Think of them a bit like artifacts, but they do stuff!
|
10
10
|
|
11
|
-
|
11
|
+

|
12
|
+
|
13
|
+
Web Applets are modular pieces of web software that:
|
14
|
+
|
15
|
+
- **Can be used directly by humans with rich, graphical interfaces**
|
16
|
+
- **Can be understood and operated by AI through a clear protocol**
|
17
|
+
- **Run locally in your environment, not on distant servers**
|
18
|
+
- **Share context and state with their environment**
|
19
|
+
- **Can be freely combined and composed**
|
20
|
+
|
21
|
+
Think of any web software you use today - maps, documents, shopping, calendars - and imagine if instead of visiting these as separate websites, you could pull them into your own environment where both you and AI could use them together seamlessly.
|
22
|
+
|
23
|
+
## Key Features
|
24
|
+
|
25
|
+
- **Built on Web Standards:** Create applets using familiar web technologies (HTML, CSS, JavaScript)
|
26
|
+
- **AI-Native Protocol:** Applets expose their state and actions in a way AI can understand and use
|
27
|
+
- **Rich Interfaces:** Full support for complex graphical UIs, not just text
|
28
|
+
- **Local-First:** Runs in your environment, keeping your data under your control
|
29
|
+
- **Composable:** Applets can work together, sharing context and state
|
30
|
+
- **Open Standard:** Designed for interoperability, not platform lock-in
|
31
|
+
|
32
|
+
Web Applets aims to do for AI-enabled software what the web did for documents - create an open platform where anyone can build, share, and connect applications. We believe the future of software should be built on open collaboration, not tight integration with closed platforms.
|
33
|
+
|
34
|
+
## Example
|
35
|
+
|
36
|
+
This is a simple applet that prints "Hello, [your name]" when given the `set_name` action.
|
37
|
+
|
38
|
+
`index.html`:
|
39
|
+
|
40
|
+
```html
|
41
|
+
<!DOCTYPE html>
|
42
|
+
<html lang="en">
|
43
|
+
<script src="./main.js" type="module"></script>
|
44
|
+
<body>
|
45
|
+
Hello! <span id="name">whoever you are</span>.
|
46
|
+
</body>
|
47
|
+
</html>
|
48
|
+
```
|
49
|
+
|
50
|
+
`main.js`:
|
51
|
+
|
52
|
+
```js
|
53
|
+
import { appletContext } from '@web-applets/sdk';
|
54
|
+
|
55
|
+
// Get view element we want to manipulate
|
56
|
+
const nameElem = document.getElementById('name');
|
57
|
+
|
58
|
+
// Connect to the applet context
|
59
|
+
const applet = appletContext.connect();
|
60
|
+
|
61
|
+
// When the set_name action is called, change the state
|
62
|
+
applet.setActionHandler('set_name', ({ name }) => {
|
63
|
+
applet.setState({ name });
|
64
|
+
});
|
65
|
+
|
66
|
+
// Whenever we get a request to render the view, update the name
|
67
|
+
applet.onrender = () => {
|
68
|
+
nameElem.innerText = applet.state?.name;
|
69
|
+
};
|
70
|
+
```
|
71
|
+
|
72
|
+
`manifest.json`:
|
73
|
+
|
74
|
+
```json
|
75
|
+
{
|
76
|
+
"type": "applet",
|
77
|
+
"name": "Hello World",
|
78
|
+
"description": "Displays a greeting to the user.",
|
79
|
+
"entrypoint": "index.html",
|
80
|
+
"actions": [
|
81
|
+
{
|
82
|
+
"id": "set_name",
|
83
|
+
"description": "Sets the name of the user to be greeted",
|
84
|
+
"params": {
|
85
|
+
"name": {
|
86
|
+
"type": "string",
|
87
|
+
"description": "The name of the user"
|
88
|
+
}
|
89
|
+
}
|
90
|
+
}
|
91
|
+
]
|
92
|
+
}
|
93
|
+
```
|
12
94
|
|
13
95
|
## Getting started
|
14
96
|
|
@@ -131,29 +213,30 @@ applet.state = { name: 'Ada Lovelace' };
|
|
131
213
|
It may also be helpful to check available applets at a domain, or in your public folder. For that you can extract the applet headers from the App Manifest at the public root (`/manifest.json`), and see the available applets and a shorthand for the actions you can take in them. This is automatically created when you build your applets.
|
132
214
|
|
133
215
|
```js
|
134
|
-
const
|
216
|
+
const applets = await applets.list('/');
|
135
217
|
```
|
136
218
|
|
137
|
-
This
|
219
|
+
This applets object looks like:
|
138
220
|
|
139
221
|
```js
|
140
|
-
|
141
|
-
{
|
222
|
+
{
|
223
|
+
'/helloworld.applet': {
|
142
224
|
name: 'Hello World',
|
143
225
|
description: 'Displays a greeting to the user.',
|
144
226
|
url: '/applets/helloworld.applet',
|
145
|
-
actions:
|
146
|
-
{
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
227
|
+
actions: {
|
228
|
+
set_name: {
|
229
|
+
description: 'Sets the name of the user to be greeted',
|
230
|
+
params: {
|
231
|
+
name: {
|
232
|
+
type: 'string',
|
233
|
+
description: 'The name of the user'
|
234
|
+
}
|
152
235
|
},
|
153
|
-
|
236
|
+
},
|
154
237
|
},
|
155
238
|
// ...
|
156
|
-
|
239
|
+
};
|
157
240
|
```
|
158
241
|
|
159
242
|
You can use it to present a quick summary of available tools to your model, and then decide on an applet and action to use.
|
package/dist/client.d.ts
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
-
import { AppletAction, AppletHeader, AppletMessage, ActionParams, AppletManifest } from './types';
|
2
|
-
export declare function
|
3
|
-
|
4
|
-
|
1
|
+
import { AppletAction, AppletHeader, AppletMessage, ActionParams, AppletManifest, AppletMessageType, AppletMessageCallback, AppletManifestDict } from './types';
|
2
|
+
export declare function list(url: string): Promise<AppletManifestDict>;
|
3
|
+
interface AppletOpts {
|
4
|
+
headless?: boolean;
|
5
|
+
unsafe?: boolean;
|
6
|
+
}
|
7
|
+
export declare function load(url: string, container?: HTMLIFrameElement, opts?: AppletOpts): Promise<Applet>;
|
5
8
|
export declare class Applet<T = unknown> extends EventTarget {
|
6
9
|
#private;
|
7
10
|
actions: AppletAction[];
|
@@ -20,5 +23,10 @@ export declare class Applet<T = unknown> extends EventTarget {
|
|
20
23
|
onstateupdated(event: CustomEvent): void;
|
21
24
|
disconnect(): void;
|
22
25
|
dispatchAction(actionId: string, params: ActionParams): Promise<AppletMessage<any>>;
|
26
|
+
send(message: AppletMessage): Promise<AppletMessage<any>>;
|
27
|
+
on(messageType: AppletMessageType, callback: AppletMessageCallback): Promise<void>;
|
23
28
|
}
|
24
29
|
export declare function loadManifest(url: string): Promise<AppletManifest>;
|
30
|
+
export declare function getHeaders(url: string): Promise<AppletHeader[]>;
|
31
|
+
export declare function getManifests(url: string): Promise<any[]>;
|
32
|
+
export {};
|
package/dist/client.js
CHANGED
@@ -9,73 +9,76 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
9
9
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
10
10
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
11
11
|
};
|
12
|
-
var _Applet_state;
|
12
|
+
var _Applet_instances, _Applet_state, _Applet_dispatchEvent;
|
13
13
|
import { AppletMessage, } from './types';
|
14
|
-
const
|
15
|
-
|
16
|
-
document.body.appendChild(
|
17
|
-
export async function
|
14
|
+
const hiddenContainer = document.createElement('iframe');
|
15
|
+
hiddenContainer.style.display = 'none';
|
16
|
+
document.body.appendChild(hiddenContainer);
|
17
|
+
export async function list(url) {
|
18
18
|
url = parseUrl(url);
|
19
19
|
try {
|
20
20
|
const request = await fetch(`${url}/manifest.json`);
|
21
21
|
const appManifest = await request.json();
|
22
|
-
const
|
23
|
-
|
22
|
+
const appletUrls = appManifest.applets;
|
23
|
+
const manifests = {};
|
24
|
+
const manifestRequests = appletUrls.map(async (appletUrl) => {
|
25
|
+
appletUrl = parseUrl(appletUrl, url);
|
26
|
+
const request = await fetch(`${appletUrl}/manifest.json`);
|
27
|
+
const manifest = await request.json();
|
28
|
+
manifests[appletUrl] = manifest;
|
29
|
+
});
|
30
|
+
await Promise.all(manifestRequests);
|
31
|
+
return manifests;
|
24
32
|
}
|
25
33
|
catch {
|
26
|
-
return
|
34
|
+
return {};
|
27
35
|
}
|
28
36
|
}
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
const request = await fetch(`${appletUrl}/manifest.json`);
|
36
|
-
return await request.json();
|
37
|
-
}));
|
38
|
-
return manifests ?? [];
|
39
|
-
}
|
40
|
-
export async function load(url, container) {
|
37
|
+
const defaultOpts = {
|
38
|
+
headless: false,
|
39
|
+
unsafe: false,
|
40
|
+
};
|
41
|
+
export async function load(url, container, opts) {
|
42
|
+
const _opts = Object.assign(defaultOpts, opts ?? {});
|
41
43
|
url = parseUrl(url);
|
42
44
|
const manifest = await loadManifest(`${url}`);
|
45
|
+
if (!container) {
|
46
|
+
container = hiddenContainer;
|
47
|
+
_opts.headless = true;
|
48
|
+
}
|
49
|
+
if (_opts.unsafe || manifest.unsafe) {
|
50
|
+
container.setAttribute('sandbox', 'allow-scripts allow-forms allow-same-origin');
|
51
|
+
}
|
52
|
+
else {
|
53
|
+
container.setAttribute('sandbox', 'allow-scripts allow-forms');
|
54
|
+
}
|
43
55
|
const applet = new Applet();
|
44
56
|
applet.manifest = manifest;
|
45
|
-
applet.actions = manifest.actions;
|
57
|
+
applet.actions = manifest.actions;
|
46
58
|
applet.container = container;
|
47
59
|
container.src = applet.manifest.entrypoint;
|
48
|
-
if (!container.isConnected)
|
49
|
-
hiddenRoot.appendChild(container);
|
50
60
|
return new Promise((resolve) => {
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
61
|
+
applet.on('ready', () => {
|
62
|
+
const initMessage = new AppletMessage('init', {
|
63
|
+
headless: _opts.headless,
|
64
|
+
});
|
65
|
+
applet.send(initMessage);
|
66
|
+
resolve(applet);
|
56
67
|
});
|
57
68
|
});
|
58
69
|
}
|
59
70
|
export class Applet extends EventTarget {
|
60
71
|
constructor() {
|
61
72
|
super();
|
73
|
+
_Applet_instances.add(this);
|
62
74
|
this.actions = [];
|
63
75
|
_Applet_state.set(this, void 0);
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
this.onstateupdated(message.data.state);
|
71
|
-
}
|
72
|
-
if (message.data.type === 'resize') {
|
73
|
-
this.resizeContainer(message.data.dimensions);
|
74
|
-
}
|
75
|
-
this.container.contentWindow?.postMessage({
|
76
|
-
type: 'resolve',
|
77
|
-
id: message.data.id,
|
78
|
-
}, '*');
|
76
|
+
this.on('state', (message) => {
|
77
|
+
__classPrivateFieldSet(this, _Applet_state, message.state, "f");
|
78
|
+
__classPrivateFieldGet(this, _Applet_instances, "m", _Applet_dispatchEvent).call(this, 'stateupdated', message.state);
|
79
|
+
});
|
80
|
+
this.on('resize', (message) => {
|
81
|
+
this.resizeContainer(message.dimensions);
|
79
82
|
});
|
80
83
|
}
|
81
84
|
get state() {
|
@@ -83,8 +86,7 @@ export class Applet extends EventTarget {
|
|
83
86
|
}
|
84
87
|
set state(state) {
|
85
88
|
__classPrivateFieldSet(this, _Applet_state, state, "f");
|
86
|
-
|
87
|
-
this.container.contentWindow?.postMessage(stateMessage.toJson(), '*');
|
89
|
+
this.send(new AppletMessage('state', { state }));
|
88
90
|
}
|
89
91
|
toJson() {
|
90
92
|
return Object.fromEntries(Object.entries(this).filter(([_, value]) => {
|
@@ -98,26 +100,30 @@ export class Applet extends EventTarget {
|
|
98
100
|
}));
|
99
101
|
}
|
100
102
|
resizeContainer(dimensions) {
|
101
|
-
this.container.style.height = `${dimensions.height}px`;
|
103
|
+
this.container.style.height = `${dimensions.height + 2}px`;
|
102
104
|
// if (!this.#styleOverrides) {
|
103
105
|
// this.#container.style.height = `${dimensions.height}px`;
|
104
106
|
// }
|
105
107
|
}
|
106
108
|
onstateupdated(event) { }
|
107
|
-
disconnect() {
|
109
|
+
disconnect() {
|
110
|
+
this.onstateupdated = () => { };
|
111
|
+
this.container.src = 'about:blank';
|
112
|
+
}
|
108
113
|
async dispatchAction(actionId, params) {
|
109
114
|
const requestMessage = new AppletMessage('action', {
|
110
115
|
actionId,
|
111
116
|
params,
|
112
117
|
});
|
113
|
-
this.
|
118
|
+
return await this.send(requestMessage);
|
119
|
+
}
|
120
|
+
async send(message) {
|
121
|
+
this.container.contentWindow?.postMessage(message.toJson(), '*');
|
114
122
|
return new Promise((resolve) => {
|
115
123
|
const listener = (messageEvent) => {
|
116
|
-
if (messageEvent.source !== this.container.contentWindow)
|
117
|
-
return;
|
118
124
|
const responseMessage = new AppletMessage(messageEvent.data.type, messageEvent.data);
|
119
125
|
if (responseMessage.type === 'resolve' &&
|
120
|
-
responseMessage.id ===
|
126
|
+
responseMessage.id === message.id) {
|
121
127
|
window.removeEventListener('message', listener);
|
122
128
|
resolve(responseMessage);
|
123
129
|
}
|
@@ -125,20 +131,37 @@ export class Applet extends EventTarget {
|
|
125
131
|
window.addEventListener('message', listener);
|
126
132
|
});
|
127
133
|
}
|
134
|
+
async on(messageType, callback) {
|
135
|
+
const listener = async (messageEvent) => {
|
136
|
+
if (messageEvent.source !== this.container.contentWindow)
|
137
|
+
return;
|
138
|
+
if (messageEvent.data.type !== messageType)
|
139
|
+
return;
|
140
|
+
const message = new AppletMessage(messageEvent.data.type, messageEvent.data);
|
141
|
+
await callback(message);
|
142
|
+
this.container.contentWindow?.postMessage(new AppletMessage('resolve', { id: message.id }), '*');
|
143
|
+
};
|
144
|
+
window.addEventListener('message', listener);
|
145
|
+
}
|
128
146
|
}
|
129
|
-
_Applet_state = new WeakMap()
|
147
|
+
_Applet_state = new WeakMap(), _Applet_instances = new WeakSet(), _Applet_dispatchEvent = function _Applet_dispatchEvent(id, detail) {
|
148
|
+
if (typeof this[`on${id}`] === 'function') {
|
149
|
+
this[`on${id}`](detail);
|
150
|
+
}
|
151
|
+
this.dispatchEvent(new CustomEvent(id, { detail }));
|
152
|
+
};
|
153
|
+
/* Helpers */
|
130
154
|
function parseUrl(url, base) {
|
131
155
|
if (['http', 'https'].includes(url.split('://')[0])) {
|
132
156
|
return url;
|
133
157
|
}
|
134
|
-
let path = url;
|
135
|
-
if (path.startsWith('/'))
|
136
|
-
path = path.slice(1);
|
137
|
-
if (path.endsWith('/'))
|
138
|
-
path = path.slice(0, -1);
|
158
|
+
let path = trimSlashes(url);
|
139
159
|
url = `${base || window.location.origin}/${path}`;
|
140
160
|
return url;
|
141
161
|
}
|
162
|
+
function trimSlashes(str) {
|
163
|
+
return str.replace(/^\/+|\/+$/g, '');
|
164
|
+
}
|
142
165
|
export async function loadManifest(url) {
|
143
166
|
url = parseUrl(url);
|
144
167
|
const request = await fetch(`${url}/manifest.json`);
|
@@ -149,3 +172,26 @@ export async function loadManifest(url) {
|
|
149
172
|
appletManifest.entrypoint = parseUrl(appletManifest.entrypoint, url);
|
150
173
|
return appletManifest;
|
151
174
|
}
|
175
|
+
export async function getHeaders(url) {
|
176
|
+
url = parseUrl(url);
|
177
|
+
try {
|
178
|
+
const request = await fetch(`${url}/manifest.json`);
|
179
|
+
const appManifest = await request.json();
|
180
|
+
const appletHeaders = appManifest.applets;
|
181
|
+
return appletHeaders ?? [];
|
182
|
+
}
|
183
|
+
catch {
|
184
|
+
return [];
|
185
|
+
}
|
186
|
+
}
|
187
|
+
export async function getManifests(url) {
|
188
|
+
url = parseUrl(url);
|
189
|
+
const request = await fetch(`${url}/manifest.json`);
|
190
|
+
const headers = (await request.json()).applets;
|
191
|
+
const manifests = await Promise.all(headers.map(async (header) => {
|
192
|
+
const appletUrl = parseUrl(header.url);
|
193
|
+
const request = await fetch(`${appletUrl}/manifest.json`);
|
194
|
+
return await request.json();
|
195
|
+
}));
|
196
|
+
return manifests ?? [];
|
197
|
+
}
|
package/dist/context.d.ts
CHANGED
@@ -3,12 +3,15 @@ import { ActionHandlerDict, AppletMessage, AppletMessageType, AppletMessageCallb
|
|
3
3
|
* Context
|
4
4
|
*/
|
5
5
|
export declare class AppletContext<StateType = any> extends EventTarget {
|
6
|
+
#private;
|
6
7
|
client: AppletClient;
|
7
8
|
actionHandlers: ActionHandlerDict;
|
8
|
-
|
9
|
+
headless: boolean;
|
9
10
|
connect(): this;
|
10
11
|
setActionHandler<T extends ActionParams>(actionId: string, handler: ActionHandler<T>): void;
|
11
|
-
|
12
|
+
set state(state: StateType);
|
13
|
+
get state(): StateType;
|
14
|
+
setState(state: StateType, shouldRender?: boolean): Promise<void>;
|
12
15
|
onload(): Promise<void> | void;
|
13
16
|
onready(): Promise<void> | void;
|
14
17
|
onrender(): void;
|
package/dist/context.js
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
2
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
3
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
4
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
5
|
+
};
|
6
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
7
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
10
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
11
|
+
};
|
12
|
+
var _AppletContext_state;
|
1
13
|
import { AppletMessage, } from './types';
|
2
14
|
/**
|
3
15
|
* Context
|
@@ -6,6 +18,8 @@ export class AppletContext extends EventTarget {
|
|
6
18
|
constructor() {
|
7
19
|
super(...arguments);
|
8
20
|
this.actionHandlers = {};
|
21
|
+
_AppletContext_state.set(this, void 0);
|
22
|
+
this.headless = false;
|
9
23
|
}
|
10
24
|
connect() {
|
11
25
|
this.client = new AppletClient();
|
@@ -34,11 +48,24 @@ export class AppletContext extends EventTarget {
|
|
34
48
|
}
|
35
49
|
});
|
36
50
|
resizeObserver.observe(document.querySelector('html'));
|
51
|
+
this.client.on('init', (message) => {
|
52
|
+
const initMessage = message;
|
53
|
+
this.headless = initMessage.headless;
|
54
|
+
});
|
37
55
|
this.client.on('state', (message) => {
|
38
56
|
if (!isStateMessage(message)) {
|
39
57
|
throw new TypeError("Message doesn't match type StateMessage");
|
40
58
|
}
|
41
|
-
|
59
|
+
// Don't render when state updates match the current state
|
60
|
+
// this retains cursor positions in text fields, for example
|
61
|
+
if (JSON.stringify(message.state) === JSON.stringify(__classPrivateFieldGet(this, _AppletContext_state, "f")))
|
62
|
+
return;
|
63
|
+
__classPrivateFieldSet(this, _AppletContext_state, message.state, "f");
|
64
|
+
// BUG: For some reason regular applets were loading headless, when instantiated not on a page reload
|
65
|
+
// if (!this.headless) {
|
66
|
+
this.onrender();
|
67
|
+
this.dispatchEvent(new CustomEvent('render'));
|
68
|
+
// }
|
42
69
|
});
|
43
70
|
this.client.on('action', async (message) => {
|
44
71
|
if (!isActionMessage(message)) {
|
@@ -47,24 +74,32 @@ export class AppletContext extends EventTarget {
|
|
47
74
|
if (Object.keys(this.actionHandlers).includes(message.actionId)) {
|
48
75
|
await this.actionHandlers[message.actionId](message.params);
|
49
76
|
}
|
50
|
-
message.resolve();
|
51
77
|
});
|
52
78
|
return this;
|
53
79
|
}
|
54
80
|
setActionHandler(actionId, handler) {
|
55
81
|
this.actionHandlers[actionId] = handler;
|
56
82
|
}
|
57
|
-
|
83
|
+
set state(state) {
|
84
|
+
this.setState(state);
|
85
|
+
}
|
86
|
+
get state() {
|
87
|
+
return __classPrivateFieldGet(this, _AppletContext_state, "f");
|
88
|
+
}
|
89
|
+
async setState(state, shouldRender) {
|
58
90
|
const message = new AppletMessage('state', { state });
|
59
91
|
await this.client.send(message);
|
60
|
-
this
|
61
|
-
this.
|
62
|
-
|
92
|
+
__classPrivateFieldSet(this, _AppletContext_state, state, "f");
|
93
|
+
if (shouldRender !== false && !this.headless) {
|
94
|
+
this.onrender();
|
95
|
+
this.dispatchEvent(new CustomEvent('render'));
|
96
|
+
}
|
63
97
|
}
|
64
98
|
onload() { }
|
65
99
|
onready() { }
|
66
100
|
onrender() { }
|
67
101
|
}
|
102
|
+
_AppletContext_state = new WeakMap();
|
68
103
|
function isActionMessage(message) {
|
69
104
|
return message.type === 'action';
|
70
105
|
}
|
@@ -76,14 +111,12 @@ function isStateMessage(message) {
|
|
76
111
|
*/
|
77
112
|
class AppletClient {
|
78
113
|
on(messageType, callback) {
|
79
|
-
window.addEventListener('message', (messageEvent) => {
|
114
|
+
window.addEventListener('message', async (messageEvent) => {
|
80
115
|
if (messageEvent.data.type !== messageType)
|
81
116
|
return;
|
82
117
|
const message = new AppletMessage(messageEvent.data.type, messageEvent.data);
|
83
|
-
|
84
|
-
|
85
|
-
};
|
86
|
-
callback(message);
|
118
|
+
await callback(message);
|
119
|
+
window.parent.postMessage(new AppletMessage('resolve', { id: message.id }), '*');
|
87
120
|
});
|
88
121
|
}
|
89
122
|
send(message) {
|
package/dist/types.d.ts
CHANGED
@@ -1,14 +1,20 @@
|
|
1
1
|
export interface AppletManifest {
|
2
2
|
type: 'applet';
|
3
3
|
name: string;
|
4
|
-
description
|
4
|
+
description?: string;
|
5
5
|
icon?: string;
|
6
|
-
|
7
|
-
|
6
|
+
unsafe?: boolean;
|
7
|
+
frameless?: boolean;
|
8
|
+
entrypoint?: string;
|
9
|
+
actions?: AppletAction[];
|
10
|
+
}
|
11
|
+
export interface AppletManifestDict {
|
12
|
+
[url: string]: AppletManifest;
|
8
13
|
}
|
9
14
|
export interface AppletAction {
|
10
15
|
id: string;
|
11
|
-
|
16
|
+
name?: string;
|
17
|
+
description?: string;
|
12
18
|
params?: ActionParamSchema;
|
13
19
|
}
|
14
20
|
export interface AppletHeader {
|
@@ -23,12 +29,12 @@ export interface AppletHeader {
|
|
23
29
|
};
|
24
30
|
}[];
|
25
31
|
}
|
26
|
-
export type AppletState =
|
32
|
+
export type AppletState = any;
|
27
33
|
export type ActionParamSchema = Record<string, {
|
28
34
|
description: string;
|
29
35
|
type: 'string';
|
30
36
|
}>;
|
31
|
-
export type ActionParams = Record<string,
|
37
|
+
export type ActionParams<T = any> = Record<string, T>;
|
32
38
|
export type ActionHandlerDict = {
|
33
39
|
[key: string]: ActionHandler<any>;
|
34
40
|
};
|
@@ -38,11 +44,22 @@ export interface AppletStateMessage<T = any> extends AppletMessage {
|
|
38
44
|
type: 'state';
|
39
45
|
state: T;
|
40
46
|
}
|
47
|
+
export interface AppletResizeMessage extends AppletMessage {
|
48
|
+
type: 'resize';
|
49
|
+
dimensions: {
|
50
|
+
height: number;
|
51
|
+
width: number;
|
52
|
+
};
|
53
|
+
}
|
41
54
|
export interface AppletActionMessage<T = any> extends AppletMessage {
|
42
55
|
type: 'action';
|
43
56
|
actionId: string;
|
44
57
|
params: T;
|
45
58
|
}
|
59
|
+
export interface AppletInitMessage extends AppletMessage {
|
60
|
+
type: 'init';
|
61
|
+
headless: boolean;
|
62
|
+
}
|
46
63
|
export declare class AppletMessage<T = any> {
|
47
64
|
type: AppletMessageType;
|
48
65
|
id: string;
|
@@ -53,9 +70,5 @@ export declare class AppletMessage<T = any> {
|
|
53
70
|
};
|
54
71
|
resolve(): void;
|
55
72
|
}
|
56
|
-
export type AppletMessageType = 'action' | 'render' | 'state' | 'ready' | 'resolve' | 'resize';
|
57
|
-
export type AppletMessageCallback = (message: AnyAppletMessage) => void;
|
58
|
-
type Serializable = string | number | boolean | null | Serializable[] | {
|
59
|
-
[key: string]: Serializable;
|
60
|
-
};
|
61
|
-
export {};
|
73
|
+
export type AppletMessageType = 'action' | 'actions' | 'render' | 'state' | 'init' | 'ready' | 'resolve' | 'resize';
|
74
|
+
export type AppletMessageCallback = (message: AnyAppletMessage) => Promise<void> | void;
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import { applets } from '../index';
|
2
|
+
class AppletView extends HTMLElement {
|
3
|
+
connectedCallback() {
|
4
|
+
const styles = document.createElement('style');
|
5
|
+
styles.textContent = this.styles;
|
6
|
+
this.appendChild(styles);
|
7
|
+
this.container = document.createElement('iframe');
|
8
|
+
this.appendChild(this.container);
|
9
|
+
}
|
10
|
+
get styles() {
|
11
|
+
return /*css*/ `
|
12
|
+
applet-frame {
|
13
|
+
display: flex;
|
14
|
+
flex-direction: column;
|
15
|
+
}
|
16
|
+
|
17
|
+
applet-frame iframe {
|
18
|
+
border: none;
|
19
|
+
}
|
20
|
+
|
21
|
+
applet-frame:not(.frameless) {
|
22
|
+
border: 1px solid #ddd;
|
23
|
+
}
|
24
|
+
|
25
|
+
applet-frame.frameless {
|
26
|
+
padding: 0 7px;
|
27
|
+
}
|
28
|
+
`;
|
29
|
+
}
|
30
|
+
set url(url) {
|
31
|
+
setTimeout(() => this.loadApplet(url), 1);
|
32
|
+
}
|
33
|
+
async loadApplet(url) {
|
34
|
+
if (!this.container)
|
35
|
+
return;
|
36
|
+
this.applet = await applets.load(url, this.container);
|
37
|
+
if (this.applet.manifest.frameless)
|
38
|
+
this.classList.add('frameless');
|
39
|
+
this.applet.onstateupdated = () => {
|
40
|
+
this.dispatchEvent(new CustomEvent('stateupdated', { detail: this.applet.state }));
|
41
|
+
};
|
42
|
+
this.dispatchEvent(new CustomEvent('load'));
|
43
|
+
}
|
44
|
+
set state(state) {
|
45
|
+
if (this.applet)
|
46
|
+
this.applet.state = state;
|
47
|
+
this.addEventListener('load', () => {
|
48
|
+
this.applet.state = state;
|
49
|
+
});
|
50
|
+
}
|
51
|
+
}
|
52
|
+
customElements.define('applet-frame', AppletView);
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import { applets } from '../index.js';
|
2
|
+
class AppletFrame extends HTMLElement {
|
3
|
+
connectedCallback() {
|
4
|
+
const styles = document.createElement('style');
|
5
|
+
styles.textContent = this.styles;
|
6
|
+
this.appendChild(styles);
|
7
|
+
this.container = document.createElement('iframe');
|
8
|
+
this.appendChild(this.container);
|
9
|
+
}
|
10
|
+
get styles() {
|
11
|
+
return /*css*/ `
|
12
|
+
applet-frame {
|
13
|
+
display: flex;
|
14
|
+
flex-direction: column;
|
15
|
+
}
|
16
|
+
|
17
|
+
applet-frame iframe {
|
18
|
+
border: none;
|
19
|
+
}
|
20
|
+
|
21
|
+
applet-frame:not(.frameless) {
|
22
|
+
border: 1px solid #ddd;
|
23
|
+
}
|
24
|
+
|
25
|
+
applet-frame.frameless {
|
26
|
+
padding: 0 7px;
|
27
|
+
}
|
28
|
+
`;
|
29
|
+
}
|
30
|
+
set url(url) {
|
31
|
+
setTimeout(() => this.loadApplet(url), 1);
|
32
|
+
}
|
33
|
+
async loadApplet(url) {
|
34
|
+
if (!this.container)
|
35
|
+
return;
|
36
|
+
this.applet = await applets.load(url, this.container);
|
37
|
+
if (this.applet.manifest.frameless)
|
38
|
+
this.classList.add('frameless');
|
39
|
+
this.applet.onstateupdated = () => {
|
40
|
+
this.dispatchEvent(new CustomEvent('stateupdated', { detail: this.applet.state }));
|
41
|
+
};
|
42
|
+
this.dispatchEvent(new CustomEvent('load'));
|
43
|
+
}
|
44
|
+
set state(state) {
|
45
|
+
if (this.applet)
|
46
|
+
this.applet.state = state;
|
47
|
+
this.addEventListener('load', () => {
|
48
|
+
this.applet.state = state;
|
49
|
+
});
|
50
|
+
}
|
51
|
+
}
|
52
|
+
customElements.define('applet-frame', AppletFrame);
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@web-applets/sdk",
|
3
|
-
"version": "0.0.
|
3
|
+
"version": "0.0.9",
|
4
4
|
"description": "The Web Applets SDK, for creating & hosting Web Applets.",
|
5
5
|
"author": "Rupert Manfredi <rupert@unternet.co>",
|
6
6
|
"license": "MIT",
|
@@ -25,6 +25,7 @@
|
|
25
25
|
"typescript": "^5.6.2"
|
26
26
|
},
|
27
27
|
"dependencies": {
|
28
|
+
"marked": "^14.1.3",
|
28
29
|
"vite": "^5.4.7"
|
29
30
|
}
|
30
31
|
}
|