agentic-zi-ui 0.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.
- package/CHANGELOG.md +7 -0
- package/RELEASE.md +10 -0
- package/dist/UiRenderer.d.ts +16 -0
- package/dist/UiRenderer.d.ts.map +1 -0
- package/dist/UiRenderer.js +384 -0
- package/dist/actions.d.ts +9 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +84 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/package.json +33 -0
package/CHANGELOG.md
ADDED
package/RELEASE.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Release checklist (agentic-zi-ui)
|
|
2
|
+
|
|
3
|
+
1) Update `CHANGELOG.md`
|
|
4
|
+
2) Bump version
|
|
5
|
+
- `pnpm -C packages/ui-renderer release:patch`
|
|
6
|
+
- or `release:minor` / `release:major`
|
|
7
|
+
3) Build
|
|
8
|
+
- `pnpm -C packages/ui-renderer build`
|
|
9
|
+
4) Publish
|
|
10
|
+
- `pnpm -C packages/ui-renderer publish:public`
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { UiNode, UiTemplate, UiActionType, WidgetDefinition } from "@acme/ui-schema";
|
|
2
|
+
export type RenderPolicy = {
|
|
3
|
+
allowedTags?: string[];
|
|
4
|
+
allowedProps?: string[];
|
|
5
|
+
allowedActions?: UiActionType[];
|
|
6
|
+
};
|
|
7
|
+
export type UiRendererProps = {
|
|
8
|
+
template: UiTemplate;
|
|
9
|
+
data?: Record<string, unknown>;
|
|
10
|
+
onFlowEvent: (name: string, payload?: Record<string, unknown>) => void;
|
|
11
|
+
resolveWidget?: (widgetId: string) => Promise<WidgetDefinition>;
|
|
12
|
+
slots?: Record<string, Array<UiNode | string>>;
|
|
13
|
+
policy?: RenderPolicy;
|
|
14
|
+
};
|
|
15
|
+
export declare function UiRenderer({ template, data, onFlowEvent, resolveWidget, slots, policy, }: UiRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
//# sourceMappingURL=UiRenderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UiRenderer.d.ts","sourceRoot":"","sources":["../src/UiRenderer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,MAAM,EACN,UAAU,EAEV,YAAY,EACZ,gBAAgB,EACjB,MAAM,iBAAiB,CAAC;AAczB,MAAM,MAAM,YAAY,GAAG;IACzB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,UAAU,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACvE,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;IAC/C,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB,CAAC;AAkQF,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,IAAI,EACJ,WAAW,EACX,aAAa,EACb,KAAK,EACL,MAAM,GACP,EAAE,eAAe,2CAkPjB"}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
import React, { useMemo, useReducer, useEffect, useState } from "react";
|
|
4
|
+
import { UI_ALLOWED_TAGS, UI_ALLOWED_PROPS, UiActionTypeSchema, } from "@acme/ui-schema";
|
|
5
|
+
import { runAction } from "./actions";
|
|
6
|
+
const BASE_ALLOWED_TAGS = new Set(UI_ALLOWED_TAGS);
|
|
7
|
+
const BASE_ALLOWED_PROPS = new Set(UI_ALLOWED_PROPS);
|
|
8
|
+
const BASE_ALLOWED_ACTIONS = new Set(UiActionTypeSchema.options);
|
|
9
|
+
function reducer(state, action) {
|
|
10
|
+
switch (action.type) {
|
|
11
|
+
case "patch":
|
|
12
|
+
return { ...state, [action.key]: action.value };
|
|
13
|
+
default:
|
|
14
|
+
return state;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function resolvePath(data, keyPath) {
|
|
18
|
+
const parts = keyPath.split(".").map((p) => p.trim()).filter(Boolean);
|
|
19
|
+
let current = data;
|
|
20
|
+
for (const part of parts) {
|
|
21
|
+
if (current == null)
|
|
22
|
+
return undefined;
|
|
23
|
+
current = current[part];
|
|
24
|
+
}
|
|
25
|
+
return current;
|
|
26
|
+
}
|
|
27
|
+
function interpolateValue(s, data) {
|
|
28
|
+
const fullMatch = s.match(/^\s*\{\{([^}]+)\}\}\s*$/);
|
|
29
|
+
if (fullMatch) {
|
|
30
|
+
const keyPath = fullMatch[1].trim();
|
|
31
|
+
const v = resolvePath(data, keyPath);
|
|
32
|
+
return v === undefined || v === null ? "" : v;
|
|
33
|
+
}
|
|
34
|
+
return s.replace(/\{\{([^}]+)\}\}/g, (_m, rawKey) => {
|
|
35
|
+
const keyPath = String(rawKey).trim();
|
|
36
|
+
const v = resolvePath(data, keyPath);
|
|
37
|
+
return v === undefined || v === null ? "" : String(v);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function interpolateString(s, data) {
|
|
41
|
+
const v = interpolateValue(s, data);
|
|
42
|
+
return typeof v === "string" ? v : v === undefined || v === null ? "" : String(v);
|
|
43
|
+
}
|
|
44
|
+
function deepInterpolate(value, data) {
|
|
45
|
+
if (typeof value === "string")
|
|
46
|
+
return interpolateValue(value, data);
|
|
47
|
+
if (Array.isArray(value))
|
|
48
|
+
return value.map((v) => deepInterpolate(v, data));
|
|
49
|
+
if (value && typeof value === "object") {
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const [k, v] of Object.entries(value)) {
|
|
52
|
+
out[k] = deepInterpolate(v, data);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
function isSafeHttpUrl(url) {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = new URL(url);
|
|
61
|
+
return parsed.protocol === "https:" || parsed.protocol === "http:";
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function isSafeImageUrl(url) {
|
|
68
|
+
if (url.startsWith("data:image/"))
|
|
69
|
+
return true;
|
|
70
|
+
return isSafeHttpUrl(url);
|
|
71
|
+
}
|
|
72
|
+
function pickSafeProps(raw) {
|
|
73
|
+
if (!raw || typeof raw !== "object")
|
|
74
|
+
return {};
|
|
75
|
+
// Only allow a small safe subset
|
|
76
|
+
const out = {};
|
|
77
|
+
for (const k of Object.keys(raw)) {
|
|
78
|
+
if (BASE_ALLOWED_PROPS.has(k) ||
|
|
79
|
+
k.startsWith("aria-") ||
|
|
80
|
+
k.startsWith("data-")) {
|
|
81
|
+
out[k] = raw[k];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const key of ["disabled", "checked", "aria-disabled", "aria-hidden"]) {
|
|
85
|
+
const value = out[key];
|
|
86
|
+
if (typeof value === "string") {
|
|
87
|
+
const normalized = value.trim().toLowerCase();
|
|
88
|
+
if (normalized === "true")
|
|
89
|
+
out[key] = true;
|
|
90
|
+
if (normalized === "false")
|
|
91
|
+
out[key] = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
function intersect(left, right) {
|
|
97
|
+
if (!right || right.length === 0)
|
|
98
|
+
return new Set(left);
|
|
99
|
+
const rightSet = new Set(right);
|
|
100
|
+
const out = new Set();
|
|
101
|
+
for (const item of left) {
|
|
102
|
+
if (rightSet.has(item))
|
|
103
|
+
out.add(item);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
function mergePolicies(base, next) {
|
|
108
|
+
if (!base && !next)
|
|
109
|
+
return undefined;
|
|
110
|
+
return {
|
|
111
|
+
allowedTags: base?.allowedTags && next?.allowedTags
|
|
112
|
+
? base.allowedTags.filter((t) => next.allowedTags?.includes(t))
|
|
113
|
+
: base?.allowedTags ?? next?.allowedTags,
|
|
114
|
+
allowedProps: base?.allowedProps && next?.allowedProps
|
|
115
|
+
? base.allowedProps.filter((p) => next.allowedProps?.includes(p))
|
|
116
|
+
: base?.allowedProps ?? next?.allowedProps,
|
|
117
|
+
allowedActions: base?.allowedActions && next?.allowedActions
|
|
118
|
+
? base.allowedActions.filter((a) => next.allowedActions?.includes(a))
|
|
119
|
+
: base?.allowedActions ?? next?.allowedActions,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function normalizeWidgetProps(widget, raw) {
|
|
123
|
+
if (!widget.props) {
|
|
124
|
+
return { props: raw };
|
|
125
|
+
}
|
|
126
|
+
const props = {};
|
|
127
|
+
for (const [key, def] of Object.entries(widget.props)) {
|
|
128
|
+
if (!(key in raw)) {
|
|
129
|
+
if (def.default !== undefined) {
|
|
130
|
+
props[key] = def.default;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (def.required) {
|
|
134
|
+
return { props: {}, error: `Missing required prop: ${key}` };
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const value = raw[key];
|
|
139
|
+
if (def.type === "string" && typeof value !== "string") {
|
|
140
|
+
return { props: {}, error: `Invalid prop type for ${key}` };
|
|
141
|
+
}
|
|
142
|
+
if (def.type === "number" && typeof value !== "number") {
|
|
143
|
+
return { props: {}, error: `Invalid prop type for ${key}` };
|
|
144
|
+
}
|
|
145
|
+
if (def.type === "boolean" && typeof value !== "boolean") {
|
|
146
|
+
return { props: {}, error: `Invalid prop type for ${key}` };
|
|
147
|
+
}
|
|
148
|
+
if (def.type === "json") {
|
|
149
|
+
props[key] = value;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
props[key] = value;
|
|
153
|
+
}
|
|
154
|
+
return { props };
|
|
155
|
+
}
|
|
156
|
+
function normalizeWidgetSlots(widget, slots) {
|
|
157
|
+
const provided = slots ?? {};
|
|
158
|
+
if (widget.slots) {
|
|
159
|
+
for (const [name, cfg] of Object.entries(widget.slots)) {
|
|
160
|
+
if (cfg.required && (!provided[name] || provided[name].length === 0)) {
|
|
161
|
+
return { slots: {}, error: `Missing required slot: ${name}` };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { slots: provided };
|
|
166
|
+
}
|
|
167
|
+
function WidgetNode(props) {
|
|
168
|
+
const { node, resolveWidget } = props;
|
|
169
|
+
const [widget, setWidget] = useState(null);
|
|
170
|
+
const [error, setError] = useState(null);
|
|
171
|
+
const widgetId = typeof node.widgetId === "string" ? node.widgetId : "";
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (!widgetId) {
|
|
174
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
175
|
+
setError("Missing widgetId.");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!resolveWidget) {
|
|
179
|
+
setError("Widget resolver not configured.");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
let cancelled = false;
|
|
183
|
+
setWidget(null);
|
|
184
|
+
setError(null);
|
|
185
|
+
resolveWidget(widgetId)
|
|
186
|
+
.then((def) => {
|
|
187
|
+
if (!cancelled)
|
|
188
|
+
setWidget(def);
|
|
189
|
+
})
|
|
190
|
+
.catch((e) => {
|
|
191
|
+
if (!cancelled)
|
|
192
|
+
setError(e?.message ?? String(e));
|
|
193
|
+
});
|
|
194
|
+
return () => {
|
|
195
|
+
cancelled = true;
|
|
196
|
+
};
|
|
197
|
+
}, [widgetId, resolveWidget]);
|
|
198
|
+
if (error) {
|
|
199
|
+
return _jsxs("div", { className: "text-xs text-red-600", children: ["Widget error: ", error] });
|
|
200
|
+
}
|
|
201
|
+
if (!widget) {
|
|
202
|
+
return _jsx("div", { className: "text-xs text-slate-500", children: "Loading widget\u2026" });
|
|
203
|
+
}
|
|
204
|
+
const { props: widgetProps, error: propError } = normalizeWidgetProps(widget, node.widgetProps ?? {});
|
|
205
|
+
if (propError) {
|
|
206
|
+
return _jsx("div", { className: "text-xs text-red-600", children: propError });
|
|
207
|
+
}
|
|
208
|
+
const { slots, error: slotError } = normalizeWidgetSlots(widget, node.slots);
|
|
209
|
+
if (slotError) {
|
|
210
|
+
return _jsx("div", { className: "text-xs text-red-600", children: slotError });
|
|
211
|
+
}
|
|
212
|
+
const mergedData = { ...props.data, ...widgetProps };
|
|
213
|
+
const mergedPolicy = mergePolicies(props.policy, widget.policy);
|
|
214
|
+
return (_jsx(UiRenderer, { template: widget.template, data: mergedData, onFlowEvent: props.onFlowEvent, resolveWidget: props.resolveWidget, slots: slots, policy: mergedPolicy }));
|
|
215
|
+
}
|
|
216
|
+
export function UiRenderer({ template, data, onFlowEvent, resolveWidget, slots, policy, }) {
|
|
217
|
+
const [state, dispatch] = useReducer(reducer, {});
|
|
218
|
+
const mergedData = useMemo(() => ({ ...(data ?? {}), state }), [data, state]);
|
|
219
|
+
const allowedTags = useMemo(() => intersect(BASE_ALLOWED_TAGS, policy?.allowedTags), [policy?.allowedTags]);
|
|
220
|
+
const allowedProps = useMemo(() => intersect(BASE_ALLOWED_PROPS, policy?.allowedProps), [policy?.allowedProps]);
|
|
221
|
+
const allowedActions = useMemo(() => intersect(BASE_ALLOWED_ACTIONS, policy?.allowedActions), [policy?.allowedActions]);
|
|
222
|
+
const ctx = useMemo(() => ({
|
|
223
|
+
sendFlowEvent: (name, payload) => {
|
|
224
|
+
const interpolatedPayload = deepInterpolate(payload ?? {}, mergedData);
|
|
225
|
+
onFlowEvent(name, interpolatedPayload);
|
|
226
|
+
},
|
|
227
|
+
patchState: (key, value) => dispatch({ type: "patch", key, value }),
|
|
228
|
+
getState: (key) => state[key],
|
|
229
|
+
toast: (message) => {
|
|
230
|
+
// POC: swap with your notification system
|
|
231
|
+
alert(message);
|
|
232
|
+
},
|
|
233
|
+
}), [mergedData, onFlowEvent, state]);
|
|
234
|
+
function attachEvent(action, handler) {
|
|
235
|
+
if (!action)
|
|
236
|
+
return undefined;
|
|
237
|
+
return handler;
|
|
238
|
+
}
|
|
239
|
+
function renderNode(node, idxPath) {
|
|
240
|
+
const tag = node.type;
|
|
241
|
+
if (tag === "slot") {
|
|
242
|
+
const slotNodes = slots?.[node.slotName] ?? [];
|
|
243
|
+
if (slotNodes.length === 0)
|
|
244
|
+
return null;
|
|
245
|
+
return (_jsx(React.Fragment, { children: slotNodes.map((c, i) => {
|
|
246
|
+
const childPath = `${idxPath}.slot.${i}`;
|
|
247
|
+
if (typeof c === "string")
|
|
248
|
+
return interpolateString(c, mergedData);
|
|
249
|
+
return renderNode(c, childPath);
|
|
250
|
+
}) }, idxPath));
|
|
251
|
+
}
|
|
252
|
+
if (tag === "widget") {
|
|
253
|
+
return (_jsx(WidgetNode, { node: node, data: mergedData, onFlowEvent: (name, payload) => ctx.sendFlowEvent(name, payload), resolveWidget: resolveWidget, policy: policy }, idxPath));
|
|
254
|
+
}
|
|
255
|
+
if (!allowedTags.has(tag)) {
|
|
256
|
+
return (_jsxs("div", { className: "text-xs text-red-600", children: ["Blocked tag: ", _jsx("span", { className: "font-mono", children: tag })] }, idxPath));
|
|
257
|
+
}
|
|
258
|
+
const className = node.className
|
|
259
|
+
? interpolateString(node.className, mergedData)
|
|
260
|
+
: undefined;
|
|
261
|
+
const rawProps = node.props
|
|
262
|
+
? deepInterpolate(node.props, mergedData)
|
|
263
|
+
: undefined;
|
|
264
|
+
const safeProps = pickSafeProps(rawProps);
|
|
265
|
+
for (const k of Object.keys(safeProps)) {
|
|
266
|
+
if (!allowedProps.has(k))
|
|
267
|
+
delete safeProps[k];
|
|
268
|
+
}
|
|
269
|
+
// Bindings (POC)
|
|
270
|
+
if (tag === "input") {
|
|
271
|
+
if (node.bind?.valueKey) {
|
|
272
|
+
safeProps.value = (ctx.getState(node.bind.valueKey) ?? "");
|
|
273
|
+
}
|
|
274
|
+
if (node.bind?.checkedKey) {
|
|
275
|
+
safeProps.checked = !!ctx.getState(node.bind.checkedKey);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Events
|
|
279
|
+
const onClick = attachEvent(node.events?.onClick, async () => {
|
|
280
|
+
if (node.events?.onClick)
|
|
281
|
+
await runAction(node.events.onClick, ctx, undefined, allowedActions);
|
|
282
|
+
});
|
|
283
|
+
const onChange = attachEvent(node.events?.onChange, async (ev) => {
|
|
284
|
+
if (node.events?.onChange)
|
|
285
|
+
await runAction(node.events.onChange, ctx, ev, allowedActions);
|
|
286
|
+
});
|
|
287
|
+
const onSubmit = attachEvent(node.events?.onSubmit, async (ev) => {
|
|
288
|
+
ev?.preventDefault?.();
|
|
289
|
+
if (node.events?.onSubmit)
|
|
290
|
+
await runAction(node.events.onSubmit, ctx, ev, allowedActions);
|
|
291
|
+
});
|
|
292
|
+
const onMouseEnter = attachEvent(node.events?.onMouseEnter, async (ev) => {
|
|
293
|
+
if (node.events?.onMouseEnter)
|
|
294
|
+
await runAction(node.events.onMouseEnter, ctx, ev, allowedActions);
|
|
295
|
+
});
|
|
296
|
+
const onMouseLeave = attachEvent(node.events?.onMouseLeave, async (ev) => {
|
|
297
|
+
if (node.events?.onMouseLeave)
|
|
298
|
+
await runAction(node.events.onMouseLeave, ctx, ev, allowedActions);
|
|
299
|
+
});
|
|
300
|
+
const onMouseOver = attachEvent(node.events?.onMouseOver, async (ev) => {
|
|
301
|
+
if (node.events?.onMouseOver)
|
|
302
|
+
await runAction(node.events.onMouseOver, ctx, ev, allowedActions);
|
|
303
|
+
});
|
|
304
|
+
const onMouseOut = attachEvent(node.events?.onMouseOut, async (ev) => {
|
|
305
|
+
if (node.events?.onMouseOut)
|
|
306
|
+
await runAction(node.events.onMouseOut, ctx, ev, allowedActions);
|
|
307
|
+
});
|
|
308
|
+
const onMouseDown = attachEvent(node.events?.onMouseDown, async (ev) => {
|
|
309
|
+
if (node.events?.onMouseDown)
|
|
310
|
+
await runAction(node.events.onMouseDown, ctx, ev, allowedActions);
|
|
311
|
+
});
|
|
312
|
+
const onMouseUp = attachEvent(node.events?.onMouseUp, async (ev) => {
|
|
313
|
+
if (node.events?.onMouseUp)
|
|
314
|
+
await runAction(node.events.onMouseUp, ctx, ev, allowedActions);
|
|
315
|
+
});
|
|
316
|
+
const onMouseMove = attachEvent(node.events?.onMouseMove, async (ev) => {
|
|
317
|
+
if (node.events?.onMouseMove)
|
|
318
|
+
await runAction(node.events.onMouseMove, ctx, ev, allowedActions);
|
|
319
|
+
});
|
|
320
|
+
const onFocus = attachEvent(node.events?.onFocus, async (ev) => {
|
|
321
|
+
if (node.events?.onFocus)
|
|
322
|
+
await runAction(node.events.onFocus, ctx, ev, allowedActions);
|
|
323
|
+
});
|
|
324
|
+
const onBlur = attachEvent(node.events?.onBlur, async (ev) => {
|
|
325
|
+
if (node.events?.onBlur)
|
|
326
|
+
await runAction(node.events.onBlur, ctx, ev, allowedActions);
|
|
327
|
+
});
|
|
328
|
+
// Children
|
|
329
|
+
const children = (node.children ?? []).map((c, i) => {
|
|
330
|
+
const childPath = `${idxPath}.${i}`;
|
|
331
|
+
if (typeof c === "string")
|
|
332
|
+
return interpolateString(c, mergedData);
|
|
333
|
+
return renderNode(c, childPath);
|
|
334
|
+
});
|
|
335
|
+
if (tag === "a") {
|
|
336
|
+
if (typeof safeProps.href === "string") {
|
|
337
|
+
if (!isSafeHttpUrl(safeProps.href)) {
|
|
338
|
+
delete safeProps.href;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
delete safeProps.href;
|
|
343
|
+
}
|
|
344
|
+
if (safeProps.target !== "_blank" && safeProps.target !== "_self") {
|
|
345
|
+
delete safeProps.target;
|
|
346
|
+
}
|
|
347
|
+
if (safeProps.target === "_blank") {
|
|
348
|
+
safeProps.rel = "noopener noreferrer";
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (tag === "img") {
|
|
352
|
+
if (typeof safeProps.src === "string") {
|
|
353
|
+
if (!isSafeImageUrl(safeProps.src)) {
|
|
354
|
+
delete safeProps.src;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
delete safeProps.src;
|
|
359
|
+
}
|
|
360
|
+
if (typeof safeProps.alt !== "string") {
|
|
361
|
+
safeProps.alt = "";
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const props = {
|
|
365
|
+
...safeProps,
|
|
366
|
+
className,
|
|
367
|
+
onClick,
|
|
368
|
+
onChange,
|
|
369
|
+
onSubmit,
|
|
370
|
+
onMouseEnter,
|
|
371
|
+
onMouseLeave,
|
|
372
|
+
onMouseOver,
|
|
373
|
+
onMouseOut,
|
|
374
|
+
onMouseDown,
|
|
375
|
+
onMouseUp,
|
|
376
|
+
onMouseMove,
|
|
377
|
+
onFocus,
|
|
378
|
+
onBlur,
|
|
379
|
+
key: idxPath,
|
|
380
|
+
};
|
|
381
|
+
return React.createElement(tag, props, ...children);
|
|
382
|
+
}
|
|
383
|
+
return _jsx(_Fragment, { children: renderNode(template.root, "0") });
|
|
384
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { UiAction, UiActionType } from "@acme/ui-schema";
|
|
2
|
+
export type ActionContext = {
|
|
3
|
+
sendFlowEvent: (name: string, payload?: Record<string, unknown>) => void;
|
|
4
|
+
patchState: (key: string, value: unknown) => void;
|
|
5
|
+
getState: (key: string) => unknown;
|
|
6
|
+
toast: (message: string) => void;
|
|
7
|
+
};
|
|
8
|
+
export declare function runAction(action: UiAction, ctx: ActionContext, ev?: unknown, allowedActions?: Set<UiActionType>): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE9D,MAAM,MAAM,aAAa,GAAG;IAC1B,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACzE,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACnC,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,CAAC;AAWF,wBAAsB,SAAS,CAC7B,MAAM,EAAE,QAAQ,EAChB,GAAG,EAAE,aAAa,EAClB,EAAE,CAAC,EAAE,OAAO,EACZ,cAAc,CAAC,EAAE,GAAG,CAAC,YAAY,CAAC,GACjC,OAAO,CAAC,IAAI,CAAC,CAuDf"}
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
function isSafeHttpUrl(url) {
|
|
2
|
+
try {
|
|
3
|
+
const u = new URL(url);
|
|
4
|
+
return u.protocol === "https:" || u.protocol === "http:";
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export async function runAction(action, ctx, ev, allowedActions) {
|
|
11
|
+
if (allowedActions && !allowedActions.has(action.type)) {
|
|
12
|
+
ctx.toast("Blocked action");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
switch (action.type) {
|
|
16
|
+
case "FLOW_EVENT": {
|
|
17
|
+
const payload = substituteEventValues(action.payload ?? {}, ev);
|
|
18
|
+
ctx.sendFlowEvent(action.name, payload);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
case "OPEN_URL": {
|
|
22
|
+
if (!isSafeHttpUrl(action.url)) {
|
|
23
|
+
ctx.toast("Blocked unsafe URL");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const target = action.target ?? "_blank";
|
|
27
|
+
window.open(action.url, target, "noopener,noreferrer");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
case "COPY": {
|
|
31
|
+
await navigator.clipboard.writeText(action.text);
|
|
32
|
+
ctx.toast("Copied");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
case "TOAST": {
|
|
36
|
+
ctx.toast(action.message);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
case "PATCH_STATE": {
|
|
40
|
+
if (action.value !== undefined) {
|
|
41
|
+
ctx.patchState(action.key, action.value);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// valueFrom reads from event
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
|
+
const e = ev;
|
|
47
|
+
if (action.valueFrom === "event.target.value") {
|
|
48
|
+
ctx.patchState(action.key, e?.target?.value);
|
|
49
|
+
}
|
|
50
|
+
else if (action.valueFrom === "event.target.checked") {
|
|
51
|
+
ctx.patchState(action.key, !!e?.target?.checked);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
ctx.patchState(action.key, null);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
default:
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function substituteEventValues(payload, ev) {
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
const e = ev;
|
|
65
|
+
const value = e?.target?.value;
|
|
66
|
+
const checked = e?.target?.checked;
|
|
67
|
+
function walk(input) {
|
|
68
|
+
if (input === "$event.target.value")
|
|
69
|
+
return value ?? null;
|
|
70
|
+
if (input === "$event.target.checked")
|
|
71
|
+
return !!checked;
|
|
72
|
+
if (Array.isArray(input))
|
|
73
|
+
return input.map(walk);
|
|
74
|
+
if (input && typeof input === "object") {
|
|
75
|
+
const out = {};
|
|
76
|
+
for (const [k, v] of Object.entries(input)) {
|
|
77
|
+
out[k] = walk(v);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
return input;
|
|
82
|
+
}
|
|
83
|
+
return walk(payload);
|
|
84
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { UiRendererProps } from "./UiRenderer";
|
|
2
|
+
export type { UiRendererProps, RenderPolicy } from "./UiRenderer";
|
|
3
|
+
export type { UiRendererProps as ZijusCustomWidgetProps } from "./UiRenderer";
|
|
4
|
+
export declare function ZijusCustomWidget(props: UiRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAElE,YAAY,EAAE,eAAe,IAAI,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAE9E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,eAAe,2CAEvD"}
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentic-zi-ui",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"private": false,
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "tsc -w",
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"prepublishOnly": "pnpm -C . build",
|
|
18
|
+
"release:patch": "npm version patch -m \"chore(release): %s\"",
|
|
19
|
+
"release:minor": "npm version minor -m \"chore(release): %s\"",
|
|
20
|
+
"release:major": "npm version major -m \"chore(release): %s\"",
|
|
21
|
+
"publish:public": "npm publish --access public"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@acme/ui-schema": "workspace:*"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"react": "^18 || ^19"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/react": "^19.2.5",
|
|
31
|
+
"typescript": "~5.9.3"
|
|
32
|
+
}
|
|
33
|
+
}
|