@plitzi/sdk-mcp 0.38.6 → 0.38.8
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 +397 -0
- package/README.md +9 -9
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -3
- package/dist/modules/mcp/helpers/guide.js +74 -35
- package/dist/modules/mcp/index.d.ts +2 -2
- package/dist/modules/mcp/server.d.ts +4 -4
- package/dist/modules/mcp/server.js +10 -5
- package/dist/modules/mcp/tests/cheapMistakes.test.d.ts +1 -0
- package/dist/modules/mcp/tests/helpers.d.ts +9 -2
- package/dist/modules/mcp/tests/intents.test.d.ts +1 -0
- package/dist/modules/mcp/tests/look.test.d.ts +1 -0
- package/dist/modules/mcp/tools/apply/index.d.ts +706 -62
- package/dist/modules/mcp/tools/apply/index.js +70 -20
- package/dist/modules/mcp/tools/describeOperation.d.ts +11 -0
- package/dist/modules/mcp/tools/describeOperation.js +40 -0
- package/dist/modules/mcp/tools/describeOperation.test.d.ts +1 -0
- package/dist/modules/mcp/tools/index.d.ts +1 -6
- package/dist/modules/mcp/tools/index.js +12 -9
- package/dist/modules/mcp/tools/intents.d.ts +5 -0
- package/dist/modules/mcp/tools/intents.js +201 -0
- package/dist/modules/mcp/tools/look.d.ts +17 -0
- package/dist/modules/mcp/tools/look.js +32 -0
- package/dist/modules/mcp/tools/operations/closed.d.ts +7 -0
- package/dist/modules/mcp/tools/operations/closed.js +29 -0
- package/dist/modules/mcp/tools/operations/index.d.ts +2928 -359
- package/dist/modules/mcp/tools/operations/index.js +72 -51
- package/dist/modules/mcp/tools/operations/schema/bindings/upsertBinding.d.ts +1 -1
- package/dist/modules/mcp/tools/operations/schema/elements/moveElement.d.ts +1 -1
- package/dist/modules/mcp/tools/operations/schema/elements/repeatElement.d.ts +1 -1
- package/dist/modules/mcp/tools/operations/schema/elements/upsertElement.d.ts +1 -1
- package/dist/modules/mcp/tools/operations/schema/interactions/upsertInteractionFlow.d.ts +1 -1
- package/dist/modules/mcp/tools/operations/schema/operations.d.ts +5 -5
- package/dist/modules/mcp/tools/operations/schema/shared.d.ts +3 -3
- package/dist/modules/mcp/tools/operations/schema/shared.js +7 -6
- package/dist/modules/mcp/tools/operations/schemaIds.js +11 -5
- package/dist/modules/mcp/tools/operations/style/definitions/patchDefinition.d.ts +90 -0
- package/dist/modules/mcp/tools/operations/style/definitions/patchDefinition.js +2 -1
- package/dist/modules/mcp/tools/operations/style/definitions/upsertDefinition.d.ts +90 -0
- package/dist/modules/mcp/tools/operations/style/definitions/upsertDefinition.js +2 -1
- package/dist/modules/mcp/tools/operations/style/definitions/upsertDefinitions.d.ts +90 -0
- package/dist/modules/mcp/tools/operations/style/definitions/upsertDefinitions.js +8 -1
- package/dist/modules/mcp/tools/operations/style/globalStyles/patchGlobalStyle.d.ts +90 -0
- package/dist/modules/mcp/tools/operations/style/globalStyles/patchGlobalStyle.js +2 -1
- package/dist/modules/mcp/tools/operations/style/globalStyles/upsertGlobalStyle.d.ts +90 -0
- package/dist/modules/mcp/tools/operations/style/globalStyles/upsertGlobalStyle.js +2 -1
- package/dist/modules/mcp/tools/operations/style/idStyles/patchIdStyle.d.ts +90 -0
- package/dist/modules/mcp/tools/operations/style/idStyles/patchIdStyle.js +2 -1
- package/dist/modules/mcp/tools/operations/style/idStyles/upsertIdStyle.d.ts +90 -0
- package/dist/modules/mcp/tools/operations/style/idStyles/upsertIdStyle.js +2 -1
- package/dist/modules/mcp/tools/operations/style/operations.d.ts +639 -9
- package/dist/modules/mcp/tools/operations/style/shared.d.ts +394 -0
- package/dist/modules/mcp/tools/operations/style/shared.js +26 -8
- package/dist/modules/mcp/tools/operations/style/translator.js +26 -1
- package/dist/modules/mcp/tools/operations/style/validate.d.ts +4 -0
- package/dist/modules/mcp/tools/operations/style/validate.js +63 -0
- package/dist/modules/mcp/tools/operations/style/write.d.ts +1 -1
- package/dist/modules/mcp/tools/operations/style/write.js +88 -2
- package/dist/modules/mcp/tools/render.d.ts +675 -45
- package/dist/modules/mcp/tools/render.js +5 -1
- package/dist/modules/mcp/tools/shared/draftBatch.d.ts +3 -2
- package/dist/modules/mcp/tools/shared/draftBatch.js +16 -5
- package/dist/modules/mcp/tools/shared/effects.d.ts +2 -0
- package/dist/modules/mcp/tools/shared/effects.js +155 -0
- package/dist/modules/mcp/tools/shared/effects.test.d.ts +1 -0
- package/dist/modules/mcp/tools/shared/look.d.ts +32 -0
- package/dist/modules/mcp/tools/shared/look.js +128 -0
- package/dist/modules/mcp/tools/shared/normalizeOperations.d.ts +6 -0
- package/dist/modules/mcp/tools/shared/normalizeOperations.js +37 -0
- package/dist/modules/mcp/tools/shared/repeats.d.ts +13 -0
- package/dist/modules/mcp/tools/shared/repeats.js +38 -0
- package/dist/modules/mcp/tools/shared/tool.d.ts +9 -6
- package/dist/modules/mcp/tools/shared/tool.js +2 -1
- package/dist/modules/mcp/tools/shared/validator/context.d.ts +0 -1
- package/dist/modules/mcp/tools/shared/validator/elements.js +0 -1
- package/dist/modules/mcp/tools/shared/validator/index.js +25 -2
- package/dist/modules/mcp/types/aiSchema.d.ts +13 -1
- package/dist/modules/mcp/types/screenshotTypes.d.ts +3 -3
- package/dist/modules/mcp/types/toolTypes.d.ts +2 -4
- package/dist/options.d.ts +4 -5
- package/dist/preview/createPreview.js +9 -18
- package/package.json +7 -7
- package/skills/plitzi-render/SKILL.md +8 -184
- package/skills/plitzi-render/reference/good-call.md +88 -0
- package/skills/plitzi-render/reference/interactive.md +64 -0
- package/skills/plitzi-render/reference/looks.md +29 -0
- package/dist/modules/mcp/tools/preview.d.ts +0 -793
- package/dist/modules/mcp/tools/preview.js +0 -37
- package/dist/modules/mcp/tools/screenshot.d.ts +0 -1078
- package/dist/modules/mcp/tools/screenshot.js +0 -127
- package/dist/modules/mcp/tools/validate.d.ts +0 -1069
- package/dist/modules/mcp/tools/validate.js +0 -32
|
@@ -2,7 +2,6 @@ import { componentView, findComponentByRef } from "../../../helpers/components.j
|
|
|
2
2
|
import { findElementByRef, findFolderByRef, findPageByRef, findRootByRef, folderAncestorIds, getLayoutElements, getPageElements, pageFoldersOf, resolveRef, routeParamNames } from "../../../helpers/space.js";
|
|
3
3
|
import { observedDataSources, observedInteractionActions } from "../../../catalogs/observed.js";
|
|
4
4
|
import { buildTypeRegistry } from "../../../catalogs/registry.js";
|
|
5
|
-
import { elementTypeNames } from "../../../catalogs/index.js";
|
|
6
5
|
import { MAX_OPS } from "../../operations/index.js";
|
|
7
6
|
import { batchDeclaredElements, batchDeclaredFolders, batchDeclaredPages, batchDeclaredVariants, batchDeclaredVars } from "./batch.js";
|
|
8
7
|
import { checkObservedName, warnOnce } from "./context.js";
|
|
@@ -13,6 +12,7 @@ import { checkRef } from "./refs.js";
|
|
|
13
12
|
import { checkElementInput, checkRawMarkup, checkTypeProps, checkVariantApplication } from "./elements.js";
|
|
14
13
|
import { checkInteractionNode } from "./interactions.js";
|
|
15
14
|
import { treeOf } from "@plitzi/sdk-schema/helpers/components";
|
|
15
|
+
import { closest } from "@plitzi/sdk-authoring";
|
|
16
16
|
import { isValidElementId } from "@plitzi/sdk-schema/helpers/elementId";
|
|
17
17
|
//#region src/modules/mcp/tools/shared/validator/index.ts
|
|
18
18
|
var STYLE_CATEGORIES = [
|
|
@@ -49,7 +49,6 @@ var buildValidationCtx = (space, ops, mode = "space") => {
|
|
|
49
49
|
errors: [],
|
|
50
50
|
warnings: [],
|
|
51
51
|
warned: /* @__PURE__ */ new Set(),
|
|
52
|
-
knownTypes: /* @__PURE__ */ new Set([...Object.keys(registry.types), ...elementTypeNames]),
|
|
53
52
|
typeProps: new Map(Object.entries(registry.types).map(([type, info]) => [type, new Set(Object.keys(info.props))])),
|
|
54
53
|
typeMeta: buildTypeMeta(space.catalog),
|
|
55
54
|
elementType: (ref) => treeOf(space.schema, ref)?.flat[ref]?.definition.type,
|
|
@@ -76,6 +75,28 @@ var validateOperations = (space, ops, mode = "space") => {
|
|
|
76
75
|
if (input.type === PROVIDER_TYPE) checkProviderElement(space, ctx, input.ref, input.runtime, input.props, batchConnectors, base);
|
|
77
76
|
(input.children ?? []).forEach((child, i) => checkProviders(child, `${base}.children[${i}]`));
|
|
78
77
|
};
|
|
78
|
+
const classes = /* @__PURE__ */ new Set([...Object.values(space.style.platform).flatMap((breakpoint) => Object.entries(breakpoint).filter(([, item]) => item.type === "class").map(([name]) => name)), ...ops.flatMap((op) => op.type === "upsertDefinition" ? [op.ref] : op.type === "upsertDefinitions" ? Object.keys(op.definitions) : [])]);
|
|
79
|
+
const checkClasses = (style, path) => {
|
|
80
|
+
const worn = [...(style?.base ?? []).map((name) => ({
|
|
81
|
+
name,
|
|
82
|
+
at: `${path}.base`
|
|
83
|
+
})), ...Object.entries(style?.slots ?? {}).flatMap(([slot, names]) => names.map((name) => ({
|
|
84
|
+
name,
|
|
85
|
+
at: `${path}.slots.${slot}`
|
|
86
|
+
})))];
|
|
87
|
+
for (const { name, at } of worn) if (!classes.has(name)) {
|
|
88
|
+
const nearest = closest(name, classes);
|
|
89
|
+
ctx.errors.push({
|
|
90
|
+
path: at,
|
|
91
|
+
message: `"${name}" is not a class of the space${nearest ? ` — did you mean "${nearest}"?` : ""}`,
|
|
92
|
+
hint: "Name one of plitzi://definitions/{env}, or declare it earlier in the batch with upsertDefinition"
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const checkTreeClasses = (input, path) => {
|
|
97
|
+
checkClasses(input.style, `${path}.style`);
|
|
98
|
+
(input.children ?? []).forEach((child, i) => checkTreeClasses(child, `${path}.children[${i}]`));
|
|
99
|
+
};
|
|
79
100
|
if (ops.length > 1e3) ctx.errors.push({
|
|
80
101
|
path: "operations",
|
|
81
102
|
message: `Batch has ${ops.length} operations (max ${MAX_OPS})`,
|
|
@@ -103,11 +124,13 @@ var validateOperations = (space, ops, mode = "space") => {
|
|
|
103
124
|
switch (op.type) {
|
|
104
125
|
case "upsertElement":
|
|
105
126
|
checkElementInput(op.element, `${base}.element`, ctx, /* @__PURE__ */ new Set());
|
|
127
|
+
checkTreeClasses(op.element, `${base}.element`);
|
|
106
128
|
checkVariantApplication(op.element.initialState, `${base}.element.initialState`, ctx);
|
|
107
129
|
checkProviders(op.element, `${base}.element`);
|
|
108
130
|
break;
|
|
109
131
|
case "patchElement": {
|
|
110
132
|
checkRef(op.ref, `${base}.ref`, ctx);
|
|
133
|
+
checkClasses(op.style, `${base}.style`);
|
|
111
134
|
const page = findRootByRef(scoped, component ? component.rootId : op.pageRef);
|
|
112
135
|
const target = page ? resolveRef(scoped, page, op.ref) : void 0;
|
|
113
136
|
if (op.props && target && target.id !== page?.id) {
|
|
@@ -223,10 +223,22 @@ export interface AIAncestorCondition extends DisplayModeCss {
|
|
|
223
223
|
states?: Record<string, DisplayModeCss>;
|
|
224
224
|
variants?: Record<string, DisplayModeCss>;
|
|
225
225
|
}
|
|
226
|
+
/** A pseudo-element of a selector (`before`, `after`, `placeholder`…): its rules per breakpoint, and in its states. */
|
|
227
|
+
export interface AIPseudo extends DisplayModeCss {
|
|
228
|
+
states?: Record<string, DisplayModeCss>;
|
|
229
|
+
}
|
|
230
|
+
/** A variant: its rules per breakpoint, its states and its pseudo-elements. */
|
|
231
|
+
export interface AIVariant extends AIPseudo {
|
|
232
|
+
pseudos?: Record<string, AIPseudo>;
|
|
233
|
+
}
|
|
234
|
+
/** The rules under a condition of the page (`motion-reduce`, `container (max-width: 30rem)`), as a variant has them. */
|
|
235
|
+
export type AICondition = AIVariant;
|
|
226
236
|
export interface AIDefinitionSlot extends DisplayModeCss {
|
|
227
237
|
states?: Record<string, DisplayModeCss>;
|
|
228
|
-
variants?: Record<string,
|
|
238
|
+
variants?: Record<string, AIVariant>;
|
|
229
239
|
ancestors?: Record<string, AIAncestorCondition>;
|
|
240
|
+
pseudos?: Record<string, AIPseudo>;
|
|
241
|
+
conditions?: Record<string, AICondition>;
|
|
230
242
|
}
|
|
231
243
|
export interface AIDefinition extends AIDefinitionSlot {
|
|
232
244
|
ref: string;
|
|
@@ -44,9 +44,9 @@ export type ScreenshotInput = {
|
|
|
44
44
|
*/
|
|
45
45
|
views?: CaptureView[];
|
|
46
46
|
};
|
|
47
|
-
/** How
|
|
48
|
-
* client to the dedicated browser pod); absent
|
|
49
|
-
*
|
|
47
|
+
/** How a look reaches the headless-browser service. The consumer injects an implementation (an HTTP
|
|
48
|
+
* client to the dedicated browser pod); absent, or a capture that fails (pod down) and answers `ok:false` rather than
|
|
49
|
+
* throwing, and the look answers the HTML instead. */
|
|
50
50
|
export type ScreenshotClient = {
|
|
51
51
|
capture: (input: ScreenshotInput) => Promise<ScreenshotResult>;
|
|
52
52
|
};
|
|
@@ -44,6 +44,8 @@ export interface WriteResponse {
|
|
|
44
44
|
updated: number;
|
|
45
45
|
deleted: number;
|
|
46
46
|
};
|
|
47
|
+
/** What the batch did, read off the space before and after it: a line per change, nothing left unsaid. */
|
|
48
|
+
effects?: string[];
|
|
47
49
|
changed: ChangedResource[];
|
|
48
50
|
elements?: WriteElement[];
|
|
49
51
|
warnings?: string[];
|
|
@@ -83,10 +85,6 @@ export interface ApplyInput {
|
|
|
83
85
|
expectedResourceVersions?: Record<string, string>;
|
|
84
86
|
operations: Operation[];
|
|
85
87
|
}
|
|
86
|
-
export interface ValidateInput {
|
|
87
|
-
environment?: string;
|
|
88
|
-
operations: Operation[];
|
|
89
|
-
}
|
|
90
88
|
export interface ValidationResult {
|
|
91
89
|
valid: boolean;
|
|
92
90
|
errors: ValidationError[];
|
package/dist/options.d.ts
CHANGED
|
@@ -58,11 +58,10 @@ export type McpOptions = {
|
|
|
58
58
|
url: string;
|
|
59
59
|
secret?: string;
|
|
60
60
|
};
|
|
61
|
-
/** Dedicated headless-browser service
|
|
62
|
-
* service that turns a URL into PNG(s); `renderBaseUrl` is the SSR base the
|
|
63
|
-
* + the one-shot `__pt` token are appended). When absent
|
|
64
|
-
*
|
|
65
|
-
* HTML preview with a warning. */
|
|
61
|
+
/** Dedicated headless-browser service a look renders an image or an accessibility outline through (off unless
|
|
62
|
+
* set). `serviceUrl` is the browser service that turns a URL into PNG(s); `renderBaseUrl` is the SSR base the
|
|
63
|
+
* browser navigates to (a page path + the one-shot `__pt` token are appended). When absent — or unreachable at call
|
|
64
|
+
* time — a look answers the HTML instead, with a warning. */
|
|
66
65
|
screenshot?: {
|
|
67
66
|
serviceUrl: string;
|
|
68
67
|
renderBaseUrl: string;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { computeVersion } from "../modules/mcp/helpers/computeVersion.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { applyOperations } from "../modules/mcp/tools/apply/dispatch.js";
|
|
2
|
+
import { findPageByRef, getPageElements } from "../modules/mcp/helpers/space.js";
|
|
3
|
+
import { draftBatch } from "../modules/mcp/tools/shared/draftBatch.js";
|
|
5
4
|
import { randomUUID } from "node:crypto";
|
|
6
5
|
import { buildBody } from "@plitzi/sdk-server/ssr";
|
|
7
6
|
//#region src/preview/createPreview.ts
|
|
@@ -41,30 +40,22 @@ var createPreview = async (body, config, renderFn, pluginManager, caches) => {
|
|
|
41
40
|
};
|
|
42
41
|
let draftOffline = offlineData;
|
|
43
42
|
if (body.operations && body.operations.length > 0) {
|
|
44
|
-
const
|
|
43
|
+
const batch = draftBatch({
|
|
45
44
|
schema: offlineData.schema,
|
|
46
45
|
style: offlineData.style,
|
|
47
46
|
connectors: [],
|
|
48
47
|
actions: []
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
if (!validation.valid) return {
|
|
48
|
+
}, env, body.operations);
|
|
49
|
+
if (!batch.ok) return {
|
|
52
50
|
ok: false,
|
|
53
51
|
error: "INVALID_OPERATIONS",
|
|
54
|
-
message: "The operations
|
|
55
|
-
errors:
|
|
56
|
-
};
|
|
57
|
-
const outcome = applyOperations(draft, env, body.operations);
|
|
58
|
-
if (outcome.errors.length > 0) return {
|
|
59
|
-
ok: false,
|
|
60
|
-
error: "APPLY_FAILED",
|
|
61
|
-
message: "The operations could not be applied.",
|
|
62
|
-
errors: outcome.errors
|
|
52
|
+
message: "The operations were refused, as plitzi_apply would refuse them.",
|
|
53
|
+
errors: batch.errors
|
|
63
54
|
};
|
|
64
55
|
draftOffline = {
|
|
65
56
|
...offlineData,
|
|
66
|
-
schema: draft.schema,
|
|
67
|
-
style: draft.style
|
|
57
|
+
schema: batch.draft.schema,
|
|
58
|
+
style: batch.draft.style
|
|
68
59
|
};
|
|
69
60
|
}
|
|
70
61
|
const pagePath = resolvePagePath(draftOffline.schema, body.pageRef);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plitzi/sdk-mcp",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.8",
|
|
4
4
|
"license": "AGPL-3.0",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@modelcontextprotocol/ext-apps": "^1.7.5",
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
33
|
-
"@plitzi/plitzi-sdk": "0.38.
|
|
34
|
-
"@plitzi/sdk-authoring": "0.38.
|
|
35
|
-
"@plitzi/sdk-schema": "0.38.
|
|
36
|
-
"@plitzi/sdk-server": "0.38.
|
|
37
|
-
"@plitzi/sdk-shared": "0.38.
|
|
38
|
-
"@plitzi/sdk-style": "0.38.
|
|
33
|
+
"@plitzi/plitzi-sdk": "0.38.8",
|
|
34
|
+
"@plitzi/sdk-authoring": "0.38.8",
|
|
35
|
+
"@plitzi/sdk-schema": "0.38.8",
|
|
36
|
+
"@plitzi/sdk-server": "0.38.8",
|
|
37
|
+
"@plitzi/sdk-shared": "0.38.8",
|
|
38
|
+
"@plitzi/sdk-style": "0.38.8",
|
|
39
39
|
"ejs": "^6.0.1",
|
|
40
40
|
"esbuild": "^0.28.2",
|
|
41
41
|
"zod": "^4.6.5"
|
|
@@ -30,125 +30,6 @@ Read the MCP resource **`plitzi://render/guide`** once per conversation. It is t
|
|
|
30
30
|
every operation, the element types and their props, the styling system, worked examples. `plitzi://render/types`
|
|
31
31
|
lists every element type with its description. Don't reconstruct the schema from memory — read the guide.
|
|
32
32
|
|
|
33
|
-
## The shape of a good call
|
|
34
|
-
|
|
35
|
-
Everything the widget needs travels in ONE call, as `operations`. Three ops carry almost every widget:
|
|
36
|
-
|
|
37
|
-
- **`upsertDefinitions`** — declare ALL the CSS classes at once, one class per *look* (not per property).
|
|
38
|
-
- **`upsertElement`** — build the whole tree in a single op, nesting with `children` under `pageRef: "render"`.
|
|
39
|
-
- **`repeatElement`** — the moment two siblings differ only in data, write the row ONCE as a template with
|
|
40
|
-
`{{item.field}}` placeholders and pass `items`. Rows come out numbered (`tile-1`, `tile-2`…). A list inside each
|
|
41
|
-
row is the same op: give a template node `repeat: { items: "{{item.<field>}}", template: … }`.
|
|
42
|
-
|
|
43
|
-
```json
|
|
44
|
-
{
|
|
45
|
-
"operations": [
|
|
46
|
-
{
|
|
47
|
-
"type": "upsertDefinitions",
|
|
48
|
-
"definitions": {
|
|
49
|
-
"panel": {
|
|
50
|
-
"desktop": {
|
|
51
|
-
"display": "flex",
|
|
52
|
-
"flex-direction": "column",
|
|
53
|
-
"gap": "12px",
|
|
54
|
-
"padding": "16px",
|
|
55
|
-
"color": "var(--color-text-primary, light-dark(#0f172a, #e8eaed))"
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
"row": {
|
|
59
|
-
"desktop": { "display": "flex", "flex-wrap": "wrap", "gap": "12px" },
|
|
60
|
-
"mobile": { "flex-direction": "column" }
|
|
61
|
-
},
|
|
62
|
-
"tile": {
|
|
63
|
-
"desktop": {
|
|
64
|
-
"flex": "1 1 0%",
|
|
65
|
-
"min-width": "160px",
|
|
66
|
-
"padding": "12px",
|
|
67
|
-
"border-radius": "10px",
|
|
68
|
-
"border": "1px solid var(--color-border-primary, light-dark(#e2e8f0, #333a48))",
|
|
69
|
-
"background-color": "var(--color-background-secondary, light-dark(#ffffff, #1f2430))"
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
"tile-title": { "desktop": { "margin": "0", "font-size": "15px" } },
|
|
73
|
-
"tile-price": { "desktop": { "margin": "0", "font-size": "13px", "opacity": "0.75" } }
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
"type": "upsertElement",
|
|
78
|
-
"pageRef": "render",
|
|
79
|
-
"element": {
|
|
80
|
-
"ref": "panel",
|
|
81
|
-
"type": "container",
|
|
82
|
-
"style": { "base": ["panel"] },
|
|
83
|
-
"children": [{ "ref": "title", "type": "heading", "subType": "h3", "props": { "content": "Plans" } }]
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
"type": "repeatElement",
|
|
88
|
-
"pageRef": "render",
|
|
89
|
-
"parentRef": "panel",
|
|
90
|
-
"ref": "plans",
|
|
91
|
-
"style": { "base": ["row"] },
|
|
92
|
-
"template": {
|
|
93
|
-
"ref": "tile",
|
|
94
|
-
"type": "container",
|
|
95
|
-
"style": { "base": ["tile"] },
|
|
96
|
-
"children": [
|
|
97
|
-
{
|
|
98
|
-
"ref": "name",
|
|
99
|
-
"type": "heading",
|
|
100
|
-
"subType": "h4",
|
|
101
|
-
"props": { "content": "{{item.name}}" },
|
|
102
|
-
"style": { "base": ["tile-title"] }
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
"ref": "price",
|
|
106
|
-
"type": "paragraph",
|
|
107
|
-
"props": { "content": "{{item.price}}" },
|
|
108
|
-
"style": { "base": ["tile-price"] }
|
|
109
|
-
}
|
|
110
|
-
]
|
|
111
|
-
},
|
|
112
|
-
"items": [
|
|
113
|
-
{ "name": "Starter", "price": "$0 / month" },
|
|
114
|
-
{ "name": "Team", "price": "$19 / month" },
|
|
115
|
-
{ "name": "Business", "price": "$49 / month" }
|
|
116
|
-
]
|
|
117
|
-
}
|
|
118
|
-
]
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
## The six things that decide whether it looks good
|
|
123
|
-
|
|
124
|
-
1. **Width is free, height is scarce.** The widget renders in a side panel. A plain container stacks its children
|
|
125
|
-
vertically — that is the tall, half-empty default to avoid. Put peers (metrics, plans, options, image + text) in
|
|
126
|
-
a wrapping row (`display:flex` + `flex-wrap:wrap`, children `flex:1 1 0%` with a `min-width` as the wrap
|
|
127
|
-
threshold) or a grid with `grid-template-columns: repeat(auto-fit, minmax(160px, 1fr))`. Stack only what reads in
|
|
128
|
-
order: heading over paragraph, forms, steps, prose.
|
|
129
|
-
2. **The host may be in dark mode.** Never hardcode a light palette. Take colours from the host variables with a
|
|
130
|
-
`light-dark()` fallback, and always set `color` wherever you set `background-color`.
|
|
131
|
-
3. **You are not styling from zero.** The SDK's per-type CSS imposes **no minimum size**: a rail, a divider, a dot
|
|
132
|
-
or a narrow cell is exactly as small as you make it, and no `"min-width": "0"` escape hatch is needed. The flip
|
|
133
|
-
side is that an element with no content and no size takes no space at all — give a spacer its own `height`.
|
|
134
|
-
What does land on your elements is the BROWSER's own defaults: `heading` and `paragraph` keep their UA margins
|
|
135
|
-
(zero them, space with the parent's `gap`), `list` a 40px `padding-left`, `button` its native chrome. Borders
|
|
136
|
-
start at `0 solid`, so `border-color` alone paints nothing — give `border-width` too.
|
|
137
|
-
4. **Draw with inline SVG, on a budget.** A logo, a sparkline, a badge or a decorative shape goes in a `blockHtml`
|
|
138
|
-
element whose `props.content` is an `<svg>` — keep a `viewBox` with `width`/`height` `100%` so the element's
|
|
139
|
-
class sizes it, and `fill`/`stroke` `currentColor` so it follows the theme. A handful of paths, drawn once and
|
|
140
|
-
reused. Never a `data:` URI, and never a full illustration or a photo-real scene: that costs more than the rest
|
|
141
|
-
of the widget, so use an `https` image, a flat colour or a CSS gradient instead. Markup only — `<script>` and
|
|
142
|
-
inline `on*` handlers are rejected.
|
|
143
|
-
5. **Only use image URLs you have seen work.** Everything the widget loads from outside is fetched for it by the
|
|
144
|
-
render server, so redirects, hotlink rules and missing CORS headers are already handled and there is nothing to
|
|
145
|
-
configure — but nothing can guess a URL. Write a direct file URL, never a page about the picture, a search
|
|
146
|
-
result, or a pattern assembled from memory: one that 404s leaves a grey box in the middle of a finished layout.
|
|
147
|
-
With no URL you trust, draw that block instead (a gradient, a flat colour, an inline `<svg>`).
|
|
148
|
-
6. **Write CSS plainly.** Kebab-case properties, shorthands welcome (`padding: 8px 16px`, `border: 1px solid red`,
|
|
149
|
-
`font: bold 16px/1.5 Arial`) — they are expanded and stored as longhands, so a breakpoint or state can override
|
|
150
|
-
one property on its own.
|
|
151
|
-
|
|
152
33
|
## Iterating on a widget you already rendered
|
|
153
34
|
|
|
154
35
|
Every render answers with a **`renderId`**. To change that widget, do NOT rebuild it: call again with
|
|
@@ -170,71 +51,6 @@ storage, a conversation resumed elsewhere), send the whole batch again without `
|
|
|
170
51
|
issues. The `path` names the operation, so fix that one and call again — you never lose the rest of the batch. An
|
|
171
52
|
unknown prop comes back as a warning naming the right one, so probing is safe.
|
|
172
53
|
|
|
173
|
-
## Making it interactive
|
|
174
|
-
|
|
175
|
-
Widgets are not only static: `upsertInteractionFlow` attaches a flow to an element — a `trigger` node first
|
|
176
|
-
(`onClick`), then the steps that run after it, in order.
|
|
177
|
-
|
|
178
|
-
**To show and hide, use `toggleState`.** Every element registers `setState` and `toggleState` as `callback`
|
|
179
|
-
actions, and `elementId` names the element they act on — the flow's own element by default, or another element's
|
|
180
|
-
ref to act on that one. `toggleState` with `category: "state"` and `key: "visibility"` is the show/hide toggle;
|
|
181
|
-
there is no separate `toggleVisibility` action. The element starts hidden with `initialState: { "visibility":
|
|
182
|
-
false }` on the element itself.
|
|
183
|
-
|
|
184
|
-
```json
|
|
185
|
-
{
|
|
186
|
-
"operations": [
|
|
187
|
-
{
|
|
188
|
-
"type": "upsertElement",
|
|
189
|
-
"pageRef": "render",
|
|
190
|
-
"element": {
|
|
191
|
-
"ref": "card",
|
|
192
|
-
"type": "container",
|
|
193
|
-
"children": [
|
|
194
|
-
{ "ref": "card-head", "type": "button", "props": { "content": "Details" } },
|
|
195
|
-
{
|
|
196
|
-
"ref": "card-body",
|
|
197
|
-
"type": "container",
|
|
198
|
-
"initialState": { "visibility": false },
|
|
199
|
-
"children": [
|
|
200
|
-
{ "ref": "card-text", "type": "paragraph", "props": { "content": "The hidden detail." } }
|
|
201
|
-
]
|
|
202
|
-
}
|
|
203
|
-
]
|
|
204
|
-
}
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
"type": "upsertInteractionFlow",
|
|
208
|
-
"pageRef": "render",
|
|
209
|
-
"ref": "card-head",
|
|
210
|
-
"nodes": [
|
|
211
|
-
{ "title": "On click", "nodeType": "trigger", "action": "onClick" },
|
|
212
|
-
{
|
|
213
|
-
"title": "Toggle body",
|
|
214
|
-
"nodeType": "callback",
|
|
215
|
-
"action": "toggleState",
|
|
216
|
-
"elementId": "card-body",
|
|
217
|
-
"params": { "category": "state", "key": "visibility" }
|
|
218
|
-
}
|
|
219
|
-
]
|
|
220
|
-
}
|
|
221
|
-
]
|
|
222
|
-
}
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
Expand/collapse is **one step on one trigger** — never two `setState` branches under opposite `when` conditions,
|
|
226
|
-
which read the state as it was when the flow started and so are always one click behind. For several flows on one
|
|
227
|
-
element (an `onClick` and an `onMouseEnter`, or two independent clicks), call `upsertInteractionFlow` again with
|
|
228
|
-
the same `ref` and **omit `flowId`**; passing an existing `flowId` replaces that flow instead.
|
|
229
|
-
|
|
230
|
-
Two things routinely go wrong. Events **bubble**, so a clickable element inside another clickable element runs
|
|
231
|
-
BOTH flows — put the trigger on one of them (the render warns you and names the pair). And a `globalCallback`
|
|
232
|
-
(`addNotification`, `navigate`, app-level `setState`) is provided by a module, not by an element, so **omit
|
|
233
|
-
`elementId`** on those; a step with the wrong node type resolves against nothing and silently does nothing.
|
|
234
|
-
|
|
235
|
-
For data-driven widgets, an `apiContainer` fetches at runtime and `upsertBinding` wires the result into elements —
|
|
236
|
-
see "Data & interactivity" in `plitzi://render/guide`, which is also the full reference for everything above.
|
|
237
|
-
|
|
238
54
|
## After it renders
|
|
239
55
|
|
|
240
56
|
The widget is shown to the user; you get a compact summary. Don't re-describe what they can already see — a short
|
|
@@ -244,3 +60,11 @@ When the widget has flows, the summary includes an `interactions` line per flow
|
|
|
244
60
|
(`"card-head onClick → toggleState card-body[visibility]"`). Read it: it is what confirms the flow landed on the
|
|
245
61
|
element you meant. But it reports the **wiring**, not a click that was performed — this tool authors the widget,
|
|
246
62
|
it does not drive it. So tell the user what you connected; never claim you verified the behaviour at runtime.
|
|
63
|
+
|
|
64
|
+
## What to read
|
|
65
|
+
|
|
66
|
+
| The task | Read |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| Writing the call: the tree, the classes, the order of operations | [the shape of a good call](reference/good-call.md) |
|
|
69
|
+
| Making it look good: spacing, type, colour, images | [the six things](reference/looks.md) |
|
|
70
|
+
| Toggles, expanders, tabs — a widget that reacts | [making it interactive](reference/interactive.md) |
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# The shape of a good call
|
|
2
|
+
|
|
3
|
+
Everything the widget needs travels in ONE call, as `operations`. Three ops carry almost every widget:
|
|
4
|
+
|
|
5
|
+
- **`upsertDefinitions`** — declare ALL the CSS classes at once, one class per *look* (not per property).
|
|
6
|
+
- **`upsertElement`** — build the whole tree in a single op, nesting with `children` under `pageRef: "render"`.
|
|
7
|
+
- **`repeatElement`** — the moment two siblings differ only in data, write the row ONCE as a template with
|
|
8
|
+
`{{item.field}}` placeholders and pass `items`. Rows come out numbered (`tile-1`, `tile-2`…). A list inside each
|
|
9
|
+
row is the same op: give a template node `repeat: { items: "{{item.<field>}}", template: … }`.
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"operations": [
|
|
14
|
+
{
|
|
15
|
+
"type": "upsertDefinitions",
|
|
16
|
+
"definitions": {
|
|
17
|
+
"panel": {
|
|
18
|
+
"desktop": {
|
|
19
|
+
"display": "flex",
|
|
20
|
+
"flex-direction": "column",
|
|
21
|
+
"gap": "12px",
|
|
22
|
+
"padding": "16px",
|
|
23
|
+
"color": "var(--color-text-primary, light-dark(#0f172a, #e8eaed))"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"row": {
|
|
27
|
+
"desktop": { "display": "flex", "flex-wrap": "wrap", "gap": "12px" },
|
|
28
|
+
"mobile": { "flex-direction": "column" }
|
|
29
|
+
},
|
|
30
|
+
"tile": {
|
|
31
|
+
"desktop": {
|
|
32
|
+
"flex": "1 1 0%",
|
|
33
|
+
"min-width": "160px",
|
|
34
|
+
"padding": "12px",
|
|
35
|
+
"border-radius": "10px",
|
|
36
|
+
"border": "1px solid var(--color-border-primary, light-dark(#e2e8f0, #333a48))",
|
|
37
|
+
"background-color": "var(--color-background-secondary, light-dark(#ffffff, #1f2430))"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"tile-title": { "desktop": { "margin": "0", "font-size": "15px" } },
|
|
41
|
+
"tile-price": { "desktop": { "margin": "0", "font-size": "13px", "opacity": "0.75" } }
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"type": "upsertElement",
|
|
46
|
+
"pageRef": "render",
|
|
47
|
+
"element": {
|
|
48
|
+
"ref": "panel",
|
|
49
|
+
"type": "container",
|
|
50
|
+
"style": { "base": ["panel"] },
|
|
51
|
+
"children": [{ "ref": "title", "type": "heading", "subType": "h3", "props": { "content": "Plans" } }]
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"type": "repeatElement",
|
|
56
|
+
"pageRef": "render",
|
|
57
|
+
"parentRef": "panel",
|
|
58
|
+
"ref": "plans",
|
|
59
|
+
"style": { "base": ["row"] },
|
|
60
|
+
"template": {
|
|
61
|
+
"ref": "tile",
|
|
62
|
+
"type": "container",
|
|
63
|
+
"style": { "base": ["tile"] },
|
|
64
|
+
"children": [
|
|
65
|
+
{
|
|
66
|
+
"ref": "name",
|
|
67
|
+
"type": "heading",
|
|
68
|
+
"subType": "h4",
|
|
69
|
+
"props": { "content": "{{item.name}}" },
|
|
70
|
+
"style": { "base": ["tile-title"] }
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"ref": "price",
|
|
74
|
+
"type": "paragraph",
|
|
75
|
+
"props": { "content": "{{item.price}}" },
|
|
76
|
+
"style": { "base": ["tile-price"] }
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
"items": [
|
|
81
|
+
{ "name": "Starter", "price": "$0 / month" },
|
|
82
|
+
{ "name": "Team", "price": "$19 / month" },
|
|
83
|
+
{ "name": "Business", "price": "$49 / month" }
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Making it interactive
|
|
2
|
+
|
|
3
|
+
Widgets are not only static: `upsertInteractionFlow` attaches a flow to an element — a `trigger` node first
|
|
4
|
+
(`onClick`), then the steps that run after it, in order.
|
|
5
|
+
|
|
6
|
+
**To show and hide, use `toggleState`.** Every element registers `setState` and `toggleState` as `callback`
|
|
7
|
+
actions, and `elementId` names the element they act on — the flow's own element by default, or another element's
|
|
8
|
+
ref to act on that one. `toggleState` with `category: "state"` and `key: "visibility"` is the show/hide toggle;
|
|
9
|
+
there is no separate `toggleVisibility` action. The element starts hidden with `initialState: { "visibility":
|
|
10
|
+
false }` on the element itself.
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"operations": [
|
|
15
|
+
{
|
|
16
|
+
"type": "upsertElement",
|
|
17
|
+
"pageRef": "render",
|
|
18
|
+
"element": {
|
|
19
|
+
"ref": "card",
|
|
20
|
+
"type": "container",
|
|
21
|
+
"children": [
|
|
22
|
+
{ "ref": "card-head", "type": "button", "props": { "content": "Details" } },
|
|
23
|
+
{
|
|
24
|
+
"ref": "card-body",
|
|
25
|
+
"type": "container",
|
|
26
|
+
"initialState": { "visibility": false },
|
|
27
|
+
"children": [
|
|
28
|
+
{ "ref": "card-text", "type": "paragraph", "props": { "content": "The hidden detail." } }
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"type": "upsertInteractionFlow",
|
|
36
|
+
"pageRef": "render",
|
|
37
|
+
"ref": "card-head",
|
|
38
|
+
"nodes": [
|
|
39
|
+
{ "title": "On click", "nodeType": "trigger", "action": "onClick" },
|
|
40
|
+
{
|
|
41
|
+
"title": "Toggle body",
|
|
42
|
+
"nodeType": "callback",
|
|
43
|
+
"action": "toggleState",
|
|
44
|
+
"elementId": "card-body",
|
|
45
|
+
"params": { "category": "state", "key": "visibility" }
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Expand/collapse is **one step on one trigger** — never two `setState` branches under opposite `when` conditions,
|
|
54
|
+
which read the state as it was when the flow started and so are always one click behind. For several flows on one
|
|
55
|
+
element (an `onClick` and an `onMouseEnter`, or two independent clicks), call `upsertInteractionFlow` again with
|
|
56
|
+
the same `ref` and **omit `flowId`**; passing an existing `flowId` replaces that flow instead.
|
|
57
|
+
|
|
58
|
+
Two things routinely go wrong. Events **bubble**, so a clickable element inside another clickable element runs
|
|
59
|
+
BOTH flows — put the trigger on one of them (the render warns you and names the pair). And a `globalCallback`
|
|
60
|
+
(`addNotification`, `navigate`, app-level `setState`) is provided by a module, not by an element, so **omit
|
|
61
|
+
`elementId`** on those; a step with the wrong node type resolves against nothing and silently does nothing.
|
|
62
|
+
|
|
63
|
+
For data-driven widgets, an `apiContainer` fetches at runtime and `upsertBinding` wires the result into elements —
|
|
64
|
+
see "Data & interactivity" in `plitzi://render/guide`, which is also the full reference for everything above.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# The six things that decide whether it looks good
|
|
2
|
+
|
|
3
|
+
1. **Width is free, height is scarce.** The widget renders in a side panel. A plain container stacks its children
|
|
4
|
+
vertically — that is the tall, half-empty default to avoid. Put peers (metrics, plans, options, image + text) in
|
|
5
|
+
a wrapping row (`display:flex` + `flex-wrap:wrap`, children `flex:1 1 0%` with a `min-width` as the wrap
|
|
6
|
+
threshold) or a grid with `grid-template-columns: repeat(auto-fit, minmax(160px, 1fr))`. Stack only what reads in
|
|
7
|
+
order: heading over paragraph, forms, steps, prose.
|
|
8
|
+
2. **The host may be in dark mode.** Never hardcode a light palette. Take colours from the host variables with a
|
|
9
|
+
`light-dark()` fallback, and always set `color` wherever you set `background-color`.
|
|
10
|
+
3. **You are not styling from zero.** The SDK's per-type CSS imposes **no minimum size**: a rail, a divider, a dot
|
|
11
|
+
or a narrow cell is exactly as small as you make it, and no `"min-width": "0"` escape hatch is needed. The flip
|
|
12
|
+
side is that an element with no content and no size takes no space at all — give a spacer its own `height`.
|
|
13
|
+
What does land on your elements is the BROWSER's own defaults: `heading` and `paragraph` keep their UA margins
|
|
14
|
+
(zero them, space with the parent's `gap`), `list` a 40px `padding-left`, `button` its native chrome. Borders
|
|
15
|
+
start at `0 solid`, so `border-color` alone paints nothing — give `border-width` too.
|
|
16
|
+
4. **Draw with inline SVG, on a budget.** A logo, a sparkline, a badge or a decorative shape goes in a `blockHtml`
|
|
17
|
+
element whose `props.content` is an `<svg>` — keep a `viewBox` with `width`/`height` `100%` so the element's
|
|
18
|
+
class sizes it, and `fill`/`stroke` `currentColor` so it follows the theme. A handful of paths, drawn once and
|
|
19
|
+
reused. Never a `data:` URI, and never a full illustration or a photo-real scene: that costs more than the rest
|
|
20
|
+
of the widget, so use an `https` image, a flat colour or a CSS gradient instead. Markup only — `<script>` and
|
|
21
|
+
inline `on*` handlers are rejected.
|
|
22
|
+
5. **Only use image URLs you have seen work.** Everything the widget loads from outside is fetched for it by the
|
|
23
|
+
render server, so redirects, hotlink rules and missing CORS headers are already handled and there is nothing to
|
|
24
|
+
configure — but nothing can guess a URL. Write a direct file URL, never a page about the picture, a search
|
|
25
|
+
result, or a pattern assembled from memory: one that 404s leaves a grey box in the middle of a finished layout.
|
|
26
|
+
With no URL you trust, draw that block instead (a gradient, a flat colour, an inline `<svg>`).
|
|
27
|
+
6. **Write CSS plainly.** Kebab-case properties, shorthands welcome (`padding: 8px 16px`, `border: 1px solid red`,
|
|
28
|
+
`font: bold 16px/1.5 Arial`) — they are expanded and stored as longhands, so a breakpoint or state can override
|
|
29
|
+
one property on its own.
|