@webx-ui/schema 0.0.1 → 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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 webx-ui
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 webx-ui
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,18 +1,28 @@
1
1
  # @webx-ui/schema
2
2
 
3
- JSON-driven rendering for WebX UI admin screens. **Work in progress** — at this stage the package
4
- only fixes the contracts (`SchemaNode`, `DataAdapter`, `Paginated`, registries); the renderer
5
- component lands in a later milestone.
3
+ Screens as JSON for WebX UI admin panels: a screen is a tree of nodes, each with a stable `id`
4
+ and a `type` the registry resolves to a Vue component. A module ships the tree, a project lays a
5
+ **patch** over it — add, remove, replace, move, set — and `WxScreenRenderer` draws the result with
6
+ the core components.
6
7
 
7
- ```ts
8
- import type { DataAdapter, SchemaNode } from '@webx-ui/schema'
8
+ ```vue
9
+ <script setup lang="ts">
10
+ import { ref } from 'vue'
11
+ import { WxScreenRenderer } from '@webx-ui/schema'
9
12
 
10
- const page: SchemaNode = {
11
- type: 'card',
12
- props: { title: 'Pages' },
13
- children: [{ type: 'table', props: { resource: 'pages' } }],
14
- }
13
+ const values = ref({ 'general.project-name': 'Acme' })
14
+ const patch = [{ op: 'set', target: 'robots', props: { rows: 16 } }]
15
+ </script>
16
+
17
+ <template>
18
+ <wx-screen-renderer :root="screen.root" :patch="patch" v-model="values" :errors="errors" />
19
+ </template>
15
20
  ```
16
21
 
17
- The package is backend-agnostic: Laravel specifics (`LengthAwarePaginator`, 422 validation errors,
18
- sort/filter query parameters) belong in `@webx-ui/adapter-laravel`.
22
+ The package also exports the pieces on their own — `applyPatch`, `validateScreen`,
23
+ `validatePatch`, `isVisible`, `coreTypes` — and ships `schemas/screen.schema.json` and
24
+ `schemas/patch.schema.json` for editors and for the server half.
25
+
26
+ The guide: https://webx-ui.github.io/webx-ui/guide/screens.html. The design and what comes next
27
+ (the screens endpoint in `module-admin`, `module-settings`) is `docs/architecture/WEBX_UI_SCREENS.md`
28
+ in the repository.
@@ -0,0 +1,43 @@
1
+ import { Patch, PatchError, ScreenModel, ScreenNode, Translate, TypeRegistry, ValidationErrors } from './types';
2
+ type __VLS_Props = {
3
+ /** The tree to draw — a module's screen, already patched by the server. */
4
+ root: ScreenNode[];
5
+ /** Client-side patch applied on top, in order. */
6
+ patch?: Patch;
7
+ /** Server-side validation errors, keyed by field name; shown under the fields. */
8
+ errors?: ValidationErrors;
9
+ /** Project types, merged over the core ones. */
10
+ types?: TypeRegistry;
11
+ /** Turns `trans::` strings into words. Without it the key shows. */
12
+ translate?: Translate;
13
+ /** Decides `can`. Without it every node is allowed. */
14
+ can?: (permission: string) => boolean;
15
+ disabled?: boolean;
16
+ labelPosition?: 'top' | 'left';
17
+ labelWidth?: string;
18
+ size?: 'sm' | 'md' | 'lg';
19
+ };
20
+ type __VLS_PublicProps = {
21
+ modelValue?: ScreenModel;
22
+ } & __VLS_Props;
23
+ declare const _default: import('vue').DefineComponent<__VLS_PublicProps, {
24
+ /** The tree after the patch — what is actually on screen. */
25
+ tree: import('vue').ComputedRef<ScreenNode[]>;
26
+ }, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
27
+ "update:modelValue": (value: ScreenModel) => any;
28
+ patchError: (errors: PatchError[]) => any;
29
+ }, string, import('vue').PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
30
+ "onUpdate:modelValue"?: ((value: ScreenModel) => any) | undefined;
31
+ onPatchError?: ((errors: PatchError[]) => any) | undefined;
32
+ }>, {
33
+ can: (permission: string) => boolean;
34
+ patch: Patch;
35
+ size: "sm" | "md" | "lg";
36
+ disabled: boolean;
37
+ translate: Translate;
38
+ labelWidth: string;
39
+ errors: ValidationErrors;
40
+ types: TypeRegistry;
41
+ labelPosition: "top" | "left";
42
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLFormElement>;
43
+ export default _default;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export type { ActionContext, ActionDescriptor, ActionHandler, ActionRegistry, ComponentRegistry, DataAdapter, ListQuery, Paginated, SchemaNode, ValidationErrors, } from './types';
2
- /**
3
- * Placeholder registry helper. The renderer itself lands in a later milestone —
4
- * for now the package only fixes the contracts so adapters can be written against them.
5
- */
6
- export declare function createComponentRegistry<T extends Record<string, unknown>>(components: T): T;
7
- //# sourceMappingURL=index.d.ts.map
1
+ export type { DataAdapter, ListQuery, NodeKind, Paginated, Patch, PatchError, PatchOperation, PatchPosition, Screen, ScreenError, ScreenModel, ScreenNode, Translate, TypeEntry, TypeRegistry, ValidationErrors, VisibilityCondition, } from './types';
2
+ export { applyPatch, collectIds, findNode } from './patch';
3
+ export { evaluateCondition, isVisible } from './visible';
4
+ export { validatePatch, validateScreen } from './validate';
5
+ export { coreTypes, defineTypes, describeTypes, typesTable, type TypeDescription } from './registry';
6
+ export { translateDeep, words, TRANS_MARKER, type RenderContext } from './render';
7
+ export { default as WxScreenRenderer } from './ScreenRenderer';
package/dist/index.js CHANGED
@@ -1,8 +1,395 @@
1
- /**
2
- * Placeholder registry helper. The renderer itself lands in a later milestone —
3
- * for now the package only fixes the contracts so adapters can be written against them.
4
- */
5
- export function createComponentRegistry(components) {
6
- return { ...components };
7
- }
8
- //# sourceMappingURL=index.js.map
1
+ import { WxAlert as T, WxText as C, WxColorPicker as I, WxDatePicker as V, WxRadioGroup as M, WxCheckbox as q, WxSwitch as D, WxSelect as F, WxInputNumber as B, WxTextarea as K, WxInput as U, WxDivider as J, WxCol as G, WxRow as Y, WxCard as _, WxTab as H, WxTabs as L, WxFormItem as Q, WxForm as X } from "@webx-ui/core";
2
+ import { defineComponent as N, h as d, useModel as Z, computed as p, watch as v, markRaw as ee, toRaw as ne, openBlock as te, createBlock as oe, unref as $, withCtx as ie, createVNode as re, mergeModels as A } from "vue";
3
+ function m(e, t) {
4
+ for (let n = 0; n < e.length; n += 1) {
5
+ const i = e[n];
6
+ if (i.id === t) return { list: e, index: n, node: i };
7
+ if (i.children) {
8
+ const o = m(i.children, t);
9
+ if (o) return o;
10
+ }
11
+ }
12
+ return null;
13
+ }
14
+ function We(e, t) {
15
+ return m(e, t)?.node ?? null;
16
+ }
17
+ function h(e) {
18
+ const t = [], n = (i) => {
19
+ for (const o of i)
20
+ t.push(o.id), o.children && n(o.children);
21
+ };
22
+ return n(e), t;
23
+ }
24
+ function S(e, t) {
25
+ if (!t || t === "last") return e.length;
26
+ if (t === "first") return 0;
27
+ const [n, i] = t.split(":", 2), o = e.findIndex((r) => r.id === i);
28
+ return o === -1 ? `no sibling "${i}" to insert ${n}` : n === "before" ? o : o + 1;
29
+ }
30
+ function f(e) {
31
+ if (Array.isArray(e)) return e.map(f);
32
+ if (e && typeof e == "object") {
33
+ const t = {};
34
+ for (const [n, i] of Object.entries(e)) t[n] = f(i);
35
+ return t;
36
+ }
37
+ return e;
38
+ }
39
+ function se(e, t) {
40
+ const n = f(e), i = [];
41
+ return t.forEach((o, r) => {
42
+ const s = ae(n, o);
43
+ s && i.push({ index: r, op: o, message: s });
44
+ }), { root: n, errors: i };
45
+ }
46
+ function ae(e, t) {
47
+ const n = m(e, t.target);
48
+ if (!n) return `target "${t.target}" not found`;
49
+ switch (t.op) {
50
+ case "add": {
51
+ const i = j(e, t.node);
52
+ if (i) return `id "${i}" already exists in the screen`;
53
+ const o = n.node.children ??= [], r = S(o, t.position);
54
+ return typeof r == "string" ? r : (o.splice(r, 0, f(t.node)), null);
55
+ }
56
+ case "remove":
57
+ return n.list.splice(n.index, 1), null;
58
+ case "replace": {
59
+ const i = e.filter((r) => r !== n.node), o = j(i, t.node, n.node);
60
+ return o ? `id "${o}" already exists in the screen` : (n.list.splice(n.index, 1, f(t.node)), null);
61
+ }
62
+ case "move": {
63
+ let i = n.list;
64
+ if (t.to !== void 0) {
65
+ const r = m(e, t.to);
66
+ if (!r) return `destination "${t.to}" not found`;
67
+ if (r.node === n.node || O(n.node, r.node))
68
+ return `cannot move "${t.target}" into itself`;
69
+ i = r.node.children ??= [];
70
+ }
71
+ n.list.splice(n.index, 1);
72
+ const o = S(i, t.position);
73
+ return typeof o == "string" ? (n.list.splice(n.index, 0, n.node), o) : (i.splice(o, 0, n.node), null);
74
+ }
75
+ case "set": {
76
+ for (const [i, o] of Object.entries(t))
77
+ i === "op" || i === "target" || (i === "props" ? n.node.props = { ...n.node.props, ...o } : n.node[i] = f(o));
78
+ return null;
79
+ }
80
+ }
81
+ }
82
+ function O(e, t) {
83
+ return (e.children ?? []).some((n) => n === t || O(n, t));
84
+ }
85
+ function j(e, t, n) {
86
+ const i = new Set(h(e));
87
+ if (n) for (const o of h([n])) i.delete(o);
88
+ return h([t]).find((o) => i.has(o)) ?? null;
89
+ }
90
+ function b(e, t) {
91
+ return e === t ? !0 : e === null || t === null || typeof e != "object" || typeof t != "object" ? !1 : JSON.stringify(e) === JSON.stringify(t);
92
+ }
93
+ function g(e, t) {
94
+ if ("all" in e) return e.all.every((i) => g(i, t));
95
+ if ("any" in e) return e.any.some((i) => g(i, t));
96
+ const n = t[e.when];
97
+ return "in" in e ? e.in.some((i) => b(i, n)) : "not" in e ? !b(e.not, n) : b(e.is, n);
98
+ }
99
+ function ce(e, t) {
100
+ const { visible: n } = e;
101
+ return n === void 0 || n === !0 ? !0 : n === !1 ? !1 : g(n, t);
102
+ }
103
+ const E = /* @__PURE__ */ new Set([
104
+ "id",
105
+ "type",
106
+ "name",
107
+ "label",
108
+ "help",
109
+ "localized",
110
+ "props",
111
+ "children",
112
+ "slot",
113
+ "visible",
114
+ "can"
115
+ ]), le = /* @__PURE__ */ new Set(["add", "remove", "replace", "move", "set"]);
116
+ function u(e) {
117
+ return typeof e == "object" && e !== null && !Array.isArray(e);
118
+ }
119
+ function R(e, t, n) {
120
+ if (!u(e)) {
121
+ n.push({ path: t, message: "visible must be a boolean or a condition object" });
122
+ return;
123
+ }
124
+ if ("all" in e || "any" in e) {
125
+ const o = e.all ?? e.any;
126
+ if (!Array.isArray(o)) {
127
+ n.push({ path: t, message: '"all" / "any" must be an array of conditions' });
128
+ return;
129
+ }
130
+ o.forEach((r, s) => R(r, `${t}[${s}]`, n));
131
+ return;
132
+ }
133
+ if (typeof e.when != "string") {
134
+ n.push({ path: t, message: 'a condition needs "when": the name of a field' });
135
+ return;
136
+ }
137
+ const i = ["is", "in", "not"].filter((o) => o in e);
138
+ i.length !== 1 ? n.push({ path: t, message: 'a condition needs exactly one of "is", "in", "not"' }) : i[0] === "in" && !Array.isArray(e.in) && n.push({ path: t, message: '"in" must be an array' });
139
+ }
140
+ function w(e, t, n, i) {
141
+ if (!u(e)) {
142
+ n.push({ path: t, message: "a node must be an object" });
143
+ return;
144
+ }
145
+ for (const o of Object.keys(e))
146
+ E.has(o) || n.push({ path: t, message: `unknown key "${o}"` });
147
+ typeof e.id != "string" || e.id === "" ? n.push({ path: t, message: '"id" is required and must be a non-empty string' }) : i.has(e.id) ? n.push({ path: t, message: `duplicate id "${e.id}"` }) : i.add(e.id), (typeof e.type != "string" || e.type === "") && n.push({ path: t, message: '"type" is required and must be a non-empty string' });
148
+ for (const o of ["name", "label", "help"])
149
+ o in e && typeof e[o] != "string" && n.push({ path: t, message: `"${o}" must be a string` });
150
+ "localized" in e && typeof e.localized != "boolean" && n.push({ path: t, message: '"localized" must be a boolean' }), "props" in e && !u(e.props) && n.push({ path: t, message: '"props" must be an object' }), "slot" in e && e.slot !== null && typeof e.slot != "string" && n.push({ path: t, message: '"slot" must be a string or null' }), "can" in e && e.can !== null && typeof e.can != "string" && n.push({ path: t, message: '"can" must be a string or null' }), "visible" in e && typeof e.visible != "boolean" && R(e.visible, `${t}.visible`, n), "children" in e && (Array.isArray(e.children) ? e.children.forEach(
151
+ (o, r) => w(o, `${t}.children[${r}]`, n, i)
152
+ ) : n.push({ path: t, message: '"children" must be an array' }));
153
+ }
154
+ function $e(e) {
155
+ const t = [];
156
+ if (!Array.isArray(e)) return [{ path: "root", message: "root must be an array of nodes" }];
157
+ const n = /* @__PURE__ */ new Set();
158
+ return e.forEach((i, o) => w(i, `root[${o}]`, t, n)), t;
159
+ }
160
+ function Ae(e) {
161
+ const t = [];
162
+ return Array.isArray(e) ? (e.forEach((n, i) => {
163
+ const o = `patch[${i}]`;
164
+ if (!u(n)) {
165
+ t.push({ path: o, message: "an operation must be an object" });
166
+ return;
167
+ }
168
+ if (typeof n.op != "string" || !le.has(n.op)) {
169
+ t.push({ path: o, message: '"op" must be one of add, remove, replace, move, set' });
170
+ return;
171
+ }
172
+ if ((typeof n.target != "string" || n.target === "") && t.push({ path: o, message: '"target" is required: the id of a node' }), (n.op === "add" || n.op === "replace") && !u(n.node) ? t.push({ path: o, message: `"${n.op}" needs a "node"` }) : (n.op === "add" || n.op === "replace") && w(n.node, `${o}.node`, t, /* @__PURE__ */ new Set()), "position" in n && !de(n.position) && t.push({ path: o, message: '"position" must be first, last, before:<id> or after:<id>' }), n.op === "move" && "to" in n && typeof n.to != "string" && t.push({ path: o, message: '"to" must be the id of the new parent' }), n.op === "set")
173
+ for (const r of Object.keys(n))
174
+ r !== "op" && r !== "target" && (!E.has(r) || r === "id") && t.push({ path: o, message: `"set" cannot change "${r}"` });
175
+ }), t) : [{ path: "patch", message: "a patch must be an array" }];
176
+ }
177
+ function de(e) {
178
+ return typeof e == "string" && (e === "first" || e === "last" || e.startsWith("before:") || e.startsWith("after:"));
179
+ }
180
+ const fe = {
181
+ "wx-tabs": { component: L, kind: "layout" },
182
+ "wx-tab": {
183
+ component: H,
184
+ kind: "layout",
185
+ labelProp: "label",
186
+ // A tab is selected by value, and the node's id is the one stable thing it has.
187
+ bind: (e) => ({ value: e.id })
188
+ },
189
+ "wx-card": { component: _, kind: "layout", labelProp: "title" },
190
+ "wx-row": { component: Y, kind: "layout" },
191
+ "wx-col": { component: G, kind: "layout" },
192
+ "wx-divider": { component: J, kind: "layout", labelProp: "label" },
193
+ "wx-input": { component: U, kind: "field" },
194
+ "wx-textarea": { component: K, kind: "field" },
195
+ "wx-input-number": { component: B, kind: "field" },
196
+ "wx-select": { component: F, kind: "field" },
197
+ "wx-switch": { component: D, kind: "field" },
198
+ "wx-checkbox": { component: q, kind: "field" },
199
+ "wx-radio-group": { component: M, kind: "field" },
200
+ "wx-date-picker": { component: V, kind: "field" },
201
+ "wx-color-picker": { component: I, kind: "field" },
202
+ "wx-text": { component: C, kind: "display" },
203
+ "wx-alert": { component: T, kind: "display", labelProp: "title" }
204
+ };
205
+ function Se(e) {
206
+ return e;
207
+ }
208
+ function ue(e) {
209
+ const t = e.component;
210
+ return t.name ?? t.__name ?? "anonymous";
211
+ }
212
+ function pe(e) {
213
+ return e.kind === "field" ? "form item" : e.labelProp ? `prop \`${e.labelProp}\`` : e.kind === "display" ? "default slot" : "—";
214
+ }
215
+ function me(e) {
216
+ return Object.entries(e).map(([t, n]) => ({
217
+ type: t,
218
+ kind: n.kind,
219
+ component: ue(n),
220
+ label: pe(n)
221
+ }));
222
+ }
223
+ function je(e) {
224
+ const t = me(e).map((r) => [
225
+ `\`${r.type}\``,
226
+ r.kind,
227
+ `\`${r.component}\``,
228
+ r.label
229
+ ]), n = ["Type", "Kind", "Component", "`label` goes to"], i = n.map(
230
+ (r, s) => Math.max(r.length, ...t.map((a) => a[s].length))
231
+ ), o = (r) => `| ${r.map((s, a) => s.padEnd(i[a])).join(" | ")} |`;
232
+ return [o(n), o(i.map((r) => "-".repeat(r))), ...t.map(o)].join(
233
+ `
234
+ `
235
+ );
236
+ }
237
+ const P = "trans::", ye = (e) => e;
238
+ function x(e, t) {
239
+ return typeof e == "string" && e.startsWith(P) ? t(e.slice(P.length)) : e;
240
+ }
241
+ function k(e, t) {
242
+ if (typeof e == "string") return x(e, t);
243
+ if (Array.isArray(e)) return e.map((n) => k(n, t));
244
+ if (e && typeof e == "object") {
245
+ const n = {};
246
+ for (const [i, o] of Object.entries(e)) n[i] = k(o, t);
247
+ return n;
248
+ }
249
+ return e;
250
+ }
251
+ function he(e) {
252
+ return d(
253
+ "div",
254
+ { key: e.id, class: "wx-screen__unknown", role: "note" },
255
+ `Unknown type: ${e.type}`
256
+ );
257
+ }
258
+ function be(e, t, n) {
259
+ const i = /* @__PURE__ */ new Map();
260
+ for (const r of e.children ?? []) {
261
+ const s = r.slot ?? t, a = i.get(s) ?? [];
262
+ a.push(r), i.set(s, a);
263
+ }
264
+ const o = {};
265
+ for (const [r, s] of i)
266
+ o[r] = () => z(s, n);
267
+ return o;
268
+ }
269
+ function ge(e, t) {
270
+ if (e.can && !t.can(e.can) || !ce(e, t.model)) return null;
271
+ const n = t.types[e.type];
272
+ if (!n) return he(e);
273
+ const { translate: i } = t, o = {
274
+ key: e.id,
275
+ ...k(e.props ?? {}, i),
276
+ ...n.bind?.(e)
277
+ }, r = x(e.label, i);
278
+ if (n.kind === "field") {
279
+ const s = e.name, a = d(n.component, {
280
+ ...o,
281
+ name: s,
282
+ localized: e.localized || void 0,
283
+ modelValue: s === void 0 ? void 0 : t.model[s],
284
+ "onUpdate:modelValue": (y) => {
285
+ s !== void 0 && t.update(s, y);
286
+ }
287
+ });
288
+ return d(
289
+ Q,
290
+ { key: e.id, name: s, label: r, help: x(e.help, i) },
291
+ () => a
292
+ );
293
+ }
294
+ return n.kind === "layout" ? (n.labelProp && r !== void 0 && (o[n.labelProp] = r), d(n.component, o, be(e, n.childrenSlot ?? "default", t))) : n.labelProp ? (r !== void 0 && (o[n.labelProp] = r), d(n.component, o)) : d(n.component, o, r === void 0 ? void 0 : () => r);
295
+ }
296
+ function z(e, t) {
297
+ const n = [];
298
+ for (const i of e) {
299
+ const o = ge(i, t);
300
+ o && n.push(o);
301
+ }
302
+ return n;
303
+ }
304
+ const xe = N({
305
+ name: "WxScreenNodes",
306
+ props: {
307
+ nodes: { type: Array, required: !0 },
308
+ context: { type: Object, required: !0 }
309
+ },
310
+ setup(e) {
311
+ return () => z(e.nodes, e.context);
312
+ }
313
+ }), Pe = /* @__PURE__ */ N({
314
+ name: "WxScreenRenderer",
315
+ __name: "ScreenRenderer",
316
+ props: /* @__PURE__ */ A({
317
+ root: {},
318
+ patch: { default: () => [] },
319
+ errors: { default: void 0 },
320
+ types: { default: void 0 },
321
+ translate: { type: Function, default: void 0 },
322
+ can: { type: Function, default: void 0 },
323
+ disabled: { type: Boolean, default: !1 },
324
+ labelPosition: { default: void 0 },
325
+ labelWidth: { default: void 0 },
326
+ size: { default: void 0 }
327
+ }, {
328
+ modelValue: { default: () => ({}) },
329
+ modelModifiers: {}
330
+ }),
331
+ emits: /* @__PURE__ */ A(["patchError"], ["update:modelValue"]),
332
+ setup(e, { expose: t, emit: n }) {
333
+ const i = e, o = n, r = Z(e, "modelValue"), s = p(() => se(i.root, i.patch));
334
+ v(
335
+ () => s.value.errors,
336
+ (c) => {
337
+ for (const l of c)
338
+ console.error(`[webx-ui/schema] patch[${l.index}] ${l.op.op}: ${l.message}`);
339
+ o("patchError", c);
340
+ },
341
+ { immediate: !0 }
342
+ );
343
+ const a = p(() => {
344
+ const c = {};
345
+ for (const [l, W] of Object.entries({ ...fe, ...i.types }))
346
+ c[l] = { ...W, component: ee(ne(W.component)) };
347
+ return c;
348
+ }), y = p(() => ({
349
+ types: a.value,
350
+ model: r.value,
351
+ update: (c, l) => {
352
+ r.value = { ...r.value, [c]: l };
353
+ },
354
+ translate: i.translate ?? ye,
355
+ can: i.can ?? (() => !0)
356
+ }));
357
+ return t({
358
+ /** The tree after the patch — what is actually on screen. */
359
+ tree: p(() => s.value.root)
360
+ }), (c, l) => (te(), oe($(X), {
361
+ class: "wx-screen",
362
+ errors: e.errors,
363
+ disabled: e.disabled,
364
+ "label-position": e.labelPosition,
365
+ "label-width": e.labelWidth,
366
+ size: e.size
367
+ }, {
368
+ default: ie(() => [
369
+ re($(xe), {
370
+ nodes: s.value.root,
371
+ context: y.value
372
+ }, null, 8, ["nodes", "context"])
373
+ ]),
374
+ _: 1
375
+ }, 8, ["errors", "disabled", "label-position", "label-width", "size"]));
376
+ }
377
+ });
378
+ export {
379
+ P as TRANS_MARKER,
380
+ Pe as WxScreenRenderer,
381
+ se as applyPatch,
382
+ h as collectIds,
383
+ fe as coreTypes,
384
+ Se as defineTypes,
385
+ me as describeTypes,
386
+ g as evaluateCondition,
387
+ We as findNode,
388
+ ce as isVisible,
389
+ k as translateDeep,
390
+ je as typesTable,
391
+ Ae as validatePatch,
392
+ $e as validateScreen,
393
+ x as words
394
+ };
395
+ //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAoC,UAAa;IACtF,OAAO,EAAE,GAAG,UAAU,EAAE,CAAA;AAC1B,CAAC"}
1
+ {"version":3,"file":"index.js","sources":["../src/patch.ts","../src/visible.ts","../src/validate.ts","../src/registry.ts","../src/render.ts","../src/ScreenRenderer.vue"],"sourcesContent":["import type { Patch, PatchError, PatchOperation, PatchPosition, ScreenNode } from './types'\n\ninterface Located {\n /** The array the node sits in: the root, or a parent's `children`. */\n list: ScreenNode[]\n index: number\n node: ScreenNode\n}\n\n/** Depth-first search by `id`, remembering the array the match lives in. */\nexport function locate(root: ScreenNode[], id: string): Located | null {\n for (let index = 0; index < root.length; index += 1) {\n const node = root[index]!\n if (node.id === id) return { list: root, index, node }\n if (node.children) {\n const found = locate(node.children, id)\n if (found) return found\n }\n }\n return null\n}\n\nexport function findNode(root: ScreenNode[], id: string): ScreenNode | null {\n return locate(root, id)?.node ?? null\n}\n\n/** Every `id` in the tree, in document order. Duplicates are kept, so callers can spot them. */\nexport function collectIds(root: ScreenNode[]): string[] {\n const ids: string[] = []\n const walk = (nodes: ScreenNode[]) => {\n for (const node of nodes) {\n ids.push(node.id)\n if (node.children) walk(node.children)\n }\n }\n walk(root)\n return ids\n}\n\n/** Index a node should be inserted at in `list`, or a message when the anchor is missing. */\nfunction indexFor(list: ScreenNode[], position: PatchPosition | undefined): number | string {\n if (!position || position === 'last') return list.length\n if (position === 'first') return 0\n const [where, anchor] = position.split(':', 2) as ['before' | 'after', string]\n const at = list.findIndex((node) => node.id === anchor)\n if (at === -1) return `no sibling \"${anchor}\" to insert ${where}`\n return where === 'before' ? at : at + 1\n}\n\n/**\n * Deep copy of a JSON value. `structuredClone` refuses a Vue reactive proxy, and a\n * tree that came out of a `ref` is exactly that; reading through the proxy is fine.\n */\nexport function clone<T>(value: T): T {\n if (Array.isArray(value)) return value.map(clone) as T\n if (value && typeof value === 'object') {\n const out: Record<string, unknown> = {}\n for (const [key, item] of Object.entries(value)) out[key] = clone(item)\n return out as T\n }\n return value\n}\n\n/**\n * Applies a patch to a tree and returns the result as a new tree; the input is not\n * touched. An operation that cannot be applied — a `target` that does not exist, an\n * anchor that is missing — is skipped and reported, and the ones after it still run:\n * a project patch that survived a rename in the module must not silence the rest.\n */\nexport function applyPatch(\n root: ScreenNode[],\n patch: Patch,\n): { root: ScreenNode[]; errors: PatchError[] } {\n const tree = clone(root)\n const errors: PatchError[] = []\n\n patch.forEach((op, index) => {\n const message = apply(tree, op)\n if (message) errors.push({ index, op, message })\n })\n\n return { root: tree, errors }\n}\n\n/** Mutates `tree`; returns a message when the operation was refused. */\nfunction apply(tree: ScreenNode[], op: PatchOperation): string | null {\n const found = locate(tree, op.target)\n if (!found) return `target \"${op.target}\" not found`\n\n switch (op.op) {\n case 'add': {\n const clash = duplicateIn(tree, op.node)\n if (clash) return `id \"${clash}\" already exists in the screen`\n const list = (found.node.children ??= [])\n const at = indexFor(list, op.position)\n if (typeof at === 'string') return at\n list.splice(at, 0, clone(op.node))\n return null\n }\n case 'remove':\n found.list.splice(found.index, 1)\n return null\n case 'replace': {\n // The replaced node's own ids are gone, so only the rest of the tree can clash.\n const others = tree.filter((node) => node !== found.node)\n const clash = duplicateIn(others, op.node, found.node)\n if (clash) return `id \"${clash}\" already exists in the screen`\n found.list.splice(found.index, 1, clone(op.node))\n return null\n }\n case 'move': {\n let list = found.list\n if (op.to !== undefined) {\n const parent = locate(tree, op.to)\n if (!parent) return `destination \"${op.to}\" not found`\n if (parent.node === found.node || contains(found.node, parent.node)) {\n return `cannot move \"${op.target}\" into itself`\n }\n list = parent.node.children ??= []\n }\n found.list.splice(found.index, 1)\n const at = indexFor(list, op.position)\n if (typeof at === 'string') {\n // Put it back where it was: a refused move leaves the tree untouched.\n found.list.splice(found.index, 0, found.node)\n return at\n }\n list.splice(at, 0, found.node)\n return null\n }\n case 'set': {\n for (const [key, value] of Object.entries(op)) {\n if (key === 'op' || key === 'target') continue\n if (key === 'props') {\n found.node.props = { ...found.node.props, ...(value as Record<string, unknown>) }\n } else {\n ;(found.node as unknown as Record<string, unknown>)[key] = clone(value)\n }\n }\n return null\n }\n }\n}\n\nfunction contains(ancestor: ScreenNode, node: ScreenNode): boolean {\n return (ancestor.children ?? []).some((child) => child === node || contains(child, node))\n}\n\n/** First id of `incoming` that already exists in `tree`, ignoring `except`'s own subtree. */\nfunction duplicateIn(tree: ScreenNode[], incoming: ScreenNode, except?: ScreenNode): string | null {\n const existing = new Set(collectIds(tree))\n if (except) for (const id of collectIds([except])) existing.delete(id)\n return collectIds([incoming]).find((id) => existing.has(id)) ?? null\n}\n","import type { ScreenModel, ScreenNode, VisibilityCondition } from './types'\n\n/** Loose enough for `{ \"is\": 1 }` to match a numeric input, strict enough for `\"1\"` not to. */\nfunction same(a: unknown, b: unknown): boolean {\n if (a === b) return true\n if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false\n return JSON.stringify(a) === JSON.stringify(b)\n}\n\nexport function evaluateCondition(condition: VisibilityCondition, model: ScreenModel): boolean {\n if ('all' in condition) return condition.all.every((item) => evaluateCondition(item, model))\n if ('any' in condition) return condition.any.some((item) => evaluateCondition(item, model))\n\n const value = model[condition.when]\n if ('in' in condition) return condition.in.some((item) => same(item, value))\n if ('not' in condition) return !same(condition.not, value)\n return same(condition.is, value)\n}\n\n/** Whether a node should render, given the current model. Absent `visible` means yes. */\nexport function isVisible(node: ScreenNode, model: ScreenModel): boolean {\n const { visible } = node\n if (visible === undefined || visible === true) return true\n if (visible === false) return false\n return evaluateCondition(visible, model)\n}\n","import type { Patch, ScreenError, ScreenNode, VisibilityCondition } from './types'\n\nconst NODE_KEYS = new Set([\n 'id',\n 'type',\n 'name',\n 'label',\n 'help',\n 'localized',\n 'props',\n 'children',\n 'slot',\n 'visible',\n 'can',\n])\n\nconst OPS = new Set(['add', 'remove', 'replace', 'move', 'set'])\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction checkCondition(value: unknown, path: string, errors: ScreenError[]): void {\n if (!isRecord(value)) {\n errors.push({ path, message: 'visible must be a boolean or a condition object' })\n return\n }\n if ('all' in value || 'any' in value) {\n const list = (value.all ?? value.any) as unknown\n if (!Array.isArray(list)) {\n errors.push({ path, message: '\"all\" / \"any\" must be an array of conditions' })\n return\n }\n list.forEach((item, index) => checkCondition(item, `${path}[${index}]`, errors))\n return\n }\n if (typeof value.when !== 'string') {\n errors.push({ path, message: 'a condition needs \"when\": the name of a field' })\n return\n }\n const forms = ['is', 'in', 'not'].filter((key) => key in value)\n if (forms.length !== 1) {\n errors.push({ path, message: 'a condition needs exactly one of \"is\", \"in\", \"not\"' })\n } else if (forms[0] === 'in' && !Array.isArray(value.in)) {\n errors.push({ path, message: '\"in\" must be an array' })\n }\n}\n\nfunction checkNode(value: unknown, path: string, errors: ScreenError[], seen: Set<string>): void {\n if (!isRecord(value)) {\n errors.push({ path, message: 'a node must be an object' })\n return\n }\n\n for (const key of Object.keys(value)) {\n if (!NODE_KEYS.has(key)) errors.push({ path, message: `unknown key \"${key}\"` })\n }\n\n if (typeof value.id !== 'string' || value.id === '') {\n errors.push({ path, message: '\"id\" is required and must be a non-empty string' })\n } else if (seen.has(value.id)) {\n errors.push({ path, message: `duplicate id \"${value.id}\"` })\n } else {\n seen.add(value.id)\n }\n\n if (typeof value.type !== 'string' || value.type === '') {\n errors.push({ path, message: '\"type\" is required and must be a non-empty string' })\n }\n\n for (const key of ['name', 'label', 'help'] as const) {\n if (key in value && typeof value[key] !== 'string') {\n errors.push({ path, message: `\"${key}\" must be a string` })\n }\n }\n if ('localized' in value && typeof value.localized !== 'boolean') {\n errors.push({ path, message: '\"localized\" must be a boolean' })\n }\n if ('props' in value && !isRecord(value.props)) {\n errors.push({ path, message: '\"props\" must be an object' })\n }\n if ('slot' in value && value.slot !== null && typeof value.slot !== 'string') {\n errors.push({ path, message: '\"slot\" must be a string or null' })\n }\n if ('can' in value && value.can !== null && typeof value.can !== 'string') {\n errors.push({ path, message: '\"can\" must be a string or null' })\n }\n if ('visible' in value && typeof value.visible !== 'boolean') {\n checkCondition(value.visible as VisibilityCondition, `${path}.visible`, errors)\n }\n\n if ('children' in value) {\n if (!Array.isArray(value.children)) {\n errors.push({ path, message: '\"children\" must be an array' })\n } else {\n value.children.forEach((child, index) =>\n checkNode(child, `${path}.children[${index}]`, errors, seen),\n )\n }\n }\n}\n\n/**\n * Checks a tree the way the JSON schema would, without a schema library: required\n * keys, closed key set, value types, and — what a schema cannot say — unique ids.\n * An empty list means the tree is sound.\n */\nexport function validateScreen(root: unknown): ScreenError[] {\n const errors: ScreenError[] = []\n if (!Array.isArray(root)) return [{ path: 'root', message: 'root must be an array of nodes' }]\n const seen = new Set<string>()\n root.forEach((node, index) => checkNode(node, `root[${index}]`, errors, seen))\n return errors\n}\n\n/** Same for a patch: each operation has the keys its `op` calls for. */\nexport function validatePatch(patch: unknown): ScreenError[] {\n const errors: ScreenError[] = []\n if (!Array.isArray(patch)) return [{ path: 'patch', message: 'a patch must be an array' }]\n\n patch.forEach((op, index) => {\n const path = `patch[${index}]`\n if (!isRecord(op)) {\n errors.push({ path, message: 'an operation must be an object' })\n return\n }\n if (typeof op.op !== 'string' || !OPS.has(op.op)) {\n errors.push({ path, message: '\"op\" must be one of add, remove, replace, move, set' })\n return\n }\n if (typeof op.target !== 'string' || op.target === '') {\n errors.push({ path, message: '\"target\" is required: the id of a node' })\n }\n if ((op.op === 'add' || op.op === 'replace') && !isRecord(op.node)) {\n errors.push({ path, message: `\"${op.op}\" needs a \"node\"` })\n } else if (op.op === 'add' || op.op === 'replace') {\n checkNode(op.node, `${path}.node`, errors, new Set())\n }\n if ('position' in op && !isPosition(op.position)) {\n errors.push({ path, message: '\"position\" must be first, last, before:<id> or after:<id>' })\n }\n if (op.op === 'move' && 'to' in op && typeof op.to !== 'string') {\n errors.push({ path, message: '\"to\" must be the id of the new parent' })\n }\n if (op.op === 'set') {\n for (const key of Object.keys(op)) {\n if (key !== 'op' && key !== 'target' && (!NODE_KEYS.has(key) || key === 'id')) {\n errors.push({ path, message: `\"set\" cannot change \"${key}\"` })\n }\n }\n }\n })\n\n return errors\n}\n\nfunction isPosition(value: unknown): boolean {\n return (\n typeof value === 'string' &&\n (value === 'first' ||\n value === 'last' ||\n value.startsWith('before:') ||\n value.startsWith('after:'))\n )\n}\n\nexport type { Patch, ScreenNode }\n","import {\n WxAlert,\n WxCard,\n WxCheckbox,\n WxCol,\n WxColorPicker,\n WxDatePicker,\n WxDivider,\n WxInput,\n WxInputNumber,\n WxRadioGroup,\n WxRow,\n WxSelect,\n WxSwitch,\n WxTab,\n WxTabs,\n WxText,\n WxTextarea,\n} from '@webx-ui/core'\nimport type { NodeKind, TypeEntry, TypeRegistry } from './types'\n\n/**\n * The types every panel has: the core's layout, form and display components under\n * their full names. A module or a project adds its own the same way — `wx-media` comes\n * from `module-media`, `map` from whoever has a map.\n */\nexport const coreTypes: TypeRegistry = {\n 'wx-tabs': { component: WxTabs, kind: 'layout' },\n 'wx-tab': {\n component: WxTab,\n kind: 'layout',\n labelProp: 'label',\n // A tab is selected by value, and the node's id is the one stable thing it has.\n bind: (node) => ({ value: node.id }),\n },\n 'wx-card': { component: WxCard, kind: 'layout', labelProp: 'title' },\n 'wx-row': { component: WxRow, kind: 'layout' },\n 'wx-col': { component: WxCol, kind: 'layout' },\n 'wx-divider': { component: WxDivider, kind: 'layout', labelProp: 'label' },\n\n 'wx-input': { component: WxInput, kind: 'field' },\n 'wx-textarea': { component: WxTextarea, kind: 'field' },\n 'wx-input-number': { component: WxInputNumber, kind: 'field' },\n 'wx-select': { component: WxSelect, kind: 'field' },\n 'wx-switch': { component: WxSwitch, kind: 'field' },\n 'wx-checkbox': { component: WxCheckbox, kind: 'field' },\n 'wx-radio-group': { component: WxRadioGroup, kind: 'field' },\n 'wx-date-picker': { component: WxDatePicker, kind: 'field' },\n 'wx-color-picker': { component: WxColorPicker, kind: 'field' },\n\n 'wx-text': { component: WxText, kind: 'display' },\n 'wx-alert': { component: WxAlert, kind: 'display', labelProp: 'title' },\n}\n\n/** Identity with a type: keeps a project's registry object checked without an import of the type. */\nexport function defineTypes<T extends TypeRegistry>(types: T): T {\n return types\n}\n\nexport interface TypeDescription {\n type: string\n kind: NodeKind\n component: string\n /** Where the node's `label` ends up, in words. */\n label: string\n}\n\nfunction componentName(entry: TypeEntry): string {\n const component = entry.component as { name?: string; __name?: string }\n return component.name ?? component.__name ?? 'anonymous'\n}\n\nfunction labelDestination(entry: TypeEntry): string {\n if (entry.kind === 'field') return 'form item'\n if (entry.labelProp) return `prop \\`${entry.labelProp}\\``\n return entry.kind === 'display' ? 'default slot' : '—'\n}\n\n/** One row per type, in registry order — what the documentation table is made of. */\nexport function describeTypes(types: TypeRegistry): TypeDescription[] {\n return Object.entries(types).map(([type, entry]) => ({\n type,\n kind: entry.kind,\n component: componentName(entry),\n label: labelDestination(entry),\n }))\n}\n\n/**\n * The registry as a Markdown table, padded the way Prettier pads one, so the generated\n * block in the guide survives `prettier --check` and a test can compare it verbatim.\n */\nexport function typesTable(types: TypeRegistry): string {\n const rows = describeTypes(types).map((row) => [\n `\\`${row.type}\\``,\n row.kind,\n `\\`${row.component}\\``,\n row.label,\n ])\n const header = ['Type', 'Kind', 'Component', '`label` goes to']\n const widths = header.map((cell, column) =>\n Math.max(cell.length, ...rows.map((row) => row[column]!.length)),\n )\n const line = (cells: string[]) =>\n `| ${cells.map((cell, column) => cell.padEnd(widths[column]!)).join(' | ')} |`\n return [line(header), line(widths.map((width) => '-'.repeat(width))), ...rows.map(line)].join(\n '\\n',\n )\n}\n","import { defineComponent, h, type PropType, type VNode } from 'vue'\nimport { WxFormItem } from '@webx-ui/core'\nimport { isVisible } from './visible'\nimport type { ScreenModel, ScreenNode, Translate, TypeRegistry } from './types'\n\nexport const TRANS_MARKER = 'trans::'\n\n/** What the recursive renderer needs at every level. */\nexport interface RenderContext {\n types: TypeRegistry\n model: ScreenModel\n update: (name: string, value: unknown) => void\n translate: Translate\n can: (permission: string) => boolean\n}\n\n/** Without a dictionary the key itself shows — honest, and easy to spot in a screenshot. */\nexport const keyAsIs: Translate = (key) => key\n\n/** Translates a marked string; anything else — including `undefined` — passes through. */\nexport function words<T>(value: T, translate: Translate): T {\n if (typeof value === 'string' && value.startsWith(TRANS_MARKER)) {\n return translate(value.slice(TRANS_MARKER.length)) as T\n }\n return value\n}\n\n/** Same, through arrays and objects: `props.options[].label` is the common case. */\nexport function translateDeep<T>(value: T, translate: Translate): T {\n if (typeof value === 'string') return words(value, translate)\n if (Array.isArray(value)) return value.map((item) => translateDeep(item, translate)) as T\n if (value && typeof value === 'object') {\n const out: Record<string, unknown> = {}\n for (const [key, item] of Object.entries(value)) out[key] = translateDeep(item, translate)\n return out as T\n }\n return value\n}\n\nfunction renderUnknown(node: ScreenNode): VNode {\n return h(\n 'div',\n { key: node.id, class: 'wx-screen__unknown', role: 'note' },\n `Unknown type: ${node.type}`,\n )\n}\n\n/** Children grouped by the slot they asked for. */\nfunction childSlots(\n node: ScreenNode,\n defaultSlot: string,\n context: RenderContext,\n): Record<string, () => VNode[]> {\n const groups = new Map<string, ScreenNode[]>()\n for (const child of node.children ?? []) {\n const name = child.slot ?? defaultSlot\n const list = groups.get(name) ?? []\n list.push(child)\n groups.set(name, list)\n }\n const slots: Record<string, () => VNode[]> = {}\n for (const [name, children] of groups) {\n slots[name] = () => renderNodes(children, context)\n }\n return slots\n}\n\nexport function renderNode(node: ScreenNode, context: RenderContext): VNode | null {\n if (node.can && !context.can(node.can)) return null\n if (!isVisible(node, context.model)) return null\n\n const entry = context.types[node.type]\n if (!entry) return renderUnknown(node)\n\n const { translate } = context\n const props: Record<string, unknown> = {\n key: node.id,\n ...translateDeep(node.props ?? {}, translate),\n ...entry.bind?.(node),\n }\n const label = words(node.label, translate)\n\n if (entry.kind === 'field') {\n const name = node.name\n const control = h(entry.component, {\n ...props,\n name,\n localized: node.localized || undefined,\n modelValue: name === undefined ? undefined : context.model[name],\n 'onUpdate:modelValue': (value: unknown) => {\n if (name !== undefined) context.update(name, value)\n },\n })\n return h(\n WxFormItem,\n { key: node.id, name, label, help: words(node.help, translate) },\n () => control,\n )\n }\n\n if (entry.kind === 'layout') {\n if (entry.labelProp && label !== undefined) props[entry.labelProp] = label\n return h(entry.component, props, childSlots(node, entry.childrenSlot ?? 'default', context))\n }\n\n // display\n if (entry.labelProp) {\n if (label !== undefined) props[entry.labelProp] = label\n return h(entry.component, props)\n }\n return h(entry.component, props, label === undefined ? undefined : () => label)\n}\n\nexport function renderNodes(nodes: ScreenNode[], context: RenderContext): VNode[] {\n const out: VNode[] = []\n for (const node of nodes) {\n const rendered = renderNode(node, context)\n if (rendered) out.push(rendered)\n }\n return out\n}\n\n/** A list of nodes as a component, so the tree can recurse through slots. */\nexport const WxScreenNodes = defineComponent({\n name: 'WxScreenNodes',\n props: {\n nodes: { type: Array as PropType<ScreenNode[]>, required: true },\n context: { type: Object as PropType<RenderContext>, required: true },\n },\n setup(props) {\n return () => renderNodes(props.nodes, props.context)\n },\n})\n","<script setup lang=\"ts\">\nimport { computed, markRaw, toRaw, watch } from 'vue'\nimport { WxForm } from '@webx-ui/core'\nimport { applyPatch } from './patch'\nimport { coreTypes } from './registry'\nimport { keyAsIs, WxScreenNodes, type RenderContext } from './render'\nimport type {\n Patch,\n PatchError,\n ScreenModel,\n ScreenNode,\n Translate,\n TypeRegistry,\n ValidationErrors,\n} from './types'\n\ndefineOptions({ name: 'WxScreenRenderer' })\n\nconst props = withDefaults(\n defineProps<{\n /** The tree to draw — a module's screen, already patched by the server. */\n root: ScreenNode[]\n /** Client-side patch applied on top, in order. */\n patch?: Patch\n /** Server-side validation errors, keyed by field name; shown under the fields. */\n errors?: ValidationErrors\n /** Project types, merged over the core ones. */\n types?: TypeRegistry\n /** Turns `trans::` strings into words. Without it the key shows. */\n translate?: Translate\n /** Decides `can`. Without it every node is allowed. */\n can?: (permission: string) => boolean\n disabled?: boolean\n labelPosition?: 'top' | 'left'\n labelWidth?: string\n size?: 'sm' | 'md' | 'lg'\n }>(),\n {\n patch: () => [],\n errors: undefined,\n types: undefined,\n translate: undefined,\n can: undefined,\n disabled: false,\n labelPosition: undefined,\n labelWidth: undefined,\n size: undefined,\n },\n)\n\nconst emit = defineEmits<{\n /** Operations that could not be applied. Also reported to the console. */\n patchError: [errors: PatchError[]]\n}>()\n\nconst model = defineModel<ScreenModel>({ default: () => ({}) })\n\nconst applied = computed(() => applyPatch(props.root, props.patch))\n\nwatch(\n () => applied.value.errors,\n (errors) => {\n for (const error of errors) {\n console.error(`[webx-ui/schema] patch[${error.index}] ${error.op.op}: ${error.message}`)\n }\n emit('patchError', errors)\n },\n { immediate: true },\n)\n\n/**\n * A registry kept in a `ref` arrives as a reactive proxy, and Vue warns about a\n * component that is one; the raw component is what `h()` wants anyway.\n */\nconst registry = computed<TypeRegistry>(() => {\n const merged: TypeRegistry = {}\n for (const [type, entry] of Object.entries({ ...coreTypes, ...props.types })) {\n merged[type] = { ...entry, component: markRaw(toRaw(entry.component)) }\n }\n return merged\n})\n\nconst context = computed<RenderContext>(() => ({\n types: registry.value,\n model: model.value,\n update: (name, value) => {\n model.value = { ...model.value, [name]: value }\n },\n translate: props.translate ?? keyAsIs,\n can: props.can ?? (() => true),\n}))\n\ndefineExpose({\n /** The tree after the patch — what is actually on screen. */\n tree: computed(() => applied.value.root),\n})\n</script>\n\n<template>\n <wx-form\n class=\"wx-screen\"\n :errors=\"errors\"\n :disabled=\"disabled\"\n :label-position=\"labelPosition\"\n :label-width=\"labelWidth\"\n :size=\"size\"\n >\n <wx-screen-nodes :nodes=\"applied.root\" :context=\"context\" />\n </wx-form>\n</template>\n\n<style>\n/* Global on purpose: the placeholder is created by a render function, outside any scope. */\n.wx-screen__unknown {\n padding: var(--wx-space-8) var(--wx-space-12);\n border: 1px dashed var(--wx-color-danger);\n border-radius: var(--wx-radius-control);\n background: var(--wx-color-danger-soft);\n color: var(--wx-color-danger);\n font-family: var(--wx-font-family-mono);\n font-size: var(--wx-font-size-sm);\n}\n</style>\n"],"names":["locate","root","id","index","node","found","findNode","collectIds","ids","walk","nodes","indexFor","list","position","where","anchor","at","clone","value","out","key","item","applyPatch","patch","tree","errors","op","message","apply","clash","duplicateIn","others","parent","contains","ancestor","child","incoming","except","existing","same","a","b","evaluateCondition","condition","model","isVisible","visible","NODE_KEYS","OPS","isRecord","checkCondition","path","forms","checkNode","seen","validateScreen","validatePatch","isPosition","coreTypes","WxTabs","WxTab","WxCard","WxRow","WxCol","WxDivider","WxInput","WxTextarea","WxInputNumber","WxSelect","WxSwitch","WxCheckbox","WxRadioGroup","WxDatePicker","WxColorPicker","WxText","WxAlert","defineTypes","types","componentName","entry","component","labelDestination","describeTypes","type","typesTable","rows","row","header","widths","cell","column","line","cells","width","TRANS_MARKER","keyAsIs","words","translate","translateDeep","renderUnknown","h","childSlots","defaultSlot","context","groups","name","slots","children","renderNodes","renderNode","props","label","control","WxFormItem","rendered","WxScreenNodes","defineComponent","__props","emit","__emit","_useModel","applied","computed","watch","error","registry","merged","markRaw","toRaw","__expose","_createBlock","_unref","WxForm","_createVNode"],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { Patch, PatchError, ScreenNode } from './types';
2
+ interface Located {
3
+ /** The array the node sits in: the root, or a parent's `children`. */
4
+ list: ScreenNode[];
5
+ index: number;
6
+ node: ScreenNode;
7
+ }
8
+ /** Depth-first search by `id`, remembering the array the match lives in. */
9
+ export declare function locate(root: ScreenNode[], id: string): Located | null;
10
+ export declare function findNode(root: ScreenNode[], id: string): ScreenNode | null;
11
+ /** Every `id` in the tree, in document order. Duplicates are kept, so callers can spot them. */
12
+ export declare function collectIds(root: ScreenNode[]): string[];
13
+ /**
14
+ * Deep copy of a JSON value. `structuredClone` refuses a Vue reactive proxy, and a
15
+ * tree that came out of a `ref` is exactly that; reading through the proxy is fine.
16
+ */
17
+ export declare function clone<T>(value: T): T;
18
+ /**
19
+ * Applies a patch to a tree and returns the result as a new tree; the input is not
20
+ * touched. An operation that cannot be applied — a `target` that does not exist, an
21
+ * anchor that is missing — is skipped and reported, and the ones after it still run:
22
+ * a project patch that survived a rename in the module must not silence the rest.
23
+ */
24
+ export declare function applyPatch(root: ScreenNode[], patch: Patch): {
25
+ root: ScreenNode[];
26
+ errors: PatchError[];
27
+ };
28
+ export {};
@@ -0,0 +1,23 @@
1
+ import { NodeKind, TypeRegistry } from './types';
2
+ /**
3
+ * The types every panel has: the core's layout, form and display components under
4
+ * their full names. A module or a project adds its own the same way — `wx-media` comes
5
+ * from `module-media`, `map` from whoever has a map.
6
+ */
7
+ export declare const coreTypes: TypeRegistry;
8
+ /** Identity with a type: keeps a project's registry object checked without an import of the type. */
9
+ export declare function defineTypes<T extends TypeRegistry>(types: T): T;
10
+ export interface TypeDescription {
11
+ type: string;
12
+ kind: NodeKind;
13
+ component: string;
14
+ /** Where the node's `label` ends up, in words. */
15
+ label: string;
16
+ }
17
+ /** One row per type, in registry order — what the documentation table is made of. */
18
+ export declare function describeTypes(types: TypeRegistry): TypeDescription[];
19
+ /**
20
+ * The registry as a Markdown table, padded the way Prettier pads one, so the generated
21
+ * block in the guide survives `prettier --check` and a test can compare it verbatim.
22
+ */
23
+ export declare function typesTable(types: TypeRegistry): string;
@@ -0,0 +1,41 @@
1
+ import { PropType, VNode } from 'vue';
2
+ import { ScreenModel, ScreenNode, Translate, TypeRegistry } from './types';
3
+ export declare const TRANS_MARKER = "trans::";
4
+ /** What the recursive renderer needs at every level. */
5
+ export interface RenderContext {
6
+ types: TypeRegistry;
7
+ model: ScreenModel;
8
+ update: (name: string, value: unknown) => void;
9
+ translate: Translate;
10
+ can: (permission: string) => boolean;
11
+ }
12
+ /** Without a dictionary the key itself shows — honest, and easy to spot in a screenshot. */
13
+ export declare const keyAsIs: Translate;
14
+ /** Translates a marked string; anything else — including `undefined` — passes through. */
15
+ export declare function words<T>(value: T, translate: Translate): T;
16
+ /** Same, through arrays and objects: `props.options[].label` is the common case. */
17
+ export declare function translateDeep<T>(value: T, translate: Translate): T;
18
+ export declare function renderNode(node: ScreenNode, context: RenderContext): VNode | null;
19
+ export declare function renderNodes(nodes: ScreenNode[], context: RenderContext): VNode[];
20
+ /** A list of nodes as a component, so the tree can recurse through slots. */
21
+ export declare const WxScreenNodes: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
22
+ nodes: {
23
+ type: PropType<ScreenNode[]>;
24
+ required: true;
25
+ };
26
+ context: {
27
+ type: PropType<RenderContext>;
28
+ required: true;
29
+ };
30
+ }>, () => VNode<import('vue').RendererNode, import('vue').RendererElement, {
31
+ [key: string]: any;
32
+ }>[], {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
33
+ nodes: {
34
+ type: PropType<ScreenNode[]>;
35
+ required: true;
36
+ };
37
+ context: {
38
+ type: PropType<RenderContext>;
39
+ required: true;
40
+ };
41
+ }>> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .wx-screen__unknown{padding:var(--wx-space-8) var(--wx-space-12);border:1px dashed var(--wx-color-danger);border-radius:var(--wx-radius-control);background:var(--wx-color-danger-soft);color:var(--wx-color-danger);font-family:var(--wx-font-family-mono);font-size:var(--wx-font-size-sm)}
package/dist/types.d.ts CHANGED
@@ -1,40 +1,129 @@
1
+ import { Component } from 'vue';
1
2
  /**
2
- * Backend-agnostic description of an admin UI.
3
+ * A screen is a tree of nodes. Every node has a stable `id` — the address patches use —
4
+ * and a `type` the registry resolves to a component. Nothing else is interpreted:
5
+ * whatever a component needs travels in `props` as-is.
3
6
  *
4
- * A screen is a tree of {@link SchemaNode}s. Rendering is done by `@webx-ui/schema`'s renderer
5
- * (not implemented yet) against a {@link ComponentRegistry} and an {@link ActionRegistry}; data
6
- * access goes through a {@link DataAdapter} so that Laravel, or anything else, stays behind
7
- * one interface.
7
+ * The keys are closed: a node carries exactly these and nothing more, so a typo is a
8
+ * validation error rather than a silently empty tab.
8
9
  */
