@rainbow-robotics/plugin-ui 0.1.2-dev.44 → 0.1.2-dev.46

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.
Files changed (61) hide show
  1. package/README.md +222 -75
  2. package/build-dist.mjs +55 -17
  3. package/dist/host-ui.d.ts +49 -0
  4. package/dist/host-ui.d.ts.map +1 -0
  5. package/dist/host-ui.js +33 -0
  6. package/dist/host-ui.js.map +7 -0
  7. package/dist/manifest.d.ts +20 -4
  8. package/dist/manifest.d.ts.map +1 -1
  9. package/dist/manifest.js +1 -1
  10. package/dist/manifest.js.map +2 -2
  11. package/dist/pluginData.d.ts +68 -0
  12. package/dist/pluginData.d.ts.map +1 -0
  13. package/dist/react.d.ts +3430 -36
  14. package/dist/react.js +5 -54
  15. package/dist/react.js.map +2 -2
  16. package/dist/sdk.d.ts +38 -2
  17. package/dist/sdk.d.ts.map +1 -1
  18. package/dist/sdk.js +29 -4
  19. package/dist/sdk.js.map +2 -2
  20. package/dist/viteHost.d.ts +48 -0
  21. package/dist/viteHost.d.ts.map +1 -0
  22. package/dist/viteHost.js +262 -0
  23. package/dist/viteHost.js.map +7 -0
  24. package/dist/ws.d.ts +53 -0
  25. package/dist/ws.d.ts.map +1 -1
  26. package/dist/ws.js +128 -0
  27. package/dist/ws.js.map +7 -0
  28. package/package.json +43 -20
  29. package/src/host-ui.ts +80 -0
  30. package/src/host.tsx +236 -308
  31. package/src/index.d.ts +1 -0
  32. package/src/manifest.ts +25 -4
  33. package/src/pluginData.tsx +241 -0
  34. package/src/react.tsx +16 -127
  35. package/src/sdk.ts +74 -7
  36. package/src/viteHost.ts +316 -0
  37. package/src/ws.ts +47 -0
  38. package/build-browser.mjs +0 -84
  39. package/dist/connect.d.ts +0 -11
  40. package/dist/connect.d.ts.map +0 -1
  41. package/dist/connect.js +0 -34
  42. package/dist/connect.js.map +0 -7
  43. package/dist/elements.d.ts +0 -195
  44. package/dist/elements.d.ts.map +0 -1
  45. package/dist/elements.js +0 -115
  46. package/dist/elements.js.map +0 -7
  47. package/dist/react.d.ts.map +0 -1
  48. package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-37HVFFJB.js +0 -1
  49. package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-4MGSRUAS.js +0 -1
  50. package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-Q6XGCIPE.js +0 -1
  51. package/dist-browser/@rainbow-robotics/plugin-ui/connect.js +0 -1
  52. package/dist-browser/@rainbow-robotics/plugin-ui/elements.js +0 -1
  53. package/dist-browser/@rainbow-robotics/plugin-ui/react.js +0 -1
  54. package/dist-browser/@rainbow-robotics/plugin-ui/sdk.js +0 -1
  55. package/dist-browser/vendor/react-dom-client.js +0 -1
  56. package/dist-browser/vendor/react-jsx-dev-runtime.js +0 -1
  57. package/dist-browser/vendor/react-jsx-runtime.js +0 -1
  58. package/dist-browser/vendor/react-runtime.js +0 -65
  59. package/dist-browser/vendor/react.js +0 -1
  60. package/src/connect.ts +0 -64
  61. package/src/elements.ts +0 -257
