@plitzi/sdk-mcp 0.37.2 → 0.37.3
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 +1156 -0
- package/README.md +6 -2
- package/dist/createServer.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/modules/mcp/accessibilityOutline.d.ts +38 -0
- package/dist/modules/mcp/accessibilityOutline.js +75 -0
- package/dist/modules/mcp/accessibilityOutline.test.d.ts +1 -0
- package/dist/modules/mcp/catalogs/index.d.ts +2 -2
- package/dist/modules/mcp/catalogs/index.js +2 -2
- package/dist/modules/mcp/catalogs/observed.js +2 -2
- package/dist/modules/mcp/catalogs/pluginCatalog.d.ts +6 -3
- package/dist/modules/mcp/catalogs/pluginCatalog.js +12 -7
- package/dist/modules/mcp/helpers/agentPrompt.js +22 -5
- package/dist/modules/mcp/helpers/guide.d.ts +2 -2
- package/dist/modules/mcp/helpers/guide.js +212 -48
- package/dist/modules/mcp/helpers/interactions.d.ts +2 -1
- package/dist/modules/mcp/helpers/interactions.js +2 -0
- package/dist/modules/mcp/helpers/space.d.ts +6 -4
- package/dist/modules/mcp/helpers/space.js +7 -20
- package/dist/modules/mcp/helpers/uris.d.ts +3 -1
- package/dist/modules/mcp/helpers/uris.js +4 -1
- package/dist/modules/mcp/index.d.ts +2 -0
- package/dist/modules/mcp/localScreenshotClient.d.ts +11 -0
- package/dist/modules/mcp/localScreenshotClient.js +24 -9
- package/dist/modules/mcp/proxy/guard.d.ts +6 -0
- package/dist/modules/mcp/proxy/guard.js +8 -12
- package/dist/modules/mcp/resources/functions.d.ts +8 -0
- package/dist/modules/mcp/resources/functions.js +35 -0
- package/dist/modules/mcp/resources/register.d.ts +2 -1
- package/dist/modules/mcp/resources/register.js +49 -2
- package/dist/modules/mcp/resources/renderGuide.js +3 -2
- package/dist/modules/mcp/resources/router.js +3 -1
- package/dist/modules/mcp/screenshotClient.js +12 -4
- package/dist/modules/mcp/server.js +20 -10
- package/dist/modules/mcp/tests/guide.test.d.ts +1 -0
- package/dist/modules/mcp/tools/apply/dispatch.js +9 -1
- package/dist/modules/mcp/tools/apply/index.d.ts +34 -0
- package/dist/modules/mcp/tools/apply/index.js +35 -70
- package/dist/modules/mcp/tools/index.js +4 -2
- package/dist/modules/mcp/tools/operations/connectors/manifest.d.ts +0 -1
- package/dist/modules/mcp/tools/operations/functions/functions.test.d.ts +1 -0
- package/dist/modules/mcp/tools/operations/functions/index.d.ts +31 -0
- package/dist/modules/mcp/tools/operations/functions/index.js +48 -0
- package/dist/modules/mcp/tools/operations/index.d.ts +122 -2
- package/dist/modules/mcp/tools/operations/index.js +4 -3
- package/dist/modules/mcp/tools/operations/schema/elements/deleteElement.js +5 -6
- package/dist/modules/mcp/tools/operations/schema/elements/moveElement.js +3 -11
- package/dist/modules/mcp/tools/operations/schema/elements/upsertElement.js +6 -9
- package/dist/modules/mcp/tools/operations/schema/interactions/patchInteractionNode.d.ts +5 -0
- package/dist/modules/mcp/tools/operations/schema/interactions/patchInteractionNode.js +3 -1
- package/dist/modules/mcp/tools/operations/schema/interactions/upsertInteractionFlow.d.ts +5 -0
- package/dist/modules/mcp/tools/operations/schema/operations.d.ts +27 -0
- package/dist/modules/mcp/tools/operations/schema/pages/deleteLayout.js +6 -5
- package/dist/modules/mcp/tools/operations/schema/pages/deletePage.js +6 -5
- package/dist/modules/mcp/tools/operations/schema/settings/patchSettings.d.ts +17 -0
- package/dist/modules/mcp/tools/operations/schema/settings/patchSettings.js +34 -5
- package/dist/modules/mcp/tools/operations/schema/shared.d.ts +6 -0
- package/dist/modules/mcp/tools/operations/schema/shared.js +5 -0
- package/dist/modules/mcp/tools/operations/schema/translator.js +3 -2
- package/dist/modules/mcp/tools/operations/schema/write.d.ts +10 -5
- package/dist/modules/mcp/tools/operations/schema/write.js +16 -16
- package/dist/modules/mcp/tools/preview.d.ts +27 -0
- package/dist/modules/mcp/tools/render.d.ts +27 -0
- package/dist/modules/mcp/tools/render.js +7 -43
- package/dist/modules/mcp/tools/screenshot.d.ts +39 -0
- package/dist/modules/mcp/tools/screenshot.js +40 -5
- package/dist/modules/mcp/tools/shared/draftBatch.d.ts +25 -0
- package/dist/modules/mcp/tools/shared/draftBatch.js +58 -0
- package/dist/modules/mcp/tools/shared/draftBatch.test.d.ts +1 -0
- package/dist/modules/mcp/tools/shared/lintDraft.d.ts +30 -0
- package/dist/modules/mcp/tools/shared/lintDraft.js +142 -0
- package/dist/modules/mcp/tools/shared/lintDraft.test.d.ts +1 -0
- package/dist/modules/mcp/tools/shared/tool.d.ts +4 -0
- package/dist/modules/mcp/tools/shared/validator/bindings.d.ts +0 -4
- package/dist/modules/mcp/tools/shared/validator/bindings.js +1 -60
- package/dist/modules/mcp/tools/shared/validator/elements.js +3 -15
- package/dist/modules/mcp/tools/shared/validator/index.d.ts +1 -4
- package/dist/modules/mcp/tools/shared/validator/index.js +25 -20
- package/dist/modules/mcp/tools/shared/validator/interactions.js +3 -58
- package/dist/modules/mcp/tools/tryFunction.d.ts +18 -0
- package/dist/modules/mcp/tools/tryFunction.js +43 -0
- package/dist/modules/mcp/tools/validate.d.ts +34 -0
- package/dist/modules/mcp/tools/validate.js +9 -26
- package/dist/modules/mcp/types/aiSchema.d.ts +8 -1
- package/dist/modules/mcp/types/previewTypes.d.ts +6 -0
- package/dist/modules/mcp/types/screenshotTypes.d.ts +16 -0
- package/dist/modules/mcp/types/toolTypes.d.ts +5 -1
- package/dist/preview/createPreview.js +5 -4
- package/dist/stages/preview.js +15 -1
- package/package.json +11 -10
- package/skills/plitzi-render/SKILL.md +1 -2
- package/dist/modules/mcp/tools/shared/validator/audit.d.ts +0 -4
- package/dist/modules/mcp/tools/shared/validator/audit.js +0 -127
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { BUILTIN_TRANSFORMERS, GLOBAL_SOURCES } from "@plitzi/sdk-authoring";
|
|
1
2
|
//#region src/modules/mcp/helpers/guide.ts
|
|
2
|
-
var
|
|
3
|
+
var GLOBALS = GLOBAL_SOURCES.map((source) => `\`${source}\``).join(", ");
|
|
4
|
+
var OTHER_TRANSFORMERS = Object.keys(BUILTIN_TRANSFORMERS).filter((action) => action !== "twigTemplate").map((action) => `\`${action}\``).join(", ");
|
|
5
|
+
var serverInstructions = "Plitzi AI server: read-then-write editing of a Plitzi space. Reads follow a filesystem model — list cheap, read one item in detail on demand; never fetch a whole tree you do not need. Workflow: (1) read plitzi://primer/{env} once — it bundles a quickstart of the guide, types, css-properties and page/definition/variable summaries in a single call; (2) plitzi_search with include:\"detail\" to jump to elements — each hit then carries its uri, stateVersion AND full style/resolvedStyle, so an edit needs no per-element read; open a page skeleton or element only when you need its tree/detail (the skeleton already lists the style classes of each node, and plitzi_read fetches many uris at once); (3) plitzi_apply with dryRun to preview a batch; (4) plitzi_apply to persist, passing expectedResourceVersions to guard against concurrent edits — apply and search both hand back the versions you need for the next edit. Use patchElement / patchDefinition to change only some props / CSS (the upsert variants replace them all). An element read (and search include:\"detail\") inlines the CSS of the definitions it attaches under resolvedStyle, so you rarely need a separate definition read. Every element has ONE name and it IS its id ([A-Za-z0-9_-] starting with a letter, unique, chosen by you) — the same name is the runtime wiring key, so a provider source is `<type>_<name>.<field>`, visible to the provider’s DESCENDANTS only (bind inside its subtree). CSS is plain kebab-case — write shorthands freely (`border: 1px solid red`, `padding: 8px 16px`, `font: bold 16px/1.5 Arial`), they are expanded to longhands for you and STORED that way, so read-back shows the longhands; style vars are var(--name), schema vars are {{name}}. READERS — do not confuse them: MCP *resources* are the browsable catalog (list them, or open one by URI); plitzi_search FINDS elements by label/type/attribute and hands you the name to write ops against; plitzi_read BATCH-fetches URIs you already hold. Reach for search/read to work; browse resources to discover. CMS / API integrations (Strapi, WordPress, Contentful, any REST service) go through a CONNECTOR: a manifest you author declaring the base URL, endpoints and auth template, which the SERVER executes — read plitzi://connector-presets for working ones and plitzi://connectors/{env} for this space’s. You never see or create the credential; the space owner attaches it, and the connector saves without one. An apiContainer reads through it only with runtime:\"server\". Elements also carry applied style variants + visibility (initialState), data bindings and interaction flows: edit them with patchElement (initialState), upsertBinding/patchBinding/deleteBinding, and upsertInteractionFlow/patchInteractionNode/deleteInteraction. An element read shows all three plus availableVariants (which variant each of its classes offers). Build pages people can use without sight: screen readers and browser agents (Claude in Chrome) work a page through its accessibility tree, so every control needs words and clicks go on buttons and links — see Accessibility in plitzi://guide, and plitzi_screenshot view:\"accessibility\" to check. Separately, to SHOW the user a small self-contained widget (offline, no space or backend) instead of editing the space — a card, hero, pricing table, a visual answer — use plitzi_render; read plitzi://render/guide for it.";
|
|
3
6
|
var widgetsOnlyInstructions = "Plitzi widget server (widgets-only connection). This connection carries NO Plitzi space: nothing can be read or edited in one, and the space tools are not offered here — do not look for them. What it does do is build self-contained UI widgets fully offline, with no backend, account or setup: call plitzi_render to SHOW the user a real rendered layout (card, hero, pricing table, checklist, form, gallery) instead of describing one. Read plitzi://render/guide first — the element/prop table, the style model and a worked example; plitzi://render/types lists every element type you can author (plitzi_read fetches both). To change a widget you already rendered, call plitzi_render again with patch:true and its renderId. If the user wants to edit their real Plitzi space from here, that is a reconnection they make: the integration must be re-authorized and granted a space (the consent screen lists theirs) — you cannot do it from this connection, and no retry will change it.";
|
|
4
7
|
var guideQuickstart = `# Plitzi AI MCP — quickstart
|
|
5
8
|
This is the condensed guide; read \`plitzi://guide\` for the full reference (every resource, op and example).
|
|
@@ -35,7 +38,7 @@ still \`display: flex\` + \`flex-direction\`/\`align-items\`/…, not a \`flex\`
|
|
|
35
38
|
|
|
36
39
|
**Data bindings** (\`upsertBinding\`, category attributes|style|initialState): connect a \`source\` to a \`to\` field.
|
|
37
40
|
A source \`<type>_<name>\` is scoped to the provider's **DESCENDANTS only** — bind inside the provider's subtree
|
|
38
|
-
(
|
|
41
|
+
(the global sources ${GLOBALS} are bindable anywhere). \`apiContainer.mockData\` is builder-only; set a
|
|
39
42
|
real \`query\` for production. \`transformers: [{action, params}]\` post-process the value — use exact action names
|
|
40
43
|
from \`plitzi://data-sources\`; \`twigTemplate\` formats it (the value is \`{{source}}\`, not \`{{value}}\`). \`when\` is
|
|
41
44
|
a QueryBuilder RuleGroup gating the binding.
|
|
@@ -46,6 +49,8 @@ computed for you). Node types: \`callback\` (an element's own callback — \`ele
|
|
|
46
49
|
(category/key/value/revertOnFinish) ≠ global \`setState\` (source \`state\`, key/type/value). To turn a step off use
|
|
47
50
|
\`patchInteractionNode {enabled:false}\` — \`deleteInteraction\` removes it (destructive; confirm first). Any param
|
|
48
51
|
**value** can be a binding token \`{{ source }}\` (e.g. notification \`content: "{{ list_<name>.item.name }}"\`).
|
|
52
|
+
Steps read the page as it is when they run; a refiring trigger is ignored unless \`whileRunning: "queue"\`; keyboard
|
|
53
|
+
shortcuts are the \`onKey\` trigger (\`keys: "shift+f, escape"\`).
|
|
49
54
|
|
|
50
55
|
**CMS / API integrations** (\`upsertConnector\`): a **connector** is a manifest declaring a provider's base URL,
|
|
51
56
|
endpoints, auth template and filter operators, executed by the **server** — so integrating Strapi, WordPress,
|
|
@@ -62,11 +67,16 @@ slugs prepend the path). A \`:name\` segment (\`"posts/:postId"\`) is a route pa
|
|
|
62
67
|
source \`navigation.routeParams.name\` → build dynamic pages this way. To move between pages **prefer the \`Link\`
|
|
63
68
|
element** (a container: \`mode\` "page"/"internal"/"external") over a \`navigate\` interaction.
|
|
64
69
|
|
|
65
|
-
**
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
**Accessible by default:** screen readers and browser agents (Claude in Chrome) read the accessibility tree, so every
|
|
71
|
+
button/link needs words (an icon-only button a \`title\`), every field a \`label\` (\`hideLabel: true\` hides it), every
|
|
72
|
+
image an \`alt\` or \`decorative: true\`; clicks go on a \`button\`/\`link\`, never a container; headings step down one
|
|
73
|
+
level at a time. \`plitzi_screenshot view:"accessibility"\` shows the tree and what has no name.
|
|
74
|
+
|
|
75
|
+
**Touched elements must be malformation-free.** Editing an element also checks its CURRENT stored content and BLOCKS
|
|
76
|
+
the save on any \`Pre-existing malformation in element …\` error (a broken transformer, a malformed step, an attribute
|
|
77
|
+
it never reads) — even parts you did not touch. These are NOT from your change (the message says so); fix them in the
|
|
68
78
|
SAME batch and re-apply (the check runs on the result, so the fix unblocks it). \`Pre-existing issue\` warnings advise
|
|
69
|
-
but do not block.
|
|
79
|
+
but do not block. A broken tree your batch leaves ANYWHERE (an element orphaned by a delete) blocks it too.
|
|
70
80
|
|
|
71
81
|
Read \`plitzi://guide\` before anything above is unclear.
|
|
72
82
|
`;
|
|
@@ -101,6 +111,8 @@ Never download a whole tree you do not need.
|
|
|
101
111
|
\`link\` navigates between pages, \`list\` repeats a template over a data array. \`plugin\` types are custom elements.
|
|
102
112
|
- \`plitzi://css-properties\` — valid kebab-case CSS property keys.
|
|
103
113
|
- \`plitzi://schema/{env}/pages\` — page **summaries** (ref, label, elementCount, folder). No element trees.
|
|
114
|
+
- \`plitzi://schema/{env}/layouts\` — the shared **layout shells** (header/sidebar/footer) and the pages rendered
|
|
115
|
+
inside each. Read one like a page. See *Shared layouts* below.
|
|
104
116
|
- \`plitzi://folders/{env}\` — page **folders** (the sidebar tree): ref, name, slug, parentId. \`/{ref}\` for one.
|
|
105
117
|
- \`plitzi://schema/{env}/pages/{ref}\` — one page as a **skeleton tree**: each node is \`ref/type/label\` **plus the
|
|
106
118
|
style classes it attaches** (\`base\`, and \`slots\` for non-base slots) — names only, no CSS. So you can map every
|
|
@@ -119,6 +131,7 @@ Never download a whole tree you do not need.
|
|
|
119
131
|
- \`plitzi://global-styles/{env}\` — element **types** that have a site-wide global style. \`/{componentType}\` for one.
|
|
120
132
|
- \`plitzi://id-styles/{env}\` — DOM **ids** that have an id rule (\`#id\`) targeting a single element. \`/{targetId}\` for one.
|
|
121
133
|
- \`plitzi://style-variables/{env}\` — design tokens by category. \`/{category}\` for one.
|
|
134
|
+
- \`plitzi://fonts/{env}\` — the font **families the space loads**, with their source and fallback. \`/{family}\` for one.
|
|
122
135
|
- \`plitzi://schema-variables/{env}\` — space-level values referenced in props as \`{{name}}\`.
|
|
123
136
|
- \`plitzi://settings/{env}\` — space-level settings: the global \`customCss\` and the state/auth (user-provider) config.
|
|
124
137
|
- \`plitzi://interactions/{env}\` — interaction **actions** observed in this space (grouped by node type): the
|
|
@@ -132,6 +145,16 @@ Never download a whole tree you do not need.
|
|
|
132
145
|
filter operators and its published fields. \`/{ref}\` opens one manifest in full. See *Connectors* below.
|
|
133
146
|
- \`plitzi://actions/{env}\` — the **server actions this space has**: what starts each one, who may run it, and the
|
|
134
147
|
input/output contract a caller is held to. \`/{ref}\` opens one flow in full. See *Server actions* below.
|
|
148
|
+
- \`plitzi://actions/{env}/tasks\` — the **server tasks this deployment can run**, each with its params: the steps an
|
|
149
|
+
action may be built from.
|
|
150
|
+
- \`plitzi://functions/{env}\` — the space's **functions**, its own server code: which files, and the tasks, routes and
|
|
151
|
+
hosts they declare. \`plitzi://functions/{env}/{+path}\` reads one file whole. See *Functions* below.
|
|
152
|
+
- \`plitzi://changes/{env}\` — the space's **change history**, read-only and newest first: who made each change (a
|
|
153
|
+
person in the builder, an agent, the autofix), when, and every element, class, token or font it touched, whole, before
|
|
154
|
+
and after. \`/{id}\` for the history of one of them — how you find what changed an element you are asked about, or
|
|
155
|
+
review what you or another agent just did.
|
|
156
|
+
- \`plitzi://render/guide\`, \`plitzi://render/types\` — the manual and the type table for \`plitzi_render\` (offline
|
|
157
|
+
widgets, not this space).
|
|
135
158
|
|
|
136
159
|
The style resources also answer under the \`plitzi://schema/{env}/…\` root as aliases — \`plitzi://schema/{env}/definitions/{ref}\`, \`plitzi://schema/{env}/style-variables/{category}\`, \`plitzi://schema/{env}/schema-variables\` — but prefer the ready-made \`uri\` from search / a write response over hand-building either form.
|
|
137
160
|
|
|
@@ -161,7 +184,7 @@ element read is the exception. Search also matches **pages** by name/slug (retur
|
|
|
161
184
|
stateVersion) and returns any **style definitions** whose name matches the query, with full CSS, under \`definitions\`.
|
|
162
185
|
When you do hold several refs to open (e.g. from a skeleton), read them together with \`plitzi_read\` rather than one at a time.
|
|
163
186
|
|
|
164
|
-
## Tools
|
|
187
|
+
## Tools
|
|
165
188
|
- \`plitzi_validate\` — check a batch, returns teachable errors/warnings. Writes nothing. Also reports **pre-existing
|
|
166
189
|
malformations** in any resource the batch touches (see below).
|
|
167
190
|
- \`plitzi_apply\` — validate → apply → persist atomically. Rejects the whole batch on any error or conflict. Pass
|
|
@@ -171,6 +194,13 @@ When you do hold several refs to open (e.g. from a skeleton), read them together
|
|
|
171
194
|
- \`plitzi_read\` — read many resource **uris in one batch** (pages, elements, definitions, variables). Pass the
|
|
172
195
|
ready-made uris from search / a write response; each result is \`{ uri, stateVersion, data }\` or a teachable error,
|
|
173
196
|
so one bad uri never fails the batch. Use it instead of N single reads whenever you already hold several refs.
|
|
197
|
+
- \`plitzi_preview\` — render a page to HTML, optionally with unsaved \`operations\` applied, to check structure.
|
|
198
|
+
- \`plitzi_screenshot\` — render a page to a PNG (desktop, mobile or \`both\`), optionally with unsaved \`operations\`:
|
|
199
|
+
the way to SEE overflow, misalignment and broken layout before you commit. \`view: "accessibility"\` reads the same
|
|
200
|
+
page as a screen reader and a browser agent (Claude in Chrome) do — its accessibility tree as an outline of roles and
|
|
201
|
+
names, with every control and picture that has no name listed in \`unnamed\` — as text, far cheaper than an image
|
|
202
|
+
(\`"both"\` returns both). Both need the SSR render service; where it is missing they answer \`PREVIEW_UNAVAILABLE\`.
|
|
203
|
+
- \`plitzi_render\` — a different job: show the user a self-contained widget built offline. It never touches the space.
|
|
174
204
|
|
|
175
205
|
## Readers: resources vs plitzi_search vs plitzi_read (do not confuse them)
|
|
176
206
|
Three ways to read, each for a different moment — pick by what you have in hand:
|
|
@@ -215,6 +245,15 @@ reports it only when it is set.
|
|
|
215
245
|
every interaction target across the space that named the old one is repointed with it, so nothing comes unwired.
|
|
216
246
|
You do not have to rewrite them.
|
|
217
247
|
|
|
248
|
+
## Elements — create, move, delete
|
|
249
|
+
- \`upsertElement\` creates an element (or replaces one by name) under \`parentRef\` — the page root when omitted — and
|
|
250
|
+
builds a whole subtree in one op through nested \`children\`. \`patchElement\` changes an existing one in place.
|
|
251
|
+
- \`moveElement { pageRef, ref, toParentRef, position }\` moves or reorders one: \`position\` \`"inside"\` (the default)
|
|
252
|
+
nests it in \`toParentRef\`; \`"before"\`/\`"after"\` make it that element's sibling. Its subtree moves with it.
|
|
253
|
+
- \`deleteElement { pageRef, ref }\` removes an element **and everything under it**. A binding or an interaction
|
|
254
|
+
elsewhere that named one of them is left pointing at nothing, and the save reports it — delete them in the same
|
|
255
|
+
batch. Destructive: confirm first.
|
|
256
|
+
|
|
218
257
|
## Styling (crosses both schemas)
|
|
219
258
|
- **Mind the type's intrinsic default style.** A type renders with a base CSS *before* any class is attached — read
|
|
220
259
|
it from \`defaultStyle\` on the type in \`plitzi://types\` (the primer includes it). Do not assume \`display: block\`:
|
|
@@ -290,6 +329,8 @@ An element read reports \`availableVariants\` (which variant each attached class
|
|
|
290
329
|
\`initialState\`, so you can see a button **has** a \`primary\` variant and whether it uses it. If the user asks for a
|
|
291
330
|
variant that does not exist yet, **create it (upsertDefinition variants) and apply it in the same batch**.
|
|
292
331
|
- \`initialState.visibility\` (boolean) sets whether the element starts shown or hidden.
|
|
332
|
+
- The \`hidden\` state of a class is how it looks while hidden — where it goes and where it comes from. With a
|
|
333
|
+
\`transition\` that includes \`display 200ms allow-discrete\`, it fades or slides in and out instead of blinking.
|
|
293
334
|
|
|
294
335
|
**Ancestor conditions** — a class that changes inside an ANCESTOR (\`.toolbar .icon\`), or while that ancestor is
|
|
295
336
|
hovered or in a variant (\`.card:hover .icon\`, a collapsed sidebar hiding its labels) is \`ancestors\` on that class,
|
|
@@ -297,23 +338,42 @@ keyed by a class the ancestor wears: \`ancestors: { "toolbar": { "desktop": {
|
|
|
297
338
|
{ "desktop": { … } } } }, "sidebar": { "variants": { "collapsed": … } } }\`.
|
|
298
339
|
Any ancestor counts, not only the parent; the class's own states and variants win over these. Never customCss for it.
|
|
299
340
|
|
|
341
|
+
## Variables, design tokens and fonts
|
|
342
|
+
- **Schema variables** are space-wide values a prop reads as \`{{name}}\`: \`upsertVariable { name, variableType,
|
|
343
|
+
value, category?, subValues? }\` (\`variableType\` is the runtime type — \`text\`, \`number\`, … — not the op's
|
|
344
|
+
\`type\`), \`deleteVariable { name }\`.
|
|
345
|
+
- **Design tokens** are CSS values a definition reads as \`var(--name)\`: \`upsertStyleVariable { category, name,
|
|
346
|
+
value }\` with \`category\` one of \`color\`, \`spacing\`, \`shadow\`, \`custom\` and \`name\` without the \`--\`. A
|
|
347
|
+
\`value\` may be one string or \`{ light, dark }\`, so a colour follows the theme. \`deleteStyleVariable\` removes one.
|
|
348
|
+
Reach for a token whenever a value repeats across classes — a hardcoded colour is a colour the theme cannot change.
|
|
349
|
+
- **Fonts**: a \`font-family\` only renders in its face when the space loads that family. \`upsertFont { family,
|
|
350
|
+
source, fallback, weights? }\` declares one — \`source\` \`google\` needs nothing else, \`remote\` a \`stylesheet\` or
|
|
351
|
+
\`files\` URL, \`system\` loads nothing. \`deleteFont { family }\` stops loading it; rules that still name it render
|
|
352
|
+
in their fallback.
|
|
353
|
+
|
|
300
354
|
## Data bindings
|
|
301
355
|
Connect a data **source** to an element field. A binding is \`{ to, source, transformers?, when?, enabled? }\` grouped
|
|
302
356
|
by **category**: \`attributes\` (a prop), \`style\` (a style value), \`initialState\` (an initial-state key).
|
|
303
357
|
- \`upsertBinding\` adds one, or replaces the binding already feeding the same \`to\` (or \`id\`).
|
|
304
358
|
- \`patchBinding\` edits an existing one (matched by \`to\`/\`id\`); \`deleteBinding\` removes it.
|
|
305
|
-
Discover valid source paths **and the transformer catalog** in \`plitzi://data-sources/{env}\`. Example — feed
|
|
306
|
-
|
|
359
|
+
Discover valid source paths **and the transformer catalog** in \`plitzi://data-sources/{env}\`. Example — feed a
|
|
360
|
+
provider's records into a list element:
|
|
307
361
|
\`{ "type": "upsertBinding", "pageRef": "home", "ref": "myList", "category": "attributes",
|
|
308
|
-
"binding": { "to": "items", "source": "
|
|
362
|
+
"binding": { "to": "items", "source": "apiContainer_products.records" } }\`.
|
|
363
|
+
|
|
364
|
+
**What an \`apiContainer\` publishes** depends on where it reads. Through a connector (\`runtime: "server"\`) it is the
|
|
365
|
+
fixed shape under *Connectors* — \`.records\` or \`.record\`, \`.pageInfo\`, … With a browser \`query\` it is the
|
|
366
|
+
response: \`.data\` (the parsed body — an API answering \`{ "items": [...] }\` is read as
|
|
367
|
+
\`apiContainer_<name>.data.items\`) and \`.status\` (the HTTP status). Both add \`.isLoading\`, \`.isEmpty\`,
|
|
368
|
+
\`.hasError\` and \`.errorMessage\`.
|
|
309
369
|
|
|
310
370
|
**Source scope — a source is visible to the provider's DESCENDANTS only.** An element source named
|
|
311
371
|
\`<type>_<name>\` (e.g. \`apiContainer_products\`, \`list_food-list\`) is published by that element into the scope of
|
|
312
372
|
its **subtree**, so **only elements INSIDE the provider can bind to it**. Binding a sibling or an unrelated element
|
|
313
|
-
to it resolves to nothing at runtime. So to consume \`apiContainer_products.
|
|
373
|
+
to it resolves to nothing at runtime. So to consume \`apiContainer_products.records\`, the bound element must live under
|
|
314
374
|
that apiContainer; inside a \`list\`, the repeated \`listItem\` and its children read the per-row source
|
|
315
|
-
(\`list_<name>.item.<field>\`).
|
|
316
|
-
|
|
375
|
+
(\`list_<name>.item.<field>\`). The global sources — ${GLOBALS} — have no \`<type>_<name>\` head and are
|
|
376
|
+
bindable anywhere. Binding an element to an element source outside its
|
|
317
377
|
provider's subtree is schema-valid but **broken at runtime** (the source is not in scope), so
|
|
318
378
|
\`plitzi_validate\`/\`plitzi_apply\` treat it as an **error and reject the batch** — move the element under the
|
|
319
379
|
provider, or bind a source that is in scope.
|
|
@@ -332,9 +392,8 @@ show a number with units:
|
|
|
332
392
|
\`{ "type": "upsertBinding", "pageRef": "home", "ref": "food-item-time", "category": "attributes",
|
|
333
393
|
"binding": { "to": "content", "source": "list_food-list.item.cookTimeMinutes",
|
|
334
394
|
"transformers": [ { "action": "twigTemplate", "params": { "template": "{{source}} min de cocción" } } ] } }\`.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
strings. Each transformer also takes an optional \`enabled\` flag: set \`"enabled": false\` to keep it in the chain but
|
|
395
|
+
The others: ${OTHER_TRANSFORMERS} — each one's purpose and params are in \`plitzi://data-sources/{env}\`.
|
|
396
|
+
Transformer \`params\` values are strings. Each transformer also takes an optional \`enabled\` flag: set \`"enabled": false\` to keep it in the chain but
|
|
338
397
|
skip it at runtime (defaults to true) — the value passes through untouched, and a disabled transformer is not
|
|
339
398
|
validated.
|
|
340
399
|
|
|
@@ -349,6 +408,14 @@ An interaction **flow** is a **trigger** (an event like \`onClick\`, \`onPageLoa
|
|
|
349
408
|
it runs, in order. You pass the steps **in order** and the stored beforeNode/afterNode/flowId links are computed for
|
|
350
409
|
you — never wire them by hand. Each step also has an \`enabled\` flag (see disable vs delete below).
|
|
351
410
|
|
|
411
|
+
**Each step reads the page as it is when it runs**: a \`when\` or a \`{{ state.x }}\` after a \`setState\` sees the new
|
|
412
|
+
value, and one after a \`delay\` sees what changed meanwhile. To act on the value from BEFORE a write, put the reading
|
|
413
|
+
step first. **A trigger fired again while its flow runs is ignored** (no double submit); set \`"whileRunning": "queue"\`
|
|
414
|
+
on the trigger node for a stream of events that must each run in order (\`"parallel"\` runs them at once).
|
|
415
|
+
**Keyboard shortcuts** are a trigger every element has: \`onKey\` with param \`keys\` — \`"f"\`, \`"shift+f"\`,
|
|
416
|
+
\`"mod+k"\` (⌘ on a Mac, Ctrl elsewhere), \`"plus, ="\`, \`"escape"\`. Heard on the whole page while the element is
|
|
417
|
+
mounted, ignored while someone types in a field; the flow reads the key pressed as \`{{ <trigger id>.key }}\`.
|
|
418
|
+
|
|
352
419
|
**Node types & \`elementId\`** — a step names which element (or module) provides the callback it runs. Picking the
|
|
353
420
|
**wrong node type for an action** makes the runtime resolve it against nothing, so the step **silently does nothing**:
|
|
354
421
|
- \`trigger\` — the event; belongs to the host element. \`elementId\` defaults to the host.
|
|
@@ -363,15 +430,16 @@ you — never wire them by hand. Each step also has an \`enabled\` flag (see dis
|
|
|
363
430
|
\`setState\` has **no** \`type\` param (that belongs to the global one below). Beside it every element registers
|
|
364
431
|
**\`toggleState\`** — the same write with **no \`value\`**: it stores the OPPOSITE of what is there
|
|
365
432
|
(\`category\`, \`key\`, \`revertOnFinish\`). That is how expand/collapse is authored — **one step on one trigger**,
|
|
366
|
-
never two \`setState\` branches under complementary \`when\` conditions
|
|
367
|
-
|
|
433
|
+
never two \`setState\` branches under complementary \`when\` conditions: every step reads the page as it is when it
|
|
434
|
+
runs, so the second sees what the first wrote and flips it back. An element type may also register its own extra
|
|
435
|
+
callbacks.
|
|
368
436
|
- \`globalCallback\` — a callback provided by a **source module**, NOT by any element: \`addNotification\` (source
|
|
369
437
|
\`space\`), \`setState\`/\`toggleState\`/\`clearState\` (\`state\`), \`navigate\` (\`navigation\`), \`login\`/\`logout\`/
|
|
370
438
|
\`refreshDetails\` (\`auth\`), \`runServerAction\`/\`cancelServerAction\` (\`actions\`). Its \`elementId\` is the
|
|
371
439
|
**source module id**, never the host element — a node that stored the host element's name here would resolve to
|
|
372
440
|
nothing at runtime. **Omit \`elementId\`**: the MCP sets the correct source and fills the
|
|
373
441
|
builder's **param defaults** (e.g. \`addNotification\` gets \`autoDismiss:true\`, \`autoDismissTimeout:5000\`,
|
|
374
|
-
\`placement:"top-right"\`, \`
|
|
442
|
+
\`placement:"top-right"\`, \`appearance:"success"\`) for any params you leave out. Use **only** the params each
|
|
375
443
|
callback declares (exact spelling) — for \`addNotification\` the visible text goes in \`content\`; there is **no**
|
|
376
444
|
\`title\`/\`message\`/\`type\` param, and any unknown key is dropped. See the full param schema for each callback under
|
|
377
445
|
\`globalCallbacks\` in \`plitzi://interactions/{env}\`.
|
|
@@ -439,26 +507,6 @@ server-rendered ignores its connector entirely.
|
|
|
439
507
|
- \`plitzi://connectors/{env}\` — the connectors **this space already has**, each with its read/write endpoint names,
|
|
440
508
|
its filter operators and its published fields. \`plitzi://connectors/{env}/{ref}\` opens one manifest in full.
|
|
441
509
|
|
|
442
|
-
### Server actions
|
|
443
|
-
Work a page cannot do in the browser — charge a card, send mail, read a system only the server reaches. An action
|
|
444
|
-
is a stored flow the SERVER runs; a page names it and never learns what it does. Write one with
|
|
445
|
-
\`upsertAction\` / \`patchAction\`, then call it from a page flow with the \`runServerAction\` step (mode
|
|
446
|
-
\`await\` to use the result, \`detached\` to fire and carry on).
|
|
447
|
-
|
|
448
|
-
Four rules worth knowing before you write:
|
|
449
|
-
|
|
450
|
-
- **Steps are server tasks.** \`plitzi://actions/{env}/tasks\` lists what this deployment can run — a browser step
|
|
451
|
-
(\`setState\`, \`navigate\`) has nothing to act on here, and the reverse is true too: a task cannot run in a page
|
|
452
|
-
flow.
|
|
453
|
-
- **The contracts.** \`input\` is coerced and anything undeclared is DROPPED. The OUTPUT is whatever the final
|
|
454
|
-
\`flow.output\` step names — there is no separate list to keep in step with it — and that step must be LAST,
|
|
455
|
-
since only the last one that runs is answered. Everything else a step produced stays on the server.
|
|
456
|
-
- **Whether it runs lives on its TRIGGERS.** Each trigger step carries \`enabled\`, and the action is on when any
|
|
457
|
-
way into it is — there is no switch beside the flow. So "pause this action" means disabling its trigger step(s),
|
|
458
|
-
never deleting them.
|
|
459
|
-
- **A step names the credential it uses**, and sees no other. \`credentials\` on the document says which ones the
|
|
460
|
-
action may ask for at all; the secret itself is never yours to see, exactly as with a connector.
|
|
461
|
-
|
|
462
510
|
### Credentials are NOT yours to write
|
|
463
511
|
You never create, see or transmit a secret. Author the manifest with the token in place
|
|
464
512
|
(\`"value": "Bearer {{credential.token}}"\`) and **leave \`connection.credential\` unset** unless the user hands you an
|
|
@@ -514,7 +562,8 @@ The provider element is \`apiContainer\`, and it needs **both halves**:
|
|
|
514
562
|
other than \`list\`), \`filters\`, \`limit\`, \`singleRecord\`, \`pagination\`, \`pageParam\`.
|
|
515
563
|
|
|
516
564
|
It then publishes one source **to its descendants only** (like any provider — bind *inside* its subtree):
|
|
517
|
-
\`apiContainer_<name>.records\` (an array
|
|
565
|
+
\`apiContainer_<name>.records\` (an array of \`{ id, values }\` — a record's fields are under \`values\`, so a row
|
|
566
|
+
reads \`list_<name>.item.values.<field>\`), \`.pageInfo\` (\`page\`, \`pageCount\`, \`total\`, \`hasNextPage\`…),
|
|
518
567
|
\`.isEmpty\`, \`.hasError\`, \`.errorMessage\`, \`.isLoading\`. With \`singleRecord: true\` it publishes \`.record\`
|
|
519
568
|
instead of \`.records\` — that is what a **detail page** uses. Bind an empty-state block's visibility to \`.isEmpty\`
|
|
520
569
|
and an error block's to \`.hasError\`; they are ordinary bindings, no special mechanism.
|
|
@@ -531,7 +580,7 @@ and an error block's to \`.hasError\`; they are ordinary bindings, no special me
|
|
|
531
580
|
{ "type": "upsertBinding", "pageRef": "blog", "ref": "posts", "category": "attributes",
|
|
532
581
|
"binding": { "to": "items", "source": "apiContainer_posts-api.records" } },
|
|
533
582
|
{ "type": "upsertBinding", "pageRef": "blog", "ref": "post-title", "category": "attributes",
|
|
534
|
-
"binding": { "to": "content", "source": "list_posts.item.title" } } ] }
|
|
583
|
+
"binding": { "to": "content", "source": "list_posts.item.values.title" } } ] }
|
|
535
584
|
\`\`\`
|
|
536
585
|
|
|
537
586
|
**A detail page** is the same provider with \`singleRecord\` and a filter resolved from the URL. Create the page with a
|
|
@@ -542,7 +591,7 @@ route param (\`"slug": "blog/:postSlug"\`), then filter on it — the filter \`v
|
|
|
542
591
|
"props": { "connector": "strapi-blog", "resource": "articles", "singleRecord": true,
|
|
543
592
|
"filters": [ { "field": "slug", "operator": "eq", "value": "{{routeParams.postSlug}}" } ] } } }
|
|
544
593
|
\`\`\`
|
|
545
|
-
Its children then bind to \`apiContainer_post-api.record.<field>\`. A filter whose template resolves to nothing
|
|
594
|
+
Its children then bind to \`apiContainer_post-api.record.values.<field>\`. A filter whose template resolves to nothing
|
|
546
595
|
returns **no records** rather than the whole collection — a URL that addressed one post never renders a different one.
|
|
547
596
|
|
|
548
597
|
**Paging** is the \`pagination\` prop: \`"none"\`, \`"url"\` (the page number rides the query string, so pages are
|
|
@@ -559,6 +608,73 @@ credential and refuses any action the manifest does not declare. Writes exist **
|
|
|
559
608
|
builder and renders empty when published. If the user has no SSR deployment, say so rather than leaving them to
|
|
560
609
|
discover it — the alternative is a browser-side \`query\`, which cannot keep a secret.
|
|
561
610
|
|
|
611
|
+
## Server actions
|
|
612
|
+
Work a page cannot do in the browser — charge a card, send mail, read a system only the server reaches. An action
|
|
613
|
+
is a stored flow the SERVER runs; a page names it and never learns what it does. Write one with
|
|
614
|
+
\`upsertAction\` / \`patchAction\`, then call it from a page flow with the \`runServerAction\` step (mode
|
|
615
|
+
\`await\` to use the result, \`detached\` to fire and carry on).
|
|
616
|
+
|
|
617
|
+
Four rules worth knowing before you write:
|
|
618
|
+
|
|
619
|
+
- **Steps are server tasks.** \`plitzi://actions/{env}/tasks\` lists what this deployment can run — a browser step
|
|
620
|
+
(\`setState\`, \`navigate\`) has nothing to act on here, and the reverse is true too: a task cannot run in a page
|
|
621
|
+
flow.
|
|
622
|
+
- **The contracts.** \`input\` is coerced and anything undeclared is DROPPED. The OUTPUT is whatever the final
|
|
623
|
+
\`flow.output\` step names — there is no separate list to keep in step with it — and that step must be LAST,
|
|
624
|
+
since only the last one that runs is answered. Everything else a step produced stays on the server.
|
|
625
|
+
- **Whether it runs lives on its TRIGGERS.** Each trigger step carries \`enabled\`, and the action is on when any
|
|
626
|
+
way into it is — there is no switch beside the flow. So "pause this action" means disabling its trigger step(s),
|
|
627
|
+
never deleting them.
|
|
628
|
+
- **A step names the credential it uses**, and sees no other. \`credentials\` on the document says which ones the
|
|
629
|
+
action may ask for at all; the secret itself is never yours to see, exactly as with a connector.
|
|
630
|
+
|
|
631
|
+
\`deleteAction { ref }\` removes one; every \`runServerAction\` step that names it stops resolving, so confirm first.
|
|
632
|
+
|
|
633
|
+
## Functions
|
|
634
|
+
When no server task does what a flow needs — parse a feed, call an API with its own shape, compute something — the
|
|
635
|
+
space can have its own: **functions**, TypeScript the platform builds and runs in a sandbox. Their tasks are steps like
|
|
636
|
+
any other (\`<namespace>.<action>\`), listed in \`plitzi://actions/{env}/tasks\` once saved.
|
|
637
|
+
|
|
638
|
+
- **Write** files with \`upsertFunctionFile { path, content }\` (whole files; \`deleteFunctionFile { path }\` removes
|
|
639
|
+
one). \`index.ts\` default-exports the definition:
|
|
640
|
+
\`export default defineFunctions({ allow: { hosts: ["api.example.com"] }, tasks: [{ namespace, action, title, params, run: async (params, ctx) => … }] })\`,
|
|
641
|
+
\`defineFunctions\` from \`@plitzi/sdk-server/functions\`. Files import each other by relative path — no package,
|
|
642
|
+
no Node built-in.
|
|
643
|
+
- **\`ctx\` is all a function reaches:** \`ctx.kv\` (the space's store), \`ctx.fetch(url, init)\` to the hosts
|
|
644
|
+
\`allow.hosts\` names (\`*.example.com\` for subdomains), \`ctx.publish/grant/revoke\` (channels), \`ctx.log\`,
|
|
645
|
+
\`ctx.user\` (who asked, never their session). A secret is NAMED, never read: \`ctx.fetch(url, { credential: "stripe",
|
|
646
|
+
headers: { authorization: "Bearer {{ credential.apiKey }}" } })\`.
|
|
647
|
+
- **Stateless:** nothing a function keeps in memory outlives the call — a module-level cache or counter starts empty
|
|
648
|
+
every time, on whichever instance runs it. What must last goes in \`ctx.kv\`. A call gets 100 ms of CPU and 10 s.
|
|
649
|
+
- **Saving builds and checks** the files: the batch is refused with each problem where it is (file and line). A
|
|
650
|
+
namespace the platform uses (\`kv\`, \`http\`, \`flow\`…) is refused.
|
|
651
|
+
- **Try it** with \`plitzi_try_function { task, params }\` before a page relies on it: the value, the logs, the error.
|
|
652
|
+
The live site runs what the space was last published with, never the draft. Running functions is part of the paid
|
|
653
|
+
plans: on another plan they save, and a run answers why it did not start — tell the user rather than work around it.
|
|
654
|
+
|
|
655
|
+
## Realtime channels
|
|
656
|
+
Pages that see each other — cursors, presence, a shared board, a game. Three parts, all needed:
|
|
657
|
+
|
|
658
|
+
1. **Declare** the topic pattern with \`patchSettings { channels: { "board:{id}": { access: { mode: "public" }, publish: "server" } } }\`.
|
|
659
|
+
\`publish: "server"\` is for what must be validated and saved first; \`"clients"\` (the default) for what pages send
|
|
660
|
+
directly — add \`presence: true\` for who-is-here.
|
|
661
|
+
2. **Subscribe** with a \`channel\` element whose \`topic\` a pattern matches (\`board:{{ id }}\`, a route param).
|
|
662
|
+
Its descendants bind \`channel_<id>\`: \`connected\`, \`members\`, \`messages\`, \`last\`; flows use its \`onMessage\`,
|
|
663
|
+
\`onJoin\`, \`onLeave\` triggers (\`from\`, \`user\` and the \`state\` the member announced — \`onJoin\` only for who
|
|
664
|
+
came after this page) and its \`publish\` / \`setPresence\` callbacks.
|
|
665
|
+
3. **Announce from the server**: an action whose last step is the \`realtime.publish\` task — validate, save, then say so.
|
|
666
|
+
|
|
667
|
+
A topic no pattern matches is refused by the server and reported by the linter (\`channel-topic\`).
|
|
668
|
+
|
|
669
|
+
**Private topics** (a room behind a password, a customer's order): declare the pattern \`grant: true\`. The action that
|
|
670
|
+
decides the visitor may be there ends with \`realtime.grant { topic }\` and answers its \`grant\`; the \`channel\`
|
|
671
|
+
element binds \`grant\` to it. Without one the topic is refused however well its name is known (\`channel-grant\`).
|
|
672
|
+
\`realtime.revoke { topic, grant }\` takes one back — or all, naming none — and lets go whoever is on it with it.
|
|
673
|
+
|
|
674
|
+
**Two writers at once**: read-change-write with \`kv.setIf\` (written only if the value is still the one read — empty
|
|
675
|
+
\`expected\` = only if nothing is there yet), ordered lists with \`list.put\` / \`list.range\`, and put
|
|
676
|
+
\`flow.rateLimit\` first in any public action that writes.
|
|
677
|
+
|
|
562
678
|
## Shared layouts — the chrome a page does NOT contain
|
|
563
679
|
A page's tree is usually **not the whole page**. The header, the sidebar and the footer normally live in a **layout
|
|
564
680
|
container**: a shell several pages are rendered inside, so the navigation is authored once instead of once per page.
|
|
@@ -640,15 +756,62 @@ Space-level configuration lives in \`plitzi://settings/{env}\` and is edited wit
|
|
|
640
756
|
(merge — only the fields you pass change):
|
|
641
757
|
- \`customCss\` — **raw global CSS** injected for the whole space. Use it only for genuinely site-wide rules
|
|
642
758
|
(\`@keyframes\`, \`@font-face\`, resets). To style an element, write a **definition** and attach it — never customCss.
|
|
643
|
-
- \`keepState\` / \`stateStorage\` —
|
|
759
|
+
- \`keepState\` / \`stateStorage\` — keep the runtime state (\`state.<key>\`, what \`setState\` writes) across reloads,
|
|
760
|
+
in \`localStorage\`/\`sessionStorage\`, filed under whoever is signed in. It is the space's setting — pages do not
|
|
761
|
+
take one. \`transientState\` lists the top-level keys never kept (a filter, a demo, a panel left open); do not
|
|
762
|
+
reset kept state from \`onPageLoad\` instead — it is restored later than that and undoes half of the reset.
|
|
763
|
+
\`paintedState\` lists the kept keys the FIRST PAINT shows (the tool a toolbar shows, a name in an avatar): they go
|
|
764
|
+
in a cookie too, so the server draws with them instead of the page swapping them in after load. Small values only.
|
|
644
765
|
- **User provider / auth**: \`userProvider\` — \`basic\` for any HTTP+JSON backend, the name of a provider registered
|
|
645
766
|
in the page, or \`""\` to disable auth — plus \`tokenStorage\`, the \`loginUrl\`/\`userUrl\`/\`refreshUrl\`/\`logoutUrl\`
|
|
646
767
|
endpoints and the \`detailsPath\`/\`tokenPath\`/\`refreshTokenPath\`/\`expirationTimePath\` mapping that says where the
|
|
647
768
|
values sit in their responses. \`sessionHintCookie\` is worth setting whenever the backend can: it names a readable
|
|
648
769
|
cookie carrying only expiries, which is what lets a page answer "nobody is signed in" without a request.
|
|
770
|
+
On the Plitzi platform, \`userProvider: "server"\` signs visitors in with their Plitzi account on the space's own
|
|
771
|
+
host: a \`link\` with \`mode: "external"\` to \`/auth/sign-in?return=/\` starts it, \`authLogout\` ends it.
|
|
772
|
+
- \`visitorRoles\` — what each visitor role gives, \`{ "author": ["postPublish"] }\`: the permissions an action's
|
|
773
|
+
\`access: { mode: "role" }\` and a page's \`can()\` ask for. A visitor holds ONLY the permissions of their roles here,
|
|
774
|
+
never their account's. Who holds a role is given by email in the builder's Visitors panel — never in settings.
|
|
775
|
+
- \`channels\` — the realtime channels, by topic pattern (see **Realtime channels**). Merged pattern by pattern;
|
|
776
|
+
\`null\` removes one.
|
|
649
777
|
Example — inject a keyframe globally:
|
|
650
778
|
\`{ "type": "patchSettings", "customCss": "@keyframes spin { to { transform: rotate(360deg); } }" }\`.
|
|
651
779
|
|
|
780
|
+
## Accessibility — pages people, screen readers and browser agents can all use
|
|
781
|
+
A page is read three ways: seen, heard through a screen reader, and worked by a **browser agent** (Claude in Chrome)
|
|
782
|
+
— which finds the controls it can press in the same **accessibility tree** a screen reader walks. A control that
|
|
783
|
+
tree names nothing is invisible to both, however it looks. So build every page to be operable without sight:
|
|
784
|
+
- **Every control has words.** A \`button\` is named by its \`content\` and the words inside it; an icon-only one needs a
|
|
785
|
+
\`title\` (also its tooltip), and one whose words do not say what it does (a key hint, a count) a \`label\`. A \`link\` is named by what it holds, or \`label\` (set it on a link wrapping a whole card).
|
|
786
|
+
A \`formControl\` is named by its \`label\` — keep it and set \`hideLabel: true\` when the design shows what the field
|
|
787
|
+
is (a swatch, a search box); a \`placeholder\` names only a field typed into, and only until something is typed.
|
|
788
|
+
- **Every picture says what it shows** (\`alt\`), or is marked \`decorative: true\` when the words beside it already say
|
|
789
|
+
it. A \`fontAwesome\` icon is decoration (hidden) unless its \`label\` gives it a meaning of its own.
|
|
790
|
+
- **Only controls are clicked.** A click flow belongs on a \`button\` (it holds children — a whole card can be one) or
|
|
791
|
+
a \`link\` when it goes somewhere — never on a \`container\`, \`text\` or \`image\`: a keyboard cannot reach those and
|
|
792
|
+
no agent finds them. An empty backdrop that closes a panel is the exception (the panel's close button and Escape
|
|
793
|
+
are the ways everyone has).
|
|
794
|
+
- **State is said, not only styled.** A toggle sets \`ariaPressed\`, a button that opens a panel \`ariaExpanded\`
|
|
795
|
+
(bind them like any attribute) — an "active" class alone is invisible to both readers.
|
|
796
|
+
- **A dropdown opens from a \`button\`** among its children, outside the popup (an avatar, a "⋯": the button holds it and
|
|
797
|
+
has a \`title\`). The dropdown marks it and moves the focus in and out; a box or an icon alone opens nothing from a
|
|
798
|
+
keyboard.
|
|
799
|
+
- **An illustration** built from elements (a mock of a page, art) is a \`container\` with \`decorative: true\`: both
|
|
800
|
+
readers skip it. Keep controls out of it.
|
|
801
|
+
- **The page has an outline.** Headings go down one level at a time (h1 → h2 → h3; size them with their class, not
|
|
802
|
+
their level). Landmarks are containers with a tag — \`header\`, \`nav\`, \`main\`, \`aside\`, \`footer\` — and a
|
|
803
|
+
\`label\` when a page has two of one kind; a \`section\` with a \`label\` is a region an assistant can jump to.
|
|
804
|
+
- **What comes built in:** modals and dialogs are \`dialog\`s named by their title, take the focus, keep Tab inside
|
|
805
|
+
and close on Escape; tabs are a tab list worked with the arrow keys; a field that breaks a rule is marked invalid and
|
|
806
|
+
described by its message; the pager marks the current page. Nothing to add for those.
|
|
807
|
+
- **A canvas is a picture** to both readers. A plugin that draws (a chart, a board, a map) also renders what it shows
|
|
808
|
+
as real elements — a list, a table — with a button for every action; one hidden until focused cannot be clicked by an
|
|
809
|
+
agent that clicks by position, so offer a visible "list view".
|
|
810
|
+
The linter warns about these (\`control-without-name\`, \`image-without-alt\`, \`click-on-static-element\`,
|
|
811
|
+
\`dropdown-without-control\`, \`heading-level-skipped\`, \`label-ignored\`, \`control-in-decorative\`) on every element
|
|
812
|
+
you touch; fix them in the same batch. Then look:
|
|
813
|
+
\`plitzi_screenshot\` with \`view: "accessibility"\` shows the page as those readers get it, and lists what has no name.
|
|
814
|
+
|
|
652
815
|
## Semantics
|
|
653
816
|
- **props are fully replaced** on \`upsertElement\`: send every prop you want to keep. To change only some props,
|
|
654
817
|
use **\`patchElement\`** — it merges \`props\`/\`style\` onto the existing element (listed keys change, \`null\` unsets
|
|
@@ -660,15 +823,16 @@ Space-level configuration lives in \`plitzi://settings/{env}\` and is edited wit
|
|
|
660
823
|
Example — recolor one definition without resending it: \`{ "type": "patchDefinition", "ref": "btn-x",
|
|
661
824
|
"desktop": { "background-color": "#111" } }\`.
|
|
662
825
|
- **Atomic batches**: if any operation fails, \`plitzi_apply\` persists nothing.
|
|
663
|
-
- **Every
|
|
664
|
-
an element
|
|
665
|
-
|
|
666
|
-
edit does not touch. Such a finding is reported as a \`Pre-existing malformation in
|
|
826
|
+
- **Every element you touch must be malformation-free — pre-existing errors block the save.** When your batch edits
|
|
827
|
+
an element, the result is read by the same linter every writer of a space is held to, and the element's **current
|
|
828
|
+
stored content** with it — a broken transformer action, a malformed step, an attribute it never reads — even in
|
|
829
|
+
parts your edit does not touch. Such a finding is reported as a \`Pre-existing malformation in element …\` **error**, and
|
|
667
830
|
\`plitzi_apply\` rejects the batch until it is fixed. These are **not caused by your change** (the message says so)
|
|
668
831
|
— do not be confused; fix them **in the same batch** and re-apply. Because the check runs on the resulting state,
|
|
669
832
|
including the fix in your batch is exactly what unblocks the save. (Advisory issues — an unobserved source/action
|
|
670
833
|
name that may still be a valid plugin, a binding target a plugin manifest does not list — come back as
|
|
671
|
-
\`Pre-existing issue …\` **warnings** and do not block.)
|
|
834
|
+
\`Pre-existing issue …\` **warnings** and do not block.) Elements you do not touch are not held against you — except
|
|
835
|
+
that a broken tree your batch leaves anywhere (an element orphaned by a delete) is refused.
|
|
672
836
|
- **Optimistic concurrency — read before you write, and prove your read is current.** Editing a resource means you
|
|
673
837
|
read it first, so you hold its \`stateVersion\`. **Always pass \`expectedResourceVersions\`** (URI → the stateVersion
|
|
674
838
|
you read) for every resource your batch changes. If another agent edited it in the meantime, the live version no
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AIBinding, AIBindings, AIInteractionFlow, AIInteractionNodeType } from '../types';
|
|
2
2
|
import { RuleGroup } from '@plitzi/plitzi-ui/QueryBuilder';
|
|
3
|
-
import { ElementBinding, ElementDefinition, ElementInteraction } from '@plitzi/sdk-shared';
|
|
3
|
+
import { ElementBinding, ElementDefinition, ElementInteraction, WhileRunning } from '@plitzi/sdk-shared';
|
|
4
4
|
/** The node shape agents supply on write: same as AIInteractionNode but `id` is optional (generated when
|
|
5
5
|
* omitted). Kept structural so the tool-layer zod type assigns to it without importing the tool layer here. */
|
|
6
6
|
export interface FlowNodeInput {
|
|
@@ -11,6 +11,7 @@ export interface FlowNodeInput {
|
|
|
11
11
|
params?: Record<string, unknown>;
|
|
12
12
|
enabled?: boolean;
|
|
13
13
|
when?: RuleGroup;
|
|
14
|
+
whileRunning?: WhileRunning;
|
|
14
15
|
elementId?: string;
|
|
15
16
|
preview?: Record<string, unknown>;
|
|
16
17
|
}
|
|
@@ -21,6 +21,7 @@ var nodeToAI = (node) => {
|
|
|
21
21
|
if (isNonEmptyObject(node.params)) ai.params = node.params;
|
|
22
22
|
if (!node.enabled) ai.enabled = false;
|
|
23
23
|
if (node.when !== void 0) ai.when = node.when;
|
|
24
|
+
if (node.whileRunning !== void 0) ai.whileRunning = node.whileRunning;
|
|
24
25
|
if (node.elementId && node.elementId !== node.id) ai.elementId = node.elementId;
|
|
25
26
|
if (isNonEmptyObject(node.preview)) ai.preview = node.preview;
|
|
26
27
|
return ai;
|
|
@@ -101,6 +102,7 @@ var materializeFlow = (nodes, ownerId) => {
|
|
|
101
102
|
enabled: node.enabled ?? true
|
|
102
103
|
};
|
|
103
104
|
if (node.when !== void 0) interaction.when = node.when;
|
|
105
|
+
if (node.whileRunning !== void 0) interaction.whileRunning = node.whileRunning;
|
|
104
106
|
record[ids[i]] = interaction;
|
|
105
107
|
});
|
|
106
108
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AIElementDetail } from '../types';
|
|
2
|
-
import { ActionEntry, ActionTaskDescriptor, ComponentCatalog, ConnectorEntry, Element, PageFolder, Schema, Style } from '@plitzi/sdk-shared';
|
|
2
|
+
import { ActionEntry, ActionTaskDescriptor, ComponentCatalog, ConnectorEntry, Element, FunctionsDraft, PageFolder, Schema, Style } from '@plitzi/sdk-shared';
|
|
3
3
|
/** The working view the tools read and mutate: the two Plitzi schemas (elements + style), which the platform
|
|
4
4
|
* stores and persists as separate documents (Space model / Style model), plus the space's `connectors` — a third
|
|
5
5
|
* store (one row per connector) that a provider element points into by identifier. `catalog` is read-only
|
|
@@ -13,6 +13,9 @@ export interface Space {
|
|
|
13
13
|
/** The server tasks this deployment can run. Read-only reference data like `catalog`, not persisted: it is what
|
|
14
14
|
* the server has, not what the space owns, and an action authored against a task it lacks cannot run. */
|
|
15
15
|
actionTasks?: ActionTaskDescriptor[];
|
|
16
|
+
/** The space's own functions — their source files, which copy of them this is, and what they declare. Absent when
|
|
17
|
+
* the deployment offers no functions: then there is nothing to read and every function op is refused. */
|
|
18
|
+
functions?: FunctionsDraft;
|
|
16
19
|
catalog?: ComponentCatalog;
|
|
17
20
|
}
|
|
18
21
|
export declare const cloneSpace: (space: Space) => Space;
|
|
@@ -69,8 +72,8 @@ export declare const indexRemoveElements: (schema: Schema, els: Element[]) => vo
|
|
|
69
72
|
export declare const indexAddPage: (schema: Schema, page: Element) => void;
|
|
70
73
|
/** A new layout shell was created. */
|
|
71
74
|
export declare const indexAddLayout: (schema: Schema, layout: Element) => void;
|
|
72
|
-
/** A layout shell and
|
|
73
|
-
export declare const indexRemoveLayout: (schema: Schema, layout: Element,
|
|
75
|
+
/** A layout shell and the elements under it (`removed`) were deleted. */
|
|
76
|
+
export declare const indexRemoveLayout: (schema: Schema, layout: Element, removed: Element[]) => void;
|
|
74
77
|
/** A page and its `descendants` (its non-page elements) were deleted. */
|
|
75
78
|
export declare const indexRemovePage: (schema: Schema, page: Element, descendants: Element[]) => void;
|
|
76
79
|
/** A move reparented an element within its page: the page map is unchanged, but the moved element's parent and
|
|
@@ -113,7 +116,6 @@ export declare const slugRouteParams: (slug: string) => string[];
|
|
|
113
116
|
export declare const routeParamNames: (schema: Schema) => string[];
|
|
114
117
|
/** Indexed lookup that reflects the runtime reality: a flat id may be dangling (rsc placeholders, stale items). */
|
|
115
118
|
export declare const elementById: (schema: Schema, id: string) => Element | undefined;
|
|
116
|
-
export declare const descendantIds: (schema: Schema, pageRootId: string) => string[];
|
|
117
119
|
/** Resolve a name to a concrete element within a page subtree (or the page root itself). The name is the element's
|
|
118
120
|
* key in `flat`; what this adds is the check that it really belongs to this page. */
|
|
119
121
|
export declare const resolveRef: (schema: Schema, page: Element, ref: string) => Element | undefined;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { descendants } from "@plitzi/sdk-schema/helpers/elementTree";
|
|
1
2
|
//#region src/modules/mcp/helpers/space.ts
|
|
2
3
|
var cloneSpace = (space) => ({
|
|
3
4
|
schema: structuredClone(space.schema),
|
|
4
5
|
style: structuredClone(space.style),
|
|
5
6
|
connectors: structuredClone(space.connectors),
|
|
6
7
|
actions: structuredClone(space.actions),
|
|
8
|
+
...space.functions ? { functions: structuredClone(space.functions) } : {},
|
|
7
9
|
...space.actionTasks ? { actionTasks: space.actionTasks } : {},
|
|
8
10
|
...space.catalog ? { catalog: space.catalog } : {}
|
|
9
11
|
});
|
|
@@ -134,15 +136,15 @@ var indexAddLayout = (schema, layout) => {
|
|
|
134
136
|
index.pageOf.set(layout.id, layout.id);
|
|
135
137
|
index.detailCache.clear();
|
|
136
138
|
};
|
|
137
|
-
/** A layout shell and
|
|
138
|
-
var indexRemoveLayout = (schema, layout,
|
|
139
|
+
/** A layout shell and the elements under it (`removed`) were deleted. */
|
|
140
|
+
var indexRemoveLayout = (schema, layout, removed) => {
|
|
139
141
|
const index = cachedIndex(schema);
|
|
140
142
|
if (!index) return;
|
|
141
143
|
index.layoutIds.delete(layout.id);
|
|
142
144
|
const at = index.layoutElements.findIndex((entry) => entry.id === layout.id);
|
|
143
145
|
if (at >= 0) index.layoutElements.splice(at, 1);
|
|
144
146
|
index.pageOf.delete(layout.id);
|
|
145
|
-
for (const el of
|
|
147
|
+
for (const el of removed) index.pageOf.delete(el.id);
|
|
146
148
|
index.detailCache.clear();
|
|
147
149
|
};
|
|
148
150
|
/** A page and its `descendants` (its non-page elements) were deleted. */
|
|
@@ -255,21 +257,6 @@ var routeParamNames = (schema) => {
|
|
|
255
257
|
};
|
|
256
258
|
/** Indexed lookup that reflects the runtime reality: a flat id may be dangling (rsc placeholders, stale items). */
|
|
257
259
|
var elementById = (schema, id) => schema.flat[id];
|
|
258
|
-
/** All element ids belonging to a page subtree (excluding the page root). */
|
|
259
|
-
var collectDescendants = (schema, rootId, acc) => {
|
|
260
|
-
const el = elementById(schema, rootId);
|
|
261
|
-
if (!el) return;
|
|
262
|
-
const childIds = el.definition.items ?? [];
|
|
263
|
-
for (const childId of childIds) if (elementById(schema, childId)) {
|
|
264
|
-
acc.push(childId);
|
|
265
|
-
collectDescendants(schema, childId, acc);
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
var descendantIds = (schema, pageRootId) => {
|
|
269
|
-
const acc = [];
|
|
270
|
-
collectDescendants(schema, pageRootId, acc);
|
|
271
|
-
return acc;
|
|
272
|
-
};
|
|
273
260
|
/** Resolve a name to a concrete element within a page subtree (or the page root itself). The name is the element's
|
|
274
261
|
* key in `flat`; what this adds is the check that it really belongs to this page. */
|
|
275
262
|
var resolveRef = (schema, page, ref) => {
|
|
@@ -285,7 +272,7 @@ var orderedChildren = (schema, el) => {
|
|
|
285
272
|
/** The page ref an element belongs to. 'unknown' when it has no page ancestor. */
|
|
286
273
|
var pageRefOfElement = (schema, el) => spaceIndex(schema).pageOf.get(el.id) ?? "unknown";
|
|
287
274
|
/** Total number of descendant elements under a subtree (excluding the root). */
|
|
288
|
-
var descendantCount = (schema, rootId) =>
|
|
275
|
+
var descendantCount = (schema, rootId) => descendants(schema.flat, rootId).length;
|
|
289
276
|
var emptySpaceMessage = "Space data not available";
|
|
290
277
|
/** The code a space-dependent answer carries when the connection reaches no space, so both the agent and a host UI
|
|
291
278
|
* can tell "this connection cannot do that" from "the call failed". */
|
|
@@ -311,4 +298,4 @@ var ReadOnlyGrantError = class extends Error {
|
|
|
311
298
|
}
|
|
312
299
|
};
|
|
313
300
|
//#endregion
|
|
314
|
-
export { NoSpaceError, ReadOnlyGrantError, cloneSpace, descendantCount,
|
|
301
|
+
export { NoSpaceError, ReadOnlyGrantError, cloneSpace, descendantCount, elementById, emptySpace, emptySpaceMessage, findActionEntry, findConnectorEntry, findElementByRef, findFolderByRef, findLayoutByRef, findPageByRef, findRootByRef, folderAncestorIds, getLayoutElements, getPageElements, indexAddElement, indexAddLayout, indexAddPage, indexInvalidateDetails, indexRemoveElements, indexRemoveLayout, indexRemovePage, invalidateIndex, isPageElement, nameOf, noSpaceErrorCode, orderedChildren, pageFoldersOf, pageRefOfElement, pagesUsingLayout, readOnlyGrantErrorCode, readOnlyGrantMessage, resolveRef, routeParamNames, slugRouteParams, slugify, sortFolders, spaceIndex, strOr, unauthorizedSpaceMessage };
|