@rainbow-robotics/plugin-ui 0.1.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 +85 -0
- package/build-browser.mjs +84 -0
- package/build-dist.mjs +42 -0
- package/dist/connect.d.ts +12 -0
- package/dist/connect.d.ts.map +1 -0
- package/dist/connect.js +29 -0
- package/dist/connect.js.map +7 -0
- package/dist/elements.d.ts +195 -0
- package/dist/elements.d.ts.map +1 -0
- package/dist/elements.js +115 -0
- package/dist/elements.js.map +7 -0
- package/dist/manifest.d.ts +65 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +29 -0
- package/dist/manifest.js.map +7 -0
- package/dist/react.d.ts +47 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +57 -0
- package/dist/react.js.map +7 -0
- package/dist/sdk.d.ts +14 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +23 -0
- package/dist/sdk.js.map +7 -0
- package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-AMCIKVG7.js +1 -0
- package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-GJWPOWHG.js +1 -0
- package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-JRCDA3EG.js +1 -0
- package/dist-browser/@rainbow-robotics/plugin-ui/connect.js +1 -0
- package/dist-browser/@rainbow-robotics/plugin-ui/elements.js +1 -0
- package/dist-browser/@rainbow-robotics/plugin-ui/react.js +1 -0
- package/dist-browser/@rainbow-robotics/plugin-ui/sdk.js +1 -0
- package/dist-browser/vendor/react-dom-client.js +1 -0
- package/dist-browser/vendor/react-jsx-dev-runtime.js +1 -0
- package/dist-browser/vendor/react-jsx-runtime.js +1 -0
- package/dist-browser/vendor/react-runtime.js +65 -0
- package/dist-browser/vendor/react.js +1 -0
- package/package.json +73 -0
- package/src/connect.ts +68 -0
- package/src/elements.ts +257 -0
- package/src/host.tsx +343 -0
- package/src/manifest.ts +92 -0
- package/src/react.tsx +131 -0
- package/src/sdk.ts +41 -0
package/src/elements.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rb-* remote element contract — shared by the sandbox (plugin author) and the
|
|
3
|
+
* nexus host. This is the *stable* surface: it must only ever grow additively
|
|
4
|
+
* (new elements / new optional properties), never break existing shapes.
|
|
5
|
+
*
|
|
6
|
+
* Verified against @remote-dom/core@1.11.x:
|
|
7
|
+
* - `RemoteElement` lives in `@remote-dom/core/elements`
|
|
8
|
+
* - static `remoteProperties` (data) + static `remoteEvents` (callbacks)
|
|
9
|
+
*/
|
|
10
|
+
import { RemoteElement } from "@remote-dom/core/elements";
|
|
11
|
+
|
|
12
|
+
type RbNoMethods = Record<never, never>;
|
|
13
|
+
type RbNoSlots = Record<never, never>;
|
|
14
|
+
type RbNoEvents = Record<never, never>;
|
|
15
|
+
|
|
16
|
+
export interface RbSliderProps {
|
|
17
|
+
value?: number;
|
|
18
|
+
min?: number;
|
|
19
|
+
max?: number;
|
|
20
|
+
step?: number;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RbButtonProps {
|
|
25
|
+
label?: string;
|
|
26
|
+
variant?: string;
|
|
27
|
+
size?: string;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RbIconButtonProps {
|
|
32
|
+
icon?: string;
|
|
33
|
+
variant?: string;
|
|
34
|
+
size?: string;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type RbTextProps = Record<never, never>;
|
|
39
|
+
|
|
40
|
+
export interface RbTextInputProps {
|
|
41
|
+
value?: string;
|
|
42
|
+
placeholder?: string;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RbSwitchProps {
|
|
47
|
+
checked?: boolean;
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Option shape carried by `rb-select`'s `options` property. */
|
|
52
|
+
export interface RbSelectOption {
|
|
53
|
+
value: string;
|
|
54
|
+
label: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RbSelectProps {
|
|
58
|
+
value?: string;
|
|
59
|
+
options?: RbSelectOption[];
|
|
60
|
+
placeholder?: string;
|
|
61
|
+
disabled?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface RbSliderEvents {
|
|
65
|
+
change(detail: number): void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface RbPressEvents {
|
|
69
|
+
press(): void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface RbTextInputEvents {
|
|
73
|
+
input(detail: string): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface RbSwitchEvents {
|
|
77
|
+
change(detail: boolean): void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface RbSelectEvents {
|
|
81
|
+
change(detail: string): void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Slider → host renders RBSlider (Radix array value, single-thumb contract). */
|
|
85
|
+
export class RbSliderElement extends RemoteElement<
|
|
86
|
+
RbSliderProps,
|
|
87
|
+
RbNoMethods,
|
|
88
|
+
RbNoSlots,
|
|
89
|
+
RbSliderEvents
|
|
90
|
+
> {
|
|
91
|
+
static get remoteProperties() {
|
|
92
|
+
return {
|
|
93
|
+
value: { type: Number },
|
|
94
|
+
min: { type: Number },
|
|
95
|
+
max: { type: Number },
|
|
96
|
+
step: { type: Number },
|
|
97
|
+
disabled: { type: Boolean },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
static get remoteEvents() {
|
|
101
|
+
return ["change"] as const;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Button → host renders RBLabelButton. */
|
|
106
|
+
export class RbButtonElement extends RemoteElement<
|
|
107
|
+
RbButtonProps,
|
|
108
|
+
RbNoMethods,
|
|
109
|
+
RbNoSlots,
|
|
110
|
+
RbPressEvents
|
|
111
|
+
> {
|
|
112
|
+
static get remoteProperties() {
|
|
113
|
+
return {
|
|
114
|
+
label: { type: String },
|
|
115
|
+
variant: { type: String },
|
|
116
|
+
size: { type: String },
|
|
117
|
+
disabled: { type: Boolean },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
static get remoteEvents() {
|
|
121
|
+
return ["press"] as const;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Icon button → host renders RBIconButton with an icon resolved from RBIcons. */
|
|
126
|
+
export class RbIconButtonElement extends RemoteElement<
|
|
127
|
+
RbIconButtonProps,
|
|
128
|
+
RbNoMethods,
|
|
129
|
+
RbNoSlots,
|
|
130
|
+
RbPressEvents
|
|
131
|
+
> {
|
|
132
|
+
static get remoteProperties() {
|
|
133
|
+
return {
|
|
134
|
+
icon: { type: String },
|
|
135
|
+
variant: { type: String },
|
|
136
|
+
size: { type: String },
|
|
137
|
+
disabled: { type: Boolean },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
static get remoteEvents() {
|
|
141
|
+
return ["press"] as const;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Plain text — renders its children as text on the host. */
|
|
146
|
+
export class RbTextElement extends RemoteElement<
|
|
147
|
+
RbTextProps,
|
|
148
|
+
RbNoMethods,
|
|
149
|
+
RbNoSlots,
|
|
150
|
+
RbNoEvents
|
|
151
|
+
> {
|
|
152
|
+
static get remoteProperties() {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Text input → host renders RBTextInput. */
|
|
158
|
+
export class RbTextInputElement extends RemoteElement<
|
|
159
|
+
RbTextInputProps,
|
|
160
|
+
RbNoMethods,
|
|
161
|
+
RbNoSlots,
|
|
162
|
+
RbTextInputEvents
|
|
163
|
+
> {
|
|
164
|
+
static get remoteProperties() {
|
|
165
|
+
return {
|
|
166
|
+
value: { type: String },
|
|
167
|
+
placeholder: { type: String },
|
|
168
|
+
disabled: { type: Boolean },
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
static get remoteEvents() {
|
|
172
|
+
return ["input"] as const;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Switch → host renders RBSwitch. */
|
|
177
|
+
export class RbSwitchElement extends RemoteElement<
|
|
178
|
+
RbSwitchProps,
|
|
179
|
+
RbNoMethods,
|
|
180
|
+
RbNoSlots,
|
|
181
|
+
RbSwitchEvents
|
|
182
|
+
> {
|
|
183
|
+
static get remoteProperties() {
|
|
184
|
+
return {
|
|
185
|
+
checked: { type: Boolean },
|
|
186
|
+
disabled: { type: Boolean },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
static get remoteEvents() {
|
|
190
|
+
return ["change"] as const;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Select → host renders RBSelectBox. `options` is `[{value,label}]`. */
|
|
195
|
+
export class RbSelectElement extends RemoteElement<
|
|
196
|
+
RbSelectProps,
|
|
197
|
+
RbNoMethods,
|
|
198
|
+
RbNoSlots,
|
|
199
|
+
RbSelectEvents
|
|
200
|
+
> {
|
|
201
|
+
static get remoteProperties() {
|
|
202
|
+
return {
|
|
203
|
+
value: { type: String },
|
|
204
|
+
options: { type: Array },
|
|
205
|
+
placeholder: { type: String },
|
|
206
|
+
disabled: { type: Boolean },
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
static get remoteEvents() {
|
|
210
|
+
return ["change"] as const;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Custom element tag names in the contract. */
|
|
215
|
+
export const RB_ELEMENT_NAMES = [
|
|
216
|
+
"rb-slider",
|
|
217
|
+
"rb-button",
|
|
218
|
+
"rb-icon-button",
|
|
219
|
+
"rb-text",
|
|
220
|
+
"rb-text-input",
|
|
221
|
+
"rb-switch",
|
|
222
|
+
"rb-select",
|
|
223
|
+
] as const;
|
|
224
|
+
|
|
225
|
+
export type RbElementName = (typeof RB_ELEMENT_NAMES)[number];
|
|
226
|
+
|
|
227
|
+
declare global {
|
|
228
|
+
interface HTMLElementTagNameMap {
|
|
229
|
+
"rb-slider": RbSliderElement;
|
|
230
|
+
"rb-button": RbButtonElement;
|
|
231
|
+
"rb-icon-button": RbIconButtonElement;
|
|
232
|
+
"rb-text": RbTextElement;
|
|
233
|
+
"rb-text-input": RbTextInputElement;
|
|
234
|
+
"rb-switch": RbSwitchElement;
|
|
235
|
+
"rb-select": RbSelectElement;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let defined = false;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Registers the rb-* custom elements. Safe to call multiple times; only the
|
|
243
|
+
* first call defines anything (subsequent calls are a no-op).
|
|
244
|
+
*/
|
|
245
|
+
export function defineElements(): void {
|
|
246
|
+
if (defined || typeof customElements === "undefined") return;
|
|
247
|
+
|
|
248
|
+
defined = true;
|
|
249
|
+
|
|
250
|
+
customElements.define("rb-slider", RbSliderElement);
|
|
251
|
+
customElements.define("rb-button", RbButtonElement);
|
|
252
|
+
customElements.define("rb-icon-button", RbIconButtonElement);
|
|
253
|
+
customElements.define("rb-text", RbTextElement);
|
|
254
|
+
customElements.define("rb-text-input", RbTextInputElement);
|
|
255
|
+
customElements.define("rb-switch", RbSwitchElement);
|
|
256
|
+
customElements.define("rb-select", RbSelectElement);
|
|
257
|
+
}
|
package/src/host.tsx
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host bindings — used by nexus (Muscat) only.
|
|
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.
|
|
9
|
+
*
|
|
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.
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
forwardRef,
|
|
18
|
+
useEffect,
|
|
19
|
+
useMemo,
|
|
20
|
+
useRef,
|
|
21
|
+
type ComponentType,
|
|
22
|
+
type ReactNode,
|
|
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 { RbSdk } from "./sdk";
|
|
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;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* -------------------------------------------------------------------------- */
|
|
53
|
+
/* Adapters: rb-* contract props → real rb-components props */
|
|
54
|
+
/* -------------------------------------------------------------------------- */
|
|
55
|
+
|
|
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
|
+
}
|
|
70
|
+
|
|
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
|
+
}
|
|
97
|
+
|
|
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>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function IconButtonAdapter({
|
|
126
|
+
icon,
|
|
127
|
+
variant,
|
|
128
|
+
size,
|
|
129
|
+
disabled,
|
|
130
|
+
onPress,
|
|
131
|
+
}: {
|
|
132
|
+
icon?: string;
|
|
133
|
+
variant?: string;
|
|
134
|
+
size?: string;
|
|
135
|
+
disabled?: boolean;
|
|
136
|
+
onPress?: () => void;
|
|
137
|
+
}) {
|
|
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
|
+
}
|
|
148
|
+
|
|
149
|
+
function TextAdapter({ children }: { children?: ReactNode }) {
|
|
150
|
+
return <span>{children}</span>;
|
|
151
|
+
}
|
|
152
|
+
|
|
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
|
+
}) {
|
|
164
|
+
return (
|
|
165
|
+
<RBTextInput
|
|
166
|
+
value={value ?? ""}
|
|
167
|
+
placeholder={placeholder}
|
|
168
|
+
disabled={disabled}
|
|
169
|
+
onChange={(e) => onInput?.(e.target.value)}
|
|
170
|
+
/>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
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
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function SelectAdapter({
|
|
193
|
+
value,
|
|
194
|
+
options = [],
|
|
195
|
+
placeholder,
|
|
196
|
+
disabled,
|
|
197
|
+
onChange,
|
|
198
|
+
}: {
|
|
199
|
+
value?: string;
|
|
200
|
+
options?: RbSelectOption[];
|
|
201
|
+
placeholder?: string;
|
|
202
|
+
disabled?: boolean;
|
|
203
|
+
onChange?: (value: string) => void;
|
|
204
|
+
}) {
|
|
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
|
+
}
|
|
220
|
+
|
|
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
|
+
]);
|
|
264
|
+
|
|
265
|
+
/* -------------------------------------------------------------------------- */
|
|
266
|
+
/* Host SDK contract + mount component */
|
|
267
|
+
/* -------------------------------------------------------------------------- */
|
|
268
|
+
|
|
269
|
+
export type RbSdkImpl = RbSdk;
|
|
270
|
+
|
|
271
|
+
/** Shape the host exposes to the sandbox over the thread (see ./connect). */
|
|
272
|
+
interface HostThreadExports {
|
|
273
|
+
connect: () => {
|
|
274
|
+
connection: {
|
|
275
|
+
mutate: (records: unknown) => void;
|
|
276
|
+
call: (id: string, method: string, ...args: unknown[]) => unknown;
|
|
277
|
+
};
|
|
278
|
+
action: (id: string, payload?: unknown) => Promise<unknown>;
|
|
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;
|
|
299
|
+
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
const iframe = iframeRef.current;
|
|
302
|
+
if (!iframe) return;
|
|
303
|
+
|
|
304
|
+
const controller = new AbortController();
|
|
305
|
+
// Host listens from mount; the sandbox calls connect() once it is ready.
|
|
306
|
+
ThreadWindow.iframe<Record<string, never>, HostThreadExports>(iframe, {
|
|
307
|
+
targetOrigin: "*",
|
|
308
|
+
signal: controller.signal,
|
|
309
|
+
exports: {
|
|
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]);
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<>
|
|
333
|
+
<iframe
|
|
334
|
+
ref={iframeRef}
|
|
335
|
+
src={sandboxUrl}
|
|
336
|
+
sandbox="allow-scripts"
|
|
337
|
+
hidden
|
|
338
|
+
title="rb-plugin-sandbox"
|
|
339
|
+
/>
|
|
340
|
+
<RemoteRootRenderer receiver={receiver} components={RB_COMPONENT_MAP} />
|
|
341
|
+
</>
|
|
342
|
+
);
|
|
343
|
+
}
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin manifest schema — the single source of truth shared by the front end
|
|
3
|
+
* (nexus reads it) and the back end (rby2 mirrors it for validation).
|
|
4
|
+
*
|
|
5
|
+
* Evolve additively only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Known contribution points the host understands. */
|
|
9
|
+
export const CONTRIBUTION_POINTS = [
|
|
10
|
+
"header.toolbar",
|
|
11
|
+
"lnb",
|
|
12
|
+
"move.panel",
|
|
13
|
+
"program.panel",
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export type ContributionPoint = (typeof CONTRIBUTION_POINTS)[number];
|
|
17
|
+
|
|
18
|
+
/** How a header.toolbar contribution opens when clicked. */
|
|
19
|
+
export type ContributionOpens = "popover" | "panel" | "page";
|
|
20
|
+
|
|
21
|
+
export interface PluginContribution {
|
|
22
|
+
/** Where this contribution mounts. */
|
|
23
|
+
point: ContributionPoint | (string & {});
|
|
24
|
+
/** Icon path relative to the plugin root (e.g. "assets/icon.svg"). */
|
|
25
|
+
icon?: string;
|
|
26
|
+
/** Display label. */
|
|
27
|
+
label?: string;
|
|
28
|
+
/** For header.toolbar: how the contribution surfaces. */
|
|
29
|
+
opens?: ContributionOpens;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PluginAction {
|
|
33
|
+
/** Action id referenced by `rb.action(id, payload)`. */
|
|
34
|
+
id: string;
|
|
35
|
+
/** Transport topic (Zenoh / ROS2) the back end routes to. */
|
|
36
|
+
topic: string;
|
|
37
|
+
/** Optional payload schema hint (field → type name). */
|
|
38
|
+
schema?: Record<string, string>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type PluginRuntime =
|
|
42
|
+
| { type: "none" }
|
|
43
|
+
| { type: "python"; entry: string }
|
|
44
|
+
| {
|
|
45
|
+
type: "ros2";
|
|
46
|
+
package: string;
|
|
47
|
+
launch: string;
|
|
48
|
+
rosClient: "rclpy" | "rclcpp";
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export interface PluginManifest {
|
|
52
|
+
/** Reverse-DNS unique id, e.g. "com.rainbow.gripper-tool". */
|
|
53
|
+
id: string;
|
|
54
|
+
/** Human-readable name. */
|
|
55
|
+
name: string;
|
|
56
|
+
/** Plugin version (semver). */
|
|
57
|
+
version: string;
|
|
58
|
+
/** Minimum host (Muscat) version this plugin supports. */
|
|
59
|
+
minAppVersion?: string;
|
|
60
|
+
/** Icon path relative to plugin root. */
|
|
61
|
+
icon?: string;
|
|
62
|
+
/** UI / chrome contributions. */
|
|
63
|
+
contributes?: PluginContribution[];
|
|
64
|
+
/** Host actions the plugin can invoke. */
|
|
65
|
+
actions?: PluginAction[];
|
|
66
|
+
/** Back-end runtime declaration. */
|
|
67
|
+
runtime?: PluginRuntime;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Lightweight structural check. Returns the list of problems (empty = valid).
|
|
72
|
+
* Kept dependency-free so both front and back ends can reuse it; richer
|
|
73
|
+
* validation (zod) can layer on top later without breaking this contract.
|
|
74
|
+
*/
|
|
75
|
+
export function validateManifest(value: unknown): string[] {
|
|
76
|
+
const errors: string[] = [];
|
|
77
|
+
if (typeof value !== "object" || value === null) {
|
|
78
|
+
return ["manifest must be an object"];
|
|
79
|
+
}
|
|
80
|
+
const m = value as Record<string, unknown>;
|
|
81
|
+
if (typeof m.id !== "string" || m.id.length === 0)
|
|
82
|
+
errors.push("manifest.id is required (string)");
|
|
83
|
+
if (typeof m.name !== "string" || m.name.length === 0)
|
|
84
|
+
errors.push("manifest.name is required (string)");
|
|
85
|
+
if (typeof m.version !== "string" || m.version.length === 0)
|
|
86
|
+
errors.push("manifest.version is required (string)");
|
|
87
|
+
if (m.contributes !== undefined && !Array.isArray(m.contributes))
|
|
88
|
+
errors.push("manifest.contributes must be an array");
|
|
89
|
+
if (m.actions !== undefined && !Array.isArray(m.actions))
|
|
90
|
+
errors.push("manifest.actions must be an array");
|
|
91
|
+
return errors;
|
|
92
|
+
}
|