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 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
  }
@@ -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.30.1",
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",