sbuilder-mcp 0.32.1 → 0.32.2

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 CHANGED
@@ -6,6 +6,12 @@ All notable changes to this project are documented in this file.
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.32.2] - 2026-09-11
10
+
11
+ ### Fixed
12
+ - A centred section built by sb_import, sb_import_site, or the built-in layout patterns applied by sb_template_use no longer renders half left-aligned: the theme's heading preset overrides inherited text alignment, so headings and paragraphs now carry `textAlign` directly on the node when the source (or pattern) centers or right-aligns them.
13
+ - The gallery-wall layout pattern applied by sb_template_use now frames every photo to a shared aspect ratio measured as the median of the pictures actually picked, instead of leaving each tile its own shape, so a wall of portraits or landscapes no longer renders with mismatched row heights.
14
+
9
15
  ## [0.32.1] - 2026-09-11
10
16
 
11
17
  ### Fixed
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,12 @@ Mọi thay đổi đáng chú ý của dự án được ghi lại trong file n
6
6
  Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.32.2] - 2026-09-11
10
+
11
+ ### Fixed
12
+ - Một section được căn giữa do sb_import, sb_import_site, hoặc các layout pattern có sẵn được sb_template_use áp dụng giờ không còn hiển thị nửa căn trái nữa: preset heading của theme ghi đè cách căn chữ được kế thừa, nên heading và đoạn văn giờ mang `textAlign` trực tiếp trên node khi trang nguồn (hoặc pattern) căn giữa hay căn phải chúng.
13
+ - Layout pattern dạng tường ảnh do sb_template_use áp dụng giờ đóng khung mọi ảnh theo một tỉ lệ khung hình chung, tính bằng trung vị của các ảnh thực sự được chọn, thay vì để mỗi ô giữ nguyên hình dạng riêng, nên một tường ảnh chân dung hay ảnh ngang không còn hiển thị với chiều cao các hàng lệch nhau.
14
+
9
15
  ## [0.32.1] - 2026-09-11
10
16
 
11
17
  ### Fixed
