sbuilder-mcp 0.11.0 → 0.11.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,20 @@ 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.11.2] - 2026-09-09
10
+
11
+ ### Fixed
12
+ - sb_set's routing of a hover write to an element's legacy home (a `button`, for instance) now also clears any leftover values in the unread `states.hover` slot in the same call, instead of leaving them behind for `sb_review` to keep reporting as `hover_dead`; the finding's own message previously named `unset` as the way to remove them, which could not reach the legacy home either, so the documented fix was unreachable.
13
+
14
+ ## [0.11.1] - 2026-09-09
15
+
16
+ ### Added
17
+ - sb_review now reports `hover_dead` for a node whose element keeps its hover state in a legacy home its own renderer reads instead of the universal `states.hover` slot — a `button`, for instance — naming the sb_set call that moves the values where they will actually paint.
18
+ - npm run codegen now refuses to run against a web_builder checkout with uncommitted changes in the directories it reads (schema/src, editor/src, server/render, server/docs), naming the dirty files instead of silently baking a concurrent session's half-finished work into the committed catalog; `--dirty` overrides it for generating against your own in-progress change.
19
+
20
+ ### Fixed
21
+ - The generated element catalog is regenerated from a committed ref, removing a `cart-count` element and its satellite owner that had leaked in from another session's uncommitted platform change; the catalog is back to 108 elements.
22
+
9
23
  ## [0.11.0] - 2026-09-09
10
24
 
11
25
  ### Added
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,20 @@ 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.11.2] - 2026-09-09
10
+
11
+ ### Fixed
12
+ - Khi sb_set định tuyến một lần ghi hover tới nơi lưu cũ của element (ví dụ `button`), giờ nó cũng xoá luôn mọi giá trị còn sót trong slot `states.hover` không ai đọc, ngay trong cùng một lệnh — thay vì để lại chúng cho sb_review tiếp tục báo là `hover_dead`; thông báo của chính finding này trước đây nêu `unset` là cách xoá, nhưng `unset` cũng không thể chạm tới nơi lưu cũ đó, khiến cách sửa được ghi ra là không thể thực hiện được.
13
+
14
+ ## [0.11.1] - 2026-09-09
15
+
16
+ ### Added
17
+ - sb_review giờ báo cáo `hover_dead` cho một node mà element của nó lưu hover state ở nơi cũ mà chính renderer của nó đọc, thay vì slot `states.hover` phổ quát — ví dụ như `button` — và nêu rõ lệnh sb_set để chuyển giá trị tới đúng nơi sẽ thực sự hiển thị.
18
+ - npm run codegen giờ từ chối chạy trên một checkout web_builder có thay đổi chưa commit trong các thư mục mà nó đọc (schema/src, editor/src, server/render, server/docs), nêu tên các file bẩn thay vì âm thầm đưa công việc dở dang của một phiên làm việc đồng thời vào catalog đã commit; `--dirty` cho phép ghi đè khi bạn tự tạo ra thay đổi đang dở dang đó.
19
+
20
+ ### Fixed
21
+ - Catalog element được sinh lại từ một ref đã commit, loại bỏ một element `cart-count` và chủ sở hữu satellite của nó vốn đã lọt vào từ thay đổi nền tảng chưa commit của một phiên làm việc khác; catalog trở lại 108 element.
22
+
9
23
  ## [0.11.0] - 2026-09-09
10
24
 
11
25
  ### Added
@@ -1,5 +1,5 @@
1
1
  export const ELEMENT_SOURCE = {
2
- "count": 109,
2
+ "count": 108,
3
3
  "docSchemaVersion": 2
4
4
  };
