@plitzi/sdk-server 0.32.17 → 0.32.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +18 -0
- package/dist/modules/mcp/apps/render/index.js +2 -1
- package/dist/modules/mcp/apps/render/view/heldBatch.ts +107 -0
- package/dist/modules/mcp/apps/render/view/index.tsx +212 -0
- package/dist/modules/mcp/apps/shared/assets.js +10 -0
- package/dist/modules/mcp/apps/shared/bundle.js +15 -14
- package/dist/modules/mcp/handler.js +1 -1
- package/dist/modules/mcp/helpers/guide.js +11 -3
- package/dist/modules/mcp/helpers/space.js +13 -1
- package/dist/modules/mcp/resources/register.js +8 -3
- package/dist/modules/mcp/resources/renderGuide.js +138 -24
- package/dist/modules/mcp/resources/router.js +13 -1
- package/dist/modules/mcp/server.js +31 -19
- package/dist/modules/mcp/tools/apply/dispatch.js +2 -0
- package/dist/modules/mcp/tools/apply/index.js +17 -3
- package/dist/modules/mcp/tools/operations/index.js +4 -0
- package/dist/modules/mcp/tools/operations/schema/elements/patchElement.js +1 -1
- package/dist/modules/mcp/tools/operations/schema/elements/repeatElement.js +148 -0
- package/dist/modules/mcp/tools/operations/schema/interactions/deleteInteraction.js +1 -1
- package/dist/modules/mcp/tools/operations/schema/interactions/patchInteractionNode.js +1 -1
- package/dist/modules/mcp/tools/operations/schema/interactions/upsertInteractionFlow.js +1 -1
- package/dist/modules/mcp/tools/operations/schema/operations.js +2 -0
- package/dist/modules/mcp/tools/operations/schema/pages/upsertPage.js +1 -1
- package/dist/modules/mcp/tools/operations/schema/shared.js +8 -5
- package/dist/modules/mcp/tools/operations/schemaIds.js +54 -0
- package/dist/modules/mcp/tools/operations/style/definitions/patchDefinition.js +1 -1
- package/dist/modules/mcp/tools/operations/style/definitions/upsertDefinition.js +1 -1
- package/dist/modules/mcp/tools/operations/style/definitions/upsertDefinitions.js +40 -0
- package/dist/modules/mcp/tools/operations/style/globalStyles/patchGlobalStyle.js +1 -1
- package/dist/modules/mcp/tools/operations/style/globalStyles/upsertGlobalStyle.js +1 -1
- package/dist/modules/mcp/tools/operations/style/idStyles/patchIdStyle.js +1 -1
- package/dist/modules/mcp/tools/operations/style/idStyles/upsertIdStyle.js +1 -1
- package/dist/modules/mcp/tools/operations/style/operations.js +2 -0
- package/dist/modules/mcp/tools/operations/style/shared.js +1 -1
- package/dist/modules/mcp/tools/read.js +21 -3
- package/dist/modules/mcp/tools/render.js +70 -8
- package/dist/modules/mcp/tools/shared/expandOperations.js +44 -0
- package/dist/modules/mcp/tools/shared/tool.js +16 -11
- package/dist/modules/mcp/tools/shared/validator/audit.js +3 -0
- package/dist/modules/mcp/tools/shared/validator/batch.js +13 -4
- package/dist/modules/mcp/tools/shared/validator/index.js +15 -0
- package/dist/modules/mcp/tools/validate.js +11 -3
- package/dist/src/modules/mcp/apps/shared/assets.d.ts +10 -0
- package/dist/src/modules/mcp/apps/shared/bundle.d.ts +3 -0
- package/dist/src/modules/mcp/apps/shared/index.d.ts +1 -0
- package/dist/src/modules/mcp/e2e/index.d.ts +2 -2
- package/dist/src/modules/mcp/e2e/mcpEndpoint.d.ts +6 -1
- package/dist/src/modules/mcp/e2e/renderingHost.d.ts +16 -1
- package/dist/src/modules/mcp/helpers/guide.d.ts +2 -1
- package/dist/src/modules/mcp/helpers/space.d.ts +9 -0
- package/dist/src/modules/mcp/resources/index.d.ts +2 -1
- package/dist/src/modules/mcp/resources/register.d.ts +6 -2
- package/dist/src/modules/mcp/resources/renderGuide.d.ts +5 -0
- package/dist/src/modules/mcp/resources/router.d.ts +4 -0
- package/dist/src/modules/mcp/server.d.ts +6 -5
- package/dist/src/modules/mcp/tools/apply/index.d.ts +50 -0
- package/dist/src/modules/mcp/tools/operations/index.d.ts +100 -0
- package/dist/src/modules/mcp/tools/operations/schema/elements/repeatElement.d.ts +55 -0
- package/dist/src/modules/mcp/tools/operations/schema/index.d.ts +1 -0
- package/dist/src/modules/mcp/tools/operations/schema/operations.d.ts +19 -0
- package/dist/src/modules/mcp/tools/operations/schema/shared.d.ts +15 -0
- package/dist/src/modules/mcp/tools/operations/schemaIds.d.ts +6 -0
- package/dist/src/modules/mcp/tools/operations/style/definitions/upsertDefinitions.d.ts +41 -0
- package/dist/src/modules/mcp/tools/operations/style/index.d.ts +1 -0
- package/dist/src/modules/mcp/tools/operations/style/operations.d.ts +33 -0
- package/dist/src/modules/mcp/tools/operations/style/shared.d.ts +4 -3
- package/dist/src/modules/mcp/tools/preview.d.ts +50 -0
- package/dist/src/modules/mcp/tools/read.d.ts +1 -0
- package/dist/src/modules/mcp/tools/render.d.ts +421 -363
- package/dist/src/modules/mcp/tools/screenshot.d.ts +50 -0
- package/dist/src/modules/mcp/tools/shared/expandOperations.d.ts +13 -0
- package/dist/src/modules/mcp/tools/shared/tool.d.ts +8 -0
- package/dist/src/modules/mcp/tools/validate.d.ts +50 -0
- package/package.json +6 -5
- package/skills/plitzi-render/SKILL.md +170 -0
- package/dist/modules/mcp/apps/render/view.tsx +0 -108
- /package/dist/modules/mcp/apps/example/{view.tsx → view/index.tsx} +0 -0
- /package/dist/src/modules/mcp/{apps/example/view.d.ts → tests/schemaIds.test.d.ts} +0 -0
- /package/dist/src/modules/mcp/{apps/render/view.d.ts → tests/skill.test.d.ts} +0 -0
|
@@ -33,8 +33,26 @@ Most widgets are **presentation only** — the structure + styling below is all
|
|
|
33
33
|
runs the live Plitzi SDK though, so it can also **fetch data** (an \`apiContainer\`) and **react to events**
|
|
34
34
|
(interaction flows); see "Data & interactivity" at the end when a widget needs them.
|
|
35
35
|
|
|
36
|
-
Each call renders a **fresh** widget
|
|
37
|
-
|
|
36
|
+
Each call renders a **fresh** widget: send **every** operation it needs in the one call.
|
|
37
|
+
|
|
38
|
+
**To CHANGE a widget you already rendered, do not rebuild it** — every render answers with a \`renderId\`; call
|
|
39
|
+
again with that id, \`patch: true\` and only the operations that differ:
|
|
40
|
+
|
|
41
|
+
\`\`\`json
|
|
42
|
+
{ "patch": true, "renderId": "r7f3a2c", "operations": [
|
|
43
|
+
{ "type": "patchDefinition", "ref": "tl-card", "desktop": { "border-radius": "20px" } },
|
|
44
|
+
{ "type": "patchElement", "pageRef": "render", "ref": "title-2", "props": { "content": "EPCOT (día largo)" } }
|
|
45
|
+
] }
|
|
46
|
+
\`\`\`
|
|
47
|
+
The widget merges the delta into the batch it was built from and reports back what it applied — including the
|
|
48
|
+
errors, if a ref does not exist. Address rows by the refs you already know (\`card-1\`, \`blk-2-3\`). If it reports
|
|
49
|
+
that the widget could not be recovered (a surface that renders none, a host that keeps no storage, a conversation
|
|
50
|
+
resumed elsewhere), send the whole batch again without \`patch\`.
|
|
51
|
+
|
|
52
|
+
**Patch only to modify what is on screen.** A different subject, or a different kind of widget, is a FRESH render
|
|
53
|
+
with no \`patch\` — the delta is merged into the previous widget, so patching a new idea leaves the user looking at
|
|
54
|
+
both at once. Rebuilding when you could have patched only costs tokens; patching when you should have rebuilt costs
|
|
55
|
+
the user a wrong widget.
|
|
38
56
|
|
|
39
57
|
## Build the whole widget in ONE upsertElement (nest with \`children\`)
|
|
40
58
|
|
|
@@ -58,24 +76,95 @@ An element is \`{ ref, type, subType?, props?, style?, children? }\`. Children r
|
|
|
58
76
|
add elements one-by-one with a top-level \`parentRef: "<existing ref>"\` and optional \`position\` — useful to append to
|
|
59
77
|
or restructure something you already created — but for a fresh widget the inline \`children\` tree is easier.)
|
|
60
78
|
|
|
61
|
-
##
|
|
79
|
+
## Repeating rows — repeatElement, never copy-paste
|
|
62
80
|
|
|
63
|
-
|
|
81
|
+
The moment two siblings have the same shape and different data — a list, steps, cards, a table, a timeline —
|
|
82
|
+
**write the shape once** and hand over the rows. \`repeatElement\` creates the wrapper (style it with the row/grid
|
|
83
|
+
class) and renders the template once per entry of \`items\`:
|
|
64
84
|
|
|
65
85
|
\`\`\`json
|
|
66
|
-
{ "type": "
|
|
86
|
+
{ "type": "repeatElement", "pageRef": "render", "ref": "steps", "style": { "base": ["list"] },
|
|
87
|
+
"template": {
|
|
88
|
+
"ref": "step", "type": "container", "style": { "base": ["row"] },
|
|
89
|
+
"children": [
|
|
90
|
+
{ "ref": "at", "type": "paragraph", "style": { "base": ["time"] }, "props": { "content": "{{item.time}}" } },
|
|
91
|
+
{ "ref": "what", "type": "paragraph", "style": { "base": ["txt"] }, "props": { "content": "{{item.text}}" } }
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
"items": [
|
|
95
|
+
{ "time": "08:00", "text": "Doors open, head straight to the main hall." },
|
|
96
|
+
{ "time": "10:30", "text": "Workshops in the east wing." },
|
|
97
|
+
{ "time": "13:00", "text": "Lunch, then the keynote." }
|
|
98
|
+
]
|
|
99
|
+
}
|
|
67
100
|
\`\`\`
|
|
101
|
+
- \`{{item.<field>}}\` is replaced by that row's field, anywhere in the template (props, a style ref, a param).
|
|
102
|
+
A placeholder that is the WHOLE value keeps the field's type (\`"{{item.count}}"\` with \`count: 3\` stays the
|
|
103
|
+
number 3); mixed with text it interpolates. Dotted paths work: \`{{item.author.name}}\`.
|
|
104
|
+
- Every ref in the template gets the row number appended — \`step\` becomes \`step-1\`, \`step-2\`… — so rows never
|
|
105
|
+
collide and you can address one later without reading anything back.
|
|
106
|
+
- Other \`{{…}}\` names are left untouched, so schema variables keep working inside a template.
|
|
107
|
+
- A row missing a field the template reads fails the batch and names the row and the fields it does carry.
|
|
108
|
+
- Up to 100 rows per op. Rows that differ in SHAPE (not just data) are not rows — write those as plain elements.
|
|
109
|
+
|
|
110
|
+
**A list inside each row** — a timeline of days each with its own steps, a menu of sections each with its dishes —
|
|
111
|
+
is one op too: give the node that should wrap the sub-list a \`repeat\`, and put the sub-rows in the row's data.
|
|
112
|
+
|
|
113
|
+
\`\`\`json
|
|
114
|
+
{ "type": "repeatElement", "pageRef": "render", "ref": "timeline", "style": { "base": ["tl"] },
|
|
115
|
+
"template": {
|
|
116
|
+
"ref": "day", "type": "container", "style": { "base": ["day"] },
|
|
117
|
+
"children": [
|
|
118
|
+
{ "ref": "title", "type": "heading", "subType": "h3", "props": { "content": "{{item.park}}" } },
|
|
119
|
+
{ "ref": "body", "type": "container", "style": { "base": ["body"] },
|
|
120
|
+
"repeat": { "items": "{{item.blocks}}", "template": {
|
|
121
|
+
"ref": "blk", "type": "container", "style": { "base": ["blk"] },
|
|
122
|
+
"children": [
|
|
123
|
+
{ "ref": "at", "type": "paragraph", "props": { "content": "{{item.time}}" } },
|
|
124
|
+
{ "ref": "what", "type": "paragraph", "props": { "content": "{{item.text}}" } }
|
|
125
|
+
]
|
|
126
|
+
} } }
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
"items": [
|
|
130
|
+
{ "park": "Magic Kingdom", "blocks": [ { "time": "08:00", "text": "Rope drop." }, { "time": "10:30", "text": "Space Mountain." } ] },
|
|
131
|
+
{ "park": "EPCOT", "blocks": [ { "time": "08:30", "text": "Cosmic Rewind." } ] }
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
\`\`\`
|
|
135
|
+
- The node carrying \`repeat\` becomes the WRAPPER of its sub-list (its own \`children\` are ignored), so put the
|
|
136
|
+
sub-list's layout class on it.
|
|
137
|
+
- Inside the sub-template \`{{item.…}}\` reads the SUB-row; a field of the outer row is not reachable from there,
|
|
138
|
+
so repeat it in each sub-row if you need it.
|
|
139
|
+
- Refs number both levels, outer first: \`blk-2-3\` is the third block of the second day.
|
|
140
|
+
- **One level of nesting**: the sub-template is a plain element tree, so it cannot carry another \`repeat\`.
|
|
141
|
+
- The whole op is capped at 500 rows across both levels.
|
|
142
|
+
|
|
143
|
+
## Style with reusable classes — upsertDefinitions
|
|
144
|
+
|
|
145
|
+
Styling is separate from structure: declare the classes, then attach them by ref. **Declare them all in ONE
|
|
146
|
+
\`upsertDefinitions\`**, keyed by class name — a widget usually needs a dozen classes, and one op per class spends a
|
|
147
|
+
noticeable slice of the call on repeated \`{"type":"upsertDefinition","ref":…}\` envelopes.
|
|
148
|
+
|
|
149
|
+
\`\`\`json
|
|
150
|
+
{ "type": "upsertDefinitions", "definitions": {
|
|
151
|
+
"card": { "desktop": { "display": "flex", "flex-direction": "column", "gap": "8px", "padding": "16px", "border-radius": "12px" } },
|
|
152
|
+
"title": { "desktop": { "font-size": "16px", "font-weight": "600", "margin-top": "0", "margin-bottom": "0" } }
|
|
153
|
+
} }
|
|
154
|
+
\`\`\`
|
|
155
|
+
Each value is exactly what \`upsertDefinition\` takes minus \`type\`/\`ref\` (\`desktop\`/\`tablet\`/\`mobile\`, \`states\`,
|
|
156
|
+
\`variants\`, \`slots\`), and the result is identical. The single \`upsertDefinition\` still exists for a one-off class,
|
|
157
|
+
and \`patchDefinition\` still changes only some CSS of one class.
|
|
68
158
|
- CSS properties in **kebab-case** (\`background-color\`, \`font-size\`, \`border-radius\`), values as plain strings.
|
|
69
159
|
- Attach to an element via \`style: { "base": ["card"] }\`. Stack classes: \`"base": ["card", "shadow"]\`.
|
|
70
160
|
- One \`ref\` can name both an element and its class (as above) — they live in different namespaces.
|
|
71
161
|
- Lay containers out with flexbox or grid — pick the direction on purpose, see **Fit the panel** below.
|
|
72
162
|
- **You are not styling from zero.** Each type lands on the page with CSS you did not write, and it is the usual
|
|
73
|
-
reason a widget does not look like the definitions say
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
wide otherwise. Any flex child that must be allowed to shrink needs it too.
|
|
163
|
+
reason a widget does not look like the definitions say:
|
|
164
|
+
- The per-type CSS the SDK ships imposes **no minimum size**: a rail, a divider, a dot or a narrow cell is
|
|
165
|
+
exactly as small as you make it, and no \`"min-width": "0"\` escape hatch is needed. The flip side is that an
|
|
166
|
+
element with no content and no size takes no space at all — give a spacer its own \`height\`. (The 50px box
|
|
167
|
+
empty containers show on the builder canvas is a drop target, scoped to it; it never reaches a widget.)
|
|
79
168
|
- The BROWSER's own defaults for the rest: \`heading\` keeps its UA font-size and ~0.67em top/bottom margins,
|
|
80
169
|
\`paragraph\` ~1em margins, \`list\` a 40px \`padding-left\`, \`button\` its native chrome, \`image\` its intrinsic
|
|
81
170
|
size, \`link\` its own colour and underline. In a compact widget set these explicitly — usually
|
|
@@ -85,9 +174,10 @@ Styling is separate from structure: declare a class, then attach it by ref.
|
|
|
85
174
|
builder — it does not paint here, so do not count on it in a widget.
|
|
86
175
|
- **Mind the intrinsic display.** Some types start non-block: \`text\` is \`display: inline\`, so to stack or size it,
|
|
87
176
|
wrap it in a \`container\` (or set \`display: block\`). \`heading\` and \`paragraph\` are already block.
|
|
88
|
-
- **
|
|
89
|
-
\`
|
|
90
|
-
|
|
177
|
+
- **Shorthands are welcome** and cheaper to write: \`padding: 8px 16px\`, \`border: 1px solid #ddd\`,
|
|
178
|
+
\`font: bold 16px/1.5 Arial\`, \`flex: 1 1 auto\`, \`transition: opacity 200ms ease\`. They are stored expanded to
|
|
179
|
+
their longhands, so a breakpoint, state or variant can override each property on its own — and a read-back shows
|
|
180
|
+
the longhands, not what you wrote. An unknown property errors with the correct kebab-case key suggested.
|
|
91
181
|
- **Responsive:** add \`tablet\` and/or \`mobile\` blocks next to \`desktop\` (same shape); they override desktop on
|
|
92
182
|
smaller screens — \`{ "desktop": { "font-size": "36px" }, "mobile": { "font-size": "24px" } }\`.
|
|
93
183
|
- **Interactive states:** nest under \`states\` keyed by pseudo-class, each with its own breakpoint block —
|
|
@@ -106,14 +196,16 @@ half-empty widget to avoid. Choose the axis every time:
|
|
|
106
196
|
- **Peers side by side** — metrics, plans, options, a comparison, an image next to its text: a row, wrapping when
|
|
107
197
|
it runs out of width, children sharing it (no fixed widths).
|
|
108
198
|
\`\`\`json
|
|
109
|
-
{ "type": "
|
|
110
|
-
|
|
199
|
+
{ "type": "upsertDefinitions", "definitions": {
|
|
200
|
+
"row": { "desktop": { "display": "flex", "flex-direction": "row", "flex-wrap": "wrap", "gap": "12px", "align-items": "stretch" } },
|
|
201
|
+
"col": { "desktop": { "flex-grow": "1", "flex-basis": "0%", "min-width": "150px" } }
|
|
202
|
+
} }
|
|
111
203
|
\`\`\`
|
|
112
204
|
\`flex-grow: 1\` + \`flex-basis: 0%\` splits the row evenly; \`min-width\` is the wrap threshold — under it the item
|
|
113
205
|
drops to the next line by itself, so a narrow panel degrades gracefully with no breakpoints.
|
|
114
206
|
- **Many uniform items** — cards, tiles, a gallery: one grid line does it all.
|
|
115
207
|
\`\`\`json
|
|
116
|
-
{ "type": "
|
|
208
|
+
{ "type": "upsertDefinitions", "definitions": { "grid": { "desktop": { "display": "grid", "grid-template-columns": "repeat(auto-fit, minmax(160px, 1fr))", "gap": "12px" } } } }
|
|
117
209
|
\`\`\`
|
|
118
210
|
- **Label + value pairs** stay on one line (\`display: flex\`, \`justify-content: space-between\`) instead of two.
|
|
119
211
|
- **Vertical is right** for reading order: a heading over its paragraph, a form, a step list, long prose.
|
|
@@ -133,12 +225,12 @@ The host publishes its palette as CSS variables on the page, so use them for eve
|
|
|
133
225
|
\`light-dark(<light>, <dark>)\` fallback for hosts that send none:
|
|
134
226
|
|
|
135
227
|
\`\`\`json
|
|
136
|
-
{ "type": "
|
|
228
|
+
{ "type": "upsertDefinitions", "definitions": { "card": { "desktop": {
|
|
137
229
|
"background-color": "var(--color-background-secondary, light-dark(#ffffff, #1f2430))",
|
|
138
230
|
"color": "var(--color-text-primary, light-dark(#0f172a, #e8eaed))",
|
|
139
231
|
"border-width": "1px", "border-style": "solid",
|
|
140
232
|
"border-color": "var(--color-border-primary, light-dark(#e2e8f0, #333a48))"
|
|
141
|
-
} }
|
|
233
|
+
} } } }
|
|
142
234
|
\`\`\`
|
|
143
235
|
|
|
144
236
|
- Surfaces: \`--color-background-primary\` (the page), \`--color-background-secondary\` / \`--color-background-tertiary\`
|
|
@@ -173,6 +265,19 @@ that is not here (lists, tabs, dialogs, forms, icons…).
|
|
|
173
265
|
\`image\`/\`video\` \`src\` accepts any \`https\` URL, or a \`data:\`/\`blob:\` URI for a fully self-contained graphic
|
|
174
266
|
(e.g. an inline SVG icon or a base64 image) — both render with no extra setup.
|
|
175
267
|
|
|
268
|
+
## Keep the call small
|
|
269
|
+
|
|
270
|
+
Everything in \`operations\` is text you write, and a widget that takes two calls because the first ran long is a
|
|
271
|
+
widget the user waits twice for. Two habits pay for themselves:
|
|
272
|
+
|
|
273
|
+
- **Do not draw pictures in \`data:\` URIs.** A hand-written SVG scene (a castle, a skyline, a logo) costs more than
|
|
274
|
+
the entire rest of the widget and renders worse than nothing at all. Use an \`https\` image the user gave you, a
|
|
275
|
+
flat colour or a two-stop \`linear-gradient\` as a banner, an emoji or an \`fontAwesome\` icon for a glyph — or drop
|
|
276
|
+
the decoration. A small self-contained \`data:\` SVG is fine for a **simple** shape (a check, an arrow, a dot).
|
|
277
|
+
- **One class per look, not per property.** Classes like \`tone-blue\` + \`tone-blue-text\` + \`tone-blue-bg\` for the
|
|
278
|
+
same card triple the declarations and the attachments. Put everything the look needs in one class, add a second
|
|
279
|
+
only for the part that genuinely varies between siblings (a colour), and reuse it — the whole point of a class.
|
|
280
|
+
|
|
176
281
|
## Full worked example — two plans side by side
|
|
177
282
|
|
|
178
283
|
The cards sit in a wrapping row and split it evenly, so the widget uses the panel's width and stays short; on a
|
|
@@ -181,10 +286,12 @@ narrow panel \`min-width\` drops the second card under the first on its own.
|
|
|
181
286
|
\`\`\`json
|
|
182
287
|
{
|
|
183
288
|
"operations": [
|
|
184
|
-
{ "type": "
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
289
|
+
{ "type": "upsertDefinitions", "definitions": {
|
|
290
|
+
"plans": { "desktop": { "display": "flex", "flex-direction": "row", "flex-wrap": "wrap", "gap": "12px", "align-items": "stretch" } },
|
|
291
|
+
"card": { "desktop": { "display": "flex", "flex-direction": "column", "gap": "6px", "flex-grow": "1", "flex-basis": "0%", "min-width": "150px", "padding": "16px", "background-color": "var(--color-background-secondary, light-dark(#ffffff, #1f2430))", "color": "var(--color-text-primary, light-dark(#0f172a, #e8eaed))", "border-width": "1px", "border-style": "solid", "border-color": "var(--color-border-primary, light-dark(#e2e8f0, #333a48))", "border-radius": "var(--border-radius-lg, 12px)", "text-align": "center" } },
|
|
292
|
+
"price": { "desktop": { "font-size": "28px", "font-weight": "800", "color": "#3b82f6" } },
|
|
293
|
+
"cta": { "desktop": { "background-color": "#3b82f6", "color": "#ffffff", "padding": "10px 16px", "border-radius": "8px", "font-weight": "600" }, "states": { "hover": { "desktop": { "background-color": "#2563eb" } } } }
|
|
294
|
+
} },
|
|
188
295
|
{ "type": "upsertElement", "pageRef": "render", "element": {
|
|
189
296
|
"ref": "plans", "type": "container", "style": { "base": ["plans"] },
|
|
190
297
|
"children": [
|
|
@@ -248,6 +355,13 @@ connects that source to a descendant's field.
|
|
|
248
355
|
The tool returns \`rendered: false\` with \`errors: [{ path, message, hint }]\`. Read the hint, fix that one op, and
|
|
249
356
|
retry — you never lose the rest of the batch.
|
|
250
357
|
`;
|
|
358
|
+
/** The same two documents through the READ path (the router, hence plitzi_read), not only as MCP resources: an
|
|
359
|
+
* agent that reaches for the batch-read tool — the reflex the guide itself teaches — must not be told its own
|
|
360
|
+
* authoring guide does not exist. Space-independent, like the core singletons. */
|
|
361
|
+
var readRenderResource = (uri) => {
|
|
362
|
+
if (uri === "plitzi://render/guide") return envelope(renderGuideText);
|
|
363
|
+
if (uri === "plitzi://render/types") return envelope(renderTypes());
|
|
364
|
+
};
|
|
251
365
|
var registerRenderResources = (server, log) => {
|
|
252
366
|
server.registerResource("Render guide", RENDER_GUIDE_URI, {
|
|
253
367
|
description: "How to author a plitzi_render widget: operations, element types, styling, examples.",
|
|
@@ -273,4 +387,4 @@ var registerRenderResources = (server, log) => {
|
|
|
273
387
|
});
|
|
274
388
|
};
|
|
275
389
|
//#endregion
|
|
276
|
-
export { RENDER_GUIDE_URI, RENDER_TYPES_URI, registerRenderResources };
|
|
390
|
+
export { RENDER_GUIDE_URI, RENDER_TYPES_URI, readRenderResource, registerRenderResources };
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { emptySpace } from "../helpers/space.js";
|
|
2
|
+
import "../helpers/uris.js";
|
|
1
3
|
import { canonicalUri } from "./canonical.js";
|
|
2
4
|
import { readCoreResource } from "./core.js";
|
|
3
5
|
import { readPrimerResource } from "./primer.js";
|
|
6
|
+
import { readRenderResource } from "./renderGuide.js";
|
|
4
7
|
import { readSchemaResource } from "./schema.js";
|
|
5
8
|
import { readStyleResource } from "./style.js";
|
|
6
9
|
//#region src/modules/mcp/resources/router.ts
|
|
7
10
|
var resolvers = [
|
|
8
11
|
(space, _env, uri) => readCoreResource(space, uri),
|
|
12
|
+
(_space, _env, uri) => readRenderResource(uri),
|
|
9
13
|
readPrimerResource,
|
|
10
14
|
readSchemaResource,
|
|
11
15
|
readStyleResource
|
|
@@ -19,7 +23,15 @@ var readResource = (space, env, rawUri) => {
|
|
|
19
23
|
}
|
|
20
24
|
return null;
|
|
21
25
|
};
|
|
26
|
+
/** What a connection carrying NO space (a guest / widgets-only grant) can still read: the space-independent
|
|
27
|
+
* singletons and the render docs. Undefined for everything else, so the caller says the connection has no space
|
|
28
|
+
* instead of answering out of an empty one — an empty page list would read as "this space has no pages". */
|
|
29
|
+
var readPublicResource = (env, rawUri) => {
|
|
30
|
+
const uri = canonicalUri(env, rawUri);
|
|
31
|
+
if (uri === "plitzi://types") return;
|
|
32
|
+
return readRenderResource(uri) ?? readCoreResource(emptySpace(), uri);
|
|
33
|
+
};
|
|
22
34
|
/** Current version of a resource, for optimistic-concurrency checks. Null when the URI is unknown. */
|
|
23
35
|
var resourceVersion = (space, env, uri) => readResource(space, env, uri)?.stateVersion ?? null;
|
|
24
36
|
//#endregion
|
|
25
|
-
export { readResource, resourceVersion };
|
|
37
|
+
export { readPublicResource, readResource, resourceVersion };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { registerApps } from "./apps/index.js";
|
|
2
|
-
import { emptySpace, emptySpaceMessage,
|
|
3
|
-
import { serverInstructions } from "./helpers/guide.js";
|
|
2
|
+
import { NoSpaceError, emptySpace, emptySpaceMessage, noSpaceError } from "./helpers/space.js";
|
|
3
|
+
import { serverInstructions, widgetsOnlyInstructions } from "./helpers/guide.js";
|
|
4
4
|
import { createMcpLog } from "./helpers/log.js";
|
|
5
5
|
import { registerResources } from "./resources/register.js";
|
|
6
6
|
import { tools } from "./tools/index.js";
|
|
@@ -13,19 +13,20 @@ var asText = (data) => ({ content: [{
|
|
|
13
13
|
type: "text",
|
|
14
14
|
text: JSON.stringify(data)
|
|
15
15
|
}] });
|
|
16
|
-
var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) => {
|
|
16
|
+
var createMcpServer = async ({ adapters, getSpaceId, preview, screenshot, logger }) => {
|
|
17
17
|
const log = createMcpLog(logger);
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
const spaceId = await getSpaceId().catch(() => void 0);
|
|
19
|
+
const hasSpace = spaceId !== void 0;
|
|
20
|
+
const requireSpaceId = () => {
|
|
21
|
+
if (spaceId === void 0) throw new NoSpaceError();
|
|
22
|
+
return spaceId;
|
|
23
|
+
};
|
|
23
24
|
const loadSpace = async () => {
|
|
24
|
-
const
|
|
25
|
+
const id = requireSpaceId();
|
|
25
26
|
const [schema, style, catalog] = await Promise.all([
|
|
26
|
-
adapters.getSchema?.(
|
|
27
|
-
adapters.getStyle?.(
|
|
28
|
-
adapters.getComponentCatalog?.(
|
|
27
|
+
adapters.getSchema?.(id, MCP_ENV),
|
|
28
|
+
adapters.getStyle?.(id, MCP_ENV),
|
|
29
|
+
adapters.getComponentCatalog?.(id, MCP_ENV).catch(() => void 0)
|
|
29
30
|
]);
|
|
30
31
|
if (!schema || !style) throw new Error(emptySpaceMessage);
|
|
31
32
|
return {
|
|
@@ -36,22 +37,22 @@ var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) =>
|
|
|
36
37
|
};
|
|
37
38
|
const { saveSchema, saveStyle } = adapters;
|
|
38
39
|
const persisters = {
|
|
39
|
-
schema: saveSchema ?
|
|
40
|
-
style: saveStyle ?
|
|
40
|
+
schema: saveSchema ? (schema) => saveSchema(requireSpaceId(), MCP_ENV, schema) : void 0,
|
|
41
|
+
style: saveStyle ? (style) => saveStyle(requireSpaceId(), MCP_ENV, style) : void 0
|
|
41
42
|
};
|
|
42
43
|
let spacePromise;
|
|
43
44
|
const getSpace = () => spacePromise ??= loadSpace();
|
|
44
45
|
const server = new McpServer({
|
|
45
46
|
name: "plitzi-mcp",
|
|
46
|
-
version: "0.32.
|
|
47
|
-
}, { instructions: serverInstructions });
|
|
48
|
-
registerResources(server, getSpace, MCP_ENV, log);
|
|
47
|
+
version: "0.32.19"
|
|
48
|
+
}, { instructions: hasSpace ? serverInstructions : widgetsOnlyInstructions });
|
|
49
|
+
registerResources(server, getSpace, MCP_ENV, log, hasSpace);
|
|
49
50
|
registerApps(server);
|
|
50
51
|
const toolContext = async () => ({
|
|
51
52
|
space: await getSpace(),
|
|
52
53
|
env: MCP_ENV,
|
|
53
54
|
persisters,
|
|
54
|
-
spaceId:
|
|
55
|
+
spaceId: requireSpaceId(),
|
|
55
56
|
preview,
|
|
56
57
|
screenshot
|
|
57
58
|
});
|
|
@@ -62,16 +63,27 @@ var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) =>
|
|
|
62
63
|
preview,
|
|
63
64
|
screenshot
|
|
64
65
|
});
|
|
66
|
+
const behaviorOf = (tool) => {
|
|
67
|
+
if (tool.spaceless) return (args) => tool.execute(args, spacelessContext());
|
|
68
|
+
if (hasSpace) return async (args) => tool.execute(args, await toolContext());
|
|
69
|
+
return tool.executePublic ? (args) => tool.executePublic?.(args, MCP_ENV) : void 0;
|
|
70
|
+
};
|
|
65
71
|
for (const tool of tools) {
|
|
66
72
|
if (tool.requires === "screenshot" && !screenshot) continue;
|
|
73
|
+
const behavior = behaviorOf(tool);
|
|
74
|
+
if (!behavior) continue;
|
|
67
75
|
const run = async (args) => {
|
|
68
76
|
const start = performance.now();
|
|
69
77
|
try {
|
|
70
|
-
const result = await
|
|
78
|
+
const result = await behavior(args);
|
|
71
79
|
log.toolCall(tool.name, args, performance.now() - start);
|
|
72
80
|
return isCallToolResult(result) ? result : asText(result);
|
|
73
81
|
} catch (error) {
|
|
74
82
|
log.toolCall(tool.name, args, performance.now() - start, error);
|
|
83
|
+
if (error instanceof NoSpaceError) return asText({
|
|
84
|
+
error: noSpaceError,
|
|
85
|
+
message: error.message
|
|
86
|
+
});
|
|
75
87
|
throw error;
|
|
76
88
|
}
|
|
77
89
|
};
|
|
@@ -19,6 +19,7 @@ import { upsertVariable } from "../operations/schema/variables/upsertVariable.js
|
|
|
19
19
|
import { deleteDefinition } from "../operations/style/definitions/deleteDefinition.js";
|
|
20
20
|
import { patchDefinition } from "../operations/style/definitions/patchDefinition.js";
|
|
21
21
|
import { upsertDefinition } from "../operations/style/definitions/upsertDefinition.js";
|
|
22
|
+
import { upsertDefinitions } from "../operations/style/definitions/upsertDefinitions.js";
|
|
22
23
|
import { deleteGlobalStyle } from "../operations/style/globalStyles/deleteGlobalStyle.js";
|
|
23
24
|
import { patchGlobalStyle } from "../operations/style/globalStyles/patchGlobalStyle.js";
|
|
24
25
|
import { upsertGlobalStyle } from "../operations/style/globalStyles/upsertGlobalStyle.js";
|
|
@@ -49,6 +50,7 @@ var executeOp = (space, env, op) => {
|
|
|
49
50
|
case "deleteInteraction": return deleteInteraction(space, env, op);
|
|
50
51
|
case "patchSettings": return patchSettings(space, env, op);
|
|
51
52
|
case "upsertDefinition": return upsertDefinition(space, env, op);
|
|
53
|
+
case "upsertDefinitions": return upsertDefinitions(space, env, op);
|
|
52
54
|
case "patchDefinition": return patchDefinition(space, env, op);
|
|
53
55
|
case "deleteDefinition": return deleteDefinition(space, env, op);
|
|
54
56
|
case "upsertGlobalStyle": return upsertGlobalStyle(space, env, op);
|
|
@@ -2,6 +2,7 @@ import { cloneSpace } from "../../helpers/space.js";
|
|
|
2
2
|
import { environment, operations } from "../operations/index.js";
|
|
3
3
|
import { applyOperations } from "./dispatch.js";
|
|
4
4
|
import { changedResources, conflictMessage, detectConflicts, resolvedElements } from "./writeResult.js";
|
|
5
|
+
import { expandOperations } from "../shared/expandOperations.js";
|
|
5
6
|
import { defineTool } from "../shared/tool.js";
|
|
6
7
|
import { validateOperations } from "../shared/validator/index.js";
|
|
7
8
|
import { auditResources } from "../shared/validator/audit.js";
|
|
@@ -22,7 +23,20 @@ var schemaErrorToValidation = (error) => ({
|
|
|
22
23
|
});
|
|
23
24
|
var apply = async (input, space, persisters) => {
|
|
24
25
|
const env = input.environment ?? "main";
|
|
25
|
-
const
|
|
26
|
+
const expansion = expandOperations(input.operations);
|
|
27
|
+
if (expansion.errors.length > 0) return {
|
|
28
|
+
applied: false,
|
|
29
|
+
persisted: false,
|
|
30
|
+
summary: {
|
|
31
|
+
created: 0,
|
|
32
|
+
updated: 0,
|
|
33
|
+
deleted: 0
|
|
34
|
+
},
|
|
35
|
+
changed: [],
|
|
36
|
+
errors: expansion.errors
|
|
37
|
+
};
|
|
38
|
+
const ops = expansion.operations;
|
|
39
|
+
const validation = validateOperations(space, ops);
|
|
26
40
|
if (!validation.valid) return {
|
|
27
41
|
applied: false,
|
|
28
42
|
persisted: false,
|
|
@@ -51,7 +65,7 @@ var apply = async (input, space, persisters) => {
|
|
|
51
65
|
}
|
|
52
66
|
};
|
|
53
67
|
const draft = cloneSpace(space);
|
|
54
|
-
const outcome = applyOperations(draft, env,
|
|
68
|
+
const outcome = applyOperations(draft, env, ops);
|
|
55
69
|
if (outcome.errors.length > 0) return {
|
|
56
70
|
applied: false,
|
|
57
71
|
persisted: false,
|
|
@@ -77,7 +91,7 @@ var apply = async (input, space, persisters) => {
|
|
|
77
91
|
errors: integrity.errors.map(schemaErrorToValidation),
|
|
78
92
|
warnings: noWarnings(validation.warnings)
|
|
79
93
|
};
|
|
80
|
-
const audit = auditResources(draft,
|
|
94
|
+
const audit = auditResources(draft, ops);
|
|
81
95
|
const warnings = [...validation.warnings, ...audit.warnings];
|
|
82
96
|
if (audit.errors.length > 0) return {
|
|
83
97
|
applied: false,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { elementOps } from "./schema/operations.js";
|
|
2
|
+
import { registerSharedSchemaIds } from "./schemaIds.js";
|
|
2
3
|
import { styleOps } from "./style/operations.js";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
//#region src/modules/mcp/tools/operations/index.ts
|
|
6
|
+
registerSharedSchemaIds();
|
|
5
7
|
var operation = z.discriminatedUnion("type", [
|
|
6
8
|
elementOps.upsertElement,
|
|
9
|
+
elementOps.repeatElement,
|
|
7
10
|
elementOps.patchElement,
|
|
8
11
|
elementOps.deleteElement,
|
|
9
12
|
elementOps.moveElement,
|
|
@@ -21,6 +24,7 @@ var operation = z.discriminatedUnion("type", [
|
|
|
21
24
|
elementOps.deleteInteraction,
|
|
22
25
|
elementOps.patchSettings,
|
|
23
26
|
styleOps.upsertDefinition,
|
|
27
|
+
styleOps.upsertDefinitions,
|
|
24
28
|
styleOps.patchDefinition,
|
|
25
29
|
styleOps.deleteDefinition,
|
|
26
30
|
styleOps.upsertGlobalStyle,
|
|
@@ -10,7 +10,7 @@ var patchElementOp = z.object({
|
|
|
10
10
|
type: z.literal("patchElement"),
|
|
11
11
|
pageRef: z.string().describe("Page ref or id"),
|
|
12
12
|
ref: z.string().describe("Existing element ref or id"),
|
|
13
|
-
idRef: z.string().optional().describe("Assign or rename
|
|
13
|
+
idRef: z.string().optional().describe("Assign or rename this element idRef ([A-Za-z0-9_-], starting with a letter, unique in the space). Without one an element publishes no data source, so it is not bindable. A rename moves the source name with it: every binding and interaction that targeted the old one is repointed for you."),
|
|
14
14
|
label: z.string().optional(),
|
|
15
15
|
subType: z.string().optional(),
|
|
16
16
|
props: z.record(z.string(), z.unknown()).optional().describe("Merged onto existing props: listed keys change, null unsets a key, others are preserved"),
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { elementInput, elementShape, position, styleRefs } from "../shared.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/modules/mcp/tools/operations/schema/elements/repeatElement.ts
|
|
4
|
+
var item = z.record(z.string(), z.unknown());
|
|
5
|
+
var MAX_ITEMS = 100;
|
|
6
|
+
var MAX_ROWS = 500;
|
|
7
|
+
var templateElement = z.lazy(() => z.object({
|
|
8
|
+
...elementShape,
|
|
9
|
+
children: z.array(templateElement).optional(),
|
|
10
|
+
repeat: z.object({
|
|
11
|
+
items: z.string().describe("The row field holding the sub-list, as \"{{item.<field>}}\" (or just the field path)"),
|
|
12
|
+
template: elementInput.describe("Rendered once per entry of that sub-list (a plain element tree)")
|
|
13
|
+
}).optional().describe("Makes THIS element the wrapper of a nested list — a list inside each row")
|
|
14
|
+
}));
|
|
15
|
+
var repeatElementOp = z.object({
|
|
16
|
+
type: z.literal("repeatElement"),
|
|
17
|
+
pageRef: z.string().describe("Page ref or id"),
|
|
18
|
+
ref: z.string().describe("Ref of the WRAPPER element this creates; the rows become its children"),
|
|
19
|
+
elementType: z.string().optional().describe("Type of the wrapper; defaults to container"),
|
|
20
|
+
label: z.string().optional(),
|
|
21
|
+
style: styleRefs.optional().describe("Classes for the wrapper — this is where the row/grid layout goes"),
|
|
22
|
+
parentRef: z.string().optional().describe("Anchor ref/id; defaults to page root"),
|
|
23
|
+
position: position.optional(),
|
|
24
|
+
template: templateElement.describe("The subtree ONE row renders, with {{item.field}} placeholders"),
|
|
25
|
+
items: z.array(item).min(1).max(MAX_ITEMS).describe("One object per row; its fields fill the {{item.field}} placeholders of the template")
|
|
26
|
+
}).describe("Build a LIST from one template plus its `items` data, instead of repeating near-identical elements. Creates a wrapper whose children are the template rendered once per row. `{{item.<field>}}` is replaced by that row field — alone as the whole value it keeps the field type, mixed with text it interpolates. Every ref gets the row number appended (\"day\" → \"day-1\"), which is how you address a row later. A template node may carry `repeat: { items: \"{{item.<list>}}\", template: … }` to nest a list inside each row: it becomes the sub-list wrapper, refs number both levels (\"step-2-3\"), and `{{item.…}}` there reads the SUB-row. Other {{…}} names are untouched, so schema variables still work.");
|
|
27
|
+
var PLACEHOLDER = /\{\{\s*item\.([A-Za-z0-9_.-]+)\s*\}\}/g;
|
|
28
|
+
var WHOLE_PLACEHOLDER = /^\{\{\s*item\.([A-Za-z0-9_.-]+)\s*\}\}$/;
|
|
29
|
+
var lookup = (row, path) => path.split(".").reduce((value, key) => {
|
|
30
|
+
if (value === null || typeof value !== "object") return;
|
|
31
|
+
return value[key];
|
|
32
|
+
}, row);
|
|
33
|
+
/** One string of the template, with its placeholders resolved. A placeholder that IS the whole string yields the
|
|
34
|
+
* field's own value (a number stays a number, an array stays an array); mixed text interpolates. */
|
|
35
|
+
var fillString = (text, row) => {
|
|
36
|
+
const whole = WHOLE_PLACEHOLDER.exec(text);
|
|
37
|
+
if (whole) {
|
|
38
|
+
const value = lookup(row, whole[1]);
|
|
39
|
+
return value === void 0 ? {
|
|
40
|
+
value: text,
|
|
41
|
+
missing: whole[1]
|
|
42
|
+
} : { value };
|
|
43
|
+
}
|
|
44
|
+
let missing;
|
|
45
|
+
const value = text.replace(PLACEHOLDER, (match, path) => {
|
|
46
|
+
const found = lookup(row, path);
|
|
47
|
+
if (found === void 0) {
|
|
48
|
+
missing ??= path;
|
|
49
|
+
return match;
|
|
50
|
+
}
|
|
51
|
+
if (typeof found === "string") return found;
|
|
52
|
+
return typeof found === "number" || typeof found === "boolean" ? found.toString() : JSON.stringify(found);
|
|
53
|
+
});
|
|
54
|
+
return missing === void 0 ? { value } : {
|
|
55
|
+
value,
|
|
56
|
+
missing
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
var fillValue = (value, row, missing) => {
|
|
60
|
+
if (typeof value === "string") {
|
|
61
|
+
const filled = fillString(value, row);
|
|
62
|
+
if (filled.missing) missing.add(filled.missing);
|
|
63
|
+
return filled.value;
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(value)) return value.map((entry) => fillValue(entry, row, missing));
|
|
66
|
+
if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, fillValue(entry, row, missing)]));
|
|
67
|
+
return value;
|
|
68
|
+
};
|
|
69
|
+
var listOf = (row, spec) => {
|
|
70
|
+
const whole = WHOLE_PLACEHOLDER.exec(spec);
|
|
71
|
+
const value = lookup(row, whole ? whole[1] : spec);
|
|
72
|
+
if (!Array.isArray(value)) return;
|
|
73
|
+
return value.map((entry) => entry !== null && typeof entry === "object" ? entry : { value: entry });
|
|
74
|
+
};
|
|
75
|
+
/** One template node against one row. `suffix` is the row numbering accumulated from the OUTSIDE in, so a nested
|
|
76
|
+
* row reads "step-2-3" (row 2, sub-row 3) rather than the other way round. */
|
|
77
|
+
var expandNode = (node, row, suffix, state) => {
|
|
78
|
+
const { children, repeat, ...own } = node;
|
|
79
|
+
const filled = fillValue(own, row, state.missing);
|
|
80
|
+
const element = {
|
|
81
|
+
...filled,
|
|
82
|
+
ref: `${filled.ref}${suffix}`
|
|
83
|
+
};
|
|
84
|
+
if (repeat) {
|
|
85
|
+
const rows = listOf(row, repeat.items);
|
|
86
|
+
if (!rows) {
|
|
87
|
+
state.notAList.add(repeat.items);
|
|
88
|
+
return element;
|
|
89
|
+
}
|
|
90
|
+
const capped = rows.slice(0, MAX_ITEMS);
|
|
91
|
+
state.rows += capped.length;
|
|
92
|
+
element.children = capped.map((subRow, index) => expandNode(repeat.template, subRow, `${suffix}-${index + 1}`, state));
|
|
93
|
+
return element;
|
|
94
|
+
}
|
|
95
|
+
if (children) element.children = children.map((child) => expandNode(child, row, suffix, state));
|
|
96
|
+
return element;
|
|
97
|
+
};
|
|
98
|
+
var rowError = (index, message, hint) => ({
|
|
99
|
+
path: `items[${index}]`,
|
|
100
|
+
message,
|
|
101
|
+
hint
|
|
102
|
+
});
|
|
103
|
+
var keysOf = (row) => Object.keys(row).join(", ") || "(nothing)";
|
|
104
|
+
/** Expand one repeat into the single upsertElement it stands for: the wrapper, with the template rendered once per
|
|
105
|
+
* row as its children. Returns the errors instead of throwing, so a bad row reads like every other op error. */
|
|
106
|
+
var expandRepeat = (op) => {
|
|
107
|
+
const errors = [];
|
|
108
|
+
const children = [];
|
|
109
|
+
let total = op.items.length;
|
|
110
|
+
for (const [index, row] of op.items.entries()) {
|
|
111
|
+
const state = {
|
|
112
|
+
missing: /* @__PURE__ */ new Set(),
|
|
113
|
+
notAList: /* @__PURE__ */ new Set(),
|
|
114
|
+
rows: 0
|
|
115
|
+
};
|
|
116
|
+
const expanded = expandNode(op.template, row, `-${index + 1}`, state);
|
|
117
|
+
total += state.rows;
|
|
118
|
+
if (state.missing.size > 0) {
|
|
119
|
+
const fields = [...state.missing];
|
|
120
|
+
errors.push(rowError(index, `Row ${index + 1} has no ${fields.map((field) => `"${field}"`).join(", ")}`, `The template reads it as {{item.${fields[0]}}}. This row carries: ${keysOf(row)}`));
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (state.notAList.size > 0) {
|
|
124
|
+
const [spec] = [...state.notAList];
|
|
125
|
+
errors.push(rowError(index, `Row ${index + 1} has no list at ${spec}`, `A nested repeat needs an ARRAY in that field. This row carries: ${keysOf(row)}`));
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
children.push(expanded);
|
|
129
|
+
}
|
|
130
|
+
if (errors.length > 0) return { errors };
|
|
131
|
+
if (total > MAX_ROWS) return { errors: [{
|
|
132
|
+
path: "items",
|
|
133
|
+
message: `This repeat expands to ${total} rows (max ${MAX_ROWS})`,
|
|
134
|
+
hint: "Shorten the lists, or split the widget into several repeats."
|
|
135
|
+
}] };
|
|
136
|
+
return {
|
|
137
|
+
element: {
|
|
138
|
+
ref: op.ref,
|
|
139
|
+
type: op.elementType ?? "container",
|
|
140
|
+
...op.label === void 0 ? {} : { label: op.label },
|
|
141
|
+
...op.style === void 0 ? {} : { style: op.style },
|
|
142
|
+
children
|
|
143
|
+
},
|
|
144
|
+
errors
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
//#endregion
|
|
148
|
+
export { expandRepeat, repeatElementOp };
|
|
@@ -9,7 +9,7 @@ var deleteInteractionOp = z.object({
|
|
|
9
9
|
ref: z.string().describe("Element ref or id"),
|
|
10
10
|
flowId: z.string().optional().describe("Remove the whole flow with this id (the trigger node id)"),
|
|
11
11
|
nodeId: z.string().optional().describe("Remove a single step; its neighbors are re-linked. Deleting a trigger removes its flow")
|
|
12
|
-
}).describe("DESTRUCTIVE, not undoable — remove
|
|
12
|
+
}).describe("DESTRUCTIVE, not undoable — remove a flow (by flowId) or a single step (by nodeId); provide exactly one. To turn a step OFF instead, patchInteractionNode { enabled: false }. Confirm with the user first.");
|
|
13
13
|
var removeFlow = (interactions, flowId) => {
|
|
14
14
|
let deleted = 0;
|
|
15
15
|
for (const [nodeId, node] of Object.entries(interactions)) if ((node.flowId || node.id) === flowId) {
|