sbuilder-mcp 0.30.1 → 0.31.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.
- package/CHANGELOG.md +5 -0
- package/CHANGELOG.vi.md +5 -0
- package/dist/domains/site/importmap.js +23 -0
- package/dist/vision/capture.js +125 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,11 @@ 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.31.0] - 2026-09-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- sb_import and sb_import_site now turn a set of panels with a button row into a real `tab` element instead of flattening it, as long as every panel's label can be read off the page (via `aria-controls`, a shared `data-*` pairing, or position when the button row's clickable count matches the panel count); a panel set whose labels are filled in by script rather than present in the DOM is reported as `panel-set-without-labels` and left flattened rather than guessed at.
|
|
13
|
+
|
|
9
14
|
## [0.30.1] - 2026-09-11
|
|
10
15
|
|
|
11
16
|
### Changed
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,11 @@ 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.31.0] - 2026-09-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- sb_import và sb_import_site giờ biến một bộ panel có hàng nút thành một element `tab` thật thay vì làm phẳng nó, miễn là nhãn của mọi panel đọc được trên trang (qua `aria-controls`, một cặp giá trị `data-*` chung, hoặc theo vị trí khi số phần tử bấm được trong hàng nút khớp với số panel); một bộ panel có nhãn do script điền vào chứ không có sẵn trong DOM sẽ được báo là `panel-set-without-labels` và giữ nguyên dạng làm phẳng thay vì bị đoán mò.
|
|
13
|
+
|
|
9
14
|
## [0.30.1] - 2026-09-11
|
|
10
15
|
|
|
11
16
|
### Changed
|
|
@@ -167,6 +167,29 @@ function one(c, t) {
|
|
|
167
167
|
children: body,
|
|
168
168
|
};
|
|
169
169
|
}
|
|
170
|
+
case 'tab': {
|
|
171
|
+
const items = (c.children ?? [])
|
|
172
|
+
.map((item) => one(item, t))
|
|
173
|
+
.filter((n) => n !== null);
|
|
174
|
+
// Two is the floor for a tab: one panel with a button over it is a
|
|
175
|
+
// heading the visitor cannot dismiss.
|
|
176
|
+
if (items.length < 2)
|
|
177
|
+
return null;
|
|
178
|
+
return { type: 'tab', children: items };
|
|
179
|
+
}
|
|
180
|
+
case 'tab-item': {
|
|
181
|
+
const label = c.text?.trim();
|
|
182
|
+
const body = (c.children ?? [])
|
|
183
|
+
.map((k) => one(k, t))
|
|
184
|
+
.filter((n) => n !== null);
|
|
185
|
+
// NO LABEL, NO PANEL — and this is stricter than the accordion beside it
|
|
186
|
+
// on purpose. `accordion-content` seeds its own summary, so an unlabelled
|
|
187
|
+
// one still opens; `tab` builds its BUTTON ROW from these labels, so an
|
|
188
|
+
// unlabelled panel is one the visitor has no way to reach.
|
|
189
|
+
if (!label || !body.length)
|
|
190
|
+
return null;
|
|
191
|
+
return { type: 'tab-content', specials: { label }, children: body };
|
|
192
|
+
}
|
|
170
193
|
case 'divider': {
|
|
171
194
|
return { type: 'divider', style: { width: '100%' } };
|
|
172
195
|
}
|
package/dist/vision/capture.js
CHANGED
|
@@ -211,6 +211,21 @@ function capturePage(limits) {
|
|
|
211
211
|
}
|
|
212
212
|
if (!hidden.length)
|
|
213
213
|
continue;
|
|
214
|
+
// STAMPED WHILE WE STILL KNOW. Once revealed, a panel is indistinguishable
|
|
215
|
+
// from an ordinary visible child, so the split is recorded here — and
|
|
216
|
+
// ONLY the ones that were actually hidden. A first version stamped every
|
|
217
|
+
// child that shared a tag with a visible peer, which swept in the BUTTON
|
|
218
|
+
// ROW itself: it is a `<div>` with children like the panels are, so the
|
|
219
|
+
// tab came out with its own controls as one of its panes.
|
|
220
|
+
let stamped = 0;
|
|
221
|
+
for (const k of hidden) {
|
|
222
|
+
if (!shownByTag[k.tagName] || k.children.length === 0)
|
|
223
|
+
continue;
|
|
224
|
+
k.setAttribute('data-sb-panel', '');
|
|
225
|
+
stamped += 1;
|
|
226
|
+
}
|
|
227
|
+
if (stamped >= 1)
|
|
228
|
+
parent.setAttribute('data-sb-panelset', '');
|
|
214
229
|
for (const k of hidden) {
|
|
215
230
|
// Real content only — an empty slot or a script-filled placeholder is
|
|
216
231
|
// not a panel worth showing, and revealing it costs a skip either way.
|
|
@@ -458,6 +473,116 @@ function capturePage(limits) {
|
|
|
458
473
|
skip('hidden');
|
|
459
474
|
return [];
|
|
460
475
|
}
|
|
476
|
+
// A TAB SET, WHEN — AND ONLY WHEN — ITS LABELS CAN BE READ.
|
|
477
|
+
//
|
|
478
|
+
// The platform's `tab` synthesizes its whole button row from each
|
|
479
|
+
// `tab-content` child's `specials.label` (render/nodes/tab/html.go), so a
|
|
480
|
+
// tab with no labels is a stack of panels wearing a control nobody can
|
|
481
|
+
// use. That makes "are there labels" the right question AND the right
|
|
482
|
+
// discriminator: MEASURED on ttgshop.vn, the panel-set stamp also lands on
|
|
483
|
+
// a `grid-cols-5` product grid that happens to hide one card, and a label
|
|
484
|
+
// row is exactly what that grid does not have.
|
|
485
|
+
//
|
|
486
|
+
// Three pairings, strongest first, because a wrong label is worse than
|
|
487
|
+
// none — it puts one collection's name over another's products:
|
|
488
|
+
// 1. ARIA. `[role=tab][aria-controls=<panel id>]` says it outright.
|
|
489
|
+
// 2. A SHARED data-* VALUE. `<button data-id="7">` beside
|
|
490
|
+
// `<div data-id="7">` is how most hand-rolled tab scripts wire
|
|
491
|
+
// themselves, and it survives reordering.
|
|
492
|
+
// 3. POSITION, and only from a row that holds exactly as many
|
|
493
|
+
// clickable items as there are panels — anything else is a guess
|
|
494
|
+
// dressed as a match.
|
|
495
|
+
//
|
|
496
|
+
// Every panel must come out with a label or this is not a tab. ttgshop.vn
|
|
497
|
+
// itself does NOT become one: its tab buttons are empty in the DOM and
|
|
498
|
+
// filled by script, and its section titles are images. Deriving a label
|
|
499
|
+
// from the `data-url` slug was available and refused — "hot-sale" is not
|
|
500
|
+
// what the page says, and a Vietnamese slug comes back stripped of its
|
|
501
|
+
// diacritics, which is the invented-copy defect this repo reports on
|
|
502
|
+
// everybody else's seeds.
|
|
503
|
+
const tabSetOf = (host) => {
|
|
504
|
+
const kids = Array.from(host.children);
|
|
505
|
+
const revealed = kids.filter((c) => c.getAttribute('data-sb-panel') !== null);
|
|
506
|
+
if (revealed.length === 0)
|
|
507
|
+
return null;
|
|
508
|
+
// THE BUTTON ROW IS A SIBLING OF THE PANELS, and it has to be taken out
|
|
509
|
+
// of the set before anything is counted — it is a container of the same
|
|
510
|
+
// shape, and leaving it in makes the panel count wrong and the pairing
|
|
511
|
+
// meaningless. It is the child that holds the clickable words.
|
|
512
|
+
// INNERMOST ONLY. `<li><button>Một</button></li>` is the ordinary way a
|
|
513
|
+
// tab bar is written, and counting both gives four clickables for two
|
|
514
|
+
// panels — which fails the count test that position pairing depends on,
|
|
515
|
+
// so a perfectly labelled tab bar read as unlabelled.
|
|
516
|
+
const clicksIn = (c) => {
|
|
517
|
+
const all = Array.from(c.querySelectorAll('button, a, li, [role="tab"]')).filter((b) => clean(b.textContent).length > 0);
|
|
518
|
+
return all.filter((b) => !all.some((o) => o !== b && b.contains(o)));
|
|
519
|
+
};
|
|
520
|
+
let bar = null;
|
|
521
|
+
for (const c of kids) {
|
|
522
|
+
if (c.getAttribute('data-sb-panel') !== null)
|
|
523
|
+
continue;
|
|
524
|
+
if (clicksIn(c).length >= 2) {
|
|
525
|
+
bar = c;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const tagOf = revealed[0].tagName;
|
|
530
|
+
const panels = kids.filter((c) => c !== bar && c.tagName === tagOf && c.children.length > 0);
|
|
531
|
+
if (panels.length < 2)
|
|
532
|
+
return null;
|
|
533
|
+
const clickable = bar ? clicksIn(bar) : [];
|
|
534
|
+
const labels = [];
|
|
535
|
+
for (let i = 0; i < panels.length; i += 1) {
|
|
536
|
+
const panel = panels[i];
|
|
537
|
+
const id = panel.getAttribute('id');
|
|
538
|
+
let label = '';
|
|
539
|
+
if (id) {
|
|
540
|
+
for (const b of Array.from(host.querySelectorAll(`[aria-controls="${id}"]`))) {
|
|
541
|
+
const t = clean(b.textContent);
|
|
542
|
+
if (t) {
|
|
543
|
+
label = t;
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (!label) {
|
|
549
|
+
for (const attr of Array.from(panel.attributes ?? [])) {
|
|
550
|
+
if (attr.name.indexOf('data-') !== 0 || attr.name === 'data-sb-panel' || !attr.value)
|
|
551
|
+
continue;
|
|
552
|
+
for (const b of clickable) {
|
|
553
|
+
if (b.getAttribute(attr.name) === attr.value) {
|
|
554
|
+
label = clean(b.textContent);
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (label)
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (!label && clickable.length === panels.length)
|
|
563
|
+
label = clean(clickable[i].textContent);
|
|
564
|
+
if (!label)
|
|
565
|
+
return null;
|
|
566
|
+
labels.push(label);
|
|
567
|
+
}
|
|
568
|
+
const children = [];
|
|
569
|
+
for (let i = 0; i < panels.length; i += 1) {
|
|
570
|
+
const body = walk(panels[i]);
|
|
571
|
+
if (body.length === 0)
|
|
572
|
+
continue;
|
|
573
|
+
children.push({ kind: 'tab-item', text: labels[i], children: body });
|
|
574
|
+
}
|
|
575
|
+
if (children.length < 2)
|
|
576
|
+
return null;
|
|
577
|
+
taken.nodes++;
|
|
578
|
+
return { kind: 'tab', children };
|
|
579
|
+
};
|
|
580
|
+
if (el.getAttribute('data-sb-panelset') !== null) {
|
|
581
|
+
const asTabs = tabSetOf(el);
|
|
582
|
+
if (asTabs)
|
|
583
|
+
return [asTabs];
|
|
584
|
+
skip('panel-set-without-labels');
|
|
585
|
+
}
|
|
461
586
|
if (tag === 'DETAILS') {
|
|
462
587
|
const kids2 = Array.from(el.children);
|
|
463
588
|
const summary = kids2.filter((c) => c.tagName === 'SUMMARY')[0];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
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",
|