mlola-ui 1.11.0 → 1.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/package.json +5 -1
- package/registry/catalog.json +16 -0
- package/registry/index.json +51 -51
- package/registry/render-ai.d.ts +102 -0
- package/registry/render-ai.js +388 -0
- package/registry/render-app.html +2 -2
- package/registry/render-catalog.json +1 -1
- package/registry/render-rules.json +7 -1
- package/registry/render.d.ts +15 -3
- package/registry/render.js +111 -25
- package/registry/source/packages/components/render-surface/render-surface.tsx +32 -4
- package/registry/source-manifest.json +1 -1
- package/src/cli.js +13 -3
package/registry/render.js
CHANGED
|
@@ -55,6 +55,11 @@ const quote = (value) => {
|
|
|
55
55
|
const escapeSegment = (segment) => String(segment).replaceAll("~", "~0").replaceAll("/", "~1");
|
|
56
56
|
const pointer = (base, ...segments) => segments.reduce((path, segment) => `${path}/${escapeSegment(segment)}`, base);
|
|
57
57
|
const list = (values) => values.map((value) => quote(value)).join(", ");
|
|
58
|
+
/** A noun as its owner and as more than one: a column's, columns; a series', series. */
|
|
59
|
+
const owned = (noun) => (noun.endsWith("s") ? `${noun}'` : `${noun}'s`);
|
|
60
|
+
const many = (noun) => (noun.endsWith("s") ? noun : `${noun}s`);
|
|
61
|
+
/** What one entry of a "columns" prop takes beside its key and label: named values, or true and false. */
|
|
62
|
+
const entryFields = (prop) => ({ ...(prop.align ? { align: prop.align } : {}), ...prop.fields });
|
|
58
63
|
|
|
59
64
|
/**
|
|
60
65
|
* Reads what an agent or a file holds: one message, an array of messages,
|
|
@@ -467,9 +472,14 @@ function checkProp(rules, surfaceId, type, name, prop, value, at, issues) {
|
|
|
467
472
|
case "options":
|
|
468
473
|
return checkOptions(rules, surfaceId, type, prop, value, at, issues);
|
|
469
474
|
case "columns":
|
|
470
|
-
return checkColumns(rules, surfaceId, prop, value, at, issues);
|
|
475
|
+
return checkColumns(rules, surfaceId, type, name, prop, value, at, issues);
|
|
471
476
|
case "rows":
|
|
472
|
-
return checkRows(rules, surfaceId, value, at, issues);
|
|
477
|
+
return checkRows(rules, surfaceId, name, prop, value, at, issues);
|
|
478
|
+
case "key":
|
|
479
|
+
if (typeof value === "string" && COLUMN_KEY.test(value) && !UNSAFE_SEGMENTS.has(value)) return;
|
|
480
|
+
return issues.errors.push(failure(surfaceId, at, `${type} ${quote(name)} names a field of each row: letters, digits, _ or -, like "month"; got ${quote(value)}.`));
|
|
481
|
+
case "numbers":
|
|
482
|
+
return checkNumbers(rules, surfaceId, type, name, prop, value, at, issues);
|
|
473
483
|
default:
|
|
474
484
|
return issues.errors.push(failure(surfaceId, at, `${type} ${quote(name)} has a kind this validator does not know (${prop.kind}).`));
|
|
475
485
|
}
|
|
@@ -618,45 +628,57 @@ function checkOptions(rules, surfaceId, type, prop, value, at, issues) {
|
|
|
618
628
|
}
|
|
619
629
|
}
|
|
620
630
|
|
|
621
|
-
function checkColumns(rules, surfaceId, prop, value, at, issues) {
|
|
631
|
+
function checkColumns(rules, surfaceId, type, name, prop, value, at, issues) {
|
|
632
|
+
const item = prop.item ?? "column";
|
|
633
|
+
const fields = entryFields(prop);
|
|
622
634
|
if (!Array.isArray(value) || value.length < (prop.minItems ?? 1)) {
|
|
623
|
-
issues.errors.push(failure(surfaceId, at,
|
|
635
|
+
issues.errors.push(failure(surfaceId, at, `${quote(name)} is a non-empty array of {"key", "label"${Object.keys(fields).map((field) => `, ${quote(field)}?`).join("")}}.`));
|
|
624
636
|
return;
|
|
625
637
|
}
|
|
626
|
-
|
|
627
|
-
|
|
638
|
+
const max = prop.maxItems ?? rules.limits.columns;
|
|
639
|
+
if (value.length > max) {
|
|
640
|
+
issues.errors.push(failure(surfaceId, at, prop.maxItems ? `${type} takes at most ${max} ${many(item)}; this is ${value.length}.` : `A table shows at most ${max} columns; more cannot be read on a phone.`));
|
|
628
641
|
return;
|
|
629
642
|
}
|
|
630
643
|
const seen = new Set();
|
|
631
644
|
for (const [index, column] of value.entries()) {
|
|
632
645
|
const columnAt = pointer(at, index);
|
|
633
646
|
if (!isObject(column)) {
|
|
634
|
-
issues.errors.push(failure(surfaceId, columnAt, `A
|
|
647
|
+
issues.errors.push(failure(surfaceId, columnAt, `A ${item} is {"key": "…", "label": "…"}; got ${typeName(column)}.`));
|
|
635
648
|
continue;
|
|
636
649
|
}
|
|
637
|
-
onlyKeys(column, ["key", "label",
|
|
638
|
-
if (typeof column.key !== "string" || !COLUMN_KEY.test(column.key) || UNSAFE_SEGMENTS.has(column.key)) issues.errors.push(failure(surfaceId, pointer(columnAt, "key"), `A
|
|
639
|
-
else if (seen.has(column.key)) issues.errors.push(failure(surfaceId, pointer(columnAt, "key"), `Two
|
|
650
|
+
onlyKeys(column, ["key", "label", ...Object.keys(fields)], columnAt, surfaceId, issues, `A ${item}`);
|
|
651
|
+
if (typeof column.key !== "string" || !COLUMN_KEY.test(column.key) || UNSAFE_SEGMENTS.has(column.key)) issues.errors.push(failure(surfaceId, pointer(columnAt, "key"), `A ${owned(item)} "key" names the row field it shows: letters, digits, _ or -, like "order".`));
|
|
652
|
+
else if (seen.has(column.key)) issues.errors.push(failure(surfaceId, pointer(columnAt, "key"), `Two ${many(item)} have the key ${quote(column.key)}.`));
|
|
640
653
|
else seen.add(column.key);
|
|
641
|
-
checkText(rules, surfaceId, column.label, pointer(columnAt, "label"), { maxLength: rules.limits.labelLength }, true, issues, `The
|
|
642
|
-
|
|
654
|
+
checkText(rules, surfaceId, column.label, pointer(columnAt, "label"), { maxLength: rules.limits.labelLength }, true, issues, `The ${owned(item)} "label"`);
|
|
655
|
+
for (const [field, spec] of Object.entries(fields)) {
|
|
656
|
+
const given = column[field];
|
|
657
|
+
if (given === undefined || (spec.values ? spec.values.includes(given) : typeof given === "boolean")) continue;
|
|
658
|
+
issues.errors.push(failure(surfaceId, pointer(columnAt, field), spec.values ? `${quote(field)} is ${list(spec.values)}.` : `${quote(field)} is true or false.`));
|
|
659
|
+
}
|
|
643
660
|
}
|
|
644
661
|
}
|
|
645
662
|
|
|
646
|
-
function checkRows(rules, surfaceId, value, at, issues) {
|
|
663
|
+
function checkRows(rules, surfaceId, name, prop, value, at, issues) {
|
|
647
664
|
if (binding(rules, surfaceId, value, at, issues)) return;
|
|
648
665
|
if (!Array.isArray(value)) {
|
|
649
|
-
issues.errors.push(failure(surfaceId, at,
|
|
666
|
+
issues.errors.push(failure(surfaceId, at, `${quote(name)} is an array of objects, or {"path": "/…"} to one in the data model; got ${typeName(value)}.`));
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (value.length < (prop.minItems ?? 0)) {
|
|
670
|
+
issues.errors.push(failure(surfaceId, at, `${quote(name)} needs at least ${prop.minItems} ${prop.minItems === 1 ? "row" : "rows"}.`));
|
|
650
671
|
return;
|
|
651
672
|
}
|
|
652
|
-
|
|
653
|
-
|
|
673
|
+
const max = Math.min(prop.maxItems ?? rules.limits.rows, rules.limits.rows);
|
|
674
|
+
if (value.length > max) {
|
|
675
|
+
issues.errors.push(failure(surfaceId, at, `${quote(name)} holds at most ${max} rows; ${prop.maxItems ? "keep the ones that matter" : "page through the rest"}.`));
|
|
654
676
|
return;
|
|
655
677
|
}
|
|
656
678
|
for (const [index, row] of value.entries()) {
|
|
657
679
|
const rowAt = pointer(at, index);
|
|
658
680
|
if (!isObject(row)) {
|
|
659
|
-
issues.errors.push(failure(surfaceId, rowAt, `A row is an object
|
|
681
|
+
issues.errors.push(failure(surfaceId, rowAt, `A row is an object of its fields, like {"name": "…"}; got ${typeName(row)}.`));
|
|
660
682
|
continue;
|
|
661
683
|
}
|
|
662
684
|
for (const [key, cell] of Object.entries(row)) {
|
|
@@ -667,6 +689,52 @@ function checkRows(rules, surfaceId, value, at, issues) {
|
|
|
667
689
|
}
|
|
668
690
|
}
|
|
669
691
|
|
|
692
|
+
function checkNumbers(rules, surfaceId, type, name, prop, value, at, issues) {
|
|
693
|
+
if (binding(rules, surfaceId, value, at, issues)) return;
|
|
694
|
+
const label = `${type} ${quote(name)}`;
|
|
695
|
+
if (!Array.isArray(value)) {
|
|
696
|
+
issues.errors.push(failure(surfaceId, at, `${label} is an array of numbers, or {"path": "/…"} to one in the data model; got ${typeName(value)}.`));
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (value.length < (prop.minItems ?? 0) || value.length > rules.limits.rows) {
|
|
700
|
+
issues.errors.push(failure(surfaceId, at, `${label} holds ${prop.minItems ?? 0} to ${rules.limits.rows} numbers; this is ${value.length}.`));
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const odd = value.findIndex((entry) => typeof entry !== "number" || !Number.isFinite(entry));
|
|
704
|
+
if (odd !== -1) issues.errors.push(failure(surfaceId, pointer(at, odd), `${label} holds numbers only; this one is ${quote(value[odd])}.`));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* What a prop reads from the rows beside it (`rows` names that prop): a name
|
|
709
|
+
* in every row, a number wherever one is drawn, and somewhere to write when
|
|
710
|
+
* a person may edit. Rows in the data model are the renderer's to read with
|
|
711
|
+
* care; rows written into the component are held here.
|
|
712
|
+
*/
|
|
713
|
+
function checkRowsRead(surfaceId, type, definition, component, at, issues) {
|
|
714
|
+
for (const [name, prop] of Object.entries(definition.props)) {
|
|
715
|
+
const given = component[name];
|
|
716
|
+
if (!prop.rows || given === undefined) continue;
|
|
717
|
+
const rows = component[prop.rows];
|
|
718
|
+
const entries = Array.isArray(given) ? given.filter(isObject) : [];
|
|
719
|
+
const writes = Object.entries(prop.fields ?? {}).filter(([, field]) => field.writes).map(([field]) => field);
|
|
720
|
+
const written = Array.isArray(given) && !(isObject(rows) && "path" in rows) ? given.findIndex((entry) => isObject(entry) && writes.some((field) => entry[field] === true)) : -1;
|
|
721
|
+
if (written >= 0) issues.errors.push(failure(surfaceId, pointer(at, name, written), `What a person enters here is written to ${quote(prop.rows)}, so bind it to the data model: {"path": "/…"}, with the rows in updateDataModel.`));
|
|
722
|
+
// A key names each row or holds its number; a list of them is read only where it is drawn as numbers.
|
|
723
|
+
if (!Array.isArray(rows) || (prop.kind !== "key" && !prop.numeric)) continue;
|
|
724
|
+
for (const key of prop.kind === "key" ? [given] : entries.map((entry) => entry.key)) {
|
|
725
|
+
// A key that is no field name was reported where it was written.
|
|
726
|
+
if (typeof key !== "string" || !COLUMN_KEY.test(key) || UNSAFE_SEGMENTS.has(key)) continue;
|
|
727
|
+
const fits = (value) => (prop.numeric ? value === undefined || value === null || (typeof value === "number" && Number.isFinite(value) && value >= (prop.minimum ?? -Infinity)) : (typeof value === "string" && value.trim() !== "") || typeof value === "number");
|
|
728
|
+
const odd = rows.findIndex((row) => isObject(row) && !fits(own(row, key)));
|
|
729
|
+
if (odd === -1) continue;
|
|
730
|
+
const value = own(rows[odd], key);
|
|
731
|
+
issues.errors.push(failure(surfaceId, value === undefined ? pointer(at, prop.rows, odd) : pointer(at, prop.rows, odd, key), prop.numeric
|
|
732
|
+
? `${type} draws ${quote(key)} as a number${prop.minimum === undefined ? "" : ` of at least ${prop.minimum}`}; this row has ${quote(value)}. Write the number itself, or leave it out for a gap.`
|
|
733
|
+
: `${type} names each row by its ${quote(key)}; this row has ${value === undefined ? "none" : quote(value)}.`));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
670
738
|
/** Rules between props of one component: a range that holds its value, a value among the options. */
|
|
671
739
|
function checkValues(rules, surfaceId, type, definition, component, at, issues) {
|
|
672
740
|
const literal = (name) => (typeof component[name] === "number" ? component[name] : definition.props[name]?.default);
|
|
@@ -693,6 +761,7 @@ function checkValues(rules, surfaceId, type, definition, component, at, issues)
|
|
|
693
761
|
const values = component.options.filter(isObject).map((option) => option.value);
|
|
694
762
|
if (!values.includes(component.value)) issues.errors.push(failure(surfaceId, pointer(at, "value"), `${type} "value" ${quote(component.value)} is not one of its options (${list(values)}).`));
|
|
695
763
|
}
|
|
764
|
+
checkRowsRead(surfaceId, type, definition, component, at, issues);
|
|
696
765
|
}
|
|
697
766
|
|
|
698
767
|
/* The tree ---------------------------------------------------------------- */
|
|
@@ -883,8 +952,13 @@ export { childIds, getAt, isObject, setAt, unescapeSegment };
|
|
|
883
952
|
|
|
884
953
|
/* For people and agents --------------------------------------------------- */
|
|
885
954
|
|
|
886
|
-
/**
|
|
887
|
-
|
|
955
|
+
/**
|
|
956
|
+
* The catalog as a guide an agent reads before composing a surface. With
|
|
957
|
+
* `themes: false` it leaves themes out, for a surface that takes the theme
|
|
958
|
+
* of the page it is shown on (a renderer's `theme: "inherit"`).
|
|
959
|
+
*/
|
|
960
|
+
export function renderCatalogGuide(rules, { themes = true } = {}) {
|
|
961
|
+
const look = themes ? `, "theme": {"name": ${quote(rules.defaultTheme)}, "mode": "system"}` : "";
|
|
888
962
|
const lines = [
|
|
889
963
|
`# ${rules.title}`,
|
|
890
964
|
"",
|
|
@@ -892,13 +966,13 @@ export function renderCatalogGuide(rules) {
|
|
|
892
966
|
"",
|
|
893
967
|
`Compose a surface as A2UI messages (version ${list(rules.protocolVersions)}):`,
|
|
894
968
|
"",
|
|
895
|
-
`1. {"version": "v0.9.1", "createSurface": {"surfaceId": "…", "catalogId": ${quote(rules.catalogId)}
|
|
969
|
+
`1. {"version": "v0.9.1", "createSurface": {"surfaceId": "…", "catalogId": ${quote(rules.catalogId)}${look}}}`,
|
|
896
970
|
`2. {"version": "v0.9.1", "updateComponents": {"surfaceId": "…", "components": [{"id": "root", "component": "Stack", "children": […]}, …]}}`,
|
|
897
971
|
`3. {"version": "v0.9.1", "updateDataModel": {"surfaceId": "…", "path": "/form", "value": {…}}} for the values inputs bind to.`,
|
|
898
972
|
"",
|
|
899
973
|
"Components are a flat list; containers name their children by id, and exactly one component has the id \"root\". Never set a color, size, class or style: pick the named values below.",
|
|
900
974
|
"",
|
|
901
|
-
`Themes: ${rules.themes.join(", ")} (default ${rules.defaultTheme}); modes: ${rules.modes.join(", ")}.`,
|
|
975
|
+
themes ? `Themes: ${rules.themes.join(", ")} (default ${rules.defaultTheme}); modes: ${rules.modes.join(", ")}.` : `Leave "theme" out of createSurface: the surface takes the theme of the page it is shown on.`,
|
|
902
976
|
"",
|
|
903
977
|
"## Data and actions",
|
|
904
978
|
"",
|
|
@@ -928,7 +1002,7 @@ export function renderCatalogGuide(rules) {
|
|
|
928
1002
|
if (rules.examples?.length) {
|
|
929
1003
|
lines.push("## Examples", "");
|
|
930
1004
|
for (const example of rules.examples) {
|
|
931
|
-
lines.push(`### ${example.title}`, "", example.description, "", "```json", "[", example.messages.map((message) => ` ${JSON.stringify(message)}`).join(",\n"), "]", "```", "");
|
|
1005
|
+
lines.push(`### ${example.title}`, "", example.description, "", "```json", "[", example.messages.map((message) => ` ${JSON.stringify(themes ? message : unthemed(message))}`).join(",\n"), "]", "```", "");
|
|
932
1006
|
}
|
|
933
1007
|
}
|
|
934
1008
|
lines.push(
|
|
@@ -945,6 +1019,12 @@ export function renderCatalogGuide(rules) {
|
|
|
945
1019
|
return `${lines.join("\n")}\n`;
|
|
946
1020
|
}
|
|
947
1021
|
|
|
1022
|
+
/** A message as it reads when the page gives the theme: createSurface without one. */
|
|
1023
|
+
function unthemed(message) {
|
|
1024
|
+
if (!isObject(message) || !isObject(message.createSurface) || !("theme" in message.createSurface)) return message;
|
|
1025
|
+
return { ...message, createSurface: Object.fromEntries(Object.entries(message.createSurface).filter(([key]) => key !== "theme")) };
|
|
1026
|
+
}
|
|
1027
|
+
|
|
948
1028
|
function describeKind(spec) {
|
|
949
1029
|
switch (spec.kind) {
|
|
950
1030
|
case "enum":
|
|
@@ -965,10 +1045,16 @@ function describeKind(spec) {
|
|
|
965
1045
|
return `{"event": {"name", "context"}}`;
|
|
966
1046
|
case "options":
|
|
967
1047
|
return `an array of {"label", "value"}, or {"path"}${spec.minItems ? `, at least ${spec.minItems}` : ""}`;
|
|
968
|
-
case "columns":
|
|
969
|
-
|
|
1048
|
+
case "columns": {
|
|
1049
|
+
const fields = Object.entries(entryFields(spec));
|
|
1050
|
+
return `an array of {"key", "label"${fields.map(([field]) => `, ${quote(field)}?`).join("")}}${spec.maxItems ? `, at most ${spec.maxItems}` : ""}${fields.map(([field, entry]) => `; ${field} is ${entry.values ? list(entry.values) : "true or false"}`).join("")}`;
|
|
1051
|
+
}
|
|
970
1052
|
case "rows":
|
|
971
|
-
return `an array of objects
|
|
1053
|
+
return `an array of objects, or {"path"}${spec.minItems ? `, at least ${spec.minItems}` : ""}${spec.maxItems ? `, at most ${spec.maxItems}` : ""}`;
|
|
1054
|
+
case "key":
|
|
1055
|
+
return `the name of a field in each row, like "month"`;
|
|
1056
|
+
case "numbers":
|
|
1057
|
+
return `an array of numbers, or {"path"}${spec.minItems ? `, at least ${spec.minItems}` : ""}`;
|
|
972
1058
|
default:
|
|
973
1059
|
return spec.kind;
|
|
974
1060
|
}
|
|
@@ -49,6 +49,23 @@ const LABELS: RenderSurfaceLabels = {
|
|
|
49
49
|
no: "No",
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
/** What a component of your own is given to draw one instance of itself. */
|
|
53
|
+
export interface RenderDrawProps {
|
|
54
|
+
/** An id for its element, unique on the page. */
|
|
55
|
+
id: string;
|
|
56
|
+
/** The component as the agent sent it. Every prop has passed the check; one bound to the data model is still `{"path"}`, so read it. */
|
|
57
|
+
props: Readonly<Record<string, unknown>>;
|
|
58
|
+
/** A prop's value now: the literal, or what its binding holds. */
|
|
59
|
+
read: (value: unknown) => unknown;
|
|
60
|
+
/** Writes what a person entered to where a prop is bound. False for a prop that is not bound. */
|
|
61
|
+
write: (binding: unknown, value: unknown) => boolean;
|
|
62
|
+
/** The component's children, drawn. */
|
|
63
|
+
children: React.ReactNode;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Components for the types of a catalog that extends Mlola's, by type. */
|
|
67
|
+
export type RenderComponents = Readonly<Record<string, React.ComponentType<RenderDrawProps>>>;
|
|
68
|
+
|
|
52
69
|
export interface RenderSurfaceProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onError"> {
|
|
53
70
|
/**
|
|
54
71
|
* A2UI messages for the Mlola Render catalog. Pass the same array with more
|
|
@@ -73,6 +90,8 @@ export interface RenderSurfaceProps extends Omit<React.HTMLAttributes<HTMLDivEle
|
|
|
73
90
|
labels?: Partial<RenderSurfaceLabels>;
|
|
74
91
|
/** The catalog to draw from. The Mlola Render catalog when left out. */
|
|
75
92
|
rules?: RenderRules;
|
|
93
|
+
/** Draws the components a catalog adds to Mlola's (`rules`), each by its type. Mlola's own are always drawn by this component. */
|
|
94
|
+
components?: RenderComponents;
|
|
76
95
|
/** Today's date for date fields, for server rendering. */
|
|
77
96
|
today?: string;
|
|
78
97
|
}
|
|
@@ -84,6 +103,7 @@ interface Drawing {
|
|
|
84
103
|
base: string;
|
|
85
104
|
labels: RenderSurfaceLabels;
|
|
86
105
|
today?: string;
|
|
106
|
+
components?: RenderComponents;
|
|
87
107
|
press: (componentId: string, scope: string) => void;
|
|
88
108
|
}
|
|
89
109
|
|
|
@@ -102,7 +122,7 @@ const domId = (base: string, key: string) => `${base}-${Array.from(key, (charact
|
|
|
102
122
|
* the surface's theme. Fields write to the surface's data model, and a
|
|
103
123
|
* pressed Button answers with A2UI's action through `onAction`.
|
|
104
124
|
*/
|
|
105
|
-
export function RenderSurface({ messages, streaming = false, host: givenHost, surfaceId, onAction, onError, theme = "surface", fallback = null, labels, rules = catalogRules, today, className, ...props }: RenderSurfaceProps) {
|
|
125
|
+
export function RenderSurface({ messages, streaming = false, host: givenHost, surfaceId, onAction, onError, theme = "surface", fallback = null, labels, rules = catalogRules, components, today, className, ...props }: RenderSurfaceProps) {
|
|
106
126
|
// The first messages are drawn in the first render, so a surface rendered on the server arrives whole.
|
|
107
127
|
const [ownHost] = React.useState(() => {
|
|
108
128
|
const made = createRenderHost(rules);
|
|
@@ -151,7 +171,7 @@ export function RenderSurface({ messages, streaming = false, host: givenHost, su
|
|
|
151
171
|
? shown.map((id) => {
|
|
152
172
|
const surface = host.surface(id);
|
|
153
173
|
if (!surface) return null;
|
|
154
|
-
const drawing: Drawing = { host, surfaceId: id, base: `${base}-${id}`, labels: words, today, press: press(id) };
|
|
174
|
+
const drawing: Drawing = { host, surfaceId: id, base: `${base}-${id}`, labels: words, today, components, press: press(id) };
|
|
155
175
|
const themed = theme === "surface" ? surface.theme : null;
|
|
156
176
|
return (
|
|
157
177
|
// audit-allow: attr.reserved — the engine's own meaning: the surface is a region in the theme and mode its agent named
|
|
@@ -468,8 +488,16 @@ function Node({ drawing, instance }: { drawing: Drawing; instance: RenderInstanc
|
|
|
468
488
|
</Table>
|
|
469
489
|
);
|
|
470
490
|
}
|
|
471
|
-
default:
|
|
472
|
-
|
|
491
|
+
default: {
|
|
492
|
+
// A type the catalog in `rules` adds: drawn by the component given for it, or not at all.
|
|
493
|
+
const Added = drawing.components && Object.hasOwn(drawing.components, node.type) ? drawing.components[node.type] : undefined;
|
|
494
|
+
if (!Added) return null;
|
|
495
|
+
return (
|
|
496
|
+
<Added id={fieldId} props={props} read={read} write={(binding, value) => host.write(surfaceId, binding, value, scope)}>
|
|
497
|
+
{inside}
|
|
498
|
+
</Added>
|
|
499
|
+
);
|
|
500
|
+
}
|
|
473
501
|
}
|
|
474
502
|
}
|
|
475
503
|
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"packages/components/progress/progress.tsx": "sha256-oDxj1Ccl1pKV4+e6A7aaJL1FThW8qs9r4Gb0hEmExeM=",
|
|
49
49
|
"packages/components/radio-group/radio-group.tsx": "sha256-jhnSEBLkdpe8dv5us/Q1sMsQF4sKGRgDXWGKm2JSFew=",
|
|
50
50
|
"packages/components/rating/rating.tsx": "sha256-prhfNzlCqLRWqURtq0MrX6MLdtuDpIOf5fA1Ie26L54=",
|
|
51
|
-
"packages/components/render-surface/render-surface.tsx": "sha256-
|
|
51
|
+
"packages/components/render-surface/render-surface.tsx": "sha256-nW48kYLXoa3eeGZQ7gXMD/bTNH9MDeJlWyl1zzrvDwo=",
|
|
52
52
|
"packages/components/resizable/resizable.tsx": "sha256-RFBpg/a2O82mmx+O5wT+um+y3y2w0rUMJJdBzRu9ONU=",
|
|
53
53
|
"packages/components/segmented-control/segmented-control.tsx": "sha256-MCBG5XHFWCwbACctsYKbqe8mvSSR6pf60tyRgdCDKDA=",
|
|
54
54
|
"packages/components/select/select.tsx": "sha256-7u1sshW6KAQgjgwQ1siN1P5GkBxboOiOJOdgLanForE=",
|
package/src/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ import { catalogStylesheets, clearCredentials, fetchProItems, GUIDE_FILENAME, ho
|
|
|
17
17
|
import { serveMcp } from "./mcp.js";
|
|
18
18
|
import { renderCatalog, renderRules } from "./knowledge.js";
|
|
19
19
|
import { formatRenderReport, renderCatalogGuide, validateRender } from "../registry/render.js";
|
|
20
|
+
import { renderSystemPrompt } from "../registry/render-ai.js";
|
|
20
21
|
import { pullTheme } from "./theme-pull.js";
|
|
21
22
|
import { buildTheme } from "./theme-build.js";
|
|
22
23
|
|
|
@@ -38,6 +39,7 @@ Usage:
|
|
|
38
39
|
mlola-ui theme build [--file mlola.theme.json]
|
|
39
40
|
mlola-ui render check <file | -> [--json]
|
|
40
41
|
mlola-ui render catalog [--schema]
|
|
42
|
+
mlola-ui render prompt [--tool] [--page-theme]
|
|
41
43
|
|
|
42
44
|
Examples:
|
|
43
45
|
npx mlola-ui init (also tells your coding agents about Mlola)
|
|
@@ -323,7 +325,12 @@ export async function run(argv, options = {}) {
|
|
|
323
325
|
if (result.unchanged.length) {
|
|
324
326
|
output.log(`• ${result.unchanged.length} file(s) already match the registry`);
|
|
325
327
|
}
|
|
328
|
+
// The stylesheet from init imports the engine, so it is always needed.
|
|
329
|
+
const engines = new Map([[config.engine.engine ?? "@mlola-ui/engine", `^${CLI_VERSION}`], ...collectEngineDependencies(items)]);
|
|
326
330
|
if (result.conflicts.length) {
|
|
331
|
+
// The files beside a refused one are written, and they need their packages now: a project
|
|
332
|
+
// left with new code on old packages does not build, and running this again would stop here again.
|
|
333
|
+
ensurePackages(cwd, engines, install);
|
|
327
334
|
output.error(
|
|
328
335
|
`✗ Refused to replace ${result.conflicts.length} modified file(s):\n ${result.conflicts.join(
|
|
329
336
|
"\n ",
|
|
@@ -335,8 +342,6 @@ export async function run(argv, options = {}) {
|
|
|
335
342
|
if (styleResult.index) output.log(`Pro styles are gathered in ${styleResult.index}; import it once, after the engine stylesheet.`);
|
|
336
343
|
if (guideResult.status === "written") output.log(`Coding agents: point your AGENTS.md at ${GUIDE_FILENAME}, which lists every Pro class and attribute.`);
|
|
337
344
|
|
|
338
|
-
// The stylesheet from init imports the engine, so it is always needed.
|
|
339
|
-
const engines = new Map([[config.engine.engine ?? "@mlola-ui/engine", `^${CLI_VERSION}`], ...collectEngineDependencies(items)]);
|
|
340
345
|
return ensurePackages(cwd, engines, install);
|
|
341
346
|
}
|
|
342
347
|
|
|
@@ -373,7 +378,12 @@ export async function run(argv, options = {}) {
|
|
|
373
378
|
output.log(hasFlag(args, "--schema") ? JSON.stringify(renderCatalog(), null, 2) : renderCatalogGuide(rules));
|
|
374
379
|
return 0;
|
|
375
380
|
}
|
|
376
|
-
if (subcommand
|
|
381
|
+
if (subcommand === "prompt") {
|
|
382
|
+
// The system prompt for a model someone calls themselves, for a server that is not JavaScript too.
|
|
383
|
+
output.log(renderSystemPrompt(rules, { output: hasFlag(args, "--tool") ? "tool" : "lines", theme: hasFlag(args, "--page-theme") ? "page" : "surface" }));
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
if (subcommand !== "check") throw new Error(`Unknown render command "${subcommand ?? ""}". Try: mlola-ui render check <file>, mlola-ui render catalog, or mlola-ui render prompt`);
|
|
377
387
|
if (!file) throw new Error("Name the file of A2UI messages to check, or - to read them from stdin: mlola-ui render check ui.json");
|
|
378
388
|
const source = file === "-" ? (options.stdin ?? fs.readFileSync(0, "utf8")) : fs.readFileSync(path.resolve(cwd, file), "utf8");
|
|
379
389
|
const result = validateRender(source, rules);
|