@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 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
- # Temporary Holding Version
1
+ # @edgestation/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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`.
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import type { WidgetApi } from './types.js';
2
+ /** Shows `error` on the widget's error card, as if a guarded callback had thrown it. */
3
+ export declare function report(api: WidgetApi, error: unknown): void;
package/dist/report.js ADDED
@@ -0,0 +1,6 @@
1
+ /** Shows `error` on the widget's error card, as if a guarded callback had thrown it. */
2
+ export function report(api, error) {
3
+ api.guard(() => {
4
+ throw error;
5
+ })();
6
+ }
@@ -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
+ }
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
  }