package/src/host.tsx CHANGED
@@ -1,356 +1,284 @@
1
1
  /**
2
- * Host bindings — used by nexus (Muscat) only.
2
+ * Host mounting — used by nexus (Muscat) only.
3
3
  *
4
- * Maps the rb-* remote element contract onto the *real* @repo/rb-components,
5
- * and renders a sandboxed plugin's remote tree with those real components.
6
- * The plugin never ships rb-components; it only emits rb-* name tags, which the
7
- * host resolves here. This keeps plugin bundles small and lets rb-components
8
- * evolve freely behind the stable contract.
4
+ * No iframe, no remote-dom, no Module Federation: a plugin renders directly in
5
+ * the host document. Two mount strategies, chosen per `PluginModuleSource.kind`
6
+ * (see ./pluginData):
9
7
  *
10
- * Verified wiring (@remote-dom/react@1.2.x host + @quilted/threads@4.x):
11
- * - `RemoteReceiver` / `RemoteRootRenderer` / `createRemoteComponentRenderer`
12
- * from `@remote-dom/react/host`
13
- * - host calls `thread.imports.render(receiver.connection, api)`; the sandbox
14
- * (see ./connect) consumes that connection with a RemoteMutationObserver.
8
+ * - "esm" (built plugin, `muscat-pk build` / robot runtime): the host does a
9
+ * plain dynamic `import()` of the plugin's built entry. The plugin is built
10
+ * with react / react-dom / rb-components / plugin-ui context modules left
11
+ * EXTERNAL as bare specifiers; the host page carries an import map (see
12
+ * @rainbow-robotics/plugin-ui/host-runtime + the shim modules nexus serves)
13
+ * that resolves those specifiers to the host's OWN already-loaded instances.
14
+ * So the loaded component shares the host's single React + rb-components +
15
+ * RbContext + host-ui — rendered as a genuinely nested React tree, with no
16
+ * duplicate React instance and minimal plugin bundle size.
17
+ *
18
+ * - "dev" (`muscat-pk dev`): the plugin's own Vite dev server serves a
19
+ * bootstrap module exposing `mount(container, opts)`. That module uses ITS
20
+ * OWN react/react-dom/plugin-ui (resolved from the plugin's own node_modules),
21
+ * so it self-mounts into the container as a fully independent React root.
22
+ * HostServices passed in are plain functions (see ./host-ui) — safe to
23
+ * call across the React-instance boundary, since they don't touch React
24
+ * internals themselves.
15
25
  */
16
26
  import {
17
- forwardRef,
27
+ useCallback,
18
28
  useEffect,
19
29
  useMemo,
20
30
  useRef,
31
+ useState,
21
32
  type ComponentType,
22
- type ReactNode,
23
33
  } from "react";
24
- import {
25
- RemoteReceiver,
26
- RemoteRootRenderer,
27
- createRemoteComponentRenderer,
28
- type RemoteComponentRendererMap,
29
- } from "@remote-dom/react/host";
30
- import { ThreadWindow } from "@quilted/threads";
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 { RbNotifyType } from "./sdk";
34
+ import { RbContext, type RbSdk } from "./sdk";
35
+ import { RbWs, createLazyRbSdk } from "./ws";
36
+ // IMPORTANT: this file is shipped as SOURCE (see package.json — "./host" maps
37
+ // to src/host.tsx, not dist), so a relative "./host-ui" import here would
38
+ // resolve to src/host-ui.ts, a DIFFERENT file than the dist/host-ui.js every
39
+ // other consumer (e.g. nexus's App.tsx) resolves to — two files, two separate
40
+ // module-scope `services` singletons. Must import through the package specifier.
41
+ import { getHostServices } from "@rainbow-robotics/plugin-ui/host-ui";
42
+ import { usePluginData, type PluginModuleSource } from "./pluginData";
43
43
 
