@edgestation/sdk 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +208 -2
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/react.d.ts +11 -0
- package/dist/react.js +20 -0
- package/dist/report.d.ts +3 -0
- package/dist/report.js +6 -0
- package/dist/svelte.d.ts +13 -0
- package/dist/svelte.js +21 -0
- package/dist/types.d.ts +280 -0
- package/dist/types.js +1 -0
- package/dist/vite.d.ts +22 -0
- package/dist/vite.js +126 -0
- package/dist/vue.d.ts +11 -0
- package/dist/vue.js +20 -0
- package/manifest.schema.json +112 -0
- package/package.json +91 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ChainList
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,209 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @edgestation/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Write widgets for [EdgeStation](https://github.com/chainlist/edgestation), in plain JavaScript, Svelte, React or Vue. EdgeStation is a widget dashboard for a secondary display: made first for the Corsair Xeneon Edge, it works on any other one. The docs call the display it runs on "the Edge", whichever it is.
|
|
4
|
+
|
|
5
|
+
A widget is a folder with a `manifest.json` and an ES module that EdgeStation loads at runtime. The app hands each widget its `api` when it mounts, so nothing from this package runs inside the app: it gives you
|
|
6
|
+
|
|
7
|
+
- the TypeScript types of the `api`, the manifest and the widget module,
|
|
8
|
+
- `defineWidget()`, to get those types in plain JavaScript too,
|
|
9
|
+
- adapters that mount a Svelte, React or Vue component as a widget,
|
|
10
|
+
- a Vite plugin that builds the one self-contained `dist/main.js` EdgeStation loads,
|
|
11
|
+
- a JSON Schema for `manifest.json`, for autocomplete and checks in your editor.
|
|
12
|
+
|
|
13
|
+
What the `api` does, the manifest fields, settings, styling and theming are documented in [Writing a widget](https://github.com/chainlist/edgestation/blob/main/widgets/README.md).
|
|
14
|
+
|
|
15
|
+
## Start a widget
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -D @edgestation/sdk vite
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
my-widget/
|
|
23
|
+
manifest.json
|
|
24
|
+
package.json ("type": "module")
|
|
25
|
+
vite.config.ts
|
|
26
|
+
src/main.ts (or main.js, main.tsx, main.jsx)
|
|
27
|
+
dist/main.js (built)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"$schema": "./node_modules/@edgestation/sdk/manifest.schema.json",
|
|
33
|
+
"name": "My widget",
|
|
34
|
+
"minAppVersion": "0.1.0",
|
|
35
|
+
"main": "dist/main.js",
|
|
36
|
+
"size": { "w": 2, "h": 1 }
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`minAppVersion` is the oldest EdgeStation that can run the widget: set it to the version of this package you build with (they are released together), and older apps show an error card instead of a broken widget.
|
|
41
|
+
|
|
42
|
+
### Plain JavaScript or TypeScript
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
// vite.config.js
|
|
46
|
+
import { widget } from '@edgestation/sdk/vite';
|
|
47
|
+
import { defineConfig } from 'vite';
|
|
48
|
+
|
|
49
|
+
export default defineConfig({ plugins: [widget()] });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
// src/main.js
|
|
54
|
+
import { defineWidget } from '@edgestation/sdk';
|
|
55
|
+
|
|
56
|
+
export default defineWidget({
|
|
57
|
+
mount(el, api) {
|
|
58
|
+
api.every(1000, async () => {
|
|
59
|
+
const { cpu } = await api.system();
|
|
60
|
+
el.textContent = `${Math.round(cpu.load)} %`;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Svelte 5
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm install -D svelte @sveltejs/vite-plugin-svelte
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// vite.config.ts
|
|
74
|
+
import { widget } from '@edgestation/sdk/vite';
|
|
75
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
76
|
+
import { defineConfig } from 'vite';
|
|
77
|
+
|
|
78
|
+
export default defineConfig({ plugins: [svelte({ emitCss: false }), widget()] });
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// src/main.ts
|
|
83
|
+
import { svelteWidget } from '@edgestation/sdk/svelte';
|
|
84
|
+
import Cpu from './Cpu.svelte';
|
|
85
|
+
|
|
86
|
+
export default svelteWidget(Cpu);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```svelte
|
|
90
|
+
<!-- src/Cpu.svelte -->
|
|
91
|
+
<script lang="ts">
|
|
92
|
+
import type { WidgetProps } from '@edgestation/sdk';
|
|
93
|
+
|
|
94
|
+
let props: WidgetProps = $props();
|
|
95
|
+
// svelte-ignore state_referenced_locally
|
|
96
|
+
const { api } = props;
|
|
97
|
+
let load = $state(0);
|
|
98
|
+
api.every(1000, async () => (load = (await api.system()).cpu.load));
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<p>{Math.round(load)} %</p>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`emitCss: false` keeps each component's styles in its JavaScript, and Svelte adds them to the widget's shadow root when it mounts.
|
|
105
|
+
|
|
106
|
+
Read `api` from the props once, at the top of the script. Svelte warns that this only captures the prop's first value (`state_referenced_locally`): that is what you want, since a setting change remounts the widget rather than changing the prop. Taking `api` out of `props` in one statement keeps the warning to that line, which the `svelte-ignore` comment silences. Destructured straight from `$props()`, `api` would raise it at every use.
|
|
107
|
+
|
|
108
|
+
### React 19
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npm install -D react react-dom @vitejs/plugin-react
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// vite.config.ts
|
|
116
|
+
import { widget } from '@edgestation/sdk/vite';
|
|
117
|
+
import react from '@vitejs/plugin-react';
|
|
118
|
+
import { defineConfig } from 'vite';
|
|
119
|
+
|
|
120
|
+
export default defineConfig({ plugins: [react(), widget()] });
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
// src/main.tsx
|
|
125
|
+
import { reactWidget } from '@edgestation/sdk/react';
|
|
126
|
+
import type { WidgetProps } from '@edgestation/sdk';
|
|
127
|
+
import { useEffect, useState } from 'react';
|
|
128
|
+
|
|
129
|
+
function Cpu({ api }: WidgetProps) {
|
|
130
|
+
const [load, setLoad] = useState(0);
|
|
131
|
+
useEffect(() => api.every(1000, async () => setLoad((await api.system()).cpu.load)), []);
|
|
132
|
+
return <p>{Math.round(load)} %</p>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default reactWidget(Cpu);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
An error thrown while rendering, that no error boundary of yours catches, shows the widget's error card.
|
|
139
|
+
|
|
140
|
+
### Vue 3
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npm install -D vue @vitejs/plugin-vue
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// vite.config.ts
|
|
148
|
+
import { widget } from '@edgestation/sdk/vite';
|
|
149
|
+
import vue from '@vitejs/plugin-vue';
|
|
150
|
+
import { defineConfig } from 'vite';
|
|
151
|
+
|
|
152
|
+
export default defineConfig({ plugins: [vue(), widget()] });
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// src/main.ts
|
|
157
|
+
import { vueWidget } from '@edgestation/sdk/vue';
|
|
158
|
+
import Cpu from './Cpu.vue';
|
|
159
|
+
|
|
160
|
+
export default vueWidget(Cpu);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```vue
|
|
164
|
+
<!-- src/Cpu.vue -->
|
|
165
|
+
<script setup lang="ts">
|
|
166
|
+
import { ref } from 'vue';
|
|
167
|
+
import type { WidgetProps } from '@edgestation/sdk';
|
|
168
|
+
|
|
169
|
+
const { api } = defineProps<WidgetProps>();
|
|
170
|
+
const load = ref(0);
|
|
171
|
+
api.every(1000, async () => (load.value = (await api.system()).cpu.load));
|
|
172
|
+
</script>
|
|
173
|
+
|
|
174
|
+
<template>
|
|
175
|
+
<p>{{ Math.round(load) }} %</p>
|
|
176
|
+
</template>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Errors Vue catches (rendering, lifecycle hooks, watchers, template event handlers) show the widget's error card.
|
|
180
|
+
|
|
181
|
+
## Build
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
npx vite build
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`widget()` builds the entry into `dist/main.js`, one ES module with every dependency bundled in, your framework's runtime included. CSS your code imports (React, Vue `<style>` blocks) goes to `dist/main.css`: add `"styles": "dist/main.css"` to the manifest so EdgeStation loads it into the widget's shadow root. The build warns when the manifest doesn't point at what it wrote, and when your own code (not your dependencies) calls `setTimeout` or `setInterval`, which keep running while the widget's page is off screen: use `api.after` and `api.every` instead.
|
|
188
|
+
|
|
189
|
+
Pass `widget({ entry: 'src/other.ts' })` for another entry. For TypeScript to accept CSS imports, add `"types": ["vite/client"]` to your `tsconfig.json`.
|
|
190
|
+
|
|
191
|
+
## Try it in EdgeStation
|
|
192
|
+
|
|
193
|
+
EdgeStation loads every folder of `%APPDATA%\com.edgestation.dashboard\widgets\` (Settings → Widgets → Open folder) that has a `manifest.json`; the folder name is the widget's id. Rather than copying your project there, link it, from a Command Prompt:
|
|
194
|
+
|
|
195
|
+
```bat
|
|
196
|
+
mklink /J "%APPDATA%\com.edgestation.dashboard\widgets\my-widget" C:\path\to\my-widget
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Then rebuild on each change, and use Settings → Widgets → Reload Edge to load the new build:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npx vite build --watch
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
To share the widget, ship the folder with `manifest.json`, `dist/` and any files it reads with `api.asset()`.
|
|
206
|
+
|
|
207
|
+
## Types
|
|
208
|
+
|
|
209
|
+
Everything is exported from `@edgestation/sdk` as types: `WidgetApi`, `WidgetModule`, `WidgetManifest`, `WidgetProps`, the settings types and the results of `api.system()`, `api.media`, `api.apps` and `api.storage`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { WidgetModule } from './types.js';
|
|
2
|
+
export type * from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Types a widget's default export, so editors know `el` and `api` without annotations, in
|
|
5
|
+
* plain JavaScript too:
|
|
6
|
+
*
|
|
7
|
+
* ```js
|
|
8
|
+
* export default defineWidget({
|
|
9
|
+
* mount(el, api) {
|
|
10
|
+
* el.textContent = api.manifest.name;
|
|
11
|
+
* }
|
|
12
|
+
* });
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Returns `widget` unchanged: the host only needs the object.
|
|
16
|
+
*/
|
|
17
|
+
export declare function defineWidget(widget: WidgetModule): WidgetModule;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types a widget's default export, so editors know `el` and `api` without annotations, in
|
|
3
|
+
* plain JavaScript too:
|
|
4
|
+
*
|
|
5
|
+
* ```js
|
|
6
|
+
* export default defineWidget({
|
|
7
|
+
* mount(el, api) {
|
|
8
|
+
* el.textContent = api.manifest.name;
|
|
9
|
+
* }
|
|
10
|
+
* });
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Returns `widget` unchanged: the host only needs the object.
|
|
14
|
+
*/
|
|
15
|
+
export function defineWidget(widget) {
|
|
16
|
+
return widget;
|
|
17
|
+
}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ComponentType } from 'react';
|
|
2
|
+
import type { WidgetModule, WidgetProps } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Makes a widget out of a React component: `export default reactWidget(Clock)`. The component
|
|
5
|
+
* is rendered in a React root on the widget's root with the `api` as its prop, and the root is
|
|
6
|
+
* unmounted with the widget.
|
|
7
|
+
*
|
|
8
|
+
* With React 19, an error thrown while rendering that no error boundary of yours catches shows
|
|
9
|
+
* the widget's error card.
|
|
10
|
+
*/
|
|
11
|
+
export declare function reactWidget(component: ComponentType<WidgetProps>): WidgetModule;
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createElement } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { report } from './report.js';
|
|
4
|
+
/**
|
|
5
|
+
* Makes a widget out of a React component: `export default reactWidget(Clock)`. The component
|
|
6
|
+
* is rendered in a React root on the widget's root with the `api` as its prop, and the root is
|
|
7
|
+
* unmounted with the widget.
|
|
8
|
+
*
|
|
9
|
+
* With React 19, an error thrown while rendering that no error boundary of yours catches shows
|
|
10
|
+
* the widget's error card.
|
|
11
|
+
*/
|
|
12
|
+
export function reactWidget(component) {
|
|
13
|
+
return {
|
|
14
|
+
mount(el, api) {
|
|
15
|
+
const root = createRoot(el, { onUncaughtError: (error) => report(api, error) });
|
|
16
|
+
root.render(createElement(component, { api }));
|
|
17
|
+
return () => root.unmount();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
package/dist/report.d.ts
ADDED
package/dist/report.js
ADDED
package/dist/svelte.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Component } from 'svelte';
|
|
2
|
+
import type { WidgetModule, WidgetProps } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Makes a widget out of a Svelte 5 component: `export default svelteWidget(Clock)`. The
|
|
5
|
+
* component is mounted in the widget's root with the `api` as its prop, and unmounted with
|
|
6
|
+
* the widget.
|
|
7
|
+
*
|
|
8
|
+
* Its `<style>` blocks reach the widget's shadow root when the component is compiled with
|
|
9
|
+
* injected CSS (`svelte({ emitCss: false })` in the Vite config). Errors thrown while Svelte
|
|
10
|
+
* draws don't show the error card; errors in callbacks wrapped with `api.guard`,
|
|
11
|
+
* `api.every` or `api.after` do.
|
|
12
|
+
*/
|
|
13
|
+
export declare function svelteWidget(component: Component<WidgetProps>): WidgetModule;
|
package/dist/svelte.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { mount, unmount } from 'svelte';
|
|
2
|
+
/**
|
|
3
|
+
* Makes a widget out of a Svelte 5 component: `export default svelteWidget(Clock)`. The
|
|
4
|
+
* component is mounted in the widget's root with the `api` as its prop, and unmounted with
|
|
5
|
+
* the widget.
|
|
6
|
+
*
|
|
7
|
+
* Its `<style>` blocks reach the widget's shadow root when the component is compiled with
|
|
8
|
+
* injected CSS (`svelte({ emitCss: false })` in the Vite config). Errors thrown while Svelte
|
|
9
|
+
* draws don't show the error card; errors in callbacks wrapped with `api.guard`,
|
|
10
|
+
* `api.every` or `api.after` do.
|
|
11
|
+
*/
|
|
12
|
+
export function svelteWidget(component) {
|
|
13
|
+
return {
|
|
14
|
+
mount(el, api) {
|
|
15
|
+
const app = mount(component, { target: el, props: { api } });
|
|
16
|
+
return () => {
|
|
17
|
+
void unmount(app);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contents of a widget's `manifest.json`, as the host hands it to the widget in `api.manifest`.
|
|
3
|
+
* `id` is not written in the file: the host sets it to the widget's folder name.
|
|
4
|
+
*/
|
|
5
|
+
export interface WidgetManifest {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
version?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
author?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Oldest EdgeStation version the widget runs on, such as `"0.1.0"`: the one whose `api` it
|
|
13
|
+
* uses. Older versions show the widget's error card instead of loading it.
|
|
14
|
+
*/
|
|
15
|
+
minAppVersion?: string;
|
|
16
|
+
/** Entry module, relative to the widget folder. Defaults to `main.js`. */
|
|
17
|
+
main?: string;
|
|
18
|
+
/** Stylesheet loaded into the widget's shadow root. */
|
|
19
|
+
styles?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Image shown next to the widget's name in settings, relative to the widget folder. Only its
|
|
22
|
+
* shape counts: it is drawn in the accent color.
|
|
23
|
+
*/
|
|
24
|
+
icon?: string;
|
|
25
|
+
/** Default size in grid cells when added to a page. */
|
|
26
|
+
size?: {
|
|
27
|
+
w: number;
|
|
28
|
+
h: number;
|
|
29
|
+
};
|
|
30
|
+
/** Options the user can set per placement, in display order. */
|
|
31
|
+
settings?: WidgetSetting[];
|
|
32
|
+
/** Lets the widget call `api.webview`, and gives its settings a Profile choice. */
|
|
33
|
+
webview?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** One per-placement option, declared in the manifest. */
|
|
36
|
+
export type WidgetSetting = SelectSetting | TextSetting;
|
|
37
|
+
interface SettingBase {
|
|
38
|
+
key: string;
|
|
39
|
+
label: string;
|
|
40
|
+
/** Only shown (and only meaningful) while every listed setting has the given value. */
|
|
41
|
+
when?: Record<string, string>;
|
|
42
|
+
}
|
|
43
|
+
/** One value out of a list. */
|
|
44
|
+
export interface SelectSetting extends SettingBase {
|
|
45
|
+
type: 'select';
|
|
46
|
+
options: {
|
|
47
|
+
value: string;
|
|
48
|
+
label: string;
|
|
49
|
+
}[];
|
|
50
|
+
/** Used until the user picks something. Defaults to the first option. */
|
|
51
|
+
default?: string;
|
|
52
|
+
}
|
|
53
|
+
/** Free text, such as a URL. */
|
|
54
|
+
export interface TextSetting extends SettingBase {
|
|
55
|
+
type: 'text';
|
|
56
|
+
/** Used until the user types something. Defaults to empty. */
|
|
57
|
+
default?: string;
|
|
58
|
+
/** Hint shown while the field is empty. */
|
|
59
|
+
placeholder?: string;
|
|
60
|
+
}
|
|
61
|
+
/** Saved setting values of one placement, by key. */
|
|
62
|
+
export type WidgetSettingValues = Record<string, string>;
|
|
63
|
+
export type Cleanup = () => void;
|
|
64
|
+
/**
|
|
65
|
+
* What a widget receives from the host. Deliberately small for now: the full API is still
|
|
66
|
+
* to be designed. Everything registered here is released when the widget unmounts.
|
|
67
|
+
*/
|
|
68
|
+
export interface WidgetApi {
|
|
69
|
+
/** Id of this placement; two copies of a widget get different ids. */
|
|
70
|
+
readonly instanceId: string;
|
|
71
|
+
readonly manifest: WidgetManifest;
|
|
72
|
+
/**
|
|
73
|
+
* This placement's settings: every key declared in the manifest, with its default when
|
|
74
|
+
* unset. The widget is remounted when the user changes one.
|
|
75
|
+
*/
|
|
76
|
+
readonly settings: Readonly<WidgetSettingValues>;
|
|
77
|
+
/** The machine's performance, read when called; nothing is monitored in between. */
|
|
78
|
+
system(): Promise<SystemInfo>;
|
|
79
|
+
/**
|
|
80
|
+
* The programs that used the most CPU since the previous call (from any widget), busiest
|
|
81
|
+
* first: at most `count`, fewer when not that many used any. Read when called.
|
|
82
|
+
*/
|
|
83
|
+
topPrograms(count: number): Promise<ProgramUsage[]>;
|
|
84
|
+
/** What is playing on the machine, read when called, and its playback controls. */
|
|
85
|
+
readonly media: MediaApi;
|
|
86
|
+
/** The apps of the Start menu: listed when called, started, and their icons. */
|
|
87
|
+
readonly apps: AppsApi;
|
|
88
|
+
/**
|
|
89
|
+
* The web's `fetch`, sent by the app rather than the Edge page, so CORS doesn't apply: any
|
|
90
|
+
* `http` or `https` URL answers as it would answer a server. Headers browsers forbid
|
|
91
|
+
* (`Cookie`, `Origin`, `Referer`, ...) can't be set. Cookies that sites set are kept across
|
|
92
|
+
* restarts and shared by every widget. A request still unanswered when the widget unmounts
|
|
93
|
+
* is cancelled.
|
|
94
|
+
*/
|
|
95
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
96
|
+
/**
|
|
97
|
+
* Data kept across remounts and restarts, shared by the widget's placements or owned by
|
|
98
|
+
* this one. Nothing in it is ever deleted automatically.
|
|
99
|
+
*/
|
|
100
|
+
readonly storage: StorageApi;
|
|
101
|
+
/** URL of a file inside the widget folder (images, data, ...). */
|
|
102
|
+
asset(path: string): string;
|
|
103
|
+
/** Wraps a callback so an error thrown in it shows this widget's error card. */
|
|
104
|
+
guard<A extends unknown[]>(fn: (...args: A) => unknown): (...args: A) => void;
|
|
105
|
+
/**
|
|
106
|
+
* A guarded `setInterval`, cleared when the widget unmounts. It stops while the widget's
|
|
107
|
+
* page is off screen or the Edge is frozen (another app is full screen), and ticks right
|
|
108
|
+
* away when it resumes. Returns a function that stops it sooner.
|
|
109
|
+
*/
|
|
110
|
+
every(ms: number, fn: () => unknown): Cleanup;
|
|
111
|
+
/**
|
|
112
|
+
* A guarded `setTimeout`, cleared if the widget unmounts first. It waits while the widget's
|
|
113
|
+
* page is off screen, and fires as soon as the page is back if it came due meanwhile.
|
|
114
|
+
* Unlike `every`, it still fires while the Edge is frozen. Returns a function that
|
|
115
|
+
* cancels it.
|
|
116
|
+
*/
|
|
117
|
+
after(ms: number, fn: () => unknown): Cleanup;
|
|
118
|
+
/**
|
|
119
|
+
* Shows web page `url` in a native browser view laid over `target`, for sites that refuse
|
|
120
|
+
* to load in an iframe. It draws above the Edge, so it hides while pages slide and while
|
|
121
|
+
* `target` is not fully on screen: once it has shown, a picture of it lies over `target`
|
|
122
|
+
* in its place meanwhile (an `<img>` added to the widget's root). It takes the touches on
|
|
123
|
+
* its area. It opens in the
|
|
124
|
+
* browser profile picked in the placement's settings (Default unless the user picks
|
|
125
|
+
* another): sign-ins last across restarts. Needs `"webview": true` in the manifest, else
|
|
126
|
+
* it shows the error card. Closed when the widget unmounts; returns a function that closes
|
|
127
|
+
* it sooner.
|
|
128
|
+
*/
|
|
129
|
+
webview(target: HTMLElement, url: string, options?: WebviewOptions): Cleanup;
|
|
130
|
+
/**
|
|
131
|
+
* Swaps the native scrollbars of `viewport`, an element of the widget that scrolls, for the
|
|
132
|
+
* app's overlay ones, which show while it scrolls. They are drawn in its parent, so wrap
|
|
133
|
+
* `viewport` in an element of its own: the parent takes the place `viewport` had in the
|
|
134
|
+
* layout, and `viewport` fills it. Removed when the widget unmounts; returns a function that
|
|
135
|
+
* removes them sooner.
|
|
136
|
+
*/
|
|
137
|
+
scrollbars(viewport: HTMLElement): Cleanup;
|
|
138
|
+
/** Runs `fn` when the widget unmounts. */
|
|
139
|
+
onDispose(fn: Cleanup): void;
|
|
140
|
+
}
|
|
141
|
+
/** How `api.webview` opens its page. */
|
|
142
|
+
export interface WebviewOptions {
|
|
143
|
+
/**
|
|
144
|
+
* Presents the view as Chrome on a phone (user agent and client hints), for every page it
|
|
145
|
+
* loads, so that sites which pick their layout from the browser serve their mobile one.
|
|
146
|
+
* Sites that only follow the width already fit the cell either way.
|
|
147
|
+
*/
|
|
148
|
+
mobile?: boolean;
|
|
149
|
+
/**
|
|
150
|
+
* Shows a Home button in the view's top right corner while it is away from `url`, which
|
|
151
|
+
* takes it back there. The view leaves that corner to the button (an element added to the
|
|
152
|
+
* widget's root, like the picture). Where `url` redirects to as it loads counts as `url`.
|
|
153
|
+
*/
|
|
154
|
+
homeButton?: boolean;
|
|
155
|
+
}
|
|
156
|
+
/** Where a widget saves data: pick the scope that fits each value. */
|
|
157
|
+
export interface StorageApi {
|
|
158
|
+
/** Shared by every placement of this widget. */
|
|
159
|
+
readonly widget: WidgetStorage;
|
|
160
|
+
/** This placement's own; other copies of the widget don't see it. */
|
|
161
|
+
readonly instance: WidgetStorage;
|
|
162
|
+
}
|
|
163
|
+
/** Saved data: JSON values by key, written to disk shortly after each change. */
|
|
164
|
+
export interface WidgetStorage {
|
|
165
|
+
/** The value saved under `key`; `undefined` when there is none. */
|
|
166
|
+
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
167
|
+
/** Saves `value` (anything JSON can hold) under `key`, replacing the previous one. */
|
|
168
|
+
set(key: string, value: unknown): Promise<void>;
|
|
169
|
+
/** Removes `key`. Resolves `true` if it had a value. */
|
|
170
|
+
delete(key: string): Promise<boolean>;
|
|
171
|
+
}
|
|
172
|
+
/** Network throughput in bytes per second. */
|
|
173
|
+
export interface NetRate {
|
|
174
|
+
down: number;
|
|
175
|
+
up: number;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The machine's performance at the time of the call. CPU load and network speeds are
|
|
179
|
+
* measured since the previous call (from any widget); calls less than 500 ms apart share
|
|
180
|
+
* one reading.
|
|
181
|
+
*/
|
|
182
|
+
export interface SystemInfo {
|
|
183
|
+
hostname: string;
|
|
184
|
+
/** Name of the connected Wi-Fi network (its Windows profile name); `null` when not connected. */
|
|
185
|
+
wifi: string | null;
|
|
186
|
+
/** Load of all CPU cores, in percent. */
|
|
187
|
+
cpu: {
|
|
188
|
+
load: number;
|
|
189
|
+
};
|
|
190
|
+
/** First NVIDIA GPU (load in percent, temperature in °C); `null` when there is none. */
|
|
191
|
+
gpu: {
|
|
192
|
+
load: number;
|
|
193
|
+
temperature: number;
|
|
194
|
+
} | null;
|
|
195
|
+
/**
|
|
196
|
+
* Since the previous call, plus averages over the calls of the last 1, 5 and 15 minutes:
|
|
197
|
+
* the full window while widgets keep asking, less right after they start or resume.
|
|
198
|
+
*/
|
|
199
|
+
net: NetRate & {
|
|
200
|
+
avg1m: NetRate;
|
|
201
|
+
avg5m: NetRate;
|
|
202
|
+
avg15m: NetRate;
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/** A program's CPU use: every process that shows the same name, added together. */
|
|
206
|
+
export interface ProgramUsage {
|
|
207
|
+
/**
|
|
208
|
+
* What Task Manager calls it: its executable's description (`THE FINALS`, `Google Chrome`),
|
|
209
|
+
* else the executable file name (`crashpad_handler.exe`).
|
|
210
|
+
*/
|
|
211
|
+
name: string;
|
|
212
|
+
/** Percent of all logical processors, like Task Manager. */
|
|
213
|
+
cpu: number;
|
|
214
|
+
}
|
|
215
|
+
export interface MediaApi {
|
|
216
|
+
/** The session Windows considers current (the one media keys control); `null` when none. */
|
|
217
|
+
current(): Promise<MediaInfo | null>;
|
|
218
|
+
/** Album art of the track from the latest `current()` call, as a data URL; `null` when none. */
|
|
219
|
+
art(): Promise<string | null>;
|
|
220
|
+
/** Asks the current session's app to play, pause or skip. Resolves `false` if it declined. */
|
|
221
|
+
control(action: MediaAction): Promise<boolean>;
|
|
222
|
+
}
|
|
223
|
+
export type MediaAction = 'play' | 'pause' | 'next' | 'previous';
|
|
224
|
+
/** A media session as an app publishes it to Windows. Fields the app doesn't set are empty. */
|
|
225
|
+
export interface MediaInfo {
|
|
226
|
+
/** App that owns the session, as Windows names it (`Spotify.exe`, `MSEdge`, ...). */
|
|
227
|
+
app: string;
|
|
228
|
+
title: string;
|
|
229
|
+
artist: string;
|
|
230
|
+
album: string;
|
|
231
|
+
playing: boolean;
|
|
232
|
+
/**
|
|
233
|
+
* Seconds into the track at the time of the call; `null` when the app reports no timeline.
|
|
234
|
+
* When the app blanks it out (Zen and Firefox do, about once a minute), it carries on from
|
|
235
|
+
* the last one reported, as long as calls come at least every 5 seconds.
|
|
236
|
+
*/
|
|
237
|
+
position: number | null;
|
|
238
|
+
duration: number | null;
|
|
239
|
+
/** Changes whenever the album art does: call `art()` then. `null` when there is no art. */
|
|
240
|
+
art: number | null;
|
|
241
|
+
/** Which `control()` actions the app accepts right now. */
|
|
242
|
+
can: Record<MediaAction, boolean>;
|
|
243
|
+
}
|
|
244
|
+
/** The apps of the Start menu's All apps list (the shell's Apps folder). */
|
|
245
|
+
export interface AppsApi {
|
|
246
|
+
/** Every app, by name, read when called. */
|
|
247
|
+
list(): Promise<AppInfo[]>;
|
|
248
|
+
/**
|
|
249
|
+
* Starts app `id` as the Start menu does, asking for its window on the main screen rather
|
|
250
|
+
* than the Edge (apps that remember their position may still open where they were).
|
|
251
|
+
* Rejects when the app is gone.
|
|
252
|
+
*/
|
|
253
|
+
launch(id: string): Promise<void>;
|
|
254
|
+
/**
|
|
255
|
+
* URL of the icon of app `id`, for an `<img>`: a PNG `size` pixels square (16 to 256,
|
|
256
|
+
* default 64). Ask for the size it shows at in device pixels: icons drawn larger are sharper.
|
|
257
|
+
*/
|
|
258
|
+
icon(id: string, size?: number): string;
|
|
259
|
+
}
|
|
260
|
+
/** An app as the Start menu lists it. */
|
|
261
|
+
export interface AppInfo {
|
|
262
|
+
/**
|
|
263
|
+
* Launches it and draws its icon: its name in the shell's Apps folder, the app's
|
|
264
|
+
* AppUserModelID or, for a program without one, a path to it. Stays the same across
|
|
265
|
+
* restarts, so widgets can save it.
|
|
266
|
+
*/
|
|
267
|
+
id: string;
|
|
268
|
+
/** What the Start menu calls it. */
|
|
269
|
+
name: string;
|
|
270
|
+
}
|
|
271
|
+
/** The default export of a widget's entry module. */
|
|
272
|
+
export interface WidgetModule {
|
|
273
|
+
/** Render into `el`. May return (or resolve to) a cleanup function. */
|
|
274
|
+
mount(el: HTMLElement, api: WidgetApi): void | Cleanup | Promise<void | Cleanup>;
|
|
275
|
+
}
|
|
276
|
+
/** Props of a widget's root component in the Svelte, React and Vue adapters. */
|
|
277
|
+
export interface WidgetProps {
|
|
278
|
+
api: WidgetApi;
|
|
279
|
+
}
|
|
280
|
+
export {};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
export interface WidgetBuildOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Entry module, relative to the project root. Defaults to the first of `src/main.ts`,
|
|
5
|
+
* `src/main.tsx`, `src/main.js` and `src/main.jsx` that exists.
|
|
6
|
+
*/
|
|
7
|
+
entry?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Builds the widget into what EdgeStation loads: one self-contained ES module,
|
|
11
|
+
* `dist/main.js`, with every dependency bundled in (frameworks included), plus `dist/main.css`
|
|
12
|
+
* when the code imports CSS. Add your framework's Vite plugin next to it:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* export default defineConfig({ plugins: [react(), widget()] });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* It also replaces `process.env.NODE_ENV`, which React and Vue read and a browser lacks, and
|
|
19
|
+
* warns when `manifest.json` doesn't point at the files it writes, or when the widget's own
|
|
20
|
+
* code (not its dependencies) calls `setTimeout` or `setInterval`, which don't pause with it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function widget(options?: WidgetBuildOptions): Plugin;
|
package/dist/vite.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Loaded by Node from the widget's Vite config: no relative imports, only erasable TypeScript.
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join, relative, resolve } from 'node:path';
|
|
4
|
+
const ENTRIES = ['src/main.ts', 'src/main.tsx', 'src/main.js', 'src/main.jsx'];
|
|
5
|
+
/** Forward slashes and no leading `./`, as paths are written in `manifest.json`. */
|
|
6
|
+
const normalize = (path) => path.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
7
|
+
/** What each global timer does that the `api` one doesn't. */
|
|
8
|
+
const TIMERS = {
|
|
9
|
+
setInterval: "keeps running while the widget's page is off screen or the Edge is frozen, and after " +
|
|
10
|
+
'the widget unmounts: use api.every instead',
|
|
11
|
+
setTimeout: "fires even while the widget's page is off screen, and after the widget unmounts: use " +
|
|
12
|
+
'api.after instead'
|
|
13
|
+
};
|
|
14
|
+
const GLOBALS = new Set(['window', 'globalThis', 'self']);
|
|
15
|
+
/** A file of the widget's own, by its module id: not a dependency, a virtual module or a query. */
|
|
16
|
+
const OWN = /^[^\0?]+\.(?:[cm]?[jt]sx?|svelte|vue)$/;
|
|
17
|
+
/** The global timer that `callee` names (`setTimeout`, `window.setInterval`, ...), if any. */
|
|
18
|
+
function timerName(callee) {
|
|
19
|
+
const { object, property } = callee;
|
|
20
|
+
const global = callee.type === 'MemberExpression' &&
|
|
21
|
+
!callee.computed &&
|
|
22
|
+
object?.type === 'Identifier' &&
|
|
23
|
+
GLOBALS.has(object.name ?? '');
|
|
24
|
+
const name = global ? property?.name : callee.type === 'Identifier' ? callee.name : undefined;
|
|
25
|
+
return name !== undefined && Object.hasOwn(TIMERS, name) ? name : undefined;
|
|
26
|
+
}
|
|
27
|
+
/** The global timers called anywhere in `node`, an ESTree node or a list of them. */
|
|
28
|
+
function timersCalled(node, found = new Set()) {
|
|
29
|
+
if (node && typeof node === 'object') {
|
|
30
|
+
const { type, callee } = node;
|
|
31
|
+
const name = type === 'CallExpression' && callee ? timerName(callee) : undefined;
|
|
32
|
+
if (name)
|
|
33
|
+
found.add(name);
|
|
34
|
+
for (const child of Object.values(node))
|
|
35
|
+
timersCalled(child, found);
|
|
36
|
+
}
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Builds the widget into what EdgeStation loads: one self-contained ES module,
|
|
41
|
+
* `dist/main.js`, with every dependency bundled in (frameworks included), plus `dist/main.css`
|
|
42
|
+
* when the code imports CSS. Add your framework's Vite plugin next to it:
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* export default defineConfig({ plugins: [react(), widget()] });
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* It also replaces `process.env.NODE_ENV`, which React and Vue read and a browser lacks, and
|
|
49
|
+
* warns when `manifest.json` doesn't point at the files it writes, or when the widget's own
|
|
50
|
+
* code (not its dependencies) calls `setTimeout` or `setInterval`, which don't pause with it.
|
|
51
|
+
*/
|
|
52
|
+
export function widget(options = {}) {
|
|
53
|
+
let root = process.cwd();
|
|
54
|
+
let outDir = 'dist';
|
|
55
|
+
let manifest;
|
|
56
|
+
/** Path of a built file as `manifest.json` should name it. */
|
|
57
|
+
const built = (file) => normalize(relative(root, resolve(root, outDir, file)));
|
|
58
|
+
return {
|
|
59
|
+
name: 'edgestation-widget',
|
|
60
|
+
apply: 'build',
|
|
61
|
+
config(config, { mode }) {
|
|
62
|
+
const base = resolve(config.root ?? process.cwd());
|
|
63
|
+
const entry = options.entry ?? ENTRIES.find((file) => existsSync(join(base, file)));
|
|
64
|
+
if (!entry) {
|
|
65
|
+
throw new Error(`No widget entry: create one of ${ENTRIES.join(', ')}, or pass widget({ entry })`);
|
|
66
|
+
}
|
|
67
|
+
const nodeEnv = mode === 'production' ? 'production' : 'development';
|
|
68
|
+
return {
|
|
69
|
+
define: config.define?.['process.env.NODE_ENV'] === undefined
|
|
70
|
+
? { 'process.env.NODE_ENV': JSON.stringify(nodeEnv) }
|
|
71
|
+
: {},
|
|
72
|
+
build: {
|
|
73
|
+
lib: { entry, formats: ['es'], fileName: () => 'main.js', cssFileName: 'main' },
|
|
74
|
+
outDir: config.build?.outDir ?? 'dist'
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
configResolved(config) {
|
|
79
|
+
root = config.root;
|
|
80
|
+
outDir = config.build.outDir;
|
|
81
|
+
},
|
|
82
|
+
buildStart() {
|
|
83
|
+
const file = join(root, 'manifest.json');
|
|
84
|
+
manifest = undefined;
|
|
85
|
+
if (!existsSync(file)) {
|
|
86
|
+
this.warn('No manifest.json in the project root: EdgeStation only lists folders with one.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
manifest = JSON.parse(readFileSync(file, 'utf8'));
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
this.warn(`manifest.json is not valid JSON: ${error.message}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const main = typeof manifest?.main === 'string' ? manifest.main : 'main.js';
|
|
97
|
+
if (normalize(main) !== built('main.js')) {
|
|
98
|
+
this.warn(`manifest.json has "main": "${main}" but the build writes ${built('main.js')}: ` +
|
|
99
|
+
`set "main": "${built('main.js')}".`);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
// Last, so TypeScript, Svelte and Vue files are plain JavaScript by then, whatever the
|
|
103
|
+
// plugin order. A dependency's timers are its own business.
|
|
104
|
+
transform: {
|
|
105
|
+
order: 'post',
|
|
106
|
+
handler(code, id) {
|
|
107
|
+
if (!OWN.test(id) || id.includes('/node_modules/'))
|
|
108
|
+
return;
|
|
109
|
+
if (!/\bset(?:Timeout|Interval)\b/.test(code))
|
|
110
|
+
return;
|
|
111
|
+
for (const name of timersCalled(this.parse(code))) {
|
|
112
|
+
this.warn(`${normalize(relative(root, id))} calls ${name}, which ${TIMERS[name]}.`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
// Not `generateBundle`: Vite's CSS plugin adds main.css to the bundle after it.
|
|
117
|
+
writeBundle(_, bundle) {
|
|
118
|
+
const css = built('main.css');
|
|
119
|
+
const styles = typeof manifest?.styles === 'string' ? normalize(manifest.styles) : undefined;
|
|
120
|
+
if (manifest && bundle['main.css'] && styles !== css) {
|
|
121
|
+
this.warn(`The build writes ${css}: set "styles": "${css}" in manifest.json so EdgeStation ` +
|
|
122
|
+
`loads it into the widget.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
package/dist/vue.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Component } from 'vue';
|
|
2
|
+
import type { WidgetModule } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Makes a widget out of a Vue 3 component: `export default vueWidget(Clock)`. The component is
|
|
5
|
+
* mounted as a Vue app on the widget's root with the `api` as its prop
|
|
6
|
+
* (`defineProps<WidgetProps>()`), and unmounted with the widget.
|
|
7
|
+
*
|
|
8
|
+
* Errors Vue catches (in rendering, lifecycle hooks, watchers and template event handlers)
|
|
9
|
+
* show the widget's error card.
|
|
10
|
+
*/
|
|
11
|
+
export declare function vueWidget(component: Component): WidgetModule;
|
package/dist/vue.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createApp } from 'vue';
|
|
2
|
+
import { report } from './report.js';
|
|
3
|
+
/**
|
|
4
|
+
* Makes a widget out of a Vue 3 component: `export default vueWidget(Clock)`. The component is
|
|
5
|
+
* mounted as a Vue app on the widget's root with the `api` as its prop
|
|
6
|
+
* (`defineProps<WidgetProps>()`), and unmounted with the widget.
|
|
7
|
+
*
|
|
8
|
+
* Errors Vue catches (in rendering, lifecycle hooks, watchers and template event handlers)
|
|
9
|
+
* show the widget's error card.
|
|
10
|
+
*/
|
|
11
|
+
export function vueWidget(component) {
|
|
12
|
+
return {
|
|
13
|
+
mount(el, api) {
|
|
14
|
+
const app = createApp(component, { api });
|
|
15
|
+
app.config.errorHandler = (error) => report(api, error);
|
|
16
|
+
app.mount(el);
|
|
17
|
+
return () => app.unmount();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "EdgeStation widget manifest",
|
|
4
|
+
"description": "The manifest.json of an EdgeStation widget. The widget's id is its folder name.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["name"],
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "type": "string" },
|
|
10
|
+
"name": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"minLength": 1,
|
|
13
|
+
"description": "Name shown in Settings."
|
|
14
|
+
},
|
|
15
|
+
"version": { "type": "string", "description": "The widget's own version." },
|
|
16
|
+
"description": { "type": "string", "description": "What the widget shows, for Settings." },
|
|
17
|
+
"author": { "type": "string" },
|
|
18
|
+
"minAppVersion": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+$",
|
|
21
|
+
"description": "Oldest EdgeStation version the widget runs on, such as \"0.1.0\": the one whose api it uses. Older versions show the error card instead."
|
|
22
|
+
},
|
|
23
|
+
"main": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"default": "main.js",
|
|
26
|
+
"description": "Entry ES module, relative to the widget folder. It default-exports { mount(el, api) }."
|
|
27
|
+
},
|
|
28
|
+
"styles": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "Stylesheet loaded into the widget's shadow root, relative to the widget folder."
|
|
31
|
+
},
|
|
32
|
+
"icon": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "Square SVG or PNG on a transparent background, shown in Settings. Only its shape counts: it is drawn in the accent color."
|
|
35
|
+
},
|
|
36
|
+
"size": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"description": "Default size in grid cells when the widget is added to a page.",
|
|
39
|
+
"required": ["w", "h"],
|
|
40
|
+
"additionalProperties": false,
|
|
41
|
+
"properties": {
|
|
42
|
+
"w": { "type": "integer", "minimum": 1 },
|
|
43
|
+
"h": { "type": "integer", "minimum": 1 }
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"webview": {
|
|
47
|
+
"type": "boolean",
|
|
48
|
+
"description": "Lets the widget call api.webview, and gives its settings a Profile choice."
|
|
49
|
+
},
|
|
50
|
+
"settings": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"description": "Options the user sets per placement, in display order. The widget reads them from api.settings.",
|
|
53
|
+
"items": { "oneOf": [{ "$ref": "#/definitions/select" }, { "$ref": "#/definitions/text" }] }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"definitions": {
|
|
57
|
+
"key": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"minLength": 1,
|
|
60
|
+
"description": "Name of the value in api.settings."
|
|
61
|
+
},
|
|
62
|
+
"label": { "type": "string", "description": "Shown next to the field." },
|
|
63
|
+
"when": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"description": "Only show this setting while the named settings have these values.",
|
|
66
|
+
"additionalProperties": { "type": "string" }
|
|
67
|
+
},
|
|
68
|
+
"select": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"description": "One value out of a list.",
|
|
71
|
+
"required": ["key", "label", "type", "options"],
|
|
72
|
+
"additionalProperties": false,
|
|
73
|
+
"properties": {
|
|
74
|
+
"key": { "$ref": "#/definitions/key" },
|
|
75
|
+
"label": { "$ref": "#/definitions/label" },
|
|
76
|
+
"type": { "const": "select" },
|
|
77
|
+
"options": {
|
|
78
|
+
"type": "array",
|
|
79
|
+
"minItems": 1,
|
|
80
|
+
"items": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"required": ["value", "label"],
|
|
83
|
+
"additionalProperties": false,
|
|
84
|
+
"properties": {
|
|
85
|
+
"value": { "type": "string", "description": "Saved value." },
|
|
86
|
+
"label": { "type": "string", "description": "Shown label." }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"default": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"description": "Value until the user picks one. Defaults to the first option."
|
|
93
|
+
},
|
|
94
|
+
"when": { "$ref": "#/definitions/when" }
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"text": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"description": "Free text, such as a URL.",
|
|
100
|
+
"required": ["key", "label", "type"],
|
|
101
|
+
"additionalProperties": false,
|
|
102
|
+
"properties": {
|
|
103
|
+
"key": { "$ref": "#/definitions/key" },
|
|
104
|
+
"label": { "$ref": "#/definitions/label" },
|
|
105
|
+
"type": { "const": "text" },
|
|
106
|
+
"default": { "type": "string", "description": "Value until the user types one." },
|
|
107
|
+
"placeholder": { "type": "string", "description": "Hint shown while the field is empty." },
|
|
108
|
+
"when": { "$ref": "#/definitions/when" }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,94 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edgestation/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Types and build helpers to write widgets for EdgeStation, the secondary display dashboard made for the Corsair Xeneon Edge, in plain JavaScript, Svelte, React or Vue.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"edgestation",
|
|
7
|
+
"widget",
|
|
8
|
+
"dashboard",
|
|
9
|
+
"secondary-display",
|
|
10
|
+
"xeneon-edge"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/chainlist/edgestation.git",
|
|
16
|
+
"directory": "packages/sdk"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./svelte": {
|
|
26
|
+
"types": "./dist/svelte.d.ts",
|
|
27
|
+
"default": "./dist/svelte.js"
|
|
28
|
+
},
|
|
29
|
+
"./react": {
|
|
30
|
+
"types": "./dist/react.d.ts",
|
|
31
|
+
"default": "./dist/react.js"
|
|
32
|
+
},
|
|
33
|
+
"./vue": {
|
|
34
|
+
"types": "./dist/vue.d.ts",
|
|
35
|
+
"default": "./dist/vue.js"
|
|
36
|
+
},
|
|
37
|
+
"./vite": {
|
|
38
|
+
"types": "./dist/vite.d.ts",
|
|
39
|
+
"default": "./dist/vite.js"
|
|
40
|
+
},
|
|
41
|
+
"./manifest.schema.json": "./manifest.schema.json"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"dist",
|
|
48
|
+
"manifest.schema.json"
|
|
49
|
+
],
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"react": ">=19",
|
|
52
|
+
"react-dom": ">=19",
|
|
53
|
+
"svelte": "^5",
|
|
54
|
+
"vite": ">=6",
|
|
55
|
+
"vue": "^3.3"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"react": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"react-dom": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"svelte": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"vite": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"vue": {
|
|
71
|
+
"optional": true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@sveltejs/vite-plugin-svelte": "^7.2.0",
|
|
76
|
+
"@types/node": "^26.6.6",
|
|
77
|
+
"@types/react": "^19.3.0",
|
|
78
|
+
"@types/react-dom": "^19.3.0",
|
|
79
|
+
"ajv": "^8.20.0",
|
|
80
|
+
"happy-dom": "^21.0.4",
|
|
81
|
+
"react": "^19.3.1",
|
|
82
|
+
"react-dom": "^19.3.1",
|
|
83
|
+
"svelte": "^5.57.2",
|
|
84
|
+
"typescript": "^6.0.3",
|
|
85
|
+
"vite": "^8.3.0",
|
|
86
|
+
"vitest": "^4.1.8",
|
|
87
|
+
"vue": "^3.5.45"
|
|
88
|
+
},
|
|
89
|
+
"scripts": {
|
|
90
|
+
"build": "tsc -p tsconfig.build.json",
|
|
91
|
+
"check": "tsc -p tsconfig.json",
|
|
92
|
+
"test": "vitest --run"
|
|
93
|
+
}
|
|
6
94
|
}
|