5
5
  export const ELEMENTS = {
@@ -1803,8 +1803,7 @@ export const ELEMENTS = {
1803
1803
  "key": "action",
1804
1804
  "label": "Action",
1805
1805
  "controls": [
1806
- "action",
1807
- "icon_cart_badge"
1806
+ "action"
1808
1807
  ]
1809
1808
  },
1810
1809
  {
@@ -1877,7 +1876,6 @@ export const ELEMENTS = {
1877
1876
  "icon_color",
1878
1877
  "icon_size",
1879
1878
  "action",
1880
- "icon_cart_badge",
1881
1879
  "padding",
1882
1880
  "margin",
1883
1881
  "align_self",
@@ -13844,166 +13842,6 @@ export const ELEMENTS = {
13844
13842
  "dynamic"
13845
13843
  ]
13846
13844
  },
13847
- "cart-count": {
13848
- "type": "cart-count",
13849
- "label": "Cart count",
13850
- "category": "store",
13851
- "isContainer": false,
13852
- "isRootOnly": false,
13853
- "locked": false,
13854
- "hideInLayer": false,
13855
- "childAllows": [],
13856
- "defaults": {
13857
- "style": {
13858
- "display": "inline-flex",
13859
- "alignItems": "center",
13860
- "justifyContent": "center",
13861
- "width": "fit-content",
13862
- "minWidth": "20px",
13863
- "height": "20px",
13864
- "paddingLeft": "6px",
13865
- "paddingRight": "6px",
13866
- "borderRadius": "999px",
13867
- "backgroundColor": "#171717",
13868
- "color": "#ffffff",
13869
- "fontSize": "12px",
13870
- "fontWeight": "600",
13871
- "lineHeight": "1"
13872
- },
13873
- "specials": {
13874
- "placeholder": "0",
13875
- "hideWhenEmpty": true
13876
- }
13877
- },
13878
- "inspector": [
13879
- {
13880
- "tab": "general",
13881
- "groups": [
13882
- {
13883
- "key": "content",
13884
- "label": "Content",
13885
- "controls": [
13886
- "cart_count_placeholder",
13887
- "cart_count_hide_empty"
13888
- ]
13889
- },
13890
- {
13891
- "key": "size",
13892
- "label": "Size",
13893
- "controls": [
13894
- "width_select",
13895
- "height_select",
13896
- "size_bounds"
13897
- ]
13898
- },
13899
- {
13900
- "key": "layout",
13901
- "label": "Layout",
13902
- "controls": [
13903
- "padding",
13904
- "margin",
13905
- "vertical",
13906
- "horizontal"
13907
- ]
13908
- },
13909
- {
13910
- "key": "typography",
13911
- "label": "Typography",
13912
- "controls": [
13913
- "font_family",
13914
- "font_size",
13915
- "text_style",
13916
- "text_color"
13917
- ]
13918
- },
13919
- {
13920
- "key": "background",
13921
- "label": "Background",
13922
- "controls": [
13923
- "bg_color"
13924
- ]
13925
- },
13926
- {
13927
- "key": "shape",
13928
- "label": "Shape",
13929
- "controls": [
13930
- "border",
13931
- "corner"
13932
- ]
13933
- }
13934
- ]
13935
- },
13936
- {
13937
- "tab": "advanced",
13938
- "groups": [
13939
- {
13940
- "key": "spacing",
13941
- "label": "Spacing",
13942
- "controls": [
13943
- "padding_margin"
13944
- ]
13945
- },
13946
- {
13947
- "key": "display",
13948
- "label": "Display",
13949
- "controls": [
13950
- "display"
13951
- ]
13952
- },
13953
- {
13954
- "key": "class_css",
13955
- "label": "Class",
13956
- "controls": [
13957
- "class_css"
13958
- ]
13959
- }
13960
- ]
13961
- }
13962
- ],
13963
- "controls": [
13964
- "cart_count_placeholder",
13965
- "cart_count_hide_empty",
13966
- "width_select",
13967
- "height_select",
13968
- "size_bounds",
13969
- "padding",
13970
- "margin",
13971
- "vertical",
13972
- "horizontal",
13973
- "font_family",
13974
- "font_size",
13975
- "text_style",
13976
- "text_color",
13977
- "bg_color",
13978
- "border",
13979
- "corner",
13980
- "padding_margin",
13981
- "display",
13982
- "class_css"
13983
- ],
13984
- "description": "The number of items in the shopper's cart, as a small badge. The figure comes from the shopper's own cart in their browser, so it changes the moment they add, remove or re-count a line. Its usual home is the cart icon in the site header, where it is the only evidence on the page that the basket holds anything.",
13985
- "useWhen": [
13986
- "a header or navigation bar carries a cart icon and a shopper needs to see the basket is not empty",
13987
- "building a sticky mobile bar or a mini-cart trigger anywhere outside the cart drawer"
13988
- ],
13989
- "avoidWhen": [
13990
- "showing what the cart COSTS — that is a cart-total, which reads a money figure",
13991
- "showing how many of ONE product a shopper picked — that is the quantity stepper on the product",
13992
- "showing a past order’s item count; an order is a record, not the live cart"
13993
- ],
13994
- "contentTips": [
13995
- "the placeholder is what the badge reads before the shopper’s cart is known, so write it as a real empty-cart count",
13996
- "leave “hide when empty” on unless the header’s layout must never shift when the first item lands",
13997
- "to put it on a cart icon, turn the badge on from the icon’s own inspector rather than placing this element and positioning it by hand"
13998
- ],
13999
- "semantics": [
14000
- "cart",
14001
- "count",
14002
- "badge",
14003
- "header",
14004
- "commerce"
14005
- ]
14006
- },
14007
13845
  "cart-total": {
14008
13846
  "type": "cart-total",
14009
13847
  "label": "Cart total",
@@ -33839,28 +33677,6 @@ export const TRAIT_WRITES = {
33839
33677
  }
33840
33678
  ]
33841
33679
  },
33842
- "cart_count_placeholder": {
33843
- "key": "cart_count_placeholder",
33844
- "label": "Empty count",
33845
- "writes": [
33846
- {
33847
- "target": "specials",
33848
- "writeKey": "placeholder",
33849
- "type": "string"
33850
- }
33851
- ]
33852
- },
33853
- "cart_count_hide_empty": {
33854
- "key": "cart_count_hide_empty",
33855
- "label": "Hide when empty",
33856
- "writes": [
33857
- {
33858
- "target": "specials",
33859
- "writeKey": "hideWhenEmpty",
33860
- "type": "boolean"
33861
- }
33862
- ]
33863
- },
33864
33680
  "cart_total_placeholder": {
33865
33681
  "key": "cart_total_placeholder",
33866
33682
  "label": "Empty amount",
@@ -34221,13 +34037,6 @@ export const TRAIT_WRITES = {
34221
34037
  }
34222
34038
  };
34223
34039
  export const SATELLITE_RULES = {
34224
- "icon": [
34225
- {
34226
- "type": "cart-count",
34227
- "configKey": "cartCountId",
34228
- "optional": true
34229
- }
34230
- ],
34231
34040
  "tab": [
34232
34041
  {
34233
34042
  "type": "tab-item",
@@ -264,6 +264,29 @@ export function setKeys(doc, id, keys, opts) {
264
264
  // compiler reads. Routed rather than refused: the caller asked for a hover
265
265
  // and there is a home that works; `hoverRoutingNote` says where it went.
266
266
  if (opts.state === HOVER_STATE && namespace === 'style' && hoverHome(doc.node(id).data.type) === 'legacy') {
267
+ // …AND CLEAR THE SLOT NOBODY READS, in the same call.
268
+ //
269
+ // On an element with this home, `states.hover` has no legitimate use: the
270
+ // universal compiler stands aside for it and its own renderer looks in the
271
+ // flat map. Leaving values there is the exact defect `sb_review` reports as
272
+ // `hover_dead`, so a rewrite that repaired the look and left the dead copy
273
+ // behind would fix the page and keep the finding — and the fix that
274
+ // finding names would still be unreachable, which is the shape this repo
275
+ // has now closed twice.
276
+ //
277
+ // Safe against the platform changing its mind: if a button ever keeps its
278
+ // hover in the node, its meta's `storage` becomes 'node', HOVER_HOMES says
279
+ // 'state', and this branch does not run at all.
280
+ const node = doc.node(id);
281
+ const dead = [];
282
+ if (node.states?.[HOVER_STATE] !== undefined) {
283
+ dead.push({ op: 'unset', path: ['nodes', id, 'states', HOVER_STATE] });
284
+ }
285
+ for (const [bp, slot] of Object.entries(node.responsive ?? {})) {
286
+ if (slot?.states?.[HOVER_STATE] !== undefined) {
287
+ dead.push({ op: 'unset', path: ['nodes', id, 'responsive', bp, 'states', HOVER_STATE] });
288
+ }
289
+ }
267
290
  return [
268
291
  ...Object.entries(keys).map(([k, v]) => ({
269
292
  op: 'set',
@@ -271,6 +294,7 @@ export function setKeys(doc, id, keys, opts) {
271
294
  value: v,
272
295
  })),
273
296
  ...(opts.unset ?? []).map((k) => ({ op: 'unset', path: legacyHoverPath(id, k) })),
297
+ ...dead,
274
298
  ];
275
299
  }
276
300
  // Base state and per-breakpoint state are DIFFERENT PLACES in the document,
@@ -60,6 +60,9 @@ export const FIX = {
60
60
  '{ "overflowX": "visible", "overflowY": "visible" } — or move the pinned node outside it. ' +
61
61
  'Sticky resolves against its nearest SCROLLING ancestor, so a clipping one becomes that ' +
62
62
  'ancestor and the node pins inside a box that never scrolls.',
63
+ hover_dead: 'Rewrite it with the same values: sb_set id "<id>", namespace style, state "hover", ' +
64
+ 'keys { … } — read them off the node first, because that one call both writes them where ' +
65
+ 'the renderer looks AND clears the slot nobody reads. Nothing else to do afterwards.',
63
66
  stuck_no_host: 'Pin something: sb_set id "<id>" (or the section it lives in), namespace style, keys ' +
64
67
  '{ "position": "sticky" } — sb_set seeds the offset and the layer order with it. Until ' +
65
68
  'then the platform compiles no rule for this state at all, so the override is stored, ' +
@@ -1,5 +1,6 @@
1
1
  import { childrenOf, childrenWithSatellites, isOverlay, pageChildren, appBlockRoot, SPEC_GLOBAL_REF, SPEC_APP_BLOCK_REF } from '../../core/tree.js';
2
2
  import { STUCK_STATE, stickyBlockedBy, stuckHostOf, isPinnedNode } from './sticky.js';
3
+ import { HOVER_STATE, hoverHome } from './hover.js';
3
4
  import { ELEMENTS, BINDING_SOURCES, BOUND_SPECIALS, FIRST_CHILD_ONLY, SATELLITE_RULES, ELEMENT_SEEDS } from '../../catalog/elements.generated.js';
4
5
  import { fill } from './findings.js';
5
6
  /**
@@ -224,6 +225,32 @@ export function reviewDesign(doc) {
224
225
  styled.states?.[STUCK_STATE],
225
226
  ...Object.values(styled.responsive ?? {}).map((r) => r?.states?.[STUCK_STATE]),
226
227
  ];
228
+ // A HOVER STORED WHERE NOBODY READS IT. The universal hover state is not the
229
+ // only home: an element whose meta declares a Hover variant keeps it
230
+ // somewhere its own renderer looks, and for a `button` that is the flat
231
+ // `config.stateHover` map. Every hover this server wrote onto a button
232
+ // before it learned that is still sitting in `states.hover`, painting
233
+ // nothing — on a control that is the single most common thing anyone gives
234
+ // a hover to.
235
+ const hoverSlots = [
236
+ styled.states?.[HOVER_STATE],
237
+ ...Object.values(styled.responsive ?? {}).map((r) => r?.states?.[HOVER_STATE]),
238
+ ];
239
+ const hasHover = hoverSlots.some((slot) => {
240
+ const h = slot;
241
+ return !!h && (Object.keys(h.style ?? {}).length > 0 || Object.keys(h.config ?? {}).length > 0);
242
+ });
243
+ if (hasHover && hoverHome(type) === 'legacy') {
244
+ out.push({
245
+ code: 'hover_dead',
246
+ nodeId: id,
247
+ type,
248
+ problem: `"${type}" keeps its hover in config.stateHover — the flat map its own renderer ` +
249
+ 'compiles — and the platform\'s universal hover compiler stands aside for it. These ' +
250
+ 'states.hover values are stored, saved, published and painted by nobody.',
251
+ fix: fill('hover_dead', { id }),
252
+ });
253
+ }
227
254
  const hasStuck = stuckSlots.some((slot) => {
228
255
  const s = slot;
229
256
  return !!s && (Object.keys(s.style ?? {}).length > 0 || Object.keys(s.config ?? {}).length > 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.11.0",
3
+ "version": "0.11.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",