@savvifi/meridian-web-react 0.1.0 → 0.2.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/package.json +7 -5
- package/src/component_kit.d.ts +15 -1
- package/src/html_kit.js +3 -0
- package/src/index.d.ts +3 -1
- package/src/index.js +2 -0
- package/src/panel_renderer.js +3 -0
- package/src/shadcn_kit.d.ts +2 -0
- package/src/shadcn_kit.js +38 -0
- package/src/view_renderer.d.ts +6 -0
- package/src/view_renderer.js +71 -0
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@savvifi/meridian-web-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "React adapter for the meridian WebRenderer seam: a kit-agnostic core (MeridianProvider, PanelRenderer, ComponentKit, reactWebRenderer) that renders meridian.ui.v1 PanelDescriptors through a swappable ComponentKit (MUI, shadcn,
|
|
5
|
+
"description": "React adapter for the meridian WebRenderer seam: a kit-agnostic core (MeridianProvider, PanelRenderer, ComponentKit, reactWebRenderer) that renders meridian.ui.v1 PanelDescriptors through a swappable ComponentKit (MUI, shadcn, \u2026). The premium web renderer tier of the meridian renderer family.",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"src/**/*.d.ts"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@savvifi/meridian-proto-ts": "^0.
|
|
27
|
-
"@savvifi/meridian-schemas": "^0.
|
|
26
|
+
"@savvifi/meridian-proto-ts": "^0.2.0",
|
|
27
|
+
"@savvifi/meridian-schemas": "^0.2.0",
|
|
28
28
|
"@bufbuild/protobuf": "2.12.1"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
"vitest": "2.1.4"
|
|
41
41
|
},
|
|
42
42
|
"pnpm": {
|
|
43
|
-
"onlyBuiltDependencies": [
|
|
43
|
+
"onlyBuiltDependencies": [
|
|
44
|
+
"esbuild"
|
|
45
|
+
]
|
|
44
46
|
}
|
|
45
47
|
}
|
package/src/component_kit.d.ts
CHANGED
|
@@ -2,10 +2,11 @@ import type { ComponentType, CSSProperties, ReactNode } from "react";
|
|
|
2
2
|
import type { GalleryPanel } from "@savvifi/meridian-proto-ts/proto/gallery_pb.js";
|
|
3
3
|
import type { LlmPromptPanel } from "@savvifi/meridian-proto-ts/proto/llm_prompt_pb.js";
|
|
4
4
|
import type { LroPanel } from "@savvifi/meridian-proto-ts/proto/lro_pb.js";
|
|
5
|
-
import type { PanelDescriptor } from "@savvifi/meridian-proto-ts/proto/panel_pb.js";
|
|
5
|
+
import type { FormPanel, PanelDescriptor } from "@savvifi/meridian-proto-ts/proto/panel_pb.js";
|
|
6
6
|
import type { PromptPanel } from "@savvifi/meridian-proto-ts/proto/prompt_pb.js";
|
|
7
7
|
import type { TablePanel } from "@savvifi/meridian-proto-ts/proto/table_pb.js";
|
|
8
8
|
import type { Theme } from "@savvifi/meridian-proto-ts/proto/theme_pb.js";
|
|
9
|
+
import type { Action } from "@savvifi/meridian-proto-ts/proto/view_pb.js";
|
|
9
10
|
import type { RpcInvoker } from "@savvifi/meridian-schemas/uiview";
|
|
10
11
|
/** Props every shape component receives: its panel, the parent descriptor, transport. */
|
|
11
12
|
export interface ShapeProps<P> {
|
|
@@ -18,6 +19,12 @@ export type PromptPanelProps = ShapeProps<PromptPanel>;
|
|
|
18
19
|
export type LroPanelProps = ShapeProps<LroPanel>;
|
|
19
20
|
export type GalleryPanelProps = ShapeProps<GalleryPanel>;
|
|
20
21
|
export type LlmPromptPanelProps = ShapeProps<LlmPromptPanel>;
|
|
22
|
+
export type FormPanelProps = ShapeProps<FormPanel>;
|
|
23
|
+
/** Props for a kit's action bar: the actions to render + transport to fire them. */
|
|
24
|
+
export interface ActionBarProps {
|
|
25
|
+
actions: Action[];
|
|
26
|
+
invoker: RpcInvoker;
|
|
27
|
+
}
|
|
21
28
|
/** A set of components + a theme binding that paints meridian panels. */
|
|
22
29
|
export interface ComponentKit {
|
|
23
30
|
/** Stable id, e.g. "mui" / "shadcn" / "html". Used in the renderer id. */
|
|
@@ -36,6 +43,8 @@ export interface ComponentKit {
|
|
|
36
43
|
Table: ComponentType<TablePanelProps>;
|
|
37
44
|
Prompt: ComponentType<PromptPanelProps>;
|
|
38
45
|
Lro: ComponentType<LroPanelProps>;
|
|
46
|
+
/** Entity "detail section" / CRUD form (FormPanel: READONLY | EDIT). */
|
|
47
|
+
Form: ComponentType<FormPanelProps>;
|
|
39
48
|
/** Optional richer shapes; PanelRenderer falls back when a kit omits them. */
|
|
40
49
|
Gallery?: ComponentType<GalleryPanelProps>;
|
|
41
50
|
LlmPrompt?: ComponentType<LlmPromptPanelProps>;
|
|
@@ -43,4 +52,9 @@ export interface ComponentKit {
|
|
|
43
52
|
Fallback: ComponentType<{
|
|
44
53
|
descriptor: PanelDescriptor;
|
|
45
54
|
}>;
|
|
55
|
+
/**
|
|
56
|
+
* Renders a view/slot's actions (ViewDescriptor.actions / Slot.actions).
|
|
57
|
+
* Optional — ViewRenderer falls back to plain buttons when a kit omits it.
|
|
58
|
+
*/
|
|
59
|
+
ActionBar?: ComponentType<ActionBarProps>;
|
|
46
60
|
}
|
package/src/html_kit.js
CHANGED
|
@@ -24,6 +24,9 @@ export const htmlKit = {
|
|
|
24
24
|
Table: ({ panel }) => (_jsxs("table", { className: "mer-table", children: [_jsx("thead", { children: _jsx("tr", { children: panel.columns.map((col, i) => (_jsx("th", { children: col.header }, i))) }) }), _jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { className: "mer-empty", colSpan: panel.columns.length || 1, children: panel.placeholder || "(load to populate)" }) }) })] })),
|
|
25
25
|
Prompt: ({ panel }) => (_jsx("form", { className: "mer-prompt", children: panel.fields.map((field) => (_jsx("label", { className: "mer-field", children: _jsx("span", { children: field.label }) }, field.fieldId))) })),
|
|
26
26
|
Lro: ({ panel }) => (_jsx("div", { className: "mer-lro", children: _jsx("button", { type: "button", children: panel.runButtonLabel || "Run" }) })),
|
|
27
|
+
Form: ({ panel }) => (
|
|
28
|
+
// FORM_MODE_EDIT = 2; anything else renders read-only.
|
|
29
|
+
_jsx("form", { className: "mer-form", "data-mode": panel.mode, children: panel.fields.map((field) => (_jsxs("label", { className: "mer-field", children: [_jsx("span", { className: "mer-field-label", children: field.label }), panel.mode === 2 ? (_jsx("input", { className: "mer-field-input", name: field.fieldId })) : (_jsx("span", { className: "mer-field-value", "data-field": field.fieldId }))] }, field.fieldId))) })),
|
|
27
30
|
Fallback: ({ descriptor }) => (_jsx("pre", { className: "mer-fallback", children: descriptor.body.case
|
|
28
31
|
? `unsupported panel shape: ${descriptor.body.case}`
|
|
29
32
|
: "(empty panel)" })),
|
package/src/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export type { ComponentKit, ShapeProps, TablePanelProps, PromptPanelProps, LroPanelProps, GalleryPanelProps, LlmPromptPanelProps, } from "./component_kit.js";
|
|
1
|
+
export type { ComponentKit, ShapeProps, TablePanelProps, PromptPanelProps, LroPanelProps, GalleryPanelProps, LlmPromptPanelProps, FormPanelProps, ActionBarProps, } from "./component_kit.js";
|
|
2
2
|
export { MeridianProvider, useMeridian, useMeridianTheme, useRpcInvoker, useComponentKit, useAdhocHandler, type MeridianContextValue, type MeridianProviderProps, type ReactAdhocFactory, } from "./provider.js";
|
|
3
3
|
export { PanelRenderer } from "./panel_renderer.js";
|
|
4
|
+
export { ViewRenderer } from "./view_renderer.js";
|
|
4
5
|
export { reactWebRenderer } from "./react_web_renderer.js";
|
|
5
6
|
export { htmlKit } from "./html_kit.js";
|
|
7
|
+
export { shadcnKit } from "./shadcn_kit.js";
|
package/src/index.js
CHANGED
|
@@ -6,5 +6,7 @@
|
|
|
6
6
|
// can mount, a peer of the web-components / TUI / native renderers.
|
|
7
7
|
export { MeridianProvider, useMeridian, useMeridianTheme, useRpcInvoker, useComponentKit, useAdhocHandler, } from "./provider.js";
|
|
8
8
|
export { PanelRenderer } from "./panel_renderer.js";
|
|
9
|
+
export { ViewRenderer } from "./view_renderer.js";
|
|
9
10
|
export { reactWebRenderer } from "./react_web_renderer.js";
|
|
10
11
|
export { htmlKit } from "./html_kit.js";
|
|
12
|
+
export { shadcnKit } from "./shadcn_kit.js";
|
package/src/panel_renderer.js
CHANGED
|
@@ -14,6 +14,9 @@ export function PanelRenderer({ descriptor, }) {
|
|
|
14
14
|
case "lro":
|
|
15
15
|
inner = (_jsx(kit.Lro, { panel: body.value, descriptor: descriptor, invoker: invoker }));
|
|
16
16
|
break;
|
|
17
|
+
case "form":
|
|
18
|
+
inner = (_jsx(kit.Form, { panel: body.value, descriptor: descriptor, invoker: invoker }));
|
|
19
|
+
break;
|
|
17
20
|
case "gallery":
|
|
18
21
|
inner = kit.Gallery ? (_jsx(kit.Gallery, { panel: body.value, descriptor: descriptor, invoker: invoker })) : (_jsx(kit.Fallback, { descriptor: descriptor }));
|
|
19
22
|
break;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Bind the meridian palette to shadcn/ui's CSS custom properties so shadcn
|
|
3
|
+
// Tailwind classes (bg-card, text-muted-foreground, border, …) paint the skin.
|
|
4
|
+
// (Hex here for the reference; a production shadcn-kit would emit hsl triplets
|
|
5
|
+
// for the `hsl(var(--token))` convention.)
|
|
6
|
+
function themeToStyle(theme) {
|
|
7
|
+
if (!theme)
|
|
8
|
+
return {};
|
|
9
|
+
const pal = theme.dark ?? theme.light;
|
|
10
|
+
if (!pal)
|
|
11
|
+
return {};
|
|
12
|
+
return {
|
|
13
|
+
["--background"]: pal.bg,
|
|
14
|
+
["--card"]: pal.surface,
|
|
15
|
+
["--foreground"]: pal.fg,
|
|
16
|
+
["--muted-foreground"]: pal.muted,
|
|
17
|
+
["--border"]: pal.border,
|
|
18
|
+
["--primary"]: pal.accent,
|
|
19
|
+
["--primary-foreground"]: pal.onAccent,
|
|
20
|
+
["--destructive"]: pal.danger,
|
|
21
|
+
background: "var(--background)",
|
|
22
|
+
color: "var(--foreground)",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export const shadcnKit = {
|
|
26
|
+
id: "shadcn",
|
|
27
|
+
themeToStyle,
|
|
28
|
+
Chrome: ({ descriptor, children }) => (_jsxs("section", { className: "rounded-lg border bg-card text-card-foreground shadow-sm", children: [_jsx("header", { className: "border-b px-4 py-3", children: _jsx("h3", { className: "text-sm font-semibold leading-none tracking-tight", children: descriptor.title || descriptor.panelId }) }), _jsx("div", { className: "p-4", children: children })] })),
|
|
29
|
+
Table: ({ panel }) => (_jsx("div", { className: "relative w-full overflow-auto", children: _jsxs("table", { className: "w-full caption-bottom text-sm", children: [_jsx("thead", { className: "[&_tr]:border-b", children: _jsx("tr", { className: "border-b transition-colors", children: panel.columns.map((col, i) => (_jsx("th", { className: "h-10 px-2 text-left align-middle font-medium text-muted-foreground", children: col.header }, i))) }) }), _jsx("tbody", { className: "[&_tr:last-child]:border-0", children: _jsx("tr", { className: "border-b transition-colors hover:bg-muted/50", children: _jsx("td", { className: "p-2 align-middle text-muted-foreground", colSpan: panel.columns.length || 1, children: panel.placeholder || "(load to populate)" }) }) })] }) })),
|
|
30
|
+
Prompt: ({ panel }) => (_jsx("form", { className: "grid gap-4", children: panel.fields.map((field) => (_jsx("div", { className: "grid gap-2", children: _jsx("label", { className: "text-sm font-medium leading-none", children: field.label }) }, field.fieldId))) })),
|
|
31
|
+
Lro: ({ panel }) => (_jsx("div", { className: "flex items-center gap-2", children: _jsx("button", { type: "button", className: "inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground", children: panel.runButtonLabel || "Run" }) })),
|
|
32
|
+
Form: ({ panel }) => (
|
|
33
|
+
// FORM_MODE_EDIT = 2; anything else renders read-only.
|
|
34
|
+
_jsx("form", { className: "grid gap-4", "data-mode": panel.mode, children: panel.fields.map((field) => (_jsxs("div", { className: "grid gap-2", children: [_jsx("label", { className: "text-sm font-medium leading-none", children: field.label }), panel.mode === 2 ? (_jsx("input", { name: field.fieldId, className: "flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm" })) : (_jsx("span", { className: "text-sm text-muted-foreground", "data-field": field.fieldId }))] }, field.fieldId))) })),
|
|
35
|
+
Fallback: ({ descriptor }) => (_jsx("div", { className: "rounded-md border border-dashed p-4 text-sm text-muted-foreground", children: descriptor.body.case
|
|
36
|
+
? `unsupported panel shape: ${descriptor.body.case}`
|
|
37
|
+
: "(empty panel)" })),
|
|
38
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ViewDescriptor } from "@savvifi/meridian-proto-ts/proto/view_pb.js";
|
|
3
|
+
/** Renders a ViewDescriptor. The layout mode selects the arrangement of slots. */
|
|
4
|
+
export declare function ViewRenderer({ view }: {
|
|
5
|
+
view: ViewDescriptor;
|
|
6
|
+
}): ReactNode;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// ViewRenderer — the composition/layout tier of the React renderer.
|
|
3
|
+
//
|
|
4
|
+
// Renders a meridian.ui.v1 ViewDescriptor: applies the Layout (list / stacked /
|
|
5
|
+
// tabbed / two-column), delegates each Slot's panel to PanelRenderer (so the
|
|
6
|
+
// kit-agnostic panel dispatch is reused), and renders view- and slot-level
|
|
7
|
+
// Actions via the invoker. The layout STRUCTURE is kit-agnostic; the panels and
|
|
8
|
+
// the action affordances come from the active ComponentKit.
|
|
9
|
+
import { useState } from "react";
|
|
10
|
+
import { PanelRenderer } from "./panel_renderer.js";
|
|
11
|
+
import { useMeridian } from "./provider.js";
|
|
12
|
+
// Fire an action's RpcCall. Binding resolution (row/form context → request
|
|
13
|
+
// fields) is a later increment; the first cut fires with an empty request and
|
|
14
|
+
// lets the invoker/backend supply defaults.
|
|
15
|
+
function fireAction(invoker, action) {
|
|
16
|
+
if (action.call) {
|
|
17
|
+
void invoker.invoke(action.call.service, action.call.method, {});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function ActionsView({ actions }) {
|
|
21
|
+
const { kit, invoker } = useMeridian();
|
|
22
|
+
if (!actions || actions.length === 0)
|
|
23
|
+
return null;
|
|
24
|
+
if (kit.ActionBar) {
|
|
25
|
+
return _jsx(kit.ActionBar, { actions: actions, invoker: invoker });
|
|
26
|
+
}
|
|
27
|
+
return (_jsx("div", { className: "mer-actions", children: actions.map((a) => (_jsx("button", { type: "button", onClick: () => fireAction(invoker, a), children: a.label }, a.id))) }));
|
|
28
|
+
}
|
|
29
|
+
// One slot: an optional title, the panel, and any slot (row) actions.
|
|
30
|
+
function SlotView({ slot }) {
|
|
31
|
+
const panel = slot.panel;
|
|
32
|
+
if (!panel)
|
|
33
|
+
return null;
|
|
34
|
+
const title = slot.title || panel.title;
|
|
35
|
+
return (_jsxs("section", { className: "mer-slot", "data-slot": slot.id, "data-role": slot.role, children: [title ? _jsx("h3", { className: "mer-slot-title", children: title }) : null, _jsx(PanelRenderer, { descriptor: panel }), _jsx(ActionsView, { actions: slot.actions })] }));
|
|
36
|
+
}
|
|
37
|
+
function byPosition(a, b) {
|
|
38
|
+
return (a.position || 0) - (b.position || 0);
|
|
39
|
+
}
|
|
40
|
+
/** Renders a ViewDescriptor. The layout mode selects the arrangement of slots. */
|
|
41
|
+
export function ViewRenderer({ view }) {
|
|
42
|
+
const slots = [...view.slots].sort(byPosition);
|
|
43
|
+
const layout = view.layout?.mode;
|
|
44
|
+
let body;
|
|
45
|
+
switch (layout?.case) {
|
|
46
|
+
case "tabbed":
|
|
47
|
+
body = _jsx(TabbedSlots, { slots: slots });
|
|
48
|
+
break;
|
|
49
|
+
case "twoColumn":
|
|
50
|
+
body = _jsx(TwoColumnSlots, { slots: slots });
|
|
51
|
+
break;
|
|
52
|
+
case "list":
|
|
53
|
+
case "stacked":
|
|
54
|
+
default:
|
|
55
|
+
// list + stacked both render slots in order; a list view is just a
|
|
56
|
+
// single-content-slot stack.
|
|
57
|
+
body = (_jsx("div", { className: "mer-stack", children: slots.map((s) => (_jsx(SlotView, { slot: s }, s.id))) }));
|
|
58
|
+
}
|
|
59
|
+
return (_jsxs("div", { className: "mer-view", "data-view": view.id, "data-kind": view.kind, children: [_jsxs("header", { className: "mer-view-header", children: [_jsx("h2", { className: "mer-view-title", children: view.title }), _jsx(ActionsView, { actions: view.actions })] }), body] }));
|
|
60
|
+
}
|
|
61
|
+
function TabbedSlots({ slots }) {
|
|
62
|
+
const [active, setActive] = useState(0);
|
|
63
|
+
const ordered = [...slots].sort((a, b) => (a.placement?.tabPosition || 0) - (b.placement?.tabPosition || 0));
|
|
64
|
+
return (_jsxs("div", { className: "mer-tabs", children: [_jsx("div", { className: "mer-tabstrip", role: "tablist", children: ordered.map((s, i) => (_jsx("button", { type: "button", role: "tab", "aria-selected": i === active, className: i === active ? "mer-tab active" : "mer-tab", onClick: () => setActive(i), children: s.placement?.tabLabel || s.title || s.id }, s.id))) }), ordered[active] ? _jsx(SlotView, { slot: ordered[active] }) : null] }));
|
|
65
|
+
}
|
|
66
|
+
function TwoColumnSlots({ slots }) {
|
|
67
|
+
// Column.COLUMN_SIDEBAR = 2; everything else (main / unspecified) is main.
|
|
68
|
+
const sidebar = slots.filter((s) => s.placement?.column === 2);
|
|
69
|
+
const main = slots.filter((s) => s.placement?.column !== 2);
|
|
70
|
+
return (_jsxs("div", { className: "mer-two-column", children: [_jsx("div", { className: "mer-col-main", children: main.map((s) => (_jsx(SlotView, { slot: s }, s.id))) }), _jsx("aside", { className: "mer-col-sidebar", children: sidebar.map((s) => (_jsx(SlotView, { slot: s }, s.id))) })] }));
|
|
71
|
+
}
|