@rainbow-robotics/plugin-ui 0.1.2-dev.5 → 0.1.2-dev.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +236 -75
- package/build-dist.mjs +55 -17
- package/dist/host-ui.d.ts +49 -0
- package/dist/host-ui.d.ts.map +1 -0
- package/dist/host-ui.js +33 -0
- package/dist/host-ui.js.map +7 -0
- package/dist/manifest.d.ts +20 -4
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +1 -1
- package/dist/manifest.js.map +2 -2
- package/dist/pluginData.d.ts +62 -0
- package/dist/pluginData.d.ts.map +1 -0
- package/dist/react.d.ts +3430 -36
- package/dist/react.js +5 -54
- package/dist/react.js.map +2 -2
- package/dist/sdk.d.ts +54 -4
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +44 -10
- package/dist/sdk.js.map +2 -2
- package/dist/viteHost.d.ts +46 -0
- package/dist/viteHost.d.ts.map +1 -0
- package/dist/viteHost.js +262 -0
- package/dist/viteHost.js.map +7 -0
- package/dist/ws.d.ts +73 -0
- package/dist/ws.d.ts.map +1 -0
- package/dist/ws.js +129 -0
- package/dist/ws.js.map +7 -0
- package/package.json +41 -22
- package/src/host-ui.ts +80 -0
- package/src/host.tsx +225 -295
- package/src/index.d.ts +1 -0
- package/src/manifest.ts +25 -4
- package/src/pluginData.tsx +232 -0
- package/src/react.tsx +9 -127
- package/src/sdk.ts +119 -18
- package/src/viteHost.ts +309 -0
- package/src/ws.ts +204 -0
- package/build-browser.mjs +0 -84
- package/dist/connect.d.ts +0 -12
- package/dist/connect.d.ts.map +0 -1
- package/dist/connect.js +0 -29
- package/dist/connect.js.map +0 -7
- package/dist/elements.d.ts +0 -195
- package/dist/elements.d.ts.map +0 -1
- package/dist/elements.js +0 -115
- package/dist/elements.js.map +0 -7
- package/dist/react.d.ts.map +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-AMCIKVG7.js +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-GJWPOWHG.js +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-JRCDA3EG.js +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/connect.js +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/elements.js +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/react.js +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/sdk.js +0 -1
- package/dist-browser/vendor/react-dom-client.js +0 -1
- package/dist-browser/vendor/react-jsx-dev-runtime.js +0 -1
- package/dist-browser/vendor/react-jsx-runtime.js +0 -1
- package/dist-browser/vendor/react-runtime.js +0 -65
- package/dist-browser/vendor/react.js +0 -1
- package/src/connect.ts +0 -68
- package/src/elements.ts +0 -257
package/src/host.tsx
CHANGED
|
@@ -1,343 +1,273 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Host
|
|
2
|
+
* Host mounting (nexus only). A plugin renders directly in the host document —
|
|
3
|
+
* no iframe. Two strategies, per `PluginModuleSource.kind` (see ./pluginData):
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* - "esm" (built plugin / robot runtime): dynamic `import()` of the plugin's
|
|
6
|
+
* built entry. react / react-dom / rb-components / plugin-ui are external bare
|
|
7
|
+
* specifiers, resolved by the host page's import map to the host's own loaded
|
|
8
|
+
* instances (see ./viteHost.ts). So the plugin shares the host's React,
|
|
9
|
+
* rb-components, and RbContext, with no duplicate React instance.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* (see ./connect) consumes that connection with a RemoteMutationObserver.
|
|
11
|
+
* - "dev" (`muscat-pk dev`): the plugin's Vite dev server serves a bootstrap
|
|
12
|
+
* module exposing `mount(container, opts)`, mounted as an independent React
|
|
13
|
+
* root using the plugin's own deps. HostServices passed in are plain
|
|
14
|
+
* functions (see ./host-ui), so they cross the React-instance boundary safely.
|
|
15
15
|
*/
|
|
16
16
|
import {
|
|
17
|
-
|
|
17
|
+
useCallback,
|
|
18
18
|
useEffect,
|
|
19
19
|
useMemo,
|
|
20
20
|
useRef,
|
|
21
|
+
useState,
|
|
21
22
|
type ComponentType,
|
|
22
|
-
type ReactNode,
|
|
23
23
|
} from "react";
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
RBSlider,
|
|
33
|
-
RBLabelButton,
|
|
34
|
-
RBIconButton,
|
|
35
|
-
RBSwitch,
|
|
36
|
-
RBSelectBox,
|
|
37
|
-
RBSelectOption,
|
|
38
|
-
RBTextInput,
|
|
39
|
-
RBIcons,
|
|
40
|
-
} from "@repo/rb-components/new_components";
|
|
41
|
-
import type { RbSelectOption } from "./elements";
|
|
42
|
-
import type { RbSdk } from "./sdk";
|
|
24
|
+
import { RbContext, type RbSdk } from "./sdk";
|
|
25
|
+
import { RbWs, createLazyRbSdk } from "./ws";
|
|
26
|
+
// Import via the package specifier, not a relative "./host-ui": this file ships
|
|
27
|
+
// as source (exports "./host" -> src/host.tsx), so a relative path would hit
|
|
28
|
+
// src/host-ui.ts while other consumers hit dist/host-ui.js — two modules, two
|
|
29
|
+
// `services` singletons.
|
|
30
|
+
import { getHostServices } from "@rainbow-robotics/plugin-ui/host-ui";
|
|
31
|
+
import { usePluginData, type PluginModuleSource } from "./pluginData";
|
|
43
32
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return Icon ? <Icon /> : null;
|
|
33
|
+
function pluginRootAttrs(pluginId: string) {
|
|
34
|
+
return {
|
|
35
|
+
"data-muscat-plugin-root": "",
|
|
36
|
+
"data-muscat-plugin-id": pluginId,
|
|
37
|
+
};
|
|
50
38
|
}
|
|
51
39
|
|
|
52
40
|
/* -------------------------------------------------------------------------- */
|
|
53
|
-
/*
|
|
41
|
+
/* rb sdk — pub/sub/action/serve over the plugin's Zenoh WebSocket */
|
|
54
42
|
/* -------------------------------------------------------------------------- */
|
|
55
43
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
*/
|
|
62
|
-
function adapt<P extends object>(
|
|
63
|
-
Component: (props: P) => ReactNode,
|
|
64
|
-
): ComponentType<P> {
|
|
65
|
-
const Wrapped = forwardRef<unknown, P>(function Adapted(props, _ref) {
|
|
66
|
-
return Component(props as P);
|
|
67
|
-
});
|
|
68
|
-
return Wrapped as unknown as ComponentType<P>;
|
|
69
|
-
}
|
|
44
|
+
/** Per-mount host-provided bits (initial data + save registration). */
|
|
45
|
+
type PluginMount = {
|
|
46
|
+
data?: unknown;
|
|
47
|
+
onSave: (getPayload: () => unknown) => () => void;
|
|
48
|
+
};
|
|
70
49
|
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
disabled?: boolean;
|
|
84
|
-
onChange?: (value: number) => void;
|
|
85
|
-
}) {
|
|
86
|
-
return (
|
|
87
|
-
<RBSlider
|
|
88
|
-
value={[value ?? min]}
|
|
89
|
-
min={min}
|
|
90
|
-
max={max}
|
|
91
|
-
step={step}
|
|
92
|
-
disabled={disabled}
|
|
93
|
-
onValueChange={(values) => onChange?.(values[0] ?? min)}
|
|
94
|
-
/>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
50
|
+
function useRbSdk(
|
|
51
|
+
socketUrl: string,
|
|
52
|
+
pluginId: string,
|
|
53
|
+
mount: PluginMount,
|
|
54
|
+
): RbSdk {
|
|
55
|
+
const wsRef = useRef<Promise<RbWs> | null>(null);
|
|
56
|
+
const keyRef = useRef("");
|
|
57
|
+
const key = `${socketUrl}::${pluginId}`;
|
|
58
|
+
if (keyRef.current !== key) {
|
|
59
|
+
keyRef.current = key;
|
|
60
|
+
wsRef.current = RbWs.connect(socketUrl, pluginId);
|
|
61
|
+
}
|
|
97
62
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
children,
|
|
105
|
-
}: {
|
|
106
|
-
label?: string;
|
|
107
|
-
variant?: string;
|
|
108
|
-
size?: string;
|
|
109
|
-
disabled?: boolean;
|
|
110
|
-
onPress?: () => void;
|
|
111
|
-
children?: ReactNode;
|
|
112
|
-
}) {
|
|
113
|
-
return (
|
|
114
|
-
<RBLabelButton
|
|
115
|
-
variant={variant as never}
|
|
116
|
-
size={size as never}
|
|
117
|
-
disabled={disabled}
|
|
118
|
-
onClick={() => onPress?.()}
|
|
119
|
-
>
|
|
120
|
-
{label ?? children}
|
|
121
|
-
</RBLabelButton>
|
|
63
|
+
return useMemo<RbSdk>(
|
|
64
|
+
// `mount.data` is captured at first render (stable for a form's lifetime);
|
|
65
|
+
// `mount.onSave` is a stable callback. Both intentionally out of deps.
|
|
66
|
+
() => createLazyRbSdk(wsRef.current!, getHostServices(), mount),
|
|
67
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
68
|
+
[key],
|
|
122
69
|
);
|
|
123
70
|
}
|
|
124
71
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
72
|
+
/* -------------------------------------------------------------------------- */
|
|
73
|
+
/* ESM mount (prod / robot runtime — import map resolves shared externals) */
|
|
74
|
+
/* -------------------------------------------------------------------------- */
|
|
75
|
+
|
|
76
|
+
function EsmPluginHost({
|
|
77
|
+
source,
|
|
78
|
+
socketUrl,
|
|
79
|
+
pluginId,
|
|
80
|
+
mount,
|
|
131
81
|
}: {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
onPress?: () => void;
|
|
82
|
+
source: Extract<PluginModuleSource, { kind: "esm" }>;
|
|
83
|
+
socketUrl: string;
|
|
84
|
+
pluginId: string;
|
|
85
|
+
mount: PluginMount;
|
|
137
86
|
}) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
variant={variant as never}
|
|
142
|
-
size={size as never}
|
|
143
|
-
disabled={disabled}
|
|
144
|
-
onClick={() => onPress?.()}
|
|
145
|
-
/>
|
|
146
|
-
);
|
|
147
|
-
}
|
|
87
|
+
const rb = useRbSdk(socketUrl, pluginId, mount);
|
|
88
|
+
const [App, setApp] = useState<ComponentType | null>(null);
|
|
89
|
+
const [error, setError] = useState<Error | null>(null);
|
|
148
90
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
let cancelled = false;
|
|
93
|
+
setApp(null);
|
|
94
|
+
setError(null);
|
|
95
|
+
import(/* @vite-ignore */ source.entryUrl)
|
|
96
|
+
.then((mod: { default?: ComponentType }) => {
|
|
97
|
+
if (cancelled) return;
|
|
98
|
+
if (!mod.default) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`plugin entry ${source.entryUrl} has no default export`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
setApp(() => mod.default!);
|
|
104
|
+
})
|
|
105
|
+
.catch((e: unknown) => {
|
|
106
|
+
if (!cancelled) setError(e instanceof Error ? e : new Error(String(e)));
|
|
107
|
+
});
|
|
108
|
+
return () => {
|
|
109
|
+
cancelled = true;
|
|
110
|
+
};
|
|
111
|
+
}, [source.entryUrl]);
|
|
152
112
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
113
|
+
if (error) {
|
|
114
|
+
return (
|
|
115
|
+
<div className="text-fg-danger p-4 text-sm">
|
|
116
|
+
Failed to load plugin: {error.message}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (!App) {
|
|
121
|
+
return (
|
|
122
|
+
<div className="flex items-center justify-center p-4">
|
|
123
|
+
<div className="border-default size-5 animate-spin rounded-full border-2 border-t-transparent" />
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
164
127
|
return (
|
|
165
|
-
<
|
|
166
|
-
value={
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
/>
|
|
128
|
+
<div {...pluginRootAttrs(pluginId)}>
|
|
129
|
+
<RbContext.Provider value={rb}>
|
|
130
|
+
<App />
|
|
131
|
+
</RbContext.Provider>
|
|
132
|
+
</div>
|
|
171
133
|
);
|
|
172
134
|
}
|
|
173
135
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
);
|
|
136
|
+
/* -------------------------------------------------------------------------- */
|
|
137
|
+
/* Dev mount (muscat-pk dev) */
|
|
138
|
+
/* -------------------------------------------------------------------------- */
|
|
139
|
+
|
|
140
|
+
interface DevBootstrapModule {
|
|
141
|
+
mount: (
|
|
142
|
+
container: HTMLElement,
|
|
143
|
+
opts: {
|
|
144
|
+
socketUrl: string;
|
|
145
|
+
pluginId: string;
|
|
146
|
+
hostServices: ReturnType<typeof getHostServices>;
|
|
147
|
+
data?: unknown;
|
|
148
|
+
onSave: PluginMount["onSave"];
|
|
149
|
+
},
|
|
150
|
+
) => () => void;
|
|
190
151
|
}
|
|
191
152
|
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
onChange,
|
|
153
|
+
function DevPluginHost({
|
|
154
|
+
source,
|
|
155
|
+
socketUrl,
|
|
156
|
+
pluginId,
|
|
157
|
+
mount,
|
|
198
158
|
}: {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
onChange?: (value: string) => void;
|
|
159
|
+
source: Extract<PluginModuleSource, { kind: "dev" }>;
|
|
160
|
+
socketUrl: string;
|
|
161
|
+
pluginId: string;
|
|
162
|
+
mount: PluginMount;
|
|
204
163
|
}) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
value={value ?? ""}
|
|
208
|
-
placeholder={placeholder}
|
|
209
|
-
disabled={disabled}
|
|
210
|
-
onChange={(e) => onChange?.((e.target as HTMLInputElement).value)}
|
|
211
|
-
>
|
|
212
|
-
{options.map((option) => (
|
|
213
|
-
<RBSelectOption key={option.value} value={option.value}>
|
|
214
|
-
{option.label}
|
|
215
|
-
</RBSelectOption>
|
|
216
|
-
))}
|
|
217
|
-
</RBSelectBox>
|
|
218
|
-
);
|
|
219
|
-
}
|
|
164
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
165
|
+
const [error, setError] = useState<Error | null>(null);
|
|
220
166
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
const container = containerRef.current;
|
|
169
|
+
if (!container) return;
|
|
170
|
+
let unmount: (() => void) | undefined;
|
|
171
|
+
let cancelled = false;
|
|
172
|
+
const deferUnmount = () => queueMicrotask(() => unmount?.());
|
|
224
173
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
[
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}),
|
|
256
|
-
],
|
|
257
|
-
[
|
|
258
|
-
"rb-select",
|
|
259
|
-
createRemoteComponentRenderer(adapt(SelectAdapter), {
|
|
260
|
-
eventProps: { onChange: { event: "change" } },
|
|
261
|
-
}),
|
|
262
|
-
],
|
|
263
|
-
]);
|
|
174
|
+
import(/* @vite-ignore */ source.bootstrapUrl)
|
|
175
|
+
.then((mod: DevBootstrapModule) => {
|
|
176
|
+
if (cancelled) return;
|
|
177
|
+
unmount = mod.mount(container, {
|
|
178
|
+
socketUrl,
|
|
179
|
+
pluginId,
|
|
180
|
+
hostServices: getHostServices(),
|
|
181
|
+
data: mount.data,
|
|
182
|
+
onSave: mount.onSave,
|
|
183
|
+
});
|
|
184
|
+
})
|
|
185
|
+
.catch((e: unknown) => {
|
|
186
|
+
if (!cancelled) setError(e instanceof Error ? e : new Error(String(e)));
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return () => {
|
|
190
|
+
cancelled = true;
|
|
191
|
+
deferUnmount();
|
|
192
|
+
};
|
|
193
|
+
}, [source.bootstrapUrl, socketUrl, pluginId]);
|
|
194
|
+
|
|
195
|
+
if (error) {
|
|
196
|
+
return (
|
|
197
|
+
<div className="text-fg-danger p-4 text-sm">
|
|
198
|
+
Failed to load plugin (dev): {error.message}
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return <div ref={containerRef} {...pluginRootAttrs(pluginId)} />;
|
|
203
|
+
}
|
|
264
204
|
|
|
265
205
|
/* -------------------------------------------------------------------------- */
|
|
266
|
-
/*
|
|
206
|
+
/* Entry point */
|
|
267
207
|
/* -------------------------------------------------------------------------- */
|
|
268
208
|
|
|
269
|
-
|
|
209
|
+
/** Renders a plugin's UI. Requires a `PluginDataProvider` above. */
|
|
210
|
+
export function PluginHost({
|
|
211
|
+
pluginId,
|
|
212
|
+
ui,
|
|
213
|
+
data,
|
|
214
|
+
onRegisterSave,
|
|
215
|
+
}: {
|
|
216
|
+
pluginId: string;
|
|
217
|
+
ui: string;
|
|
218
|
+
/** Initial data handed to the plugin (e.g. previously-saved form values). */
|
|
219
|
+
data?: unknown;
|
|
220
|
+
/**
|
|
221
|
+
* Called once with a getter the host can invoke (e.g. from a Save button) to
|
|
222
|
+
* collect the plugin's current payload — `undefined` until the plugin
|
|
223
|
+
* registers via `useRbSave`, or if it never does.
|
|
224
|
+
*/
|
|
225
|
+
onRegisterSave?: (getPayload: () => unknown) => void;
|
|
226
|
+
}) {
|
|
227
|
+
const { getModuleSource, getSocketUrl } = usePluginData();
|
|
228
|
+
const source = useMemo(
|
|
229
|
+
() => getModuleSource(pluginId, ui),
|
|
230
|
+
[getModuleSource, pluginId, ui],
|
|
231
|
+
);
|
|
232
|
+
const socketUrl = getSocketUrl(pluginId);
|
|
270
233
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
234
|
+
// The plugin registers its payload getter here; the host reads it back
|
|
235
|
+
// through the stable getter handed to `onRegisterSave`.
|
|
236
|
+
const saveRef = useRef<(() => unknown) | null>(null);
|
|
237
|
+
const registerSave = useCallback((getPayload: () => unknown) => {
|
|
238
|
+
saveRef.current = getPayload;
|
|
239
|
+
return () => {
|
|
240
|
+
if (saveRef.current === getPayload) saveRef.current = null;
|
|
277
241
|
};
|
|
278
|
-
|
|
279
|
-
notify: (message: string, type?: string) => void;
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export interface PluginContentProps {
|
|
284
|
-
/** URL of the plugin sandbox document (e.g. /plugin/:id/sandbox.html). */
|
|
285
|
-
sandboxUrl: string;
|
|
286
|
-
/** Host SDK implementation handed to the plugin. */
|
|
287
|
-
rb: RbSdkImpl;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Renders a sandboxed plugin: spins up a hidden iframe running the plugin's
|
|
292
|
-
* remote tree, and mirrors that tree with real rb-components in the host.
|
|
293
|
-
*/
|
|
294
|
-
export function PluginContent({ sandboxUrl, rb }: PluginContentProps) {
|
|
295
|
-
const receiver = useMemo(() => new RemoteReceiver(), []);
|
|
296
|
-
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
297
|
-
const rbRef = useRef(rb);
|
|
298
|
-
rbRef.current = rb;
|
|
242
|
+
}, []);
|
|
299
243
|
|
|
300
244
|
useEffect(() => {
|
|
301
|
-
|
|
302
|
-
|
|
245
|
+
onRegisterSave?.(() => saveRef.current?.());
|
|
246
|
+
}, [onRegisterSave]);
|
|
303
247
|
|
|
304
|
-
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
connect() {
|
|
311
|
-
return {
|
|
312
|
-
// forward the receiver's low-level connection across the boundary
|
|
313
|
-
connection: {
|
|
314
|
-
mutate: (records: unknown) =>
|
|
315
|
-
receiver.connection.mutate(records as never),
|
|
316
|
-
call: (id: string, method: string, ...args: unknown[]) =>
|
|
317
|
-
receiver.connection.call(id, method, ...args),
|
|
318
|
-
},
|
|
319
|
-
action: (id: string, payload?: unknown) =>
|
|
320
|
-
rbRef.current.action(id, payload),
|
|
321
|
-
notify: (message: string, type?: string) =>
|
|
322
|
-
rbRef.current.notify(message, type as never),
|
|
323
|
-
};
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
return () => controller.abort();
|
|
329
|
-
}, [receiver]);
|
|
248
|
+
const mount = useMemo<PluginMount>(
|
|
249
|
+
// data captured once (stable per form lifetime); registerSave is stable.
|
|
250
|
+
() => ({ data, onSave: registerSave }),
|
|
251
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
252
|
+
[registerSave],
|
|
253
|
+
);
|
|
330
254
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
<
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
title="rb-plugin-sandbox"
|
|
255
|
+
if (source.kind === "dev") {
|
|
256
|
+
return (
|
|
257
|
+
<DevPluginHost
|
|
258
|
+
source={source}
|
|
259
|
+
socketUrl={socketUrl}
|
|
260
|
+
pluginId={pluginId}
|
|
261
|
+
mount={mount}
|
|
339
262
|
/>
|
|
340
|
-
|
|
341
|
-
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return (
|
|
266
|
+
<EsmPluginHost
|
|
267
|
+
source={source}
|
|
268
|
+
socketUrl={socketUrl}
|
|
269
|
+
pluginId={pluginId}
|
|
270
|
+
mount={mount}
|
|
271
|
+
/>
|
|
342
272
|
);
|
|
343
273
|
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="@repo/typescript-config" />
|
package/src/manifest.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1
2
|
/**
|
|
2
3
|
* Plugin manifest schema — the single source of truth shared by the front end
|
|
3
4
|
* (nexus reads it) and the back end (rby2 mirrors it for validation).
|
|
@@ -7,7 +8,7 @@
|
|
|
7
8
|
|
|
8
9
|
/** Known contribution points the host understands. */
|
|
9
10
|
export const CONTRIBUTION_POINTS = [
|
|
10
|
-
"
|
|
11
|
+
"toolbar",
|
|
11
12
|
"lnb",
|
|
12
13
|
"move.panel",
|
|
13
14
|
"program.panel",
|
|
@@ -21,12 +22,26 @@ export type ContributionOpens = "popover" | "panel" | "page";
|
|
|
21
22
|
export interface PluginContribution {
|
|
22
23
|
/** Where this contribution mounts. */
|
|
23
24
|
point: ContributionPoint | (string & {});
|
|
25
|
+
/** UI entry name relative to ui/ (without extension). Defaults to "content". */
|
|
26
|
+
ui?: string;
|
|
24
27
|
/** Icon path relative to the plugin root (e.g. "assets/icon.svg"). */
|
|
25
|
-
icon?:
|
|
28
|
+
icon?:
|
|
29
|
+
| string
|
|
30
|
+
| {
|
|
31
|
+
"16"?: string;
|
|
32
|
+
"48"?: string;
|
|
33
|
+
"126"?: string;
|
|
34
|
+
};
|
|
26
35
|
/** Display label. */
|
|
27
|
-
label?: string
|
|
36
|
+
label?: string | Record<string, string>;
|
|
28
37
|
/** For header.toolbar: how the contribution surfaces. */
|
|
29
38
|
opens?: ContributionOpens;
|
|
39
|
+
commandInfo?: {
|
|
40
|
+
name: string;
|
|
41
|
+
icon: string;
|
|
42
|
+
funcName: string;
|
|
43
|
+
args?: Record<string, any>;
|
|
44
|
+
};
|
|
30
45
|
}
|
|
31
46
|
|
|
32
47
|
export interface PluginAction {
|
|
@@ -58,7 +73,13 @@ export interface PluginManifest {
|
|
|
58
73
|
/** Minimum host (Muscat) version this plugin supports. */
|
|
59
74
|
minAppVersion?: string;
|
|
60
75
|
/** Icon path relative to plugin root. */
|
|
61
|
-
|
|
76
|
+
icons:
|
|
77
|
+
| string
|
|
78
|
+
| {
|
|
79
|
+
"16"?: string;
|
|
80
|
+
"48"?: string;
|
|
81
|
+
"128"?: string;
|
|
82
|
+
};
|
|
62
83
|
/** UI / chrome contributions. */
|
|
63
84
|
contributes?: PluginContribution[];
|
|
64
85
|
/** Host actions the plugin can invoke. */
|