sbuilder-mcp 0.32.0 → 0.32.1
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 +9 -0
- package/CHANGELOG.vi.md +9 -0
- package/dist/catalog/elements.generated.js +13 -0
- package/dist/domains/site/ids.js +28 -1
- package/dist/domains/site/importmap.js +80 -2
- package/dist/domains/site/patterns.js +20 -2
- package/dist/vision/capture.js +11 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,15 @@ 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.1] - 2026-09-11
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- sb_import, sb_import_site, and the built-in layout patterns applied by sb_template_use now cap every section's width at 1200px instead of leaving it unbounded, so an imported or generated band no longer sets prose on a 1392px line.
|
|
13
|
+
- A wrapping row of two or more columns (a photo gallery, a card shelf) is now built as a CSS grid instead of a flex row, so every cell comes out the same size instead of the last, shorter line stretching to fill the row.
|
|
14
|
+
- Headings built by sb_import, sb_import_site, and the built-in layout patterns now wear the site's own theme text style (heading-1 through heading-6, by reference) instead of the flat 48px default, so an h2 and an h3 no longer render at an identical size.
|
|
15
|
+
- A row of two unequal-weight columns (a hero's text beside its photograph) now keeps the source page's own cross-axis alignment, such as centering, instead of always aligning to the top and leaving dead space under the shorter column.
|
|
16
|
+
- Node ids generated during a build (sb_add, sb_import, sb_import_site) can no longer collide: each process now remembers every id it has issued and redraws on a repeat instead of risking one node silently overwriting another.
|
|
17
|
+
|
|
9
18
|
## [0.32.0] - 2026-09-11
|
|
10
19
|
|
|
11
20
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,15 @@ 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.1] - 2026-09-11
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- sb_import, sb_import_site, và các layout pattern có sẵn được sb_template_use áp dụng giờ giới hạn chiều rộng mỗi section ở 1200px thay vì để không giới hạn, nên một band được nhập hoặc dựng sẵn không còn đặt đoạn văn trên một dòng dài 1392px.
|
|
13
|
+
- Một hàng bị wrap gồm hai cột trở lên (một dải ảnh, một kệ thẻ sản phẩm) giờ được dựng bằng CSS grid thay vì hàng flex, nên mọi ô đều có cùng kích thước thay vì dòng cuối ngắn hơn bị kéo giãn để lấp đầy hàng.
|
|
14
|
+
- Các heading do sb_import, sb_import_site, và các layout pattern có sẵn dựng ra giờ mang text style của theme site (heading-1 đến heading-6, theo tham chiếu) thay vì cỡ 48px cố định, nên một h2 và một h3 không còn hiển thị cùng một cỡ chữ.
|
|
15
|
+
- Một hàng gồm hai cột có trọng số khác nhau (phần chữ của hero cạnh ảnh của nó) giờ giữ nguyên cách căn theo trục ngang của trang nguồn, ví dụ căn giữa, thay vì luôn căn lên trên và để lại khoảng trống bên dưới cột ngắn hơn.
|
|
16
|
+
- Id của node được tạo trong lúc dựng trang (sb_add, sb_import, sb_import_site) giờ không thể trùng nhau nữa: mỗi tiến trình ghi nhớ mọi id đã cấp và tạo lại nếu bị trùng thay vì để một node âm thầm ghi đè lên node khác.
|
|
17
|
+
|
|
9
18
|
## [0.32.0] - 2026-09-11
|
|
10
19
|
|
|
11
20
|
### Added
|
|
@@ -36215,6 +36215,19 @@ export const BASE_ONLY_EXCEPTIONS = [
|
|
|
36215
36215
|
* It is also BASE-ONLY (see BASE_ONLY_CONFIG) — render/css.go emits it into the
|
|
36216
36216
|
* base lane because the config object is read with no responsive merge.
|
|
36217
36217
|
*/
|
|
36218
|
+
/**
|
|
36219
|
+
* The eight node-style keys a THEME TEXT STYLE controls, and the var prop each
|
|
36220
|
+
* compiles to: a node wears a style by setting these to
|
|
36221
|
+
* var(--wb-ts-<slug>-<prop>), never by writing the value.
|
|
36222
|
+
*
|
|
36223
|
+
* The theme ships heading-1 (48px) through heading-6, and text-1..3 — a real
|
|
36224
|
+
* type scale that every page built by these tools was ignoring, because the
|
|
36225
|
+
* mapper wrote only htmlTag and the heading-default preset pins 48px flat.
|
|
36226
|
+
*
|
|
36227
|
+
* BY REFERENCE, NEVER BY LITERAL. A literal outranks the preset beneath it
|
|
36228
|
+
* permanently and stops the node following the theme.
|
|
36229
|
+
*/
|
|
36230
|
+
export const TEXT_STYLE_KEYS = [["fontFamily", "family"], ["fontSize", "size"], ["fontWeight", "weight"], ["fontStyle", "style"], ["lineHeight", "line-height"], ["letterSpacing", "letter-spacing"], ["textTransform", "transform"], ["color", "color"]];
|
|
36218
36231
|
export const ANIMATION = {
|
|
36219
36232
|
"types": [
|
|
36220
36233
|
"fade_in",
|
package/dist/domains/site/ids.js
CHANGED
|
@@ -21,7 +21,34 @@ const PREFIXES = {
|
|
|
21
21
|
'tab-content': 'tc',
|
|
22
22
|
'tab-item': 'ti',
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Every id this process has issued, so it can never issue one twice.
|
|
26
|
+
*
|
|
27
|
+
* FOUR RANDOM BYTES IS 32 BITS, AND THAT IS THIN FOR THE JOB. The birthday
|
|
28
|
+
* bound puts a collision at roughly 1 in 34,000 across 500 draws — which
|
|
29
|
+
* sounds like never until you notice that one `sb_import_site` run mints
|
|
30
|
+
* thousands, and that the suite's own uniqueness test HIT IT: 499 unique out of
|
|
31
|
+
* 500, once, in an ordinary run.
|
|
32
|
+
*
|
|
33
|
+
* The consequence is not a warning. Two nodes sharing an id means one
|
|
34
|
+
* overwrites the other in `doc.nodes` — content silently gone, a parent's child
|
|
35
|
+
* list pointing at the survivor, and a document that validates, saves and
|
|
36
|
+
* publishes. It is the same family as every other entry in this repo's file:
|
|
37
|
+
* the failure has no error attached to it.
|
|
38
|
+
*
|
|
39
|
+
* The width stays FOUR BYTES because the platform's own ids are eight hex
|
|
40
|
+
* characters and a document this server builds should be indistinguishable
|
|
41
|
+
* from one a human built. Uniqueness comes from remembering instead — which is
|
|
42
|
+
* the scope that actually matters, since one process builds one document.
|
|
43
|
+
*/
|
|
44
|
+
const issued = new Set();
|
|
24
45
|
export function genId(type) {
|
|
25
46
|
const prefix = PREFIXES[type] ?? (type.replace(/[^a-z]/g, '').slice(0, 2) || 'nd');
|
|
26
|
-
|
|
47
|
+
for (;;) {
|
|
48
|
+
const id = `${prefix}_${randomBytes(4).toString('hex')}`;
|
|
49
|
+
if (issued.has(id))
|
|
50
|
+
continue;
|
|
51
|
+
issued.add(id);
|
|
52
|
+
return id;
|
|
53
|
+
}
|
|
27
54
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { walk } from '../../core/tree.js';
|
|
2
2
|
import { stickySeeds } from './sticky.js';
|
|
3
|
+
import { TEXT_STYLE_KEYS } from '../../catalog/elements.generated.js';
|
|
3
4
|
import { normalizeUrl } from './discover.js';
|
|
4
5
|
import { ICON_NAMES } from '../../catalog/icons.generated.js';
|
|
5
6
|
/** Style keys read off a node, ignoring anything unset. */
|
|
@@ -101,17 +102,59 @@ function iconFor(raw) {
|
|
|
101
102
|
* empty node is the difference between an imported page and an imported page
|
|
102
103
|
* plus twenty findings from `sb_review`.
|
|
103
104
|
*/
|
|
105
|
+
/**
|
|
106
|
+
* THE SITE'S OWN TYPE SCALE, worn by reference.
|
|
107
|
+
*
|
|
108
|
+
* The theme ships `heading-1` (48px) through `heading-6` (16px) and `text-1`..
|
|
109
|
+
* `text-3`, and every page these tools built ignored all of it: the mapper
|
|
110
|
+
* wrote `htmlTag` and nothing else, so a heading took the `heading-default`
|
|
111
|
+
* preset, which pins `fontSize: 48px` FLAT. MEASURED on a real build — a
|
|
112
|
+
* section title and the three item titles beneath it came out identical, on a
|
|
113
|
+
* page whose document correctly said h2 and h3. "About four type sizes" is the
|
|
114
|
+
* checklist item; the page had one.
|
|
115
|
+
*
|
|
116
|
+
* BY REFERENCE, NEVER BY LITERAL — `var(--wb-ts-<slug>-<prop>)` is what the
|
|
117
|
+
* editor's own picker stamps. A literal `36px` would outrank the preset beneath
|
|
118
|
+
* it permanently and stop the node following the theme, which is the detachment
|
|
119
|
+
* this repo already records for imported icons.
|
|
120
|
+
*
|
|
121
|
+
* SIZE AND LINE HEIGHT ONLY, of the eight keys a style controls. Colour and
|
|
122
|
+
* weight are already answered by the tokens read off the target page (rule 0),
|
|
123
|
+
* and overwriting those here would make an imported band stop matching the page
|
|
124
|
+
* it landed on — which is the one thing the token pass exists to prevent. The
|
|
125
|
+
* editor supports exactly this partial state: the pick is recorded so the
|
|
126
|
+
* picker shows it, and it can also show that the style has drifted.
|
|
127
|
+
*
|
|
128
|
+
* A theme with no such style leaves the var undefined, so every ref carries the
|
|
129
|
+
* element's own former answer as its CSS fallback: a site on a slimmer theme
|
|
130
|
+
* renders exactly as it did before this existed.
|
|
131
|
+
*/
|
|
132
|
+
function textScale(slug, fallback) {
|
|
133
|
+
const style = {};
|
|
134
|
+
for (const [key, prop] of TEXT_STYLE_KEYS) {
|
|
135
|
+
if (key !== 'fontSize' && key !== 'lineHeight')
|
|
136
|
+
continue;
|
|
137
|
+
const fb = fallback[key];
|
|
138
|
+
style[key] = `var(--wb-ts-${slug}-${prop}${fb ? `, ${fb}` : ''})`;
|
|
139
|
+
}
|
|
140
|
+
return { style, config: { textGlobalStyle: slug } };
|
|
141
|
+
}
|
|
104
142
|
function one(c, t) {
|
|
105
143
|
switch (c.kind) {
|
|
106
144
|
case 'heading': {
|
|
107
145
|
const text = c.text?.trim();
|
|
108
146
|
if (!text)
|
|
109
147
|
return null;
|
|
148
|
+
// The level is already decided and already correct in the document; it
|
|
149
|
+
// simply reached no CSS. heading-1..6 are the theme's own slugs.
|
|
150
|
+
const scale = textScale(`heading-${Math.min(6, Math.max(1, c.level ?? 2))}`, {});
|
|
110
151
|
return {
|
|
111
152
|
type: 'heading',
|
|
112
153
|
specials: { htmlTag: headingTag(c.level), text },
|
|
154
|
+
config: scale.config,
|
|
113
155
|
style: {
|
|
114
156
|
margin: '0',
|
|
157
|
+
...scale.style,
|
|
115
158
|
...(t.headingColor ? { color: t.headingColor } : {}),
|
|
116
159
|
...(t.headingWeight ? { fontWeight: t.headingWeight } : {}),
|
|
117
160
|
},
|
|
@@ -336,6 +379,41 @@ function one(c, t) {
|
|
|
336
379
|
}
|
|
337
380
|
if (kids.length === 1)
|
|
338
381
|
return kids[0];
|
|
382
|
+
// A WRAPPING ROW IS A GRID, because flex cannot wrap into EQUAL CELLS.
|
|
383
|
+
//
|
|
384
|
+
// `flex: 1 1 <basis>` lets every item absorb the free space on ITS OWN
|
|
385
|
+
// LINE, so a short last line is a disaster: MEASURED on a page built by
|
|
386
|
+
// these tools, a gallery of five photographs came out as four cells of
|
|
387
|
+
// 330x220 and a fifth of 1392x420 — the same picture, four times the size,
|
|
388
|
+
// under the others. Nothing reported it, because no box overflowed and no
|
|
389
|
+
// two boxes overlapped; `measure` cannot see a cell that is merely wrong.
|
|
390
|
+
//
|
|
391
|
+
// Dropping the grow factor instead (`0 1 280px`) fixes the blow-up and
|
|
392
|
+
// buys a ragged right edge on every full line. `repeat(auto-fill,
|
|
393
|
+
// minmax(280px, 1fr))` is the thing actually wanted and the platform
|
|
394
|
+
// renders it — verified against a live server before this was written:
|
|
395
|
+
// as many cells as fit, all equal, the last line cell-sized like the rest.
|
|
396
|
+
//
|
|
397
|
+
// Mobile gets one column by name. The flex path's `flexDirection: column`
|
|
398
|
+
// says nothing to a grid, and a mobile answer that silently does nothing
|
|
399
|
+
// is rule 3 failing with a value in the document to prove it tried.
|
|
400
|
+
if (c.wrap) {
|
|
401
|
+
return {
|
|
402
|
+
type: 'flex-block',
|
|
403
|
+
style: {
|
|
404
|
+
width: '100%',
|
|
405
|
+
display: 'grid',
|
|
406
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
|
|
407
|
+
gap: '24px',
|
|
408
|
+
},
|
|
409
|
+
responsive: { mobile: { style: { gridTemplateColumns: '1fr', gap: '16px' } } },
|
|
410
|
+
children: kids.map((k) => ({
|
|
411
|
+
type: 'flex-block',
|
|
412
|
+
style: { width: '100%', minWidth: '0', display: 'flex', flexDirection: 'column', gap: '12px' },
|
|
413
|
+
children: [k],
|
|
414
|
+
})),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
339
417
|
// A ROW OF TWO OR MORE COLUMNS NEEDS AN EXPLICIT STACK BREAKPOINT, and
|
|
340
418
|
// nothing catches it for you: the columns SHRINK to fit, so no box
|
|
341
419
|
// overflows and `measure` stays silent while a photo becomes a sliver and
|
|
@@ -352,8 +430,8 @@ function one(c, t) {
|
|
|
352
430
|
width: '100%',
|
|
353
431
|
display: 'flex',
|
|
354
432
|
flexDirection: 'row',
|
|
355
|
-
flexWrap:
|
|
356
|
-
alignItems: 'flex-start',
|
|
433
|
+
flexWrap: 'nowrap',
|
|
434
|
+
alignItems: c.align && c.align !== 'start' ? c.align : 'flex-start',
|
|
357
435
|
gap: '24px',
|
|
358
436
|
},
|
|
359
437
|
responsive: { mobile: { style: { flexDirection: 'column', gap: '16px' } } },
|
|
@@ -43,6 +43,19 @@ export const THEME_TOKENS = {
|
|
|
43
43
|
textColor: 'var(--wb-color-text)',
|
|
44
44
|
buttonBg: 'var(--wb-color-primary)',
|
|
45
45
|
buttonColor: '#ffffff',
|
|
46
|
+
// A PAGE WITH NO MEASURE IS NOT AN UNSTYLED PAGE, IT IS A WRONGLY STYLED ONE.
|
|
47
|
+
//
|
|
48
|
+
// `sectionMaxWidth` is read off the TARGET page so an imported band matches
|
|
49
|
+
// what is already there — and a blank page offers nothing to read, so it came
|
|
50
|
+
// out unbounded. MEASURED at 1440 on a page built entirely by these tools:
|
|
51
|
+
// every block 1392px wide, every heading and paragraph set on a 1392px line.
|
|
52
|
+
// That is roughly 200 characters where prose is readable at 60-75, and it is
|
|
53
|
+
// the single loudest way a generated page announces itself.
|
|
54
|
+
//
|
|
55
|
+
// Unbounded was a decision too, and the worse one. This is the same class of
|
|
56
|
+
// answer as the `64px 24px` padding and the `16px` gap the mapper already
|
|
57
|
+
// commits to for a page that cannot answer for itself.
|
|
58
|
+
sectionMaxWidth: '1200px',
|
|
46
59
|
};
|
|
47
60
|
/** One section, through the same mapper an import goes through. */
|
|
48
61
|
function section(children, t, style, innerStyle) {
|
|
@@ -63,10 +76,11 @@ function section(children, t, style, innerStyle) {
|
|
|
63
76
|
children: [{ ...inner, style: { ...inner.style, ...innerStyle } }, ...rest],
|
|
64
77
|
};
|
|
65
78
|
}
|
|
66
|
-
const row = (children, wrap = false) => ({
|
|
79
|
+
const row = (children, wrap = false, align) => ({
|
|
67
80
|
kind: 'group',
|
|
68
81
|
direction: 'row',
|
|
69
82
|
wrap,
|
|
83
|
+
...(align ? { align } : {}),
|
|
70
84
|
children,
|
|
71
85
|
});
|
|
72
86
|
const h = (text, level = 2) => ({ kind: 'heading', level, text });
|
|
@@ -97,6 +111,10 @@ export const LAYOUT_PATTERNS = [
|
|
|
97
111
|
build: (t, pool) => {
|
|
98
112
|
const used = new Set();
|
|
99
113
|
return section([
|
|
114
|
+
// CENTRED, because a hero's two columns are unequal BY DESIGN: a few
|
|
115
|
+
// words on one side, a photograph on the other. Top-aligned they
|
|
116
|
+
// measured 154px beside 420px, and the band read as a caption that
|
|
117
|
+
// had slipped off the picture.
|
|
100
118
|
row([
|
|
101
119
|
{ kind: 'group', direction: 'column', children: [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')] },
|
|
102
120
|
{
|
|
@@ -106,7 +124,7 @@ export const LAYOUT_PATTERNS = [
|
|
|
106
124
|
img(pick(pool, used, 'landscape'), 'Ảnh mở đầu', 'Chưa có ảnh nào trong thư viện — sb_media_upload một URL, rồi sb_set src lên khối này.'),
|
|
107
125
|
],
|
|
108
126
|
},
|
|
109
|
-
]),
|
|
127
|
+
], false, 'center'),
|
|
110
128
|
], t);
|
|
111
129
|
},
|
|
112
130
|
},
|
package/dist/vision/capture.js
CHANGED
|
@@ -838,6 +838,16 @@ function capturePage(limits) {
|
|
|
838
838
|
// cards because it had more than a dozen. The bound that remains is the
|
|
839
839
|
// whole-import node budget, which is the honest place for "this page is
|
|
840
840
|
// enormous".
|
|
841
|
+
// THE SOURCE'S OWN CROSS-AXIS ANSWER. A hero that centres its words
|
|
842
|
+
// against a tall photograph is making a layout decision, and a copy
|
|
843
|
+
// that top-aligns them is not the same band — the same reasoning the
|
|
844
|
+
// `pinned` read already carries. Only the three values that mean
|
|
845
|
+
// something here; `stretch` is the browser's default and saying it
|
|
846
|
+
// would put a value in every imported row for nothing.
|
|
847
|
+
const av = cs.alignItems;
|
|
848
|
+
const align = av === 'center' ? 'center'
|
|
849
|
+
: av === 'flex-end' || av === 'end' ? 'end'
|
|
850
|
+
: undefined;
|
|
841
851
|
const wraps = grid || cs.flexWrap === 'wrap';
|
|
842
852
|
const ROW_MAX = wraps ? 60 : 12;
|
|
843
853
|
if (lays && row && kids.length >= 2 && kids.length <= ROW_MAX) {
|
|
@@ -849,6 +859,7 @@ function capturePage(limits) {
|
|
|
849
859
|
// `nowrap` on one because the property does not apply. Carrying that
|
|
850
860
|
// literally gave the columns nowhere to go at any width.
|
|
851
861
|
wrap: wraps,
|
|
862
|
+
...(align ? { align } : {}),
|
|
852
863
|
children: kids,
|
|
853
864
|
}];
|
|
854
865
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.1",
|
|
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",
|