webcake-storefront-mcp 1.16.0 → 1.17.0

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.
@@ -36,7 +36,26 @@ children with a grid:
36
36
  - each child config also has \`rowStart/rowEnd\` (1-based grid lines),
37
37
  \`constraintX\` (['left'|'right'|'centerLeft']), \`constraintY\` (['top'|'bottom'|'centerTop']).
38
38
  new_section does ALL of this for you: pass children and they are stacked one row each in
39
- the centre column. To build multi-column layouts, nest a container child with its own grid.
39
+ the centre column.
40
+
41
+ ## Multi-column rows (cards side by side) — USE THIS, real pages are full of them
42
+ A plain vertical stack looks like a blog post, not a designed page. Feature cards,
43
+ category tiles, footer columns, a text+image hero — all are HORIZONTAL rows. Two ways:
44
+ - new_row(children=[colA, colB, colC]) → a container whose children sit SIDE BY SIDE in
45
+ equal columns. Place the returned node as a child of a section.
46
+ - Inside new_section, add \`layout:"row"\` to a CONTAINER child spec and its children become
47
+ columns: \`{ type:"container", layout:"row", children:[ {..}, {..}, {..} ] }\`.
48
+ Rows are RESPONSIVE automatically: finalizeForRender collapses them on small screens
49
+ (default tablet=2 columns, mobile=1) so cards never shrink to slivers — override with
50
+ \`collapse:{bp3:2,bp4:1}\`. Uneven columns via \`colWidths\` (e.g. a 2:1 text/image split:
51
+ \`colWidths:[{unit:'fr',value:2},{unit:'fr',value:1}]\`), spacing via \`columnGap\`/\`rowGap\`.
52
+ Each column is usually a \`container\` holding its own stacked children (image + title +
53
+ text). Example feature row:
54
+ \`new_row(children=[
55
+ { type:"container", children:[ {type:"image",opts:{...}}, {type:"text",opts:{text:"Giao nhanh"}} ] },
56
+ { type:"container", children:[ {type:"image",opts:{...}}, {type:"text",opts:{text:"Chất lượng"}} ] },
57
+ { type:"container", children:[ {type:"image",opts:{...}}, {type:"text",opts:{text:"Bảo hành"}} ] },
58
+ ])\`
40
59
 
41
60
  ## Where layout/style live: per-breakpoint keys (NOT \`runtime\`)
42
61
  new_section / new_element emit a temporary \`runtime: { style, config }\`. That is a
@@ -84,6 +84,68 @@ export function stackChildren(container, children, opts = {}) {
84
84
  container.children = children;
85
85
  return container;
86
86
  }
87
+ const ROW_PLACEHOLDER_ROW = () => ({ unit: "min/max", min: { unit: "px", absValue: 50 }, max: { unit: "max-c" } });
88
+ /** Default responsive collapse for a multi-column row: full columns on desktop/laptop,
89
+ * 2 columns on tablet, 1 column on mobile. `null` = keep the full column count. */
90
+ const ROW_COLLAPSE_DEFAULT = { bp1: null, bp2: null, bp3: 2, bp4: 1 };
91
+ /**
92
+ * Lay children out HORIZONTALLY — side by side in one grid row — the way real BuilderX
93
+ * pages build feature cards, category tiles, footer columns, 2-col hero, etc. The grid is
94
+ * `Nx1` with N equal `fr` columns and each child placed in its own column. Values go to
95
+ * `runtime` plus `__row`/`__cell` markers that finalizeForRender() reads to emit a
96
+ * RESPONSIVE grid per breakpoint (it auto-collapses to fewer columns on tablet/mobile so
97
+ * the row never renders as cramped slivers).
98
+ */
99
+ export function rowChildren(container, children, opts = {}) {
100
+ const cols = children.length || 1;
101
+ const colWidths = opts.colWidths && opts.colWidths.length === cols
102
+ ? opts.colWidths
103
+ : Array.from({ length: cols }, () => ({ unit: "fr", value: 1 }));
104
+ const collapse = { ...ROW_COLLAPSE_DEFAULT, ...(opts.collapse || {}) };
105
+ const columnGap = opts.columnGap ?? 24;
106
+ const rowGap = opts.rowGap ?? 24;
107
+ const meta = { cols, count: children.length, collapse, columnGap, rowGap };
108
+ container.runtime = container.runtime || {};
109
+ container.runtime.config = {
110
+ ...(container.runtime.config || {}),
111
+ grid: `${cols}x1`,
112
+ columns: colWidths,
113
+ rows: [ROW_PLACEHOLDER_ROW()],
114
+ columnGap,
115
+ rowGap,
116
+ heightUnit: "auto",
117
+ __row: meta,
118
+ };
119
+ children.forEach((child, i) => {
120
+ child.runtime = child.runtime || {};
121
+ child.runtime.config = {
122
+ ...(child.runtime.config || {}),
123
+ columnStart: i + 1,
124
+ columnEnd: i + 2,
125
+ rowStart: 1,
126
+ rowEnd: 2,
127
+ constraintX: (child.runtime.config && child.runtime.config.constraintX) || ["centerLeft"],
128
+ constraintY: (child.runtime.config && child.runtime.config.constraintY) || ["top"],
129
+ loaded: true,
130
+ __cell: { index: i, ...meta },
131
+ };
132
+ });
133
+ container.children = children;
134
+ return container;
135
+ }
136
+ /** Build a standalone multi-column ROW container from child specs (each laid side by side).
137
+ * Used by the new_row tool; inside new_section pass `layout:"row"` on a container spec. */
138
+ export function buildRow(childSpecs = [], opts = {}) {
139
+ const container = buildElement("container", opts.containerOpts || {});
140
+ const children = childSpecs.map((spec) => buildFromSpec(spec));
141
+ return rowChildren(container, children, opts);
142
+ }
143
+ /** How many columns a row shows at a given breakpoint (clamped to its real column count). */
144
+ function colsForBp(meta, bp) {
145
+ const c = meta.collapse ? meta.collapse[bp] : null;
146
+ const k = c == null ? meta.cols : Math.min(c, meta.cols);
147
+ return Math.max(1, k);
148
+ }
87
149
  /**
88
150
  * Build a ready-to-place section from a list of child specs.
89
151
  * Each spec: { type, opts?, children? } where children is a nested array of specs.
@@ -118,7 +180,19 @@ function buildFromSpec(spec) {
118
180
  const node = buildElement(spec.type, spec.opts || {});
119
181
  if (Array.isArray(spec.children) && spec.children.length) {
120
182
  const kids = spec.children.map((c) => buildFromSpec(c));
121
- stackChildren(node, kids);
183
+ // `layout:"row"` lays the children out side by side (responsive); default is a
184
+ // top-to-bottom vertical stack. rowGap/columnGap/colWidths/collapse tune the layout.
185
+ if (spec.layout === "row") {
186
+ rowChildren(node, kids, {
187
+ columnGap: spec.columnGap,
188
+ rowGap: spec.rowGap,
189
+ colWidths: spec.colWidths,
190
+ collapse: spec.collapse,
191
+ });
192
+ }
193
+ else {
194
+ stackChildren(node, kids, { rowGap: spec.rowGap });
195
+ }
122
196
  }
123
197
  return node;
124
198
  }
@@ -193,9 +267,14 @@ function expandNodeToBreakpoints(node) {
193
267
  const baseStyle = rt.style || {};
194
268
  const baseConfig = { ...(rt.config || {}), loaded: true };
195
269
  const isSection = node.type === "section";
270
+ const rowMeta = baseConfig.__row; // this node is a multi-column row container
271
+ const cellMeta = baseConfig.__cell; // this node is a cell inside a row
196
272
  for (const [bp, [minW]] of Object.entries(BREAKPOINTS)) {
197
273
  const style = clone(baseStyle);
198
274
  const config = clone(baseConfig);
275
+ // Internal build-time markers never get persisted.
276
+ delete config.__row;
277
+ delete config.__cell;
199
278
  if (isSection) {
200
279
  const g = genGridByBp(minW);
201
280
  const sectionRows = baseConfig.rows && baseConfig.rows.length ? clone(baseConfig.rows) : clone(g.rows);
@@ -204,6 +283,29 @@ function expandNodeToBreakpoints(node) {
204
283
  config.grid = `3x${sectionRows.length}`;
205
284
  config.heightUnit = config.heightUnit || "auto";
206
285
  }
286
+ // A multi-column row collapses to fewer columns on smaller screens so cards never
287
+ // shrink to slivers: recompute the grid + this container's columns per breakpoint.
288
+ if (rowMeta) {
289
+ const k = colsForBp(rowMeta, bp);
290
+ const nrows = Math.ceil(rowMeta.count / k);
291
+ config.columns =
292
+ k === rowMeta.cols && Array.isArray(baseConfig.columns) && baseConfig.columns.length === k
293
+ ? clone(baseConfig.columns)
294
+ : Array.from({ length: k }, () => ({ unit: "fr", value: 1 }));
295
+ config.rows = Array.from({ length: nrows }, () => ROW_PLACEHOLDER_ROW());
296
+ config.grid = `${k}x${nrows}`;
297
+ config.heightUnit = config.heightUnit || "auto";
298
+ }
299
+ // A cell repositions itself into the collapsed grid (wraps to a new row as needed).
300
+ if (cellMeta) {
301
+ const k = colsForBp(cellMeta, bp);
302
+ const c = cellMeta.index % k;
303
+ const r = Math.floor(cellMeta.index / k);
304
+ config.columnStart = c + 1;
305
+ config.columnEnd = c + 2;
306
+ config.rowStart = r + 1;
307
+ config.rowEnd = r + 2;
308
+ }
207
309
  node[bp] = { style, config };
208
310
  }
209
311
  delete node.runtime;
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "v": "1.17.0",
4
+ "d": "24/06/2026",
5
+ "type": "Added",
6
+ "en": "New new_row tool builds a multi-column responsive row container where child elements are placed side by side (left to right); the row auto-collapses…",
7
+ "vi": "Tool mới new_row tạo một container đa cột responsive với các phần tử con được đặt nằm ngang cạnh nhau (từ trái sang phải); hàng tự động thu gọn…"
8
+ },
2
9
  {
3
10
  "v": "1.16.0",
4
11
  "d": "24/06/2026",
@@ -33,12 +40,5 @@
33
40
  "type": "Added",
34
41
  "en": "New restore_file_version tool rolls a CMS file back to a saved version in one step: it reads the chosen version's content (via get_file_versions)…",
35
42
  "vi": "Tool mới restore_file_version khôi phục CMS file về một phiên bản đã lưu chỉ trong một bước: đọc nội dung phiên bản được chọn (qua…"
36
- },
37
- {
38
- "v": "1.12.1",
39
- "d": "24/06/2026",
40
- "type": "Fixed",
41
- "en": "save_file_version and get_file_versions both sent the parameter as cms_file_id, but the backend reads file_id; versions were saved unlinked to any…",
42
- "vi": "save_file_version và get_file_versions đều gửi tham số dưới key cms_file_id, trong khi backend đọc file_id; các phiên bản được lưu mà không liên kết…"
43
43
  }
44
44
  ]
@@ -3,12 +3,17 @@ import { BUILD_GUIDE } from "../builder/guide.js";
3
3
  import { listElements, getElement, buildElement } from "../builder/catalog.js";
4
4
  import { describeEventsCatalog } from "../builder/events.js";
5
5
  import { describeBindingsCatalog } from "../builder/bindings.js";
6
- import { buildSection, newPageSkeleton, validatePage, finalizeForRender, reassignIds, } from "../builder/page.js";
6
+ import { buildSection, buildRow, newPageSkeleton, validatePage, finalizeForRender, reassignIds, } from "../builder/page.js";
7
7
  // Recursive spec for new_section / build_page children.
8
8
  const elementSpec = z.object({
9
9
  type: z.string().describe("Element type (see list_elements)"),
10
10
  opts: z.record(z.any()).optional().describe("Factory opts: { style, config, specials, text, src, width, height, ... }"),
11
11
  children: z.array(z.any()).optional().describe("Nested child specs (same shape) for container types"),
12
+ layout: z.enum(["row", "column"]).optional().describe("How this node's children are laid out: 'row' = side by side (responsive columns, auto-collapses on tablet/mobile), 'column' = stacked top-to-bottom (default). Use 'row' on a container to make feature cards / category tiles / footer columns / 2-col hero."),
13
+ rowGap: z.number().optional().describe("Vertical gap (px) between stacked children / wrapped rows"),
14
+ columnGap: z.number().optional().describe("Horizontal gap (px) between columns (layout:'row' only)"),
15
+ colWidths: z.array(z.any()).optional().describe("layout:'row' only — explicit per-column unit objects (length = #children), e.g. [{unit:'fr',value:2},{unit:'fr',value:1}] for a 2:1 split. Default: equal columns."),
16
+ collapse: z.record(z.number()).optional().describe("layout:'row' only — columns to show per breakpoint, e.g. {bp3:2,bp4:1}. Default: tablet 2, mobile 1."),
12
17
  });
13
18
  function parseSource(src) {
14
19
  if (src == null)
@@ -48,6 +53,24 @@ Example children: [{ "type":"text", "opts":{"text":"Welcome","style":{"fontSize"
48
53
  children: z.array(elementSpec).default([]).describe("Child element specs, stacked vertically in the section"),
49
54
  section_opts: z.record(z.any()).optional().describe("Optional factory opts for the section node itself"),
50
55
  }, ({ children, section_opts }) => handle(async () => buildSection(children || [], section_opts || {})));
56
+ server.tool("new_row", `Build a multi-column ROW container: children laid out SIDE BY SIDE (not stacked).
57
+ This is how real pages build feature cards, category tiles, footer columns, a 2-col hero, etc.
58
+ The row is RESPONSIVE — it auto-collapses to fewer columns on tablet/mobile (default tablet 2, mobile 1) so cards never become cramped slivers.
59
+ Place the returned node as a child inside a section (section children still stack vertically; nest a row for horizontal layout).
60
+ Example children: [{ "type":"container", "children":[{"type":"image","opts":{...}},{"type":"text","opts":{...}}] }, { ... }, { ... }]`, {
61
+ children: z.array(elementSpec).default([]).describe("Child specs, one per column (laid out left-to-right)"),
62
+ column_gap: z.number().optional().describe("Horizontal gap (px) between columns (default 24)"),
63
+ row_gap: z.number().optional().describe("Vertical gap (px) between wrapped rows (default 24)"),
64
+ col_widths: z.array(z.any()).optional().describe("Explicit per-column unit objects (length = #children), e.g. [{unit:'fr',value:2},{unit:'fr',value:1}]. Default: equal columns."),
65
+ collapse: z.record(z.number()).optional().describe("Columns to show per breakpoint, e.g. {bp3:2,bp4:1}. Default: tablet 2, mobile 1."),
66
+ container_opts: z.record(z.any()).optional().describe("Optional factory opts for the row container node itself (e.g. { style:{...} })"),
67
+ }, ({ children, column_gap, row_gap, col_widths, collapse, container_opts }) => handle(async () => buildRow(children || [], {
68
+ columnGap: column_gap,
69
+ rowGap: row_gap,
70
+ colWidths: col_widths,
71
+ collapse,
72
+ containerOpts: container_opts || {},
73
+ })));
51
74
  server.tool("new_page_skeleton", "Return an empty but valid page source: { sections: [] }. Add sections built with new_section, then save with build_page.", {}, () => handle(async () => newPageSkeleton()));
52
75
  server.tool("validate_page", "Validate a page source ({ sections: [...] }). Returns errors (block saving: duplicate/missing ids, missing types) and warnings (unknown types, form fields without field_name, dangling event targets) plus stats. Always run this before build_page.", {
53
76
  source: z.any().describe("Page source object or JSON string"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-storefront-mcp",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "MCP server for the WebCake/StoreCake storefront builder — page CRUD, page authoring, products, orders, and more",
5
5
  "mcpName": "io.github.vuluu2k/webcake-storefront-mcp",
6
6
  "license": "MIT",