sbuilder-mcp 0.85.0 → 0.87.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 +24 -0
- package/CHANGELOG.vi.md +24 -0
- package/README.md +1 -1
- package/README.vi.md +1 -1
- package/dist/catalog/api.generated.js +225 -3
- package/dist/catalog/deadkeys.generated.js +2 -2
- package/dist/catalog/search.js +9 -1
- package/dist/catalog/shapes.generated.js +46 -4
- package/dist/catalog/source.generated.js +2 -2
- package/dist/domains/site/examplecopy.js +56 -0
- package/dist/domains/site/findings.js +7 -0
- package/dist/domains/site/importmap.js +48 -2
- package/dist/domains/site/offscreen.js +13 -0
- package/dist/domains/site/readiness-fetch.js +4 -0
- package/dist/domains/site/readiness.js +17 -0
- package/dist/domains/site/review.js +119 -1
- package/dist/tools/api.js +1 -1
- package/dist/tools/page.js +36 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,30 @@ 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.87.0] - 2026-10-12
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `sb_review` reports `example_copy` for text, labels and alt text that still carry a built-in layout's example copy, such as "Tiêu đề chính", invented figures, promises and customer quotes.
|
|
13
|
+
- `sb_review` reports `invisible_text` for text whose colour matches its background.
|
|
14
|
+
- `sb_review` reports `demo_variant_options` for a variant picker that shows fallback options instead of the product's own.
|
|
15
|
+
- `sb_review` reports `empty_page` for a page that has only a header and footer.
|
|
16
|
+
- `sb_review` flags product categories without an image as a `categoryImages` readiness gap when the page shows category tiles.
|
|
17
|
+
- `sb_template_use` reports how many elements still carry the layout's example copy.
|
|
18
|
+
- `sb_page_create` reports how many findings a seeded layout skeleton already has, so the placeholder copy and empty slots get filled.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- `sb_api_find` and `sb_api_call` resolve an operation id regardless of letter case, so `{siteID}` and `{siteId}` reach the same route when the match is unique.
|
|
22
|
+
- `sb_import` reads page tokens only from bands on a light ground, so an inverted band such as a call-to-action no longer supplies the page's button fill.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- `sb_add` on the page root without `index` places the new section before the footer instead of after it, which the band-order check refused to save.
|
|
26
|
+
- `sb_review` no longer reports `off_canvas` findings for a header's closed mobile menu.
|
|
27
|
+
|
|
28
|
+
## [0.86.0] - 2026-10-12
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- `sb_api_find` and `sb_api_call` reach 849 platform operations, including the GrabFood delivery app: connecting a site, listing the orders it pushes, and accepting or rejecting them.
|
|
32
|
+
|
|
9
33
|
## [0.85.0] - 2026-10-11
|
|
10
34
|
|
|
11
35
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,30 @@ 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.87.0] - 2026-10-12
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `sb_review` báo `example_copy` cho chữ, nhãn và alt còn mang nội dung mẫu của layout dựng sẵn, như "Tiêu đề chính", số liệu, lời hứa và lời khen khách hàng bịa.
|
|
13
|
+
- `sb_review` báo `invisible_text` cho chữ có màu trùng với nền.
|
|
14
|
+
- `sb_review` báo `demo_variant_options` cho bộ chọn biến thể hiện tuỳ chọn giả thay vì tuỳ chọn của chính sản phẩm.
|
|
15
|
+
- `sb_review` báo `empty_page` cho trang chỉ có header và footer.
|
|
16
|
+
- `sb_review` đánh dấu danh mục sản phẩm chưa có ảnh là thiếu sót `categoryImages` khi trang hiển thị ô danh mục.
|
|
17
|
+
- `sb_template_use` cho biết còn bao nhiêu phần tử mang nội dung mẫu của layout.
|
|
18
|
+
- `sb_page_create` cho biết khung layout vừa tạo đã có bao nhiêu phát hiện, để điền chữ giữ chỗ và các ô trống.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- `sb_api_find` và `sb_api_call` nhận id operation không phân biệt hoa thường, nên `{siteID}` và `{siteId}` cùng tới một route khi khớp là duy nhất.
|
|
22
|
+
- `sb_import` chỉ đọc token của trang từ các dải có nền sáng, nên dải đảo màu như call-to-action không còn quyết định màu nền nút của trang.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- `sb_add` vào gốc trang khi không có `index` đặt section mới trước footer thay vì sau footer, vị trí mà kiểm tra thứ tự dải từng từ chối lưu.
|
|
26
|
+
- `sb_review` không còn báo `off_canvas` cho menu di động đang đóng trong header.
|
|
27
|
+
|
|
28
|
+
## [0.86.0] - 2026-10-12
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- `sb_api_find` và `sb_api_call` truy cập được 849 operation của platform, gồm app giao hàng GrabFood: kết nối site, liệt kê đơn hàng được đẩy về, và nhận hoặc từ chối đơn.
|
|
32
|
+
|
|
9
33
|
## [0.85.0] - 2026-10-11
|
|
10
34
|
|
|
11
35
|
### Added
|
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ in this client: one would put the very key the platform exists to hold back into
|
|
|
118
118
|
| `sb_store` | Run a store flow that must happen in a fixed order — `checkout` (the four writes that make a working one), `checkout_sync` (the store's current payment and delivery methods into every existing order form, checkout page republished), `form` (any of the platform's 17 templates with its own field document), `chrome` (one shared header or footer on a real site menu — desktop menu, mobile drawer, cart and account icons), `menu` (a menu node bound to the site's menu, its links resolved), `overlay_attach` (a pop-up or quick view on the open page), `cart` (the site's cart drawer, when it has none; `relocalize` puts an existing one in the site's language) and `app` (a built-in app plus the pages it needs), `global_attach` / `global_detach` (put an EXISTING shared section on the open page, or take it off), `global_promote` (make a section on the page a NEW shared section) and `template_save` (save a section as a reusable template) |
|
|
119
119
|
| `sb_undo` | Undo / redo this session's writes — page and form edits and `sb_api_call` PUTs; a page is refused if someone else saved it since. The platform's versions and history (`sb_page_version`) outlive this process |
|
|
120
120
|
|
|
121
|
-
Twenty-eight tools, **
|
|
121
|
+
Twenty-eight tools, **849 API operations** (312 of the 393 writes carrying a body shape read
|
|
122
122
|
off the handler), 123 elements, 94 binding sources. `sb_api_find`
|
|
123
123
|
is an index rather than a tool per endpoint, so the tool list stays short while everything
|
|
124
124
|
the platform can do stays reachable — and operations added to the platform arrive with the
|
package/README.vi.md
CHANGED
|
@@ -115,7 +115,7 @@ nhét ngược lại vào mọi bản cài.
|
|
|
115
115
|
| `sb_store` | Chạy một luồng cửa hàng bắt buộc đúng thứ tự — `checkout` (bốn lệnh ghi tạo nên trang thanh toán), `checkout_sync` (đưa phương thức thanh toán và giao hàng hiện tại vào mọi form đặt hàng đã có, publish lại trang thanh toán), `form` (một trong 17 template của nền tảng kèm field document của nó), `chrome` (một header hoặc footer dùng chung trên menu thật của site — menu desktop, drawer mobile, icon giỏ hàng và tài khoản), `menu` (một node menu bind vào menu của site, link đã phân giải), `overlay_attach` (một pop-up hay quick view trên trang đang mở), `cart` (ngăn giỏ hàng của site, khi chưa có; `relocalize` đưa ngăn giỏ đã có về ngôn ngữ của site), `app` (một app dựng sẵn kèm những trang nó cần), và `global_attach` / `global_detach` (đặt một section dùng chung ĐÃ CÓ lên trang đang mở, hoặc gỡ ra), `global_promote` (biến một section trên trang thành section dùng chung MỚI) và `template_save` (lưu một section làm mẫu dùng lại) |
|
|
116
116
|
| `sb_undo` | Hoàn tác / làm lại các thao tác ghi trong phiên — chỉnh sửa trang, form và PUT qua `sb_api_call`; trang bị từ chối nếu người khác đã lưu sau đó. Version và history của nền tảng (`sb_page_version`) sống lâu hơn process này |
|
|
117
117
|
|
|
118
|
-
Hai mươi tám tool, **
|
|
118
|
+
Hai mươi tám tool, **849 operation API** (312 trong 393 lệnh ghi có hình dạng body đọc thẳng
|
|
119
119
|
từ handler), 123 element, 94 nguồn binding. `sb_api_find` là
|
|
120
120
|
một chỉ mục chứ không phải mỗi endpoint một tool, nên danh sách tool vẫn ngắn trong khi mọi
|
|
121
121
|
thứ nền tảng làm được vẫn với tới — và operation mới thêm bên nền tảng sẽ tự có sau lần
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const SWAGGER_SOURCE = {
|
|
2
|
-
"operations":
|
|
3
|
-
"definitions":
|
|
4
|
-
"bodyCarrying":
|
|
2
|
+
"operations": 849,
|
|
3
|
+
"definitions": 280,
|
|
4
|
+
"bodyCarrying": 376,
|
|
5
5
|
"bodyUndescribed": 102,
|
|
6
6
|
"generatedFrom": "server/docs/swagger.json"
|
|
7
7
|
};
|
|
@@ -781,6 +781,34 @@ export const API_OPERATIONS = [
|
|
|
781
781
|
"bodyRef": null,
|
|
782
782
|
"credential": "siteScoped"
|
|
783
783
|
},
|
|
784
|
+
{
|
|
785
|
+
"id": "post:/_wb/delivery/{app}/{path}",
|
|
786
|
+
"method": "POST",
|
|
787
|
+
"path": "/_wb/delivery/{app}/{path}",
|
|
788
|
+
"tags": [
|
|
789
|
+
"pos"
|
|
790
|
+
],
|
|
791
|
+
"summary": "Delivery-app partner endpoints (GrabFood)",
|
|
792
|
+
"params": [
|
|
793
|
+
{
|
|
794
|
+
"name": "app",
|
|
795
|
+
"in": "path",
|
|
796
|
+
"required": true,
|
|
797
|
+
"type": "string",
|
|
798
|
+
"description": "App id (grabfood)"
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
"name": "path",
|
|
802
|
+
"in": "path",
|
|
803
|
+
"required": true,
|
|
804
|
+
"type": "string",
|
|
805
|
+
"description": "oauth/token | orders | order-state | menu-sync-state"
|
|
806
|
+
}
|
|
807
|
+
],
|
|
808
|
+
"bodyDescribed": false,
|
|
809
|
+
"bodyRef": null,
|
|
810
|
+
"credential": "siteScoped"
|
|
811
|
+
},
|
|
784
812
|
{
|
|
785
813
|
"id": "get:/_wb/feed/facets",
|
|
786
814
|
"method": "GET",
|
|
@@ -7550,6 +7578,181 @@ export const API_OPERATIONS = [
|
|
|
7550
7578
|
"bodyRef": null,
|
|
7551
7579
|
"credential": "siteScoped"
|
|
7552
7580
|
},
|
|
7581
|
+
{
|
|
7582
|
+
"id": "get:/api/sites/{siteId}/delivery-apps/orders",
|
|
7583
|
+
"method": "GET",
|
|
7584
|
+
"path": "/api/sites/{siteId}/delivery-apps/orders",
|
|
7585
|
+
"tags": [
|
|
7586
|
+
"pos"
|
|
7587
|
+
],
|
|
7588
|
+
"summary": "List orders pushed by delivery apps",
|
|
7589
|
+
"params": [
|
|
7590
|
+
{
|
|
7591
|
+
"name": "siteId",
|
|
7592
|
+
"in": "path",
|
|
7593
|
+
"required": true,
|
|
7594
|
+
"type": "string",
|
|
7595
|
+
"description": "Site ID"
|
|
7596
|
+
},
|
|
7597
|
+
{
|
|
7598
|
+
"name": "state",
|
|
7599
|
+
"in": "query",
|
|
7600
|
+
"required": false,
|
|
7601
|
+
"type": "string",
|
|
7602
|
+
"description": "new|deciding|accepted|rejected|cancelled|completed"
|
|
7603
|
+
},
|
|
7604
|
+
{
|
|
7605
|
+
"name": "limit",
|
|
7606
|
+
"in": "query",
|
|
7607
|
+
"required": false,
|
|
7608
|
+
"type": "integer",
|
|
7609
|
+
"description": "page size"
|
|
7610
|
+
},
|
|
7611
|
+
{
|
|
7612
|
+
"name": "offset",
|
|
7613
|
+
"in": "query",
|
|
7614
|
+
"required": false,
|
|
7615
|
+
"type": "integer",
|
|
7616
|
+
"description": "offset"
|
|
7617
|
+
}
|
|
7618
|
+
],
|
|
7619
|
+
"bodyDescribed": false,
|
|
7620
|
+
"bodyRef": null,
|
|
7621
|
+
"credential": "siteScoped"
|
|
7622
|
+
},
|
|
7623
|
+
{
|
|
7624
|
+
"id": "post:/api/sites/{siteId}/delivery-apps/orders/{id}/accept",
|
|
7625
|
+
"method": "POST",
|
|
7626
|
+
"path": "/api/sites/{siteId}/delivery-apps/orders/{id}/accept",
|
|
7627
|
+
"tags": [
|
|
7628
|
+
"pos"
|
|
7629
|
+
],
|
|
7630
|
+
"summary": "Accept a delivery-app order",
|
|
7631
|
+
"params": [
|
|
7632
|
+
{
|
|
7633
|
+
"name": "siteId",
|
|
7634
|
+
"in": "path",
|
|
7635
|
+
"required": true,
|
|
7636
|
+
"type": "string",
|
|
7637
|
+
"description": "Site ID"
|
|
7638
|
+
},
|
|
7639
|
+
{
|
|
7640
|
+
"name": "id",
|
|
7641
|
+
"in": "path",
|
|
7642
|
+
"required": true,
|
|
7643
|
+
"type": "string",
|
|
7644
|
+
"description": "Order ID"
|
|
7645
|
+
}
|
|
7646
|
+
],
|
|
7647
|
+
"bodyDescribed": false,
|
|
7648
|
+
"bodyRef": null,
|
|
7649
|
+
"credential": "siteScoped"
|
|
7650
|
+
},
|
|
7651
|
+
{
|
|
7652
|
+
"id": "post:/api/sites/{siteId}/delivery-apps/orders/{id}/reject",
|
|
7653
|
+
"method": "POST",
|
|
7654
|
+
"path": "/api/sites/{siteId}/delivery-apps/orders/{id}/reject",
|
|
7655
|
+
"tags": [
|
|
7656
|
+
"pos"
|
|
7657
|
+
],
|
|
7658
|
+
"summary": "Reject a delivery-app order",
|
|
7659
|
+
"params": [
|
|
7660
|
+
{
|
|
7661
|
+
"name": "siteId",
|
|
7662
|
+
"in": "path",
|
|
7663
|
+
"required": true,
|
|
7664
|
+
"type": "string",
|
|
7665
|
+
"description": "Site ID"
|
|
7666
|
+
},
|
|
7667
|
+
{
|
|
7668
|
+
"name": "id",
|
|
7669
|
+
"in": "path",
|
|
7670
|
+
"required": true,
|
|
7671
|
+
"type": "string",
|
|
7672
|
+
"description": "Order ID"
|
|
7673
|
+
},
|
|
7674
|
+
{
|
|
7675
|
+
"name": "body",
|
|
7676
|
+
"in": "body",
|
|
7677
|
+
"required": true,
|
|
7678
|
+
"type": "object",
|
|
7679
|
+
"description": "{reason}"
|
|
7680
|
+
}
|
|
7681
|
+
],
|
|
7682
|
+
"bodyDescribed": true,
|
|
7683
|
+
"bodyRef": "internal_deliveryapps_rest.rejectBody",
|
|
7684
|
+
"credential": "siteScoped"
|
|
7685
|
+
},
|
|
7686
|
+
{
|
|
7687
|
+
"id": "get:/api/sites/{siteId}/delivery-apps/{app}/connection",
|
|
7688
|
+
"method": "GET",
|
|
7689
|
+
"path": "/api/sites/{siteId}/delivery-apps/{app}/connection",
|
|
7690
|
+
"tags": [
|
|
7691
|
+
"pos"
|
|
7692
|
+
],
|
|
7693
|
+
"summary": "Read or replace the site's connection to a delivery app",
|
|
7694
|
+
"params": [
|
|
7695
|
+
{
|
|
7696
|
+
"name": "siteId",
|
|
7697
|
+
"in": "path",
|
|
7698
|
+
"required": true,
|
|
7699
|
+
"type": "string",
|
|
7700
|
+
"description": "Site ID"
|
|
7701
|
+
},
|
|
7702
|
+
{
|
|
7703
|
+
"name": "app",
|
|
7704
|
+
"in": "path",
|
|
7705
|
+
"required": true,
|
|
7706
|
+
"type": "string",
|
|
7707
|
+
"description": "App id (grabfood, shopeefood)"
|
|
7708
|
+
},
|
|
7709
|
+
{
|
|
7710
|
+
"name": "connection",
|
|
7711
|
+
"in": "body",
|
|
7712
|
+
"required": false,
|
|
7713
|
+
"type": "object",
|
|
7714
|
+
"description": "Connection (PUT only)"
|
|
7715
|
+
}
|
|
7716
|
+
],
|
|
7717
|
+
"bodyDescribed": true,
|
|
7718
|
+
"bodyRef": "internal_deliveryapps_rest.connectionInput",
|
|
7719
|
+
"credential": "siteScoped"
|
|
7720
|
+
},
|
|
7721
|
+
{
|
|
7722
|
+
"id": "put:/api/sites/{siteId}/delivery-apps/{app}/connection",
|
|
7723
|
+
"method": "PUT",
|
|
7724
|
+
"path": "/api/sites/{siteId}/delivery-apps/{app}/connection",
|
|
7725
|
+
"tags": [
|
|
7726
|
+
"pos"
|
|
7727
|
+
],
|
|
7728
|
+
"summary": "Read or replace the site's connection to a delivery app",
|
|
7729
|
+
"params": [
|
|
7730
|
+
{
|
|
7731
|
+
"name": "siteId",
|
|
7732
|
+
"in": "path",
|
|
7733
|
+
"required": true,
|
|
7734
|
+
"type": "string",
|
|
7735
|
+
"description": "Site ID"
|
|
7736
|
+
},
|
|
7737
|
+
{
|
|
7738
|
+
"name": "app",
|
|
7739
|
+
"in": "path",
|
|
7740
|
+
"required": true,
|
|
7741
|
+
"type": "string",
|
|
7742
|
+
"description": "App id (grabfood, shopeefood)"
|
|
7743
|
+
},
|
|
7744
|
+
{
|
|
7745
|
+
"name": "connection",
|
|
7746
|
+
"in": "body",
|
|
7747
|
+
"required": false,
|
|
7748
|
+
"type": "object",
|
|
7749
|
+
"description": "Connection (PUT only)"
|
|
7750
|
+
}
|
|
7751
|
+
],
|
|
7752
|
+
"bodyDescribed": true,
|
|
7753
|
+
"bodyRef": "internal_deliveryapps_rest.connectionInput",
|
|
7754
|
+
"credential": "siteScoped"
|
|
7755
|
+
},
|
|
7553
7756
|
{
|
|
7554
7757
|
"id": "get:/api/sites/{siteId}/discounts",
|
|
7555
7758
|
"method": "GET",
|
|
@@ -32486,6 +32689,25 @@ export const API_DEFINITIONS = {
|
|
|
32486
32689
|
}
|
|
32487
32690
|
}
|
|
32488
32691
|
},
|
|
32692
|
+
"internal_deliveryapps_rest.connectionInput": {
|
|
32693
|
+
"type": "object",
|
|
32694
|
+
"properties": {
|
|
32695
|
+
"enabled": {
|
|
32696
|
+
"type": "boolean"
|
|
32697
|
+
},
|
|
32698
|
+
"externalMerchantId": {
|
|
32699
|
+
"type": "string"
|
|
32700
|
+
}
|
|
32701
|
+
}
|
|
32702
|
+
},
|
|
32703
|
+
"internal_deliveryapps_rest.rejectBody": {
|
|
32704
|
+
"type": "object",
|
|
32705
|
+
"properties": {
|
|
32706
|
+
"reason": {
|
|
32707
|
+
"type": "string"
|
|
32708
|
+
}
|
|
32709
|
+
}
|
|
32710
|
+
},
|
|
32489
32711
|
"internal_discounts_rest.grantRequest": {
|
|
32490
32712
|
"type": "object",
|
|
32491
32713
|
"properties": {
|
package/dist/catalog/search.js
CHANGED
|
@@ -28,7 +28,15 @@ const PRODUCT_WRITE_IDS = new Set([
|
|
|
28
28
|
'put:/api/v1/products/{id}',
|
|
29
29
|
]);
|
|
30
30
|
export function findOperation(id) {
|
|
31
|
-
|
|
31
|
+
const exact = API_OPERATIONS.find((o) => o.id === id);
|
|
32
|
+
if (exact)
|
|
33
|
+
return exact;
|
|
34
|
+
// The platform spells one parameter two ways — `{siteId}` almost everywhere,
|
|
35
|
+
// `{siteID}` on the menu routes — and a caller (or a readiness fix text) that
|
|
36
|
+
// learned the common one was told "unknown operation" for one letter. Only a
|
|
37
|
+
// UNIQUE fold is taken, so two routes differing by case alone stay refused.
|
|
38
|
+
const folded = API_OPERATIONS.filter((o) => o.id.toLowerCase() === id.toLowerCase());
|
|
39
|
+
return folded.length === 1 ? folded[0] : undefined;
|
|
32
40
|
}
|
|
33
41
|
export function searchOperations(query, opts = {}) {
|
|
34
42
|
const limit = opts.limit ?? DEFAULT_FIND_LIMIT;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export const SHAPE_SOURCE = {
|
|
2
|
-
"writeOperations":
|
|
3
|
-
"shaped":
|
|
4
|
-
"fromHandlers":
|
|
2
|
+
"writeOperations": 393,
|
|
3
|
+
"shaped": 312,
|
|
4
|
+
"fromHandlers": 302,
|
|
5
5
|
"fromSwaggerOnly": 10,
|
|
6
6
|
"withReadOnly": 30,
|
|
7
|
-
"structsRead":
|
|
7
|
+
"structsRead": 3226
|
|
8
8
|
};
|
|
9
9
|
export const REQUEST_SHAPES = {
|
|
10
10
|
"post:/api/sites/{id}/ai-credits/purchase": {
|
|
@@ -3188,6 +3188,48 @@ export const REQUEST_SHAPES = {
|
|
|
3188
3188
|
"updatedAt"
|
|
3189
3189
|
]
|
|
3190
3190
|
},
|
|
3191
|
+
"put:/api/sites/{siteId}/delivery-apps/{app}/connection": {
|
|
3192
|
+
"fields": [
|
|
3193
|
+
{
|
|
3194
|
+
"name": "enabled",
|
|
3195
|
+
"type": "boolean"
|
|
3196
|
+
},
|
|
3197
|
+
{
|
|
3198
|
+
"name": "externalMerchantId",
|
|
3199
|
+
"type": "string"
|
|
3200
|
+
}
|
|
3201
|
+
],
|
|
3202
|
+
"source": "go",
|
|
3203
|
+
"goType": "connectionInput"
|
|
3204
|
+
},
|
|
3205
|
+
"post:/api/sites/{siteId}/delivery-apps/orders/{id}/reject": {
|
|
3206
|
+
"fields": [
|
|
3207
|
+
{
|
|
3208
|
+
"name": "reason",
|
|
3209
|
+
"type": "string"
|
|
3210
|
+
}
|
|
3211
|
+
],
|
|
3212
|
+
"source": "go",
|
|
3213
|
+
"goType": "rejectBody"
|
|
3214
|
+
},
|
|
3215
|
+
"post:/_wb/delivery/{app}/{path}": {
|
|
3216
|
+
"fields": [
|
|
3217
|
+
{
|
|
3218
|
+
"name": "client_id",
|
|
3219
|
+
"type": "string"
|
|
3220
|
+
},
|
|
3221
|
+
{
|
|
3222
|
+
"name": "client_secret",
|
|
3223
|
+
"type": "string"
|
|
3224
|
+
},
|
|
3225
|
+
{
|
|
3226
|
+
"name": "grant_type",
|
|
3227
|
+
"type": "string"
|
|
3228
|
+
}
|
|
3229
|
+
],
|
|
3230
|
+
"source": "go",
|
|
3231
|
+
"goType": "(inline)"
|
|
3232
|
+
},
|
|
3191
3233
|
"post:/api/sites/{siteId}/discounts": {
|
|
3192
3234
|
"fields": [
|
|
3193
3235
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GENERATED by scripts/gen-catalog.ts — do not edit by hand.
|
|
2
2
|
// The platform commit every other generated file here describes.
|
|
3
3
|
export const PLATFORM_SOURCE = {
|
|
4
|
-
"commit": "
|
|
5
|
-
"committedAt": "2026-10-
|
|
4
|
+
"commit": "df9accca9c8a0e402ca5520beddcb52c0843f7f5",
|
|
5
|
+
"committedAt": "2026-10-12T09:14:30+07:00",
|
|
6
6
|
"dirty": false
|
|
7
7
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { LAYOUT_PATTERNS, THEME_TOKENS } from './patterns.js';
|
|
2
|
+
/**
|
|
3
|
+
* THE BUILT-IN LAYOUTS' EXAMPLE COPY, so a review can tell it was never replaced.
|
|
4
|
+
*
|
|
5
|
+
* Measured on a store built end to end with these tools: the home page published
|
|
6
|
+
* "Tiêu đề chính", "Lợi ích một / Một hoặc hai câu." and, in the hero's picture
|
|
7
|
+
* slot, the sentence telling the AGENT to run sb_media_upload — all to shoppers —
|
|
8
|
+
* and sb_review called the page clean, because none of it is an element's own
|
|
9
|
+
* seed. Worse are the CLAIMS: "1.000+ khách hàng", "4,9/5", "Đổi mới trong 7 ngày",
|
|
10
|
+
* a testimonial signed "Nguyễn Minh Anh" — published, they are statements about
|
|
11
|
+
* this shop that nobody made.
|
|
12
|
+
*
|
|
13
|
+
* DERIVED by building every pattern, minus the short list of labels that are fine
|
|
14
|
+
* as they stand. Derived rather than listed so a pattern added or reworded later is
|
|
15
|
+
* covered on the day it lands: new copy is example copy until someone says it is a
|
|
16
|
+
* usable label — the safe direction to be wrong in.
|
|
17
|
+
*/
|
|
18
|
+
const USABLE_AS_IS = new Set([
|
|
19
|
+
'Mua ngay', 'Tìm hiểu thêm', 'Tìm hiểu thêm →', 'Xem tất cả', 'Sản phẩm nổi bật', 'Danh mục sản phẩm',
|
|
20
|
+
'Bộ sưu tập mới', 'Chào mừng', 'Lợi ích', 'Vì sao chọn chúng tôi', 'Sẵn sàng bắt đầu?', 'Thư viện ảnh',
|
|
21
|
+
'Hỗ trợ', 'Câu hỏi thường gặp', 'Giao hàng mất bao lâu?', 'Đổi trả thế nào?', 'Thanh toán ra sao?',
|
|
22
|
+
'Đối tác của chúng tôi', 'Cam kết của chúng tôi', 'Giao hàng nhanh', 'Bảo hành chính hãng',
|
|
23
|
+
'Đổi trả dễ dàng', 'Thanh toán an toàn', 'Khách hàng nói gì', '★★★★★', 'Cách hoạt động',
|
|
24
|
+
'Ba bước đơn giản', '01', '02', '03', 'Chọn sản phẩm', 'Đặt hàng', 'Nhận hàng', 'Câu chuyện',
|
|
25
|
+
'Bảng giá', 'Chọn gói phù hợp', 'Cơ bản', 'Chọn gói', 'Khách hàng', 'Đánh giá trung bình',
|
|
26
|
+
'Giao trong nội thành', 'Tiêu chuẩn', 'Cao cấp',
|
|
27
|
+
]);
|
|
28
|
+
const TEXT_KEYS = ['text', 'label', 'alt'];
|
|
29
|
+
let cache = null;
|
|
30
|
+
/** Example copy → the id of the layout it came from. Built once per process. */
|
|
31
|
+
export function exampleCopy() {
|
|
32
|
+
if (cache)
|
|
33
|
+
return cache;
|
|
34
|
+
const out = new Map();
|
|
35
|
+
const walk = (n, id) => {
|
|
36
|
+
if (!n)
|
|
37
|
+
return;
|
|
38
|
+
for (const k of TEXT_KEYS) {
|
|
39
|
+
const v = n.specials?.[k];
|
|
40
|
+
if (typeof v === 'string' && v.trim() && !USABLE_AS_IS.has(v) && !out.has(v))
|
|
41
|
+
out.set(v, id);
|
|
42
|
+
}
|
|
43
|
+
for (const c of n.children ?? [])
|
|
44
|
+
walk(c, id);
|
|
45
|
+
};
|
|
46
|
+
for (const p of LAYOUT_PATTERNS) {
|
|
47
|
+
try {
|
|
48
|
+
walk(p.build(THEME_TOKENS, []), p.id);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// A pattern that cannot build without a pool contributes nothing here.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
cache = out;
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
@@ -22,6 +22,13 @@ export const FIX = {
|
|
|
22
22
|
// this surface EXISTS. An empty state is authored out of sight — it only shows
|
|
23
23
|
// when the list is empty — so an agent that never saw it does not know there is
|
|
24
24
|
// anything to write.
|
|
25
|
+
demo_variant_options: 'Clear the fallback: sb_set id "<id>", namespace config, keys { "attributes": [] }, base: true. ' +
|
|
26
|
+
'Products WITH options still show their own (the binding wins); products without show no picker.',
|
|
27
|
+
invisible_text: 'Give it a colour that reads on its ground: sb_set id "<id>", namespace style, keys { "color": … } ' +
|
|
28
|
+
'(or unset "color" so the theme decides). For a button, change its fill or its text — not both to the same.',
|
|
29
|
+
example_copy: 'Write this shop\'s own words: sb_set id "<id>", namespace specials, keys { "<key>": … }. ' +
|
|
30
|
+
'A figure, a delivery or return promise, or a customer quote must be TRUE for this shop — ' +
|
|
31
|
+
'if you do not know it, remove the element (sb_remove) rather than guess.',
|
|
25
32
|
default_seed_copy: 'Write it in the shop\'s own language and ink: sb_set id "<id>", namespace specials, ' +
|
|
26
33
|
'keys { "<key>": … }. It is a seeded satellite (an empty state, an element\'s chrome), ' +
|
|
27
34
|
'so it only appears when the list is empty or the control is idle — sb_outline lists it ' +
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pageChildren
|
|
1
|
+
import { pageChildren } from '../../core/tree.js';
|
|
2
2
|
import { isGlobal } from './traps.js';
|
|
3
3
|
import { stickySeeds } from './sticky.js';
|
|
4
4
|
import { TEXT_STYLE_KEYS } from '../../catalog/elements.generated.js';
|
|
@@ -79,9 +79,29 @@ export function tokensFromPage(doc) {
|
|
|
79
79
|
else if (!block && t === 'flex-block')
|
|
80
80
|
block = n;
|
|
81
81
|
};
|
|
82
|
+
// NOT FROM A BAND ON A DARK OR COLOURED GROUND. Its type and buttons are
|
|
83
|
+
// inverted on purpose, so they say nothing about the page's own. Measured: a
|
|
84
|
+
// hero re-added below an sb_cta_band (primary-green ground, white button with
|
|
85
|
+
// green text) took that white button as the page's fill — and published a
|
|
86
|
+
// white "Mua ngay" on the white hero, plus a white eyebrow, both invisible.
|
|
87
|
+
// By id, not through walk(): its visitor gets the node, and a node is not
|
|
88
|
+
// guaranteed to carry its own id.
|
|
89
|
+
const read = (id, onDark, seen) => {
|
|
90
|
+
const n = doc.nodes[id];
|
|
91
|
+
if (!n || seen.has(id))
|
|
92
|
+
return;
|
|
93
|
+
seen.add(id);
|
|
94
|
+
const t = n.data.type;
|
|
95
|
+
const dark = onDark || ((t === 'flex-section' || t === 'flex-block') && !lightGround(styleOf(n).backgroundColor));
|
|
96
|
+
if (!dark)
|
|
97
|
+
visit(n);
|
|
98
|
+
for (const k of n.data.nodes ?? [])
|
|
99
|
+
read(k, dark, seen);
|
|
100
|
+
};
|
|
101
|
+
const seen = new Set();
|
|
82
102
|
for (const id of pageChildren(doc))
|
|
83
103
|
if (!isGlobal(doc, id))
|
|
84
|
-
|
|
104
|
+
read(id, false, seen);
|
|
85
105
|
const h = styleOf(heading);
|
|
86
106
|
const p = styleOf(text);
|
|
87
107
|
const b = styleOf(button);
|
|
@@ -99,6 +119,32 @@ export function tokensFromPage(doc) {
|
|
|
99
119
|
...(str(bl.maxWidth) ? { sectionMaxWidth: str(bl.maxWidth) } : {}),
|
|
100
120
|
};
|
|
101
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Is this a ground ordinary type reads on? Unset, transparent, white, the
|
|
124
|
+
* theme's own background token, or a literal colour light enough. Anything
|
|
125
|
+
* else — a dark or saturated literal, another theme var — is treated as an
|
|
126
|
+
* inverted band, which only ever costs a token read, never a wrong one.
|
|
127
|
+
*/
|
|
128
|
+
function lightGround(bg) {
|
|
129
|
+
const v = typeof bg === 'string' ? bg.trim().toLowerCase() : '';
|
|
130
|
+
if (!v || v === 'transparent' || v === 'white' || v === 'var(--wb-color-background)')
|
|
131
|
+
return true;
|
|
132
|
+
let rgb = null;
|
|
133
|
+
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/.exec(v);
|
|
134
|
+
if (hex) {
|
|
135
|
+
const h = hex[1].length === 3 ? hex[1].replace(/./g, (c) => c + c) : hex[1];
|
|
136
|
+
rgb = [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const m = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/.exec(v);
|
|
140
|
+
if (m)
|
|
141
|
+
rgb = [m[1], m[2], m[3]].map(Number);
|
|
142
|
+
}
|
|
143
|
+
if (!rgb)
|
|
144
|
+
return false;
|
|
145
|
+
const [r, g, b] = rgb.map((c) => c / 255);
|
|
146
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.8;
|
|
147
|
+
}
|
|
102
148
|
/** A heading's tag, clamped to what HTML has. */
|
|
103
149
|
function headingTag(level) {
|
|
104
150
|
const n = Math.min(6, Math.max(1, Math.round(level ?? 2)));
|
|
@@ -80,8 +80,21 @@ export function offscreenNodes(doc) {
|
|
|
80
80
|
for (const n of subtreeIds(doc, id))
|
|
81
81
|
out.add(n);
|
|
82
82
|
}
|
|
83
|
+
// THE MOBILE MENU LIVES INSIDE THE HEADER, legitimately, so the ROOT-only
|
|
84
|
+
// sweep above never reached it. Measured: a store header from sb_store
|
|
85
|
+
// action:"chrome" — its closed menu-drawer, parked at x −420 — gave four
|
|
86
|
+
// off_canvas findings at 390px on every page. Only the menu types: a popup
|
|
87
|
+
// nested in a section is still a document the platform refuses.
|
|
88
|
+
for (const [id, n] of Object.entries(doc.nodes)) {
|
|
89
|
+
if (!NESTED_OFFSCREEN.has(n?.data?.type ?? '') || out.has(id))
|
|
90
|
+
continue;
|
|
91
|
+
for (const k of subtreeIds(doc, id))
|
|
92
|
+
out.add(k);
|
|
93
|
+
}
|
|
83
94
|
return out;
|
|
84
95
|
}
|
|
96
|
+
/** Overlay types that legitimately sit deep in a tree (a header's mobile menu). */
|
|
97
|
+
const NESTED_OFFSCREEN = new Set(['hamburger-menu', 'menu-drawer', 'menu-panel']);
|
|
85
98
|
/**
|
|
86
99
|
* The overlay this node sits in, for the RENDER question "what must be opened
|
|
87
100
|
* before I can photograph this".
|
|
@@ -89,6 +89,9 @@ export async function gatherReadiness(ctx, siteId, pageNodes, openPageId) {
|
|
|
89
89
|
const blogCats = Array.isArray(rawBlogCats) ? rawBlogCats : null;
|
|
90
90
|
const cats = categoryList?.categories ?? categoryList?.productCategories;
|
|
91
91
|
const categories = Array.isArray(cats) ? (categoryList?.total ?? cats.length) : null;
|
|
92
|
+
const categoriesUnpictured = Array.isArray(cats)
|
|
93
|
+
? cats.filter((c) => !String(c?.image ?? '').trim()).length
|
|
94
|
+
: null;
|
|
92
95
|
const links = pageLinks?.links ?? pageLinks?.pageLinks;
|
|
93
96
|
const categoryPageLinks = Array.isArray(links)
|
|
94
97
|
? links.filter((l) => l?.linkType === 'productCategory').length
|
|
@@ -159,6 +162,7 @@ export async function gatherReadiness(ctx, siteId, pageNodes, openPageId) {
|
|
|
159
162
|
: null,
|
|
160
163
|
overlayKinds: Array.isArray(overlayList?.overlays) ? overlayList.overlays.map((o) => o.kind ?? '') : null,
|
|
161
164
|
categories,
|
|
165
|
+
categoriesUnpictured,
|
|
162
166
|
categoryPageLinks,
|
|
163
167
|
forms: formList?.forms ?? null,
|
|
164
168
|
articles: Array.isArray(articleList?.articles)
|
|
@@ -612,6 +612,23 @@ export function readinessGaps(input) {
|
|
|
612
612
|
'/api/media/{siteId}/from-url ingests straight from a url in one hop.',
|
|
613
613
|
});
|
|
614
614
|
}
|
|
615
|
+
// A COLLECTION TILE WITH NO PICTURE IS A BROKEN-IMAGE ICON, not a grey card:
|
|
616
|
+
// render/nodes/collection-media writes `<img src="">` (the image element has a
|
|
617
|
+
// placeholder; this one does not). Measured on the sb_category_strip a store
|
|
618
|
+
// built here put on its home page — two broken icons above the category names.
|
|
619
|
+
// Asked only where a collection tile is actually on screen.
|
|
620
|
+
if ((input.categoriesUnpictured ?? 0) > 0 &&
|
|
621
|
+
[...input.pageNodes, ...(input.globalNodes ?? [])].some((n) => n.data?.type === 'collection-media')) {
|
|
622
|
+
const n = input.categoriesUnpictured;
|
|
623
|
+
gaps.push({
|
|
624
|
+
id: 'categoryImages',
|
|
625
|
+
draft: false,
|
|
626
|
+
problem: `${n} product categor${n === 1 ? 'y has' : 'ies have'} no image, and this page shows category ` +
|
|
627
|
+
'tiles — each renders a broken-image icon above its name.',
|
|
628
|
+
fix: 'Upload a picture (sb_media_upload), then PUT the category back with image: url ' +
|
|
629
|
+
'(sb_api_call put:/api/sites/{siteId}/product-categories/{id}, merge:true).',
|
|
630
|
+
});
|
|
631
|
+
}
|
|
615
632
|
if (input.shippingMethods === 0) {
|
|
616
633
|
gaps.push({
|
|
617
634
|
id: 'shipping',
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
1
2
|
import { childrenOf, childrenWithSatellites, isOverlay, isSharedSection, pageChildren, appBlockRoot, SPEC_GLOBAL_REF, SPEC_APP_BLOCK_REF } from '../../core/tree.js';
|
|
2
3
|
import { STUCK_STATE, stickyBlockedBy, stuckHostOf, isPinnedNode } from './sticky.js';
|
|
3
|
-
import { bandOf } from './traps.js';
|
|
4
|
+
import { bandOf, isGlobal } from './traps.js';
|
|
5
|
+
import { exampleCopy } from './examplecopy.js';
|
|
4
6
|
import { HOVER_STATE, hoverHome } from './hover.js';
|
|
5
7
|
import { ELEMENTS, BINDING_SOURCES, BOUND_SPECIALS, FIRST_CHILD_ONLY, SATELLITE_RULES, ELEMENT_SEEDS } from '../../catalog/elements.generated.js';
|
|
6
8
|
import { fill } from './findings.js';
|
|
@@ -130,6 +132,33 @@ const LINK_SPECIALS = new Set(['boundHref', 'boundHrefLabel', 'boundProductURL']
|
|
|
130
132
|
function drawsItsOwnContent(type) {
|
|
131
133
|
return (BOUND_SPECIALS[type] ?? []).some((k) => !LINK_SPECIALS.has(k));
|
|
132
134
|
}
|
|
135
|
+
/** A literal #rgb / #rrggbb / rgb() colour as 0–255 channels, or null. */
|
|
136
|
+
function rgbOf(v) {
|
|
137
|
+
const c = typeof v === 'string' ? v.trim().toLowerCase() : '';
|
|
138
|
+
if (c === 'white')
|
|
139
|
+
return [255, 255, 255];
|
|
140
|
+
if (c === 'black')
|
|
141
|
+
return [0, 0, 0];
|
|
142
|
+
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/.exec(c);
|
|
143
|
+
if (hex) {
|
|
144
|
+
const h = hex[1].length === 3 ? hex[1].replace(/./g, (x) => x + x) : hex[1];
|
|
145
|
+
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
|
|
146
|
+
}
|
|
147
|
+
const m = /^rgb\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)\s*\)$/.exec(c);
|
|
148
|
+
return m ? [m[1], m[2], m[3]].map(Number) : null;
|
|
149
|
+
}
|
|
150
|
+
/** WCAG contrast ratio of two sRGB colours. */
|
|
151
|
+
function contrast(a, b) {
|
|
152
|
+
const lum = (rgb) => {
|
|
153
|
+
const [r, g, bl] = rgb.map((v) => {
|
|
154
|
+
const s = v / 255;
|
|
155
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
156
|
+
});
|
|
157
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * bl;
|
|
158
|
+
};
|
|
159
|
+
const [hi, lo] = [lum(a), lum(b)].sort((x, y) => y - x);
|
|
160
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
161
|
+
}
|
|
133
162
|
/** Binding-source prefixes that name a RECORD (not `site.*`, which every page has). */
|
|
134
163
|
// cartLine: only a cart-order's line row supplies it (render/nodes/cart-order,
|
|
135
164
|
// scope.PlaceholderCartLine) — see ROW_SUPPLIER. searchRow is left out: no
|
|
@@ -248,6 +277,27 @@ opts = {}) {
|
|
|
248
277
|
});
|
|
249
278
|
return out;
|
|
250
279
|
}
|
|
280
|
+
// ONLY THE SHARED HEADER AND FOOTER is a blank page too. Measured: an "account"
|
|
281
|
+
// page created by sb_page_create (no seed for that type) wore the site chrome
|
|
282
|
+
// and nothing else, reviewed clean, and so did the readiness gap it was made
|
|
283
|
+
// to close — a shopper landing on /account met a header, a footer and nothing.
|
|
284
|
+
// Composed (globalId, what a read carries) or stored (globalRef) alike.
|
|
285
|
+
// HEADER AND FOOTER only: a `custom` shared section is body content.
|
|
286
|
+
const isChrome = (id) => {
|
|
287
|
+
const sp = d.nodes[id]?.specials ?? {};
|
|
288
|
+
if (isGlobal(d, id))
|
|
289
|
+
return bandOf(d, id) !== 'middle';
|
|
290
|
+
return sp[SPEC_GLOBAL_REF] !== undefined && (sp.globalKind === 'header' || sp.globalKind === 'footer');
|
|
291
|
+
};
|
|
292
|
+
if (pageChildren(d).every(isChrome)) {
|
|
293
|
+
out.push({
|
|
294
|
+
code: 'empty_page',
|
|
295
|
+
nodeId: d.root_node_id,
|
|
296
|
+
type: 'root',
|
|
297
|
+
problem: 'Only the shared header and footer — the page between them is blank.',
|
|
298
|
+
fix: fill('empty_page', {}),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
251
301
|
// Document order, depth-first from ROOT: the order a reader meets them.
|
|
252
302
|
const seen = new Set();
|
|
253
303
|
const walkOrder = [];
|
|
@@ -383,6 +433,54 @@ opts = {}) {
|
|
|
383
433
|
});
|
|
384
434
|
continue;
|
|
385
435
|
}
|
|
436
|
+
// TYPE THE SAME COLOUR AS WHAT IT SITS ON. Measured: a pattern dressed in
|
|
437
|
+
// tokens read off an inverted band published a white "Mua ngay" on its own
|
|
438
|
+
// white fill — present in the HTML, invisible on the page, clean in review.
|
|
439
|
+
// LITERAL COLOURS ONLY, on both sides, and the ground is the element's own
|
|
440
|
+
// fill or the nearest ancestor that sets one: a theme var or an unset page
|
|
441
|
+
// ground is not known here, and a dark theme would make a guessed "white"
|
|
442
|
+
// ground cry wolf on every light heading.
|
|
443
|
+
if ((type === 'text' || type === 'heading' || type === 'button') && !inOverlay.has(id)) {
|
|
444
|
+
const fg = rgbOf(n.style?.color);
|
|
445
|
+
let ground = null;
|
|
446
|
+
for (let x = id; x && d.nodes[x] && !ground; x = d.nodes[x].data.parent) {
|
|
447
|
+
const bg = d.nodes[x].style?.backgroundColor;
|
|
448
|
+
if (bg === undefined || bg === 'transparent' || bg === '')
|
|
449
|
+
continue;
|
|
450
|
+
ground = rgbOf(bg);
|
|
451
|
+
if (!ground)
|
|
452
|
+
break; // a var() or a gradient: unknown, say nothing
|
|
453
|
+
}
|
|
454
|
+
if (fg && ground && contrast(fg, ground) < 1.5) {
|
|
455
|
+
out.push({
|
|
456
|
+
code: 'invisible_text',
|
|
457
|
+
nodeId: id,
|
|
458
|
+
type,
|
|
459
|
+
problem: `Its colour is the colour it sits on (contrast ${contrast(fg, ground).toFixed(2)}:1) — the words are there and nobody can read them.`,
|
|
460
|
+
fix: fill('invisible_text', { id }),
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// THE DEMO OPTIONS. product-variants seeds config.attributes Color
|
|
465
|
+
// Red/Green/Blue, Size S/M/L, and that authored config is the documented
|
|
466
|
+
// FALLBACK for a product with no option axes of its own (schema binding.ts:
|
|
467
|
+
// "the picker keeps its authored config"). So every simple product on a
|
|
468
|
+
// published page offered three colours and three sizes that do not exist —
|
|
469
|
+
// measured on a store built here, and reviewed clean.
|
|
470
|
+
if (type === 'product-variants') {
|
|
471
|
+
const authored = n.config?.attributes;
|
|
472
|
+
const seeded = ELEMENTS[type]?.defaults?.config?.attributes;
|
|
473
|
+
if (Array.isArray(seeded) && seeded.length && isDeepStrictEqual(authored, seeded)) {
|
|
474
|
+
out.push({
|
|
475
|
+
code: 'demo_variant_options',
|
|
476
|
+
nodeId: id,
|
|
477
|
+
type,
|
|
478
|
+
problem: 'Still the picker\'s demo options (Color Red/Green/Blue, Size S/M/L). They render for ' +
|
|
479
|
+
'every product that has no options of its own — a shopper is offered choices that do not exist.',
|
|
480
|
+
fix: fill('demo_variant_options', { id }),
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
386
484
|
// A NAVIGATION CONTROL WITH NOWHERE TO GO, and it is the sharpest silent
|
|
387
485
|
// failure on this list because the page is CORRECT in every way a check can
|
|
388
486
|
// otherwise see. The tree holds the event the author asked for; the pixels
|
|
@@ -669,11 +767,18 @@ opts = {}) {
|
|
|
669
767
|
// is "add something inside it", which on the next save is decomposed away
|
|
670
768
|
// again. Measured on a page built with `sb_add`: the header and footer refs
|
|
671
769
|
// both flagged, and both correct.
|
|
770
|
+
// OR IT DRAWS ITS OWN CONTROL. `search-input` is a container only for the
|
|
771
|
+
// dropdown an author MAY compose under it; with no children it still renders
|
|
772
|
+
// its `<input>` (render/nodes/search-input/html.go). The platform's own
|
|
773
|
+
// search seed ships it childless, so every seeded search page was flagged.
|
|
774
|
+
// Not `select`: its face lives in its trigger/panel children.
|
|
775
|
+
const drawsOwnControl = type === 'search-input';
|
|
672
776
|
const isReference = (n.specials ?? {})[SPEC_GLOBAL_REF] !== undefined ||
|
|
673
777
|
(n.specials ?? {})[SPEC_APP_BLOCK_REF] !== undefined;
|
|
674
778
|
if (meta.isContainer &&
|
|
675
779
|
childrenOf(d, id).length === 0 &&
|
|
676
780
|
!isReference &&
|
|
781
|
+
!drawsOwnControl &&
|
|
677
782
|
!(bindings.length > 0 && drawsItsOwnContent(type))) {
|
|
678
783
|
out.push({
|
|
679
784
|
code: 'empty_container',
|
|
@@ -762,6 +867,19 @@ opts = {}) {
|
|
|
762
867
|
fix: fill('default_seed_copy', { id, key }),
|
|
763
868
|
});
|
|
764
869
|
}
|
|
870
|
+
// A BUILT-IN LAYOUT'S EXAMPLE COPY — see examplecopy.ts.
|
|
871
|
+
else if (typeof value === 'string' && exampleCopy().has(value)) {
|
|
872
|
+
out.push({
|
|
873
|
+
code: 'example_copy',
|
|
874
|
+
nodeId: id,
|
|
875
|
+
type,
|
|
876
|
+
problem: `Still the "${exampleCopy().get(value)}" layout's example copy (${JSON.stringify(value)}). ` +
|
|
877
|
+
'A shopper reads it as this shop speaking — and a number, a promise or a ' +
|
|
878
|
+
'testimonial in it is a claim nobody made.',
|
|
879
|
+
key,
|
|
880
|
+
fix: fill('example_copy', { id, key }),
|
|
881
|
+
});
|
|
882
|
+
}
|
|
765
883
|
}
|
|
766
884
|
// A FORM NOBODY LINKED publishes as an empty box, and the platform stays
|
|
767
885
|
// deliberately quiet about it: form.go:221 skips an empty formId with the
|
package/dist/tools/api.js
CHANGED
|
@@ -455,7 +455,7 @@ export function resolveOperation(args) {
|
|
|
455
455
|
'method+path for a route the catalog does not carry.');
|
|
456
456
|
}
|
|
457
457
|
if (args.id) {
|
|
458
|
-
const op =
|
|
458
|
+
const op = findOperation(args.id);
|
|
459
459
|
if (!op)
|
|
460
460
|
throw new Error(`sbuilder: unknown operation "${args.id}" — use sb_api_find first`);
|
|
461
461
|
return op;
|
package/dist/tools/page.js
CHANGED
|
@@ -24,6 +24,7 @@ import { siteToken } from './credentialpick.js';
|
|
|
24
24
|
import { validateForSave } from '../domains/site/validate.js';
|
|
25
25
|
import { formDocChecks, sayChecks, specCheck, writeCheck } from '../domains/site/writecheck.js';
|
|
26
26
|
import { reviewDesign, REVIEW_NOTICE } from '../domains/site/review.js';
|
|
27
|
+
import { exampleCopy } from '../domains/site/examplecopy.js';
|
|
27
28
|
import { compactFindings } from '../domains/site/findings.js';
|
|
28
29
|
import { readinessGaps, READINESS_NOTICE, siteLanguage } from '../domains/site/readiness.js';
|
|
29
30
|
import { gatherReadiness } from '../domains/site/readiness-fetch.js';
|
|
@@ -1427,7 +1428,16 @@ export function registerPageTools(server, ctx) {
|
|
|
1427
1428
|
}, async ({ parent_id, spec, index, dry_run, force }) => {
|
|
1428
1429
|
const d = session.current();
|
|
1429
1430
|
const guard = { force, forced: [] };
|
|
1430
|
-
|
|
1431
|
+
// ROOT WITH NO INDEX MEANS "ANOTHER SECTION OF THE PAGE": the end of the
|
|
1432
|
+
// middle band, as sb_template_use and sb_duplicate already place one.
|
|
1433
|
+
// Appended last it landed after a shared footer, and the band-order check
|
|
1434
|
+
// refused the save — on exactly the call empty_page's fix names.
|
|
1435
|
+
// Only when a footer is there to land behind: a FORM document's root has
|
|
1436
|
+
// none, and addSubtree's own default (above the submit button) is right.
|
|
1437
|
+
const rootKids = d.doc.nodes[d.doc.root_node_id]?.data.nodes.length ?? 0;
|
|
1438
|
+
const mid = parent_id === d.doc.root_node_id ? middleEnd(d.doc) : rootKids;
|
|
1439
|
+
const at = index ?? (mid < rootKids ? mid : undefined);
|
|
1440
|
+
const { patches, ids } = addSubtree(d, parent_id, spec, at, guard);
|
|
1431
1441
|
const forced = guard.forced.length ? { forced: guard.forced } : {};
|
|
1432
1442
|
// ELEMENTS THAT RENDER CONVINCINGLY WHILE WIRED TO NOTHING. The add
|
|
1433
1443
|
// succeeds completely, the tree is correct and the page photographs
|
|
@@ -2045,9 +2055,22 @@ export function registerPageTools(server, ctx) {
|
|
|
2045
2055
|
});
|
|
2046
2056
|
}
|
|
2047
2057
|
await session.applyAndSave(patches);
|
|
2058
|
+
// Say it at the moment it is placed: the example copy publishes as if the
|
|
2059
|
+
// shop wrote it, and sb_review is the only other place it surfaces.
|
|
2060
|
+
const examples = ids.filter((id) => {
|
|
2061
|
+
const sp = doc.doc.nodes[id]?.specials ?? {};
|
|
2062
|
+
return ['text', 'label', 'alt'].some((k) => typeof sp[k] === 'string' && exampleCopy().has(sp[k]));
|
|
2063
|
+
}).length;
|
|
2048
2064
|
return text({
|
|
2049
2065
|
added: pattern.name,
|
|
2050
2066
|
section: ids[0],
|
|
2067
|
+
...(examples
|
|
2068
|
+
? {
|
|
2069
|
+
copy: `${examples} element(s) still carry the layout's example copy — write this shop's own ` +
|
|
2070
|
+
'words over them (sb_review lists each as example_copy); remove any number, promise ' +
|
|
2071
|
+
'or quote you cannot vouch for.',
|
|
2072
|
+
}
|
|
2073
|
+
: {}),
|
|
2051
2074
|
nodes: ids.length,
|
|
2052
2075
|
into: page_id,
|
|
2053
2076
|
rev: doc.rev,
|
|
@@ -2429,10 +2452,21 @@ export function registerPageTools(server, ctx) {
|
|
|
2429
2452
|
// entity URL once published — say what to add, whatever the reason
|
|
2430
2453
|
// (no platform seed, seed:false, a refused seed write).
|
|
2431
2454
|
const blankNote = !seeded || 'failed' in seeded ? emptyEntityTemplateNote(type) : null;
|
|
2455
|
+
// A LAYOUT is the editor's skeleton, and it ships its elements' own
|
|
2456
|
+
// placeholders ("Heading", "Enter your text here", an image with no src).
|
|
2457
|
+
// Measured: an "about" page created here reviewed with four findings before
|
|
2458
|
+
// anything else was done, and the create answer had said nothing about it.
|
|
2459
|
+
const skeletonFindings = seeded && 'layout' in seeded && layout
|
|
2460
|
+
? reviewDesign(PageDoc.from(layoutDocument(layout)), { pageType: type ?? 'page' }).length
|
|
2461
|
+
: 0;
|
|
2462
|
+
const skeletonNote = skeletonFindings
|
|
2463
|
+
? `This is the "${String(seeded?.layout)}" skeleton and it already has ${skeletonFindings} ` +
|
|
2464
|
+
'finding(s) — placeholder copy, empty slots. Open it and fill them; sb_review lists each.'
|
|
2465
|
+
: null;
|
|
2432
2466
|
return text({
|
|
2433
2467
|
...res,
|
|
2434
2468
|
...(seeded ? { seeded } : {}),
|
|
2435
|
-
...(blankNote ? { next: blankNote } : {}),
|
|
2469
|
+
...(blankNote ? { next: blankNote } : skeletonNote ? { next: skeletonNote } : {}),
|
|
2436
2470
|
...(wearing ? { chrome: wearing } : {}),
|
|
2437
2471
|
...(renamed
|
|
2438
2472
|
? {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.87.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",
|