@@ -155,6 +155,10 @@ function one(c, t) {
155
155
  style: {
156
156
  margin: '0',
157
157
  ...scale.style,
158
+ // ON THE NODE, never left to inheritance: the theme's heading preset
159
+ // declares its own textAlign, and a class rule beats an inherited
160
+ // value.
161
+ ...(c.textAlign ? { textAlign: c.textAlign } : {}),
158
162
  ...(t.headingColor ? { color: t.headingColor } : {}),
159
163
  ...(t.headingWeight ? { fontWeight: t.headingWeight } : {}),
160
164
  },
@@ -169,6 +173,7 @@ function one(c, t) {
169
173
  specials: { htmlTag: 'p', text },
170
174
  style: {
171
175
  lineHeight: '1.7',
176
+ ...(c.textAlign ? { textAlign: c.textAlign } : {}),
172
177
  ...(t.textColor ? { color: t.textColor } : {}),
173
178
  ...(t.textSize ? { fontSize: t.textSize } : {}),
174
179
  },
@@ -288,6 +293,27 @@ function one(c, t) {
288
293
  // a width/height ATTRIBUTE is a used height, and without this the cap below
289
294
  // would be the thing ignored. `contain` because the source's crop is not
290
295
  // ours to guess.
296
+ // A FRAME, WHEN THE CALLER MEASURED ONE. `height: auto` is what makes
297
+ // `aspect-ratio` work here at all — the platform writes the intrinsic
298
+ // width/height ATTRIBUTES onto every <img>, and a presentational height is
299
+ // a USED height, so the ratio would otherwise be ignored. That is the same
300
+ // fix the platform applied to its own media CSS.
301
+ //
302
+ // `cover` only inside a frame: filling one means cropping, which is the
303
+ // trade a wall of tiles makes on purpose. With no frame the answer stays
304
+ // `contain` — the source's crop is not ours to guess.
305
+ if (c.ratio) {
306
+ return {
307
+ type: 'image',
308
+ specials: { src: c.src, alt: c.alt ?? '' },
309
+ style: {
310
+ width: '100%',
311
+ height: 'auto',
312
+ aspectRatio: c.ratio,
313
+ objectFit: 'cover',
314
+ },
315
+ };
316
+ }
291
317
  return {
292
318
  type: 'image',
293
319
  specials: { src: c.src, alt: c.alt ?? '' },
@@ -83,8 +83,17 @@ const row = (children, wrap = false, align) => ({
83
83
  ...(align ? { align } : {}),
84
84
  children,
85
85
  });
86
- const h = (text, level = 2) => ({ kind: 'heading', level, text });
87
- const p = (text) => ({ kind: 'text', text });
86
+ const h = (text, level = 2, textAlign) => ({
87
+ kind: 'heading',
88
+ level,
89
+ text,
90
+ ...(textAlign ? { textAlign } : {}),
91
+ });
92
+ const p = (text, textAlign) => ({
93
+ kind: 'text',
94
+ text,
95
+ ...(textAlign ? { textAlign } : {}),
96
+ });
88
97
  const cta = (text, href = '#') => ({ kind: 'button', variant: 'cta', text, href });
89
98
  /**
90
99
  * The picture slot: a real image when the site has one, and WORDS when it does
@@ -132,7 +141,10 @@ export const LAYOUT_PATTERNS = [
132
141
  id: 'sb_hero_centered',
133
142
  name: 'Hero — canh giữa',
134
143
  use: 'Mở đầu trang khi chưa có ảnh: tiêu đề lớn, một câu, một nút',
135
- build: (t) => section([h('Tiêu đề chính', 1), p('Một câu nói rõ bạn bán gì và cho ai.'), cta('Mua ngay')], t, { alignItems: 'center', textAlign: 'center' }, { alignItems: 'center' }),
144
+ build: (t) => section(
145
+ // Centred on the NODES, not left to the section's inherited textAlign —
146
+ // the theme's heading preset declares its own and wins.
147
+ [h('Tiêu đề chính', 1, 'center'), p('Một câu nói rõ bạn bán gì và cho ai.', 'center'), cta('Mua ngay')], t, { alignItems: 'center', textAlign: 'center' }, { alignItems: 'center' }),
136
148
  },
137
149
  {
138
150
  id: 'sb_feature_trio',
@@ -153,9 +165,9 @@ export const LAYOUT_PATTERNS = [
153
165
  use: 'Ba đến bốn con số lớn với nhãn bên dưới — bằng chứng, không phải lời hứa',
154
166
  build: (t) => section([
155
167
  row([
156
- { kind: 'group', direction: 'column', children: [h('1.000+', 2), p('Khách hàng')] },
157
- { kind: 'group', direction: 'column', children: [h('4,9/5', 2), p('Đánh giá trung bình')] },
158
- { kind: 'group', direction: 'column', children: [h('24h', 2), p('Giao trong nội thành')] },
168
+ { kind: 'group', direction: 'column', children: [h('1.000+', 2, 'center'), p('Khách hàng', 'center')] },
169
+ { kind: 'group', direction: 'column', children: [h('4,9/5', 2, 'center'), p('Đánh giá trung bình', 'center')] },
170
+ { kind: 'group', direction: 'column', children: [h('24h', 2, 'center'), p('Giao trong nội thành', 'center')] },
159
171
  ]),
160
172
  ], t, { alignItems: 'center', textAlign: 'center' }, { alignItems: 'center' }),
161
173
  },
@@ -163,7 +175,11 @@ export const LAYOUT_PATTERNS = [
163
175
  id: 'sb_cta_band',
164
176
  name: 'Dải kêu gọi hành động',
165
177
  use: 'Một câu và một nút, đặt cuối trang hoặc giữa hai band nội dung',
166
- build: (t) => section([h('Sẵn sàng bắt đầu?'), p('Một câu nhắc lại lời hứa chính.'), cta('Mua ngay')], t, { alignItems: 'center', textAlign: 'center' }, { alignItems: 'center' }),
178
+ build: (t) => section(
179
+ // CENTRED ON THE NODES. The section's own `textAlign` is inherited, and
180
+ // the theme's heading preset declares its own — so the band came out
181
+ // with its words hard left and only the button in the middle.
182
+ [h('Sẵn sàng bắt đầu?', 2, 'center'), p('Một câu nhắc lại lời hứa chính.', 'center'), cta('Mua ngay')], t, { alignItems: 'center', textAlign: 'center' }, { alignItems: 'center' }),
167
183
  },
168
184
  {
169
185
  id: 'sb_gallery',
@@ -176,11 +192,32 @@ export const LAYOUT_PATTERNS = [
176
192
  // SIX IS A WALL, THREE IS A ROW. Bounded because a gallery is a design
177
193
  // decision and not a dump of the library — a merchant with 164 assets does
178
194
  // not want 164 of them in one band.
195
+ const picked = [];
179
196
  for (let i = 0; i < 6; i += 1) {
180
197
  const m = pick(pool, used, 'any');
181
198
  if (!m)
182
199
  break;
183
- shots.push({ kind: 'image', src: m.url, alt: m.name ?? 'Ảnh' });
200
+ picked.push(m);
201
+ }
202
+ // ONE FRAME FOR THE WALL, MEASURED RATHER THAN INVENTED.
203
+ //
204
+ // Rule 6 says match a frame's ratio to the ASSET, and a wall of
205
+ // photographs is the case it does not cover: there is no single asset.
206
+ // Left alone, every tile keeps its own shape and the grid's rows come out
207
+ // different heights — measured on a real build, four tiles at 330x220 and
208
+ // a fifth noticeably taller, which reads as unfinished.
209
+ //
210
+ // The MEDIAN of the pictures actually being shown is the honest frame:
211
+ // most crop by nothing, the outliers crop least, and a library of
212
+ // portraits gets a portrait wall rather than a landscape one imposed on
213
+ // it. A picture whose size the library did not report votes for nothing.
214
+ const ratios = picked
215
+ .map((m) => (m.width && m.height ? m.width / m.height : 0))
216
+ .filter((r) => r > 0)
217
+ .sort((a, b) => a - b);
218
+ const frame = ratios.length ? ratios[Math.floor(ratios.length / 2)].toFixed(2) : undefined;
219
+ for (const m of picked) {
220
+ shots.push({ kind: 'image', src: m.url, alt: m.name ?? 'Ảnh', ...(frame ? { ratio: frame } : {}) });
184
221
  }
185
222
  if (shots.length === 0) {
186
223
  return section([
@@ -274,6 +274,23 @@ function capturePage(limits) {
274
274
  * loader is displaying until the real one arrives, which is precisely the
275
275
  * placeholder this import must not ship.
276
276
  */
277
+ /**
278
+ * The SOURCE'S OWN text alignment, and only when it is not the default.
279
+ *
280
+ * A hero that centres its copy is making a decision; a copy that left-aligns
281
+ * it is not the same band. `start`/`left` is the browser's answer and saying
282
+ * it out loud would put a value on every imported paragraph for nothing —
283
+ * and worse, would override the target page's own centred preset with a
284
+ * literal.
285
+ */
286
+ const alignOf = (el) => {
287
+ const a = getComputedStyle(el).textAlign;
288
+ if (a === 'center')
289
+ return { textAlign: 'center' };
290
+ if (a === 'right' || a === 'end')
291
+ return { textAlign: 'right' };
292
+ return {};
293
+ };
277
294
  const LAZY_SRC = ['data-src', 'data-original', 'data-lazy-src', 'data-lazy', 'data-echo', 'data-url'];
278
295
  const widest = (srcset) => {
279
296
  let best = '';
@@ -657,7 +674,7 @@ function capturePage(limits) {
657
674
  if (!text)
658
675
  return [];
659
676
  taken.nodes++;
660
- return [{ kind: 'heading', level: Number(tag.slice(1)), text }];
677
+ return [{ kind: 'heading', level: Number(tag.slice(1)), text, ...alignOf(el) }];
661
678
  }
662
679
  if (tag === 'IMG') {
663
680
  const src = realSrc(el);
@@ -801,7 +818,7 @@ function capturePage(limits) {
801
818
  if (!text)
802
819
  return [];
803
820
  taken.nodes++;
804
- return [{ kind: 'text', text }];
821
+ return [{ kind: 'text', text, ...alignOf(el) }];
805
822
  }
806
823
  const kids = walkChildren(el);
807
824
  if (kids.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.32.1",
3
+ "version": "0.32.2",
4
4
  "description": "MCP server that designs and operates a Store Builder site — pages, data, theme and publish — through the platform's own API and live-edit protocol.",
5
5
  "mcpName": "io.github.vuluu2k/sbuilder-mcp",
6
6
  "type": "module",