@bardioc/app-sdk 0.4.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 +5 -0
- package/README.md +368 -0
- package/assets/fonts/README.md +11 -0
- package/assets/fonts/bardioc-fonts.css +55 -0
- package/assets/fonts/v1/geist-mono-latin-wght-normal.woff2 +0 -0
- package/assets/fonts/v1/nunito-sans-latin-wght-italic.woff2 +0 -0
- package/assets/fonts/v1/nunito-sans-latin-wght-normal.woff2 +0 -0
- package/dist/contract-matrix.d.ts +130 -0
- package/dist/contract-matrix.js +132 -0
- package/dist/dev-auth-proxy-core.d.ts +24 -0
- package/dist/dev-auth-proxy-core.js +59 -0
- package/dist/dev-auth-vite.d.ts +34 -0
- package/dist/dev-auth-vite.js +221 -0
- package/dist/dev-proxy.d.ts +8 -0
- package/dist/dev-proxy.js +40 -0
- package/dist/dev-session-cli.d.ts +34 -0
- package/dist/dev-session-cli.js +125 -0
- package/dist/dev.d.ts +33 -0
- package/dist/dev.js +223 -0
- package/dist/dot-env.d.ts +2 -0
- package/dist/dot-env.js +22 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +57 -0
- package/dist/host-bridge.d.ts +3 -0
- package/dist/host-bridge.js +260 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +6 -0
- package/dist/manifest.d.ts +78 -0
- package/dist/manifest.js +169 -0
- package/dist/protocol.d.ts +26 -0
- package/dist/protocol.js +28 -0
- package/dist/react.d.ts +26 -0
- package/dist/react.js +208 -0
- package/dist/transports/graph-transport.d.ts +224 -0
- package/dist/transports/graph-transport.js +584 -0
- package/dist/transports/os-transport.d.ts +189 -0
- package/dist/transports/os-transport.js +444 -0
- package/dist/types.d.ts +343 -0
- package/dist/vite.d.ts +9 -0
- package/dist/vite.js +262 -0
- package/package.json +101 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Schema for `public/app-manifest.json` — required in every iframe app's build output. */
|
|
2
|
+
export type AppManifestPermission = 'notify' | 'storage' | 'indexdb' | 'kernel-proxy' | 'transport';
|
|
3
|
+
export interface CustomWindowSize {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
minWidth?: number;
|
|
7
|
+
minHeight?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare const WINDOW_SIZES: {
|
|
10
|
+
readonly xs: {
|
|
11
|
+
readonly width: 480;
|
|
12
|
+
readonly height: 320;
|
|
13
|
+
readonly minWidth: 360;
|
|
14
|
+
readonly minHeight: 280;
|
|
15
|
+
};
|
|
16
|
+
readonly sm: {
|
|
17
|
+
readonly width: 640;
|
|
18
|
+
readonly height: 480;
|
|
19
|
+
readonly minWidth: 480;
|
|
20
|
+
readonly minHeight: 320;
|
|
21
|
+
};
|
|
22
|
+
readonly md: {
|
|
23
|
+
readonly width: 800;
|
|
24
|
+
readonly height: 600;
|
|
25
|
+
readonly minWidth: 480;
|
|
26
|
+
readonly minHeight: 320;
|
|
27
|
+
};
|
|
28
|
+
readonly lg: {
|
|
29
|
+
readonly width: 1024;
|
|
30
|
+
readonly height: 800;
|
|
31
|
+
readonly minWidth: 640;
|
|
32
|
+
readonly minHeight: 480;
|
|
33
|
+
};
|
|
34
|
+
readonly xl: {
|
|
35
|
+
readonly width: 1280;
|
|
36
|
+
readonly height: 800;
|
|
37
|
+
readonly minWidth: 800;
|
|
38
|
+
readonly minHeight: 600;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
export type WindowSizePreset = keyof typeof WINDOW_SIZES;
|
|
42
|
+
export declare const DEFAULT_WINDOW_SIZE: WindowSizePreset;
|
|
43
|
+
export interface AppManifestWindowConfig {
|
|
44
|
+
size?: WindowSizePreset | CustomWindowSize;
|
|
45
|
+
canHaveMultipleWindows?: boolean;
|
|
46
|
+
isInstanceAware?: boolean;
|
|
47
|
+
resizable?: boolean;
|
|
48
|
+
maximizable?: boolean;
|
|
49
|
+
minimizable?: boolean;
|
|
50
|
+
customScroll?: boolean;
|
|
51
|
+
centered?: boolean;
|
|
52
|
+
isHeaderless?: boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface AppManifest {
|
|
55
|
+
/** Schema version. Use `2` for new apps. */
|
|
56
|
+
manifestVersion: 1 | 2;
|
|
57
|
+
/** Lowercase app identifier, e.g. `"my-app"` or `"weather"`. */
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
version: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
icon?: string;
|
|
63
|
+
entry?: string;
|
|
64
|
+
author?: {
|
|
65
|
+
name: string;
|
|
66
|
+
url?: string;
|
|
67
|
+
};
|
|
68
|
+
permissions?: AppManifestPermission[];
|
|
69
|
+
categories?: string[];
|
|
70
|
+
showInDock?: boolean;
|
|
71
|
+
showInDockAppsMenu?: boolean;
|
|
72
|
+
window?: AppManifestWindowConfig;
|
|
73
|
+
}
|
|
74
|
+
export declare const MANIFEST_FILENAME = "app-manifest.json";
|
|
75
|
+
export declare function validateManifest(data: unknown): {
|
|
76
|
+
valid: boolean;
|
|
77
|
+
errors: string[];
|
|
78
|
+
};
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/** Schema for `public/app-manifest.json` — required in every iframe app's build output. */
|
|
2
|
+
// standard window sizes - pick one via window.size
|
|
3
|
+
export const WINDOW_SIZES = {
|
|
4
|
+
xs: { width: 480, height: 320, minWidth: 360, minHeight: 280 },
|
|
5
|
+
sm: { width: 640, height: 480, minWidth: 480, minHeight: 320 },
|
|
6
|
+
md: { width: 800, height: 600, minWidth: 480, minHeight: 320 },
|
|
7
|
+
lg: { width: 1024, height: 800, minWidth: 640, minHeight: 480 },
|
|
8
|
+
xl: { width: 1280, height: 800, minWidth: 800, minHeight: 600 },
|
|
9
|
+
};
|
|
10
|
+
// size used when an app declares no window.size
|
|
11
|
+
export const DEFAULT_WINDOW_SIZE = 'md';
|
|
12
|
+
export const MANIFEST_FILENAME = 'app-manifest.json';
|
|
13
|
+
const VALID_PERMISSIONS = new Set([
|
|
14
|
+
'notify',
|
|
15
|
+
'storage',
|
|
16
|
+
'indexdb',
|
|
17
|
+
// Deprecated: legacy/internal only. Do not request for normal hosted apps.
|
|
18
|
+
'kernel-proxy',
|
|
19
|
+
'transport',
|
|
20
|
+
]);
|
|
21
|
+
function validateBooleanField(value, fieldPath, errors) {
|
|
22
|
+
if (value !== undefined && typeof value !== 'boolean') {
|
|
23
|
+
errors.push(`${fieldPath} must be a boolean`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function validateWindowConfig(windowConfig, errors) {
|
|
27
|
+
if (typeof windowConfig !== 'object' || windowConfig === null || Array.isArray(windowConfig)) {
|
|
28
|
+
errors.push('window must be an object');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const w = windowConfig;
|
|
32
|
+
if (w.size !== undefined) {
|
|
33
|
+
if (typeof w.size === 'string') {
|
|
34
|
+
if (!(w.size in WINDOW_SIZES)) {
|
|
35
|
+
errors.push(`window.size must be one of: ${Object.keys(WINDOW_SIZES).join(', ')}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else if (typeof w.size === 'object' && w.size !== null && !Array.isArray(w.size)) {
|
|
39
|
+
const size = w.size;
|
|
40
|
+
const width = size.width;
|
|
41
|
+
const height = size.height;
|
|
42
|
+
if (typeof width !== 'number' ||
|
|
43
|
+
!Number.isFinite(width) ||
|
|
44
|
+
width <= 0 ||
|
|
45
|
+
typeof height !== 'number' ||
|
|
46
|
+
!Number.isFinite(height) ||
|
|
47
|
+
height <= 0) {
|
|
48
|
+
errors.push('window.size custom dimensions require positive width and height');
|
|
49
|
+
}
|
|
50
|
+
if (size.minWidth !== undefined &&
|
|
51
|
+
(typeof size.minWidth !== 'number' ||
|
|
52
|
+
!Number.isFinite(size.minWidth) ||
|
|
53
|
+
size.minWidth <= 0 ||
|
|
54
|
+
(typeof width === 'number' && size.minWidth > width))) {
|
|
55
|
+
errors.push('window.size custom minWidth must be positive and no greater than width');
|
|
56
|
+
}
|
|
57
|
+
if (size.minHeight !== undefined &&
|
|
58
|
+
(typeof size.minHeight !== 'number' ||
|
|
59
|
+
!Number.isFinite(size.minHeight) ||
|
|
60
|
+
size.minHeight <= 0 ||
|
|
61
|
+
(typeof height === 'number' && size.minHeight > height))) {
|
|
62
|
+
errors.push('window.size custom minHeight must be positive and no greater than height');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
errors.push(`window.size must be one of: ${Object.keys(WINDOW_SIZES).join(', ')}, or a custom { width, height } object`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
validateBooleanField(w.canHaveMultipleWindows, 'window.canHaveMultipleWindows', errors);
|
|
70
|
+
validateBooleanField(w.isInstanceAware, 'window.isInstanceAware', errors);
|
|
71
|
+
validateBooleanField(w.resizable, 'window.resizable', errors);
|
|
72
|
+
validateBooleanField(w.maximizable, 'window.maximizable', errors);
|
|
73
|
+
validateBooleanField(w.minimizable, 'window.minimizable', errors);
|
|
74
|
+
validateBooleanField(w.customScroll, 'window.customScroll', errors);
|
|
75
|
+
validateBooleanField(w.centered, 'window.centered', errors);
|
|
76
|
+
validateBooleanField(w.isHeaderless, 'window.isHeaderless', errors);
|
|
77
|
+
}
|
|
78
|
+
function validateCoreFields(m, errors) {
|
|
79
|
+
if (m.manifestVersion !== 1 && m.manifestVersion !== 2) {
|
|
80
|
+
errors.push('manifestVersion must be 1 or 2');
|
|
81
|
+
}
|
|
82
|
+
if (!m.id || typeof m.id !== 'string' || m.id.trim().length === 0) {
|
|
83
|
+
errors.push('id is required and must be a non-empty string (e.g. "my-app")');
|
|
84
|
+
}
|
|
85
|
+
else if (!/^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/.test(m.id)) {
|
|
86
|
+
errors.push('id must be lowercase alphanumeric with dots, hyphens, or underscores (e.g. "my-app")');
|
|
87
|
+
}
|
|
88
|
+
if (!m.name || typeof m.name !== 'string' || m.name.trim().length === 0) {
|
|
89
|
+
errors.push('name is required and must be a non-empty string');
|
|
90
|
+
}
|
|
91
|
+
if (!m.version || typeof m.version !== 'string') {
|
|
92
|
+
errors.push('version is required and must be a string');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function validateOptionalFields(m, errors) {
|
|
96
|
+
if (m.description !== undefined && typeof m.description !== 'string') {
|
|
97
|
+
errors.push('description must be a string');
|
|
98
|
+
}
|
|
99
|
+
if (m.icon !== undefined && typeof m.icon !== 'string') {
|
|
100
|
+
errors.push('icon must be a string');
|
|
101
|
+
}
|
|
102
|
+
if (m.entry !== undefined && typeof m.entry !== 'string') {
|
|
103
|
+
errors.push('entry must be a string');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function validateAuthor(m, errors) {
|
|
107
|
+
if (m.author !== undefined) {
|
|
108
|
+
if (typeof m.author !== 'object' || m.author === null) {
|
|
109
|
+
errors.push('author must be an object');
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const a = m.author;
|
|
113
|
+
if (!a.name || typeof a.name !== 'string') {
|
|
114
|
+
errors.push('author.name is required and must be a string');
|
|
115
|
+
}
|
|
116
|
+
if (a.url !== undefined && typeof a.url !== 'string') {
|
|
117
|
+
errors.push('author.url must be a string');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function validatePermissions(m, errors) {
|
|
123
|
+
if (m.permissions !== undefined) {
|
|
124
|
+
if (!Array.isArray(m.permissions)) {
|
|
125
|
+
errors.push('permissions must be an array');
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
for (const perm of m.permissions) {
|
|
129
|
+
if (typeof perm !== 'string' || !VALID_PERMISSIONS.has(perm)) {
|
|
130
|
+
errors.push(`Invalid permission: "${String(perm)}". Valid: ${[...VALID_PERMISSIONS].join(', ')}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function validateCategories(m, errors) {
|
|
137
|
+
if (m.categories !== undefined) {
|
|
138
|
+
if (!Array.isArray(m.categories)) {
|
|
139
|
+
errors.push('categories must be an array');
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
for (const cat of m.categories) {
|
|
143
|
+
if (typeof cat !== 'string')
|
|
144
|
+
errors.push('each category must be a string');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function validateDockFields(m, errors) {
|
|
150
|
+
validateBooleanField(m.showInDock, 'showInDock', errors);
|
|
151
|
+
validateBooleanField(m.showInDockAppsMenu, 'showInDockAppsMenu', errors);
|
|
152
|
+
}
|
|
153
|
+
export function validateManifest(data) {
|
|
154
|
+
const errors = [];
|
|
155
|
+
if (!data || typeof data !== 'object') {
|
|
156
|
+
return { valid: false, errors: ['Manifest must be a JSON object'] };
|
|
157
|
+
}
|
|
158
|
+
const m = data;
|
|
159
|
+
validateCoreFields(m, errors);
|
|
160
|
+
validateOptionalFields(m, errors);
|
|
161
|
+
validateAuthor(m, errors);
|
|
162
|
+
validatePermissions(m, errors);
|
|
163
|
+
validateCategories(m, errors);
|
|
164
|
+
validateDockFields(m, errors);
|
|
165
|
+
if (m.window !== undefined) {
|
|
166
|
+
validateWindowConfig(m.window, errors);
|
|
167
|
+
}
|
|
168
|
+
return { valid: errors.length === 0, errors };
|
|
169
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type { SdkCommandDefinition, SdkCommandInvokePayload, SdkIdbDeletePayload, SdkIdbGetPayload, SdkIdbPutPayload, SdkIdbQueryPayload, SdkMenuItem, SdkMenuRoot, SdkMessage, SdkNotifyPayload, SdkResponse, SdkSetActiveTabPayload, SdkSetCommandsPayload, SdkSetMenuPayload, SdkShortcutEventPayload, SdkStorageGetPayload, SdkStorageSetPayload, SdkTransportPayload, } from './types.js';
|
|
2
|
+
export declare const ALLOWED_KERNEL_MESSAGES: ReadonlySet<string>;
|
|
3
|
+
export declare const SDK_MSG: {
|
|
4
|
+
readonly INIT: "SDK_INIT";
|
|
5
|
+
readonly INIT_ACK: "SDK_INIT_ACK";
|
|
6
|
+
readonly RESPONSE: "SDK_RESPONSE";
|
|
7
|
+
readonly NOTIFY: "SDK_NOTIFY";
|
|
8
|
+
readonly KERNEL_PROXY: "SDK_KERNEL_PROXY";
|
|
9
|
+
readonly TRANSPORT: "SDK_TRANSPORT";
|
|
10
|
+
readonly SET_COMMANDS: "SDK_SET_COMMANDS";
|
|
11
|
+
readonly SET_MENU: "SDK_SET_MENU";
|
|
12
|
+
readonly SET_ACTIVE_TAB: "SDK_SET_ACTIVE_TAB";
|
|
13
|
+
readonly SHORTCUT_EVENT: "SDK_SHORTCUT_EVENT";
|
|
14
|
+
readonly COMMAND_INVOKE: "SDK_COMMAND_INVOKE";
|
|
15
|
+
readonly STORAGE_GET: "SDK_STORAGE_GET";
|
|
16
|
+
readonly STORAGE_SET: "SDK_STORAGE_SET";
|
|
17
|
+
readonly STORAGE_DELETE: "SDK_STORAGE_DELETE";
|
|
18
|
+
readonly STORAGE_KEYS: "SDK_STORAGE_KEYS";
|
|
19
|
+
readonly IDB_PUT: "SDK_IDB_PUT";
|
|
20
|
+
readonly IDB_GET: "SDK_IDB_GET";
|
|
21
|
+
readonly IDB_DELETE: "SDK_IDB_DELETE";
|
|
22
|
+
readonly IDB_QUERY: "SDK_IDB_QUERY";
|
|
23
|
+
readonly LAUNCH_APP: "SDK_LAUNCH_APP";
|
|
24
|
+
readonly OS_CONFIG_UPDATE: "SDK_OS_CONFIG_UPDATE";
|
|
25
|
+
};
|
|
26
|
+
export type SdkMessageType = (typeof SDK_MSG)[keyof typeof SDK_MSG];
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// host ↔ iframe postMessage protocol — wire-format types and constants
|
|
2
|
+
// Legacy/internal kernel proxy allow-list. Generic resource CRUD is intentionally
|
|
3
|
+
// not iframe-reachable; apps should use scoped SDK APIs such as idb/transport.
|
|
4
|
+
export const ALLOWED_KERNEL_MESSAGES = new Set(['PING']);
|
|
5
|
+
// All postMessage type strings used in the SDK protocol
|
|
6
|
+
export const SDK_MSG = {
|
|
7
|
+
INIT: 'SDK_INIT',
|
|
8
|
+
INIT_ACK: 'SDK_INIT_ACK',
|
|
9
|
+
RESPONSE: 'SDK_RESPONSE',
|
|
10
|
+
NOTIFY: 'SDK_NOTIFY',
|
|
11
|
+
KERNEL_PROXY: 'SDK_KERNEL_PROXY',
|
|
12
|
+
TRANSPORT: 'SDK_TRANSPORT',
|
|
13
|
+
SET_COMMANDS: 'SDK_SET_COMMANDS',
|
|
14
|
+
SET_MENU: 'SDK_SET_MENU',
|
|
15
|
+
SET_ACTIVE_TAB: 'SDK_SET_ACTIVE_TAB',
|
|
16
|
+
SHORTCUT_EVENT: 'SDK_SHORTCUT_EVENT',
|
|
17
|
+
COMMAND_INVOKE: 'SDK_COMMAND_INVOKE',
|
|
18
|
+
STORAGE_GET: 'SDK_STORAGE_GET',
|
|
19
|
+
STORAGE_SET: 'SDK_STORAGE_SET',
|
|
20
|
+
STORAGE_DELETE: 'SDK_STORAGE_DELETE',
|
|
21
|
+
STORAGE_KEYS: 'SDK_STORAGE_KEYS',
|
|
22
|
+
IDB_PUT: 'SDK_IDB_PUT',
|
|
23
|
+
IDB_GET: 'SDK_IDB_GET',
|
|
24
|
+
IDB_DELETE: 'SDK_IDB_DELETE',
|
|
25
|
+
IDB_QUERY: 'SDK_IDB_QUERY',
|
|
26
|
+
LAUNCH_APP: 'SDK_LAUNCH_APP',
|
|
27
|
+
OS_CONFIG_UPDATE: 'SDK_OS_CONFIG_UPDATE',
|
|
28
|
+
};
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type DevAuthStorageOptions } from './dev.js';
|
|
3
|
+
import type { HostBridge, HostBridgeConfig, NotifyLevel, OsConfig, SdkCommandDefinition, SdkCommandHandler, SdkMenuRoot } from './types.js';
|
|
4
|
+
export interface AppSdkProviderProps extends HostBridgeConfig {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
export interface AppCommand extends SdkCommandDefinition {
|
|
8
|
+
handler: SdkCommandHandler;
|
|
9
|
+
}
|
|
10
|
+
/** Mounts the SDK bridge and makes it available to descendant hooks. */
|
|
11
|
+
export declare function AppSdkProvider({ children, ...config }: Readonly<AppSdkProviderProps>): import("react/jsx-runtime").JSX.Element | null;
|
|
12
|
+
/** Returns the raw {@link HostBridge}. Must be used inside `<AppSdkProvider>`. */
|
|
13
|
+
export declare function useSdk(): HostBridge;
|
|
14
|
+
/** Returns a stable callback that fires a host notification toast. */
|
|
15
|
+
export declare function useNotify(): (message: string, level?: NotifyLevel) => void;
|
|
16
|
+
/** Returns a stable callback that proxies a message to the OS service worker. */
|
|
17
|
+
export declare function useSendToKernel(): <R = unknown>(kernelType: string, kernelPayload?: unknown) => Promise<R>;
|
|
18
|
+
export declare function useCommandMenu(commands: AppCommand[], menu: SdkMenuRoot[] | null): void;
|
|
19
|
+
export declare function useActiveTab(activeTab: string | null): void;
|
|
20
|
+
/**
|
|
21
|
+
* Returns the current OS configuration (theme, language, systemSize) from the host.
|
|
22
|
+
* Automatically subscribes to updates and returns the latest values.
|
|
23
|
+
*/
|
|
24
|
+
export declare function useOsConfig(): OsConfig | null;
|
|
25
|
+
/** Returns a stable callback that clears standalone dev auth state and reloads the page. */
|
|
26
|
+
export declare function useStandaloneDevLogout(options: DevAuthStorageOptions): () => void;
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState, } from 'react';
|
|
3
|
+
import { clearDevAuthSession } from './dev.js';
|
|
4
|
+
import { createHostBridge } from './host-bridge.js';
|
|
5
|
+
const SdkContext = createContext(null);
|
|
6
|
+
const SYSTEM_SIZE_CLASS_NAMES = ['text-sm', 'text-base', 'text-lg', 'text-xl'];
|
|
7
|
+
function applyOsConfigToDocument(osConfig) {
|
|
8
|
+
if (!osConfig) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const root = document.documentElement;
|
|
12
|
+
root.classList.toggle('dark', osConfig.theme === 'dark');
|
|
13
|
+
if (osConfig.language) {
|
|
14
|
+
root.lang = osConfig.language;
|
|
15
|
+
}
|
|
16
|
+
root.classList.remove(...SYSTEM_SIZE_CLASS_NAMES);
|
|
17
|
+
if (osConfig.systemSize && SYSTEM_SIZE_CLASS_NAMES.includes(osConfig.systemSize)) {
|
|
18
|
+
root.classList.add(osConfig.systemSize);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function hasShortcutItems(items) {
|
|
22
|
+
return items.some(item => {
|
|
23
|
+
switch (item.type) {
|
|
24
|
+
case 'command':
|
|
25
|
+
return typeof item.shortcut === 'string' && item.shortcut.trim().length > 0;
|
|
26
|
+
case 'submenu':
|
|
27
|
+
return hasShortcutItems(item.items);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function hasShortcutRoots(menu) {
|
|
32
|
+
if (!menu?.length) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return menu.some(root => hasShortcutItems(root.items));
|
|
36
|
+
}
|
|
37
|
+
/** Mounts the SDK bridge and makes it available to descendant hooks. */
|
|
38
|
+
export function AppSdkProvider({ children, ...config }) {
|
|
39
|
+
const bridgeRef = useRef(null);
|
|
40
|
+
const { appId, connectTimeout, debug, targetOrigin } = config;
|
|
41
|
+
const configKey = JSON.stringify([
|
|
42
|
+
appId ?? null,
|
|
43
|
+
connectTimeout ?? null,
|
|
44
|
+
debug ?? false,
|
|
45
|
+
targetOrigin ?? null,
|
|
46
|
+
]);
|
|
47
|
+
const bridgeConfig = { appId, connectTimeout, debug, targetOrigin };
|
|
48
|
+
const bridgeConfigKeyRef = useRef(null);
|
|
49
|
+
const [readyConfigKey, setReadyConfigKey] = useState(null);
|
|
50
|
+
const [errorState, setErrorState] = useState(null);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
let cancelled = false;
|
|
53
|
+
setReadyConfigKey(null);
|
|
54
|
+
setErrorState(null);
|
|
55
|
+
const bridge = createHostBridge(bridgeConfig);
|
|
56
|
+
bridgeRef.current = bridge;
|
|
57
|
+
bridgeConfigKeyRef.current = configKey;
|
|
58
|
+
bridge
|
|
59
|
+
.connect()
|
|
60
|
+
.then(() => {
|
|
61
|
+
if (!cancelled) {
|
|
62
|
+
setReadyConfigKey(configKey);
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
.catch((err) => {
|
|
66
|
+
if (!cancelled) {
|
|
67
|
+
bridgeRef.current = null;
|
|
68
|
+
bridgeConfigKeyRef.current = null;
|
|
69
|
+
setErrorState({ configKey, message: err.message });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return () => {
|
|
73
|
+
cancelled = true;
|
|
74
|
+
bridge.destroy();
|
|
75
|
+
if (bridgeRef.current === bridge) {
|
|
76
|
+
bridgeRef.current = null;
|
|
77
|
+
}
|
|
78
|
+
if (bridgeConfigKeyRef.current === configKey) {
|
|
79
|
+
bridgeConfigKeyRef.current = null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}, [appId, configKey, connectTimeout, debug, targetOrigin]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (readyConfigKey !== configKey ||
|
|
85
|
+
bridgeConfigKeyRef.current !== configKey ||
|
|
86
|
+
!bridgeRef.current) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
applyOsConfigToDocument(bridgeRef.current.context?.osConfig ?? null);
|
|
90
|
+
const unsubscribe = bridgeRef.current.onOsConfigUpdate(newConfig => {
|
|
91
|
+
applyOsConfigToDocument(newConfig);
|
|
92
|
+
});
|
|
93
|
+
return unsubscribe;
|
|
94
|
+
}, [configKey, readyConfigKey]);
|
|
95
|
+
if (errorState?.configKey === configKey) {
|
|
96
|
+
return _jsx("div", { "data-sdk-error": true, children: errorState.message });
|
|
97
|
+
}
|
|
98
|
+
if (readyConfigKey !== configKey ||
|
|
99
|
+
bridgeConfigKeyRef.current !== configKey ||
|
|
100
|
+
!bridgeRef.current) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return _jsx(SdkContext.Provider, { value: bridgeRef.current, children: children });
|
|
104
|
+
}
|
|
105
|
+
/** Returns the raw {@link HostBridge}. Must be used inside `<AppSdkProvider>`. */
|
|
106
|
+
export function useSdk() {
|
|
107
|
+
const bridge = useContext(SdkContext);
|
|
108
|
+
if (!bridge)
|
|
109
|
+
throw new Error('useSdk() must be used inside <AppSdkProvider>');
|
|
110
|
+
return bridge;
|
|
111
|
+
}
|
|
112
|
+
/** Returns a stable callback that fires a host notification toast. */
|
|
113
|
+
export function useNotify() {
|
|
114
|
+
const bridge = useSdk();
|
|
115
|
+
return useCallback((message, level) => {
|
|
116
|
+
bridge.notify(message, level);
|
|
117
|
+
}, [bridge]);
|
|
118
|
+
}
|
|
119
|
+
/** Returns a stable callback that proxies a message to the OS service worker. */
|
|
120
|
+
export function useSendToKernel() {
|
|
121
|
+
const bridge = useSdk();
|
|
122
|
+
return useCallback((kernelType, kernelPayload) => bridge.sendToKernel(kernelType, kernelPayload), [bridge]);
|
|
123
|
+
}
|
|
124
|
+
export function useCommandMenu(commands, menu) {
|
|
125
|
+
const bridge = useSdk();
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const unregister = bridge.registerCommandHandlers(Object.fromEntries(commands.map(command => [command.id, command.handler])));
|
|
128
|
+
return unregister;
|
|
129
|
+
}, [bridge, commands]);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
const commandState = commands.map(({ handler: _handler, ...command }) => command);
|
|
132
|
+
void bridge.setCommands(commandState).catch((error) => {
|
|
133
|
+
console.error('[app-sdk] failed to sync command state', error);
|
|
134
|
+
});
|
|
135
|
+
void bridge.setMenu(menu).catch((error) => {
|
|
136
|
+
console.error('[app-sdk] failed to sync app menu', error);
|
|
137
|
+
});
|
|
138
|
+
}, [bridge, commands, menu]);
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!hasShortcutRoots(menu)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const handleKeyDown = (event) => {
|
|
144
|
+
if (event.defaultPrevented || event.isComposing || event.repeat) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (!event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
bridge.sendShortcutEvent({
|
|
151
|
+
code: event.code,
|
|
152
|
+
ctrlKey: event.ctrlKey,
|
|
153
|
+
metaKey: event.metaKey,
|
|
154
|
+
altKey: event.altKey,
|
|
155
|
+
shiftKey: event.shiftKey,
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
159
|
+
return () => {
|
|
160
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
161
|
+
};
|
|
162
|
+
}, [bridge, menu]);
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
return () => {
|
|
165
|
+
void bridge.setMenu(null).catch(() => undefined);
|
|
166
|
+
void bridge.setCommands([]).catch(() => undefined);
|
|
167
|
+
};
|
|
168
|
+
}, [bridge]);
|
|
169
|
+
}
|
|
170
|
+
// interface for active tab for iframe app
|
|
171
|
+
export function useActiveTab(activeTab) {
|
|
172
|
+
const bridge = useSdk();
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
void bridge.setActiveTab(activeTab).catch((error) => {
|
|
175
|
+
console.error('[app-sdk] failed to sync active tab', error);
|
|
176
|
+
});
|
|
177
|
+
return () => {
|
|
178
|
+
void bridge.setActiveTab(null).catch(() => undefined);
|
|
179
|
+
};
|
|
180
|
+
}, [bridge, activeTab]);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Returns the current OS configuration (theme, language, systemSize) from the host.
|
|
184
|
+
* Automatically subscribes to updates and returns the latest values.
|
|
185
|
+
*/
|
|
186
|
+
export function useOsConfig() {
|
|
187
|
+
const bridge = useSdk();
|
|
188
|
+
const [osConfig, setOsConfig] = useState(() => bridge.context?.osConfig ?? null);
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
// Sync with current context on mount
|
|
191
|
+
if (bridge.context?.osConfig) {
|
|
192
|
+
setOsConfig(bridge.context.osConfig);
|
|
193
|
+
}
|
|
194
|
+
// Subscribe to OS config updates from host
|
|
195
|
+
const unsubscribe = bridge.onOsConfigUpdate(newConfig => {
|
|
196
|
+
setOsConfig(newConfig);
|
|
197
|
+
});
|
|
198
|
+
return unsubscribe;
|
|
199
|
+
}, [bridge]);
|
|
200
|
+
return osConfig;
|
|
201
|
+
}
|
|
202
|
+
/** Returns a stable callback that clears standalone dev auth state and reloads the page. */
|
|
203
|
+
export function useStandaloneDevLogout(options) {
|
|
204
|
+
return useCallback(() => {
|
|
205
|
+
clearDevAuthSession(options);
|
|
206
|
+
window.location.reload();
|
|
207
|
+
}, [options]);
|
|
208
|
+
}
|