@statezero/core 0.2.37 → 0.2.39
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/dist/adaptors/vue/components/LayoutRenderer.js +163 -0
- package/dist/adaptors/vue/components/defaults/AlertElement.js +31 -0
- package/dist/adaptors/vue/components/defaults/DisplayElement.js +44 -0
- package/dist/adaptors/vue/components/defaults/DividerElement.js +10 -0
- package/dist/adaptors/vue/components/defaults/ErrorBlock.js +24 -0
- package/dist/adaptors/vue/components/defaults/GroupElement.js +41 -0
- package/dist/adaptors/vue/components/defaults/LabelElement.js +21 -0
- package/dist/adaptors/vue/components/defaults/TabsElement.js +38 -0
- package/dist/adaptors/vue/components/defaults/index.js +7 -7
- package/dist/adaptors/vue/components/index.js +1 -1
- package/package.json +6 -5
- package/dist/adaptors/vue/components/LayoutRenderer.vue +0 -361
- package/dist/adaptors/vue/components/defaults/AlertElement.vue +0 -38
- package/dist/adaptors/vue/components/defaults/DisplayElement.vue +0 -57
- package/dist/adaptors/vue/components/defaults/DividerElement.vue +0 -13
- package/dist/adaptors/vue/components/defaults/ErrorBlock.vue +0 -28
- package/dist/adaptors/vue/components/defaults/GroupElement.vue +0 -53
- package/dist/adaptors/vue/components/defaults/LabelElement.vue +0 -25
- package/dist/adaptors/vue/components/defaults/TabsElement.vue +0 -54
- package/dist/adaptors/vue/components/layout.tailwind.css +0 -51
- /package/{dist → src}/adaptors/vue/components/layout.css +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { computed as y, provide as q, unref as m, inject as G, onMounted as T, resolveComponent as U, createElementBlock as d, openBlock as l, Fragment as v, createBlock as u, createCommentVNode as g, resolveDynamicComponent as i, normalizeClass as F, renderList as b, withCtx as z, createVNode as H, toDisplayString as M } from "vue";
|
|
2
|
+
const N = {
|
|
3
|
+
key: 11,
|
|
4
|
+
class: "text-red-500 text-sm"
|
|
5
|
+
}, I = {
|
|
6
|
+
__name: "LayoutRenderer",
|
|
7
|
+
props: {
|
|
8
|
+
// The layout element to render (root or nested)
|
|
9
|
+
layout: { type: Object, required: !0 },
|
|
10
|
+
// Schema with field definitions (input_properties/properties)
|
|
11
|
+
schema: { type: Object, default: () => ({}) },
|
|
12
|
+
// Component registry: { Control, Display, Alert, Label, Divider, Group, Tabs, ErrorBlock }
|
|
13
|
+
components: { type: Object, required: !0 },
|
|
14
|
+
// Form data (v-model for Controls)
|
|
15
|
+
formData: { type: Object, default: () => ({}) },
|
|
16
|
+
// Workflow context (for Display elements)
|
|
17
|
+
context: { type: Object, default: () => ({}) },
|
|
18
|
+
// Errors object - can be raw DRF response: { fieldName: ['error'], non_field_errors: ['global error'] }
|
|
19
|
+
// The renderer automatically extracts non_field_errors/__all__ for the ErrorBlock
|
|
20
|
+
errors: { type: Object, default: () => ({}) },
|
|
21
|
+
// Explicit non-field errors (optional override - if not provided, extracted from errors.non_field_errors)
|
|
22
|
+
nonFieldErrors: { type: Array, default: null },
|
|
23
|
+
// Whether this is the root renderer (internal)
|
|
24
|
+
isRoot: { type: Boolean, default: !0 }
|
|
25
|
+
},
|
|
26
|
+
emits: ["update:formData"],
|
|
27
|
+
setup(e, { emit: E }) {
|
|
28
|
+
const t = e, D = E, h = y(() => {
|
|
29
|
+
if (t.nonFieldErrors !== null)
|
|
30
|
+
return t.nonFieldErrors;
|
|
31
|
+
const n = t.errors?.non_field_errors || t.errors?.__all__;
|
|
32
|
+
return n ? Array.isArray(n) ? n.map((o) => typeof o == "string" ? o : o?.message ? o.message : String(o)) : [String(n)] : [];
|
|
33
|
+
}), L = y(() => t.components), j = y(() => t.schema), R = y(() => t.formData), A = y(() => t.context), S = y(() => t.errors);
|
|
34
|
+
t.isRoot && q("layoutRenderer", {
|
|
35
|
+
components: L,
|
|
36
|
+
schema: j,
|
|
37
|
+
formData: R,
|
|
38
|
+
context: A,
|
|
39
|
+
errors: S,
|
|
40
|
+
updateField: (n, o) => {
|
|
41
|
+
const a = m(R) || {};
|
|
42
|
+
D("update:formData", { ...a, [n]: o });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const s = t.isRoot ? null : G("layoutRenderer", null), r = y(() => t.isRoot ? t.components : s && m(s.components) || t.components), p = y(() => t.isRoot ? t.schema : s && m(s.schema) || t.schema), x = y(() => t.isRoot ? t.formData : s && m(s.formData) || t.formData), f = y(() => t.isRoot ? t.context : s && m(s.context) || t.context), B = y(() => t.isRoot ? t.errors : s && m(s.errors) || t.errors), O = t.isRoot ? (n, o) => D("update:formData", { ...t.formData, [n]: o }) : s?.updateField || (() => {
|
|
46
|
+
});
|
|
47
|
+
T(() => {
|
|
48
|
+
t.isRoot;
|
|
49
|
+
});
|
|
50
|
+
function w(n) {
|
|
51
|
+
try {
|
|
52
|
+
return new Function("formData", "context", `return ${n}`)(x.value, f.value);
|
|
53
|
+
} catch (o) {
|
|
54
|
+
return console.warn("[LayoutRenderer] Conditional eval failed:", n, o), !1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function V(n) {
|
|
58
|
+
return (p.value?.properties || p.value?.input_properties || {})[n] || {};
|
|
59
|
+
}
|
|
60
|
+
function $(n) {
|
|
61
|
+
if (n === "non_field_errors" || n === "__all__") return [];
|
|
62
|
+
const o = B.value?.[n];
|
|
63
|
+
return o ? Array.isArray(o) ? o.map((a) => typeof a == "string" ? a : a?.message ? a.message : String(a)) : [String(o)] : [];
|
|
64
|
+
}
|
|
65
|
+
function C(n) {
|
|
66
|
+
if (!n) return;
|
|
67
|
+
const o = n.split(".");
|
|
68
|
+
let a = f.value;
|
|
69
|
+
for (const c of o) {
|
|
70
|
+
if (a == null) return;
|
|
71
|
+
a = a[c];
|
|
72
|
+
}
|
|
73
|
+
return a;
|
|
74
|
+
}
|
|
75
|
+
return (n, o) => {
|
|
76
|
+
const a = U("LayoutRenderer", !0);
|
|
77
|
+
return l(), d(v, null, [
|
|
78
|
+
e.isRoot && h.value.length > 0 && r.value.ErrorBlock ? (l(), u(i(r.value.ErrorBlock), {
|
|
79
|
+
key: 0,
|
|
80
|
+
errors: h.value
|
|
81
|
+
}, null, 8, ["errors"])) : g("", !0),
|
|
82
|
+
e.layout.type === "VerticalLayout" ? (l(), d("div", {
|
|
83
|
+
key: 1,
|
|
84
|
+
class: F(["sz-layout sz-layout-vertical", `sz-gap-${e.layout.gap || "md"}`])
|
|
85
|
+
}, [
|
|
86
|
+
(l(!0), d(v, null, b(e.layout.elements, (c, k) => (l(), u(a, {
|
|
87
|
+
key: k,
|
|
88
|
+
layout: c,
|
|
89
|
+
components: r.value,
|
|
90
|
+
"is-root": !1
|
|
91
|
+
}, null, 8, ["layout", "components"]))), 128))
|
|
92
|
+
], 2)) : e.layout.type === "HorizontalLayout" ? (l(), d("div", {
|
|
93
|
+
key: 2,
|
|
94
|
+
class: F(["sz-layout sz-layout-horizontal", [`sz-gap-${e.layout.gap || "md"}`, `sz-align-${e.layout.align || "start"}`]])
|
|
95
|
+
}, [
|
|
96
|
+
(l(!0), d(v, null, b(e.layout.elements, (c, k) => (l(), u(a, {
|
|
97
|
+
key: k,
|
|
98
|
+
layout: c,
|
|
99
|
+
components: r.value,
|
|
100
|
+
"is-root": !1
|
|
101
|
+
}, null, 8, ["layout", "components"]))), 128))
|
|
102
|
+
], 2)) : e.layout.type === "Group" ? (l(), u(i(r.value.Group), {
|
|
103
|
+
key: 3,
|
|
104
|
+
element: e.layout
|
|
105
|
+
}, {
|
|
106
|
+
default: z(() => [
|
|
107
|
+
e.layout.layout ? (l(), u(a, {
|
|
108
|
+
key: 0,
|
|
109
|
+
layout: e.layout.layout,
|
|
110
|
+
components: r.value,
|
|
111
|
+
"is-root": !1
|
|
112
|
+
}, null, 8, ["layout", "components"])) : g("", !0)
|
|
113
|
+
]),
|
|
114
|
+
_: 1
|
|
115
|
+
}, 8, ["element"])) : e.layout.type === "Tabs" ? (l(), u(i(r.value.Tabs), {
|
|
116
|
+
key: 4,
|
|
117
|
+
element: e.layout
|
|
118
|
+
}, {
|
|
119
|
+
tab: z(({ tab: c }) => [
|
|
120
|
+
H(a, {
|
|
121
|
+
layout: c.layout,
|
|
122
|
+
components: r.value,
|
|
123
|
+
"is-root": !1
|
|
124
|
+
}, null, 8, ["layout", "components"])
|
|
125
|
+
]),
|
|
126
|
+
_: 1
|
|
127
|
+
}, 8, ["element"])) : e.layout.type === "Conditional" ? (l(), d(v, { key: 5 }, [
|
|
128
|
+
w(e.layout.when) ? (l(), u(a, {
|
|
129
|
+
key: 0,
|
|
130
|
+
layout: e.layout.layout,
|
|
131
|
+
components: r.value,
|
|
132
|
+
"is-root": !1
|
|
133
|
+
}, null, 8, ["layout", "components"])) : g("", !0)
|
|
134
|
+
], 64)) : e.layout.type === "Control" ? (l(), u(i(r.value.Control), {
|
|
135
|
+
key: 6,
|
|
136
|
+
element: e.layout,
|
|
137
|
+
"model-value": x.value[e.layout.field_name],
|
|
138
|
+
errors: $(e.layout.field_name),
|
|
139
|
+
context: f.value,
|
|
140
|
+
"form-data": x.value,
|
|
141
|
+
schema: V(e.layout.field_name),
|
|
142
|
+
"onUpdate:modelValue": o[0] || (o[0] = (c) => m(O)(e.layout.field_name, c))
|
|
143
|
+
}, null, 8, ["element", "model-value", "errors", "context", "form-data", "schema"])) : e.layout.type === "Display" ? (l(), u(i(r.value.Display), {
|
|
144
|
+
key: 7,
|
|
145
|
+
element: e.layout,
|
|
146
|
+
context: f.value,
|
|
147
|
+
value: C(e.layout.context_path)
|
|
148
|
+
}, null, 8, ["element", "context", "value"])) : e.layout.type === "Alert" ? (l(), u(i(r.value.Alert), {
|
|
149
|
+
key: 8,
|
|
150
|
+
element: e.layout,
|
|
151
|
+
context: f.value,
|
|
152
|
+
text: e.layout.text || C(e.layout.context_path)
|
|
153
|
+
}, null, 8, ["element", "context", "text"])) : e.layout.type === "Label" ? (l(), u(i(r.value.Label), {
|
|
154
|
+
key: 9,
|
|
155
|
+
element: e.layout
|
|
156
|
+
}, null, 8, ["element"])) : e.layout.type === "Divider" ? (l(), u(i(r.value.Divider), { key: 10 })) : (l(), d("div", N, " [LayoutRenderer] Unknown element type: " + M(e.layout.type), 1))
|
|
157
|
+
], 64);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
export {
|
|
162
|
+
I as default
|
|
163
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createElementBlock as o, openBlock as l, normalizeClass as c, createElementVNode as s, toDisplayString as n } from "vue";
|
|
2
|
+
const i = { class: "flex-shrink-0" }, a = { class: "text-sm" }, g = {
|
|
3
|
+
__name: "AlertElement",
|
|
4
|
+
props: {
|
|
5
|
+
element: { type: Object, required: !0 },
|
|
6
|
+
context: { type: Object, default: () => ({}) },
|
|
7
|
+
text: { type: String, default: "" }
|
|
8
|
+
},
|
|
9
|
+
setup(e) {
|
|
10
|
+
const t = {
|
|
11
|
+
info: "bg-blue-500/10 border-blue-500/20 text-blue-400",
|
|
12
|
+
warning: "bg-yellow-500/10 border-yellow-500/20 text-yellow-400",
|
|
13
|
+
error: "bg-red-500/10 border-red-500/20 text-red-400",
|
|
14
|
+
success: "bg-green-500/10 border-green-500/20 text-green-400"
|
|
15
|
+
}, r = {
|
|
16
|
+
info: "ℹ️",
|
|
17
|
+
warning: "⚠️",
|
|
18
|
+
error: "❌",
|
|
19
|
+
success: "✓"
|
|
20
|
+
};
|
|
21
|
+
return (d, b) => (l(), o("div", {
|
|
22
|
+
class: c(["flex items-start gap-3 p-3 rounded-lg border", t[e.element.severity] || t.info])
|
|
23
|
+
}, [
|
|
24
|
+
s("span", i, n(r[e.element.severity] || r.info), 1),
|
|
25
|
+
s("p", a, n(e.text), 1)
|
|
26
|
+
], 2));
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
export {
|
|
30
|
+
g as default
|
|
31
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createElementBlock as t, openBlock as n, createCommentVNode as c, createElementVNode as o, toDisplayString as l, Fragment as s } from "vue";
|
|
2
|
+
const m = { class: "space-y-1" }, u = {
|
|
3
|
+
key: 0,
|
|
4
|
+
class: "block text-sm font-medium text-muted-foreground"
|
|
5
|
+
}, i = { class: "text-sm" }, r = { key: 0 }, y = {
|
|
6
|
+
key: 1,
|
|
7
|
+
class: "text-muted-foreground italic"
|
|
8
|
+
}, x = {
|
|
9
|
+
key: 1,
|
|
10
|
+
class: "px-2 py-1 bg-muted rounded font-mono text-sm"
|
|
11
|
+
}, f = {
|
|
12
|
+
key: 2,
|
|
13
|
+
class: "flex items-center gap-2"
|
|
14
|
+
}, v = { class: "flex-1 px-2 py-1 bg-muted rounded font-mono text-xs truncate" }, b = { class: "text-xs text-muted-foreground ml-2" }, k = {
|
|
15
|
+
__name: "DisplayElement",
|
|
16
|
+
props: {
|
|
17
|
+
element: { type: Object, required: !0 },
|
|
18
|
+
context: { type: Object, default: () => ({}) },
|
|
19
|
+
value: { default: null }
|
|
20
|
+
},
|
|
21
|
+
setup(e) {
|
|
22
|
+
return (d, a) => (n(), t("div", m, [
|
|
23
|
+
e.element.label ? (n(), t("label", u, l(e.element.label), 1)) : c("", !0),
|
|
24
|
+
o("div", i, [
|
|
25
|
+
!e.element.display_component || e.element.display_component === "text" ? (n(), t(s, { key: 0 }, [
|
|
26
|
+
e.value !== null && e.value !== void 0 ? (n(), t("span", r, l(e.value), 1)) : (n(), t("span", y, "—"))
|
|
27
|
+
], 64)) : e.element.display_component === "code" ? (n(), t("code", x, l(e.value), 1)) : e.element.display_component === "copy-url" ? (n(), t("div", f, [
|
|
28
|
+
o("code", v, l(e.value), 1),
|
|
29
|
+
o("button", {
|
|
30
|
+
type: "button",
|
|
31
|
+
class: "px-2 py-1 text-xs bg-primary text-primary-foreground rounded hover:bg-primary/90",
|
|
32
|
+
onClick: a[0] || (a[0] = (p) => d.navigator.clipboard.writeText(e.value))
|
|
33
|
+
}, " Copy ")
|
|
34
|
+
])) : (n(), t(s, { key: 3 }, [
|
|
35
|
+
o("span", null, l(e.value), 1),
|
|
36
|
+
o("span", b, "(" + l(e.element.display_component) + ")", 1)
|
|
37
|
+
], 64))
|
|
38
|
+
])
|
|
39
|
+
]));
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
export {
|
|
43
|
+
k as default
|
|
44
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createElementBlock as e, createCommentVNode as d, openBlock as r, createElementVNode as o, Fragment as l, renderList as c, toDisplayString as i } from "vue";
|
|
2
|
+
const m = {
|
|
3
|
+
key: 0,
|
|
4
|
+
class: "flex items-start gap-3 p-3 rounded-lg border bg-red-500/10 border-red-500/20"
|
|
5
|
+
}, p = { class: "space-y-1" }, g = {
|
|
6
|
+
__name: "ErrorBlock",
|
|
7
|
+
props: {
|
|
8
|
+
errors: { type: Array, required: !0 }
|
|
9
|
+
},
|
|
10
|
+
setup(t) {
|
|
11
|
+
return (u, s) => t.errors.length > 0 ? (r(), e("div", m, [
|
|
12
|
+
s[0] || (s[0] = o("span", { class: "flex-shrink-0 text-red-400" }, "❌", -1)),
|
|
13
|
+
o("div", p, [
|
|
14
|
+
(r(!0), e(l, null, c(t.errors, (n, a) => (r(), e("p", {
|
|
15
|
+
key: a,
|
|
16
|
+
class: "text-sm text-red-400"
|
|
17
|
+
}, i(n), 1))), 128))
|
|
18
|
+
])
|
|
19
|
+
])) : d("", !0);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
export {
|
|
23
|
+
g as default
|
|
24
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ref as a, createElementBlock as o, openBlock as s, createElementVNode as t, withDirectives as m, normalizeClass as u, createCommentVNode as c, toDisplayString as n, renderSlot as p, vShow as f } from "vue";
|
|
2
|
+
const v = { class: "border border-border rounded-lg overflow-hidden" }, h = { class: "flex items-center justify-between" }, b = { class: "font-medium" }, g = {
|
|
3
|
+
key: 0,
|
|
4
|
+
class: "text-sm text-muted-foreground mt-0.5"
|
|
5
|
+
}, _ = {
|
|
6
|
+
key: 0,
|
|
7
|
+
class: "text-muted-foreground"
|
|
8
|
+
}, x = { class: "p-4" }, C = {
|
|
9
|
+
__name: "GroupElement",
|
|
10
|
+
props: {
|
|
11
|
+
element: { type: Object, required: !0 }
|
|
12
|
+
},
|
|
13
|
+
setup(e) {
|
|
14
|
+
const r = e, l = a(r.element.collapsed || !1);
|
|
15
|
+
function i() {
|
|
16
|
+
r.element.collapsible && (l.value = !l.value);
|
|
17
|
+
}
|
|
18
|
+
return (d, y) => (s(), o("div", v, [
|
|
19
|
+
t("div", {
|
|
20
|
+
class: u(["px-4 py-3 bg-muted/50", { "cursor-pointer hover:bg-muted/70": e.element.collapsible }]),
|
|
21
|
+
onClick: i
|
|
22
|
+
}, [
|
|
23
|
+
t("div", h, [
|
|
24
|
+
t("div", null, [
|
|
25
|
+
t("h4", b, n(e.element.label), 1),
|
|
26
|
+
e.element.description ? (s(), o("p", g, n(e.element.description), 1)) : c("", !0)
|
|
27
|
+
]),
|
|
28
|
+
e.element.collapsible ? (s(), o("span", _, n(l.value ? "▶" : "▼"), 1)) : c("", !0)
|
|
29
|
+
])
|
|
30
|
+
], 2),
|
|
31
|
+
m(t("div", x, [
|
|
32
|
+
p(d.$slots, "default")
|
|
33
|
+
], 512), [
|
|
34
|
+
[f, !l.value]
|
|
35
|
+
])
|
|
36
|
+
]));
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
export {
|
|
40
|
+
C as default
|
|
41
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createElementBlock as n, openBlock as a, normalizeClass as o, toDisplayString as r } from "vue";
|
|
2
|
+
const i = {
|
|
3
|
+
__name: "LabelElement",
|
|
4
|
+
props: {
|
|
5
|
+
element: { type: Object, required: !0 }
|
|
6
|
+
},
|
|
7
|
+
setup(e) {
|
|
8
|
+
const t = {
|
|
9
|
+
heading: "text-lg font-semibold",
|
|
10
|
+
subheading: "text-base font-medium text-muted-foreground",
|
|
11
|
+
body: "text-sm",
|
|
12
|
+
caption: "text-xs text-muted-foreground"
|
|
13
|
+
};
|
|
14
|
+
return (s, l) => (a(), n("p", {
|
|
15
|
+
class: o(t[e.element.variant] || t.body)
|
|
16
|
+
}, r(e.element.text), 3));
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
export {
|
|
20
|
+
i as default
|
|
21
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ref as b, createElementBlock as t, openBlock as r, createElementVNode as a, Fragment as c, renderList as u, normalizeClass as m, toDisplayString as d, withDirectives as p, renderSlot as v, vShow as f } from "vue";
|
|
2
|
+
const y = { class: "flex border-b border-border" }, _ = ["onClick"], h = { class: "pt-4" }, E = {
|
|
3
|
+
__name: "TabsElement",
|
|
4
|
+
props: {
|
|
5
|
+
element: { type: Object, required: !0 }
|
|
6
|
+
},
|
|
7
|
+
setup(o) {
|
|
8
|
+
const n = b(o.element.default_tab || 0);
|
|
9
|
+
function i(l) {
|
|
10
|
+
n.value = l;
|
|
11
|
+
}
|
|
12
|
+
return (l, x) => (r(), t("div", null, [
|
|
13
|
+
a("div", y, [
|
|
14
|
+
(r(!0), t(c, null, u(o.element.tabs, (s, e) => (r(), t("button", {
|
|
15
|
+
key: e,
|
|
16
|
+
type: "button",
|
|
17
|
+
class: m(["px-4 py-2 text-sm font-medium transition-colors", [
|
|
18
|
+
n.value === e ? "text-primary border-b-2 border-primary -mb-px" : "text-muted-foreground hover:text-foreground"
|
|
19
|
+
]]),
|
|
20
|
+
onClick: (g) => i(e)
|
|
21
|
+
}, d(s.label), 11, _))), 128))
|
|
22
|
+
]),
|
|
23
|
+
a("div", h, [
|
|
24
|
+
(r(!0), t(c, null, u(o.element.tabs, (s, e) => p((r(), t("div", { key: e }, [
|
|
25
|
+
v(l.$slots, "tab", {
|
|
26
|
+
tab: s,
|
|
27
|
+
index: e
|
|
28
|
+
})
|
|
29
|
+
], 512)), [
|
|
30
|
+
[f, n.value === e]
|
|
31
|
+
])), 128))
|
|
32
|
+
])
|
|
33
|
+
]));
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
export {
|
|
37
|
+
E as default
|
|
38
|
+
};
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* These are minimal, unstyled implementations that follow the contracts.
|
|
5
5
|
* Users can use these as-is or as reference for custom implementations.
|
|
6
6
|
*/
|
|
7
|
-
export { default as AlertElement } from './AlertElement.
|
|
8
|
-
export { default as LabelElement } from './LabelElement.
|
|
9
|
-
export { default as DividerElement } from './DividerElement.
|
|
10
|
-
export { default as DisplayElement } from './DisplayElement.
|
|
11
|
-
export { default as GroupElement } from './GroupElement.
|
|
12
|
-
export { default as TabsElement } from './TabsElement.
|
|
13
|
-
export { default as ErrorBlock } from './ErrorBlock.
|
|
7
|
+
export { default as AlertElement } from './AlertElement.js';
|
|
8
|
+
export { default as LabelElement } from './LabelElement.js';
|
|
9
|
+
export { default as DividerElement } from './DividerElement.js';
|
|
10
|
+
export { default as DisplayElement } from './DisplayElement.js';
|
|
11
|
+
export { default as GroupElement } from './GroupElement.js';
|
|
12
|
+
export { default as TabsElement } from './TabsElement.js';
|
|
13
|
+
export { default as ErrorBlock } from './ErrorBlock.js';
|
|
14
14
|
/**
|
|
15
15
|
* Create a default components registry (without Control - user must provide)
|
|
16
16
|
*
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* Vue components for StateZero
|
|
3
3
|
*/
|
|
4
4
|
// Main layout renderer
|
|
5
|
-
export { default as LayoutRenderer } from './LayoutRenderer.
|
|
5
|
+
export { default as LayoutRenderer } from './LayoutRenderer.js';
|
|
6
6
|
// Default component implementations
|
|
7
7
|
export { AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents } from './defaults/index.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statezero/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.39",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"module": "ESNext",
|
|
6
6
|
"description": "The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate",
|
|
@@ -26,8 +26,7 @@
|
|
|
26
26
|
"import": "./dist/vue-entry.js",
|
|
27
27
|
"require": "./dist/vue-entry.js"
|
|
28
28
|
},
|
|
29
|
-
"./vue/layout.css": "./
|
|
30
|
-
"./vue/layout.tailwind.css": "./dist/adaptors/vue/components/layout.tailwind.css",
|
|
29
|
+
"./vue/layout.css": "./src/adaptors/vue/components/layout.css",
|
|
31
30
|
"./testing": {
|
|
32
31
|
"import": "./dist/testing.js",
|
|
33
32
|
"require": "./dist/testing.js"
|
|
@@ -42,8 +41,7 @@
|
|
|
42
41
|
"generate:test-apps": "ts-node scripts/generate-test-apps.js",
|
|
43
42
|
"test:adaptors": "playwright test tests/adaptors",
|
|
44
43
|
"test:coverage": "vitest run --coverage",
|
|
45
|
-
"build": "tsc &&
|
|
46
|
-
"copy-vue": "cp -r src/adaptors/vue/components/*.vue dist/adaptors/vue/components/ && cp -r src/adaptors/vue/components/defaults/*.vue dist/adaptors/vue/components/defaults/ && cp src/adaptors/vue/components/*.css dist/adaptors/vue/components/",
|
|
44
|
+
"build": "tsc && vite build",
|
|
47
45
|
"parse-queries": "node scripts/perfect-query-parser.js",
|
|
48
46
|
"sync": "node src/cli/index.js sync",
|
|
49
47
|
"sync:dev": "npx cross-env NODE_ENV=test npm run sync",
|
|
@@ -76,6 +74,7 @@
|
|
|
76
74
|
},
|
|
77
75
|
"files": [
|
|
78
76
|
"dist",
|
|
77
|
+
"src/adaptors/vue/components/layout.css",
|
|
79
78
|
"LICENSE",
|
|
80
79
|
"README.md"
|
|
81
80
|
],
|
|
@@ -114,6 +113,7 @@
|
|
|
114
113
|
"@types/node": "^22.13.1",
|
|
115
114
|
"@types/react": "^18.3.18",
|
|
116
115
|
"@types/yargs": "^17.0.32",
|
|
116
|
+
"@vitejs/plugin-vue": "^6.0.4",
|
|
117
117
|
"@vitest/coverage-v8": "^3.0.5",
|
|
118
118
|
"fake-indexeddb": "^6.0.0",
|
|
119
119
|
"fast-glob": "^3.3.3",
|
|
@@ -121,6 +121,7 @@
|
|
|
121
121
|
"rimraf": "^5.0.5",
|
|
122
122
|
"ts-node": "^10.9.2",
|
|
123
123
|
"typescript": "^5.7.3",
|
|
124
|
+
"vite": "^7.3.1",
|
|
124
125
|
"vitest": "^3.0.5",
|
|
125
126
|
"vue": "^3.2.0"
|
|
126
127
|
},
|
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { computed, provide, inject, onMounted, unref } from 'vue'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* LayoutRenderer - Renders a statezero layout tree
|
|
6
|
-
*
|
|
7
|
-
* Expects a components registry with implementations for each element type.
|
|
8
|
-
* Components must follow contracts (validated at mount time).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const props = defineProps({
|
|
12
|
-
// The layout element to render (root or nested)
|
|
13
|
-
layout: { type: Object, required: true },
|
|
14
|
-
|
|
15
|
-
// Schema with field definitions (input_properties/properties)
|
|
16
|
-
schema: { type: Object, default: () => ({}) },
|
|
17
|
-
|
|
18
|
-
// Component registry: { Control, Display, Alert, Label, Divider, Group, Tabs, ErrorBlock }
|
|
19
|
-
components: { type: Object, required: true },
|
|
20
|
-
|
|
21
|
-
// Form data (v-model for Controls)
|
|
22
|
-
formData: { type: Object, default: () => ({}) },
|
|
23
|
-
|
|
24
|
-
// Workflow context (for Display elements)
|
|
25
|
-
context: { type: Object, default: () => ({}) },
|
|
26
|
-
|
|
27
|
-
// Errors object - can be raw DRF response: { fieldName: ['error'], non_field_errors: ['global error'] }
|
|
28
|
-
// The renderer automatically extracts non_field_errors/__all__ for the ErrorBlock
|
|
29
|
-
errors: { type: Object, default: () => ({}) },
|
|
30
|
-
|
|
31
|
-
// Explicit non-field errors (optional override - if not provided, extracted from errors.non_field_errors)
|
|
32
|
-
nonFieldErrors: { type: Array, default: null },
|
|
33
|
-
|
|
34
|
-
// Whether this is the root renderer (internal)
|
|
35
|
-
isRoot: { type: Boolean, default: true }
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
const emit = defineEmits(['update:formData'])
|
|
39
|
-
|
|
40
|
-
// Extract non-field errors from the errors object (DRF format)
|
|
41
|
-
// Supports both "non_field_errors" and "__all__" keys
|
|
42
|
-
const extractedNonFieldErrors = computed(() => {
|
|
43
|
-
// If explicit nonFieldErrors prop provided, use it
|
|
44
|
-
if (props.nonFieldErrors !== null) {
|
|
45
|
-
return props.nonFieldErrors
|
|
46
|
-
}
|
|
47
|
-
// Otherwise extract from errors object
|
|
48
|
-
const nfe = props.errors?.non_field_errors || props.errors?.__all__
|
|
49
|
-
if (!nfe) return []
|
|
50
|
-
if (Array.isArray(nfe)) {
|
|
51
|
-
return nfe.map(err => {
|
|
52
|
-
if (typeof err === 'string') return err
|
|
53
|
-
if (err?.message) return err.message
|
|
54
|
-
return String(err)
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
return [String(nfe)]
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
const rootComponents = computed(() => props.components)
|
|
61
|
-
const rootSchema = computed(() => props.schema)
|
|
62
|
-
const rootFormData = computed(() => props.formData)
|
|
63
|
-
const rootContext = computed(() => props.context)
|
|
64
|
-
const rootErrors = computed(() => props.errors)
|
|
65
|
-
|
|
66
|
-
// Provide context to nested renderers
|
|
67
|
-
if (props.isRoot) {
|
|
68
|
-
provide('layoutRenderer', {
|
|
69
|
-
components: rootComponents,
|
|
70
|
-
schema: rootSchema,
|
|
71
|
-
formData: rootFormData,
|
|
72
|
-
context: rootContext,
|
|
73
|
-
errors: rootErrors,
|
|
74
|
-
updateField: (fieldName, value) => {
|
|
75
|
-
const currentFormData = unref(rootFormData) || {}
|
|
76
|
-
emit('update:formData', { ...currentFormData, [fieldName]: value })
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Inject from parent if not root
|
|
82
|
-
const injected = props.isRoot ? null : inject('layoutRenderer', null)
|
|
83
|
-
const components = computed(() => props.isRoot ? props.components : (injected ? (unref(injected.components) || props.components) : props.components))
|
|
84
|
-
const schema = computed(() => props.isRoot ? props.schema : (injected ? (unref(injected.schema) || props.schema) : props.schema))
|
|
85
|
-
const formData = computed(() => props.isRoot ? props.formData : (injected ? (unref(injected.formData) || props.formData) : props.formData))
|
|
86
|
-
const context = computed(() => props.isRoot ? props.context : (injected ? (unref(injected.context) || props.context) : props.context))
|
|
87
|
-
const errors = computed(() => props.isRoot ? props.errors : (injected ? (unref(injected.errors) || props.errors) : props.errors))
|
|
88
|
-
const updateField = props.isRoot
|
|
89
|
-
? (fieldName, value) => emit('update:formData', { ...props.formData, [fieldName]: value })
|
|
90
|
-
: injected?.updateField || (() => {})
|
|
91
|
-
|
|
92
|
-
// Contract definitions for component validation
|
|
93
|
-
//
|
|
94
|
-
// Control: Polymorphic form field component (like AutoField)
|
|
95
|
-
// - Should check element.display_component for custom component override
|
|
96
|
-
// - Should use schema (type/format) to determine default field rendering
|
|
97
|
-
// - Receives: element (includes field_name, display_component, label, extra, etc.)
|
|
98
|
-
// modelValue, errors (array), context, formData, schema (field schema)
|
|
99
|
-
// - Emits: update:modelValue
|
|
100
|
-
//
|
|
101
|
-
// Display: Read-only component for showing context values
|
|
102
|
-
// - Receives: element (includes context_path, label, display_component), context, value (resolved)
|
|
103
|
-
// - No v-model binding, just displays value
|
|
104
|
-
//
|
|
105
|
-
const CONTRACTS = {
|
|
106
|
-
Control: {
|
|
107
|
-
props: ['element', 'modelValue', 'errors', 'context', 'formData', 'schema'],
|
|
108
|
-
emits: ['update:modelValue']
|
|
109
|
-
},
|
|
110
|
-
Display: {
|
|
111
|
-
props: ['element', 'context', 'value'],
|
|
112
|
-
emits: []
|
|
113
|
-
},
|
|
114
|
-
Alert: {
|
|
115
|
-
props: ['element', 'context', 'text'],
|
|
116
|
-
emits: []
|
|
117
|
-
},
|
|
118
|
-
Label: {
|
|
119
|
-
props: ['element'],
|
|
120
|
-
emits: []
|
|
121
|
-
},
|
|
122
|
-
Divider: {
|
|
123
|
-
props: [],
|
|
124
|
-
emits: []
|
|
125
|
-
},
|
|
126
|
-
Group: {
|
|
127
|
-
props: ['element'],
|
|
128
|
-
emits: []
|
|
129
|
-
},
|
|
130
|
-
Tabs: {
|
|
131
|
-
props: ['element'],
|
|
132
|
-
emits: []
|
|
133
|
-
},
|
|
134
|
-
ErrorBlock: {
|
|
135
|
-
props: ['errors'],
|
|
136
|
-
emits: []
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Validate components on mount (dev warning only)
|
|
141
|
-
function validateComponents(comps) {
|
|
142
|
-
for (const [type, component] of Object.entries(comps)) {
|
|
143
|
-
const contract = CONTRACTS[type]
|
|
144
|
-
if (!contract) continue
|
|
145
|
-
if (!component) {
|
|
146
|
-
console.warn(`[LayoutRenderer] Missing component for type: ${type}`)
|
|
147
|
-
continue
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Get component props (handle different formats)
|
|
151
|
-
let componentProps = []
|
|
152
|
-
if (component.props) {
|
|
153
|
-
if (Array.isArray(component.props)) {
|
|
154
|
-
componentProps = component.props
|
|
155
|
-
} else if (typeof component.props === 'object') {
|
|
156
|
-
componentProps = Object.keys(component.props)
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Get component emits
|
|
161
|
-
let componentEmits = []
|
|
162
|
-
if (component.emits) {
|
|
163
|
-
if (Array.isArray(component.emits)) {
|
|
164
|
-
componentEmits = component.emits
|
|
165
|
-
} else if (typeof component.emits === 'object') {
|
|
166
|
-
componentEmits = Object.keys(component.emits)
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Check required props
|
|
171
|
-
for (const prop of contract.props) {
|
|
172
|
-
if (!componentProps.includes(prop)) {
|
|
173
|
-
console.warn(`[LayoutRenderer] ${type} component missing required prop: "${prop}"`)
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Check required emits
|
|
178
|
-
for (const emitName of contract.emits) {
|
|
179
|
-
if (!componentEmits.includes(emitName)) {
|
|
180
|
-
console.warn(`[LayoutRenderer] ${type} component missing required emit: "${emitName}"`)
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
onMounted(() => {
|
|
187
|
-
if (props.isRoot && import.meta.env?.DEV) {
|
|
188
|
-
validateComponents(props.components)
|
|
189
|
-
}
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
// Evaluate conditional expressions
|
|
193
|
-
function evalCondition(expr) {
|
|
194
|
-
try {
|
|
195
|
-
return new Function('formData', 'context', `return ${expr}`)(formData.value, context.value)
|
|
196
|
-
} catch (e) {
|
|
197
|
-
console.warn('[LayoutRenderer] Conditional eval failed:', expr, e)
|
|
198
|
-
return false
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Get field schema from schema.properties or schema.input_properties
|
|
203
|
-
function getFieldSchema(fieldName) {
|
|
204
|
-
const props = schema.value?.properties || schema.value?.input_properties || {}
|
|
205
|
-
return props[fieldName] || {}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Get errors for a specific field (excludes non_field_errors/__all__)
|
|
209
|
-
function getFieldErrors(fieldName) {
|
|
210
|
-
if (fieldName === 'non_field_errors' || fieldName === '__all__') return []
|
|
211
|
-
const fieldErrors = errors.value?.[fieldName]
|
|
212
|
-
if (!fieldErrors) return []
|
|
213
|
-
// Normalize to array of strings (DRF can return array of strings or objects with message)
|
|
214
|
-
if (Array.isArray(fieldErrors)) {
|
|
215
|
-
return fieldErrors.map(err => {
|
|
216
|
-
if (typeof err === 'string') return err
|
|
217
|
-
if (err?.message) return err.message
|
|
218
|
-
return String(err)
|
|
219
|
-
})
|
|
220
|
-
}
|
|
221
|
-
return [String(fieldErrors)]
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Resolve context path (dot notation) to value
|
|
225
|
-
function resolveContextPath(path) {
|
|
226
|
-
if (!path) return undefined
|
|
227
|
-
const parts = path.split('.')
|
|
228
|
-
let value = context.value
|
|
229
|
-
for (const part of parts) {
|
|
230
|
-
if (value === null || value === undefined) return undefined
|
|
231
|
-
value = value[part]
|
|
232
|
-
}
|
|
233
|
-
return value
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
</script>
|
|
237
|
-
|
|
238
|
-
<template>
|
|
239
|
-
<!-- ErrorBlock at root level -->
|
|
240
|
-
<component
|
|
241
|
-
v-if="isRoot && extractedNonFieldErrors.length > 0 && components.ErrorBlock"
|
|
242
|
-
:is="components.ErrorBlock"
|
|
243
|
-
:errors="extractedNonFieldErrors"
|
|
244
|
-
/>
|
|
245
|
-
|
|
246
|
-
<!-- VerticalLayout -->
|
|
247
|
-
<div
|
|
248
|
-
v-if="layout.type === 'VerticalLayout'"
|
|
249
|
-
class="sz-layout sz-layout-vertical"
|
|
250
|
-
:class="`sz-gap-${layout.gap || 'md'}`"
|
|
251
|
-
>
|
|
252
|
-
<LayoutRenderer
|
|
253
|
-
v-for="(child, i) in layout.elements"
|
|
254
|
-
:key="i"
|
|
255
|
-
:layout="child"
|
|
256
|
-
:components="components"
|
|
257
|
-
:is-root="false"
|
|
258
|
-
/>
|
|
259
|
-
</div>
|
|
260
|
-
|
|
261
|
-
<!-- HorizontalLayout -->
|
|
262
|
-
<div
|
|
263
|
-
v-else-if="layout.type === 'HorizontalLayout'"
|
|
264
|
-
class="sz-layout sz-layout-horizontal"
|
|
265
|
-
:class="[`sz-gap-${layout.gap || 'md'}`, `sz-align-${layout.align || 'start'}`]"
|
|
266
|
-
>
|
|
267
|
-
<LayoutRenderer
|
|
268
|
-
v-for="(child, i) in layout.elements"
|
|
269
|
-
:key="i"
|
|
270
|
-
:layout="child"
|
|
271
|
-
:components="components"
|
|
272
|
-
:is-root="false"
|
|
273
|
-
/>
|
|
274
|
-
</div>
|
|
275
|
-
|
|
276
|
-
<!-- Group -->
|
|
277
|
-
<component
|
|
278
|
-
v-else-if="layout.type === 'Group'"
|
|
279
|
-
:is="components.Group"
|
|
280
|
-
:element="layout"
|
|
281
|
-
>
|
|
282
|
-
<LayoutRenderer
|
|
283
|
-
v-if="layout.layout"
|
|
284
|
-
:layout="layout.layout"
|
|
285
|
-
:components="components"
|
|
286
|
-
:is-root="false"
|
|
287
|
-
/>
|
|
288
|
-
</component>
|
|
289
|
-
|
|
290
|
-
<!-- Tabs -->
|
|
291
|
-
<component
|
|
292
|
-
v-else-if="layout.type === 'Tabs'"
|
|
293
|
-
:is="components.Tabs"
|
|
294
|
-
:element="layout"
|
|
295
|
-
>
|
|
296
|
-
<template #tab="{ tab }">
|
|
297
|
-
<LayoutRenderer
|
|
298
|
-
:layout="tab.layout"
|
|
299
|
-
:components="components"
|
|
300
|
-
:is-root="false"
|
|
301
|
-
/>
|
|
302
|
-
</template>
|
|
303
|
-
</component>
|
|
304
|
-
|
|
305
|
-
<!-- Conditional -->
|
|
306
|
-
<LayoutRenderer
|
|
307
|
-
v-else-if="layout.type === 'Conditional' && evalCondition(layout.when)"
|
|
308
|
-
:layout="layout.layout"
|
|
309
|
-
:components="components"
|
|
310
|
-
:is-root="false"
|
|
311
|
-
/>
|
|
312
|
-
|
|
313
|
-
<!-- Control (form field) -->
|
|
314
|
-
<component
|
|
315
|
-
v-else-if="layout.type === 'Control'"
|
|
316
|
-
:is="components.Control"
|
|
317
|
-
:element="layout"
|
|
318
|
-
:model-value="formData[layout.field_name]"
|
|
319
|
-
:errors="getFieldErrors(layout.field_name)"
|
|
320
|
-
:context="context"
|
|
321
|
-
:form-data="formData"
|
|
322
|
-
:schema="getFieldSchema(layout.field_name)"
|
|
323
|
-
@update:model-value="updateField(layout.field_name, $event)"
|
|
324
|
-
/>
|
|
325
|
-
|
|
326
|
-
<!-- Display -->
|
|
327
|
-
<component
|
|
328
|
-
v-else-if="layout.type === 'Display'"
|
|
329
|
-
:is="components.Display"
|
|
330
|
-
:element="layout"
|
|
331
|
-
:context="context"
|
|
332
|
-
:value="resolveContextPath(layout.context_path)"
|
|
333
|
-
/>
|
|
334
|
-
|
|
335
|
-
<!-- Alert -->
|
|
336
|
-
<component
|
|
337
|
-
v-else-if="layout.type === 'Alert'"
|
|
338
|
-
:is="components.Alert"
|
|
339
|
-
:element="layout"
|
|
340
|
-
:context="context"
|
|
341
|
-
:text="layout.text || resolveContextPath(layout.context_path)"
|
|
342
|
-
/>
|
|
343
|
-
|
|
344
|
-
<!-- Label -->
|
|
345
|
-
<component
|
|
346
|
-
v-else-if="layout.type === 'Label'"
|
|
347
|
-
:is="components.Label"
|
|
348
|
-
:element="layout"
|
|
349
|
-
/>
|
|
350
|
-
|
|
351
|
-
<!-- Divider -->
|
|
352
|
-
<component
|
|
353
|
-
v-else-if="layout.type === 'Divider'"
|
|
354
|
-
:is="components.Divider"
|
|
355
|
-
/>
|
|
356
|
-
|
|
357
|
-
<!-- Unknown type warning -->
|
|
358
|
-
<div v-else class="text-red-500 text-sm">
|
|
359
|
-
[LayoutRenderer] Unknown element type: {{ layout.type }}
|
|
360
|
-
</div>
|
|
361
|
-
</template>
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
/**
|
|
3
|
-
* Default Alert element for LayoutRenderer
|
|
4
|
-
*
|
|
5
|
-
* Contract:
|
|
6
|
-
* - props: element, context, text
|
|
7
|
-
* - emits: none
|
|
8
|
-
*/
|
|
9
|
-
defineProps({
|
|
10
|
-
element: { type: Object, required: true },
|
|
11
|
-
context: { type: Object, default: () => ({}) },
|
|
12
|
-
text: { type: String, default: '' }
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
const severityClasses = {
|
|
16
|
-
info: 'bg-blue-500/10 border-blue-500/20 text-blue-400',
|
|
17
|
-
warning: 'bg-yellow-500/10 border-yellow-500/20 text-yellow-400',
|
|
18
|
-
error: 'bg-red-500/10 border-red-500/20 text-red-400',
|
|
19
|
-
success: 'bg-green-500/10 border-green-500/20 text-green-400'
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const severityIcons = {
|
|
23
|
-
info: 'ℹ️',
|
|
24
|
-
warning: '⚠️',
|
|
25
|
-
error: '❌',
|
|
26
|
-
success: '✓'
|
|
27
|
-
}
|
|
28
|
-
</script>
|
|
29
|
-
|
|
30
|
-
<template>
|
|
31
|
-
<div
|
|
32
|
-
class="flex items-start gap-3 p-3 rounded-lg border"
|
|
33
|
-
:class="severityClasses[element.severity] || severityClasses.info"
|
|
34
|
-
>
|
|
35
|
-
<span class="flex-shrink-0">{{ severityIcons[element.severity] || severityIcons.info }}</span>
|
|
36
|
-
<p class="text-sm">{{ text }}</p>
|
|
37
|
-
</div>
|
|
38
|
-
</template>
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
/**
|
|
3
|
-
* Default Display element for LayoutRenderer
|
|
4
|
-
*
|
|
5
|
-
* Renders context data in a display-only format.
|
|
6
|
-
* For custom rendering, use element.display_component and register custom components.
|
|
7
|
-
*
|
|
8
|
-
* Contract:
|
|
9
|
-
* - props: element, context, value
|
|
10
|
-
* - emits: none
|
|
11
|
-
*/
|
|
12
|
-
defineProps({
|
|
13
|
-
element: { type: Object, required: true },
|
|
14
|
-
context: { type: Object, default: () => ({}) },
|
|
15
|
-
value: { default: null }
|
|
16
|
-
})
|
|
17
|
-
</script>
|
|
18
|
-
|
|
19
|
-
<template>
|
|
20
|
-
<div class="space-y-1">
|
|
21
|
-
<label v-if="element.label" class="block text-sm font-medium text-muted-foreground">
|
|
22
|
-
{{ element.label }}
|
|
23
|
-
</label>
|
|
24
|
-
<div class="text-sm">
|
|
25
|
-
<!-- Default text display -->
|
|
26
|
-
<template v-if="!element.display_component || element.display_component === 'text'">
|
|
27
|
-
<span v-if="value !== null && value !== undefined">{{ value }}</span>
|
|
28
|
-
<span v-else class="text-muted-foreground italic">—</span>
|
|
29
|
-
</template>
|
|
30
|
-
|
|
31
|
-
<!-- Code display -->
|
|
32
|
-
<template v-else-if="element.display_component === 'code'">
|
|
33
|
-
<code class="px-2 py-1 bg-muted rounded font-mono text-sm">{{ value }}</code>
|
|
34
|
-
</template>
|
|
35
|
-
|
|
36
|
-
<!-- Copy URL display -->
|
|
37
|
-
<template v-else-if="element.display_component === 'copy-url'">
|
|
38
|
-
<div class="flex items-center gap-2">
|
|
39
|
-
<code class="flex-1 px-2 py-1 bg-muted rounded font-mono text-xs truncate">{{ value }}</code>
|
|
40
|
-
<button
|
|
41
|
-
type="button"
|
|
42
|
-
class="px-2 py-1 text-xs bg-primary text-primary-foreground rounded hover:bg-primary/90"
|
|
43
|
-
@click="navigator.clipboard.writeText(value)"
|
|
44
|
-
>
|
|
45
|
-
Copy
|
|
46
|
-
</button>
|
|
47
|
-
</div>
|
|
48
|
-
</template>
|
|
49
|
-
|
|
50
|
-
<!-- Fallback for unknown display_component -->
|
|
51
|
-
<template v-else>
|
|
52
|
-
<span>{{ value }}</span>
|
|
53
|
-
<span class="text-xs text-muted-foreground ml-2">({{ element.display_component }})</span>
|
|
54
|
-
</template>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
</template>
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
/**
|
|
3
|
-
* Default ErrorBlock element for LayoutRenderer
|
|
4
|
-
*
|
|
5
|
-
* Displays non-field errors (global form errors).
|
|
6
|
-
*
|
|
7
|
-
* Contract:
|
|
8
|
-
* - props: errors (array of error strings)
|
|
9
|
-
* - emits: none
|
|
10
|
-
*/
|
|
11
|
-
defineProps({
|
|
12
|
-
errors: { type: Array, required: true }
|
|
13
|
-
})
|
|
14
|
-
</script>
|
|
15
|
-
|
|
16
|
-
<template>
|
|
17
|
-
<div
|
|
18
|
-
v-if="errors.length > 0"
|
|
19
|
-
class="flex items-start gap-3 p-3 rounded-lg border bg-red-500/10 border-red-500/20"
|
|
20
|
-
>
|
|
21
|
-
<span class="flex-shrink-0 text-red-400">❌</span>
|
|
22
|
-
<div class="space-y-1">
|
|
23
|
-
<p v-for="(error, i) in errors" :key="i" class="text-sm text-red-400">
|
|
24
|
-
{{ error }}
|
|
25
|
-
</p>
|
|
26
|
-
</div>
|
|
27
|
-
</div>
|
|
28
|
-
</template>
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { ref } from 'vue'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Default Group element for LayoutRenderer
|
|
6
|
-
*
|
|
7
|
-
* A labeled section container. Children are rendered via default slot.
|
|
8
|
-
*
|
|
9
|
-
* Contract:
|
|
10
|
-
* - props: element
|
|
11
|
-
* - emits: none
|
|
12
|
-
* - slot: default (for nested layout content)
|
|
13
|
-
*/
|
|
14
|
-
const props = defineProps({
|
|
15
|
-
element: { type: Object, required: true }
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
const isCollapsed = ref(props.element.collapsed || false)
|
|
19
|
-
|
|
20
|
-
function toggleCollapse() {
|
|
21
|
-
if (props.element.collapsible) {
|
|
22
|
-
isCollapsed.value = !isCollapsed.value
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
</script>
|
|
26
|
-
|
|
27
|
-
<template>
|
|
28
|
-
<div class="border border-border rounded-lg overflow-hidden">
|
|
29
|
-
<!-- Header -->
|
|
30
|
-
<div
|
|
31
|
-
class="px-4 py-3 bg-muted/50"
|
|
32
|
-
:class="{ 'cursor-pointer hover:bg-muted/70': element.collapsible }"
|
|
33
|
-
@click="toggleCollapse"
|
|
34
|
-
>
|
|
35
|
-
<div class="flex items-center justify-between">
|
|
36
|
-
<div>
|
|
37
|
-
<h4 class="font-medium">{{ element.label }}</h4>
|
|
38
|
-
<p v-if="element.description" class="text-sm text-muted-foreground mt-0.5">
|
|
39
|
-
{{ element.description }}
|
|
40
|
-
</p>
|
|
41
|
-
</div>
|
|
42
|
-
<span v-if="element.collapsible" class="text-muted-foreground">
|
|
43
|
-
{{ isCollapsed ? '▶' : '▼' }}
|
|
44
|
-
</span>
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<!-- Content -->
|
|
49
|
-
<div v-show="!isCollapsed" class="p-4">
|
|
50
|
-
<slot />
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
</template>
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
/**
|
|
3
|
-
* Default Label element for LayoutRenderer
|
|
4
|
-
*
|
|
5
|
-
* Contract:
|
|
6
|
-
* - props: element
|
|
7
|
-
* - emits: none
|
|
8
|
-
*/
|
|
9
|
-
defineProps({
|
|
10
|
-
element: { type: Object, required: true }
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
const variantClasses = {
|
|
14
|
-
heading: 'text-lg font-semibold',
|
|
15
|
-
subheading: 'text-base font-medium text-muted-foreground',
|
|
16
|
-
body: 'text-sm',
|
|
17
|
-
caption: 'text-xs text-muted-foreground'
|
|
18
|
-
}
|
|
19
|
-
</script>
|
|
20
|
-
|
|
21
|
-
<template>
|
|
22
|
-
<p :class="variantClasses[element.variant] || variantClasses.body">
|
|
23
|
-
{{ element.text }}
|
|
24
|
-
</p>
|
|
25
|
-
</template>
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { ref } from 'vue'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Default Tabs element for LayoutRenderer
|
|
6
|
-
*
|
|
7
|
-
* A tabbed container. Tab content is rendered via scoped slot.
|
|
8
|
-
*
|
|
9
|
-
* Contract:
|
|
10
|
-
* - props: element
|
|
11
|
-
* - emits: none
|
|
12
|
-
* - slot: #tab="{ tab }" (for rendering each tab's content)
|
|
13
|
-
*/
|
|
14
|
-
const props = defineProps({
|
|
15
|
-
element: { type: Object, required: true }
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
const activeTab = ref(props.element.default_tab || 0)
|
|
19
|
-
|
|
20
|
-
function selectTab(index) {
|
|
21
|
-
activeTab.value = index
|
|
22
|
-
}
|
|
23
|
-
</script>
|
|
24
|
-
|
|
25
|
-
<template>
|
|
26
|
-
<div>
|
|
27
|
-
<!-- Tab buttons -->
|
|
28
|
-
<div class="flex border-b border-border">
|
|
29
|
-
<button
|
|
30
|
-
v-for="(tab, index) in element.tabs"
|
|
31
|
-
:key="index"
|
|
32
|
-
type="button"
|
|
33
|
-
class="px-4 py-2 text-sm font-medium transition-colors"
|
|
34
|
-
:class="[
|
|
35
|
-
activeTab === index
|
|
36
|
-
? 'text-primary border-b-2 border-primary -mb-px'
|
|
37
|
-
: 'text-muted-foreground hover:text-foreground'
|
|
38
|
-
]"
|
|
39
|
-
@click="selectTab(index)"
|
|
40
|
-
>
|
|
41
|
-
{{ tab.label }}
|
|
42
|
-
</button>
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
<!-- Tab content -->
|
|
46
|
-
<div class="pt-4">
|
|
47
|
-
<template v-for="(tab, index) in element.tabs" :key="index">
|
|
48
|
-
<div v-show="activeTab === index">
|
|
49
|
-
<slot name="tab" :tab="tab" :index="index" />
|
|
50
|
-
</div>
|
|
51
|
-
</template>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
</template>
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* StateZero Layout Renderer - Tailwind CSS Styles
|
|
3
|
-
*
|
|
4
|
-
* Import this file if you're using Tailwind CSS:
|
|
5
|
-
* import '@statezero/core/vue/layout.tailwind.css'
|
|
6
|
-
*
|
|
7
|
-
* This maps sz-* classes to Tailwind utilities via @apply.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/* Layout containers */
|
|
11
|
-
.sz-layout {
|
|
12
|
-
@apply flex;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.sz-layout-vertical {
|
|
16
|
-
@apply flex-col;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
.sz-layout-horizontal {
|
|
20
|
-
@apply flex-row;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/* Gap utilities */
|
|
24
|
-
.sz-gap-sm {
|
|
25
|
-
@apply gap-2;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.sz-gap-md {
|
|
29
|
-
@apply gap-4;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
.sz-gap-lg {
|
|
33
|
-
@apply gap-6;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/* Horizontal alignment */
|
|
37
|
-
.sz-align-start {
|
|
38
|
-
@apply items-start;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.sz-align-center {
|
|
42
|
-
@apply items-center;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.sz-align-end {
|
|
46
|
-
@apply items-end;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.sz-align-stretch {
|
|
50
|
-
@apply items-stretch;
|
|
51
|
-
}
|
|
File without changes
|