@rainbow-robotics/plugin-ui 0.1.2-dev.9 → 1.0.1

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 +236 -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 +62 -0
  12. package/dist/pluginData.d.ts.map +1 -0
  13. package/dist/react.d.ts +3436 -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 +46 -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 +141 -0
  27. package/dist/ws.js.map +7 -0
  28. package/package.json +45 -21
  29. package/src/host-ui.ts +80 -0
  30. package/src/host.tsx +229 -308
  31. package/src/index.d.ts +1 -0
  32. package/src/manifest.ts +25 -4
  33. package/src/pluginData.tsx +232 -0
  34. package/src/react.tsx +9 -127
  35. package/src/sdk.ts +81 -13
  36. package/src/viteHost.ts +309 -0
  37. package/src/ws.ts +127 -20
  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 -62
  61. package/src/elements.ts +0 -257
package/src/host.tsx CHANGED
@@ -1,356 +1,277 @@
1
1
  /**
2
- * Host bindings — used by nexus (Muscat) only.
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
- * 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.
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
- * 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.
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
- forwardRef,
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
- 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";
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 resolveIcon(name?: string): ReactNode {
47
- if (!name) return null;
48
- const Icon = (RBIcons as Record<string, IconComponent | undefined>)[name];
49
- return Icon ? <Icon /> : null;
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
- /* Adapters: rb-* contract props → real rb-components props */
45
+ /* rb sdk — pub/sub/action/serve over the plugin's Zenoh WebSocket */
54
46
  /* -------------------------------------------------------------------------- */
55
47
 
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
- }
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 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
- }
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
- 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>
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
- function IconButtonAdapter({
126
- icon,
127
- variant,
128
- size,
129
- disabled,
130
- onPress,
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
- icon?: string;
133
- variant?: string;
134
- size?: string;
135
- disabled?: boolean;
136
- onPress?: () => void;
86
+ source: Extract<PluginModuleSource, { kind: "esm" }>;
87
+ socketUrl: string;
88
+ pluginId: string;
89
+ mount: PluginMount;
137
90
  }) {
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
- }
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
- function TextAdapter({ children }: { children?: ReactNode }) {
150
- return <span>{children}</span>;
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
- 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
- }) {
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
- <RBTextInput
166
- value={value ?? ""}
167
- placeholder={placeholder}
168
- disabled={disabled}
169
- onChange={(e) => onInput?.(e.target.value)}
170
- />
132
+ <div {...pluginRootAttrs(pluginId)}>
133
+ <RbContext.Provider value={rb}>
134
+ <App />
135
+ </RbContext.Provider>
136
+ </div>
171
137
  );
172
138
  }
173
139
 
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
- );
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 SelectAdapter({
193
- value,
194
- options = [],
195
- placeholder,
196
- disabled,
197
- onChange,
157
+ function DevPluginHost({
158
+ source,
159
+ socketUrl,
160
+ pluginId,
161
+ mount,
198
162
  }: {
199
- value?: string;
200
- options?: RbSelectOption[];
201
- placeholder?: string;
202
- disabled?: boolean;
203
- onChange?: (value: string) => void;
163
+ source: Extract<PluginModuleSource, { kind: "dev" }>;
164
+ socketUrl: string;
165
+ pluginId: string;
166
+ mount: PluginMount;
204
167
  }) {
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
- }
168
+ const containerRef = useRef<HTMLDivElement>(null);
169
+ const [error, setError] = useState<Error | null>(null);
220
170
 
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
- ]);
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
- /* Host SDK contract + mount component */
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
- * 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;
193
+ return () => {
194
+ cancelled = true;
195
+ deferUnmount();
280
196
  };
281
- toast: (message: string, type?: RbNotifyType) => void;
282
- socketUrl: string;
283
- pluginId: string;
284
- };
285
- }
197
+ }, [source.bootstrapUrl, socketUrl, pluginId]);
286
198
 
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;
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
- * 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,
209
+ /* -------------------------------------------------------------------------- */
210
+ /* Entry point */
211
+ /* -------------------------------------------------------------------------- */
212
+
213
+ /** Renders a plugin's UI. Requires a `PluginDataProvider` above. */
214
+ export function PluginHost({
305
215
  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;
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
- useEffect(() => {
314
- const iframe = iframeRef.current;
315
- if (!iframe) return;
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
- 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
- });
248
+ useEffect(() => {
249
+ onRegisterSave?.(() => saveRef.current?.());
250
+ }, [onRegisterSave]);
340
251
 
341
- return () => controller.abort();
342
- }, [receiver, socketUrl, pluginId]);
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
- return (
345
- <>
346
- <iframe
347
- ref={iframeRef}
348
- src={sandboxUrl}
349
- sandbox="allow-scripts"
350
- hidden
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
- <RemoteRootRenderer receiver={receiver} components={RB_COMPONENT_MAP} />
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
- "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. */