@rainbow-robotics/plugin-ui 0.1.2-dev.8 → 1.0.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/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 +3436 -36
- package/dist/react.js +5 -54
- package/dist/react.js.map +2 -2
- package/dist/sdk.d.ts +38 -2
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +29 -4
- 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 +53 -0
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +141 -0
- package/dist/ws.js.map +7 -0
- package/package.json +45 -21
- package/src/host-ui.ts +80 -0
- package/src/host.tsx +229 -308
- 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 +81 -13
- package/src/viteHost.ts +309 -0
- package/src/ws.ts +127 -20
- package/build-browser.mjs +0 -84
- package/dist/connect.d.ts +0 -11
- package/dist/connect.d.ts.map +0 -1
- package/dist/connect.js +0 -34
- 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-37HVFFJB.js +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-4MGSRUAS.js +0 -1
- package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-Q6XGCIPE.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 -62
- package/src/elements.ts +0 -257
package/src/host.tsx
CHANGED
|
@@ -1,356 +1,277 @@
|
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
RBSelectBox,
|
|
37
|
-
RBSelectOption,
|
|
38
|
-
RBTextInput,
|
|
39
|
-
RBIcons,
|
|
40
|
-
} from "@repo/rb-components/new_components";
|
|
41
|
-
import type { RbSelectOption } from "./elements";
|
|
42
|
-
import type { RbNotifyType } from "./sdk";
|
|
43
|
-
|
|
44
|
-
type IconComponent = ComponentType<{ title?: string }>;
|
|
24
|
+
// Import shared modules via the package specifier, NOT relative paths. This
|
|
25
|
+
// file ships as source (exports "./host" -> src/host.tsx), while /sdk, /ws,
|
|
26
|
+
// /host-ui ship as dist/*.js. A relative "./sdk" would bundle src/sdk.ts,
|
|
27
|
+
// whereas the plugin (and nexus's pluginShared) load dist/sdk.js via the
|
|
28
|
+
// import map — two RbContext instances, so the host's <RbContext.Provider>
|
|
29
|
+
// and the plugin's useRb() would read different contexts and every rb.* call
|
|
30
|
+
// would hit the no-op "outside of a host context" fallback.
|
|
31
|
+
import type { RbSdk } from "@rainbow-robotics/plugin-ui/sdk";
|
|
32
|
+
import { RbContext } from "@rainbow-robotics/plugin-ui/sdk";
|
|
33
|
+
import { RbWs, createLazyRbSdk } from "@rainbow-robotics/plugin-ui/ws";
|
|
34
|
+
import { getHostServices } from "@rainbow-robotics/plugin-ui/host-ui";
|
|
35
|
+
import { usePluginData, type PluginModuleSource } from "./pluginData";
|
|
45
36
|
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
37
|
+
function pluginRootAttrs(pluginId: string) {
|
|
38
|
+
return {
|
|
39
|
+
"data-muscat-plugin-root": "",
|
|
40
|
+
"data-muscat-plugin-id": pluginId,
|
|
41
|
+
};
|
|
50
42
|
}
|
|
51
43
|
|
|
52
44
|
/* -------------------------------------------------------------------------- */
|
|
53
|
-
/*
|
|
45
|
+
/* rb sdk — pub/sub/action/serve over the plugin's Zenoh WebSocket */
|
|
54
46
|
/* -------------------------------------------------------------------------- */
|
|
55
47
|
|
|
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
|
-
}
|
|
48
|
+
/** Per-mount host-provided bits (initial data + save registration). */
|
|
49
|
+
type PluginMount = {
|
|
50
|
+
data?: unknown;
|
|
51
|
+
onSave: (getPayload: () => unknown) => () => void;
|
|
52
|
+
};
|
|
70
53
|
|
|
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
|
-
}
|
|
54
|
+
function useRbSdk(
|
|
55
|
+
socketUrl: string,
|
|
56
|
+
pluginId: string,
|
|
57
|
+
mount: PluginMount,
|
|
58
|
+
): RbSdk {
|
|
59
|
+
const wsRef = useRef<Promise<RbWs> | null>(null);
|
|
60
|
+
const keyRef = useRef("");
|
|
61
|
+
const key = `${socketUrl}::${pluginId}`;
|
|
62
|
+
if (keyRef.current !== key) {
|
|
63
|
+
keyRef.current = key;
|
|
64
|
+
wsRef.current = RbWs.connect(socketUrl, pluginId);
|
|
65
|
+
}
|
|
97
66
|
|
|
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>
|
|
67
|
+
return useMemo<RbSdk>(
|
|
68
|
+
// `mount.data` is captured at first render (stable for a form's lifetime);
|
|
69
|
+
// `mount.onSave` is a stable callback. Both intentionally out of deps.
|
|
70
|
+
() => createLazyRbSdk(wsRef.current!, getHostServices(), mount),
|
|
71
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
72
|
+
[key],
|
|
122
73
|
);
|
|
123
74
|
}
|
|
124
75
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
76
|
+
/* -------------------------------------------------------------------------- */
|
|
77
|
+
/* ESM mount (prod / robot runtime — import map resolves shared externals) */
|
|
78
|
+
/* -------------------------------------------------------------------------- */
|
|
79
|
+
|
|
80
|
+
function EsmPluginHost({
|
|
81
|
+
source,
|
|
82
|
+
socketUrl,
|
|
83
|
+
pluginId,
|
|
84
|
+
mount,
|
|
131
85
|
}: {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
onPress?: () => void;
|
|
86
|
+
source: Extract<PluginModuleSource, { kind: "esm" }>;
|
|
87
|
+
socketUrl: string;
|
|
88
|
+
pluginId: string;
|
|
89
|
+
mount: PluginMount;
|
|
137
90
|
}) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
variant={variant as never}
|
|
142
|
-
size={size as never}
|
|
143
|
-
disabled={disabled}
|
|
144
|
-
onClick={() => onPress?.()}
|
|
145
|
-
/>
|
|
146
|
-
);
|
|
147
|
-
}
|
|
91
|
+
const rb = useRbSdk(socketUrl, pluginId, mount);
|
|
92
|
+
const [App, setApp] = useState<ComponentType | null>(null);
|
|
93
|
+
const [error, setError] = useState<Error | null>(null);
|
|
148
94
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
let cancelled = false;
|
|
97
|
+
setApp(null);
|
|
98
|
+
setError(null);
|
|
99
|
+
import(/* @vite-ignore */ source.entryUrl)
|
|
100
|
+
.then((mod: { default?: ComponentType }) => {
|
|
101
|
+
if (cancelled) return;
|
|
102
|
+
if (!mod.default) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`plugin entry ${source.entryUrl} has no default export`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
setApp(() => mod.default!);
|
|
108
|
+
})
|
|
109
|
+
.catch((e: unknown) => {
|
|
110
|
+
if (!cancelled) setError(e instanceof Error ? e : new Error(String(e)));
|
|
111
|
+
});
|
|
112
|
+
return () => {
|
|
113
|
+
cancelled = true;
|
|
114
|
+
};
|
|
115
|
+
}, [source.entryUrl]);
|
|
152
116
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
117
|
+
if (error) {
|
|
118
|
+
return (
|
|
119
|
+
<div className="text-fg-danger p-4 text-sm">
|
|
120
|
+
Failed to load plugin: {error.message}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (!App) {
|
|
125
|
+
return (
|
|
126
|
+
<div className="flex items-center justify-center p-4">
|
|
127
|
+
<div className="border-default size-5 animate-spin rounded-full border-2 border-t-transparent" />
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
164
131
|
return (
|
|
165
|
-
<
|
|
166
|
-
value={
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
/>
|
|
132
|
+
<div {...pluginRootAttrs(pluginId)}>
|
|
133
|
+
<RbContext.Provider value={rb}>
|
|
134
|
+
<App />
|
|
135
|
+
</RbContext.Provider>
|
|
136
|
+
</div>
|
|
171
137
|
);
|
|
172
138
|
}
|
|
173
139
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
);
|
|
140
|
+
/* -------------------------------------------------------------------------- */
|
|
141
|
+
/* Dev mount (muscat-pk dev) */
|
|
142
|
+
/* -------------------------------------------------------------------------- */
|
|
143
|
+
|
|
144
|
+
interface DevBootstrapModule {
|
|
145
|
+
mount: (
|
|
146
|
+
container: HTMLElement,
|
|
147
|
+
opts: {
|
|
148
|
+
socketUrl: string;
|
|
149
|
+
pluginId: string;
|
|
150
|
+
hostServices: ReturnType<typeof getHostServices>;
|
|
151
|
+
data?: unknown;
|
|
152
|
+
onSave: PluginMount["onSave"];
|
|
153
|
+
},
|
|
154
|
+
) => () => void;
|
|
190
155
|
}
|
|
191
156
|
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
onChange,
|
|
157
|
+
function DevPluginHost({
|
|
158
|
+
source,
|
|
159
|
+
socketUrl,
|
|
160
|
+
pluginId,
|
|
161
|
+
mount,
|
|
198
162
|
}: {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
onChange?: (value: string) => void;
|
|
163
|
+
source: Extract<PluginModuleSource, { kind: "dev" }>;
|
|
164
|
+
socketUrl: string;
|
|
165
|
+
pluginId: string;
|
|
166
|
+
mount: PluginMount;
|
|
204
167
|
}) {
|
|
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
|
-
}
|
|
168
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
169
|
+
const [error, setError] = useState<Error | null>(null);
|
|
220
170
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
"rb-slider",
|
|
228
|
-
createRemoteComponentRenderer(adapt(SliderAdapter), {
|
|
229
|
-
eventProps: { onChange: { event: "change" } },
|
|
230
|
-
}),
|
|
231
|
-
],
|
|
232
|
-
[
|
|
233
|
-
"rb-button",
|
|
234
|
-
createRemoteComponentRenderer(adapt(ButtonAdapter), {
|
|
235
|
-
eventProps: { onPress: { event: "press" } },
|
|
236
|
-
}),
|
|
237
|
-
],
|
|
238
|
-
[
|
|
239
|
-
"rb-icon-button",
|
|
240
|
-
createRemoteComponentRenderer(adapt(IconButtonAdapter), {
|
|
241
|
-
eventProps: { onPress: { event: "press" } },
|
|
242
|
-
}),
|
|
243
|
-
],
|
|
244
|
-
["rb-text", createRemoteComponentRenderer(adapt(TextAdapter))],
|
|
245
|
-
[
|
|
246
|
-
"rb-text-input",
|
|
247
|
-
createRemoteComponentRenderer(adapt(TextInputAdapter), {
|
|
248
|
-
eventProps: { onInput: { event: "input" } },
|
|
249
|
-
}),
|
|
250
|
-
],
|
|
251
|
-
[
|
|
252
|
-
"rb-switch",
|
|
253
|
-
createRemoteComponentRenderer(adapt(SwitchAdapter), {
|
|
254
|
-
eventProps: { onChange: { event: "change" } },
|
|
255
|
-
}),
|
|
256
|
-
],
|
|
257
|
-
[
|
|
258
|
-
"rb-select",
|
|
259
|
-
createRemoteComponentRenderer(adapt(SelectAdapter), {
|
|
260
|
-
eventProps: { onChange: { event: "change" } },
|
|
261
|
-
}),
|
|
262
|
-
],
|
|
263
|
-
]);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
const container = containerRef.current;
|
|
173
|
+
if (!container) return;
|
|
174
|
+
let unmount: (() => void) | undefined;
|
|
175
|
+
let cancelled = false;
|
|
176
|
+
const deferUnmount = () => queueMicrotask(() => unmount?.());
|
|
264
177
|
|
|
265
|
-
/*
|
|
266
|
-
|
|
267
|
-
|
|
178
|
+
import(/* @vite-ignore */ source.bootstrapUrl)
|
|
179
|
+
.then((mod: DevBootstrapModule) => {
|
|
180
|
+
if (cancelled) return;
|
|
181
|
+
unmount = mod.mount(container, {
|
|
182
|
+
socketUrl,
|
|
183
|
+
pluginId,
|
|
184
|
+
hostServices: getHostServices(),
|
|
185
|
+
data: mount.data,
|
|
186
|
+
onSave: mount.onSave,
|
|
187
|
+
});
|
|
188
|
+
})
|
|
189
|
+
.catch((e: unknown) => {
|
|
190
|
+
if (!cancelled) setError(e instanceof Error ? e : new Error(String(e)));
|
|
191
|
+
});
|
|
268
192
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
* and implements pub/sub/action/serve directly; the host only forwards the
|
|
273
|
-
* remote-dom connection, the Socket.IO config, and toast().
|
|
274
|
-
*/
|
|
275
|
-
interface HostThreadExports {
|
|
276
|
-
connect: () => {
|
|
277
|
-
connection: {
|
|
278
|
-
mutate: (records: unknown) => void;
|
|
279
|
-
call: (id: string, method: string, ...args: unknown[]) => unknown;
|
|
193
|
+
return () => {
|
|
194
|
+
cancelled = true;
|
|
195
|
+
deferUnmount();
|
|
280
196
|
};
|
|
281
|
-
|
|
282
|
-
socketUrl: string;
|
|
283
|
-
pluginId: string;
|
|
284
|
-
};
|
|
285
|
-
}
|
|
197
|
+
}, [source.bootstrapUrl, socketUrl, pluginId]);
|
|
286
198
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
toast: (message: string, type?: RbNotifyType) => void;
|
|
199
|
+
if (error) {
|
|
200
|
+
return (
|
|
201
|
+
<div className="text-fg-danger p-4 text-sm">
|
|
202
|
+
Failed to load plugin (dev): {error.message}
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return <div ref={containerRef} {...pluginRootAttrs(pluginId)} />;
|
|
296
207
|
}
|
|
297
208
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
socketUrl,
|
|
209
|
+
/* -------------------------------------------------------------------------- */
|
|
210
|
+
/* Entry point */
|
|
211
|
+
/* -------------------------------------------------------------------------- */
|
|
212
|
+
|
|
213
|
+
/** Renders a plugin's UI. Requires a `PluginDataProvider` above. */
|
|
214
|
+
export function PluginHost({
|
|
305
215
|
pluginId,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
216
|
+
ui,
|
|
217
|
+
data,
|
|
218
|
+
onRegisterSave,
|
|
219
|
+
}: {
|
|
220
|
+
pluginId: string;
|
|
221
|
+
ui: string;
|
|
222
|
+
/** Initial data handed to the plugin (e.g. previously-saved form values). */
|
|
223
|
+
data?: unknown;
|
|
224
|
+
/**
|
|
225
|
+
* Called once with a getter the host can invoke (e.g. from a Save button) to
|
|
226
|
+
* collect the plugin's current payload — `undefined` until the plugin
|
|
227
|
+
* registers via `useRbSave`, or if it never does.
|
|
228
|
+
*/
|
|
229
|
+
onRegisterSave?: (getPayload: () => unknown) => void;
|
|
230
|
+
}) {
|
|
231
|
+
const { getModuleSource, getSocketUrl } = usePluginData();
|
|
232
|
+
const source = useMemo(
|
|
233
|
+
() => getModuleSource(pluginId, ui),
|
|
234
|
+
[getModuleSource, pluginId, ui],
|
|
235
|
+
);
|
|
236
|
+
const socketUrl = getSocketUrl(pluginId);
|
|
312
237
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
238
|
+
// The plugin registers its payload getter here; the host reads it back
|
|
239
|
+
// through the stable getter handed to `onRegisterSave`.
|
|
240
|
+
const saveRef = useRef<(() => unknown) | null>(null);
|
|
241
|
+
const registerSave = useCallback((getPayload: () => unknown) => {
|
|
242
|
+
saveRef.current = getPayload;
|
|
243
|
+
return () => {
|
|
244
|
+
if (saveRef.current === getPayload) saveRef.current = null;
|
|
245
|
+
};
|
|
246
|
+
}, []);
|
|
316
247
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
targetOrigin: "*",
|
|
321
|
-
signal: controller.signal,
|
|
322
|
-
exports: {
|
|
323
|
-
connect() {
|
|
324
|
-
return {
|
|
325
|
-
// forward the receiver's low-level connection across the boundary
|
|
326
|
-
connection: {
|
|
327
|
-
mutate: (records: unknown) =>
|
|
328
|
-
receiver.connection.mutate(records as never),
|
|
329
|
-
call: (id: string, method: string, ...args: unknown[]) =>
|
|
330
|
-
receiver.connection.call(id, method, ...args),
|
|
331
|
-
},
|
|
332
|
-
toast: (message: string, type?: RbNotifyType) =>
|
|
333
|
-
toastRef.current(message, type),
|
|
334
|
-
socketUrl,
|
|
335
|
-
pluginId,
|
|
336
|
-
};
|
|
337
|
-
},
|
|
338
|
-
},
|
|
339
|
-
});
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
onRegisterSave?.(() => saveRef.current?.());
|
|
250
|
+
}, [onRegisterSave]);
|
|
340
251
|
|
|
341
|
-
|
|
342
|
-
|
|
252
|
+
const mount = useMemo<PluginMount>(
|
|
253
|
+
// data captured once (stable per form lifetime); registerSave is stable.
|
|
254
|
+
() => ({ data, onSave: registerSave }),
|
|
255
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
256
|
+
[registerSave],
|
|
257
|
+
);
|
|
343
258
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
title="rb-plugin-sandbox"
|
|
259
|
+
if (source.kind === "dev") {
|
|
260
|
+
return (
|
|
261
|
+
<DevPluginHost
|
|
262
|
+
source={source}
|
|
263
|
+
socketUrl={socketUrl}
|
|
264
|
+
pluginId={pluginId}
|
|
265
|
+
mount={mount}
|
|
352
266
|
/>
|
|
353
|
-
|
|
354
|
-
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
return (
|
|
270
|
+
<EsmPluginHost
|
|
271
|
+
source={source}
|
|
272
|
+
socketUrl={socketUrl}
|
|
273
|
+
pluginId={pluginId}
|
|
274
|
+
mount={mount}
|
|
275
|
+
/>
|
|
355
276
|
);
|
|
356
277
|
}
|
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. */
|