@web-applets/sdk 0.0.7 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![Demo of a web applets chatbot](./docs/assets/applets-chat-demo.gif)
|
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
|
}
|