sbuilder-mcp 0.9.2 → 0.11.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 +17 -0
- package/CHANGELOG.vi.md +17 -0
- package/dist/catalog/elements.generated.js +236 -2
- package/dist/domains/site/builder.js +63 -8
- package/dist/domains/site/hover.js +194 -0
- package/dist/domains/site/review.js +14 -2
- package/dist/tools/page.js +40 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,23 @@ 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.0] - 2026-09-09
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- sb_set's `state:"hover"` now routes each write to the home its element's own renderer actually reads, since the platform's universal hover compiler deliberately stands aside for twelve element types that declare their own Hover variant; a `button` write now lands in `config.stateHover` (flat, base-only) instead of the unread `states.hover` slot, and the response reports where it went so a caller reading the node back is not surprised.
|
|
13
|
+
- sb_set refuses a `state:"parentHover"` write on a node with no box to key off — a satellite, a direct child of the page root, or an orphan — naming the reason instead of storing a rule the platform would never match.
|
|
14
|
+
- sb_set refuses `config.revealOnHover` on a node with no such box for the same reason, since the platform emits neither half of the reveal without one and the element would simply stay visible.
|
|
15
|
+
- sb_set now translates `hidden: true` under a hover state into `display: none`, and refuses any other config key or a `false` value, matching the same contract already enforced for the `stuck` state.
|
|
16
|
+
- sb_set warns when a write targets `product-image-list`'s hover state: its meta promises `states.hover`, but measured on 2026-09-09 nothing in the platform compiles it yet, so the override is stored where it belongs and will start painting once the platform closes the gap.
|
|
17
|
+
|
|
18
|
+
## [0.10.0] - 2026-09-09
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- sb_set now takes `unset`, an array of key names removed from the same slot a write would target — base, a breakpoint, or either home of a state — giving every finding that names "remove the override" as its fix a tool that can actually perform it. `keys` is now optional when `unset` carries the work, and a pure removal is exempt from the sticky-host guard so it can repair a `stuck_no_host` finding rather than being refused by it.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- sb_review's `stuck_no_host` check now counts the keys inside a stuck state slot instead of only checking whether the slot exists, matching the platform's own `HasStuckOverrides`; an empty slot (the state a repair with `unset` leaves behind) no longer keeps reporting the finding it was used to fix.
|
|
25
|
+
|
|
9
26
|
## [0.9.2] - 2026-09-09
|
|
10
27
|
|
|
11
28
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,23 @@ 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.0] - 2026-09-09
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `state:"hover"` của sb_set giờ định tuyến mỗi lần ghi tới đúng nơi mà renderer của element đó thực sự đọc, vì bộ biên dịch hover phổ quát của nền tảng chủ động đứng ngoài đối với mười hai loại element tự khai báo biến thể Hover riêng; một lần ghi trên `button` giờ vào `config.stateHover` (dạng phẳng, chỉ ở base) thay vì slot `states.hover` không ai đọc, và kết quả trả về cho biết giá trị đã đi vào đâu để người gọi đọc lại node không bị bất ngờ.
|
|
13
|
+
- sb_set từ chối ghi `state:"parentHover"` trên một node không có "hộp" nào để dựa vào — một satellite, một con trực tiếp của root trang, hoặc một node mồ côi — nêu rõ lý do thay vì lưu một rule mà nền tảng sẽ không bao giờ khớp.
|
|
14
|
+
- sb_set từ chối `config.revealOnHover` trên một node không có hộp như vậy vì cùng lý do, vì nền tảng sẽ không phát ra cả hai nửa của hiệu ứng reveal nếu thiếu nó, khiến phần tử đơn giản là luôn hiển thị.
|
|
15
|
+
- sb_set giờ dịch `hidden: true` dưới một hover state thành `display: none`, và từ chối mọi config key khác hoặc giá trị `false`, khớp với đúng quy tắc đã áp dụng cho trạng thái `stuck`.
|
|
16
|
+
- sb_set cảnh báo khi một lần ghi nhắm vào hover state của `product-image-list`: meta của nó hứa hẹn `states.hover`, nhưng đo được vào ngày 2026-09-09 thì chưa có gì trong nền tảng biên dịch nó cả, nên override vẫn được lưu đúng chỗ meta quy định và sẽ bắt đầu hiển thị khi nền tảng khắc phục xong khoảng trống này.
|
|
17
|
+
|
|
18
|
+
## [0.10.0] - 2026-09-09
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- sb_set giờ nhận `unset`, một mảng tên key cần xoá khỏi đúng slot mà một lần ghi sẽ nhắm tới — base, một breakpoint, hoặc một trong hai nơi lưu của một state — nhờ đó mọi finding nêu "xoá override đi" làm cách sửa giờ có công cụ để thực sự làm điều đó. `keys` giờ có thể bỏ qua khi `unset` đã làm phần việc, và một lần xoá thuần tuý được miễn khỏi kiểm tra sticky-host để nó có thể sửa một finding `stuck_no_host` thay vì bị chính kiểm tra đó từ chối.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Kiểm tra `stuck_no_host` của sb_review giờ đếm số key bên trong slot trạng thái stuck thay vì chỉ kiểm tra slot có tồn tại hay không, khớp với `HasStuckOverrides` của chính nền tảng; một slot rỗng (trạng thái mà một lần sửa bằng `unset` để lại) không còn tiếp tục báo cáo finding mà nó vừa được dùng để khắc phục.
|
|
25
|
+
|
|
9
26
|
## [0.9.2] - 2026-09-09
|
|
10
27
|
|
|
11
28
|
### Added
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const ELEMENT_SOURCE = {
|
|
2
|
-
"count":
|
|
2
|
+
"count": 109,
|
|
3
3
|
"docSchemaVersion": 2
|
|
4
4
|
};
|
|
5
5
|
export const ELEMENTS = {
|
|
@@ -1803,7 +1803,8 @@ export const ELEMENTS = {
|
|
|
1803
1803
|
"key": "action",
|
|
1804
1804
|
"label": "Action",
|
|
1805
1805
|
"controls": [
|
|
1806
|
-
"action"
|
|
1806
|
+
"action",
|
|
1807
|
+
"icon_cart_badge"
|
|
1807
1808
|
]
|
|
1808
1809
|
},
|
|
1809
1810
|
{
|
|
@@ -1876,6 +1877,7 @@ export const ELEMENTS = {
|
|
|
1876
1877
|
"icon_color",
|
|
1877
1878
|
"icon_size",
|
|
1878
1879
|
"action",
|
|
1880
|
+
"icon_cart_badge",
|
|
1879
1881
|
"padding",
|
|
1880
1882
|
"margin",
|
|
1881
1883
|
"align_self",
|
|
@@ -13842,6 +13844,166 @@ export const ELEMENTS = {
|
|
|
13842
13844
|
"dynamic"
|
|
13843
13845
|
]
|
|
13844
13846
|
},
|
|
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
|
+
},
|
|
13845
14007
|
"cart-total": {
|
|
13846
14008
|
"type": "cart-total",
|
|
13847
14009
|
"label": "Cart total",
|
|
@@ -33564,6 +33726,11 @@ export const TRAIT_WRITES = {
|
|
|
33564
33726
|
"target": "config",
|
|
33565
33727
|
"writeKey": "hidden",
|
|
33566
33728
|
"type": "boolean"
|
|
33729
|
+
},
|
|
33730
|
+
{
|
|
33731
|
+
"target": "config",
|
|
33732
|
+
"writeKey": "revealOnHover",
|
|
33733
|
+
"type": "boolean"
|
|
33567
33734
|
}
|
|
33568
33735
|
]
|
|
33569
33736
|
},
|
|
@@ -33672,6 +33839,28 @@ export const TRAIT_WRITES = {
|
|
|
33672
33839
|
}
|
|
33673
33840
|
]
|
|
33674
33841
|
},
|
|
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
|
+
},
|
|
33675
33864
|
"cart_total_placeholder": {
|
|
33676
33865
|
"key": "cart_total_placeholder",
|
|
33677
33866
|
"label": "Empty amount",
|
|
@@ -34032,6 +34221,13 @@ export const TRAIT_WRITES = {
|
|
|
34032
34221
|
}
|
|
34033
34222
|
};
|
|
34034
34223
|
export const SATELLITE_RULES = {
|
|
34224
|
+
"icon": [
|
|
34225
|
+
{
|
|
34226
|
+
"type": "cart-count",
|
|
34227
|
+
"configKey": "cartCountId",
|
|
34228
|
+
"optional": true
|
|
34229
|
+
}
|
|
34230
|
+
],
|
|
34035
34231
|
"tab": [
|
|
34036
34232
|
{
|
|
34037
34233
|
"type": "tab-item",
|
|
@@ -34600,3 +34796,41 @@ export const ELEMENT_SEEDS = {
|
|
|
34600
34796
|
export const FIRST_CHILD_ONLY = [
|
|
34601
34797
|
"list-dataset"
|
|
34602
34798
|
];
|
|
34799
|
+
export const HOVER_HOMES = {
|
|
34800
|
+
"button": {
|
|
34801
|
+
"home": "legacy"
|
|
34802
|
+
},
|
|
34803
|
+
"filter-checkbox": {
|
|
34804
|
+
"home": "state"
|
|
34805
|
+
},
|
|
34806
|
+
"filter-color": {
|
|
34807
|
+
"home": "state"
|
|
34808
|
+
},
|
|
34809
|
+
"filter-radio": {
|
|
34810
|
+
"home": "state"
|
|
34811
|
+
},
|
|
34812
|
+
"filter-tag": {
|
|
34813
|
+
"home": "state"
|
|
34814
|
+
},
|
|
34815
|
+
"tab-item": {
|
|
34816
|
+
"home": "state"
|
|
34817
|
+
},
|
|
34818
|
+
"accordion-item": {
|
|
34819
|
+
"home": "state"
|
|
34820
|
+
},
|
|
34821
|
+
"menu-item": {
|
|
34822
|
+
"home": "state"
|
|
34823
|
+
},
|
|
34824
|
+
"product-image-list": {
|
|
34825
|
+
"home": "state"
|
|
34826
|
+
},
|
|
34827
|
+
"quantity-button": {
|
|
34828
|
+
"home": "state"
|
|
34829
|
+
},
|
|
34830
|
+
"product-variant-option": {
|
|
34831
|
+
"home": "state"
|
|
34832
|
+
},
|
|
34833
|
+
"text-dataset": {
|
|
34834
|
+
"home": "state"
|
|
34835
|
+
}
|
|
34836
|
+
};
|
|
@@ -3,6 +3,7 @@ import { ELEMENTS, ELEMENT_SEEDS, SATELLITE_RULES } from '../../catalog/elements
|
|
|
3
3
|
import { bindingsForConfig, createNode, mintSatellites } from './node.js';
|
|
4
4
|
import { refuseSecondTemplate } from './traps.js';
|
|
5
5
|
import { STUCK_STATE, refuseStuckAfter, refuseStuckConfig, requireStuckHost, stickySeeds, } from './sticky.js';
|
|
6
|
+
import { HOVER_STATE, PARENT_HOVER_STATE, hoverHome, legacyHoverPath, refuseHoverConfig, refuseReveal, requireHoverHost, } from './hover.js';
|
|
6
7
|
import { genId } from './ids.js';
|
|
7
8
|
/** Append sentinel: splice clamps a too-large index, and `isSyncablePatch`
|
|
8
9
|
* deliberately allows one — an append is a legitimate thing to describe. */
|
|
@@ -186,8 +187,27 @@ export function setKeys(doc, id, keys, opts) {
|
|
|
186
187
|
// different ways without a word — see sticky.ts. Checked here rather than in
|
|
187
188
|
// the state branch because it is written on the node that PINS, at base or at
|
|
188
189
|
// a breakpoint, never inside a stuck slot.
|
|
189
|
-
if (namespace === 'config')
|
|
190
|
+
if (namespace === 'config') {
|
|
190
191
|
refuseStuckAfter(doc.doc, id, keys);
|
|
192
|
+
// The reveal is a BASE-level config key with a host precondition, so it is
|
|
193
|
+
// checked here rather than in the state branch — it is not a state at all.
|
|
194
|
+
refuseReveal(doc.doc, id, keys);
|
|
195
|
+
}
|
|
196
|
+
// The paths `keys` and `unset` share. Computed once so a removal can never
|
|
197
|
+
// land somewhere a write would not have.
|
|
198
|
+
const slot = () => {
|
|
199
|
+
if (namespace === 'specials')
|
|
200
|
+
return ['nodes', id, 'specials'];
|
|
201
|
+
if (opts.state) {
|
|
202
|
+
return opts.base
|
|
203
|
+
? ['nodes', id, 'states', opts.state, namespace]
|
|
204
|
+
: ['nodes', id, 'responsive', opts.breakpoint ?? 'desktop', 'states', opts.state, namespace];
|
|
205
|
+
}
|
|
206
|
+
if (opts.base)
|
|
207
|
+
return ['nodes', id, namespace];
|
|
208
|
+
return ['nodes', id, 'responsive', opts.breakpoint ?? 'desktop', namespace];
|
|
209
|
+
};
|
|
210
|
+
const removals = (opts.unset ?? []).map((k) => ({ op: 'unset', path: [...slot(), k] }));
|
|
191
211
|
if (namespace === 'specials') {
|
|
192
212
|
refuseComposedStamp(keys);
|
|
193
213
|
// `specials` is content and identity, base-only by definition, and states
|
|
@@ -198,11 +218,14 @@ export function setKeys(doc, id, keys, opts) {
|
|
|
198
218
|
'specials is content and identity (text, htmlTag, bound…), which do not vary by ' +
|
|
199
219
|
'state. Style is what has states.');
|
|
200
220
|
}
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
221
|
+
return [
|
|
222
|
+
...Object.entries(keys).map(([k, v]) => ({
|
|
223
|
+
op: 'set',
|
|
224
|
+
path: ['nodes', id, 'specials', k],
|
|
225
|
+
value: v,
|
|
226
|
+
})),
|
|
227
|
+
...removals,
|
|
228
|
+
];
|
|
206
229
|
}
|
|
207
230
|
// A STATE IS ANSWERED BEFORE `base`, because `base` used to be tested first
|
|
208
231
|
// and swallowed it: `base:true state:"hover"` wrote the hover value straight
|
|
@@ -216,11 +239,40 @@ export function setKeys(doc, id, keys, opts) {
|
|
|
216
239
|
// class a runtime island toggles on the PINNED element, and the renderer
|
|
217
240
|
// emits no rule at all when there is nothing pinned to hang it off. See
|
|
218
241
|
// sticky.ts — this is the whole reason that module exists.
|
|
219
|
-
|
|
242
|
+
// A pure REMOVAL is exempt: unsetting a hostless stuck override is exactly
|
|
243
|
+
// the repair `sb_review` asks for, and refusing it would leave the caller
|
|
244
|
+
// holding a finding they cannot act on.
|
|
245
|
+
if (opts.state === STUCK_STATE && Object.keys(keys).length) {
|
|
220
246
|
requireStuckHost(doc.doc, id);
|
|
221
247
|
if (namespace === 'config')
|
|
222
248
|
refuseStuckConfig(keys);
|
|
223
249
|
}
|
|
250
|
+
// THE PARENT-HOVER STATE has a host precondition of its own, and a different
|
|
251
|
+
// one: not "something pins above me" but "I am a real child of a real box".
|
|
252
|
+
if (opts.state === PARENT_HOVER_STATE && Object.keys(keys).length) {
|
|
253
|
+
requireHoverHost(doc.doc, id);
|
|
254
|
+
}
|
|
255
|
+
if ((opts.state === HOVER_STATE || opts.state === PARENT_HOVER_STATE) &&
|
|
256
|
+
namespace === 'config' &&
|
|
257
|
+
Object.keys(keys).length) {
|
|
258
|
+
refuseHoverConfig(keys, opts.state);
|
|
259
|
+
}
|
|
260
|
+
// HOVER HAS THREE HOMES and only one of them is `states.hover`. An element
|
|
261
|
+
// that declares a Hover variant of its own is served by its own renderer
|
|
262
|
+
// from a flat, base-only `config.stateHover` map, and the universal compiler
|
|
263
|
+
// stands aside for it — so writing the state slot here would store keys no
|
|
264
|
+
// compiler reads. Routed rather than refused: the caller asked for a hover
|
|
265
|
+
// and there is a home that works; `hoverRoutingNote` says where it went.
|
|
266
|
+
if (opts.state === HOVER_STATE && namespace === 'style' && hoverHome(doc.node(id).data.type) === 'legacy') {
|
|
267
|
+
return [
|
|
268
|
+
...Object.entries(keys).map(([k, v]) => ({
|
|
269
|
+
op: 'set',
|
|
270
|
+
path: legacyHoverPath(id, k),
|
|
271
|
+
value: v,
|
|
272
|
+
})),
|
|
273
|
+
...(opts.unset ?? []).map((k) => ({ op: 'unset', path: legacyHoverPath(id, k) })),
|
|
274
|
+
];
|
|
275
|
+
}
|
|
224
276
|
// Base state and per-breakpoint state are DIFFERENT PLACES in the document,
|
|
225
277
|
// and the old path (`states[state][bp][ns]`) was neither of them: it buried
|
|
226
278
|
// a breakpoint inside the base-state cluster, where nothing reads it.
|
|
@@ -233,6 +285,7 @@ export function setKeys(doc, id, keys, opts) {
|
|
|
233
285
|
path: [...prefix, namespace, k],
|
|
234
286
|
value: v,
|
|
235
287
|
})),
|
|
288
|
+
...removals,
|
|
236
289
|
...(rebind ? [rebind] : []),
|
|
237
290
|
];
|
|
238
291
|
}
|
|
@@ -270,6 +323,7 @@ export function setKeys(doc, id, keys, opts) {
|
|
|
270
323
|
path: ['nodes', id, namespace, k],
|
|
271
324
|
value: v,
|
|
272
325
|
})),
|
|
326
|
+
...removals,
|
|
273
327
|
...(rebind ? [rebind] : []),
|
|
274
328
|
];
|
|
275
329
|
}
|
|
@@ -280,6 +334,7 @@ export function setKeys(doc, id, keys, opts) {
|
|
|
280
334
|
path: ['nodes', id, 'responsive', bp, namespace, k],
|
|
281
335
|
value: v,
|
|
282
336
|
})),
|
|
337
|
+
...removals,
|
|
283
338
|
...(rebind ? [rebind] : []),
|
|
284
339
|
];
|
|
285
340
|
}
|
|
@@ -492,7 +547,7 @@ export function setMany(doc, edits) {
|
|
|
492
547
|
const patches = [];
|
|
493
548
|
const touched = [];
|
|
494
549
|
for (const e of edits) {
|
|
495
|
-
patches.push(...setKeys(doc, e.id, e.keys, { namespace: e.namespace, breakpoint: e.breakpoint, base: e.base, state: e.state }));
|
|
550
|
+
patches.push(...setKeys(doc, e.id, e.keys, { namespace: e.namespace, breakpoint: e.breakpoint, base: e.base, state: e.state, unset: e.unset }));
|
|
496
551
|
touched.push({ id: e.id, keys: Object.keys(e.keys) });
|
|
497
552
|
}
|
|
498
553
|
return { patches, touched };
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { HOVER_HOMES } from '../../catalog/elements.generated.js';
|
|
2
|
+
/**
|
|
3
|
+
* HOVER, AND THE FACT THAT IT HAS THREE HOMES.
|
|
4
|
+
*
|
|
5
|
+
* The platform grew a UNIVERSAL hover state in September 2026
|
|
6
|
+
* (`schema/src/hoverState.ts`, Go mirror `render/style/hover.go`) alongside the
|
|
7
|
+
* pinned one, and the obvious reading — "hover is `states.hover` now" — is
|
|
8
|
+
* wrong for twelve element types, silently.
|
|
9
|
+
*
|
|
10
|
+
* states.hover → <scope>:hover{…} the pointer is on THIS
|
|
11
|
+
* states.parentHover → <host>:hover <scope>{…} …on the box around this
|
|
12
|
+
*
|
|
13
|
+
* But the universal compiler STANDS ASIDE for every element whose meta declares
|
|
14
|
+
* a Hover variant of its own (`generated.HoverStateOwners`), because those have
|
|
15
|
+
* their own storage and their own compiler and a second generic rule would fight
|
|
16
|
+
* the narrower one. For a BUTTON that storage is `config.stateHover` — a flat,
|
|
17
|
+
* base-only style map — and the editor routes a hover edit there deliberately:
|
|
18
|
+
* "a hover edit on a button must go on being the button's :hover rule"
|
|
19
|
+
* (editor/src/trait/values.ts). Nothing reads `states.hover` on a button.
|
|
20
|
+
*
|
|
21
|
+
* MEASURED, on one publish of one page: `state:"hover"` on a product card's
|
|
22
|
+
* dataset-block emitted `@media (hover:hover){#card:hover{…}}`; the identical
|
|
23
|
+
* write on the BUTTON inside it emitted nothing at all. Both reported success.
|
|
24
|
+
* Every hover this server had ever written onto a button was dead.
|
|
25
|
+
*
|
|
26
|
+
* So the home is a per-type question, and `HOVER_HOMES` is generated from the
|
|
27
|
+
* metas plus the Go renderers that actually read the legacy map — never
|
|
28
|
+
* hand-kept, because the platform is still moving here.
|
|
29
|
+
*/
|
|
30
|
+
/** The node's own pointer state. */
|
|
31
|
+
export const HOVER_STATE = 'hover';
|
|
32
|
+
/** "While the box around me is hovered." Universal on every element. */
|
|
33
|
+
export const PARENT_HOVER_STATE = 'parentHover';
|
|
34
|
+
/** The flat, base-only style map a Hover-variant element's own renderer reads. */
|
|
35
|
+
export const LEGACY_HOVER_KEY = 'stateHover';
|
|
36
|
+
/**
|
|
37
|
+
* The config key behind "only show while the card is hovered". Base-level,
|
|
38
|
+
* because a state can only override what the base already paints — and, like
|
|
39
|
+
* the parent state, it needs a host to hover.
|
|
40
|
+
*/
|
|
41
|
+
export const REVEAL_ON_HOVER = 'revealOnHover';
|
|
42
|
+
/**
|
|
43
|
+
* Where this element type's `hover` state has to be written.
|
|
44
|
+
*
|
|
45
|
+
* 'state' is the default and the majority: the universal compiler serves it.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* Where this element type's `hover` state has to be written.
|
|
49
|
+
*
|
|
50
|
+
* 'state' is the default and the overwhelming majority — `node.states.hover`,
|
|
51
|
+
* compiled either by the universal state or, for the twelve types that declare a
|
|
52
|
+
* Hover variant, by the element's own CSS. 'legacy' is the flat, base-only
|
|
53
|
+
* `config.stateHover` map, which is what the element's meta means when it
|
|
54
|
+
* declares a Hover variant with no `storage: 'node'`.
|
|
55
|
+
*/
|
|
56
|
+
export function hoverHome(type) {
|
|
57
|
+
return HOVER_HOMES[type]?.home ?? 'state';
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Element types that PROMISE `states.hover` and whose hover nothing compiles.
|
|
61
|
+
*
|
|
62
|
+
* Measured 2026-09-09 by rendering one node per Hover-variant type with a
|
|
63
|
+
* `states.hover` override and looking for the value in `BundleCSS`: every other
|
|
64
|
+
* type painted, `product-image-list` did not. Its meta declares
|
|
65
|
+
* `storage: 'node'`, so the value goes where the meta says — and the universal
|
|
66
|
+
* compiler stands aside for it, while nothing element-specific picks it up.
|
|
67
|
+
*
|
|
68
|
+
* A LIST, not a derivation, because there is nothing in the metas to derive it
|
|
69
|
+
* from: the fact lives in which Go renderer happens to read the slot. It is
|
|
70
|
+
* therefore a measurement with a date on it, and the note says so rather than
|
|
71
|
+
* pretending the platform still behaves this way.
|
|
72
|
+
*/
|
|
73
|
+
const HOVER_UNCOMPILED = new Set(['product-image-list']);
|
|
74
|
+
/**
|
|
75
|
+
* The box whose `:hover` a parent-hover rule keys off, or null.
|
|
76
|
+
*
|
|
77
|
+
* Mirrors `HoverHostOf`: the node's PARENT, and only when the node is a real
|
|
78
|
+
* child of it. Three cases answer null, and each is a rule that would compile to
|
|
79
|
+
* nothing:
|
|
80
|
+
*
|
|
81
|
+
* - a SATELLITE, which hangs off `config[key]` rather than `data.nodes` and
|
|
82
|
+
* renders no element the selector could name;
|
|
83
|
+
* - a node whose parent is ROOT, because the pointer is inside the page
|
|
84
|
+
* whenever it is inside the window;
|
|
85
|
+
* - an orphan whose parent id points at nothing.
|
|
86
|
+
*/
|
|
87
|
+
export function hoverHostOf(doc, id) {
|
|
88
|
+
const node = doc.nodes[id];
|
|
89
|
+
const parentId = node?.data.parent;
|
|
90
|
+
if (!node || !parentId)
|
|
91
|
+
return null;
|
|
92
|
+
const parent = doc.nodes[parentId];
|
|
93
|
+
if (!parent || !(parent.data.nodes ?? []).includes(id))
|
|
94
|
+
return null;
|
|
95
|
+
if (parent.data.type === 'root' || parentId === doc.root_node_id)
|
|
96
|
+
return null;
|
|
97
|
+
return parentId;
|
|
98
|
+
}
|
|
99
|
+
/** Is this node a satellite — referenced from its parent's config, not its children? */
|
|
100
|
+
export function isSatelliteNode(doc, id) {
|
|
101
|
+
const parentId = doc.nodes[id]?.data.parent;
|
|
102
|
+
if (!parentId)
|
|
103
|
+
return false;
|
|
104
|
+
const parent = doc.nodes[parentId];
|
|
105
|
+
return !!parent && !(parent.data.nodes ?? []).includes(id);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Refuse a parent-hover override that no selector would ever match, naming which
|
|
109
|
+
* of the three reasons applies — the caller is one structural fact away from the
|
|
110
|
+
* design they wanted, and "it does nothing" would not tell them which.
|
|
111
|
+
*/
|
|
112
|
+
export function requireHoverHost(doc, id) {
|
|
113
|
+
if (hoverHostOf(doc, id))
|
|
114
|
+
return;
|
|
115
|
+
if (isSatelliteNode(doc, id)) {
|
|
116
|
+
throw new Error(`sbuilder: "${PARENT_HOVER_STATE}" needs a box to hover, and ${id} is a SATELLITE — it ` +
|
|
117
|
+
'hangs off its owner\'s config rather than its children and renders no element of its ' +
|
|
118
|
+
'own, so there is nothing for the rule to name. Style its own hover instead ' +
|
|
119
|
+
`(state: "${HOVER_STATE}"), which its owner compiles.`);
|
|
120
|
+
}
|
|
121
|
+
const parentId = doc.nodes[id]?.data.parent;
|
|
122
|
+
if (!parentId) {
|
|
123
|
+
throw new Error(`sbuilder: ${id} has no parent, so "${PARENT_HOVER_STATE}" has nothing to key off.`);
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`sbuilder: "${PARENT_HOVER_STATE}" keys off the node's PARENT, and ${id}'s parent is ROOT — ` +
|
|
126
|
+
'the pointer is inside the page whenever it is inside the window, so the platform emits ' +
|
|
127
|
+
'no rule. Put this node inside a real box (a section\'s inner block, a card) and set it ' +
|
|
128
|
+
`there, or use state: "${HOVER_STATE}" for the node's own pointer state.`);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The one config key a hover state translates into a declaration, and only
|
|
132
|
+
* `true` — the same contract `stuckDecls` has, for the same reason: `false`
|
|
133
|
+
* would need `display: revert`, which rolls past the element's own static CSS to
|
|
134
|
+
* the UA default.
|
|
135
|
+
*/
|
|
136
|
+
export function refuseHoverConfig(keys, state) {
|
|
137
|
+
const stray = Object.keys(keys).filter((k) => k !== 'hidden');
|
|
138
|
+
if (stray.length) {
|
|
139
|
+
throw new Error(`sbuilder: the "${state}" state translates exactly one config key — "hidden" — into a ` +
|
|
140
|
+
`declaration (display:none). ${stray.map((k) => `"${k}"`).join(', ')} would be stored ` +
|
|
141
|
+
'and read by no compiler. Style is what a state paints.');
|
|
142
|
+
}
|
|
143
|
+
if ('hidden' in keys && keys.hidden !== true) {
|
|
144
|
+
throw new Error(`sbuilder: "${state}" config.hidden takes only true. false would have to mean "show it ` +
|
|
145
|
+
'again while hovered", which needs display:revert — wrong here, because revert rolls ' +
|
|
146
|
+
"past the element's own static CSS to the UA default. To stop hiding it, remove the " +
|
|
147
|
+
'override.');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Refuse a reveal that compiles to nothing.
|
|
152
|
+
*
|
|
153
|
+
* `compileHoverCss` emits the reveal pair only `if (hostScope && …)`, so the
|
|
154
|
+
* switch on a satellite or a top-level section is stored and the element simply
|
|
155
|
+
* stays visible — the caller asked for a quick-add button that appears on hover
|
|
156
|
+
* and got one that is always there, with nothing to say so.
|
|
157
|
+
*/
|
|
158
|
+
export function refuseReveal(doc, id, keys) {
|
|
159
|
+
if (keys[REVEAL_ON_HOVER] !== true)
|
|
160
|
+
return;
|
|
161
|
+
if (hoverHostOf(doc, id))
|
|
162
|
+
return;
|
|
163
|
+
throw new Error(`sbuilder: config.${REVEAL_ON_HOVER} hides this until the box AROUND it is hovered, and ` +
|
|
164
|
+
`${id} has no such box (a satellite, or a direct child of ROOT). The platform emits ` +
|
|
165
|
+
'neither half of the reveal, so the element would simply stay visible. Put it inside a ' +
|
|
166
|
+
'card or a block and set it there.');
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The note a caller gets when a hover write is routed somewhere other than
|
|
170
|
+
* `states.hover`, and the warning when nothing will read it either way.
|
|
171
|
+
*
|
|
172
|
+
* Said rather than silently done: the routing is right, but a caller who later
|
|
173
|
+
* reads the node back would otherwise find their keys somewhere they did not put
|
|
174
|
+
* them.
|
|
175
|
+
*/
|
|
176
|
+
export function hoverRoutingNote(type) {
|
|
177
|
+
if (HOVER_UNCOMPILED.has(type)) {
|
|
178
|
+
return (`"${type}" declares a Hover variant, so the platform's universal hover compiler stands ` +
|
|
179
|
+
'aside for it — and measured on 2026-09-09, no element-specific compiler picks the slot ' +
|
|
180
|
+
'up either, so this override paints nothing. Written where its meta says it belongs ' +
|
|
181
|
+
'(states.hover), which is where it will start painting when the platform closes the gap. ' +
|
|
182
|
+
'Style a wrapper around it if the hover has to be visible now.');
|
|
183
|
+
}
|
|
184
|
+
if (hoverHome(type) !== 'legacy')
|
|
185
|
+
return null;
|
|
186
|
+
return (`"${type}" stores its hover in config.${LEGACY_HOVER_KEY} — a flat map its own renderer ` +
|
|
187
|
+
'compiles into the node\'s :hover rule — and the universal hover state stands aside for it, ' +
|
|
188
|
+
'so states.hover here would be read by nobody. Written to that map instead. It is BASE-ONLY, ' +
|
|
189
|
+
'so this value applies at every width.');
|
|
190
|
+
}
|
|
191
|
+
/** The path a legacy hover write lands on — flat, base-only, one key per patch. */
|
|
192
|
+
export function legacyHoverPath(id, key) {
|
|
193
|
+
return ['nodes', id, 'config', LEGACY_HOVER_KEY, key];
|
|
194
|
+
}
|
|
@@ -214,8 +214,20 @@ export function reviewDesign(doc) {
|
|
|
214
214
|
});
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
// COUNT THE KEYS, don't ask whether the slot exists — the platform's own
|
|
218
|
+
// `HasStuckOverrides` tests `len(slot.Style) > 0`, and an EMPTY slot paints
|
|
219
|
+
// nothing whether or not there is a host. Asking the weaker question made
|
|
220
|
+
// this a false positive on the one document the fix produces: unsetting the
|
|
221
|
+
// last key leaves `{}` behind, so the repair the finding names left the
|
|
222
|
+
// finding standing.
|
|
223
|
+
const stuckSlots = [
|
|
224
|
+
styled.states?.[STUCK_STATE],
|
|
225
|
+
...Object.values(styled.responsive ?? {}).map((r) => r?.states?.[STUCK_STATE]),
|
|
226
|
+
];
|
|
227
|
+
const hasStuck = stuckSlots.some((slot) => {
|
|
228
|
+
const s = slot;
|
|
229
|
+
return !!s && (Object.keys(s.style ?? {}).length > 0 || Object.keys(s.config ?? {}).length > 0);
|
|
230
|
+
});
|
|
219
231
|
if (hasStuck && !stuckHostOf(d, id)) {
|
|
220
232
|
out.push({
|
|
221
233
|
code: 'stuck_no_host',
|
package/dist/tools/page.js
CHANGED
|
@@ -15,6 +15,7 @@ import { globalWarning, restampPatches, RESPONSIVE_NOTICE } from '../domains/sit
|
|
|
15
15
|
import { catalogMatches, traitsFor } from '../catalog/element-search.js';
|
|
16
16
|
import { applyPatches } from '../core/patch.js';
|
|
17
17
|
import { stickyWarning } from '../domains/site/sticky.js';
|
|
18
|
+
import { HOVER_STATE, hoverRoutingNote } from '../domains/site/hover.js';
|
|
18
19
|
import { siteFor } from './context.js';
|
|
19
20
|
import { projectList, PAGE_FIELDS, TEMPLATE_FIELDS } from './project.js';
|
|
20
21
|
/**
|
|
@@ -262,6 +263,11 @@ export function registerPageTools(server, ctx) {
|
|
|
262
263
|
id: z.string().optional(),
|
|
263
264
|
namespace: z.enum(['style', 'config', 'specials']).optional(),
|
|
264
265
|
keys: z.record(z.unknown()).optional(),
|
|
266
|
+
unset: z
|
|
267
|
+
.array(z.string())
|
|
268
|
+
.optional()
|
|
269
|
+
.describe('Keys to REMOVE from the same slot — the only way to undo a write. Setting null ' +
|
|
270
|
+
'is not the same: null is a stored value, so the override still counts as present.'),
|
|
265
271
|
breakpoint: z.enum(['desktop', 'laptop', 'tablet', 'mobile']).optional(),
|
|
266
272
|
base: z.boolean().optional(),
|
|
267
273
|
state: z
|
|
@@ -278,22 +284,38 @@ export function registerPageTools(server, ctx) {
|
|
|
278
284
|
breakpoint: z.enum(['desktop', 'laptop', 'tablet', 'mobile']).optional(),
|
|
279
285
|
base: z.boolean().optional(),
|
|
280
286
|
state: z.string().optional(),
|
|
287
|
+
unset: z.array(z.string()).optional(),
|
|
281
288
|
}))
|
|
282
289
|
.optional(),
|
|
283
290
|
dry_run: z.boolean().optional(),
|
|
284
291
|
},
|
|
285
292
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
286
|
-
}, async ({ id, namespace, keys, breakpoint, base, state, edits, dry_run }) => {
|
|
293
|
+
}, async ({ id, namespace, keys, breakpoint, base, state, unset, edits, dry_run }) => {
|
|
287
294
|
const d = session.current();
|
|
288
295
|
// One shape inside: a single edit is a batch of one.
|
|
289
296
|
const batch = edits ?? [];
|
|
290
297
|
if (!edits) {
|
|
291
|
-
|
|
292
|
-
|
|
298
|
+
// `keys` is optional when `unset` carries the work: a pure removal is a
|
|
299
|
+
// legitimate edit, and demanding an empty object alongside it would make
|
|
300
|
+
// the repair `sb_review` names read like a workaround.
|
|
301
|
+
if (!id || !namespace || (!keys && !unset?.length)) {
|
|
302
|
+
throw new Error('sbuilder: sb_set needs id + namespace + keys (or unset), or edits[]');
|
|
293
303
|
}
|
|
294
|
-
batch.push({ id, namespace, keys, breakpoint: breakpoint, base, state });
|
|
304
|
+
batch.push({ id, namespace, keys: keys ?? {}, breakpoint: breakpoint, base, state, unset });
|
|
295
305
|
}
|
|
296
306
|
const { patches, touched } = setMany(d, batch);
|
|
307
|
+
// WHERE A HOVER ACTUALLY WENT. Routing it silently would leave a caller
|
|
308
|
+
// who reads the node back looking for keys in a slot they never wrote to —
|
|
309
|
+
// and, for the elements with no compiler on either home, believing they
|
|
310
|
+
// had styled something.
|
|
311
|
+
const hoverNotes = {};
|
|
312
|
+
for (const e of batch) {
|
|
313
|
+
if (e.state !== HOVER_STATE || e.namespace !== 'style')
|
|
314
|
+
continue;
|
|
315
|
+
const note = hoverRoutingNote(d.doc.nodes[e.id]?.data.type ?? '');
|
|
316
|
+
if (note)
|
|
317
|
+
hoverNotes[e.id] = note;
|
|
318
|
+
}
|
|
297
319
|
// THE STICKY WARNING IS COMPUTED AGAINST THE DOCUMENT AS IT WILL BE, so
|
|
298
320
|
// the dry run and the real run say the same thing. A caller who is told
|
|
299
321
|
// only after committing has already shipped a header that does not move.
|
|
@@ -324,6 +346,7 @@ export function registerPageTools(server, ctx) {
|
|
|
324
346
|
dry_run: true,
|
|
325
347
|
patches,
|
|
326
348
|
...(Object.keys(sw).length ? { warnings: sw } : {}),
|
|
349
|
+
...(Object.keys(hoverNotes).length ? { hover: hoverNotes } : {}),
|
|
327
350
|
...(note ? { note } : {}),
|
|
328
351
|
});
|
|
329
352
|
}
|
|
@@ -337,9 +360,20 @@ export function registerPageTools(server, ctx) {
|
|
|
337
360
|
}
|
|
338
361
|
if (!edits) {
|
|
339
362
|
const warn = warnings[batch[0].id];
|
|
340
|
-
|
|
363
|
+
const hv = hoverNotes[batch[0].id];
|
|
364
|
+
return text({
|
|
365
|
+
set: touched[0].keys,
|
|
366
|
+
rev: d.rev,
|
|
367
|
+
...(warn ? { warning: warn } : {}),
|
|
368
|
+
...(hv ? { hover: hv } : {}),
|
|
369
|
+
});
|
|
341
370
|
}
|
|
342
|
-
return text({
|
|
371
|
+
return text({
|
|
372
|
+
set: touched,
|
|
373
|
+
rev: d.rev,
|
|
374
|
+
...(Object.keys(warnings).length ? { warnings } : {}),
|
|
375
|
+
...(Object.keys(hoverNotes).length ? { hover: hoverNotes } : {}),
|
|
376
|
+
});
|
|
343
377
|
});
|
|
344
378
|
server.registerTool('sb_move', {
|
|
345
379
|
description: 'Move a node to another parent at an index.',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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",
|