9
- export interface SchemaNode {
10
- /** Registry key of the component to render, e.g. `"card"` or `"input"`. */
10
+ export interface ScreenNode {
11
+ /** Unique within the screen. Renaming one is a breaking change for every patch. */
12
+ id: string;
13
+ /** Registry key: the full component name (`wx-input`), or whatever a project registered. */
11
14
  type: string;
12
- /** Props passed to the component as-is. */
15
+ /**
16
+ * For fields: the key in the model. A literal string — dots are part of the key,
17
+ * not a path. Nesting is what `wx-repeater` is for.
18
+ */
19
+ name?: string;
20
+ label?: string;
21
+ /** Hint under a field. */
22
+ help?: string;
23
+ /** The field is edited per content language; the value is a record keyed by locale. */
24
+ localized?: boolean;
25
+ /** Passed to the component untouched. */
13
26
  props?: Record<string, unknown>;
14
- /** Child nodes, or plain text for leaf nodes. */
15
- children?: SchemaNode[] | string;
16
- /** Event name -> action descriptor, resolved through the action registry. */
17
- on?: Record<string, ActionDescriptor>;
18
- /** Expression or boolean controlling whether the node renders. */
19
- visible?: boolean | string;
20
- /** Stable key for list rendering. */
21
- key?: string;
27
+ children?: ScreenNode[];
28
+ /** Named slot of the parent to land in; the parent's default slot otherwise. */
29
+ slot?: string | null;
30
+ /** `false` hides the node; a condition is evaluated against the model. */
31
+ visible?: boolean | VisibilityCondition;
32
+ /** Permission required to render the node, e.g. `settings.manage`. */
33
+ can?: string | null;
22
34
  }
23
- export interface ActionDescriptor {
24
- /** Registry key of the action, e.g. `"submit"` or `"navigate"`. */
25
- type: string;
26
- payload?: Record<string, unknown>;
35
+ /** The file a module ships and the answer `GET /api/cms/screens/<name>` gives. */
36
+ export interface Screen {
37
+ $schema?: string;
38
+ /** `<module>.<screen>`, e.g. `settings.index`. */
39
+ screen: string;
40
+ /** What the values belong to — informational for now. */
41
+ model?: string;
42
+ root: ScreenNode[];
43
+ }
44
+ /**
45
+ * "Show this when that field holds this value." Evaluated on the client against the
46
+ * current model; the server never sees it.
47
+ */
48
+ export type VisibilityCondition = {
49
+ when: string;
50
+ is: unknown;
51
+ } | {
52
+ when: string;
53
+ in: unknown[];
54
+ } | {
55
+ when: string;
56
+ not: unknown;
57
+ } | {
58
+ all: VisibilityCondition[];
59
+ } | {
60
+ any: VisibilityCondition[];
61
+ };
62
+ /** Where an added or moved node lands among its siblings. `last` is the default. */
63
+ export type PatchPosition = 'first' | 'last' | `before:${string}` | `after:${string}`;
64
+ export type PatchOperation = {
65
+ op: 'add';
66
+ target: string;
67
+ node: ScreenNode;
68
+ position?: PatchPosition;
69
+ } | {
70
+ op: 'remove';
71
+ target: string;
72
+ } | {
73
+ op: 'replace';
74
+ target: string;
75
+ node: ScreenNode;
76
+ } | {
77
+ op: 'move';
78
+ target: string;
79
+ position?: PatchPosition;
80
+ to?: string;
81
+ } | ({
82
+ op: 'set';
83
+ target: string;
84
+ } & Partial<Omit<ScreenNode, 'id'>>);
85
+ export type Patch = PatchOperation[];
86
+ /** An operation that could not be applied. The tree is left as it was before it. */
87
+ export interface PatchError {
88
+ /** Index of the operation in the patch. */
89
+ index: number;
90
+ op: PatchOperation;
91
+ message: string;
92
+ }
93
+ /** A problem found by {@link validateScreen}, with the path of the node it is about. */
94
+ export interface ScreenError {
95
+ path: string;
96
+ message: string;
27
97
  }
28
- /** Maps schema `type` values to concrete Vue components. */
29
- export type ComponentRegistry = Record<string, unknown>;
30
- /** Maps action `type` values to handlers. */
31
- export type ActionRegistry = Record<string, ActionHandler>;
32
- export type ActionHandler = (context: ActionContext) => void | Promise<void>;
33
- export interface ActionContext {
34
- payload: Record<string, unknown>;
35
- event?: unknown;
36
- node: SchemaNode;
98
+ export type NodeKind = 'layout' | 'field' | 'display';
99
+ /** How the renderer treats one type. */
100
+ export interface TypeEntry {
101
+ component: Component;
102
+ /**
103
+ * `layout` gets its children in slots and never touches the model; `field` is bound
104
+ * to the model by `name` and wrapped in a form item; `display` only draws.
105
+ */
106
+ kind: NodeKind;
107
+ /** Slot that receives children without a `slot` of their own. Default: `default`. */
108
+ childrenSlot?: string;
109
+ /**
110
+ * Prop the node's `label` goes to (`title` on a card). For a `display` type without
111
+ * one the label becomes the default slot content; a `field` shows it in its form item.
112
+ */
113
+ labelProp?: string;
114
+ /** Props derived from the node itself, beyond `props` — a tab's `value`, say. */
115
+ bind?: (node: ScreenNode) => Record<string, unknown>;
37
116
  }
117
+ export type TypeRegistry = Record<string, TypeEntry>;
118
+ /**
119
+ * Turns `trans::<namespace>::<key>` into words. Receives what follows the marker —
120
+ * `<namespace>::<key>` — and returns the translation, or the key when there is none.
121
+ */
122
+ export type Translate = (key: string) => string;
123
+ /** The values a screen shows and edits, keyed by node `name`. */
124
+ export type ScreenModel = Record<string, unknown>;
125
+ /** Field name -> validation messages, as produced by Laravel's 422 responses. */
126
+ export type ValidationErrors = Record<string, string[]>;
38
127
  /** Normalised, backend-agnostic page of records. */
39
128
  export interface Paginated<T> {
40
129
  items: T[];
@@ -53,11 +142,9 @@ export interface ListQuery {
53
142
  filters?: Record<string, unknown>;
54
143
  search?: string;
55
144
  }
56
- /** Field name -> validation messages, as produced by Laravel's 422 responses. */
57
- export type ValidationErrors = Record<string, string[]>;
58
145
  /**
59
- * Everything the renderer needs from a backend. Implemented per backend, e.g. by
60
- * `@webx-ui/adapter-laravel`.
146
+ * Everything a list screen will need from a backend. Implemented per backend, e.g. by
147
+ * `@webx-ui/adapter-laravel`; nothing in this package calls it yet.
61
148
  */
62
149
  export interface DataAdapter {
63
150
  list<T>(resource: string, query?: ListQuery): Promise<Paginated<T>>;
@@ -66,4 +153,3 @@ export interface DataAdapter {
66
153
  update<T>(resource: string, id: string | number, payload: Record<string, unknown>): Promise<T>;
67
154
  remove(resource: string, id: string | number): Promise<void>;
68
155
  }
69
- //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,10 @@
1
+ import { Patch, ScreenError, ScreenNode } from './types';
2
+ /**
3
+ * Checks a tree the way the JSON schema would, without a schema library: required
4
+ * keys, closed key set, value types, and — what a schema cannot say — unique ids.
5
+ * An empty list means the tree is sound.
6
+ */
7
+ export declare function validateScreen(root: unknown): ScreenError[];
8
+ /** Same for a patch: each operation has the keys its `op` calls for. */
9
+ export declare function validatePatch(patch: unknown): ScreenError[];
10
+ export type { Patch, ScreenNode };
@@ -0,0 +1,4 @@
1
+ import { ScreenModel, ScreenNode, VisibilityCondition } from './types';
2
+ export declare function evaluateCondition(condition: VisibilityCondition, model: ScreenModel): boolean;
3
+ /** Whether a node should render, given the current model. Absent `visible` means yes. */
4
+ export declare function isVisible(node: ScreenNode, model: ScreenModel): boolean;
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@webx-ui/schema",
3
- "version": "0.0.1",
4
- "description": "JSON-driven renderer contracts for WebX UI admin screens (work in progress).",
3
+ "version": "0.1.0",
4
+ "description": "Screens as JSON for WebX UI admin panels: the node format, patches, the type registry and the renderer.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "sideEffects": [
8
+ "*.css"
9
+ ],
7
10
  "keywords": [
8
11
  "webx-ui",
9
12
  "vue",
@@ -16,12 +19,13 @@
16
19
  "url": "git+https://github.com/webx-ui/webx-ui.git",
17
20
  "directory": "packages/schema"
18
21
  },
19
- "homepage": "https://webx-ui.github.io/webx-ui/guide/roadmap.html",
22
+ "homepage": "https://webx-ui.github.io/webx-ui/guide/screens.html",
20
23
  "publishConfig": {
21
24
  "access": "public"
22
25
  },
23
26
  "files": [
24
- "dist"
27
+ "dist",
28
+ "schemas"
25
29
  ],
26
30
  "main": "./dist/index.js",
27
31
  "module": "./dist/index.js",
@@ -31,21 +35,28 @@
31
35
  "types": "./dist/index.d.ts",
32
36
  "import": "./dist/index.js"
33
37
  },
38
+ "./style.css": "./dist/style.css",
39
+ "./schemas/screen.json": "./schemas/screen.schema.json",
40
+ "./schemas/patch.json": "./schemas/patch.schema.json",
34
41
  "./package.json": "./package.json"
35
42
  },
36
43
  "peerDependencies": {
37
44
  "vue": "^3.5.0"
38
45
  },
39
- "peerDependenciesMeta": {
40
- "vue": {
41
- "optional": true
42
- }
46
+ "dependencies": {
47
+ "@webx-ui/core": "^0.17.0"
43
48
  },
44
49
  "devDependencies": {
45
- "typescript": "^5.9.3"
50
+ "@types/node": "^24.10.1",
51
+ "@vitejs/plugin-vue": "^6.0.1",
52
+ "typescript": "^5.9.3",
53
+ "vite": "^7.1.14",
54
+ "vite-plugin-dts": "^4.5.4",
55
+ "vue": "^3.5.24",
56
+ "vue-tsc": "^3.1.3"
46
57
  },
47
58
  "scripts": {
48
- "build": "tsc -p tsconfig.build.json",
49
- "typecheck": "tsc -p tsconfig.json --noEmit"
59
+ "build": "vite build",
60
+ "typecheck": "vue-tsc -p tsconfig.json --noEmit"
50
61
  }
51
62
  }
@@ -0,0 +1,98 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://webx-ui.github.io/webx-ui/schema/patch.json",
4
+ "title": "WebX UI screen patch",
5
+ "description": "An ordered list of operations over a screen tree. Targets are node ids.",
6
+ "type": "array",
7
+ "items": { "$ref": "#/definitions/operation" },
8
+ "definitions": {
9
+ "position": {
10
+ "type": "string",
11
+ "description": "first, last (default), before:<id> or after:<id>",
12
+ "pattern": "^(first|last|before:.+|after:.+)$"
13
+ },
14
+ "operation": {
15
+ "oneOf": [
16
+ {
17
+ "type": "object",
18
+ "required": ["op", "target", "node"],
19
+ "additionalProperties": false,
20
+ "properties": {
21
+ "op": { "const": "add" },
22
+ "target": { "type": "string", "description": "Parent to insert into." },
23
+ "node": {
24
+ "$ref": "https://webx-ui.github.io/webx-ui/schema/screen.json#/definitions/node"
25
+ },
26
+ "position": { "$ref": "#/definitions/position" }
27
+ }
28
+ },
29
+ {
30
+ "type": "object",
31
+ "required": ["op", "target"],
32
+ "additionalProperties": false,
33
+ "properties": {
34
+ "op": { "const": "remove" },
35
+ "target": { "type": "string" }
36
+ }
37
+ },
38
+ {
39
+ "type": "object",
40
+ "required": ["op", "target", "node"],
41
+ "additionalProperties": false,
42
+ "properties": {
43
+ "op": { "const": "replace" },
44
+ "target": { "type": "string" },
45
+ "node": {
46
+ "$ref": "https://webx-ui.github.io/webx-ui/schema/screen.json#/definitions/node"
47
+ }
48
+ }
49
+ },
50
+ {
51
+ "type": "object",
52
+ "required": ["op", "target"],
53
+ "additionalProperties": false,
54
+ "properties": {
55
+ "op": { "const": "move" },
56
+ "target": { "type": "string" },
57
+ "position": { "$ref": "#/definitions/position" },
58
+ "to": { "type": "string", "description": "Id of the new parent." }
59
+ }
60
+ },
61
+ {
62
+ "type": "object",
63
+ "required": ["op", "target"],
64
+ "additionalProperties": false,
65
+ "properties": {
66
+ "op": { "const": "set" },
67
+ "target": { "type": "string" },
68
+ "type": { "type": "string", "minLength": 1 },
69
+ "name": { "type": "string" },
70
+ "label": { "type": "string" },
71
+ "help": { "type": "string" },
72
+ "localized": { "type": "boolean" },
73
+ "props": {
74
+ "type": "object",
75
+ "description": "Merged key by key into the node's props."
76
+ },
77
+ "children": {
78
+ "type": "array",
79
+ "items": {
80
+ "$ref": "https://webx-ui.github.io/webx-ui/schema/screen.json#/definitions/node"
81
+ }
82
+ },
83
+ "slot": { "type": ["string", "null"] },
84
+ "visible": {
85
+ "oneOf": [
86
+ { "type": "boolean" },
87
+ {
88
+ "$ref": "https://webx-ui.github.io/webx-ui/schema/screen.json#/definitions/condition"
89
+ }
90
+ ]
91
+ },
92
+ "can": { "type": ["string", "null"] }
93
+ }
94
+ }
95
+ ]
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,84 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://webx-ui.github.io/webx-ui/schema/screen.json",
4
+ "title": "WebX UI screen",
5
+ "description": "A panel screen described as a tree of nodes. Every node has a stable id — the address patches use — and a type the registry resolves to a component.",
6
+ "type": "object",
7
+ "required": ["screen", "root"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": { "type": "string" },
11
+ "screen": {
12
+ "type": "string",
13
+ "description": "<module>.<screen>, e.g. settings.index",
14
+ "pattern": "^[a-z0-9-]+\\.[a-z0-9-]+$"
15
+ },
16
+ "model": { "type": "string", "description": "What the values belong to." },
17
+ "root": { "type": "array", "items": { "$ref": "#/definitions/node" } }
18
+ },
19
+ "definitions": {
20
+ "node": {
21
+ "type": "object",
22
+ "required": ["id", "type"],
23
+ "additionalProperties": false,
24
+ "properties": {
25
+ "id": {
26
+ "type": "string",
27
+ "minLength": 1,
28
+ "description": "Unique within the screen. Renaming one is a breaking change for every patch."
29
+ },
30
+ "type": {
31
+ "type": "string",
32
+ "minLength": 1,
33
+ "description": "Registry key: the full component name (wx-input) or a project's own type."
34
+ },
35
+ "name": {
36
+ "type": "string",
37
+ "description": "For fields: the key in the model. A literal string — dots are part of the key."
38
+ },
39
+ "label": { "type": "string" },
40
+ "help": { "type": "string", "description": "Hint under a field." },
41
+ "localized": {
42
+ "type": "boolean",
43
+ "description": "The value is a record keyed by content language."
44
+ },
45
+ "props": {
46
+ "type": "object",
47
+ "description": "Passed to the component untouched.",
48
+ "additionalProperties": true
49
+ },
50
+ "children": { "type": "array", "items": { "$ref": "#/definitions/node" } },
51
+ "slot": {
52
+ "type": ["string", "null"],
53
+ "description": "Named slot of the parent to land in."
54
+ },
55
+ "visible": {
56
+ "oneOf": [{ "type": "boolean" }, { "$ref": "#/definitions/condition" }]
57
+ },
58
+ "can": {
59
+ "type": ["string", "null"],
60
+ "description": "Permission required to render the node, e.g. settings.manage."
61
+ }
62
+ }
63
+ },
64
+ "condition": {
65
+ "type": "object",
66
+ "additionalProperties": false,
67
+ "oneOf": [
68
+ { "required": ["when", "is"] },
69
+ { "required": ["when", "in"] },
70
+ { "required": ["when", "not"] },
71
+ { "required": ["all"] },
72
+ { "required": ["any"] }
73
+ ],
74
+ "properties": {
75
+ "when": { "type": "string", "description": "Name of the field the condition reads." },
76
+ "is": {},
77
+ "in": { "type": "array" },
78
+ "not": {},
79
+ "all": { "type": "array", "items": { "$ref": "#/definitions/condition" } },
80
+ "any": { "type": "array", "items": { "$ref": "#/definitions/condition" } }
81
+ }
82
+ }
83
+ }
84
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,WAAW,EACX,SAAS,EACT,SAAS,EACT,UAAU,EACV,gBAAgB,GACjB,MAAM,SAAS,CAAA;AAEhB;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,CAE3F"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js DELETED
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=types.js.map
package/dist/types.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}