44
- type IconComponent = ComponentType<{ title?: string }>;
45
-
46
- function resolveIcon(name?: string): ReactNode {
47
- if (!name) return null;
48
- const Icon = (RBIcons as Record<string, IconComponent | undefined>)[name];
49
- return Icon ? <Icon /> : null;
44
+ function pluginRootAttrs(pluginId: string) {
45
+ return {
46
+ "data-muscat-plugin-root": "",
47
+ "data-muscat-plugin-id": pluginId,
48
+ };
50
49
  }
51
50
 
52
51
  /* -------------------------------------------------------------------------- */
53
- /* Adapters: rb-* contract props → real rb-components props */
52
+ /* rb sdk — pub/sub/action/serve over the plugin's Zenoh WebSocket */
54
53
  /* -------------------------------------------------------------------------- */
55
54
 
56
- /**
57
- * `createRemoteComponentRenderer` forwards a ref to the wrapped component. Our
58
- * elements expose no remote methods, so wrap each adapter in `forwardRef` (the
59
- * ref is intentionally unused) to avoid React's "function components cannot be
60
- * given refs" warning.
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
- }
55
+ /** Per-mount host-provided bits (initial data + save registration). */
56
+ type PluginMount = {
57
+ data?: unknown;
58
+ onSave: (getPayload: () => unknown) => () => void;
59
+ };
70
60
 
71
- function SliderAdapter({
72
- value,
73
- min = 0,
74
- max = 100,
75
- step,
76
- disabled,
77
- onChange,
78
- }: {
79
- value?: number;
80
- min?: number;
81
- max?: number;
82
- step?: number;
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
- }
61
+ function useRbSdk(
62
+ socketUrl: string,
63
+ pluginId: string,
64
+ mount: PluginMount,
65
+ ): RbSdk {
66
+ const wsRef = useRef<Promise<RbWs> | null>(null);
67
+ const keyRef = useRef("");
68
+ const key = `${socketUrl}::${pluginId}`;
69
+ if (keyRef.current !== key) {
70
+ keyRef.current = key;
71
+ wsRef.current = RbWs.connect(socketUrl, pluginId);
72
+ }
97
73
 
98
- function ButtonAdapter({
99
- label,
100
- variant,
101
- size,
102
- disabled,
103
- onPress,
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>
74
+ return useMemo<RbSdk>(
75
+ // `mount.data` is captured at first render (stable for a form's lifetime);
76
+ // `mount.onSave` is a stable callback. Both intentionally out of deps.
77
+ () => createLazyRbSdk(wsRef.current!, getHostServices(), mount),
78
+ // eslint-disable-next-line react-hooks/exhaustive-deps
79
+ [key],
122
80
  );
123
81
  }
124
82
 
125
- function IconButtonAdapter({
126
- icon,
127
- variant,
128
- size,
129
- disabled,
130
- onPress,
83
+ /* -------------------------------------------------------------------------- */
84
+ /* ESM mount (prod / robot runtime — import map resolves shared externals) */
85
+ /* -------------------------------------------------------------------------- */
86
+
87
+ function EsmPluginHost({
88
+ source,
89
+ socketUrl,
90
+ pluginId,
91
+ mount,
131
92
  }: {
132
- icon?: string;
133
- variant?: string;
134
- size?: string;
135
- disabled?: boolean;
136
- onPress?: () => void;
93
+ source: Extract<PluginModuleSource, { kind: "esm" }>;
94
+ socketUrl: string;
95
+ pluginId: string;
96
+ mount: PluginMount;
137
97
  }) {
138
- return (
139
- <RBIconButton
140
- icon={resolveIcon(icon)}
141
- variant={variant as never}
142
- size={size as never}
143
- disabled={disabled}
144
- onClick={() => onPress?.()}
145
- />
146
- );
147
- }
98
+ const rb = useRbSdk(socketUrl, pluginId, mount);
99
+ const [App, setApp] = useState<ComponentType | null>(null);
100
+ const [error, setError] = useState<Error | null>(null);
148
101
 
149
- function TextAdapter({ children }: { children?: ReactNode }) {
150
- return <span>{children}</span>;
151
- }
102
+ useEffect(() => {
103
+ let cancelled = false;
104
+ setApp(null);
105
+ setError(null);
106
+ import(/* @vite-ignore */ source.entryUrl)
107
+ .then((mod: { default?: ComponentType }) => {
108
+ if (cancelled) return;
109
+ if (!mod.default) {
110
+ throw new Error(
111
+ `plugin entry ${source.entryUrl} has no default export`,
112
+ );
113
+ }
114
+ setApp(() => mod.default!);
115
+ })
116
+ .catch((e: unknown) => {
117
+ if (!cancelled) setError(e instanceof Error ? e : new Error(String(e)));
118
+ });
119
+ return () => {
120
+ cancelled = true;
121
+ };
122
+ }, [source.entryUrl]);
152
123
 
153
- function TextInputAdapter({
154
- value,
155
- placeholder,
156
- disabled,
157
- onInput,
158
- }: {
159
- value?: string;
160
- placeholder?: string;
161
- disabled?: boolean;
162
- onInput?: (value: string) => void;
163
- }) {
124
+ if (error) {
125
+ return (
126
+ <div className="text-fg-danger p-4 text-sm">
127
+ Failed to load plugin: {error.message}
128
+ </div>
129
+ );
130
+ }
131
+ if (!App) {
132
+ return (
133
+ <div className="flex items-center justify-center p-4">
134
+ <div className="border-default size-5 animate-spin rounded-full border-2 border-t-transparent" />
135
+ </div>
136
+ );
137
+ }
164
138
  return (
165
- <RBTextInput
166
- value={value ?? ""}
167
- placeholder={placeholder}
168
- disabled={disabled}
169
- onChange={(e) => onInput?.(e.target.value)}
170
- />
139
+ <div {...pluginRootAttrs(pluginId)}>
140
+ <RbContext.Provider value={rb}>
141
+ <App />
142
+ </RbContext.Provider>
143
+ </div>
171
144
  );
172
145
  }
173
146
 
174
- function SwitchAdapter({
175
- checked,
176
- disabled,
177
- onChange,
178
- }: {
179
- checked?: boolean;
180
- disabled?: boolean;
181
- onChange?: (checked: boolean) => void;
182
- }) {
183
- return (
184
- <RBSwitch
185
- checked={checked ?? false}
186
- disabled={disabled}
187
- onCheckedChange={(next) => onChange?.(next)}
188
- />
189
- );
147
+ /* -------------------------------------------------------------------------- */
148
+ /* Dev mount (muscat-pk dev) */
149
+ /* -------------------------------------------------------------------------- */
150
+
151
+ interface DevBootstrapModule {
152
+ mount: (
153
+ container: HTMLElement,
154
+ opts: {
155
+ socketUrl: string;
156
+ pluginId: string;
157
+ hostServices: ReturnType<typeof getHostServices>;
158
+ data?: unknown;
159
+ onSave: PluginMount["onSave"];
160
+ },
161
+ ) => () => void;
190
162
  }
191
163
 
192
- function SelectAdapter({
193
- value,
194
- options = [],
195
- placeholder,
196
- disabled,
197
- onChange,
164
+ function DevPluginHost({
165
+ source,
166
+ socketUrl,
167
+ pluginId,
168
+ mount,
198
169
  }: {
199
- value?: string;
200
- options?: RbSelectOption[];
201
- placeholder?: string;
202
- disabled?: boolean;
203
- onChange?: (value: string) => void;
170
+ source: Extract<PluginModuleSource, { kind: "dev" }>;
171
+ socketUrl: string;
172
+ pluginId: string;
173
+ mount: PluginMount;
204
174
  }) {
205
- return (
206
- <RBSelectBox
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
- }
175
+ const containerRef = useRef<HTMLDivElement>(null);
176
+ const [error, setError] = useState<Error | null>(null);
220
177
 
221
- /* -------------------------------------------------------------------------- */
222
- /* Component map: tag → renderer */
223
- /* -------------------------------------------------------------------------- */
224
-
225
- export const RB_COMPONENT_MAP: RemoteComponentRendererMap = new Map([
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
- ]);
178
+ useEffect(() => {
179
+ const container = containerRef.current;
180
+ if (!container) return;
181
+ let unmount: (() => void) | undefined;
182
+ let cancelled = false;
183
+ const deferUnmount = () => queueMicrotask(() => unmount?.());
264
184
 
265
- /* -------------------------------------------------------------------------- */
266
- /* Host SDK contract + mount component */
267
- /* -------------------------------------------------------------------------- */
185
+ import(/* @vite-ignore */ source.bootstrapUrl)
186
+ .then((mod: DevBootstrapModule) => {
187
+ if (cancelled) return;
188
+ unmount = mod.mount(container, {
189
+ socketUrl,
190
+ pluginId,
191
+ hostServices: getHostServices(),
192
+ data: mount.data,
193
+ onSave: mount.onSave,
194
+ });
195
+ })
196
+ .catch((e: unknown) => {
197
+ if (!cancelled) setError(e instanceof Error ? e : new Error(String(e)));
198
+ });
268
199
 
269
- /**
270
- * Shape the host exposes to the sandbox over the thread (see ./connect).
271
- * The sandbox opens its own Socket.IO connection from `socketUrl` + `pluginId`
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;
200
+ return () => {
201
+ cancelled = true;
202
+ deferUnmount();
280
203
  };
281
- toast: (message: string, type?: RbNotifyType) => void;
282
- socketUrl: string;
283
- pluginId: string;
284
- };
285
- }
204
+ }, [source.bootstrapUrl, socketUrl, pluginId]);
286
205
 
287
- export interface PluginContentProps {
288
- /** URL of the plugin sandbox document (e.g. /plugin/:id/sandbox.html). */
289
- sandboxUrl: string;
290
- /** Zenoh router URL the sandbox connects to. */
291
- socketUrl: string;
292
- /** Plugin id — used for Zenoh topic namespacing inside the sandbox. */
293
- pluginId: string;
294
- /** Host toast notification handed to the plugin. */
295
- toast: (message: string, type?: RbNotifyType) => void;
206
+ if (error) {
207
+ return (
208
+ <div className="text-fg-danger p-4 text-sm">
209
+ Failed to load plugin (dev): {error.message}
210
+ </div>
211
+ );
212
+ }
213
+ return <div ref={containerRef} {...pluginRootAttrs(pluginId)} />;
296
214
  }
297
215
 
298
- /**
299
- * Renders a sandboxed plugin: spins up a hidden iframe running the plugin's
300
- * remote tree, and mirrors that tree with real rb-components in the host.
301
- */
302
- export function PluginContent({
303
- sandboxUrl,
304
- socketUrl,
216
+ /* -------------------------------------------------------------------------- */
217
+ /* Entry point */
218
+ /* -------------------------------------------------------------------------- */
219
+
220
+ /** Renders a plugin's UI. Requires a `PluginDataProvider` above. */
221
+ export function PluginHost({
305
222
  pluginId,
306
- toast,
307
- }: PluginContentProps) {
308
- const receiver = useMemo(() => new RemoteReceiver(), []);
309
- const iframeRef = useRef<HTMLIFrameElement>(null);
310
- const toastRef = useRef(toast);
311
- toastRef.current = toast;
223
+ ui,
224
+ data,
225
+ onRegisterSave,
226
+ }: {
227
+ pluginId: string;
228
+ ui: string;
229
+ /** Initial data handed to the plugin (e.g. previously-saved form values). */
230
+ data?: unknown;
231
+ /**
232
+ * Called once with a getter the host can invoke (e.g. from a Save button) to
233
+ * collect the plugin's current payload — `undefined` until the plugin
234
+ * registers via `useRbSave`, or if it never does.
235
+ */
236
+ onRegisterSave?: (getPayload: () => unknown) => void;
237
+ }) {
238
+ const { getModuleSource, getSocketUrl } = usePluginData();
239
+ const source = useMemo(
240
+ () => getModuleSource(pluginId, ui),
241
+ [getModuleSource, pluginId, ui],
242
+ );
243
+ const socketUrl = getSocketUrl(pluginId);
312
244
 
313
- useEffect(() => {
314
- const iframe = iframeRef.current;
315
- if (!iframe) return;
245
+ // The plugin registers its payload getter here; the host reads it back
246
+ // through the stable getter handed to `onRegisterSave`.
247
+ const saveRef = useRef<(() => unknown) | null>(null);
248
+ const registerSave = useCallback((getPayload: () => unknown) => {
249
+ saveRef.current = getPayload;
250
+ return () => {
251
+ if (saveRef.current === getPayload) saveRef.current = null;
252
+ };
253
+ }, []);
316
254
 
317
- const controller = new AbortController();
318
- // Host listens from mount; the sandbox calls connect() once it is ready.
319
- ThreadWindow.iframe<Record<string, never>, HostThreadExports>(iframe, {
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
- });
255
+ useEffect(() => {
256
+ onRegisterSave?.(() => saveRef.current?.());
257
+ }, [onRegisterSave]);
340
258
 
341
- return () => controller.abort();
342
- }, [receiver, socketUrl, pluginId]);
259
+ const mount = useMemo<PluginMount>(
260
+ // data captured once (stable per form lifetime); registerSave is stable.
261
+ () => ({ data, onSave: registerSave }),
262
+ // eslint-disable-next-line react-hooks/exhaustive-deps
263
+ [registerSave],
264
+ );
343
265
 
344
- return (
345
- <>
346
- <iframe
347
- ref={iframeRef}
348
- src={sandboxUrl}
349
- sandbox="allow-scripts"
350
- hidden
351
- title="rb-plugin-sandbox"
266
+ if (source.kind === "dev") {
267
+ return (
268
+ <DevPluginHost
269
+ source={source}
270
+ socketUrl={socketUrl}
271
+ pluginId={pluginId}
272
+ mount={mount}
352
273
  />
353
- <RemoteRootRenderer receiver={receiver} components={RB_COMPONENT_MAP} />
354
- </>
274
+ );
275
+ }
276
+ return (
277
+ <EsmPluginHost
278
+ source={source}
279
+ socketUrl={socketUrl}
280
+ pluginId={pluginId}
281
+ mount={mount}
282
+ />
355
283
  );
356
284
  }
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
- "header.toolbar",
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?: string;
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
- icon?: string;
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. */