@plitzi/sdk-server 0.32.15 → 0.32.17

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/adapters/jsonAdapters.js +2 -1
  3. package/dist/core/createServer.js +10 -2
  4. package/dist/core/http/stages/middlewares.js +3 -2
  5. package/dist/core/http/stages/pluginAssets.js +3 -2
  6. package/dist/core/http/stages/static.js +3 -2
  7. package/dist/core/requestParser.js +1 -1
  8. package/dist/core/server/{ssrServer.js → pageServer.js} +15 -6
  9. package/dist/core/services/registry.js +2 -2
  10. package/dist/helpers/cache/defaults.js +3 -3
  11. package/dist/helpers/serverLog.js +3 -3
  12. package/dist/index.js +1 -1
  13. package/dist/modules/mcp/apps/shared/shell.ejs +19 -2
  14. package/dist/modules/mcp/catalogs/cssCatalog/border.js +30 -0
  15. package/dist/modules/mcp/catalogs/cssCatalog/box.js +31 -0
  16. package/dist/modules/mcp/catalogs/cssCatalog/helpers.js +185 -0
  17. package/dist/modules/mcp/catalogs/cssCatalog/index.js +131 -0
  18. package/dist/modules/mcp/catalogs/cssCatalog/layout.js +98 -0
  19. package/dist/modules/mcp/catalogs/cssCatalog/visual.js +144 -0
  20. package/dist/modules/mcp/catalogs/registry.js +1 -1
  21. package/dist/modules/mcp/helpers/guide.js +23 -13
  22. package/dist/modules/mcp/helpers/space.js +1 -1
  23. package/dist/modules/mcp/resources/core.js +5 -2
  24. package/dist/modules/mcp/resources/primer.js +2 -1
  25. package/dist/modules/mcp/resources/register.js +6 -3
  26. package/dist/modules/mcp/resources/renderGuide.js +90 -10
  27. package/dist/modules/mcp/server.js +1 -1
  28. package/dist/modules/mcp/tools/operations/schema/interactions/upsertInteractionFlow.js +3 -2
  29. package/dist/modules/mcp/tools/operations/style/shared.js +2 -2
  30. package/dist/modules/mcp/tools/operations/style/write.js +2 -2
  31. package/dist/modules/mcp/tools/render.js +1 -1
  32. package/dist/modules/mcp/tools/shared/validator/audit.js +1 -4
  33. package/dist/modules/mcp/tools/shared/validator/css.js +3 -11
  34. package/dist/modules/mcp/tools/shared/validator/elements.js +4 -1
  35. package/dist/modules/mcp/tools/shared/validator/index.js +15 -15
  36. package/dist/modules/mcp/tools/shared/validator/interactions.js +1 -4
  37. package/dist/modules/oauth/authorize.js +2 -1
  38. package/dist/modules/oauth/metadata.js +15 -2
  39. package/dist/modules/oauth/records.js +1 -1
  40. package/dist/modules/oauth/token.js +2 -2
  41. package/dist/modules/ssr/registerExternalPlugins.js +1 -1
  42. package/dist/plugins/manager.js +1 -1
  43. package/dist/src/core/createServer.d.ts +9 -1
  44. package/dist/src/core/server/pageServer.d.ts +10 -0
  45. package/dist/src/core/services/registry.d.ts +1 -1
  46. package/dist/src/helpers/serverLog.d.ts +1 -1
  47. package/dist/src/modules/mcp/catalogs/cssCatalog/border.d.ts +4 -0
  48. package/dist/src/modules/mcp/catalogs/cssCatalog/box.d.ts +4 -0
  49. package/dist/src/modules/mcp/catalogs/cssCatalog/helpers.d.ts +34 -0
  50. package/dist/src/modules/mcp/catalogs/{cssCatalog.d.ts → cssCatalog/index.d.ts} +10 -5
  51. package/dist/src/modules/mcp/catalogs/cssCatalog/layout.d.ts +9 -0
  52. package/dist/src/modules/mcp/catalogs/cssCatalog/visual.d.ts +8 -0
  53. package/dist/src/modules/mcp/helpers/guide.d.ts +2 -2
  54. package/dist/src/modules/mcp/helpers/space.d.ts +1 -1
  55. package/dist/src/modules/mcp/resources/index.d.ts +1 -1
  56. package/dist/src/modules/mcp/tests/apply.test.d.ts +1 -0
  57. package/dist/src/modules/mcp/tests/bindings.test.d.ts +1 -0
  58. package/dist/src/modules/mcp/tests/css.test.d.ts +1 -0
  59. package/dist/src/modules/mcp/tests/helpers.d.ts +13 -0
  60. package/dist/src/modules/mcp/tests/interactions.test.d.ts +1 -0
  61. package/dist/src/modules/mcp/tests/pages.test.d.ts +1 -0
  62. package/dist/src/modules/mcp/tests/reads.test.d.ts +1 -0
  63. package/dist/src/modules/mcp/tests/search.test.d.ts +1 -0
  64. package/dist/src/modules/mcp/tests/styles.test.d.ts +1 -0
  65. package/dist/src/modules/mcp/tests/tools.test.d.ts +1 -0
  66. package/dist/src/modules/mcp/tests/validation.test.d.ts +1 -0
  67. package/dist/src/modules/mcp/tools/screenshot.d.ts +1 -1
  68. package/dist/src/modules/oauth/metadata.d.ts +14 -1
  69. package/package.json +10 -10
  70. package/dist/modules/mcp/catalogs/cssCatalog.js +0 -205
  71. package/dist/src/core/server/ssrServer.d.ts +0 -2
  72. /package/dist/src/modules/mcp/{mcp.test.d.ts → catalogs/cssCatalog/cssCatalog.test.d.ts} +0 -0
@@ -0,0 +1,98 @@
1
+ import { FLEX_DIRECTIONS, FLEX_WRAPS, LENGTH_RE, NUMBER_RE, QUOTE_RE, splitOn, splitPair, splitTokens } from "./helpers.js";
2
+ //#region src/modules/mcp/catalogs/cssCatalog/layout.ts
3
+ var expandOverflow = (value, out) => {
4
+ const [x, y = x] = splitTokens(value);
5
+ out["overflow-x"] = x;
6
+ out["overflow-y"] = y;
7
+ };
8
+ var FLEX_KEYWORDS = /* @__PURE__ */ new Map([
9
+ ["none", [
10
+ "0",
11
+ "0",
12
+ "auto"
13
+ ]],
14
+ ["auto", [
15
+ "1",
16
+ "1",
17
+ "auto"
18
+ ]],
19
+ ["initial", [
20
+ "0",
21
+ "1",
22
+ "auto"
23
+ ]]
24
+ ]);
25
+ var writeFlex = (out, grow, shrink, basis) => {
26
+ out["flex-grow"] = grow;
27
+ out["flex-shrink"] = shrink;
28
+ out["flex-basis"] = basis;
29
+ };
30
+ var expandFlex = (value, out) => {
31
+ const tokens = splitTokens(value);
32
+ if (tokens.length === 1) {
33
+ const [only] = tokens;
34
+ const keyword = FLEX_KEYWORDS.get(only);
35
+ if (keyword) writeFlex(out, ...keyword);
36
+ else if (NUMBER_RE.test(only)) writeFlex(out, only, "1", "0%");
37
+ else writeFlex(out, "1", "1", only);
38
+ return;
39
+ }
40
+ const [grow, second, third] = tokens;
41
+ if (!NUMBER_RE.test(grow)) return;
42
+ if (tokens.length === 2) if (NUMBER_RE.test(second)) writeFlex(out, grow, second, "0%");
43
+ else writeFlex(out, grow, "1", second);
44
+ else writeFlex(out, grow, second, third);
45
+ };
46
+ var expandFlexFlow = (value, out) => {
47
+ for (const token of splitTokens(value)) if (FLEX_DIRECTIONS.has(token)) out["flex-direction"] = token;
48
+ else if (FLEX_WRAPS.has(token)) out["flex-wrap"] = token;
49
+ };
50
+ var expandPlacePair = (prefix, value, out) => {
51
+ const [align, justify = align] = splitTokens(value);
52
+ const prop = prefix.slice(6);
53
+ out[`align-${prop}`] = align;
54
+ out[`justify-${prop}`] = justify;
55
+ };
56
+ var expandGridLine = (key, value, out) => {
57
+ const axis = key.slice(5);
58
+ const [start, end = "auto"] = splitOn(value, "/");
59
+ out[`grid-${axis}-start`] = start;
60
+ out[`grid-${axis}-end`] = end;
61
+ };
62
+ var expandGridArea = (value, out) => {
63
+ const [rowStart, colStart = rowStart, rowEnd = colStart, colEnd = rowEnd] = splitOn(value, "/");
64
+ out["grid-row-start"] = rowStart;
65
+ out["grid-column-start"] = colStart;
66
+ out["grid-row-end"] = rowEnd;
67
+ out["grid-column-end"] = colEnd;
68
+ };
69
+ var expandColumns = (value, out) => {
70
+ const tokens = splitTokens(value);
71
+ if (tokens.length === 1) {
72
+ const [only] = tokens;
73
+ if (NUMBER_RE.test(only)) out["column-count"] = only;
74
+ else out["column-width"] = only;
75
+ } else if (tokens.length >= 2) {
76
+ const [first, second] = tokens;
77
+ const [width, count] = NUMBER_RE.test(first) ? [second, first] : [first, second];
78
+ out["column-width"] = width;
79
+ out["column-count"] = count;
80
+ }
81
+ };
82
+ var isTrackList = (tokens) => tokens.some((token) => token === "none" || token === "auto" || token === "min-content" || token === "max-content" || token.startsWith("[") || token.startsWith("minmax(") || token.startsWith("repeat(") || token.startsWith("fit-content(") || LENGTH_RE.test(token) || /^-?[\d.]+fr$/.test(token));
83
+ var expandGridTemplate = (value, out) => {
84
+ const [first, second] = splitPair(value, "/");
85
+ const rowTokens = splitTokens(first);
86
+ const areas = rowTokens.filter((token) => QUOTE_RE.test(token));
87
+ const sizes = rowTokens.filter((token) => !QUOTE_RE.test(token));
88
+ if (second !== void 0) {
89
+ if (second !== "") out["grid-template-columns"] = second;
90
+ } else if (areas.length === 0) {
91
+ if (isTrackList(rowTokens)) out["grid-template-columns"] = first;
92
+ return;
93
+ }
94
+ if (areas.length > 0) out["grid-template-areas"] = areas.join(" ");
95
+ if (sizes.length > 0) out["grid-template-rows"] = sizes.join(" ");
96
+ };
97
+ //#endregion
98
+ export { expandColumns, expandFlex, expandFlexFlow, expandGridArea, expandGridLine, expandGridTemplate, expandOverflow, expandPlacePair };
@@ -0,0 +1,144 @@
1
+ import { ANIMATION_DIRECTIONS, ANIMATION_FILL_MODES, ANIMATION_PLAY_STATES, FONT_STYLE_KEYWORDS, FONT_VARIANT_KEYWORDS, FONT_WEIGHT_KEYWORDS, LENGTH_RE, LIST_STYLE_POSITIONS, LIST_STYLE_TYPES, NUMBER_RE, TEXT_DECORATION_LINES, TEXT_DECORATION_STYLES, TIME_RE, isTimingFunction, splitOn, splitPair, splitTokens } from "./helpers.js";
2
+ import { classifyBorderTokens } from "./border.js";
3
+ //#region src/modules/mcp/catalogs/cssCatalog/visual.ts
4
+ var expandLayers = (value, out, expandLayer) => {
5
+ const layers = splitOn(value, ",");
6
+ if (layers.length === 1) {
7
+ expandLayer(layers[0], out);
8
+ return;
9
+ }
10
+ const perLayer = layers.map((layer) => {
11
+ const layerOut = {};
12
+ expandLayer(layer, layerOut);
13
+ return layerOut;
14
+ });
15
+ const keys = new Set(perLayer.flatMap((layer) => Object.keys(layer)));
16
+ for (const key of keys) out[key] = perLayer.map((layer) => String(layer[key] ?? "initial")).join(", ");
17
+ };
18
+ var expandOutline = (value, out) => {
19
+ for (const [kind, token] of Object.entries(classifyBorderTokens(value))) out[`outline-${kind}`] = token;
20
+ };
21
+ var expandListStyle = (value, out) => {
22
+ for (const token of splitTokens(value)) if (token.startsWith("url(")) out["list-style-image"] = token;
23
+ else if (LIST_STYLE_POSITIONS.has(token)) out["list-style-position"] = token;
24
+ else if (LIST_STYLE_TYPES.has(token)) out["list-style-type"] = token;
25
+ };
26
+ var expandTextDecoration = (value, out) => {
27
+ for (const token of splitTokens(value)) if (TEXT_DECORATION_LINES.has(token)) out["text-decoration-line"] = token;
28
+ else if (TEXT_DECORATION_STYLES.has(token)) out["text-decoration-style"] = token;
29
+ else out["text-decoration-color"] = token;
30
+ };
31
+ var expandTransitionLayer = (layer, out) => {
32
+ let foundDuration = false;
33
+ for (const token of splitTokens(layer)) if (isTimingFunction(token)) out["transition-timing-function"] = token;
34
+ else if (TIME_RE.test(token) || NUMBER_RE.test(token)) if (foundDuration) out["transition-delay"] = token;
35
+ else {
36
+ out["transition-duration"] = token;
37
+ foundDuration = true;
38
+ }
39
+ else if (!out["transition-property"]) out["transition-property"] = token;
40
+ };
41
+ var expandTransition = (value, out) => expandLayers(value, out, expandTransitionLayer);
42
+ var expandAnimationLayer = (layer, out) => {
43
+ let foundDuration = false;
44
+ for (const token of splitTokens(layer)) if (isTimingFunction(token)) out["animation-timing-function"] = token;
45
+ else if (TIME_RE.test(token)) if (foundDuration) out["animation-delay"] = token;
46
+ else {
47
+ out["animation-duration"] = token;
48
+ foundDuration = true;
49
+ }
50
+ else if (token === "infinite" || NUMBER_RE.test(token)) out["animation-iteration-count"] = token;
51
+ else if (ANIMATION_DIRECTIONS.has(token) && !out["animation-direction"]) out["animation-direction"] = token;
52
+ else if (ANIMATION_FILL_MODES.has(token) && !out["animation-fill-mode"]) out["animation-fill-mode"] = token;
53
+ else if (ANIMATION_PLAY_STATES.has(token) && !out["animation-play-state"]) out["animation-play-state"] = token;
54
+ else if (!out["animation-name"]) out["animation-name"] = token;
55
+ };
56
+ var expandAnimation = (value, out) => expandLayers(value, out, expandAnimationLayer);
57
+ var BACKGROUND_REPEATS = /* @__PURE__ */ new Set([
58
+ "repeat",
59
+ "no-repeat",
60
+ "repeat-x",
61
+ "repeat-y",
62
+ "space",
63
+ "round"
64
+ ]);
65
+ var BACKGROUND_ATTACHMENTS = /* @__PURE__ */ new Set([
66
+ "scroll",
67
+ "fixed",
68
+ "local"
69
+ ]);
70
+ var BACKGROUND_BOXES = /* @__PURE__ */ new Set([
71
+ "border-box",
72
+ "padding-box",
73
+ "content-box"
74
+ ]);
75
+ var BACKGROUND_POSITIONS = /* @__PURE__ */ new Set([
76
+ "center",
77
+ "top",
78
+ "bottom",
79
+ "left",
80
+ "right"
81
+ ]);
82
+ var isImage = (token) => token.startsWith("url(") || token.includes("-gradient(");
83
+ var isPosition = (token) => BACKGROUND_POSITIONS.has(token) || LENGTH_RE.test(token);
84
+ var isSize = (token) => token === "cover" || token === "contain" || token === "auto" || LENGTH_RE.test(token);
85
+ var expandBackgroundLayer = (layer, out) => {
86
+ const [beforeSlash, afterSlash] = splitPair(layer, "/");
87
+ const tokens = splitTokens(beforeSlash);
88
+ if (afterSlash !== void 0) {
89
+ let positionStart = tokens.length;
90
+ while (positionStart > 0 && isPosition(tokens[positionStart - 1])) positionStart -= 1;
91
+ const position = tokens.splice(positionStart, tokens.length - positionStart);
92
+ if (position.length > 0) out["background-position"] = position.join(" ");
93
+ const afterTokens = splitTokens(afterSlash);
94
+ let sizeEnd = 0;
95
+ while (sizeEnd < afterTokens.length && isSize(afterTokens[sizeEnd])) sizeEnd += 1;
96
+ if (sizeEnd > 0) out["background-size"] = afterTokens.slice(0, sizeEnd).join(" ");
97
+ tokens.push(...afterTokens.slice(sizeEnd));
98
+ }
99
+ const positions = [];
100
+ for (const token of tokens) if (isImage(token)) out["background-image"] = token;
101
+ else if (BACKGROUND_REPEATS.has(token)) out["background-repeat"] = token;
102
+ else if (token === "cover" || token === "contain") out["background-size"] = token;
103
+ else if (BACKGROUND_ATTACHMENTS.has(token)) out["background-attachment"] = token;
104
+ else if (BACKGROUND_BOXES.has(token)) if (out["background-origin"]) out["background-clip"] = token;
105
+ else out["background-origin"] = token;
106
+ else if (isPosition(token)) positions.push(token);
107
+ else if (!out["background-color"]) out["background-color"] = token;
108
+ if (positions.length > 0) out["background-position"] = positions.join(" ");
109
+ };
110
+ var expandBackground = (value, out) => expandLayers(value, out, expandBackgroundLayer);
111
+ var expandFont = (value, out) => {
112
+ const [beforeSlash, afterSlash] = splitPair(value, "/");
113
+ const tokens = splitTokens(beforeSlash);
114
+ if (afterSlash !== void 0) {
115
+ const [lineHeight, ...family] = splitTokens(afterSlash);
116
+ out["line-height"] = lineHeight;
117
+ if (family.length > 0) out["font-family"] = family.join(" ");
118
+ }
119
+ const family = [];
120
+ let foundSize = false;
121
+ for (const token of tokens) {
122
+ if (foundSize) {
123
+ family.push(token);
124
+ continue;
125
+ }
126
+ if (token === "normal") {
127
+ const slot = [
128
+ "font-style",
129
+ "font-variant",
130
+ "font-weight"
131
+ ].find((prop) => !out[prop]);
132
+ if (slot) out[slot] = token;
133
+ } else if (FONT_STYLE_KEYWORDS.has(token) && !out["font-style"]) out["font-style"] = token;
134
+ else if (FONT_VARIANT_KEYWORDS.has(token) && !out["font-variant"]) out["font-variant"] = token;
135
+ else if (FONT_WEIGHT_KEYWORDS.has(token) && !out["font-weight"]) out["font-weight"] = token;
136
+ else if (LENGTH_RE.test(token)) {
137
+ out["font-size"] = token;
138
+ foundSize = true;
139
+ }
140
+ }
141
+ if (family.length > 0) out["font-family"] = family.join(" ");
142
+ };
143
+ //#endregion
144
+ export { expandAnimation, expandBackground, expandFont, expandListStyle, expandOutline, expandTextDecoration, expandTransition };
@@ -1,5 +1,5 @@
1
1
  import { BUILTIN_COMPONENTS } from "./builtinComponents.js";
2
- import { cssProperties } from "./cssCatalog.js";
2
+ import { cssProperties } from "./cssCatalog/index.js";
3
3
  //#region src/modules/mcp/catalogs/registry.ts
4
4
  var enrichType = (typeName, info, catalog) => {
5
5
  const entry = catalog?.[typeName];
@@ -1,5 +1,5 @@
1
1
  //#region src/modules/mcp/helpers/guide.ts
2
- 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 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. Refs accept a semantic idRef ([A-Za-z0-9_-] starting with a letter, unique, chosen by you) or the raw id — the idRef is ALSO the runtime wiring key, so a provider source is `<type>_<idRef>.<field>`, visible to the provider’s DESCENDANTS only (bind inside its subtree). CSS is kebab-case and ATOMIC (shorthands like border/padding expanded; compound ones like flex/background/font rejected use 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 refs by label/type/attribute; plitzi_read BATCH-fetches URIs you already hold. Reach for search/read to work; browse resources to discover. 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). 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.";
2
+ 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 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. Refs accept a semantic idRef ([A-Za-z0-9_-] starting with a letter, unique, chosen by you) or the raw id — the idRef is ALSO the runtime wiring key, so a provider source is `<type>_<idRef>.<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 refs by label/type/attribute; plitzi_read BATCH-fetches URIs you already hold. Reach for search/read to work; browse resources to discover. 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). 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
3
  var guideQuickstart = `# Plitzi AI MCP — quickstart
4
4
  This is the condensed guide; read \`plitzi://guide\` for the full reference (every resource, op and example).
5
5
 
@@ -19,10 +19,12 @@ interactions target by it. A dot would split that path; an **underscore is fine*
19
19
  from the idRef (element types have none), so \`list_food_item\` reads unambiguously as type \`list\`, idRef \`food_item\`.
20
20
 
21
21
  **Styling:** CSS keys are **kebab-case** (\`background-color\`); \`var(--token)\` for style vars, \`{{name}}\` for schema
22
- vars. Shorthands \`border\`/\`padding\`/\`margin\`/\`gap\` are expanded for you. Write **atomically no compound
23
- shorthands** (\`flex\`, \`background\`, \`font\`, \`transition\`, \`grid\` are rejected/warned): use their longhands, and
24
- express flex layout as \`display: flex\` + \`flex-direction\`/\`align-items\`/… Mind a type's \`defaultStyle\` (\`text\`
25
- is \`display: inline\`). Global styles (\`button {…}\`) and id styles (\`#id\`) have their own ops.
22
+ vars. **Write plain CSS** shorthands (\`border\`, \`padding\`, \`margin\`, \`gap\`, \`overflow\`, \`flex\`, \`background\`,
23
+ \`font\`, \`transition\`, \`animation\`, \`grid\`, \`grid-row\`/\`grid-column\`, \`place-*\`, \`outline\`, \`columns\`,
24
+ \`list-style\`, \`text-decoration\`) are expanded to their longhands for you, so \`border: 1px solid red\` is stored as
25
+ \`border-top-color\`/\`border-top-width\`/\`border-top-style\`/… That is also how you read them back. Flex layout is
26
+ still \`display: flex\` + \`flex-direction\`/\`align-items\`/…, not a \`flex\` value. Mind a type's \`defaultStyle\`
27
+ (\`text\` is \`display: inline\`). Global styles (\`button {…}\`) and id styles (\`#id\`) have their own ops.
26
28
 
27
29
  **Data bindings** (\`upsertBinding\`, category attributes|style|initialState): connect a \`source\` to a \`to\` field.
28
30
  A source \`<type>_<idRef>\` is scoped to the provider's **DESCENDANTS only** — bind inside the provider's subtree
@@ -202,14 +204,22 @@ pointed at the old name is repointed with it, so the element stays wired. You do
202
204
  - A definition lives in the **style schema**; an element's \`style.base\` (element schema) is the link that applies
203
205
  it. Styling an element = upsertDefinition + upsertElement with that ref in \`style.base\`, in one batch.
204
206
  - CSS keys are **kebab-case** (\`background-color\`). camelCase is rejected — read \`plitzi://css-properties\`.
205
- - Common **shorthands are accepted** and expanded for you: \`border\`, \`border-{side}\`, \`border-radius\`,
206
- \`padding\`, \`margin\`, \`inset\`, \`gap\` (they persist as their longhand keys).
207
- - **Write CSS atomically no multi-value compound shorthands.** Plitzi stores each property on its own so a
208
- breakpoint/state/variant can override just that one, so \`flex\`, \`background\`, \`font\`, \`transition\`,
209
- \`animation\`, \`grid\`, \`place-*\`, \`outline\`, \`columns\` are **rejected** (or warned) use their longhands
210
- instead: \`background\` \`background-color\`/\`background-image\`/…, \`font\` \`font-size\`/\`font-weight\`/
211
- \`line-height\`, \`flex: 1\` \`flex-grow\`/\`flex-shrink\`/\`flex-basis\`. **Flex layout is not a \`flex\` value**: set
212
- \`display: flex\` **plus** \`flex-direction\`, \`align-items\`, \`justify-content\` as separate properties.
207
+ - **Write normal CSS — shorthands are accepted and expanded for you.** \`border\`, \`border-{side}\`,
208
+ \`border-width\`/\`-color\`/\`-style\`, \`border-radius\`, \`padding\`, \`margin\`, \`inset\`, \`gap\`, \`overflow\`,
209
+ \`flex\`, \`flex-flow\`, \`background\`, \`font\`, \`transition\`, \`animation\`, \`grid\`, \`grid-template\`,
210
+ \`grid-area\`, \`grid-row\`, \`grid-column\`, \`place-content\`, \`place-items\`, \`place-self\`, \`outline\`,
211
+ \`columns\`, \`list-style\`, \`text-decoration\` the full list is in \`plitzi://css-properties\` under
212
+ \`shorthands\`. They **persist as longhand keys** (that is what a read gives back), so a breakpoint/state/variant
213
+ can still override one property at a time.
214
+ - \`border: 1px solid red\` \`border-top-width: 1px\`, \`border-top-style: solid\`, \`border-top-color: red\`,
215
+ for all four sides. A shorthand also RESETS what it omits, so \`border: none\` clears a width a previous
216
+ definition set.
217
+ - Comma-separated layers are kept per longhand: \`transition: opacity 200ms, transform 300ms\` →
218
+ \`transition-property: opacity, transform\` + \`transition-duration: 200ms, 300ms\`.
219
+ - In a **patch**, a shorthand replaces the longhands it controls (\`padding: 8px\` overwrites a previous
220
+ \`padding-left\`), and \`"border": null\` removes all twelve border longhands.
221
+ - **Flex layout is not a \`flex\` value**: set \`display: flex\` **plus** \`flex-direction\`, \`align-items\`,
222
+ \`justify-content\` as separate properties.
213
223
  - CSS is grouped by breakpoint: \`desktop\`, \`tablet\`, \`mobile\`.
214
224
  - Reference a style variable in CSS as \`var(--name)\`; a schema variable in a prop as \`{{name}}\`.
215
225
  - \`element.style.base\` is a **list** of definition refs; other slots go under \`element.style.slots\`.
@@ -280,7 +280,7 @@ var pageRefOfElement = (schema, el) => {
280
280
  /** Total number of descendant elements under a subtree (excluding the root). */
281
281
  var descendantCount = (schema, rootId) => descendantIds(schema, rootId).length;
282
282
  var emptySpaceMessage = "Space data not available";
283
- var unauthorizedSpaceMessage = "This tool/resource needs a space, but no spaceId could be resolved from the Authorization token";
283
+ var unauthorizedSpaceMessage = "This connection has no space attached (a guest or widgets-only grant, or a token that carries no space), so NOTHING in a space can be read or edited — every other space tool will fail the same way, do not retry them. Use plitzi_render instead: it builds a self-contained widget offline, with no space, backend or account (read plitzi://render/guide). To edit a real space, the user must reconnect the integration and grant access to one.";
284
284
  var generateObjectId = () => {
285
285
  return `${Math.floor(Date.now() / 1e3).toString(16).padStart(8, "0")}${Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join("")}`;
286
286
  };
@@ -1,4 +1,4 @@
1
- import { cssProperties } from "../catalogs/cssCatalog.js";
1
+ import { cssProperties, cssShorthands } from "../catalogs/cssCatalog/index.js";
2
2
  import { buildTypeRegistry } from "../catalogs/registry.js";
3
3
  import "../helpers/uris.js";
4
4
  import { guideText } from "../helpers/guide.js";
@@ -9,7 +9,10 @@ import { envelope } from "./envelope.js";
9
9
  var readCoreResource = (space, uri) => {
10
10
  if (uri === "plitzi://guide") return envelope(guideText);
11
11
  if (uri === "plitzi://types") return envelope(buildTypeRegistry(space.schema, space.catalog));
12
- if (uri === "plitzi://css-properties") return envelope(cssProperties);
12
+ if (uri === "plitzi://css-properties") return envelope({
13
+ properties: cssProperties,
14
+ shorthands: cssShorthands
15
+ });
13
16
  };
14
17
  //#endregion
15
18
  export { readCoreResource };
@@ -1,4 +1,4 @@
1
- import { cssProperties } from "../catalogs/cssCatalog.js";
1
+ import { cssProperties, cssShorthands } from "../catalogs/cssCatalog/index.js";
2
2
  import { buildDataSourceCatalog, buildInteractionCatalog } from "../catalogs/observed.js";
3
3
  import { buildTypeRegistry } from "../catalogs/registry.js";
4
4
  import { guideQuickstart } from "../helpers/guide.js";
@@ -16,6 +16,7 @@ var readPrimerResource = (space, env, uri) => {
16
16
  guide: guideQuickstart,
17
17
  types: buildTypeRegistry(space.schema, space.catalog),
18
18
  cssProperties,
19
+ cssShorthands,
19
20
  pages: pageSummariesToAI(space.schema),
20
21
  folders: foldersToAI(space.schema),
21
22
  definitions: definitionRefs(space.style),
@@ -1,4 +1,4 @@
1
- import { cssProperties } from "../catalogs/cssCatalog.js";
1
+ import { cssProperties, cssShorthands } from "../catalogs/cssCatalog/index.js";
2
2
  import { guideText } from "../helpers/guide.js";
3
3
  import { resourceErrorMessage } from "./canonical.js";
4
4
  import { envelope, jsonContents } from "./envelope.js";
@@ -36,9 +36,12 @@ var registerResources = (server, getSpace, env, log) => {
36
36
  text: guideText
37
37
  }] })));
38
38
  server.registerResource("CSS properties", "plitzi://css-properties", {
39
- description: "Valid kebab-case CSS property keys",
39
+ description: "Valid kebab-case CSS property keys, plus the shorthands that are accepted and auto-expanded",
40
40
  mimeType: "application/json"
41
- }, () => emitStatic("plitzi://css-properties", () => jsonContents("plitzi://css-properties", envelope(cssProperties))));
41
+ }, () => emitStatic("plitzi://css-properties", () => jsonContents("plitzi://css-properties", envelope({
42
+ properties: cssProperties,
43
+ shorthands: cssShorthands
44
+ }))));
42
45
  registerRenderResources(server, log);
43
46
  const fixed = [
44
47
  [
@@ -68,7 +68,21 @@ Styling is separate from structure: declare a class, then attach it by ref.
68
68
  - CSS properties in **kebab-case** (\`background-color\`, \`font-size\`, \`border-radius\`), values as plain strings.
69
69
  - Attach to an element via \`style: { "base": ["card"] }\`. Stack classes: \`"base": ["card", "shadow"]\`.
70
70
  - One \`ref\` can name both an element and its class (as above) — they live in different namespaces.
71
- - Lay containers out with flexbox: \`{ "display": "flex", "flex-direction": "column", "gap": "12px" }\`.
71
+ - Lay containers out with flexbox or grid pick the direction on purpose, see **Fit the panel** below.
72
+ - **You are not styling from zero.** Each type lands on the page with CSS you did not write, and it is the usual
73
+ reason a widget does not look like the definitions say. The SDK stylesheet resets almost nothing (\`box-sizing\`,
74
+ \`border: 0 solid\`, \`body\` margin), so anything you leave unset comes from one of two places:
75
+ - The per-type rule the SDK ships — the one that changes layouts is \`container\`, which carries
76
+ \`min-width: 50px; min-height: 50px\`. A rail, a divider, a dot, a spacer or a narrow cell will NOT go below
77
+ 50px until you say \`"min-width": "0"\` (and/or \`"min-height": "0"\`) on it: a 2px timeline line renders 50px
78
+ wide otherwise. Any flex child that must be allowed to shrink needs it too.
79
+ - The BROWSER's own defaults for the rest: \`heading\` keeps its UA font-size and ~0.67em top/bottom margins,
80
+ \`paragraph\` ~1em margins, \`list\` a 40px \`padding-left\`, \`button\` its native chrome, \`image\` its intrinsic
81
+ size, \`link\` its own colour and underline. In a compact widget set these explicitly — usually
82
+ \`"margin-top": "0"\`, \`"margin-bottom": "0"\` — and space things with the parent's \`gap\` instead.
83
+ - Borders start at \`0 solid\`, so \`border-color\` alone paints nothing: give \`border-width\` (and the colour).
84
+ - The \`defaultStyle\` a type reports in the EDITING catalog (\`plitzi://types\`) is authoring metadata for the
85
+ builder — it does not paint here, so do not count on it in a widget.
72
86
  - **Mind the intrinsic display.** Some types start non-block: \`text\` is \`display: inline\`, so to stack or size it,
73
87
  wrap it in a \`container\` (or set \`display: block\`). \`heading\` and \`paragraph\` are already block.
74
88
  - **Use atomic longhands.** \`padding\`, \`margin\`, \`border\`, \`border-radius\` are fine (they expand cleanly), but
@@ -80,6 +94,62 @@ Styling is separate from structure: declare a class, then attach it by ref.
80
94
  \`{ "desktop": { "background-color": "#3b82f6" }, "states": { "hover": { "desktop": { "background-color": "#2563eb" } } } }\`
81
95
  (\`hover\`, \`active\`, \`focus\`).
82
96
 
97
+ ## Fit the panel — go wide, stay short
98
+
99
+ The widget renders in a **side panel** (Claude Desktop, ChatGPT, the Plitzi builder), so it gets a usable width but
100
+ very little height: everything past the first screenful costs the user a scroll. Height is the scarce resource —
101
+ spend width instead.
102
+
103
+ Plain containers are blocks, so doing nothing stacks children **vertically** and produces exactly the tall,
104
+ half-empty widget to avoid. Choose the axis every time:
105
+
106
+ - **Peers side by side** — metrics, plans, options, a comparison, an image next to its text: a row, wrapping when
107
+ it runs out of width, children sharing it (no fixed widths).
108
+ \`\`\`json
109
+ { "type": "upsertDefinition", "ref": "row", "desktop": { "display": "flex", "flex-direction": "row", "flex-wrap": "wrap", "gap": "12px", "align-items": "stretch" } }
110
+ { "type": "upsertDefinition", "ref": "col", "desktop": { "flex-grow": "1", "flex-basis": "0%", "min-width": "150px" } }
111
+ \`\`\`
112
+ \`flex-grow: 1\` + \`flex-basis: 0%\` splits the row evenly; \`min-width\` is the wrap threshold — under it the item
113
+ drops to the next line by itself, so a narrow panel degrades gracefully with no breakpoints.
114
+ - **Many uniform items** — cards, tiles, a gallery: one grid line does it all.
115
+ \`\`\`json
116
+ { "type": "upsertDefinition", "ref": "grid", "desktop": { "display": "grid", "grid-template-columns": "repeat(auto-fit, minmax(160px, 1fr))", "gap": "12px" } }
117
+ \`\`\`
118
+ - **Label + value pairs** stay on one line (\`display: flex\`, \`justify-content: space-between\`) instead of two.
119
+ - **Vertical is right** for reading order: a heading over its paragraph, a form, a step list, long prose.
120
+ - Force a stack on tiny screens with the \`mobile\` block: \`"mobile": { "flex-direction": "column" }\`.
121
+
122
+ Keep it compact, and the numbers low: \`padding\` 12–16px (24+ only on a single hero card), \`gap\` 8–12px,
123
+ \`font-size\` 13–15px for body and 16–20px for headings. Let the outer container **fill** the panel — no \`width\` on
124
+ it — and reach for \`max-width\` only to stop one lone card from stretching across the whole panel.
125
+
126
+ ## Match the host theme — never hardcode a light palette
127
+
128
+ The widget is embedded in the host's own UI (Claude Desktop, ChatGPT, the builder), and that UI **may be in dark
129
+ mode**. A widget painted with fixed light colours is the most common way to ship something unusable: a white card
130
+ in a dark chat glares, and — worse — text left at a dark default disappears against the host's dark background.
131
+
132
+ The host publishes its palette as CSS variables on the page, so use them for every colour, with a
133
+ \`light-dark(<light>, <dark>)\` fallback for hosts that send none:
134
+
135
+ \`\`\`json
136
+ { "type": "upsertDefinition", "ref": "card", "desktop": {
137
+ "background-color": "var(--color-background-secondary, light-dark(#ffffff, #1f2430))",
138
+ "color": "var(--color-text-primary, light-dark(#0f172a, #e8eaed))",
139
+ "border-width": "1px", "border-style": "solid",
140
+ "border-color": "var(--color-border-primary, light-dark(#e2e8f0, #333a48))"
141
+ } }
142
+ \`\`\`
143
+
144
+ - Surfaces: \`--color-background-primary\` (the page), \`--color-background-secondary\` / \`--color-background-tertiary\`
145
+ (cards, raised areas). Text: \`--color-text-primary\`, \`--color-text-secondary\`, \`--color-text-tertiary\` (muted).
146
+ Borders: \`--color-border-primary\` / \`--color-border-secondary\`. Status pairs: \`--color-background-danger\` |
147
+ \`success\` | \`warning\` | \`info\` with the matching \`--color-text-…\`. Also \`--font-sans\`, \`--border-radius-md\` /
148
+ \`lg\` / \`full\`, \`--shadow-sm\` / \`md\`.
149
+ - **Set \`color\` wherever you set \`background-color\`** — the pair is what stays legible, either one alone is a
150
+ gamble. Same rule for a brand accent: a CTA on your own blue must state its own text colour (\`#ffffff\`).
151
+ - Prefer a border to a drop shadow for separation: shadows all but vanish on a dark surface.
152
+
83
153
  ## Element types (type → what to set)
84
154
 
85
155
  | type | renders | set |
@@ -103,21 +173,31 @@ that is not here (lists, tabs, dialogs, forms, icons…).
103
173
  \`image\`/\`video\` \`src\` accepts any \`https\` URL, or a \`data:\`/\`blob:\` URI for a fully self-contained graphic
104
174
  (e.g. an inline SVG icon or a base64 image) — both render with no extra setup.
105
175
 
106
- ## Full worked example — a pricing card
176
+ ## Full worked example — two plans side by side
177
+
178
+ The cards sit in a wrapping row and split it evenly, so the widget uses the panel's width and stays short; on a
179
+ narrow panel \`min-width\` drops the second card under the first on its own.
107
180
 
108
181
  \`\`\`json
109
182
  {
110
183
  "operations": [
111
- { "type": "upsertDefinition", "ref": "card", "desktop": { "display": "flex", "flex-direction": "column", "gap": "8px", "padding": "24px", "background-color": "#ffffff", "border-radius": "12px", "width": "260px", "text-align": "center", "box-shadow": "0 4px 20px rgba(0,0,0,0.08)" } },
112
- { "type": "upsertDefinition", "ref": "price", "desktop": { "font-size": "36px", "font-weight": "800", "color": "#3b82f6" } },
113
- { "type": "upsertDefinition", "ref": "cta", "desktop": { "background-color": "#3b82f6", "color": "#ffffff", "padding": "12px 20px", "border-radius": "8px", "font-weight": "600" }, "states": { "hover": { "desktop": { "background-color": "#2563eb" } } } },
184
+ { "type": "upsertDefinition", "ref": "plans", "desktop": { "display": "flex", "flex-direction": "row", "flex-wrap": "wrap", "gap": "12px", "align-items": "stretch" } },
185
+ { "type": "upsertDefinition", "ref": "card", "desktop": { "display": "flex", "flex-direction": "column", "gap": "6px", "flex-grow": "1", "flex-basis": "0%", "min-width": "150px", "padding": "16px", "background-color": "var(--color-background-secondary, light-dark(#ffffff, #1f2430))", "color": "var(--color-text-primary, light-dark(#0f172a, #e8eaed))", "border-width": "1px", "border-style": "solid", "border-color": "var(--color-border-primary, light-dark(#e2e8f0, #333a48))", "border-radius": "var(--border-radius-lg, 12px)", "text-align": "center" } },
186
+ { "type": "upsertDefinition", "ref": "price", "desktop": { "font-size": "28px", "font-weight": "800", "color": "#3b82f6" } },
187
+ { "type": "upsertDefinition", "ref": "cta", "desktop": { "background-color": "#3b82f6", "color": "#ffffff", "padding": "10px 16px", "border-radius": "8px", "font-weight": "600" }, "states": { "hover": { "desktop": { "background-color": "#2563eb" } } } },
114
188
  { "type": "upsertElement", "pageRef": "render", "element": {
115
- "ref": "card", "type": "container", "style": { "base": ["card"] },
189
+ "ref": "plans", "type": "container", "style": { "base": ["plans"] },
116
190
  "children": [
117
- { "ref": "plan", "type": "heading", "subType": "h3", "props": { "content": "Pro" } },
118
- { "ref": "amount", "type": "text", "props": { "content": "$29/mo" }, "style": { "base": ["price"] } },
119
- { "ref": "feat", "type": "paragraph", "props": { "content": "Unlimited projects" } },
120
- { "ref": "buy", "type": "button", "props": { "content": "Start free trial" }, "style": { "base": ["cta"] } }
191
+ { "ref": "free", "type": "container", "style": { "base": ["card"] }, "children": [
192
+ { "ref": "free-plan", "type": "heading", "subType": "h3", "props": { "content": "Starter" } },
193
+ { "ref": "free-amount", "type": "text", "props": { "content": "$0" }, "style": { "base": ["price"] } },
194
+ { "ref": "free-buy", "type": "button", "props": { "content": "Start free" }, "style": { "base": ["cta"] } }
195
+ ] },
196
+ { "ref": "pro", "type": "container", "style": { "base": ["card"] }, "children": [
197
+ { "ref": "pro-plan", "type": "heading", "subType": "h3", "props": { "content": "Pro" } },
198
+ { "ref": "pro-amount", "type": "text", "props": { "content": "$29/mo" }, "style": { "base": ["price"] } },
199
+ { "ref": "pro-buy", "type": "button", "props": { "content": "Start free trial" }, "style": { "base": ["cta"] } }
200
+ ] }
121
201
  ]
122
202
  } }
123
203
  ]
@@ -43,7 +43,7 @@ var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) =>
43
43
  const getSpace = () => spacePromise ??= loadSpace();
44
44
  const server = new McpServer({
45
45
  name: "plitzi-mcp",
46
- version: "0.32.15"
46
+ version: "0.32.17"
47
47
  }, { instructions: serverInstructions });
48
48
  registerResources(server, getSpace, MCP_ENV, log);
49
49
  registerApps(server);
@@ -16,13 +16,14 @@ var upsertInteractionFlow = (space, env, op) => {
16
16
  const found = resolveElement(space, env, op.pageRef, op.ref);
17
17
  if ("error" in found) return found.error;
18
18
  const ownerRef = ensureIdRef(space, found.el);
19
- const { flowId, record } = materializeFlow((op.flowId && !op.nodes[0].id ? [{
19
+ const nodes = (op.flowId && !op.nodes[0].id ? [{
20
20
  ...op.nodes[0],
21
21
  id: op.flowId
22
22
  }, ...op.nodes.slice(1)] : op.nodes).map((node) => node.elementId === void 0 || node.nodeType === "utility" ? node : {
23
23
  ...node,
24
24
  elementId: resolveTargetRef(space, node.elementId)
25
- }), ownerRef);
25
+ });
26
+ const { flowId, record } = materializeFlow(nodes, ownerRef);
26
27
  const interactions = found.el.definition.interactions ?? {};
27
28
  const targetFlow = op.flowId ?? flowId;
28
29
  let replaced = false;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  //#region src/modules/mcp/tools/operations/style/shared.ts
3
- var cssMap = z.record(z.string(), z.union([z.string(), z.number()])).describe("kebab-case CSS props; use var(--name) for tokens and {{name}} for schema vars");
3
+ var cssMap = z.record(z.string(), z.union([z.string(), z.number()])).describe("Plain kebab-case CSS. Shorthands are welcome — `border: 1px solid red`, `padding: 8px 16px`, `font: bold 16px/1.5 Arial`, `transition: opacity 200ms ease` — and are stored as their longhands so a breakpoint/state/variant can override each property on its own. Use var(--name) for style tokens and {{name}} for schema vars.");
4
4
  var displayModeCss = z.object({
5
5
  desktop: cssMap.optional(),
6
6
  tablet: cssMap.optional(),
@@ -14,7 +14,7 @@ var cssPatchMap = z.record(z.string(), z.union([
14
14
  z.string(),
15
15
  z.number(),
16
16
  z.null()
17
- ])).describe("kebab-case CSS props; a value of null removes the property, others are merged onto the existing CSS");
17
+ ])).describe("Plain kebab-case CSS merged onto the existing declarations; shorthands are accepted and expanded, so `padding: 8px` replaces all four sides. A value of null removes the property null on a shorthand (`border: null`) removes every longhand it controls.");
18
18
  var displayModeCssPatch = z.object({
19
19
  desktop: cssPatchMap.optional(),
20
20
  tablet: cssPatchMap.optional(),
@@ -1,4 +1,4 @@
1
- import { expandShorthand } from "../../../catalogs/cssCatalog.js";
1
+ import { expandShorthand, expandShorthandPatch } from "../../../catalogs/cssCatalog/index.js";
2
2
  import { defUri, defsUri, globalUri, globalsUri, idUri, idsUri, styleVarUri, styleVarsUri } from "../../../helpers/uris.js";
3
3
  import { fail } from "../../../helpers/opResult.js";
4
4
  import processSelector from "@plitzi/sdk-style/helpers/processSelector";
@@ -65,7 +65,7 @@ var writeStyleItem = (style, ref, base, slots, itemType, componentType) => {
65
65
  };
66
66
  var mergeCss = (base, patch) => {
67
67
  const merged = { ...base };
68
- for (const [key, value] of Object.entries(patch ?? {})) if (value === null) Reflect.deleteProperty(merged, key);
68
+ for (const [key, value] of Object.entries(expandShorthandPatch(patch ?? {}))) if (value === null) Reflect.deleteProperty(merged, key);
69
69
  else merged[key] = value;
70
70
  return Object.keys(merged).length > 0 ? merged : void 0;
71
71
  };
@@ -105,7 +105,7 @@ var toRenderResult = (res) => {
105
105
  var renderTool = defineTool({
106
106
  name: "plitzi_render",
107
107
  title: "Render widget",
108
- description: "Show the user a real, rendered UI widget instead of describing one — cards, hero sections, pricing tables, forms, menus, checklists, profiles, galleries. It runs the Plitzi SDK fully offline: no backend, account, or setup. Reach for it whenever a visual layout beats prose: the user asks you to design/build/show something, OR your answer is naturally visual (a recipe → a card, a comparison → a table, steps → a checklist). Prefer showing over telling.\n\nAuthor the widget as an ordered list of `operations` that build an element tree under the pre-seeded root page \"render\". Three rules:\n1. STRUCTURE — one upsertElement builds the whole tree: set pageRef:\"render\" and give element a nested `children` array. Each element is { ref (unique), type, subType?, props?, style?, children? }; children render in order. (To attach to something you already made, use a top-level parentRef:\"<existing ref>\" instead.)\n2. STYLE — declare reusable classes with upsertDefinition { ref, desktop:{ …CSS props in kebab-case… } }, then attach via the element style:{ base:[\"<class ref>\"] }. Lay containers out with flex/grid.\n3. CONTENT — visible copy goes in props.content (text, heading, paragraph, button); heading level is the element subType (\"h1\"..\"h6\"); image/video take props.src. An unknown prop comes back as a warning naming the right one.\n\nCommon types: container, heading, paragraph, text, button, link, image, video, list, listItem, markdown (plitzi://render/types lists every built-in type with descriptions). Widgets can also be data-driven and interactive — an apiContainer fetches at runtime, upsertBinding wires data into elements, and upsertInteractionFlow makes them react to clicks (see the guide).\nREAD the resource plitzi://render/guide first — it has the element/prop table, the style model and a full worked example, and following it is the difference between a widget that renders and repeated failed calls.\nReturns a compact summary (the widget is shown to the user); on failure it returns teachable errors (path + hint) — read them and retry.",
108
+ description: "Show the user a real, rendered UI widget instead of describing one — cards, hero sections, pricing tables, forms, menus, checklists, profiles, galleries. It runs the Plitzi SDK fully offline: no backend, account, or setup. Reach for it whenever a visual layout beats prose: the user asks you to design/build/show something, OR your answer is naturally visual (a recipe → a card, a comparison → a table, steps → a checklist). Prefer showing over telling.\n\nAuthor the widget as an ordered list of `operations` that build an element tree under the pre-seeded root page \"render\". Three rules:\n1. STRUCTURE — one upsertElement builds the whole tree: set pageRef:\"render\" and give element a nested `children` array. Each element is { ref (unique), type, subType?, props?, style?, children? }; children render in order. (To attach to something you already made, use a top-level parentRef:\"<existing ref>\" instead.)\n2. STYLE — declare reusable classes with upsertDefinition { ref, desktop:{ …CSS props in kebab-case… } }, then attach via the element style:{ base:[\"<class ref>\"] }. Lay containers out with flex/grid.\n2b. LAYOUT — it renders in a side panel, so width is free and HEIGHT is scarce. Plain containers stack children vertically, which is the tall half-empty default to avoid: put peers (metrics, plans, options, image + text) in a wrapping row — display:flex, flex-direction:row, flex-wrap:wrap, children flex-grow:\"1\" + flex-basis:\"0%\" + min-width — or a grid with grid-template-columns:\"repeat(auto-fit, minmax(160px, 1fr))\". Keep padding 12-16px and gap 8-12px, and let the outer container fill the panel. Stack only what reads in order (heading over paragraph, forms, steps, prose). Watch the SDK defaults: every container has min-width/min-height 50px (set them to \"0\" for rails, dividers, dots and any flex child that must shrink), and heading/paragraph keep the browser's own margins (zero them and space with the parent's gap).\n2c. THEME — it is embedded in the host UI, which MAY BE DARK, so never hardcode a light palette. Take colours from the host variables with a light-dark() fallback — background-color:\"var(--color-background-secondary, light-dark(#ffffff, #1f2430))\", color:\"var(--color-text-primary, light-dark(#0f172a, #e8eaed))\", border-color:\"var(--color-border-primary, light-dark(#e2e8f0, #333a48))\" — and always set `color` wherever you set `background-color` (a brand accent states its own text colour too).\n3. CONTENT — visible copy goes in props.content (text, heading, paragraph, button); heading level is the element subType (\"h1\"..\"h6\"); image/video take props.src. An unknown prop comes back as a warning naming the right one.\n\nCommon types: container, heading, paragraph, text, button, link, image, video, list, listItem, markdown (plitzi://render/types lists every built-in type with descriptions). Widgets can also be data-driven and interactive — an apiContainer fetches at runtime, upsertBinding wires data into elements, and upsertInteractionFlow makes them react to clicks (see the guide).\nREAD the resource plitzi://render/guide first — it has the element/prop table, the style model and a full worked example, and following it is the difference between a widget that renders and repeated failed calls.\nReturns a compact summary (the widget is shown to the user); on failure it returns teachable errors (path + hint) — read them and retry.",
109
109
  inputShape: renderShape,
110
110
  access: "read",
111
111
  spaceless: true,
@@ -83,10 +83,7 @@ var collectTouched = (ops) => {
83
83
  touched.globalStyles.add(op.componentType);
84
84
  break;
85
85
  case "upsertIdStyle":
86
- case "patchIdStyle":
87
- touched.idStyles.add(op.targetId);
88
- break;
89
- default: break;
86
+ case "patchIdStyle": touched.idStyles.add(op.targetId);
90
87
  }
91
88
  return touched;
92
89
  };